diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 7385174..317f41a 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -31,7 +31,7 @@ jobs: sudo apt-get update sudo apt-get install -y python3-tk python -m pip install --upgrade pip - pip install pylint + pip install pylint pillow python -m pip install mypy - name: Analysing the code with pylint diff --git a/Assets/Icons/battery.png b/Assets/Icons/battery.png new file mode 100644 index 0000000..7cc8101 Binary files /dev/null and b/Assets/Icons/battery.png differ diff --git a/arduino_logique.py b/arduino_logique.py index ba425c9..1932b75 100644 --- a/arduino_logique.py +++ b/arduino_logique.py @@ -22,7 +22,7 @@ def main(): # Creating main window win = tk.Tk() win.title("Laboratoire virtuel de circuit logique - GIF-1002") - win.geometry("1400x800") # Initial window size + win.geometry("1700x800") # Initial window size win.resizable(False, False) # Disabling window resizing win.configure(bg="#333333") # Setting consistent background color diff --git a/breadboard.py b/breadboard.py index 13de819..e3395d9 100644 --- a/breadboard.py +++ b/breadboard.py @@ -14,6 +14,7 @@ HORIZONTAL, PERSO, VERTICAL, + USED, ) @@ -156,7 +157,7 @@ def draw_matrix_points(self, scale=1): # used to debug the matrix radius = 2 * scale # Adjust size as needed self.canvas.create_oval(x - radius, y - radius, x + radius, y + radius, fill=color, outline="") - def draw_blank_board_model(self, x_origin: int = 50, y_origin: int = 10): + def draw_blank_board_model(self, x_origin: int = 50, y_origin: int = 10, battery_pos_wire_end=None, battery_neg_wire_end=None): """ Draws a blank breadboard model on the canvas. """ @@ -223,3 +224,24 @@ def draw_blank_board_model(self, x_origin: int = 50, y_origin: int = 10): (self.sketcher.go_xy, 1, {"line": 0, "column": 0, "id_origin": "circTest"}), ] self.sketcher.circuit(x_origin, y_origin, scale=self.sketcher.scale_factor, model=blank_board_model) + + battery_x = x_origin + 1200 # Adjust as needed for proper positioning + battery_y = y_origin + 300 # Adjust as needed for proper positioning + + # Reset all matrix elements' states to FREE + for key in self.sketcher.matrix: + self.sketcher.matrix[key]['state'] = FREE + + self.sketcher.draw_battery( + battery_x, + battery_y, + pos_wire_end=battery_pos_wire_end, + neg_wire_end=battery_neg_wire_end, + ) + if battery_pos_wire_end: + allowed_positions = self.sketcher.get_power_line_last_pins() + nearest_point, nearest_point_coord = self.sketcher.find_nearest_allowed_grid_point(battery_pos_wire_end[0], battery_pos_wire_end[1], allowed_positions) + col, line = nearest_point_coord + self.sketcher.matrix[f'{col},{line}']['state'] = USED + + \ No newline at end of file diff --git a/component_sketch.py b/component_sketch.py index 89aea81..e85dffc 100644 --- a/component_sketch.py +++ b/component_sketch.py @@ -9,6 +9,9 @@ from tkinter import font import math from typing import Any, Callable +from PIL import Image, ImageTk +import os + from dataCDLT import ( @@ -66,6 +69,8 @@ def __init__(self, canvas) -> None: self.current_dict_circuit: dict[str, Any] = {} self.matrix: dict[str, Any] = {} self.id_origins = {"xyOrigin": (0, 0)} + self.battery_wire_drag_data: dict[str, Any] = {} + def circuit(self, x_distance=0, y_distance=0, scale=1, width=-1, direction=VERTICAL, **kwargs): """ @@ -2660,6 +2665,8 @@ def draw_pin_io(self, x_distance, y_distance, scale=1, width=-1, direction=HORIZ matrix[f"{coord[0][0]},{coord[0][1]}"]["state"] = USED return x_distance, y_distance + + def clear_board(self): """Clear the board of all drawn components.""" @@ -2672,3 +2679,345 @@ def clear_board(self): self.id_type[key] = 0 self.current_dict_circuit.clear() # TODO Khalid update the Circuit instance + + + + + def draw_battery(self, x_distance, y_distance, scale=1, width=-1, direction='HORIZONTAL', pos_wire_end=None, neg_wire_end=None, **kwargs): + """ + Draws a battery image at the given coordinates with two hanging wires on the left side. + + Parameters: + - x_distance (int): The x-coordinate where the battery will be drawn. + - y_distance (int): The y-coordinate where the battery will be drawn. + - scale (float): Scaling factor for the battery size. + - width (int): Specific width if needed, otherwise calculated from scale. + - direction (str): Orientation of the battery, currently only 'HORIZONTAL' is handled. + - pos_wire_end (tuple): Coordinates where the positive wire should end. + - neg_wire_end (tuple): Coordinates where the negative wire should end. + - kwargs: Additional keyword arguments. + + Returns: + - Tuple of (x_distance, y_distance) + """ + battery_id = '_battery' + + # Check if battery already exists + if battery_id in self.current_dict_circuit: + print("Battery already exists in the circuit.") + return x_distance, y_distance + + image_path = os.path.join('Assets', 'Icons', 'battery.png') + + if not os.path.isfile(image_path): + print(f"Battery image not found at {image_path}.") + return x_distance, y_distance + + try: + battery_image = Image.open(image_path) + + original_width, original_height = battery_image.size + new_width = int(original_width * scale * 0.7) + new_height = int(original_height * scale * 0.7) + battery_image = battery_image.resize((new_width, new_height), Image.LANCZOS) + + battery_photo = ImageTk.PhotoImage(battery_image) + + except Exception as e: + print(f"Error loading battery image: {e}") + return x_distance, y_distance + + battery_obj = self.canvas.create_image( + x_distance - 10, + y_distance, + anchor='nw', + image=battery_photo, + tags=() + ) + + if not hasattr(self, 'image_references'): + self.image_references = [] + self.image_references.append(battery_photo) + neg_wire_offset_x = 0 # Left edge + neg_wire_offset_y = new_height * 0.2 # 20% from the top + + pos_wire_offset_x = 0 # Left edge + pos_wire_offset_y = new_height * 0.8 + + neg_wire_start_x = x_distance + neg_wire_offset_x + neg_wire_start_y = y_distance + neg_wire_offset_y + + pos_wire_start_x = x_distance + pos_wire_offset_x + pos_wire_start_y = y_distance + pos_wire_offset_y + + self.current_dict_circuit[battery_id] = { + 'id': battery_id, + 'tags': [battery_id] + } + + neg_wire_id = '_battery_neg_wire' + if neg_wire_end: + neg_wire_end_x, neg_wire_end_y = neg_wire_end + else: + neg_wire_end_x = neg_wire_start_x - 100 * scale # Wires go to the left + neg_wire_end_y = neg_wire_start_y + + self.draw_battery_wire( + wire_id=neg_wire_id, + start_x=neg_wire_start_x, + start_y=neg_wire_start_y, + end_x=neg_wire_end_x + 3, + end_y=neg_wire_end_y + 3, + color=(0, 0, 0), + terminal_type='neg' + ) + + pos_wire_id = '_battery_pos_wire' + if pos_wire_end: + pos_wire_end_x, pos_wire_end_y = pos_wire_end + else: + pos_wire_end_x = pos_wire_start_x - 100 * scale + pos_wire_end_y = pos_wire_start_y + + self.draw_battery_wire( + wire_id=pos_wire_id, + start_x=pos_wire_start_x, + start_y=pos_wire_start_y, + end_x=pos_wire_end_x + 3, + end_y=pos_wire_end_y + 3, + color=(255, 0, 0), + terminal_type='pos' + ) + + self.canvas.tag_raise(battery_id) + + self.current_dict_circuit[battery_id]['tags'].extend( + self.current_dict_circuit[pos_wire_id]['tags'] + self.current_dict_circuit[neg_wire_id]['tags'] + ) + + return x_distance, y_distance + + def draw_battery_wire(self, wire_id, start_x, start_y, end_x, end_y, color, terminal_type): + """ + Draws a battery wire with appearance similar to draw_wire, but with separate event handling. + """ + + thickness = 1 * self.scale_factor + encre = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}" + contour = f"#{max(color[0] - 100, 0):02x}{max(color[1] - 100, 0):02x}{max(color[2] - 100, 0):02x}" + + wire_body_tag = f"{wire_id}_body" + wire_body_shadow_tag = f"{wire_id}_body_shadow" + endpoint_tag = f"{wire_id}_endpoint" + + # shadow line + self.canvas.create_line( + start_x, + start_y, + end_x, + end_y, + fill=contour, + width=8 * thickness, + tags=(wire_id, wire_body_shadow_tag) + ) + # main line + self.canvas.create_line( + start_x, + start_y, + end_x, + end_y, + fill=encre, + width=4 * thickness, + tags=(wire_id, wire_body_tag) + ) + + radius = 2 * self.scale_factor + endpoint = self.canvas.create_oval( + end_x - radius, + end_y - radius, + end_x + radius, + end_y + radius, + fill='green' if terminal_type == 'pos' else 'black', + outline='', + tags=(endpoint_tag,) + ) + + self.current_dict_circuit[wire_id] = { + 'id': wire_id, + 'tags': [wire_id, wire_body_tag, wire_body_shadow_tag, endpoint_tag], + 'start': (start_x, start_y), + 'end': (end_x, end_y), + 'color': color, + 'terminal_type': terminal_type, + 'endpoint_tag': endpoint_tag, + } + + self.canvas.tag_bind( + endpoint_tag, + "", + lambda event, wire_id=wire_id: self.on_battery_wire_endpoint_click(event, wire_id) + ) + self.canvas.tag_bind( + endpoint_tag, + "", + lambda event, wire_id=wire_id: self.on_battery_wire_endpoint_drag(event, wire_id) + ) + self.canvas.tag_bind( + endpoint_tag, + "", + lambda event, wire_id=wire_id: self.on_battery_wire_endpoint_release(event, wire_id) + ) + + def create_battery_wire_endpoint(self, x, y, wire_id, terminal_type): + """ + Creates an interactive endpoint for a battery wire. + """ + endpoint_tag = f"{wire_id}_endpoint" + + radius = 5 + endpoint = self.canvas.create_oval( + x - radius, + y - radius, + x + radius, + y + radius, + fill='green' if terminal_type == 'pos' else 'black', + outline='', + tags=(endpoint_tag,) + ) + + self.canvas.tag_bind( + endpoint_tag, + "", + lambda event, wire_id=wire_id: self.on_battery_wire_endpoint_click(event, wire_id) + ) + self.canvas.tag_bind( + endpoint_tag, + "", + lambda event, wire_id=wire_id: self.on_battery_wire_endpoint_drag(event, wire_id) + ) + self.canvas.tag_bind( + endpoint_tag, + "", + lambda event, wire_id=wire_id: self.on_battery_wire_endpoint_release(event, wire_id) + ) + + self.current_dict_circuit[wire_id]['endpoint_tag'] = endpoint_tag + + def on_battery_wire_endpoint_click(self, event, wire_id): + """ + Handler for when a battery wire endpoint is clicked. + """ + allowed_positions = self.get_power_line_last_pins() + x, y = event.x, event.y + _, nearest_point_coord = self.find_nearest_grid_point(x, y) + old_col, old_line = nearest_point_coord + self.matrix[f'{old_col},{old_line}']['state'] = FREE + self.battery_wire_drag_data = { + 'wire_id': wire_id, + } + + def on_battery_wire_endpoint_drag(self, event, wire_id): + """ + Handler for dragging a battery wire endpoint. + Updates the wire's end position as it's being dragged. + """ + if self.battery_wire_drag_data['wire_id'] != wire_id: + return + + wire_data = self.current_dict_circuit[wire_id] + start_x, start_y = wire_data['start'] + + wire_body_tag = f"{wire_id}_body" + wire_body_shadow_tag = f"{wire_id}_body_shadow" + + self.canvas.coords(wire_body_shadow_tag, start_x, start_y, event.x, event.y) + self.canvas.coords(wire_body_tag, start_x, start_y, event.x, event.y) + + endpoint_tag = wire_data['endpoint_tag'] + radius = 5 * self.scale_factor + self.canvas.coords( + endpoint_tag, + event.x - radius, + event.y - radius, + event.x + radius, + event.y + radius + ) + + wire_data['end'] = (event.x, event.y) + + def on_battery_wire_endpoint_release(self, event, wire_id): + """ + Handler for when a battery wire endpoint is released. + """ + allowed_positions = self.get_power_line_last_pins() + x, y = event.x, event.y + nearest_point, nearest_point_coord = self.find_nearest_allowed_grid_point(x, y, allowed_positions) + + if nearest_point_coord is None: + print("No free hole available.") + return + + wire_data = self.current_dict_circuit[wire_id] + start_x, start_y = wire_data['start'] + new_end_x, new_end_y = nearest_point + + wire_body_tag = f"{wire_id}_body" + wire_body_shadow_tag = f"{wire_id}_body_shadow" + + self.canvas.coords(wire_body_shadow_tag, start_x, start_y, new_end_x + 3, new_end_y + 3) + self.canvas.coords(wire_body_tag, start_x, start_y, new_end_x + 3, new_end_y + 3) + + endpoint_tag = wire_data['endpoint_tag'] + radius = 2 * self.scale_factor + self.canvas.coords( + endpoint_tag, + new_end_x - radius + 3, + new_end_y - radius + 3, + new_end_x + radius + 3, + new_end_y + radius + 3 + ) + + wire_data['end'] = (new_end_x, new_end_y) + + col, line = nearest_point_coord + self.matrix[f'{col},{line}']['state'] = USED + + self.battery_wire_drag_data = {} + + def get_power_line_last_pins(self): + """ + Returns a list of allowed positions (x, y, col, line) for the battery wires. + """ + allowed_positions = [] + last_col = 61 + power_lines = [1, 2, 13, 14, 15, 16, 27, 28] + + for line in power_lines: + col = last_col + key = f"{col},{line}" + if key in self.matrix: + x, y = self.matrix[key]['xy'] + x += self.id_origins["xyOrigin"][0] + y += self.id_origins["xyOrigin"][1] + allowed_positions.append((x, y, col, line)) + return allowed_positions + + def find_nearest_allowed_grid_point(self, x, y, allowed_positions): + """ + Find the nearest grid point among the allowed positions to the given x, y coordinates. + Skips positions that are already USED. + """ + min_distance = float('inf') + nearest_point = (x, y) + nearest_point_coord = None + for grid_x, grid_y, col, line in allowed_positions: + # Check if the hole is FREE + hole_state = self.matrix.get(f'{col},{line}', {}).get('state', None) + if hole_state != FREE: + continue + distance = math.hypot(x - grid_x, y - grid_y) + if distance < min_distance: + min_distance = distance + nearest_point = (grid_x, grid_y) + nearest_point_coord = (col, line) + return nearest_point, nearest_point_coord \ No newline at end of file diff --git a/menus.py b/menus.py index 9495a7a..dd65c18 100644 --- a/menus.py +++ b/menus.py @@ -14,7 +14,7 @@ from breadboard import Breadboard -from dataCDLT import INPUT, OUTPUT +from dataCDLT import INPUT, OUTPUT, USED MICROCONTROLLER_PINS = { "Arduino Mega": { @@ -109,7 +109,6 @@ def __init__( self.selected_microcontroller = None """The selected microcontroller.""" - # Create the menu bar frame (do not pack here) self.menu_bar = tk.Frame(parent, bg="#333333") """The frame containing the menu bar buttons.""" @@ -127,7 +126,6 @@ def __init__( "Aide": ["Documentation", "À propos"], } - # Mapping menu labels to their handler functions menu_commands = { "Nouveau": self.new_file, "Ouvrir": self.open_file, @@ -140,7 +138,6 @@ def __init__( "À propos": self.about, } - # Create each menu button and its dropdown for menu_name, options in menus.items(): self.create_menu(menu_name, options, menu_commands) @@ -381,6 +378,8 @@ def new_file(self): # Clear the canvas and reset the circuit self.board.sketcher.clear_board() self.board.fill_matrix_1260_pts() + self.board.draw_blank_board_model() + print("New file created.") messagebox.showinfo("New File", "A new circuit has been created.") @@ -393,17 +392,32 @@ def open_file(self): with open(file_path, "r", encoding="utf-8") as file: circuit_data = json.load(file) print(f"Circuit loaded from {file_path}") - # Update current_dict_circuit and redraw the circuit self.board.sketcher.clear_board() x_o, y_o = self.board.sketcher.id_origins["xyOrigin"] self.board.sketcher.circuit(x_o, y_o, model=[]) + battery_pos_wire_end = None + battery_neg_wire_end = None + + for key, val in circuit_data.items(): + if key == "_battery_pos_wire": + battery_pos_wire_end = val['end'] + elif key == "_battery_neg_wire": + battery_neg_wire_end = val['end'] + + self.board.draw_blank_board_model( + x_o, + y_o, + battery_pos_wire_end=battery_pos_wire_end, + battery_neg_wire_end=battery_neg_wire_end, + ) + for key, val in circuit_data.items(): if "chip" in key: self.load_chip(val) - elif "wire" in key: + elif "wire" in key and not key.startswith("_battery"): self.load_wire(val) elif "io" in key: @@ -484,18 +498,18 @@ def save_file(self): ) if file_path: try: - # Extract the circuit data from current_dict_circuit circuit_data = deepcopy(self.current_dict_circuit) - circuit_data.pop("last_id", None) # Remove the "last_id" key + circuit_data.pop("last_id", None) for key, comp_data in circuit_data.items(): - # Remove the "id" and "tags" keys before saving comp_data.pop("id", None) comp_data.pop("tags", None) if "label" in comp_data: comp_data["label"] = comp_data["type"] if "wire" in key: - comp_data.pop("XY", None) # Remove XY, will be recalculated anyway + comp_data.pop("XY", None) # Remove XY, will be recalculated anyway + if key == "_battery": + comp_data.pop("battery_rect", None) # Save the data to a JSON file with open(file_path, "w", encoding="utf-8") as file: json.dump(circuit_data, file, indent=4) @@ -516,21 +530,16 @@ def configure_ports(self): print(message) messagebox.showwarning("No COM Ports", message) else: - # Create a new top-level window for the dialog dialog = tk.Toplevel(self.parent) dialog.title("Configure Ports") - # Set the size and position of the dialog dialog.geometry("300x150") - # Create a label for the combobox label = tk.Label(dialog, text="Select an option:") label.pack(pady=10) - # Create a combobox with the options combobox = ttk.Combobox(dialog, values=options) combobox.pack(pady=10) - # Create a button to confirm the selection def confirm_selection(): selected_option = combobox.get() print(f"Selected option: {selected_option}")