From 677175183d03bd352d54a02ac7b93359846e997c Mon Sep 17 00:00:00 2001 From: cuban2 <147667036+cuban2@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:36:35 -0700 Subject: [PATCH 1/7] Expanded_main.py Change Log: Metric Thread Generator Script Moved all user inputs to the top of the script for easy access. Added clear START and END markers around the user input section. Replaced static pitch list with dynamic pitch range using pitch_start, pitch_end, and pitch_step. Created a function called generate_pitch_list() to calculate pitch values from the input range. Renamed designator() function to format_number() for clarity. Introduced a Thread class to store thread properties with named attributes. Modularized the code using named functions: generate_pitch_list() and generate_xml(). Grouped thread generation logic into a MetricThreadGenerator class. Made output filename customizable using output_filename variable. Added metadata fields to the XML output: Name, CustomName, Unit, Angle, SortOrder. Used ET.indent() to format the XML output (requires Python 3.9 or later). Added detailed comments throughout the script to explain each section. Included instructional examples in the input section to guide new users. Added a confirmation message at the end of the script to notify successful XML creation. Preserved original ISO metric thread geometry calculations (H, pitch diameter, minor diameter). Maintained original offset logic for internal and external threads. Preserved tap drill calculation for internal threads (D - P). Ensured compatibility with Python 3.9 and avoided advanced syntax to keep the script beginner-friendly. --- Expanded_main.py | 150 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 Expanded_main.py diff --git a/Expanded_main.py b/Expanded_main.py new file mode 100644 index 0000000..6785760 --- /dev/null +++ b/Expanded_main.py @@ -0,0 +1,150 @@ +# ๐Ÿš€ USER INPUT SECTION โ€” CHANGE THESE VALUES TO CUSTOMIZE YOUR THREADS + +# ๐Ÿ”ง Set the pitch range you want to generate: +pitch_start = .25 # Starting pitch value in mm (e.g., 1.0) +pitch_end = 3.5 # Ending pitch value in mm (e.g., 6.0) +pitch_step = 0.01 # Interval between pitches in mm (e.g., 0.5) + +# ๐Ÿ“ Set the thread diameters you want to include: +thread_sizes = list(range(8, 51)) # Diameters from 8mm to 50mm + +# โš™๏ธ Set tolerance offsets to simulate different thread classes: +tolerance_offsets = [0.0, 0.1, 0.2, 0.4, 0.8] + +# ๐Ÿงพ Set the name of the output XML file: +output_filename = "3DPrintedMetricV3.xml" + +# ๐Ÿงพ Metadata for the thread type +thread_name = "3D-Printed Metric Threads V3" +unit = "mm" +thread_angle = 60.0 # Standard ISO metric thread angle + +# ๐Ÿ“ฆ Import required modules +import math +import xml.etree.ElementTree as ET +from abc import ABC, abstractmethod + +# ๐Ÿ”ข Generate pitch list from user input +def generate_pitch_list(start, end, step): + pitches = [] + current = start + while current <= end + 1e-9: + pitches.append(round(current, 4)) + current += step + return pitches + +# ๐Ÿงผ Format numbers for display +def format_number(val): + return str(int(val)) if val == int(val) else str(val) + +# ๐Ÿงต Thread object to store dimensions +class Thread: + def __init__(self): + self.gender = None + self.thread_class = None + self.major_dia = 0.0 + self.pitch_dia = 0.0 + self.minor_dia = 0.0 + self.tap_drill = None + +# ๐Ÿงฉ Abstract base class +class ThreadProfile(ABC): + @abstractmethod + def sizes(self): + pass + + @abstractmethod + def designations(self, size): + pass + + @abstractmethod + def threads(self, designation): + pass + +# ๐Ÿงต Metric thread generator class +class MetricThreadGenerator(ThreadProfile): + class Designation: + def __init__(self, diameter, pitch): + self.nominal_diameter = diameter + self.pitch = pitch + self.name = f"M{format_number(diameter)}x{format_number(pitch)}" + + def __init__(self, pitch_list): + self.pitches = pitch_list + + def sizes(self): + return thread_sizes + + def designations(self, size): + return [self.Designation(size, pitch) for pitch in self.pitches] + + def threads(self, designation): + threads = [] + P = designation.pitch + D = designation.nominal_diameter + H = (P / 2) / math.tan(math.radians(thread_angle / 2)) + pitch_dia = D - 2 * (3 * H / 8) + minor_dia = D - 2 * (5 * H / 8) + + for offset in tolerance_offsets: + class_label = f"O.{str(offset)[2:]}" # e.g., 0.1 โ†’ "O.1" + + ext = Thread() + ext.gender = "external" + ext.thread_class = class_label + ext.major_dia = D - offset + ext.pitch_dia = pitch_dia - offset + ext.minor_dia = minor_dia - offset + threads.append(ext) + + int_thread = Thread() + int_thread.gender = "internal" + int_thread.thread_class = class_label + int_thread.major_dia = D + offset + int_thread.pitch_dia = pitch_dia + offset + int_thread.minor_dia = minor_dia + offset + int_thread.tap_drill = D - P + threads.append(int_thread) + + return threads + +# ๐Ÿงพ XML generator +def generate_xml(): + pitch_list = generate_pitch_list(pitch_start, pitch_end, pitch_step) + profile = MetricThreadGenerator(pitch_list) + + root = ET.Element("ThreadType") + tree = ET.ElementTree(root) + + ET.SubElement(root, "Name").text = thread_name + ET.SubElement(root, "CustomName").text = thread_name + ET.SubElement(root, "Unit").text = unit + ET.SubElement(root, "Angle").text = str(thread_angle) + ET.SubElement(root, "SortOrder").text = "3" + + for size in profile.sizes(): + size_elem = ET.SubElement(root, "ThreadSize") + ET.SubElement(size_elem, "Size").text = str(size) + for designation in profile.designations(size): + des_elem = ET.SubElement(size_elem, "Designation") + ET.SubElement(des_elem, "ThreadDesignation").text = designation.name + ET.SubElement(des_elem, "CTD").text = designation.name + ET.SubElement(des_elem, "Pitch").text = str(designation.pitch) + for thread in profile.threads(designation): + thread_elem = ET.SubElement(des_elem, "Thread") + ET.SubElement(thread_elem, "Gender").text = thread.gender + ET.SubElement(thread_elem, "Class").text = thread.thread_class + ET.SubElement(thread_elem, "MajorDia").text = f"{thread.major_dia:.4g}" + ET.SubElement(thread_elem, "PitchDia").text = f"{thread.pitch_dia:.4g}" + ET.SubElement(thread_elem, "MinorDia").text = f"{thread.minor_dia:.4g}" + if thread.tap_drill is not None: + ET.SubElement(thread_elem, "TapDrill").text = f"{thread.tap_drill:.4g}" + + ET.indent(tree) + tree.write(output_filename, encoding="UTF-8", xml_declaration=True) + +# ๐Ÿš€ Run the generator +if __name__ == "__main__": + generate_xml() + print(f"โœ… XML file '{output_filename}' created for pitches from {pitch_start} mm to {pitch_end} mm in {pitch_step} mm steps.") + From b6b4407a0fad181d96338aa849c2ce758851e602 Mon Sep 17 00:00:00 2001 From: cuban2 <147667036+cuban2@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:49:56 -0700 Subject: [PATCH 2/7] Add files via upload --- expand_threads.py | 257 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 expand_threads.py diff --git a/expand_threads.py b/expand_threads.py new file mode 100644 index 0000000..85ec1ec --- /dev/null +++ b/expand_threads.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +import xml.etree.ElementTree as ET +from xml.sax.saxutils import escape +from decimal import Decimal, getcontext +import os +import math + +# === CONFIGURATION === + +INPUT_FILE = "ISOMetricprofile.txt" # Base Fusion XML to clone/augment +OUTPUT_FILE = "3D_Printable_ThreadLibrary_MultiFit.txt" + +THREAD_ANGLE = Decimal("90.0") # 45ยฐ printable profile + +# Size/pitch ranges +SIZE_START = Decimal("5.0") +SIZE_END = Decimal("30.0") +SIZE_STEP = Decimal("1.0") + +PITCH_START = Decimal("0.5") +PITCH_END = Decimal("3.5") +PITCH_STEP = Decimal("0.1") + +# Fit types (tune for printer/material) +# min_clearance is radial (difference in diameters) +FIT_TYPES = { + "Functional": { + "min_clearance": Decimal("0.20"), + "es": Decimal("0.15"), # external upper deviation (shifts external down) + "Td": Decimal("0.20") # tolerance width proxy + }, + "Loose": { + "min_clearance": Decimal("0.30"), + "es": Decimal("0.10"), + "Td": Decimal("0.25") + } +} + +# Heuristic build limit to avoid ultra-tight helices that tend to fail in Fusion +MIN_PITCH_TO_DIAM_RATIO = Decimal("0.01") # allow smaller pitches than before + +# Numeric stability for Decimal ops +getcontext().prec = 12 + +# === GEOMETRY HELPERS === + +def thread_height(pitch: Decimal, angle_deg: Decimal) -> Decimal: + # H = 0.5 * P * cot(theta/2) + return Decimal("0.5") * pitch * Decimal(str(1 / math.tan(math.radians(float(angle_deg) / 2.0)))) + +def h3(pitch: Decimal, angle_deg: Decimal) -> Decimal: + # h3 = 5/8 * H + return Decimal("0.625") * thread_height(pitch, angle_deg) + +def crest_flat(pitch: Decimal, angle_deg: Decimal) -> Decimal: + # Clamp crest flat so it doesn't exceed half the thread height + cf = pitch / Decimal("8") + H = thread_height(pitch, angle_deg) + return cf if cf <= H / Decimal("2") else H / Decimal("2") + +def root_flat(pitch: Decimal) -> Decimal: + return pitch / Decimal("4") + +def frange(start: Decimal, stop: Decimal, step: Decimal): + cur = start + while cur <= stop + Decimal("0.0001"): + yield cur.quantize(Decimal("0.001")) + cur += step + +# === TOLERANCE CALCULATOR (scaled EI to help small pitches) === +# External: +# major_max = size - es +# pitch_max = size - es (proxy for tolerance band top) +# Internal: +# Choose EI so that internal_minor >= external_major + clearance +# EI_required = es + clearance - 2*h3 +# Also scale EI with pitch to keep small threads printable: +# EI_scaled = 0.05 + 0.5 * pitch (tunable) +# EI = max(EI_required, EI_scaled) +# We carry TD/Td as metadata; geometry uses top values to keep XML simple. + +def calculate_tolerances(size: Decimal, pitch: Decimal, fit: dict) -> dict: + h3_val = h3(pitch, THREAD_ANGLE) + es = fit["es"] + Td = fit["Td"] + clearance = fit["min_clearance"] + + EI_required = es + clearance - Decimal("2") * h3_val + EI_scaled = Decimal("0.05") + (Decimal("0.5") * pitch) + EI = EI_required if EI_required >= EI_scaled else EI_scaled + + return { + "external": {"es": es, "Td": Td}, + "internal": {"EI": EI, "TD": Td} + } + +# === BUILDABILITY CHECKS FOR EXTERNAL === + +def external_buildable(size: Decimal, pitch: Decimal, tol: dict, fit: dict) -> bool: + # Helix tightness check + if size > 0 and pitch < size * MIN_PITCH_TO_DIAM_RATIO: + return False + + # Clearance check: internal minor >= external major + min_clearance + h3_val = h3(pitch, THREAD_ANGLE) + minor_dia_nom = size - Decimal("2") * h3_val + external_major_max = size - tol["external"]["es"] + internal_minor_min = minor_dia_nom + tol["internal"]["EI"] + if internal_minor_min < external_major_max + fit["min_clearance"]: + return False + + # Crest flat sanity check + H = thread_height(pitch, THREAD_ANGLE) + if crest_flat(pitch, THREAD_ANGLE) > H / Decimal("2"): + return False + + return True + +# === THREAD XML BUILDERS === + +def make_thread(gender: str, size: Decimal, pitch: Decimal, tol: dict, fit_name: str) -> ET.Element: + H = thread_height(pitch, THREAD_ANGLE) + h3_val = h3(pitch, THREAD_ANGLE) + + pitch_dia_nom = (size - Decimal("0.75") * H).quantize(Decimal("0.000")) + minor_dia_nom = (size - Decimal("2") * h3_val).quantize(Decimal("0.000")) + + if gender == "external": + es = tol["external"]["es"] + # We publish the top-of-zone (max) values in the XML fields + major_max = (size - es).quantize(Decimal("0.000")) + pitch_max = (size - es).quantize(Decimal("0.000")) + minor = minor_dia_nom # geometry-based for modeled shape + flat_val = crest_flat(pitch, THREAD_ANGLE).quantize(Decimal("0.000")) + flat_tag = "CrestFlat" + maj_out = major_max + p_out = pitch_max + else: + EI = tol["internal"]["EI"] + TD = tol["internal"]["TD"] + major_min = (size + EI).quantize(Decimal("0.000")) + major_max = (major_min + TD).quantize(Decimal("0.000")) + pitch_max = (major_min + TD).quantize(Decimal("0.000")) + minor = (minor_dia_nom + EI).quantize(Decimal("0.000")) + flat_val = root_flat(pitch).quantize(Decimal("0.000")) + flat_tag = "RootFlat" + maj_out = major_max + p_out = pitch_max + + t = ET.Element("Thread") + ET.SubElement(t, "Gender").text = gender + ET.SubElement(t, "Class").text = fit_name + ET.SubElement(t, "MajorDia").text = f"{maj_out:.3f}" + ET.SubElement(t, "PitchDia").text = f"{p_out:.3f}" + ET.SubElement(t, "MinorDia").text = f"{minor:.3f}" + ET.SubElement(t, flat_tag).text = f"{flat_val:.3f}" + return t + +def make_designation(size: Decimal, pitch: Decimal) -> ET.Element: + size_str = f"{size:.3f}".rstrip("0").rstrip(".") + pitch_str = f"{pitch:.3f}".rstrip("0").rstrip(".") + label = f"M{size_str}x{pitch_str}" + + des = ET.Element("Designation") + ET.SubElement(des, "ThreadDesignation").text = label + ET.SubElement(des, "CTD").text = label + ET.SubElement(des, "Pitch").text = pitch_str + + for fit_name, fit in FIT_TYPES.items(): + tol = calculate_tolerances(size, pitch, fit) + + # External-first policy: if external fails, skip the pair (as requested) + if not external_buildable(size, pitch, tol, fit): + # Optional: uncomment for debug logging + # print(f"Skip M{size}x{pitch} [{fit_name}] - external failed checks") + continue + + ext_thread = make_thread("external", size, pitch, tol, fit_name) + int_thread = make_thread("internal", size, pitch, tol, fit_name) + + des.append(ext_thread) + des.append(int_thread) + + return des + +def make_thread_size(size: Decimal) -> ET.Element: + ts = ET.Element("ThreadSize") + ET.SubElement(ts, "Size").text = f"{size:.3f}".rstrip("0").rstrip(".") + for pitch in frange(PITCH_START, PITCH_END, PITCH_STEP): + des = make_designation(size, pitch) + # Only append designation if it contains at least one valid thread pair + if list(des.findall("Thread")): + ts.append(des) + return ts + +# === XML WRITER (tabs + newlines) === + +def serialize_elem(elem: ET.Element, level: int = 0) -> str: + indent = "\t" * level + tag = elem.tag + text = (elem.text or "").strip() + children = list(elem) + + if not children: + return f"{indent}<{tag}>{escape(text)}" if text else f"{indent}<{tag}>" + + lines = [f"{indent}<{tag}>"] + if text: + lines.append(f"{indent}\t{escape(text)}") + for child in children: + lines.append(serialize_elem(child, level + 1)) + lines.append(f"{indent}") + return "\n".join(lines) + +def write_xml_with_tabs(root: ET.Element, output_file: str): + xml_header = "\n" + body = serialize_elem(root, 0) + with open(output_file, "w", encoding="utf-8") as f: + f.write(xml_header + body) + +# === MAIN === + +def main(): + tree = ET.parse(INPUT_FILE) + root = tree.getroot() + + # Reset key metadata + for tag in ("Name", "CustomName", "Angle", "Unit", "SortOrder"): + node = root.find(tag) + if node is not None: + root.remove(node) + + ET.SubElement(root, "Name").text = "3D Printable Metric Profile (Multi-Fit, Safe External)" + ET.SubElement(root, "CustomName").text = "Threads โ€” Functional & Loose (skip unbuildable pairs)" + ET.SubElement(root, "Unit").text = "mm" + ET.SubElement(root, "Angle").text = f"{THREAD_ANGLE:.1f}" + ET.SubElement(root, "SortOrder").text = "3" + + # Remove existing sizes + for ts in list(root.findall("ThreadSize")): + root.remove(ts) + + # Build sizes + any_sizes = False + for size in frange(SIZE_START, SIZE_END, SIZE_STEP): + ts_elem = make_thread_size(size) + if list(ts_elem.findall("Designation")): + root.append(ts_elem) + any_sizes = True + + write_xml_with_tabs(root, OUTPUT_FILE) + print(f"โœ… Wrote: {os.path.abspath(OUTPUT_FILE)}") + if not any_sizes: + print("โš ๏ธ No valid thread pairs generated. Consider loosening fit settings or angle/pitch limits.") + +if __name__ == "__main__": + main() From 084d2eddade6906306d08341c062187a7dd5b624 Mon Sep 17 00:00:00 2001 From: cuban2 <147667036+cuban2@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:55:33 -0700 Subject: [PATCH 3/7] Delete Expanded_main.py --- Expanded_main.py | 150 ----------------------------------------------- 1 file changed, 150 deletions(-) delete mode 100644 Expanded_main.py diff --git a/Expanded_main.py b/Expanded_main.py deleted file mode 100644 index 6785760..0000000 --- a/Expanded_main.py +++ /dev/null @@ -1,150 +0,0 @@ -# ๐Ÿš€ USER INPUT SECTION โ€” CHANGE THESE VALUES TO CUSTOMIZE YOUR THREADS - -# ๐Ÿ”ง Set the pitch range you want to generate: -pitch_start = .25 # Starting pitch value in mm (e.g., 1.0) -pitch_end = 3.5 # Ending pitch value in mm (e.g., 6.0) -pitch_step = 0.01 # Interval between pitches in mm (e.g., 0.5) - -# ๐Ÿ“ Set the thread diameters you want to include: -thread_sizes = list(range(8, 51)) # Diameters from 8mm to 50mm - -# โš™๏ธ Set tolerance offsets to simulate different thread classes: -tolerance_offsets = [0.0, 0.1, 0.2, 0.4, 0.8] - -# ๐Ÿงพ Set the name of the output XML file: -output_filename = "3DPrintedMetricV3.xml" - -# ๐Ÿงพ Metadata for the thread type -thread_name = "3D-Printed Metric Threads V3" -unit = "mm" -thread_angle = 60.0 # Standard ISO metric thread angle - -# ๐Ÿ“ฆ Import required modules -import math -import xml.etree.ElementTree as ET -from abc import ABC, abstractmethod - -# ๐Ÿ”ข Generate pitch list from user input -def generate_pitch_list(start, end, step): - pitches = [] - current = start - while current <= end + 1e-9: - pitches.append(round(current, 4)) - current += step - return pitches - -# ๐Ÿงผ Format numbers for display -def format_number(val): - return str(int(val)) if val == int(val) else str(val) - -# ๐Ÿงต Thread object to store dimensions -class Thread: - def __init__(self): - self.gender = None - self.thread_class = None - self.major_dia = 0.0 - self.pitch_dia = 0.0 - self.minor_dia = 0.0 - self.tap_drill = None - -# ๐Ÿงฉ Abstract base class -class ThreadProfile(ABC): - @abstractmethod - def sizes(self): - pass - - @abstractmethod - def designations(self, size): - pass - - @abstractmethod - def threads(self, designation): - pass - -# ๐Ÿงต Metric thread generator class -class MetricThreadGenerator(ThreadProfile): - class Designation: - def __init__(self, diameter, pitch): - self.nominal_diameter = diameter - self.pitch = pitch - self.name = f"M{format_number(diameter)}x{format_number(pitch)}" - - def __init__(self, pitch_list): - self.pitches = pitch_list - - def sizes(self): - return thread_sizes - - def designations(self, size): - return [self.Designation(size, pitch) for pitch in self.pitches] - - def threads(self, designation): - threads = [] - P = designation.pitch - D = designation.nominal_diameter - H = (P / 2) / math.tan(math.radians(thread_angle / 2)) - pitch_dia = D - 2 * (3 * H / 8) - minor_dia = D - 2 * (5 * H / 8) - - for offset in tolerance_offsets: - class_label = f"O.{str(offset)[2:]}" # e.g., 0.1 โ†’ "O.1" - - ext = Thread() - ext.gender = "external" - ext.thread_class = class_label - ext.major_dia = D - offset - ext.pitch_dia = pitch_dia - offset - ext.minor_dia = minor_dia - offset - threads.append(ext) - - int_thread = Thread() - int_thread.gender = "internal" - int_thread.thread_class = class_label - int_thread.major_dia = D + offset - int_thread.pitch_dia = pitch_dia + offset - int_thread.minor_dia = minor_dia + offset - int_thread.tap_drill = D - P - threads.append(int_thread) - - return threads - -# ๐Ÿงพ XML generator -def generate_xml(): - pitch_list = generate_pitch_list(pitch_start, pitch_end, pitch_step) - profile = MetricThreadGenerator(pitch_list) - - root = ET.Element("ThreadType") - tree = ET.ElementTree(root) - - ET.SubElement(root, "Name").text = thread_name - ET.SubElement(root, "CustomName").text = thread_name - ET.SubElement(root, "Unit").text = unit - ET.SubElement(root, "Angle").text = str(thread_angle) - ET.SubElement(root, "SortOrder").text = "3" - - for size in profile.sizes(): - size_elem = ET.SubElement(root, "ThreadSize") - ET.SubElement(size_elem, "Size").text = str(size) - for designation in profile.designations(size): - des_elem = ET.SubElement(size_elem, "Designation") - ET.SubElement(des_elem, "ThreadDesignation").text = designation.name - ET.SubElement(des_elem, "CTD").text = designation.name - ET.SubElement(des_elem, "Pitch").text = str(designation.pitch) - for thread in profile.threads(designation): - thread_elem = ET.SubElement(des_elem, "Thread") - ET.SubElement(thread_elem, "Gender").text = thread.gender - ET.SubElement(thread_elem, "Class").text = thread.thread_class - ET.SubElement(thread_elem, "MajorDia").text = f"{thread.major_dia:.4g}" - ET.SubElement(thread_elem, "PitchDia").text = f"{thread.pitch_dia:.4g}" - ET.SubElement(thread_elem, "MinorDia").text = f"{thread.minor_dia:.4g}" - if thread.tap_drill is not None: - ET.SubElement(thread_elem, "TapDrill").text = f"{thread.tap_drill:.4g}" - - ET.indent(tree) - tree.write(output_filename, encoding="UTF-8", xml_declaration=True) - -# ๐Ÿš€ Run the generator -if __name__ == "__main__": - generate_xml() - print(f"โœ… XML file '{output_filename}' created for pitches from {pitch_start} mm to {pitch_end} mm in {pitch_step} mm steps.") - From 2b28b2c85e5370082db261775f13c5477691d818 Mon Sep 17 00:00:00 2001 From: cuban2 <147667036+cuban2@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:57:56 -0700 Subject: [PATCH 4/7] Delete expand_threads.py --- expand_threads.py | 257 ---------------------------------------------- 1 file changed, 257 deletions(-) delete mode 100644 expand_threads.py diff --git a/expand_threads.py b/expand_threads.py deleted file mode 100644 index 85ec1ec..0000000 --- a/expand_threads.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/env python3 -import xml.etree.ElementTree as ET -from xml.sax.saxutils import escape -from decimal import Decimal, getcontext -import os -import math - -# === CONFIGURATION === - -INPUT_FILE = "ISOMetricprofile.txt" # Base Fusion XML to clone/augment -OUTPUT_FILE = "3D_Printable_ThreadLibrary_MultiFit.txt" - -THREAD_ANGLE = Decimal("90.0") # 45ยฐ printable profile - -# Size/pitch ranges -SIZE_START = Decimal("5.0") -SIZE_END = Decimal("30.0") -SIZE_STEP = Decimal("1.0") - -PITCH_START = Decimal("0.5") -PITCH_END = Decimal("3.5") -PITCH_STEP = Decimal("0.1") - -# Fit types (tune for printer/material) -# min_clearance is radial (difference in diameters) -FIT_TYPES = { - "Functional": { - "min_clearance": Decimal("0.20"), - "es": Decimal("0.15"), # external upper deviation (shifts external down) - "Td": Decimal("0.20") # tolerance width proxy - }, - "Loose": { - "min_clearance": Decimal("0.30"), - "es": Decimal("0.10"), - "Td": Decimal("0.25") - } -} - -# Heuristic build limit to avoid ultra-tight helices that tend to fail in Fusion -MIN_PITCH_TO_DIAM_RATIO = Decimal("0.01") # allow smaller pitches than before - -# Numeric stability for Decimal ops -getcontext().prec = 12 - -# === GEOMETRY HELPERS === - -def thread_height(pitch: Decimal, angle_deg: Decimal) -> Decimal: - # H = 0.5 * P * cot(theta/2) - return Decimal("0.5") * pitch * Decimal(str(1 / math.tan(math.radians(float(angle_deg) / 2.0)))) - -def h3(pitch: Decimal, angle_deg: Decimal) -> Decimal: - # h3 = 5/8 * H - return Decimal("0.625") * thread_height(pitch, angle_deg) - -def crest_flat(pitch: Decimal, angle_deg: Decimal) -> Decimal: - # Clamp crest flat so it doesn't exceed half the thread height - cf = pitch / Decimal("8") - H = thread_height(pitch, angle_deg) - return cf if cf <= H / Decimal("2") else H / Decimal("2") - -def root_flat(pitch: Decimal) -> Decimal: - return pitch / Decimal("4") - -def frange(start: Decimal, stop: Decimal, step: Decimal): - cur = start - while cur <= stop + Decimal("0.0001"): - yield cur.quantize(Decimal("0.001")) - cur += step - -# === TOLERANCE CALCULATOR (scaled EI to help small pitches) === -# External: -# major_max = size - es -# pitch_max = size - es (proxy for tolerance band top) -# Internal: -# Choose EI so that internal_minor >= external_major + clearance -# EI_required = es + clearance - 2*h3 -# Also scale EI with pitch to keep small threads printable: -# EI_scaled = 0.05 + 0.5 * pitch (tunable) -# EI = max(EI_required, EI_scaled) -# We carry TD/Td as metadata; geometry uses top values to keep XML simple. - -def calculate_tolerances(size: Decimal, pitch: Decimal, fit: dict) -> dict: - h3_val = h3(pitch, THREAD_ANGLE) - es = fit["es"] - Td = fit["Td"] - clearance = fit["min_clearance"] - - EI_required = es + clearance - Decimal("2") * h3_val - EI_scaled = Decimal("0.05") + (Decimal("0.5") * pitch) - EI = EI_required if EI_required >= EI_scaled else EI_scaled - - return { - "external": {"es": es, "Td": Td}, - "internal": {"EI": EI, "TD": Td} - } - -# === BUILDABILITY CHECKS FOR EXTERNAL === - -def external_buildable(size: Decimal, pitch: Decimal, tol: dict, fit: dict) -> bool: - # Helix tightness check - if size > 0 and pitch < size * MIN_PITCH_TO_DIAM_RATIO: - return False - - # Clearance check: internal minor >= external major + min_clearance - h3_val = h3(pitch, THREAD_ANGLE) - minor_dia_nom = size - Decimal("2") * h3_val - external_major_max = size - tol["external"]["es"] - internal_minor_min = minor_dia_nom + tol["internal"]["EI"] - if internal_minor_min < external_major_max + fit["min_clearance"]: - return False - - # Crest flat sanity check - H = thread_height(pitch, THREAD_ANGLE) - if crest_flat(pitch, THREAD_ANGLE) > H / Decimal("2"): - return False - - return True - -# === THREAD XML BUILDERS === - -def make_thread(gender: str, size: Decimal, pitch: Decimal, tol: dict, fit_name: str) -> ET.Element: - H = thread_height(pitch, THREAD_ANGLE) - h3_val = h3(pitch, THREAD_ANGLE) - - pitch_dia_nom = (size - Decimal("0.75") * H).quantize(Decimal("0.000")) - minor_dia_nom = (size - Decimal("2") * h3_val).quantize(Decimal("0.000")) - - if gender == "external": - es = tol["external"]["es"] - # We publish the top-of-zone (max) values in the XML fields - major_max = (size - es).quantize(Decimal("0.000")) - pitch_max = (size - es).quantize(Decimal("0.000")) - minor = minor_dia_nom # geometry-based for modeled shape - flat_val = crest_flat(pitch, THREAD_ANGLE).quantize(Decimal("0.000")) - flat_tag = "CrestFlat" - maj_out = major_max - p_out = pitch_max - else: - EI = tol["internal"]["EI"] - TD = tol["internal"]["TD"] - major_min = (size + EI).quantize(Decimal("0.000")) - major_max = (major_min + TD).quantize(Decimal("0.000")) - pitch_max = (major_min + TD).quantize(Decimal("0.000")) - minor = (minor_dia_nom + EI).quantize(Decimal("0.000")) - flat_val = root_flat(pitch).quantize(Decimal("0.000")) - flat_tag = "RootFlat" - maj_out = major_max - p_out = pitch_max - - t = ET.Element("Thread") - ET.SubElement(t, "Gender").text = gender - ET.SubElement(t, "Class").text = fit_name - ET.SubElement(t, "MajorDia").text = f"{maj_out:.3f}" - ET.SubElement(t, "PitchDia").text = f"{p_out:.3f}" - ET.SubElement(t, "MinorDia").text = f"{minor:.3f}" - ET.SubElement(t, flat_tag).text = f"{flat_val:.3f}" - return t - -def make_designation(size: Decimal, pitch: Decimal) -> ET.Element: - size_str = f"{size:.3f}".rstrip("0").rstrip(".") - pitch_str = f"{pitch:.3f}".rstrip("0").rstrip(".") - label = f"M{size_str}x{pitch_str}" - - des = ET.Element("Designation") - ET.SubElement(des, "ThreadDesignation").text = label - ET.SubElement(des, "CTD").text = label - ET.SubElement(des, "Pitch").text = pitch_str - - for fit_name, fit in FIT_TYPES.items(): - tol = calculate_tolerances(size, pitch, fit) - - # External-first policy: if external fails, skip the pair (as requested) - if not external_buildable(size, pitch, tol, fit): - # Optional: uncomment for debug logging - # print(f"Skip M{size}x{pitch} [{fit_name}] - external failed checks") - continue - - ext_thread = make_thread("external", size, pitch, tol, fit_name) - int_thread = make_thread("internal", size, pitch, tol, fit_name) - - des.append(ext_thread) - des.append(int_thread) - - return des - -def make_thread_size(size: Decimal) -> ET.Element: - ts = ET.Element("ThreadSize") - ET.SubElement(ts, "Size").text = f"{size:.3f}".rstrip("0").rstrip(".") - for pitch in frange(PITCH_START, PITCH_END, PITCH_STEP): - des = make_designation(size, pitch) - # Only append designation if it contains at least one valid thread pair - if list(des.findall("Thread")): - ts.append(des) - return ts - -# === XML WRITER (tabs + newlines) === - -def serialize_elem(elem: ET.Element, level: int = 0) -> str: - indent = "\t" * level - tag = elem.tag - text = (elem.text or "").strip() - children = list(elem) - - if not children: - return f"{indent}<{tag}>{escape(text)}" if text else f"{indent}<{tag}>" - - lines = [f"{indent}<{tag}>"] - if text: - lines.append(f"{indent}\t{escape(text)}") - for child in children: - lines.append(serialize_elem(child, level + 1)) - lines.append(f"{indent}") - return "\n".join(lines) - -def write_xml_with_tabs(root: ET.Element, output_file: str): - xml_header = "\n" - body = serialize_elem(root, 0) - with open(output_file, "w", encoding="utf-8") as f: - f.write(xml_header + body) - -# === MAIN === - -def main(): - tree = ET.parse(INPUT_FILE) - root = tree.getroot() - - # Reset key metadata - for tag in ("Name", "CustomName", "Angle", "Unit", "SortOrder"): - node = root.find(tag) - if node is not None: - root.remove(node) - - ET.SubElement(root, "Name").text = "3D Printable Metric Profile (Multi-Fit, Safe External)" - ET.SubElement(root, "CustomName").text = "Threads โ€” Functional & Loose (skip unbuildable pairs)" - ET.SubElement(root, "Unit").text = "mm" - ET.SubElement(root, "Angle").text = f"{THREAD_ANGLE:.1f}" - ET.SubElement(root, "SortOrder").text = "3" - - # Remove existing sizes - for ts in list(root.findall("ThreadSize")): - root.remove(ts) - - # Build sizes - any_sizes = False - for size in frange(SIZE_START, SIZE_END, SIZE_STEP): - ts_elem = make_thread_size(size) - if list(ts_elem.findall("Designation")): - root.append(ts_elem) - any_sizes = True - - write_xml_with_tabs(root, OUTPUT_FILE) - print(f"โœ… Wrote: {os.path.abspath(OUTPUT_FILE)}") - if not any_sizes: - print("โš ๏ธ No valid thread pairs generated. Consider loosening fit settings or angle/pitch limits.") - -if __name__ == "__main__": - main() From 4bf1f9cdf3c8b216e7b9559f13aa74a41607b59b Mon Sep 17 00:00:00 2001 From: cuban2 <147667036+cuban2@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:58:36 -0700 Subject: [PATCH 5/7] Add files via upload --- Expanded_3DPrintable_CutomeThreads.py | 161 ++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 Expanded_3DPrintable_CutomeThreads.py diff --git a/Expanded_3DPrintable_CutomeThreads.py b/Expanded_3DPrintable_CutomeThreads.py new file mode 100644 index 0000000..5853870 --- /dev/null +++ b/Expanded_3DPrintable_CutomeThreads.py @@ -0,0 +1,161 @@ +# ============================================================ +# ๐Ÿš€ USER INPUT SECTION โ€” START HERE +# Change the values below to customize your thread generation +# ============================================================ + +# ๐Ÿ”ง Pitch range settings (in millimeters) +pitch_start = 1.0 # Starting pitch value (e.g., 1.0 mm) +pitch_end = 6.0 # Ending pitch value (e.g., 6.0 mm) +pitch_step = 0.5 # Interval between pitches (e.g., 0.5 mm) + +# ๐Ÿ“ Thread diameters to include (in millimeters) +thread_sizes = list(range(8, 51)) # Diameters from 8 mm to 50 mm + +# โš™๏ธ Tolerance offsets to simulate different thread classes +tolerance_offsets = [0.0, 0.1, 0.2, 0.4, 0.8] + +# ๐Ÿงพ Output file name +output_filename = "3DPrintedMetricThreads.xml" + +# ๐Ÿ“‹ Metadata for the thread type +thread_name = "3D-Printed Metric Threads V3" +unit = "mm" +thread_angle = 60.0 # Standard ISO metric thread angle + +# ============================================================ +# ๐Ÿšซ USER INPUT SECTION โ€” END HERE +# You don't need to change anything below this line +# ============================================================ + +import math +import xml.etree.ElementTree as ET +from abc import ABC, abstractmethod + +# ๐Ÿ”ข Generate pitch values from user-defined range +def generate_pitch_list(start, end, step): + pitches = [] + current = start + while current <= end + 1e-9: + pitches.append(round(current, 4)) + current += step + return pitches + +# ๐Ÿงผ Format numbers for display (e.g., 5.0 โ†’ "5") +def format_number(val): + return str(int(val)) if val == int(val) else str(val) + +# ๐Ÿงต Thread object to store calculated dimensions +class Thread: + def __init__(self): + self.gender = None + self.thread_class = None + self.major_dia = 0.0 + self.pitch_dia = 0.0 + self.minor_dia = 0.0 + self.tap_drill = None + +# ๐Ÿงฉ Abstract base class for thread profiles +class ThreadProfile(ABC): + @abstractmethod + def sizes(self): + pass + + @abstractmethod + def designations(self, size): + pass + + @abstractmethod + def threads(self, designation): + pass + +# ๐Ÿงต Metric thread generator class +class MetricThreadGenerator(ThreadProfile): + class Designation: + def __init__(self, diameter, pitch): + self.nominal_diameter = diameter + self.pitch = pitch + self.name = f"M{format_number(diameter)}x{format_number(pitch)}" + + def __init__(self, pitch_list): + self.pitches = pitch_list + + def sizes(self): + return thread_sizes + + def designations(self, size): + return [self.Designation(size, pitch) for pitch in self.pitches] + + def threads(self, designation): + threads = [] + P = designation.pitch + D = designation.nominal_diameter + H = (P / 2) / math.tan(math.radians(thread_angle / 2)) + pitch_dia = D - 2 * (3 * H / 8) + minor_dia = D - 2 * (5 * H / 8) + + for offset in tolerance_offsets: + class_label = f"O.{str(offset)[2:]}" # e.g., 0.1 โ†’ "O.1" + + # External thread + ext = Thread() + ext.gender = "external" + ext.thread_class = class_label + ext.major_dia = D - offset + ext.pitch_dia = pitch_dia - offset + ext.minor_dia = minor_dia - offset + threads.append(ext) + + # Internal thread + int_thread = Thread() + int_thread.gender = "internal" + int_thread.thread_class = class_label + int_thread.major_dia = D + offset + int_thread.pitch_dia = pitch_dia + offset + int_thread.minor_dia = minor_dia + offset + int_thread.tap_drill = D - P + threads.append(int_thread) + + return threads + +# ๐Ÿงพ XML generator function +def generate_xml(): + pitch_list = generate_pitch_list(pitch_start, pitch_end, pitch_step) + profile = MetricThreadGenerator(pitch_list) + + root = ET.Element("ThreadType") + tree = ET.ElementTree(root) + + # Add metadata + ET.SubElement(root, "Name").text = thread_name + ET.SubElement(root, "CustomName").text = thread_name + ET.SubElement(root, "Unit").text = unit + ET.SubElement(root, "Angle").text = str(thread_angle) + ET.SubElement(root, "SortOrder").text = "3" + + # Add thread sizes and designations + for size in profile.sizes(): + size_elem = ET.SubElement(root, "ThreadSize") + ET.SubElement(size_elem, "Size").text = str(size) + for designation in profile.designations(size): + des_elem = ET.SubElement(size_elem, "Designation") + ET.SubElement(des_elem, "ThreadDesignation").text = designation.name + ET.SubElement(des_elem, "CTD").text = designation.name + ET.SubElement(des_elem, "Pitch").text = str(designation.pitch) + for thread in profile.threads(designation): + thread_elem = ET.SubElement(des_elem, "Thread") + ET.SubElement(thread_elem, "Gender").text = thread.gender + ET.SubElement(thread_elem, "Class").text = thread.thread_class + ET.SubElement(thread_elem, "MajorDia").text = f"{thread.major_dia:.4g}" + ET.SubElement(thread_elem, "PitchDia").text = f"{thread.pitch_dia:.4g}" + ET.SubElement(thread_elem, "MinorDia").text = f"{thread.minor_dia:.4g}" + if thread.tap_drill is not None: + ET.SubElement(thread_elem, "TapDrill").text = f"{thread.tap_drill:.4g}" + + ET.indent(tree) + tree.write(output_filename, encoding="UTF-8", xml_declaration=True) + +# ๐Ÿš€ Run the generator +if __name__ == "__main__": + generate_xml() + print(f"โœ… XML file '{output_filename}' created for pitches from {pitch_start} mm to {pitch_end} mm in {pitch_step} mm steps.") + From b0953da6d19b04fc4f4951ce9bedfa2450bb5a26 Mon Sep 17 00:00:00 2001 From: cuban2 <147667036+cuban2@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:01:11 -0700 Subject: [PATCH 6/7] Delete Expanded_3DPrintable_CutomeThreads.py --- Expanded_3DPrintable_CutomeThreads.py | 161 -------------------------- 1 file changed, 161 deletions(-) delete mode 100644 Expanded_3DPrintable_CutomeThreads.py diff --git a/Expanded_3DPrintable_CutomeThreads.py b/Expanded_3DPrintable_CutomeThreads.py deleted file mode 100644 index 5853870..0000000 --- a/Expanded_3DPrintable_CutomeThreads.py +++ /dev/null @@ -1,161 +0,0 @@ -# ============================================================ -# ๐Ÿš€ USER INPUT SECTION โ€” START HERE -# Change the values below to customize your thread generation -# ============================================================ - -# ๐Ÿ”ง Pitch range settings (in millimeters) -pitch_start = 1.0 # Starting pitch value (e.g., 1.0 mm) -pitch_end = 6.0 # Ending pitch value (e.g., 6.0 mm) -pitch_step = 0.5 # Interval between pitches (e.g., 0.5 mm) - -# ๐Ÿ“ Thread diameters to include (in millimeters) -thread_sizes = list(range(8, 51)) # Diameters from 8 mm to 50 mm - -# โš™๏ธ Tolerance offsets to simulate different thread classes -tolerance_offsets = [0.0, 0.1, 0.2, 0.4, 0.8] - -# ๐Ÿงพ Output file name -output_filename = "3DPrintedMetricThreads.xml" - -# ๐Ÿ“‹ Metadata for the thread type -thread_name = "3D-Printed Metric Threads V3" -unit = "mm" -thread_angle = 60.0 # Standard ISO metric thread angle - -# ============================================================ -# ๐Ÿšซ USER INPUT SECTION โ€” END HERE -# You don't need to change anything below this line -# ============================================================ - -import math -import xml.etree.ElementTree as ET -from abc import ABC, abstractmethod - -# ๐Ÿ”ข Generate pitch values from user-defined range -def generate_pitch_list(start, end, step): - pitches = [] - current = start - while current <= end + 1e-9: - pitches.append(round(current, 4)) - current += step - return pitches - -# ๐Ÿงผ Format numbers for display (e.g., 5.0 โ†’ "5") -def format_number(val): - return str(int(val)) if val == int(val) else str(val) - -# ๐Ÿงต Thread object to store calculated dimensions -class Thread: - def __init__(self): - self.gender = None - self.thread_class = None - self.major_dia = 0.0 - self.pitch_dia = 0.0 - self.minor_dia = 0.0 - self.tap_drill = None - -# ๐Ÿงฉ Abstract base class for thread profiles -class ThreadProfile(ABC): - @abstractmethod - def sizes(self): - pass - - @abstractmethod - def designations(self, size): - pass - - @abstractmethod - def threads(self, designation): - pass - -# ๐Ÿงต Metric thread generator class -class MetricThreadGenerator(ThreadProfile): - class Designation: - def __init__(self, diameter, pitch): - self.nominal_diameter = diameter - self.pitch = pitch - self.name = f"M{format_number(diameter)}x{format_number(pitch)}" - - def __init__(self, pitch_list): - self.pitches = pitch_list - - def sizes(self): - return thread_sizes - - def designations(self, size): - return [self.Designation(size, pitch) for pitch in self.pitches] - - def threads(self, designation): - threads = [] - P = designation.pitch - D = designation.nominal_diameter - H = (P / 2) / math.tan(math.radians(thread_angle / 2)) - pitch_dia = D - 2 * (3 * H / 8) - minor_dia = D - 2 * (5 * H / 8) - - for offset in tolerance_offsets: - class_label = f"O.{str(offset)[2:]}" # e.g., 0.1 โ†’ "O.1" - - # External thread - ext = Thread() - ext.gender = "external" - ext.thread_class = class_label - ext.major_dia = D - offset - ext.pitch_dia = pitch_dia - offset - ext.minor_dia = minor_dia - offset - threads.append(ext) - - # Internal thread - int_thread = Thread() - int_thread.gender = "internal" - int_thread.thread_class = class_label - int_thread.major_dia = D + offset - int_thread.pitch_dia = pitch_dia + offset - int_thread.minor_dia = minor_dia + offset - int_thread.tap_drill = D - P - threads.append(int_thread) - - return threads - -# ๐Ÿงพ XML generator function -def generate_xml(): - pitch_list = generate_pitch_list(pitch_start, pitch_end, pitch_step) - profile = MetricThreadGenerator(pitch_list) - - root = ET.Element("ThreadType") - tree = ET.ElementTree(root) - - # Add metadata - ET.SubElement(root, "Name").text = thread_name - ET.SubElement(root, "CustomName").text = thread_name - ET.SubElement(root, "Unit").text = unit - ET.SubElement(root, "Angle").text = str(thread_angle) - ET.SubElement(root, "SortOrder").text = "3" - - # Add thread sizes and designations - for size in profile.sizes(): - size_elem = ET.SubElement(root, "ThreadSize") - ET.SubElement(size_elem, "Size").text = str(size) - for designation in profile.designations(size): - des_elem = ET.SubElement(size_elem, "Designation") - ET.SubElement(des_elem, "ThreadDesignation").text = designation.name - ET.SubElement(des_elem, "CTD").text = designation.name - ET.SubElement(des_elem, "Pitch").text = str(designation.pitch) - for thread in profile.threads(designation): - thread_elem = ET.SubElement(des_elem, "Thread") - ET.SubElement(thread_elem, "Gender").text = thread.gender - ET.SubElement(thread_elem, "Class").text = thread.thread_class - ET.SubElement(thread_elem, "MajorDia").text = f"{thread.major_dia:.4g}" - ET.SubElement(thread_elem, "PitchDia").text = f"{thread.pitch_dia:.4g}" - ET.SubElement(thread_elem, "MinorDia").text = f"{thread.minor_dia:.4g}" - if thread.tap_drill is not None: - ET.SubElement(thread_elem, "TapDrill").text = f"{thread.tap_drill:.4g}" - - ET.indent(tree) - tree.write(output_filename, encoding="UTF-8", xml_declaration=True) - -# ๐Ÿš€ Run the generator -if __name__ == "__main__": - generate_xml() - print(f"โœ… XML file '{output_filename}' created for pitches from {pitch_start} mm to {pitch_end} mm in {pitch_step} mm steps.") - From 0da427303ee66f2ab9868e9e8a0d758808ba42ee Mon Sep 17 00:00:00 2001 From: cuban2 <147667036+cuban2@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:01:28 -0700 Subject: [PATCH 7/7] Add files via upload --- Expanded_3DPrintable_CustomThreads.py | 161 ++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 Expanded_3DPrintable_CustomThreads.py diff --git a/Expanded_3DPrintable_CustomThreads.py b/Expanded_3DPrintable_CustomThreads.py new file mode 100644 index 0000000..5853870 --- /dev/null +++ b/Expanded_3DPrintable_CustomThreads.py @@ -0,0 +1,161 @@ +# ============================================================ +# ๐Ÿš€ USER INPUT SECTION โ€” START HERE +# Change the values below to customize your thread generation +# ============================================================ + +# ๐Ÿ”ง Pitch range settings (in millimeters) +pitch_start = 1.0 # Starting pitch value (e.g., 1.0 mm) +pitch_end = 6.0 # Ending pitch value (e.g., 6.0 mm) +pitch_step = 0.5 # Interval between pitches (e.g., 0.5 mm) + +# ๐Ÿ“ Thread diameters to include (in millimeters) +thread_sizes = list(range(8, 51)) # Diameters from 8 mm to 50 mm + +# โš™๏ธ Tolerance offsets to simulate different thread classes +tolerance_offsets = [0.0, 0.1, 0.2, 0.4, 0.8] + +# ๐Ÿงพ Output file name +output_filename = "3DPrintedMetricThreads.xml" + +# ๐Ÿ“‹ Metadata for the thread type +thread_name = "3D-Printed Metric Threads V3" +unit = "mm" +thread_angle = 60.0 # Standard ISO metric thread angle + +# ============================================================ +# ๐Ÿšซ USER INPUT SECTION โ€” END HERE +# You don't need to change anything below this line +# ============================================================ + +import math +import xml.etree.ElementTree as ET +from abc import ABC, abstractmethod + +# ๐Ÿ”ข Generate pitch values from user-defined range +def generate_pitch_list(start, end, step): + pitches = [] + current = start + while current <= end + 1e-9: + pitches.append(round(current, 4)) + current += step + return pitches + +# ๐Ÿงผ Format numbers for display (e.g., 5.0 โ†’ "5") +def format_number(val): + return str(int(val)) if val == int(val) else str(val) + +# ๐Ÿงต Thread object to store calculated dimensions +class Thread: + def __init__(self): + self.gender = None + self.thread_class = None + self.major_dia = 0.0 + self.pitch_dia = 0.0 + self.minor_dia = 0.0 + self.tap_drill = None + +# ๐Ÿงฉ Abstract base class for thread profiles +class ThreadProfile(ABC): + @abstractmethod + def sizes(self): + pass + + @abstractmethod + def designations(self, size): + pass + + @abstractmethod + def threads(self, designation): + pass + +# ๐Ÿงต Metric thread generator class +class MetricThreadGenerator(ThreadProfile): + class Designation: + def __init__(self, diameter, pitch): + self.nominal_diameter = diameter + self.pitch = pitch + self.name = f"M{format_number(diameter)}x{format_number(pitch)}" + + def __init__(self, pitch_list): + self.pitches = pitch_list + + def sizes(self): + return thread_sizes + + def designations(self, size): + return [self.Designation(size, pitch) for pitch in self.pitches] + + def threads(self, designation): + threads = [] + P = designation.pitch + D = designation.nominal_diameter + H = (P / 2) / math.tan(math.radians(thread_angle / 2)) + pitch_dia = D - 2 * (3 * H / 8) + minor_dia = D - 2 * (5 * H / 8) + + for offset in tolerance_offsets: + class_label = f"O.{str(offset)[2:]}" # e.g., 0.1 โ†’ "O.1" + + # External thread + ext = Thread() + ext.gender = "external" + ext.thread_class = class_label + ext.major_dia = D - offset + ext.pitch_dia = pitch_dia - offset + ext.minor_dia = minor_dia - offset + threads.append(ext) + + # Internal thread + int_thread = Thread() + int_thread.gender = "internal" + int_thread.thread_class = class_label + int_thread.major_dia = D + offset + int_thread.pitch_dia = pitch_dia + offset + int_thread.minor_dia = minor_dia + offset + int_thread.tap_drill = D - P + threads.append(int_thread) + + return threads + +# ๐Ÿงพ XML generator function +def generate_xml(): + pitch_list = generate_pitch_list(pitch_start, pitch_end, pitch_step) + profile = MetricThreadGenerator(pitch_list) + + root = ET.Element("ThreadType") + tree = ET.ElementTree(root) + + # Add metadata + ET.SubElement(root, "Name").text = thread_name + ET.SubElement(root, "CustomName").text = thread_name + ET.SubElement(root, "Unit").text = unit + ET.SubElement(root, "Angle").text = str(thread_angle) + ET.SubElement(root, "SortOrder").text = "3" + + # Add thread sizes and designations + for size in profile.sizes(): + size_elem = ET.SubElement(root, "ThreadSize") + ET.SubElement(size_elem, "Size").text = str(size) + for designation in profile.designations(size): + des_elem = ET.SubElement(size_elem, "Designation") + ET.SubElement(des_elem, "ThreadDesignation").text = designation.name + ET.SubElement(des_elem, "CTD").text = designation.name + ET.SubElement(des_elem, "Pitch").text = str(designation.pitch) + for thread in profile.threads(designation): + thread_elem = ET.SubElement(des_elem, "Thread") + ET.SubElement(thread_elem, "Gender").text = thread.gender + ET.SubElement(thread_elem, "Class").text = thread.thread_class + ET.SubElement(thread_elem, "MajorDia").text = f"{thread.major_dia:.4g}" + ET.SubElement(thread_elem, "PitchDia").text = f"{thread.pitch_dia:.4g}" + ET.SubElement(thread_elem, "MinorDia").text = f"{thread.minor_dia:.4g}" + if thread.tap_drill is not None: + ET.SubElement(thread_elem, "TapDrill").text = f"{thread.tap_drill:.4g}" + + ET.indent(tree) + tree.write(output_filename, encoding="UTF-8", xml_declaration=True) + +# ๐Ÿš€ Run the generator +if __name__ == "__main__": + generate_xml() + print(f"โœ… XML file '{output_filename}' created for pitches from {pitch_start} mm to {pitch_end} mm in {pitch_step} mm steps.") +