diff --git a/app/BrickColors.csv b/app/BrickColours.csv similarity index 100% rename from app/BrickColors.csv rename to app/BrickColours.csv diff --git a/app/app.py b/app/app.py index 4625fdd..4fff130 100644 --- a/app/app.py +++ b/app/app.py @@ -1,5 +1,5 @@ -from stlToDat import stlToDat -from brickcolor import is_brickcolor, brickcolor, get_contrast_color +from stlToDat import stl_to_dat +from brickcolour import is_brickcolour, brickcolour, get_contrast_colour import customtkinter from tkinter import messagebox as tkMessageBox import os @@ -23,11 +23,11 @@ def __init__(self): self.input_file_Var = customtkinter.StringVar() self.output_file_Var = customtkinter.StringVar() - self.color_code_Var = customtkinter.StringVar() - self.color_toggle_Var = customtkinter.StringVar(value="off") + self.colour_code_Var = customtkinter.StringVar() + self.colour_toggle_Var = customtkinter.StringVar(value="off") - self.color_code_Var.trace("w", self.update_color_preview) - self.color_toggle_Var.trace("w", self.update_color_preview) + self.colour_code_Var.trace("w", self.update_colour_preview) + self.colour_toggle_Var.trace("w", self.update_colour_preview) customtkinter.CTkLabel(self.main_frame, text="Input File:").grid(sticky="w", columnspan=2, row=0, column=0) self.input_file_path_label = customtkinter.CTkEntry(self.main_frame, textvariable=self.input_file_Var) @@ -37,20 +37,20 @@ def __init__(self): command=self.get_input_file) self.load_file_button.grid(sticky="ew", row=1, column=2) - customtkinter.CTkLabel(self.main_frame, text="LDraw Color Code:").grid(sticky="w", columnspan=2, + customtkinter.CTkLabel(self.main_frame, text="LDraw Colour Code:").grid(sticky="w", columnspan=2, row=2, column=0) - self.color_code_label = customtkinter.CTkEntry(self.main_frame, textvariable=self.color_code_Var) - self.color_code_label.grid(sticky="ew", columnspan=2, row=3, column=0) - self.color_toggle_checkbox = customtkinter.CTkCheckBox(self.main_frame, text="Apply", - variable=self.color_toggle_Var, onvalue="on", - offvalue="off") - self.color_toggle_checkbox.grid(sticky="ew", columnspan=2, row=3, column=2) - - customtkinter.CTkLabel(self.main_frame, text="Color Preview:").grid(sticky="w", columnspan=1, + self.colour_code_label = customtkinter.CTkEntry(self.main_frame, textvariable=self.colour_code_Var) + self.colour_code_label.grid(sticky="ew", columnspan=2, row=3, column=0) + self.colour_toggle_checkbox = customtkinter.CTkCheckBox(self.main_frame, text="Apply", + variable=self.colour_toggle_Var, onvalue="on", + offvalue="off") + self.colour_toggle_checkbox.grid(sticky="ew", columnspan=2, row=3, column=2) + + customtkinter.CTkLabel(self.main_frame, text="Colour Preview:").grid(sticky="w", columnspan=1, row=4, column=0) - self.color_preview = customtkinter.CTkLabel(self.main_frame, text="Main_Colour", text_color="#00008F", - fg_color="#FFFF80", padx=3, corner_radius=6) - self.color_preview.grid(sticky="w", columnspan=2, row=4, column=1) + self.colour_preview = customtkinter.CTkLabel(self.main_frame, text="Main_Colour", text_color="#00008F", + fg_color="#FFFF80", padx=3, corner_radius=6) + self.colour_preview.grid(sticky="w", columnspan=2, row=4, column=1) customtkinter.CTkLabel(self.main_frame, text="Output File:").grid(sticky="w", columnspan=2, row=5, column=0) self.output_file_label = customtkinter.CTkEntry(self.main_frame, textvariable=self.output_file_Var) @@ -60,8 +60,8 @@ def __init__(self): command=self.set_output_file) self.output_file_button.grid(sticky="ew", row=6, column=2) - self.convertFileButton = customtkinter.CTkButton(self.main_frame, text="convert file", command=self.convert_file) - self.convertFileButton.grid(sticky="ew", row=7, column=1) + self.convert_file_button = customtkinter.CTkButton(self.main_frame, text="convert file", command=self.convert_file) + self.convert_file_button.grid(sticky="ew", row=7, column=1) def get_input_file(self): input_file_path = customtkinter.filedialog.askopenfilename(filetypes=[('stl files', '*.stl')]) @@ -85,38 +85,38 @@ def set_output_file(self): if len(output_file_path) > 0: self.output_file_Var.set(output_file_path) - def update_color_preview(self, *args): - set_color = brickcolor("16") - if self.color_toggle_Var.get() == "on": - set_color = brickcolor(self.color_code_Var.get()) - if set_color is not None: - if set_color.color_type == "LDraw" and set_color.ldrawname is not None: - text_color = get_contrast_color(set_color.rgb_values) - self.color_preview.configure(text=set_color.ldrawname, text_color=text_color, - fg_color=set_color.rgb_values) + def update_colour_preview(self, *args): + set_colour = brickcolour("16") + if self.colour_toggle_Var.get() == "on": + set_colour = brickcolour(self.colour_code_Var.get()) + if set_colour is not None: + if set_colour.colour_type == "LDraw" and set_colour.ldrawname is not None: + text_colour = get_contrast_colour(set_colour.rgb_values) + self.colour_preview.configure(text=set_colour.ldrawname, text_color=text_colour, + fg_color=set_colour.rgb_values) return - elif set_color.color_type == "Direct": - self.color_preview.configure(text=set_color.rgb_values, text_color=set_color.rgb_edge, - fg_color=set_color.rgb_values) + elif set_colour.colour_type == "Direct": + self.colour_preview.configure(text=set_colour.rgb_values, text_color=set_colour.rgb_edge, + fg_color=set_colour.rgb_values) return else: - self.color_preview.configure(text="Unknown or invalid color", text_color="#FFFFFF", - fg_color="#000000") - elif len(self.color_code_Var.get()) == 0: - self.color_preview.configure(text="", fg_color="transparent") + self.colour_preview.configure(text="Unknown or invalid colour", text_color="#FFFFFF", + fg_color="#000000") + elif len(self.colour_code_Var.get()) == 0: + self.colour_preview.configure(text="", fg_color="transparent") return else: - self.color_preview.configure(text="Unknown or invalid color", text_color="#FFFFFF", fg_color="#000000") + self.colour_preview.configure(text="Unknown or invalid colour", text_color="#FFFFFF", fg_color="#000000") def convert_file(self): input_file_path = self.input_file_Var.get() output_file_path = self.output_file_Var.get() - color_code = "16" - if self.color_toggle_Var.get() == "on": - color_code = self.color_code_Var.get() - color_check = is_brickcolor(color_code) - if not color_check[0]: - tkMessageBox.showwarning(color_check[1], color_check[2]) + colour_code = "16" + if self.colour_toggle_Var.get() == "on": + colour_code = self.colour_code_Var.get() + colour_check = is_brickcolour(colour_code) + if not colour_check[0]: + tkMessageBox.showwarning(colour_check[1], colour_check[2]) return if not os.path.isfile(input_file_path): @@ -130,7 +130,7 @@ def convert_file(self): f"'{os.path.dirname(output_file_path)}' is not a valid output directory") return - number_triangles = stlToDat(input_file_path, output_file_path, color_code) + number_triangles = stl_to_dat(input_file_path, output_file_path, colour_code) tkMessageBox.showwarning('Converted File', f'stl file converted to "{output_file_path}"\n' f'Part contains {number_triangles} triangles.') diff --git a/app/brickcolour.py b/app/brickcolour.py new file mode 100644 index 0000000..5c9cfa0 --- /dev/null +++ b/app/brickcolour.py @@ -0,0 +1,97 @@ +def is_brickcolour(colour_code: str): + if len(colour_code) < 1: + return False, "No Colour Code", "Apply Checkbox was toggled, but no colour code provided" + elif not colour_code.startswith("0x2"): + if not colour_code.isdigit(): + return (False, "Invalid Colour Code", + f"The provided colour code '{colour_code}' is not a number.\n " + f"Use a code from the LDraw Colour Definition Reference.\n" + f"If you wanted to use a Direct/HTML colour the format is 0x2RRGGBB " + f"(R,B and G are hexadecimal).") + elif colour_code.startswith("0x2"): + if len(colour_code) > 9: + return (False, "Invalid Colour Code", + f"The provided colour '{colour_code}' seems to be a Direct/HTML colour but is to long.") + elif len(colour_code) < 9: + return (False, "Invalid Colour Code", + f"The provided colour '{colour_code}' seems to be a Direct/HTML colour but is to short.") + for i in range(2, 9): + if colour_code[i] not in ["A", "B", "C", "D", "E", "F"] and not colour_code[i].isdigit(): + return (False, "Invalid Colour Code", + f"The provided colour '{colour_code}' seems to be a Direct/HTML colour, " + f"but contains a invalid charcter at position: {i - 2} - '{colour_code[i]}'.\n" + f"Valid characters are 0-9 and A-F(uppercase)") + return True, + + +class brickcolour: + def __new__(cls, colour_code: str): + if not is_brickcolour(colour_code)[0]: + return None + instance = super().__new__(cls) + return instance + + def __init__(self, colour_code: str): + self.colour_code = colour_code + if colour_code.startswith("0x2"): + self.colour_type = "Direct" + self.rgb_values = f"#{self.colour_code[3:]}" + self.rgb_edge = get_contrast_colour(self.rgb_values) + else: + self.colour_type = "LDraw" + self.ldrawname, _, \ + self.rgb_values, \ + self.rgb_edge, \ + self.alpha, \ + self.luminance, \ + self.material, \ + self.legoname, \ + self.legoid, \ + self.category = get_colour_info_by_id(self.colour_code) + + def __str__(self): + if self.colour_type == "Direct": + return f"Direct Colour: {self.colour_code}" + else: + if self.ldrawname is not None: + return f"LDraw Colour {self.colour_code}: {self.ldrawname}, {self.rgb_values}" + else: + return f"Unknown LDraw Colour {self.colour_code}" + + def __repr__(self): + return f"brickcolour({self.colour_code})" + + +def get_colour_info_by_id(id: str): + found_colour = [None] * 10 + with open("BrickColours.csv", "r", encoding="utf-8") as source: + # skip row with column names + source.readline() + for line in source: + values = line.split(";") + if values[1] == id: + for i in range(len(values)): + # replace empty values with None + if len(values[i]) == 0: + values[i] = None + found_colour = values + break + + return found_colour + +def get_contrast_colour(rgb_values: str): + r = 0 if int(rgb_values[1:3], 16) < 128 else 1 + g = 0 if int(rgb_values[3:5], 16) < 128 else 1 + b = 0 if int(rgb_values[5:7], 16) < 128 else 1 + if r+g+b < 2: + return "#FFFFFF" + else: + return "#000000" + + +def get_complementary_colour(rgb_values: str): + red = '%02X' % (255 - int(rgb_values[1:3], 16)) + green = '%02X' % (255 - int(rgb_values[3:5], 16)) + blue = '%02X' % (255 - int(rgb_values[5:7], 16)) + + return f"#{''.join([red, green, blue])}" diff --git a/app/stlToDat.py b/app/stlToDat.py index cafec01..0fc61c7 100644 --- a/app/stlToDat.py +++ b/app/stlToDat.py @@ -11,13 +11,13 @@ from stl import mesh -def stlToDat(input_filename: str, output_filename: str, colour: str = "16"): +def stl_to_dat(input_filename: str, output_filename: str, colour: str = "16"): mm_to_ldu = 1.0 / 0.4 - inputMesh = mesh.Mesh.from_file(input_filename) + input_mesh = mesh.Mesh.from_file(input_filename) - inputMesh.x *= mm_to_ldu - inputMesh.y *= mm_to_ldu - inputMesh.z *= mm_to_ldu + input_mesh.x *= mm_to_ldu + input_mesh.y *= mm_to_ldu + input_mesh.z *= mm_to_ldu with open(output_filename, "w", encoding="utf-8") as fp_out: # 0: Comment or META command the first 0 line is alway the filename @@ -26,12 +26,12 @@ def stlToDat(input_filename: str, output_filename: str, colour: str = "16"): # Todo: Add License Meta command to file fp_out.write("0 BFC CERTIFY CCW\n") - for index, triangle in enumerate(inputMesh): - # 3:filled triangle, 16:Default color + for index, triangle in enumerate(input_mesh): + # 3:filled triangle, 16:Default colour fp_out.write(f"3 {colour} {' '.join(map(lambda a: str(a), triangle))}\n") # return number of triangles - return len(inputMesh) + return len(input_mesh) if __name__ == '__main__': @@ -59,4 +59,4 @@ def stlToDat(input_filename: str, output_filename: str, colour: str = "16"): elif sys.argv[4] == "-c": colour = sys.argv[5] - print(f"Part contains {stlToDat(input_filename, output_filename, colour)} triangles.") + print(f"Part contains {stl_to_dat(input_filename, output_filename, colour)} triangles.") diff --git a/graphical_userinterface.png b/graphical_userinterface.png index a9be42b..e73e639 100644 Binary files a/graphical_userinterface.png and b/graphical_userinterface.png differ