Source code for yapcad.fasteners.builders

"""Fastener construction using catalog data.

This module builds fasteners using dimensions from YAML catalogs,
delegating to the core thread generation in yapcad.threadgen.
"""

from __future__ import annotations

from dataclasses import replace
from pathlib import Path
from typing import Optional

from yapcad.geom import epsilon
from yapcad.threadgen import ThreadProfile, metric_profile, unified_profile

from .catalog import get_bolt_data, get_nut_data

__all__ = [
    "build_hex_bolt_from_catalog",
    "build_hex_nut_from_catalog",
]


def _make_profile_from_catalog(
    thread_data: dict,
    internal: bool = False,
    handedness: str = "right",
    starts: int = 1,
) -> ThreadProfile:
    """Create a ThreadProfile from catalog thread data.

    Uses the tolerances from the catalog to adjust thread geometry.
    """
    nominal_d = thread_data["nominal_diameter"]
    pitch = thread_data["pitch"]
    tolerances = thread_data.get("tolerances", {})

    # Determine if metric or unified based on presence of 'tpi'
    # (Unified catalog stores tpi, metric stores pitch directly)
    # Both catalogs store pitch in mm for consistency

    # Get tolerance adjustments
    if internal:
        # Internal thread: apply EI (lower deviation, usually 0 for H position)
        ei = tolerances.get("EI", 0.0)
        # For internal threads, the minor diameter limit is what matters
        # TD1 is the tolerance on minor diameter
        td1 = tolerances.get("TD1", 0.0)

        # Adjust nominal diameter for internal thread
        # Basic internal thread has D (major) = nominal
        # The actual cutting diameter accounts for allowance
        adjusted_d = nominal_d + ei

        profile = ThreadProfile(
            D_nominal=adjusted_d,
            P_pitch=pitch,
            thread_angle=60.0,
            crest_flat_ratio=1.0 / 8.0,
            root_flat_ratio=1.0 / 8.0,  # Internal threads have smaller root flat
            thread_depth_ratio=0.57,
            handedness=handedness,
            starts=starts,
            internal=True,
        )
    else:
        # External thread: apply es (upper deviation, negative for g position)
        es = tolerances.get("es", 0.0)
        # Td2 is pitch diameter tolerance, Td is major diameter tolerance
        td2 = tolerances.get("Td2", 0.0)
        td = tolerances.get("Td", 0.0)

        # Adjust nominal diameter for external thread
        # The es deviation is typically negative (undersized)
        adjusted_d = nominal_d + es

        profile = ThreadProfile(
            D_nominal=adjusted_d,
            P_pitch=pitch,
            thread_angle=60.0,
            crest_flat_ratio=1.0 / 8.0,
            root_flat_ratio=1.0 / 4.0,  # External threads have larger root flat
            thread_depth_ratio=0.57,
            handedness=handedness,
            starts=starts,
            internal=False,
        )

    return profile


def _default_thread_length(shank_length: float, diameter: float) -> float:
    """Calculate default thread length per ISO 4014 / ASME B18.2.1.

    For bolts up to 125mm: thread_length = 2*d + 6mm
    For bolts 125-200mm: thread_length = 2*d + 12mm
    For bolts over 200mm: thread_length = 2*d + 25mm

    But never more than shank_length.
    """
    if shank_length <= 125:
        tl = 2 * diameter + 6
    elif shank_length <= 200:
        tl = 2 * diameter + 12
    else:
        tl = 2 * diameter + 25

    # Thread length can't exceed shank length
    return min(tl, shank_length)


[docs] def build_hex_bolt_from_catalog( thread_series: str, size: str, length: float, tolerance_class: str = "6g", thread_length: Optional[float] = None, starts: int = 1, thread_arc_samples: int = 180, thread_samples_per_pitch: int = 6, catalog_path: Optional[Path] = None, ): """Build a hex bolt using catalog dimensions. Args: thread_series: "metric_coarse", "unified_coarse", etc. size: Size designation from catalog length: Shank length in mm tolerance_class: Thread tolerance class thread_length: Thread length in mm (or computed if None) starts: Number of thread starts thread_arc_samples: Angular samples for thread surface thread_samples_per_pitch: Profile samples per pitch catalog_path: Optional custom catalog path Returns: yapCAD solid representing the bolt """ # Import here to avoid circular dependency from yapcad.fasteners_legacy import ( HexCapScrewSpec, build_hex_cap_screw, ) # Get catalog data bolt_data = get_bolt_data(thread_series, size, tolerance_class, catalog_path) thread_data = bolt_data["thread"] head_data = bolt_data["head"] # Create thread profile from catalog data profile = _make_profile_from_catalog( thread_data, internal=False, handedness="right", starts=starts, ) # Calculate thread length nominal_d = thread_data["nominal_diameter"] if thread_length is None: thread_length = _default_thread_length(length, nominal_d) thread_length = min(max(thread_length, epsilon), length) # Build spec for legacy builder spec = HexCapScrewSpec( diameter=nominal_d, thread_length=thread_length, shank_length=length, head_height=head_data["head_height"], head_flat_diameter=head_data["width_across_flats"], washer_thickness=head_data.get("washer_face_thickness", 0.5), washer_diameter=head_data.get("washer_face_diameter"), shank_diameter=nominal_d, # Could be slightly larger in reality starts=starts, thread_arc_samples=thread_arc_samples, thread_samples_per_pitch=thread_samples_per_pitch, ) return build_hex_cap_screw(profile, spec)
[docs] def build_hex_nut_from_catalog( thread_series: str, size: str, tolerance_class: str = "6H", handedness: str = "right", starts: int = 1, thread_arc_samples: int = 180, thread_samples_per_pitch: int = 6, catalog_path: Optional[Path] = None, ): """Build a hex nut using catalog dimensions. Args: thread_series: "metric_coarse", "unified_coarse", etc. size: Size designation from catalog tolerance_class: Thread tolerance class handedness: "right" or "left" starts: Number of thread starts thread_arc_samples: Angular samples for thread surface thread_samples_per_pitch: Profile samples per pitch catalog_path: Optional custom catalog path Returns: yapCAD solid representing the nut """ # Import here to avoid circular dependency from yapcad.fasteners_legacy import ( HexNutSpec, build_hex_nut, ) # Get catalog data nut_data = get_nut_data(thread_series, size, tolerance_class, catalog_path) thread_data = nut_data["thread"] body_data = nut_data["body"] # Create thread profile from catalog data profile = _make_profile_from_catalog( thread_data, internal=True, handedness=handedness, starts=starts, ) # Build spec for legacy builder spec = HexNutSpec( diameter=thread_data["nominal_diameter"], pitch=thread_data["pitch"], width_flat=body_data["width_across_flats"], thickness=body_data["thickness"], handedness=handedness, starts=starts, thread_arc_samples=thread_arc_samples, thread_samples_per_pitch=thread_samples_per_pitch, ) return build_hex_nut(profile, spec)