thcal

that which calculates cutting parameters for CNC thread turning.
git clone https://git.sharifjon.com/thcal
Log | Files | Refs | README | LICENSE

commit 09aca4a1cc49bd10eea63eece3f451439ae3c20a
parent 158d5b85838be6d65f1681df4904e68b3ba956b2
Author: Sharifjon Sharipov <ss2393506@gmail.com>
Date:   Sun, 15 Feb 2026 10:53:48 +0000

Add files via upload
Diffstat:
AT_d.csv | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Adefaults.csv | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aes.csv | 26++++++++++++++++++++++++++
Aflake.nix | 25+++++++++++++++++++++++++
Amain.py | 772+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apreamble.py | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 1236 insertions(+), 0 deletions(-)

diff --git a/T_d.csv b/T_d.csv @@ -0,0 +1,49 @@ +dmin,dmax,p,g3,g4,g5,g6,g7,g8,g9 +0.99,1.4,0.2,24,30,38,48,,, +0.99,1.4,0.25,26,34,42,53,,, +0.99,1.4,0.3,28,36,45,56,,, +1.4,2.8,0.2,25,32,40,50,,, +1.4,2.8,0.25,28,36,45,56,,, +1.4,2.8,0.35,32,40,50,63,80,, +1.4,2.8,0.4,34,42,53,67,85,, +1.4,2.8,0.45,36,45,56,71,90,, +2.8,5.6,0.35,34,42,53,67,85,, +2.8,5.6,0.5,38,48,60,75,95,, +2.8,5.6,0.6,42,53,67,85,106,, +2.8,5.6,0.7,45,56,71,90,112,, +2.8,5.6,0.75,45,56,71,90,112,, +2.8,5.6,0.8,48,60,75,95,118,150,190 +5.6,11.2,0.75,50,63,80,100,125,, +5.6,11.2,1,56,71,90,112,140,180,224 +5.6,11.2,1.25,60,75,95,118,150,190,236 +5.6,11.2,1.5,67,85,106,132,170,212,265 +11.2,22.4,1,60,75,95,118,150,190,236 +11.2,22.4,1.25,67,85,106,132,170,212,265 +11.2,22.4,1.5,71,90,112,140,180,224,280 +11.2,22.4,1.75,75,95,118,150,190,236,300 +11.2,22.4,2,80,100,125,160,200,250,315 +11.2,22.4,2.5,85,106,132,170,212,265,335 +22.4,45,1,63,80,100,125,160,200,250 +22.4,45,1.5,75,95,118,150,190,236,300 +22.4,45,2,85,106,132,170,212,265,335 +22.4,45,3,100,125,160,200,250,315,400 +22.4,45,3.5,106,132,170,212,265,335,425 +22.4,45,4,112,140,180,224,280,355,450 +22.4,45,4.5,118,150,190,236,300,375,475 +45,90,1.5,80,100,125,160,200,250,315 +45,90,2,90,112,140,180,224,280,355 +45,90,3,106,132,170,212,265,335,425 +45,90,4,118,150,190,236,300,375,475 +45,90,5,125,160,200,250,315,400,500 +45,90,5.5,132,170,212,265,335,425,530 +45,90,6,140,180,224,280,355,450,560 +90,180,2,95,118,150,190,236,300,375 +90,180,3,112,140,180,224,280,355,450 +90,180,4,125,160,200,250,315,400,500 +90,180,6,150,190,236,300,375,450,560 +90,180,8,170,212,265,335,425,530,670 +180,355,3,125,160,200,250,315,400,500 +180,355,4,140,180,224,280,355,450,560 +180,355,6,160,200,250,315,400,500,630 +180,355,8,180,224,280,355,450,560,710 + diff --git a/defaults.csv b/defaults.csv @@ -0,0 +1,107 @@ +d,p +1,0.25 +1.1,0.25 +1.2,0.25 +1.4,0.3 +1.6,0.35 +1.8,0.35 +2,0.4 +2.2,0.45 +2.5,0.45 +3,0.5 +3.5,0.6 +4,0.7 +4.5,0.75 +5,0.8 +5.5,0.75 +6,1 +7,1 +8,1.25 +9,1.25 +10,1.5 +11,1.5 +12,1.75 +14,2 +15,1.5 +16,2 +17,1.5 +18,2.5 +20,2.5 +22,2.5 +24,3 +25,2 +26,1.5 +27,3 +28,2 +30,3.5 +32,2 +33,3.5 +35,1.5 +36,4 +38,2 +39,4 +40,3 +42,4.5 +45,4.5 +48,5 +50,3 +52,5 +55,5.5 +56,5.5 +58,4 +60,5.5 +62,4 +64,6 +65,4 +68,6 +70,4 +72,6 +75,7 +76,6 +78,6 +80,6 +82,6 +85,6 +90,6 +95,6 +100,6 +105,6 +110,6 +115,6 +120,6 +125,8 +130,8 +135,8 +140,8 +145,6 +150,8 +155,6 +160,8 +165,6 +170,8 +175,8 +180,8 +185,6 +190,8 +195,6 +200,8 +205,6 +210,8 +215,6 +220,8 +225,6 +230,8 +235,6 +240,8 +245,6 +250,8 +255,6 +260,8 +265,6 +270,8 +275,6 +280,8 +285,6 +290,8 +295,6 +300,8 diff --git a/es.csv b/es.csv @@ -0,0 +1,26 @@ +a,b,c,d,e,f,g,h +0.2,,,,,,,-17,0 +0.25,,,,,,,-18,0 +0.3,,,,,,,-18,0 +0.35,,,,,,-34.0,-19,0 +0.4,,,,,,-34.0,-19,0 +0.45,,,,,,-35.0,-20,0 +0.5,,,,,-50.0,-36.0,-20,0 +0.6,,,,,-53.0,-36.0,-21,0 +0.7,,,,,-56.0,-38.0,-22,0 +0.75,,,,,-56.0,-38.0,-22,0 +0.8,,,,,-60.0,-38.0,-24,0 +1.0,-290.0,-200.0,-130.0,-85.0,-60.0,-40.0,-26,0 +1.25,-295.0,-205.0,-135.0,-90.0,-63.0,-42.0,-28,0 +1.5,-300.0,-212.0,-140.0,-95.0,-67.0,-45.0,-32,0 +1.75,-310.0,-220.0,-145.0,-100.0,-71.0,-48.0,-34,0 +2.0,-315.0,-225.0,-150.0,-105.0,-71.0,-52.0,-38,0 +2.5,-325.0,-235.0,-160.0,-110.0,-80.0,-58.0,-42,0 +3.0,-335.0,-245.0,-170.0,-115.0,-85.0,-63.0,-48,0 +3.5,-345.0,-255.0,-180.0,-125.0,-90.0,-70.0,-53,0 +4.0,-355.0,-265.0,-190.0,-130.0,-95.0,-75.0,-60,0 +4.5,-365.0,-280.0,-200.0,-135.0,-100.0,-80.0,-63,0 +5.0,-375.0,-290.0,-212.0,-140.0,-106.0,-85.0,-71,0 +5.5,-385.0,-300.0,-224.0,-150.0,-112.0,-90.0,-75,0 +6.0,-395.0,-310.0,-236.0,-155.0,-118.0,-95.0,-80,0 +8.0,-425.0,-340.0,-265.0,-180.0,-140.0,-118.0,-100,0 diff --git a/flake.nix b/flake.nix @@ -0,0 +1,25 @@ +{ + description = "A very basic flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + }; + + outputs = { self, nixpkgs }: + let + pkgs = nixpkgs.legacyPackages."x86_64-linux"; + in + { + + devShells."x86_64-linux".default = pkgs.mkShell { + + packages = [ pkgs.python313Packages.tkinter ]; + + shellHook = '' + export PS1='\[\e[92m\](nix-shell)\[\e[0m\]\w ' + + ''; + }; + }; +} + diff --git a/main.py b/main.py @@ -0,0 +1,772 @@ +import ctypes + +def enable_high_dpi_awareness(): + try: + # Windows 8.1+ + ctypes.windll.shcore.SetProcessDpiAwareness(2) # PROCESS_PER_MONITOR_DPI_AWARE + except Exception: + try: + # Windows Vista–8.0 + ctypes.windll.user32.SetProcessDPIAware() + except Exception: + pass + +enable_high_dpi_awareness() + + +import math +from preamble import ThreadCalculator, ThreadDatabase, InputParser +import tkinter as tk +from tkinter import ttk +import os +import sys + +def resource_path(relative_path): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + return os.path.join(base_path, relative_path) + + + +class App(tk.Tk): + def __init__(self): + super().__init__() + + + # Adjust scaling for high-DPI displays + + dpi = self.winfo_fpixels('1i') + scale_factor = dpi / 72.0 + + # Apply scaling to Tkinter + self.tk.call('tk', 'scaling', scale_factor) + + # Optional: also set a nicer default font + self.option_add("*Font", ("Segoe UI", 10)) + + + self.bind_enter_to_button() + + + + icon = tk.PhotoImage(file=resource_path("icon.png")) + self.iconphoto(True, icon) + + self.title("Thread Calculator") + self.minsize(950, 400) + + + self.parser = InputParser() + self.db_td = ThreadDatabase(resource_path("T_d.csv")) + self.db_es = ThreadDatabase(resource_path("es.csv")) + + self.columnconfigure(0, weight=2) + self.columnconfigure(1, weight=1) + self.rowconfigure(0, weight=7) + self.rowconfigure(1, weight=3) + + self.container = ttk.Frame(self) + self.container.pack(expand=True, fill="both") + + self.main_menu = self.build_main_menu() + self.ext_env = self.build_ext_env() + self.int_env = self.build_int_env() + + self.show(self.main_menu) + + def bind_enter_to_button(self): + def trigger_focused_button(event): + widget = self.focus_get() + if isinstance(widget, tk.Button) or isinstance(widget, ttk.Button): + widget.invoke() # Trigger the button's command + self.bind_all("<Return>", trigger_focused_button) + + def show(self, frame): + frame.tkraise() + + + + # ----------------- MAIN MENU ------------------ + def build_main_menu(self): + frame = ttk.Frame(self.container) + frame.place(relwidth=1, relheight=1) + + frame_buttons = ttk.Frame(frame) + frame_buttons.pack(pady=50) + + # Buttons + btn_ext = ttk.Button(frame_buttons, text="External Thread Calculator", + command=lambda: self.show(self.ext_env)) + btn_ext.pack(padx=10, pady=20) + + btn_int = ttk.Button(frame_buttons, text="Internal Thread Calculator", + command=lambda: self.show(self.int_env)) + btn_int.pack(padx=10) + + return frame + + # ---------------- EXTERNAL THREAD ENVIRONMENT ------------------ + def build_ext_env(self): + frame = ttk.Frame(self.container) + frame.place(relwidth=1, relheight=1) + + ttk.Button(frame, text="← Back", command=lambda: self.show(self.main_menu)).pack(anchor="w", pady=5, padx=5) + + notebook = ttk.Notebook(frame) + notebook.pack(expand=True, fill="both", padx=5, pady=5) + + # --- TAB 1: EXTERNAL THREAD WITH RUNOUT --- # + tab_runout = ttk.Frame(notebook) + tab_runout.columnconfigure(0, weight=1, uniform="half") + tab_runout.columnconfigure(1, weight=1, uniform="half") + tab_runout.rowconfigure(0, weight=1) + + notebook.add(tab_runout, text="With Runout") + + tab_runout_left = ttk.LabelFrame(tab_runout, text="Input") + tab_runout_left.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") + tab_runout_left.columnconfigure(0, weight=1) + tab_runout_left.rowconfigure(1, weight=1) + tab_runout_left.rowconfigure(0, weight=3) + + + tab_runout_left_top = ttk.Frame(tab_runout_left) + tab_runout_left_top.grid(row=0, column=0, padx=5, pady=5, sticky="new") + tab_runout_left_top.columnconfigure(1, weight=1, uniform="half") + tab_runout_left_top.columnconfigure(0, weight=1, uniform="half") + + + + tab_runout_left_bottom = ttk.Frame(tab_runout_left) + tab_runout_left_bottom.grid(row=1, column=0, padx=5, pady=5, sticky="sew") + tab_runout_left_bottom.columnconfigure(0, weight=1) + tab_runout_left_bottom.columnconfigure(1, weight=1) + + + + tab_runout_right = ttk.LabelFrame(tab_runout, text="Output") + tab_runout_right.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") + tab_runout_right.columnconfigure(0, weight=1) + tab_runout_right.rowconfigure(0, weight=1) + + tk.Label(tab_runout_left_top, text="Designation:").grid(padx=5, pady=5, row=0, column=0, sticky="nse") + runout_designation_entry = ttk.Entry(tab_runout_left_top) + runout_designation_entry.grid(padx=5, pady=5, row=0, column=1, sticky="nsew") + + tk.Label(tab_runout_left_top, text="Thread start point and its length:").grid(padx=5, pady=5, row=1, column=0, sticky="nse") + runout_sl_entry = ttk.Entry(tab_runout_left_top) + runout_sl_entry.grid(padx=5, pady=5, row=1, column=1, sticky="nsew") + + # CALCULATIONS FOR EXTERNAL THREAD WITH RUNOUT + + def calc_runout(): + runout_output_box.config(state="normal") + runout_output_box.delete("1.0", tk.END) + + designation = runout_designation_entry.get() + designation_parsed = self.parser.parse_designation(designation) + if isinstance(designation_parsed, str): + runout_output_box.insert(tk.END, designation_parsed + "\n") + runout_output_box.config(state="disabled") + return + d, pitch, grade, class_letter = designation_parsed + + sl = runout_sl_entry.get() + sl_parsed = self.parser.parse_sl(sl) + if isinstance(sl_parsed, str): + runout_output_box.insert(tk.END, sl_parsed + "\n") + runout_output_box.config(state="disabled") + return + s, l = sl_parsed + + T_d_value = self.db_td.lookup_value_T_d(diameter=d, pitch=pitch, grade=grade) + if T_d_value is None: + runout_output_box.insert(tk.END, "No T_d-value found in database!\n") + runout_output_box.config(state="disabled") + return + + es_value = self.db_es.lookup_value_es(pitch=pitch, class_letter=class_letter) + if es_value is None: + runout_output_box.insert(tk.END, "No es-value found in database!\n") + runout_output_box.config(state="disabled") + return + + + depth = ThreadCalculator.calc_depth(pitch, es_value, T_d_value) + fal = ThreadCalculator.cal_fal(pitch) + noc = ThreadCalculator.calc_noc(pitch) + + # DISPLAY RESULTS + runout_output_box.insert(tk.END, f"SPL: {s}\n") + runout_output_box.insert(tk.END, f"DM1: {d}\n") + runout_output_box.insert(tk.END, f"FPL: {s - l - 2.5*pitch}\n") + runout_output_box.insert(tk.END, f"APP: {pitch}\n") + runout_output_box.insert(tk.END, f"ROP: {2.5*pitch}\n") + runout_output_box.insert(tk.END, f"TDEP: {depth:.4f}\n") + runout_output_box.insert(tk.END, f"FAL: {fal}\n") + runout_output_box.insert(tk.END, f"IANG: 28\n") + if noc is None: + runout_output_box.insert(tk.END, f"NRC: {noc}, choose different pitch (0.5-6)!\n") + else: + runout_output_box.insert(tk.END, f"NRC: {noc-1}\n") + runout_output_box.insert(tk.END, f"PIT: {pitch}\n") + runout_output_box.insert(tk.END, f"VARI: 300103 (constant area!)\n") + + runout_output_box.config(state="disabled") + + def clear_runout_output(): + runout_output_box.config(state="normal") + runout_output_box.delete("1.0", tk.END) + runout_output_box.config(state="disabled") + + ttk.Button(tab_runout_left_bottom, text="Calculate", command=calc_runout).grid(padx=5, pady=5, row=5, column=0, sticky="sew") + + + + ttk.Button(tab_runout_left_bottom, text="Clear Output", command=clear_runout_output).grid(padx=5, pady=5, row=5, column=1, sticky="sew") + + + + runout_output_box = tk.Text(tab_runout_right) + runout_output_box.grid(padx=5, pady=5, row=0, column=0, sticky="nsew") + runout_output_box.config(state="disabled") # Read-only initially + + + + + + + + # --- TAB 2: EXTERNAL THREAD WITH UNDERCUT --- # + tab_undercut = ttk.Frame(notebook) + tab_undercut.columnconfigure(0, weight=1, uniform="half") + tab_undercut.columnconfigure(1, weight=1, uniform="half") + tab_undercut.rowconfigure(0, weight=1) + + notebook.add(tab_undercut, text="With Undercut") + + tab_undercut_left = ttk.LabelFrame(tab_undercut, text="Input") + tab_undercut_left.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") + tab_undercut_left.columnconfigure(0, weight=1) + tab_undercut_left.rowconfigure(1, weight=1) + tab_undercut_left.rowconfigure(0, weight=3) + tab_undercut_left.rowconfigure(1, weight=1) + + + tab_undercut_left_top = ttk.Frame(tab_undercut_left) + tab_undercut_left_top.grid(row=0, column=0, padx=5, pady=5, sticky="new") + tab_undercut_left_top.columnconfigure(1, weight=1, uniform="half") + tab_undercut_left_top.columnconfigure(0, weight=1, uniform="half") + + tab_undercut_left_bottom = ttk.Frame(tab_undercut_left) + tab_undercut_left_bottom.grid(row=1, column=0, padx=5, pady=5, sticky="sew") + tab_undercut_left_bottom.columnconfigure(0, weight=1) + tab_undercut_left_bottom.columnconfigure(1, weight=1) + + tab_undercut_right = ttk.LabelFrame(tab_undercut, text="Output") + tab_undercut_right.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") + tab_undercut_right.columnconfigure(0, weight=1) + tab_undercut_right.rowconfigure(0, weight=1) + + + + tk.Label(tab_undercut_left_top, text="Designation:").grid(padx=5, pady=5, row=0, column=0, sticky="nse") + undercut_designation_entry = ttk.Entry(tab_undercut_left_top) + undercut_designation_entry.grid(padx=5, pady=5, row=0, column=1, sticky="nsew") + + tk.Label(tab_undercut_left_top, text="Thread start point and its length:").grid(padx=5, pady=5, row=1, column=0, sticky="nse") + undercut_sl_entry = ttk.Entry(tab_undercut_left_top) + undercut_sl_entry.grid(padx=5, pady=5, row=1, column=1, sticky="nsew") + + tk.Label(tab_undercut_left_top, text="PDX-value of your insert:").grid(padx=5, pady=5, row=2, column=0, sticky="nse") + pdx_entry = ttk.Entry(tab_undercut_left_top) + pdx_entry.grid(padx=5, pady=5, row=2, column=1, sticky="nsew") + + # CALCULATIONS FOR EXTERNAL THREAD WITH UNDERCUT + + def calc_undercut(): + undercut_output_box.config(state="normal") + undercut_output_box.delete("1.0", tk.END) + + designation = undercut_designation_entry.get() + designation_parsed = self.parser.parse_designation(designation) + if isinstance(designation_parsed, str): + undercut_output_box.insert(tk.END, designation_parsed + "\n") + undercut_output_box.config(state="disabled") + return + d, pitch, grade, class_letter = designation_parsed + + sl = undercut_sl_entry.get() + sl_parsed = self.parser.parse_sl(sl) + if isinstance(sl_parsed, str): + undercut_output_box.insert(tk.END, sl_parsed + "\n") + undercut_output_box.config(state="disabled") + return + s, l = sl_parsed + + PDX = self.parser.validated(pdx_entry.get()) + if PDX is None: + undercut_output_box.insert(tk.END, "Invalid PDX-value!\n") + + T_d_value = self.db_td.lookup_value_T_d(diameter=d, pitch=pitch, grade=grade) + if T_d_value is None: + undercut_output_box.insert(tk.END, "No T_d-value found in database!\n") + undercut_output_box.config(state="disabled") + return + + es_value = self.db_es.lookup_value_es(pitch=pitch, class_letter=class_letter) + if es_value is None: + undercut_output_box.insert(tk.END, "No es-value found in database!\n") + undercut_output_box.config(state="disabled") + return + + + depth = ThreadCalculator.calc_depth(pitch, es_value, T_d_value) + fal = ThreadCalculator.cal_fal(pitch) + noc = ThreadCalculator.calc_noc(pitch) + + # Display results + undercut_output_box.insert(tk.END, f"SPL: {s}\n") + undercut_output_box.insert(tk.END, f"DM1: {d}\n") + if PDX is None: + undercut_output_box.insert(tk.END, "FPL: Not Calculated\n") + else: + undercut_output_box.insert(tk.END, f"FPL: {s - l + 0.5 + PDX}\n") + undercut_output_box.insert(tk.END, f"APP: {pitch}\n") + if PDX is None: + undercut_output_box.insert(tk.END, "ROP: Not Calculated\n") + else: + undercut_output_box.insert(tk.END, f"ROP: g1-({0.5+PDX})\n") + undercut_output_box.insert(tk.END, f"TDEP: {depth:.4f}\n") + if fal is None: + undercut_output_box.insert(tk.END, f"FAL: {fal}, choose different pitch (0.5-6)!\n") + else: + undercut_output_box.insert(tk.END, f"FAL: {fal}\n") + undercut_output_box.insert(tk.END, f"IANG: 28\n") + if noc is None: + undercut_output_box.insert(tk.END, f"NRC: {noc}, choose different pitch (0.5-6)!\n") + else: + undercut_output_box.insert(tk.END, f"NRC: {noc-1}\n") + undercut_output_box.insert(tk.END, f"PIT: {pitch}\n") + undercut_output_box.insert(tk.END, f"VARI: 300103 (constant area!)\n") + + undercut_output_box.config(state="disabled") + + def clear_undercut_output(): + undercut_output_box.config(state="normal") + undercut_output_box.delete("1.0", tk.END) + undercut_output_box.config(state="disabled") + + + + ttk.Button(tab_undercut_left_bottom, text="Calculate", command=calc_undercut).grid(padx=5, pady=5, row=0, column=0, sticky="sew") + + ttk.Button(tab_undercut_left_bottom, text="Clear Output", command=clear_undercut_output).grid(padx=5, pady=5, row=0, column=1, sticky="sew") + + undercut_output_box = tk.Text(tab_undercut_right, height=20, width=80) + undercut_output_box.grid(padx=5, pady=5, row=0, column=0, sticky="nsew") + undercut_output_box.config(state="disabled") # Read-only initially + + return frame + + + + + + + # ---------------- INTERNAL THREAD ENVIRONMENT ------------------ + def build_int_env(self): + frame = ttk.Frame(self.container) + frame.place(relwidth=1, relheight=1) + + ttk.Button(frame, text="← Back", command=lambda: self.show(self.main_menu)).pack(anchor="w", pady=5, padx=5) + + notebook = ttk.Notebook(frame) + notebook.pack(expand=True, fill="both", padx=5, pady=10) + + + # --- TAB 1: CENTER DRILL --- # + tab_center_drill = ttk.Frame(notebook) + tab_center_drill.columnconfigure(0, weight=1, uniform="half") + tab_center_drill.columnconfigure(1, weight=1, uniform="half") + tab_center_drill.rowconfigure(0, weight=1) + + notebook.add(tab_center_drill, text="Center Drill") + + tab_center_drill_left = ttk.LabelFrame(tab_center_drill, text="Input") + tab_center_drill_left.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") + tab_center_drill_left.columnconfigure(0, weight=1) + tab_center_drill_left.rowconfigure(1, weight=1) + tab_center_drill_left.rowconfigure(0, weight=1) + + + tab_center_drill_left_top = ttk.Frame(tab_center_drill_left) + tab_center_drill_left_top.grid(row=0, column=0, padx=5, pady=5, sticky="new") + tab_center_drill_left_top.columnconfigure(1, weight=1, uniform="half") + tab_center_drill_left_top.columnconfigure(0, weight=1, uniform="half") + + + + tab_center_drill_left_bottom = ttk.Frame(tab_center_drill_left) + tab_center_drill_left_bottom.grid(row=1, column=0, padx=5, pady=5, sticky="sew") + tab_center_drill_left_bottom.columnconfigure(0, weight=1) + tab_center_drill_left_bottom.columnconfigure(1, weight=1) + + + tab_center_drill_right = ttk.LabelFrame(tab_center_drill, text="Output") + tab_center_drill_right.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") + tab_center_drill_right.columnconfigure(0, weight=1) + tab_center_drill_right.rowconfigure(0, weight=1) + + + + tk.Label(tab_center_drill_left_top, text="Designation:").grid(padx=5, pady=5, row=0, column=0, sticky="nse") + center_drill_designation_entry = ttk.Entry(tab_center_drill_left_top) + center_drill_designation_entry.grid(padx=5, pady=5, row=0, column=1, sticky="nsew") + + tk.Label(tab_center_drill_left_top, text="Hole start point:").grid(padx=5, pady=5, row=1, column=0, sticky="nse") + center_drill_s_entry = ttk.Entry(tab_center_drill_left_top) + center_drill_s_entry.grid(padx=5, pady=5, row=1, column=1, sticky="nsew") + + tk.Label(tab_center_drill_left_top, text="Center drill cone angle:").grid(padx=5, pady=5, row=2, column=0, sticky="nse") + center_drill_angle_entry = ttk.Entry(tab_center_drill_left_top) + center_drill_angle_entry.grid(padx=5, pady=5, row=2, column=1, sticky="nsew") + + # CALCULATIONS FOR CENTER DRILL + + def calc_center_drill(): + center_drill_output_box.config(state="normal") + center_drill_output_box.delete("1.0", tk.END) + + designation = center_drill_designation_entry.get() + designation_parsed = self.parser.parse_designation(designation) + if isinstance(designation_parsed, str): + center_drill_output_box.insert(tk.END, designation_parsed + "\n") + center_drill_output_box.config(state="disabled") + return + d, pitch, grade, class_letter = designation_parsed + + d2 = d - pitch + + s = self.parser.validated(center_drill_s_entry.get()) + if s is None: + center_drill_output_box.insert(tk.END, "Invalid hole start point!\n") + center_drill_output_box.config(state="disabled") + return + + + a1 = self.parser.validated(center_drill_angle_entry.get()) + if a1 is None: + center_drill_output_box.insert(tk.END, "Invalid center drill cone angle!\n") + center_drill_output_box.config(state="disabled") + return + + h1 = (0.65*d2)/(2*math.tan(math.radians(a1/2))) + + # DISPLAY RESULTS + + center_drill_output_box.insert(tk.END, f"RFP: {s}\n") + center_drill_output_box.insert(tk.END, f"DP: {s-h1: .5f}\n") + center_drill_output_box.config(state="disabled") + + def clear_center_drill_output(): + center_drill_output_box.config(state="normal") + center_drill_output_box.delete("1.0", tk.END) + center_drill_output_box.config(state="disabled") + + + ttk.Button(tab_center_drill_left_bottom, text="Calculate", command=calc_center_drill).grid(padx=5, pady=5, row=0, column=0, sticky="sew") + + ttk.Button(tab_center_drill_left_bottom, text="Clear Output", command=clear_center_drill_output).grid(padx=5, pady=5, row=0, column=1, sticky="sew") + + + center_drill_output_box = tk.Text(tab_center_drill_right, height=20, width=80) + center_drill_output_box.grid(padx=5, pady=5, row=0, column=0, sticky="nsew") + center_drill_output_box.config(state="disabled") # Read-only initially + + + + + # --- TAB 2: DEEP HOLE DRILL --- # + tab_deep_hole_drill = ttk.Frame(notebook) + tab_deep_hole_drill.columnconfigure(0, weight=1, uniform="half") + tab_deep_hole_drill.columnconfigure(1, weight=1, uniform="half") + tab_deep_hole_drill.rowconfigure(0, weight=1) + + notebook.add(tab_deep_hole_drill, text="Deep Hole Drill") + + tab_deep_hole_drill_left = ttk.LabelFrame(tab_deep_hole_drill, text="Input") + tab_deep_hole_drill_left.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") + tab_deep_hole_drill_left.columnconfigure(0, weight=1) + tab_deep_hole_drill_left.rowconfigure(1, weight=1) + tab_deep_hole_drill_left.rowconfigure(0, weight=1) + tab_deep_hole_drill_left.rowconfigure(1, weight=1) + + tab_deep_hole_drill_left_top = ttk.Frame(tab_deep_hole_drill_left) + tab_deep_hole_drill_left_top.grid(row=0, column=0, padx=5, pady=5, sticky="new") + tab_deep_hole_drill_left_top.columnconfigure(1, weight=1, uniform="half") + tab_deep_hole_drill_left_top.columnconfigure(0, weight=1, uniform="half") + + tab_deep_hole_drill_left_bottom = ttk.Frame(tab_deep_hole_drill_left) + tab_deep_hole_drill_left_bottom.grid(row=1, column=0, padx=5, pady=5, sticky="sew") + tab_deep_hole_drill_left_bottom.columnconfigure(0, weight=1) + tab_deep_hole_drill_left_bottom.columnconfigure(1, weight=1) + + tab_deep_hole_drill_right = ttk.LabelFrame(tab_deep_hole_drill, text="Output") + tab_deep_hole_drill_right.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") + tab_deep_hole_drill_right.columnconfigure(0, weight=1) + tab_deep_hole_drill_right.rowconfigure(0, weight=1) + + + + tk.Label(tab_deep_hole_drill_left_top, text="Designation:").grid(padx=5, pady=5, row=0, column=0, sticky="nse") + deep_hole_drill_designation_entry = ttk.Entry(tab_deep_hole_drill_left_top) + deep_hole_drill_designation_entry.grid(padx=5, pady=5, row=0, column=1, sticky="nsew") + + tk.Label(tab_deep_hole_drill_left_top, text="Tapping tool form (e.g., C):").grid(padx=5, pady=5, row=1, column=0, sticky="nse") + deep_hole_drill_form_entry = ttk.Entry(tab_deep_hole_drill_left_top) + deep_hole_drill_form_entry.grid(padx=5, pady=5, row=1, column=1, sticky="nsew") + + tk.Label(tab_deep_hole_drill_left_top, text="Hole start point and thread length:").grid(padx=5, pady=5, row=2, column=0, sticky="nse") + deep_hole_drill_sl_entry = ttk.Entry(tab_deep_hole_drill_left_top) + deep_hole_drill_sl_entry.grid(padx=5, pady=5, row=2, column=1, sticky="nsew") + + tk.Label(tab_deep_hole_drill_left_top, text="Deep hole drill cone angle:").grid(padx=5, pady=5, row=3, column=0, sticky="nse") + deep_hole_drill_angle_entry = ttk.Entry(tab_deep_hole_drill_left_top) + deep_hole_drill_angle_entry.grid(padx=5, pady=5, row=3, column=1, sticky="nsew") + + # CALCULATIONS FOR DEEP HOLE DRILLING + + def calc_deep_hole_drill(): + deep_hole_drill_output_box.config(state="normal") + deep_hole_drill_output_box.delete("1.0", tk.END) + + designation = deep_hole_drill_designation_entry.get() + designation_parsed = self.parser.parse_designation(designation) + if isinstance(designation_parsed, str): + deep_hole_drill_output_box.insert(tk.END, designation_parsed + "\n") + deep_hole_drill_output_box.config(state="disabled") + return + d, pitch, grade, class_letter = designation_parsed + + d2 = d - pitch + + form = deep_hole_drill_form_entry.get().lower() + form_range = { + "a": 8, + "b": 5, + "c": 3, + "d": 5, + "e": 2 + } + if not isinstance(form, str): + deep_hole_drill_output_box.insert(tk.END, "Invalid type!\n") + deep_hole_drill_output_box.config(state="disabled") + return + elif form not in form_range: + deep_hole_drill_output_box.insert(tk.END, "Entered tool form not found!\n") + deep_hole_drill_output_box.config(state="disabled") + return + k = form_range.get(form) + + sl = deep_hole_drill_sl_entry.get() + sl_parsed = self.parser.parse_sl(sl) + if isinstance(sl_parsed, str): + deep_hole_drill_output_box.insert(tk.END, sl_parsed + "\n") + deep_hole_drill_output_box.config(state="disabled") + return + s, l = sl_parsed + + a2 = self.parser.validated(deep_hole_drill_angle_entry.get()) + if a2 is None: + deep_hole_drill_output_box.insert(tk.END, "Invalid deep hole drill cone angle!\n") + deep_hole_drill_output_box.config(state="disabled") + return + + h_2 = ThreadCalculator.half_ceil(l + (k+2)*pitch + (d2)/(2*math.tan(math.radians(a2/2)))) + h2 = s - h_2 + + deep_hole_drill_output_box.insert(tk.END, f"RFP: {s}\n") + deep_hole_drill_output_box.insert(tk.END, f"DP: {h2: .3f}\n") + fdep = ThreadCalculator.depth_per_storke(h_2) + if fdep is None: + deep_hole_drill_output_box.insert(tk.END, "FDEP: Invalid depth!\n") + else: + deep_hole_drill_output_box.insert(tk.END, f"FDEP: {s - fdep}\n") + deep_hole_drill_output_box.config(state="disabled") + + def clear_deep_hole_drill_output(): + deep_hole_drill_output_box.config(state="normal") + deep_hole_drill_output_box.delete("1.0", tk.END) + deep_hole_drill_output_box.config(state="disabled") + + def deep_hole_drill_get_data(): + if center_drill_designation_entry.get(): + deep_hole_drill_designation_entry.delete(0, tk.END) + deep_hole_drill_designation_entry.insert(0, center_drill_designation_entry.get()) + if center_drill_s_entry.get(): + deep_hole_drill_sl_entry.delete(0, tk.END) + deep_hole_drill_sl_entry.insert(0, center_drill_s_entry.get()+",") + + + ttk.Button(tab_deep_hole_drill_left_bottom, text="Get Data", command=deep_hole_drill_get_data).grid(padx=5, pady=5, row=0, column=0, sticky="sew") + + ttk.Button(tab_deep_hole_drill_left_bottom, text="Calculate", command=calc_deep_hole_drill).grid(padx=5, pady=5, row=1, column=0, sticky="sew") + ttk.Button(tab_deep_hole_drill_left_bottom, text="Clear Output", command=clear_deep_hole_drill_output).grid(padx=5, pady=5, row=1, column=1, sticky="sew") + + + deep_hole_drill_output_box = tk.Text(tab_deep_hole_drill_right, height=20, width=80) + deep_hole_drill_output_box.grid(padx=5, pady=5, row=0, column=0, sticky="nsew") + deep_hole_drill_output_box.config(state="disabled") # Read-only initially + + + + # --- TAB 3: TAPPING --- # + tab_tapping = ttk.Frame(notebook) + tab_tapping.columnconfigure(0, weight=1, uniform="half") + tab_tapping.columnconfigure(1, weight=1, uniform="half") + tab_tapping.rowconfigure(0, weight=1) + + notebook.add(tab_tapping, text="Tapping") + + tab_tapping_left = ttk.LabelFrame(tab_tapping, text="Input") + tab_tapping_left.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") + tab_tapping_left.columnconfigure(0, weight=1) + tab_tapping_left.rowconfigure(1, weight=1) + tab_tapping_left.rowconfigure(0, weight=1) + tab_tapping_left.rowconfigure(1, weight=1) + + tab_tapping_left_top = ttk.Frame(tab_tapping_left) + tab_tapping_left_top.grid(row=0, column=0, padx=5, pady=5, sticky="new") + tab_tapping_left_top.columnconfigure(1, weight=1, uniform="half") + tab_tapping_left_top.columnconfigure(0, weight=1, uniform="half") + + + tab_tapping_left_bottom = ttk.Frame(tab_tapping_left) + tab_tapping_left_bottom.grid(row=1, column=0, padx=5, pady=5, sticky="sew") + tab_tapping_left_bottom.columnconfigure(0, weight=1) + tab_tapping_left_bottom.columnconfigure(1, weight=1) + + tab_tapping_right = ttk.LabelFrame(tab_tapping, text="Output") + tab_tapping_right.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") + tab_tapping_right.columnconfigure(0, weight=1) + tab_tapping_right.rowconfigure(0, weight=1) + + + tk.Label(tab_tapping_left_top, text="Designation:").grid(padx=5, pady=5, row=0, column=0, sticky="nse") + tapping_designation_entry = ttk.Entry(tab_tapping_left_top) + tapping_designation_entry.grid(padx=5, pady=5, row=0, column=1, sticky="nsew") + + tk.Label(tab_tapping_left_top, text="Tapping tool form (e.g., C):").grid(padx=5, pady=5, row=1, column=0, sticky="nse") + tapping_form_entry = ttk.Entry(tab_tapping_left_top) + tapping_form_entry.grid(padx=5, pady=5, row=1, column=1, sticky="nsew") + + tk.Label(tab_tapping_left_top, text="Hole start point and thread length:").grid(padx=5, pady=5, row=2, column=0, sticky="nse") + tapping_sl_entry = ttk.Entry(tab_tapping_left_top) + tapping_sl_entry.grid(padx=5, pady=5, row=2, column=1, sticky="nsew") + + # CALCULATIONS FOR TAPPING + + def calc_tapping(): + tapping_output_box.config(state="normal") + tapping_output_box.delete("1.0", tk.END) + + designation = tapping_designation_entry.get() + designation_parsed = self.parser.parse_designation(designation) + if isinstance(designation_parsed, str): + tapping_output_box.insert(tk.END, designation_parsed + "\n") + tapping_output_box.config(state="disabled") + return + d, pitch, grade, class_letter = designation_parsed + + form = tapping_form_entry.get().lower() + form_range = { + "a": 8, + "b": 5, + "c": 3, + "d": 5, + "e": 2 + } + if not isinstance(form, str): + tapping_output_box.insert(tk.END, "Invalid type!\n") + tapping_output_box.config(state="disabled") + return + elif form not in form_range: + tapping_output_box.insert(tk.END, "Entered tool form not found!\n") + tapping_output_box.config(state="disabled") + return + k = form_range.get(form) + + sl = tapping_sl_entry.get() + sl_parsed = self.parser.parse_sl(sl) + if isinstance(sl_parsed, str): + tapping_output_box.insert(tk.END, sl_parsed + "\n") + tapping_output_box.config(state="disabled") + return + s, l = sl_parsed + + h4 = s - (l + k*pitch) + + tapping_output_box.insert(tk.END, f"RFP: {s}\n") + tapping_output_box.insert(tk.END, f"DP: {h4}\n") + tapping_output_box.insert(tk.END, "SDAC: 5\n") + tapping_output_box.insert(tk.END, f"PIT: {pitch}\n") + + + tapping_output_box.config(state="disabled") + + def clear_tapping_output(): + tapping_output_box.config(state="normal") + tapping_output_box.delete("1.0", tk.END) + tapping_output_box.config(state="disabled") + + def get_tapping_data(): + if deep_hole_drill_designation_entry.get(): + tapping_designation_entry.delete(0, tk.END) + tapping_designation_entry.insert(0, deep_hole_drill_designation_entry.get()) + + if (not deep_hole_drill_designation_entry.get()) and center_drill_designation_entry.get(): + tapping_designation_entry.delete(0, tk.END) + tapping_designation_entry.insert(0, center_drill_designation_entry.get()) + + if (not deep_hole_drill_sl_entry.get()) and center_drill_s_entry.get(): + tapping_sl_entry.delete(0, tk.END) + tapping_sl_entry.insert(0, center_drill_s_entry.get()+",") + + if deep_hole_drill_form_entry.get(): + tapping_form_entry.delete(0, tk.END) + tapping_form_entry.insert(0, deep_hole_drill_form_entry.get()) + + if deep_hole_drill_sl_entry.get(): + tapping_sl_entry.delete(0, tk.END) + tapping_sl_entry.insert(0, deep_hole_drill_sl_entry.get()) + + ttk.Button(tab_tapping_left_bottom, text="Get Data", command=get_tapping_data).grid(padx=5, pady=5, row=0, column=0, sticky="sew") + + ttk.Button(tab_tapping_left_bottom, text="Calculate", command=calc_tapping).grid(padx=5, pady=5, row=1, column=0, sticky="sew") + + ttk.Button(tab_tapping_left_bottom, text="Clear Output", command=clear_tapping_output).grid(padx=5, pady=5, row=1, column=1, sticky="sew") + + + tapping_output_box = tk.Text(tab_tapping_right) + tapping_output_box.grid(padx=5, pady=5, row=0, column=0, sticky="nsew") + tapping_output_box.config(state="disabled") # Read-only initially + + + + return frame + + + + + + + + +if __name__ == "__main__": + app = App() + app.mainloop() + diff --git a/preamble.py b/preamble.py @@ -0,0 +1,257 @@ +import pandas as pd +import math + + +import os +import sys + +def resource_path(relative_path): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + + base_path = sys._MEIPASS + except Exception: + + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + +class ThreadDatabase: + def __init__(self, csv_path): + full_path = resource_path(csv_path) + self.df = pd.read_csv(full_path) + + def lookup_value_T_d(self, diameter, pitch, grade): + + row = self.df[( self.df["dmin"] < diameter) & ( diameter <= self.df["dmax"] ) & (self.df["p"] == pitch)] + + if row.empty: + return None + + value = 0.001*row.iat[0, grade] + + if pd.isna(value): + return None + + return float(value) + + def lookup_value_es(self, pitch, class_letter): + + value1=-0.001*self.df.loc[pitch, class_letter] + + if pd.isna(value1): + return None + + return float(value1) + + +class InputParser: + def __init__(self): + self.defaults = pd.read_csv(resource_path("defaults.csv")) + + def validated(self, value): + try: + value_float = float(value) + return value_float + except ValueError: + return None + + def parse_designation(self, designation): + """Return (diameter and pitch) with defaults if needed.""" + + designation = designation.lower().replace("x", "×") + + # Minimal format (e.g. M10) + if ("×" not in designation) and ("-" not in designation): + d = self.validated(designation.replace("m", "")) + # check format + if d is None: + return "Invalid diameter format!" + + #check availability + elif not self.defaults["d"].eq(d).any(): + return "Diameter not available!" + + else: + row=self.defaults[self.defaults["d"]==d] + pitch = row.iat[0, 1] + + grade = 6 # default tolerance grade + if d <= 1.4: + class_letter = "h" # default class up to and including 1.4 nominal diameter + else: + class_letter = "g" # default class for larger diameters + return d, pitch, grade, class_letter + + + # Semi-full format 1 (e.g. M10×1.5) + + elif ("×" in designation) and ("-" not in designation): + d = self.validated(designation.replace("m", "").split("×")[0]) + + # check format + if d is None: + return "Invalid diameter format!" + + # check availability + elif not self.defaults["d"].eq(d).any(): + return "Diameter not available!" + + pitch = self.validated(designation.split("×")[1]) + + # check format + if pitch is None: + return "Invalid pitch format!" + + # check availability + elif not self.defaults["p"].eq(pitch).any(): + return "Pitch not available!" + + # check compatibility + elif pitch > self.defaults[self.defaults["d"]==d].iat[0, 1]: + return "Incompatible pitch for given diameter!" + + grade = 6 # default tolerance grade + if d <= 1.4: + class_letter = "h" # default class up to and including 1.4 nominal diameter + else: + class_letter = "g" # default class for larger diameters + return d, pitch, grade, class_letter + + # Semi-full format 2 (e.g. M10-6g) + elif ("×" not in designation) and ("-" in designation): + try: + before_dash, after_dash = designation.split("-") + d = self.validated(before_dash.replace("m", "")) + if d is None: + return "Invalid diameter format!" + elif not self.defaults["d"].eq(d).any(): + return "Diameter not available!" + else: + row=self.defaults[self.defaults["d"]==d] + pitch = row.iat[0, 1] + try: + grade = int(''.join(filter(str.isdigit, after_dash))) + if (grade < 3) or (grade > 9): + return "Invalid tolerance grade!" + except ValueError: + return "Invalid tolerance grade!" + + try: + class_letter = ''.join(filter(str.isalpha, after_dash)) + if len(class_letter) != 1: + return "Invalid class letter!" + except ValueError: + return "Invalid class letter!" + return d, pitch, grade, class_letter + except ValueError: + print("Error") + return None + + # Full format (e.g. M10×1.5-6g) + else: + try: + before_dash, after_dash = designation.split("-") + d = self.validated(before_dash.replace("m", "").split("×")[0]) + if d is None: + return "Invalid diameter format!" + elif not self.defaults["d"].eq(d).any(): + return "Diameter not available!" + pitch = self.validated(before_dash.split("×")[1]) + if pitch is None: + return "Invalid pitch format!" + elif not self.defaults["p"].eq(pitch).any(): + return "Pitch not available!" + elif pitch > self.defaults[self.defaults["d"]==d].iat[0, 1]: + return "Incompatible pitch for given diameter!" + grade = int(''.join(filter(str.isdigit, after_dash))) + if (grade < 3) or (grade > 9): + return "Invalid tolerance grade!" + class_letter = ''.join(filter(str.isalpha, after_dash)) + if len(class_letter) > 1: + return "Invalid class letter!" + return d, pitch, grade, class_letter + except Exception: + print("ERROR: invalid thread format.") + return None + + + + def parse_sl(self, sl): # sl = "start,length" + """Return (start and thread length).""" + + sl = sl.replace(" ", "") + + start = self.validated(sl.split(",")[0]) + if start is None: + return "Invalid start point" + + length = self.validated(sl.split(",")[1]) + if length is None: + return "Invalid length" + return start, length + + + + + + + + +class ThreadCalculator: + + # Functions for external thread + @staticmethod + def calc_depth(pitch, es, T_d): + return 0.6134 * pitch + 0.5*es + 0.25*T_d + + + ranges_fal = [ + ((0.5, 0.5), 0.06), + ((0.75, 0.75), 0.07), + ((0.8, 4), 0.08), + ((4.5, 6), 0.1) + ] + + @staticmethod + def cal_fal(pitch): + for (pmin, pmax), value in ThreadCalculator.ranges_fal: + if pmin <= pitch <= pmax: + return value + return None + + + ranges_noc = [ + ((0.5, 0.8), 4), + ((1, 1), 5), + ((1.25, 1.5), 6), + ((1.75, 2), 8), + ((2.5, 2.5), 10), + ((3, 3.5), 12), + ((4, 5), 14), + ((5.5, 6), 16) + ] + + @staticmethod + def calc_noc(pitch): + for (pmin, pmax), value in ThreadCalculator.ranges_noc: + if pmin <= pitch <= pmax: + return value + return None + + + # Functions for internal thread + + @staticmethod + def half_ceil(x): + return math.ceil(2*x)/2 + + @staticmethod + def depth_per_storke(depth): + if (depth > 0) and (depth <= 6): + return depth + elif depth > 6: + strokes = math.ceil(depth/6) + return math.ceil(depth/strokes) + else: + return None