1 """ 2 3 Parametric Threads 4 5 name: thread.py 6 by: Gumyr 7 date: November 11th 2021 8 9 desc: This python/cadquery code is a parameterized thread generator. 10 11 license: 12 13 Copyright 2021 Gumyr 14 15 Licensed under the Apache License, Version 2.0 (the "License"); 16 you may not use this file except in compliance with the License. 17 You may obtain a copy of the License at 18 19 http://www.apache.org/licenses/LICENSE-2.0 20 21 Unless required by applicable law or agreed to in writing, software 22 distributed under the License is distributed on an "AS IS" BASIS, 23 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 See the License for the specific language governing permissions and 25 limitations under the License. 26 27 """ 28 import re 29 from warnings import warn 30 from abc import ABC, abstractmethod 31 from typing import Literal, Optional, Tuple, List 32 from math import sin, cos, tan, radians, pi 33 import cadquery as cq 34 from cadquery import Solid, Compound 35 from OCP.TopoDS import TopoDS_Shape 36 37 # from functools import cached_property, cache 38 39 MM = 1 40 IN = 25.4 * MM 41 42 43 def is_safe(value: str) -> bool: 44 """Evaluate if the given string is a fractional number safe for eval()""" 45 return len(value) <= 10 and all(c in "0123456789./ " for c in set(value)) 46 47 48 def imperial_str_to_float(measure: str) -> float: 49 """Convert an imperial measurement (possibly a fraction) to a float value""" 50 if is_safe(measure): 51 # pylint: disable=eval-used 52 # Before eval() is called the string extracted from the csv file is verified as safe 53 result = eval(measure.strip().replace(" ", "+")) * IN 54 else: 55 result = measure 56 return result 57 58 59 class Thread(Solid): 60 """Helical thread 61 62 The most general thread class used to build all of the other threads. 63 Creates right or left hand helical thread with the given 64 root and apex radii. 65 66 Args: 67 apex_radius: Radius at the narrow tip of the thread. 68 apex_width: Radius at the wide base of the thread. 69 root_radius: Radius at the wide base of the thread. 70 root_width: Thread base width. 71 pitch: Length of 360° of thread rotation. 72 length: End to end length of the thread. 73 apex_offset: Asymmetric thread apex offset from center. Defaults to 0.0. 74 hand: Twist direction. Defaults to "right". 75 taper_angle: Cone angle for tapered thread. Defaults to None. 76 end_finishes: Profile of each end, one of: 77 78 "raw" 79 unfinished which typically results in the thread 80 extended below z=0 or above z=length 81 "fade" 82 the thread height drops to zero over 90° of arc 83 (or 1/4 pitch) 84 "square" 85 clipped by the z=0 or z=length plane 86 "chamfer" 87 conical ends which facilitates alignment of a bolt 88 into a nut 89 90 Defaults to ("raw","raw"). 91 simple: Stop at thread calculation, don't create thread. Defaults to False. 92 93 Raises: 94 ValueError: if end_finishes not in ["raw", "square", "fade", "chamfer"]: 95 """ 96 97 def fade_helix( 98 self, t: float, apex: bool, vertical_displacement: float 99 ) -> Tuple[float, float, float]: 100 """A helical function used to create the faded tips of threads that spirals 101 self.tooth_height in self.pitch/4""" 102 if self.external: 103 radius = ( 104 self.apex_radius - sin(t * pi / 2) * self.tooth_height 105 if apex 106 else self.root_radius 107 ) 108 else: 109 radius = ( 110 self.apex_radius + sin(t * pi / 2) * self.tooth_height 111 if apex 112 else self.root_radius 113 ) 114 115 z_pos = t * self.pitch / 4 + t * vertical_displacement 116 x_pos = radius * cos(t * pi / 2) 117 y_pos = radius * sin(t * pi / 2) 118 return (x_pos, y_pos, z_pos) 119 120 @property 121 def cq_object(self): 122 """A cadquery Solid thread as defined by class attributes""" 123 warn("cq_object will be deprecated.", DeprecationWarning, stacklevel=2) 124 return Solid(self.wrapped) 125 126 def __init__( 127 self, 128 apex_radius: float, 129 apex_width: float, 130 root_radius: float, 131 root_width: float, 132 pitch: float, 133 length: float, 134 apex_offset: float = 0.0, 135 hand: Literal["right", "left"] = "right", 136 taper_angle: Optional[float] = None, 137 end_finishes: Tuple[ 138 Literal["raw", "square", "fade", "chamfer"], 139 Literal["raw", "square", "fade", "chamfer"], 140 ] = ("raw", "raw"), 141 simple: bool = False, 142 ): 143 """Store the parameters and create the thread object""" 144 for finish in end_finishes: 145 if finish not in ["raw", "square", "fade", "chamfer"]: 146 raise ValueError( 147 'end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer"' 148 ) 149 self.external = apex_radius > root_radius 150 self.apex_radius = apex_radius 151 self.apex_width = apex_width 152 # Unfortunately, when creating "fade" ends inaccuracies in parametric curve calculations 153 # can result in a gap which causes the OCCT core to fail when combining with other 154 # object (like the core of the thread). To avoid this, subtract (or add) a fudge factor 155 # to the root radius to make it small enough to intersect the given radii. 156 self.root_radius = root_radius - (0.001 if self.external else -0.001) 157 self.root_width = root_width 158 self.pitch = pitch 159 self.length = length 160 self.apex_offset = apex_offset 161 self.right_hand = hand == "right" 162 self.end_finishes = end_finishes 163 self.tooth_height = abs(self.apex_radius - self.root_radius) 164 self.taper = 360 if taper_angle is None else taper_angle 165 self.simple = simple 166 167 if not simple: 168 # Create base cylindrical thread 169 number_faded_ends = self.end_finishes.count("fade") 170 cylindrical_thread_length = self.length + self.pitch * ( 171 1 - 1 * number_faded_ends 172 ) 173 if self.end_finishes[0] == "fade": 174 cylindrical_thread_displacement = self.pitch / 2 175 else: 176 cylindrical_thread_displacement = -self.pitch / 2 177 178 # Either create a cylindrical thread for further processing 179 # or create a cylindrical thread segment with faded ends 180 if number_faded_ends == 0: 181 cq_object = self.make_thread_solid(cylindrical_thread_length).translate( 182 (0, 0, cylindrical_thread_displacement) 183 ) 184 else: 185 cq_object = self.make_thread_with_faded_ends( 186 number_faded_ends, 187 cylindrical_thread_length, 188 cylindrical_thread_displacement, 189 ) 190 191 # Square off ends if requested 192 cq_object = self.square_off_ends(cq_object) 193 # Chamfer ends if requested 194 cq_object = self.chamfer_ends(cq_object) 195 if isinstance(cq_object, Compound) and len(cq_object.Solids()) == 1: 196 super().__init__(cq_object.Solids()[0].wrapped) 197 else: 198 super().__init__(cq_object.wrapped) 199 else: 200 # Initialize with a valid shape then nullify 201 super().__init__(Solid.makeBox(1, 1, 1).wrapped) 202 self.wrapped = TopoDS_Shape() 203 204 def make_thread_with_faded_ends( 205 self, 206 number_faded_ends, 207 cylindrical_thread_length, 208 cylindrical_thread_displacement, 209 ): 210 """Build the thread object from cylindrical thread faces and 211 faded ends faces""" 212 (thread_faces, end_faces) = self.make_thread_faces(cylindrical_thread_length) 213 214 # Need to operate on each face below 215 thread_faces = [ 216 f.translate((0, 0, cylindrical_thread_displacement)) for f in thread_faces 217 ] 218 end_faces = [ 219 f.translate((0, 0, cylindrical_thread_displacement)) for f in end_faces 220 ] 221 cylindrical_thread_angle = ( 222 (360 if self.right_hand else -360) * cylindrical_thread_length / self.pitch 223 ) 224 (fade_faces, _fade_ends) = self.make_thread_faces( 225 self.pitch / 4, fade_helix=True 226 ) 227 if not self.right_hand: 228 fade_faces = [f.mirror("XZ") for f in fade_faces] 229 230 if self.end_finishes[0] == "fade": 231 # If the thread is asymmetric the bottom fade end needs to be recreated as 232 # no amount of flipping or rotating can generate the shape 233 if self.apex_offset != 0: 234 (fade_faces_bottom, _fade_ends) = self.make_thread_faces( 235 self.pitch / 4, fade_helix=True, asymmetric_flip=True 236 ) 237 if not self.right_hand: 238 fade_faces_bottom = [f.mirror("XZ") for f in fade_faces_bottom] 239 else: 240 fade_faces_bottom = fade_faces 241 fade_faces_bottom = [ 242 f.mirror("XZ").mirror("XY").translate(cq.Vector(0, 0, self.pitch / 2)) 243 for f in fade_faces_bottom 244 ] 245 if self.end_finishes[1] == "fade": 246 fade_faces_top = [ 247 f.translate( 248 cq.Vector( 249 0, 250 0, 251 cylindrical_thread_length + cylindrical_thread_displacement, 252 ) 253 ).rotate((0, 0, 0), (0, 0, 1), cylindrical_thread_angle) 254 for f in fade_faces 255 ] 256 if number_faded_ends == 2: 257 thread_shell = cq.Shell.makeShell( 258 thread_faces + fade_faces_bottom + fade_faces_top 259 ) 260 elif self.end_finishes[0] == "fade": 261 thread_shell = cq.Shell.makeShell( 262 thread_faces + fade_faces_bottom + [end_faces[1]] 263 ) 264 else: 265 thread_shell = cq.Shell.makeShell( 266 thread_faces + fade_faces_top + [end_faces[0]] 267 ) 268 return cq.Solid.makeSolid(thread_shell) 269 270 def square_off_ends(self, cq_object: Solid): 271 """Square off the ends of the thread""" 272 273 squared = cq_object 274 if self.end_finishes.count("square") != 0: 275 # Note: box_size must be > max(apex,root) radius or the core doesn't cut correctly 276 half_box_size = 2 * max(self.apex_radius, self.root_radius) 277 box_size = 2 * half_box_size 278 cutter = cq.Solid.makeBox( 279 length=box_size, 280 width=box_size, 281 height=self.length, 282 pnt=cq.Vector(-half_box_size, -half_box_size, -self.length), 283 ) 284 for i in range(2): 285 if self.end_finishes[i] == "square": 286 squared = cq_object.cut( 287 cutter.translate(cq.Vector(0, 0, 2 * i * self.length)) 288 ) 289 return squared 290 291 def chamfer_ends(self, cq_object: Solid): 292 """Chamfer the ends of the thread""" 293 294 chamfered = cq_object 295 if self.end_finishes.count("chamfer") != 0: 296 cutter = ( 297 cq.Workplane("XY") 298 .circle(self.root_radius) 299 .circle(self.apex_radius) 300 .extrude(self.length) 301 ) 302 face_selectors = ["<Z", ">Z"] 303 edge_radius_selector = 1 if self.apex_radius > self.root_radius else 0 304 for i in range(2): 305 if self.end_finishes[i] == "chamfer": 306 cutter = ( 307 cutter.faces(face_selectors[i]) 308 .edges(cq.selectors.RadiusNthSelector(edge_radius_selector)) 309 .chamfer(self.tooth_height * 0.5, self.tooth_height * 0.75) 310 ) 311 chamfered = cq_object.intersect(cutter.val()) 312 return chamfered 313 314 def make_thread_faces( 315 self, length: float, fade_helix: bool = False, asymmetric_flip: bool = False 316 ) -> Tuple[List[cq.Face]]: 317 """Create the thread object from basic CadQuery objects 318 319 This method creates three types of thread objects: 320 1. cylindrical - i.e. following a simple helix 321 2. tapered - i.e. following a conical helix 322 3. faded - cylindrical but spiralling towards the root in 90° 323 324 After testing many alternatives (sweep, extrude with rotation, etc.) the 325 following algorithm was found to be the fastest and most reliable: 326 a. first create all the edges - helical, linear or parametric 327 b. create either 5 or 6 faces from the edges (faded needs 5) 328 c. create a shell from the faces 329 d. create a solid from the shell 330 """ 331 local_apex_offset = -self.apex_offset if asymmetric_flip else self.apex_offset 332 apex_helix_wires = [ 333 cq.Workplane("XY") 334 .parametricCurve( 335 lambda t: self.fade_helix(t, apex=True, vertical_displacement=0) 336 ) 337 .val() 338 .translate((0, 0, i * self.apex_width + local_apex_offset)) 339 if fade_helix 340 else cq.Wire.makeHelix( 341 pitch=self.pitch, 342 height=length, 343 radius=self.apex_radius, 344 angle=self.taper, 345 lefthand=not self.right_hand, 346 ).translate((0, 0, i * self.apex_width + local_apex_offset)) 347 for i in [-0.5, 0.5] 348 ] 349 assert apex_helix_wires[0].isValid() 350 root_helix_wires = [ 351 cq.Workplane("XY") 352 .parametricCurve( 353 lambda t: self.fade_helix( 354 t, 355 apex=False, 356 vertical_displacement=-i * (self.root_width - self.apex_width), 357 ) 358 ) 359 .val() 360 .translate((0, 0, i * self.root_width)) 361 if fade_helix 362 else cq.Wire.makeHelix( 363 pitch=self.pitch, 364 height=length, 365 radius=self.root_radius, 366 angle=self.taper, 367 lefthand=not self.right_hand, 368 ).translate((0, 0, i * self.root_width)) 369 for i in [-0.5, 0.5] 370 ] 371 # When creating a cylindrical or tapered thread two end faces are required 372 # to enclose the thread object, while faded thread only has one end face 373 end_caps = [0] if fade_helix else [0, 1] 374 end_cap_wires = [ 375 cq.Wire.makePolygon( 376 [ 377 apex_helix_wires[0].positionAt(i), 378 apex_helix_wires[1].positionAt(i), 379 root_helix_wires[1].positionAt(i), 380 root_helix_wires[0].positionAt(i), 381 apex_helix_wires[0].positionAt(i), 382 ] 383 ) 384 for i in end_caps 385 ] 386 thread_faces = [ 387 cq.Face.makeRuledSurface(apex_helix_wires[0], apex_helix_wires[1]), 388 cq.Face.makeRuledSurface(apex_helix_wires[1], root_helix_wires[1]), 389 cq.Face.makeRuledSurface(root_helix_wires[1], root_helix_wires[0]), 390 cq.Face.makeRuledSurface(root_helix_wires[0], apex_helix_wires[0]), 391 ] 392 end_faces = [cq.Face.makeFromWires(end_cap_wires[i]) for i in end_caps] 393 return (thread_faces, end_faces) 394 395 def make_thread_solid( 396 self, 397 length: float, 398 fade_helix: bool = False, 399 ) -> cq.Solid: 400 """Create a solid object by first creating the faces""" 401 (thread_faces, end_faces) = self.make_thread_faces(length, fade_helix) 402 403 thread_shell = cq.Shell.makeShell(thread_faces + end_faces) 404 thread_solid = cq.Solid.makeSolid(thread_shell) 405 return thread_solid 406 407 408 class IsoThread(Solid): 409 """ISO Standard Thread 410 411 Both external and internal ISO standard 60° threads as shown in 412 the following diagram (from https://en.wikipedia.org/wiki/ISO_metric_screw_thread): 413 414 .. image:: https://upload.wikimedia.org/wikipedia/commons/4/4b/ISO_and_UTS_Thread_Dimensions.svg 415 416 The following is an example of an internal thread with a chamfered end as might 417 be found inside a nut: 418 419 .. image:: internal_iso_thread.png 420 421 Args: 422 major_diameter (float): Primary thread diameter 423 pitch (float): Length of 360° of thread rotation 424 length (float): End to end length of the thread 425 external (bool, optional): External or internal thread selector. Defaults to True. 426 hand (Literal[, optional): Twist direction. Defaults to "right". 427 end_finishes (Tuple[ Literal[, optional): Profile of each end, one of: 428 429 "raw" 430 unfinished which typically results in the thread 431 extended below z=0 or above z=length 432 "fade" 433 the thread height drops to zero over 90° of arc 434 (or 1/4 pitch) 435 "square" 436 clipped by the z=0 or z=length plane 437 "chamfer" 438 conical ends which facilitates alignment of a bolt 439 into a nut 440 441 Defaults to ("fade", "square"). 442 simple: Stop at thread calculation, don't create thread. Defaults to False. 443 444 Attributes: 445 thread_angle (int): 60 degrees 446 h_parameter (float): Value of `h` as shown in the thread diagram 447 min_radius (float): Inside radius of the thread diagram 448 449 Raises: 450 ValueError: if hand not in ["right", "left"]: 451 ValueError: end_finishes not in ["raw", "square", "fade", "chamfer"] 452 453 """ 454 455 @property 456 def h_parameter(self) -> float: 457 """Calculate the h parameter""" 458 return (self.pitch / 2) / tan(radians(self.thread_angle / 2)) 459 460 @property 461 def min_radius(self) -> float: 462 """The radius of the root of the thread""" 463 return (self.major_diameter - 2 * (5 / 8) * self.h_parameter) / 2 464 465 @property 466 def cq_object(self): 467 """A cadquery Solid thread as defined by class attributes""" 468 warn("cq_object will be deprecated.", DeprecationWarning, stacklevel=2) 469 return Solid(self.wrapped) 470 471 def __init__( 472 self, 473 major_diameter: float, 474 pitch: float, 475 length: float, 476 external: bool = True, 477 hand: Literal["right", "left"] = "right", 478 end_finishes: Tuple[ 479 Literal["raw", "square", "fade", "chamfer"], 480 Literal["raw", "square", "fade", "chamfer"], 481 ] = ("fade", "square"), 482 simple: bool = False, 483 ): 484 485 self.major_diameter = major_diameter 486 self.pitch = pitch 487 self.length = length 488 self.external = external 489 self.thread_angle = 60 490 if hand not in ["right", "left"]: 491 raise ValueError(f'hand must be one of "right" or "left" not {hand}') 492 self.hand = hand 493 for finish in end_finishes: 494 if finish not in ["raw", "square", "fade", "chamfer"]: 495 raise ValueError( 496 'end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer"' 497 ) 498 self.end_finishes = end_finishes 499 self.simple = simple 500 self.apex_radius = self.major_diameter / 2 if external else self.min_radius 501 apex_width = self.pitch / 8 if external else self.pitch / 4 502 self.root_radius = self.min_radius if external else self.major_diameter / 2 503 root_width = 3 * self.pitch / 4 if external else 7 * self.pitch / 8 504 cq_object = Thread( 505 apex_radius=self.apex_radius, 506 apex_width=apex_width, 507 root_radius=self.root_radius, 508 root_width=root_width, 509 pitch=self.pitch, 510 length=self.length, 511 end_finishes=self.end_finishes, 512 hand=self.hand, 513 simple=simple, 514 ) 515 if simple: 516 # Initialize with a valid shape then nullify 517 super().__init__(Solid.makeBox(1, 1, 1).wrapped) 518 self.wrapped = TopoDS_Shape() 519 else: 520 super().__init__(cq_object.wrapped) 521 522 523 class TrapezoidalThread(ABC, Solid): 524 """Trapezoidal Thread Base Class 525 526 Trapezoidal Thread base class for Metric and Acme derived classes 527 528 Trapezoidal thread forms are screw thread profiles with trapezoidal outlines. They are 529 the most common forms used for leadscrews (power screws). They offer high strength 530 and ease of manufacture. They are typically found where large loads are required, as 531 in a vise or the leadscrew of a lathe. 532 533 Args: 534 size (str): specified by derived class 535 length (float): thread length 536 external (bool, optional): external or internal thread selector. Defaults to True. 537 hand (Literal[, optional): twist direction. Defaults to "right". 538 end_finishes (Tuple[ Literal[, optional): Profile of each end, one of: 539 540 "raw" 541 unfinished which typically results in the thread 542 extended below z=0 or above z=length 543 "fade" 544 the thread height drops to zero over 90° of arc 545 (or 1/4 pitch) 546 "square" 547 clipped by the z=0 or z=length plane 548 "chamfer" 549 conical ends which facilitates alignment of a bolt 550 into a nut 551 552 Defaults to ("fade", "fade"). 553 554 Raises: 555 ValueError: hand must be one of "right" or "left" 556 ValueError: end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer" 557 558 Attributes: 559 thread_angle (int): thread angle in degrees 560 diameter (float): thread diameter 561 pitch (float): thread pitch 562 563 """ 564 565 @property 566 def cq_object(self): 567 """A cadquery Solid thread as defined by class attributes""" 568 warn("cq_object will be deprecated.", DeprecationWarning, stacklevel=2) 569 return Solid(self.wrapped) 570 571 @property 572 @abstractmethod 573 def thread_angle(self) -> float: # pragma: no cover 574 """The thread angle in degrees""" 575 return NotImplementedError 576 577 @classmethod 578 @abstractmethod 579 def parse_size(cls, size: str) -> Tuple[float, float]: # pragma: no cover 580 """Convert the provided size into a tuple of diameter and pitch""" 581 return NotImplementedError 582 583 def __init__( 584 self, 585 size: str, 586 length: float, 587 external: bool = True, 588 hand: Literal["right", "left"] = "right", 589 end_finishes: tuple[ 590 Literal["raw", "square", "fade", "chamfer"], 591 Literal["raw", "square", "fade", "chamfer"], 592 ] = ("fade", "fade"), 593 ): 594 self.size = size 595 self.external = external 596 self.length = length 597 (self.diameter, self.pitch) = self.parse_size(self.size) 598 shoulder_width = (self.pitch / 2) * tan(radians(self.thread_angle / 2)) 599 apex_width = (self.pitch / 2) - shoulder_width 600 root_width = (self.pitch / 2) + shoulder_width 601 if self.external: 602 self.apex_radius = self.diameter / 2 603 self.root_radius = self.diameter / 2 - self.pitch / 2 604 else: 605 self.apex_radius = self.diameter / 2 - self.pitch / 2 606 self.root_radius = self.diameter / 2 607 608 if hand not in ["right", "left"]: 609 raise ValueError(f'hand must be one of "right" or "left" not {hand}') 610 self.hand = hand 611 for finish in end_finishes: 612 if not finish in ["raw", "square", "fade", "chamfer"]: 613 raise ValueError( 614 'end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer"' 615 ) 616 self.end_finishes = end_finishes 617 cq_object = Thread( 618 apex_radius=self.apex_radius, 619 apex_width=apex_width, 620 root_radius=self.root_radius, 621 root_width=root_width, 622 pitch=self.pitch, 623 length=self.length, 624 end_finishes=self.end_finishes, 625 hand=self.hand, 626 ) 627 super().__init__(cq_object.wrapped) 628 629 630 class AcmeThread(TrapezoidalThread): 631 """ACME Thread 632 633 The original trapezoidal thread form, and still probably the one most commonly encountered 634 worldwide, with a 29° thread angle, is the Acme thread form. 635 636 The following is the acme thread with faded ends: 637 638 .. image:: acme_thread.png 639 640 Args: 641 size (str): size as a string (i.e. "3/4" or "1 1/4") 642 length (float): thread length 643 external (bool, optional): external or internal thread selector. Defaults to True. 644 hand (Literal[, optional): twist direction. Defaults to "right". 645 end_finishes (Tuple[ Literal[, optional): Profile of each end, one of: 646 647 "raw" 648 unfinished which typically results in the thread 649 extended below z=0 or above z=length 650 "fade" 651 the thread height drops to zero over 90° of arc 652 (or 1/4 pitch) 653 "square" 654 clipped by the z=0 or z=length plane 655 "chamfer" 656 conical ends which facilitates alignment of a bolt 657 into a nut 658 659 Defaults to ("fade", "fade"). 660 661 Raises: 662 ValueError: hand must be one of "right" or "left" 663 ValueError: end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer" 664 665 Attributes: 666 thread_angle (int): thread angle in degrees 667 diameter (float): thread diameter 668 pitch (float): thread pitch 669 670 """ 671 672 acme_pitch = { 673 "1/4": (1 / 16) * IN, 674 "5/16": (1 / 14) * IN, 675 "3/8": (1 / 12) * IN, 676 "1/2": (1 / 10) * IN, 677 "5/8": (1 / 8) * IN, 678 "3/4": (1 / 6) * IN, 679 "7/8": (1 / 6) * IN, 680 "1": (1 / 5) * IN, 681 "1 1/4": (1 / 5) * IN, 682 "1 1/2": (1 / 4) * IN, 683 "1 3/4": (1 / 4) * IN, 684 "2": (1 / 4) * IN, 685 "2 1/2": (1 / 3) * IN, 686 "3": (1 / 2) * IN, 687 } 688 689 thread_angle = 29.0 # in degrees 690 691 @classmethod 692 def sizes(cls) -> List[str]: 693 """Return a list of the thread sizes""" 694 return list(AcmeThread.acme_pitch.keys()) 695 696 @classmethod 697 def parse_size(cls, size: str) -> Tuple[float, float]: 698 """Convert the provided size into a tuple of diameter and pitch""" 699 if not size in AcmeThread.acme_pitch.keys(): 700 raise ValueError( 701 f"size invalid, must be one of {AcmeThread.acme_pitch.keys()}" 702 ) 703 diameter = imperial_str_to_float(size) 704 pitch = AcmeThread.acme_pitch[size] 705 return (diameter, pitch) 706 707 708 class MetricTrapezoidalThread(TrapezoidalThread): 709 """Metric Trapezoidal Thread 710 711 The ISO 2904 standard metric trapezoidal thread with a thread angle of 30° 712 713 Args: 714 size (str): specified as a sting with diameter x pitch in mm (i.e. "8x1.5") 715 length (float): End to end length of the thread 716 external (bool, optional): external or internal thread selector. Defaults to True. 717 hand (Literal[, optional): twist direction. Defaults to "right". 718 end_finishes (Tuple[ Literal[, optional): Profile of each end, one of: 719 720 "raw" 721 unfinished which typically results in the thread 722 extended below z=0 or above z=length 723 "fade" 724 the thread height drops to zero over 90° of arc 725 (or 1/4 pitch) 726 "square" 727 clipped by the z=0 or z=length plane 728 "chamfer" 729 conical ends which facilitates alignment of a bolt 730 into a nut 731 732 Defaults to ("fade", "fade"). 733 734 Raises: 735 ValueError: hand must be one of "right" or "left" 736 ValueError: end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer" 737 738 Attributes: 739 thread_angle (int): thread angle in degrees 740 diameter (float): thread diameter 741 pitch (float): thread pitch 742 """ 743 744 # Turn off black auto-format for this array as it will be spread over hundreds of lines 745 # fmt: off 746 standard_sizes = [ 747 "8x1.5","9x1.5","9x2","10x1.5","10x2","11x2","11x3","12x2","12x3","14x2", 748 "14x3","16x2","16x3","16x4","18x2","18x3","18x4","20x2","20x3","20x4", 749 "22x3","22x5","22x8","24x3","24x5","24x8","26x3","26x5","26x8","28x3", 750 "28x5","28x8","30x3","30x6","30x10","32x3","32x6","32x10","34x3","34x6", 751 "34x10","36x3","36x6","36x10","38x3","38x7","38x10","40x3","40x7","40x10", 752 "42x3","42x7","42x10","44x3","44x7","44x12","46x3","46x8","46x12","48x3", 753 "48x8","48x12","50x3","50x8","50x12","52x3","52x8","52x12","55x3","55x9", 754 "55x14","60x3","60x9","60x14","65x4","65x10","65x16","70x4","70x10","70x16", 755 "75x4","75x10","75x16","80x4","80x10","80x16","85x4","85x12","85x18","90x4", 756 "90x12","90x18","95x4","95x12","95x18","100x4","100x12","100x20","105x4", 757 "105x12","105x20","110x4","110x12","110x20","115x6","115x12","115x14", 758 "115x22","120x6","120x12","120x14","120x22","125x6","125x12","125x14", 759 "125x22","130x6","130x12","130x14","130x22","135x6","135x12","135x14", 760 "135x24","140x6","140x12","140x14","140x24","145x6","145x12","145x14", 761 "145x24","150x6","150x12","150x16","150x24","155x6","155x12","155x16", 762 "155x24","160x6","160x12","160x16","160x28","165x6","165x12","165x16", 763 "165x28","170x6","170x12","170x16","170x28","175x8","175x12","175x16", 764 "175x28","180x8","180x12","180x18","180x28","185x8","185x12","185x18", 765 "185x24","185x32","190x8","190x12","190x18","190x24","190x32","195x8", 766 "195x12","195x18","195x24","195x32","200x8","200x12","200x18","200x24", 767 "200x32","205x4","210x4","210x8","210x12","210x20","210x24","210x36","215x4", 768 "220x4","220x8","220x12","220x20","220x24","220x36","230x4","230x8","230x12", 769 "230x20","230x24","230x36","235x4","240x4","240x8","240x12","240x20", 770 "240x22","240x24","240x36","250x4","250x12","250x22","250x24","250x40", 771 "260x4","260x12","260x20","260x22","260x24","260x40","270x12","270x24", 772 "270x40","275x4","280x4","280x12","280x24","280x40","290x4","290x12", 773 "290x24","290x44","295x4","300x4","300x12","300x24","300x44","310x5","315x5" 774 ] 775 # fmt: on 776 777 thread_angle = 30.0 # in degrees 778 779 @classmethod 780 def sizes(cls) -> List[str]: 781 """Return a list of the thread sizes""" 782 return MetricTrapezoidalThread.standard_sizes 783 784 @classmethod 785 def parse_size(cls, size: str) -> Tuple[float, float]: 786 """Convert the provided size into a tuple of diameter and pitch""" 787 if not size in MetricTrapezoidalThread.standard_sizes: 788 raise ValueError( 789 f"size invalid, must be one of {MetricTrapezoidalThread.standard_sizes}" 790 ) 791 (diameter, pitch) = (float(part) for part in size.split("x")) 792 return (diameter, pitch) 793 794 795 class PlasticBottleThread(Solid): 796 """ASTM D2911 Plastic Bottle Thread 797 798 The `ASTM D2911 Standard <https://www.astm.org/d2911-10.html>`_ Plastic Bottle Thread. 799 800 L Style: 801 All-Purpose Thread - trapezoidal shape with 30° shoulders, metal or platsic closures 802 M Style: 803 Modified Buttress Thread - asymmetric shape with 10° and 40/45/50° 804 shoulders, plastic closures 805 806 .. image:: plasticThread.png 807 808 Args: 809 size (str): as defined by the ASTM is specified as 810 [L|M][diameter(mm)]SP[100|103|110|200|400|410|415|425|444] 811 external (bool, optional): external or internal thread selector. Defaults to True. 812 hand (Literal[, optional): twist direction. Defaults to "right". 813 manufacturingCompensation (float, optional): used to compensate for over-extrusion of 3D 814 printers. A value of 0.2mm will reduce the radius of an external thread by 0.2mm (and 815 increase the radius of an internal thread) such that the resulting 3D printed part 816 matches the target dimensions. Defaults to 0.0. 817 818 Raises: 819 ValueError: hand must be one of "right" or "left" 820 ValueError: size invalid, must match 821 [L|M][diameter(mm)]SP[100|103|110|200|400|410|415:425|444] 822 ValueError: finish invalid 823 ValueError: diameter invalid 824 825 Example: 826 .. code-block:: python 827 828 thread = PlasticBottleThread( 829 size="M38SP444", external=False, manufacturingCompensation=0.2 * MM 830 ) 831 832 """ 833 834 # {TPI: [root_width,thread_height]} 835 l_style_thread_dimensions = { 836 4: [3.18, 1.57], 837 5: [3.05, 1.52], 838 6: [2.39, 1.19], 839 8: [2.13, 1.07], 840 12: [1.14, 0.76], 841 } 842 m_style_thread_dimensions = { 843 4: [3.18, 1.57], 844 5: [3.05, 1.52], 845 6: [2.39, 1.19], 846 8: [2.13, 1.07], 847 12: [1.29, 0.76], 848 } 849 850 thread_angles = { 851 "L100": [30, 30], 852 "M100": [10, 40], 853 "L103": [30, 30], 854 "M103": [10, 40], 855 "L110": [30, 30], 856 "M110": [10, 50], 857 "L200": [30, 30], 858 "M200": [10, 40], 859 "L400": [30, 30], 860 "M400": [10, 45], 861 "L410": [30, 30], 862 "M410": [10, 45], 863 "L415": [30, 30], 864 "M415": [10, 45], 865 "L425": [30, 30], 866 "M425": [10, 45], 867 "L444": [30, 30], 868 "M444": [10, 45], 869 } 870 871 # {finish:[min turns,[diameters,...]]} 872 # fmt: off 873 finish_data = { 874 100: [1.125,[22,24,28,30,33,35,38]], 875 103: [1.125,[26]], 876 110: [1.125,[28]], 877 200: [1.5,[24.28]], 878 400: [1.0,[18,20,22,24,28,30,33,35,38,40,43,45,48,51,53,58,60,63,66,70,75,77,83,89,100,110,120]], 879 410: [1.5,[18,20,22,24,28]], 880 415: [2.0,[13,15,18,20,22,24,28,30,33]], 881 425: [2.0,[13,15]], 882 444: [1.125,[24,28,30,33,35,38,40,43,45,48,51,53,58,60,63,66,70,75,77,83]] 883 } 884 # fmt: on 885 886 # {thread_size:[max,min,TPI]} 887 thread_dimensions = { 888 13: [13.06, 12.75, 12], 889 15: [14.76, 14.45, 12], 890 18: [17.88, 17.47, 8], 891 20: [19.89, 19.48, 8], 892 22: [21.89, 21.49, 8], 893 24: [23.88, 23.47, 8], 894 26: [25.63, 25.12, 8], 895 28: [27.64, 27.13, 6], 896 30: [28.62, 28.12, 6], 897 33: [32.13, 31.52, 6], 898 35: [34.64, 34.04, 6], 899 38: [37.49, 36.88, 6], 900 40: [40.13, 39.37, 6], 901 43: [42.01, 41.25, 6], 902 45: [44.20, 43.43, 6], 903 48: [47.50, 46.74, 6], 904 51: [49.99, 49.10, 6], 905 53: [52.50, 51.61, 6], 906 58: [56.49, 55.60, 6], 907 60: [59.49, 58.60, 6], 908 63: [62.51, 61.62, 6], 909 66: [65.51, 64.62, 6], 910 70: [69.49, 68.60, 6], 911 75: [73.99, 73.10, 6], 912 77: [77.09, 76.20, 6], 913 83: [83.01, 82.12, 5], 914 89: [89.18, 88.29, 5], 915 100: [100.00, 99.11, 5], 916 110: [110.01, 109.12, 5], 917 120: [119.99, 119.10, 5], 918 } 919 920 @property 921 def cq_object(self): 922 """A cadquery Solid thread as defined by class attributes""" 923 warn("cq_object will be deprecated.", DeprecationWarning, stacklevel=2) 924 return Solid(self.wrapped) 925 926 def __init__( 927 self, 928 size: str, 929 external: bool = True, 930 hand: Literal["right", "left"] = "right", 931 manufacturingCompensation: float = 0.0, 932 ): 933 self.size = size 934 self.external = external 935 if hand not in ["right", "left"]: 936 raise ValueError(f'hand must be one of "right" or "left" not {hand}') 937 self.hand = hand 938 size_match = re.match(r"([LM])(\d+)SP(\d+)", size) 939 if not size_match: 940 raise ValueError( 941 "size invalid, must match \ 942 [L|M][diameter(mm)]SP[100|103|110|200|400|410|415:425|444]" 943 ) 944 self.style = size_match.group(1) 945 self.diameter = int(size_match.group(2)) 946 self.finish = int(size_match.group(3)) 947 if self.finish not in PlasticBottleThread.finish_data.keys(): 948 raise ValueError( 949 f"finish ({self.finish}) invalid, must be one of" 950 f" {list(PlasticBottleThread.finish_data.keys())}" 951 ) 952 if not self.diameter in PlasticBottleThread.finish_data[self.finish][1]: 953 raise ValueError( 954 f"diameter ({self.diameter}) invalid, must be one" 955 f" of {PlasticBottleThread.finish_data[self.finish][1]}" 956 ) 957 (diameter_max, diameter_min, self.tpi) = PlasticBottleThread.thread_dimensions[ 958 self.diameter 959 ] 960 if self.style == "L": 961 ( 962 self.root_width, 963 thread_height, 964 ) = PlasticBottleThread.l_style_thread_dimensions[self.tpi] 965 else: 966 ( 967 self.root_width, 968 thread_height, 969 ) = PlasticBottleThread.m_style_thread_dimensions[self.tpi] 970 if self.external: 971 self.apex_radius = diameter_min / 2 - manufacturingCompensation 972 self.root_radius = ( 973 diameter_min / 2 - thread_height - manufacturingCompensation 974 ) 975 else: 976 self.root_radius = diameter_max / 2 + manufacturingCompensation 977 self.apex_radius = ( 978 diameter_max / 2 - thread_height + manufacturingCompensation 979 ) 980 self.thread_angles = PlasticBottleThread.thread_angles[ 981 self.style + str(self.finish) 982 ] 983 shoulders = [thread_height * tan(radians(a)) for a in self.thread_angles] 984 self.apex_width = self.root_width - sum(shoulders) 985 self.apex_offset = shoulders[0] + self.apex_width / 2 - self.root_width / 2 986 if not self.external: 987 self.apex_offset = -self.apex_offset 988 self.pitch = 25.4 * MM / self.tpi 989 self.length = ( 990 PlasticBottleThread.finish_data[self.finish][0] + 0.75 991 ) * self.pitch 992 cq_object = Thread( 993 apex_radius=self.apex_radius, 994 apex_width=self.apex_width, 995 root_radius=self.root_radius, 996 root_width=self.root_width, 997 pitch=self.pitch, 998 length=self.length, 999 apex_offset=self.apex_offset, 1000 hand=self.hand, 1001 end_finishes=("fade", "fade"), 1002 ) 1003 super().__init__(cq_object.wrapped)
https://cq-warehouse.readthedocs.io/en/latest/thread.html#isothread
标签:end,thread,apex,self,radius,fade,Cq From: https://www.cnblogs.com/arwen-xu/p/17965030