diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..e3e49dc
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,15 @@
+version: 2
+
+build:
+ os: "ubuntu-20.04"
+ tools:
+ python: "3.9"
+
+# Build from the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/conf.py
+
+# Explicitly set the version of Python and its requirements
+python:
+ install:
+ - requirements: docs/requirements.txt
\ No newline at end of file
diff --git a/README.md b/README.md
index b447b69..52e7d29 100644
--- a/README.md
+++ b/README.md
@@ -1,1255 +1,22 @@
-
-The cq_warehouse python/cadquery package contains a set of parametric parts which can
-be customized and used within your projects or saved to a CAD file
-in STEP or STL format for use in a wide variety of CAD
-or CAM systems.
+
-# Table of Contents
-- [Table of Contents](#table-of-contents)
-- [Installation](#installation)
-- [Package Structure](#package-structure)
- - [sprocket sub-package](#sprocket-sub-package)
- - [Input Parameters](#input-parameters)
- - [Instance Variables](#instance-variables)
- - [Methods](#methods)
- - [Tooth Tip Shape](#tooth-tip-shape)
- - [chain sub-package](#chain-sub-package)
- - [Input Parameters](#input-parameters-1)
- - [Instance Variables](#instance-variables-1)
- - [Methods](#methods-1)
- - [Future Enhancements](#future-enhancements)
- - [drafting sub-package](#drafting-sub-package)
- - [dimension_line](#dimension_line)
- - [extension_line](#extension_line)
- - [callout](#callout)
- - [thread sub-package](#thread-sub-package)
- - [Thread](#thread)
- - [IsoThread](#isothread)
- - [AcmeThread](#acmethread)
- - [MetricTrapezoidalThread](#metrictrapezoidalthread)
- - [TrapezoidalThread](#trapezoidalthread)
- - [PlasticBottleThread](#plasticbottlethread)
- - [fastener sub-package](#fastener-sub-package)
- - [Nut](#nut)
- - [Nut Selection](#nut-selection)
- - [Derived Nut Classes](#derived-nut-classes)
- - [Screw](#screw)
- - [Screw Selection](#screw-selection)
- - [Derived Screw Classes](#derived-screw-classes)
- - [Washer](#washer)
- - [Washer Selection](#washer-selection)
- - [Derived Washer Classes](#derived-washer-classes)
- - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
- - [API](#api)
- - [Fastener Locations](#fastener-locations)
- - [API](#api-1)
- - [Bill of Materials](#bill-of-materials)
- - [Extending the fastener sub-package](#extending-the-fastener-sub-package)
- - [extensions sub-package](#extensions-sub-package)
- - [Assembly class extensions](#assembly-class-extensions)
- - [Translate](#translate)
- - [Rotate](#rotate)
- - [Plane class extensions](#plane-class-extensions)
- - [Transform to Local Coordinates](#transform-to-local-coordinates)
- - [Vector class extensions](#vector-class-extensions)
- - [Rotate about X,Y and Z Axis](#rotate-about-xy-and-z-axis)
- - [Map 2D Vector to 3D Vector](#map-2d-vector-to-3d-vector)
- - [Translate to Vertex](#translate-to-vertex)
- - [Get Signed Angle between Vectors](#get-signed-angle-between-vectors)
- - [Vertex class extensions](#vertex-class-extensions)
- - [Add](#add)
- - [Subtract](#subtract)
- - [Display](#display)
- - [Convert to Vector](#convert-to-vector)
- - [Workplane class extensions](#workplane-class-extensions)
- - [Text on 2D Path](#text-on-2d-path)
- - [Hex Array](#hex-array)
- - [Thicken Non-Planar Face](#thicken-non-planar-face)
- - [Face class extensions](#face-class-extensions)
- - [Thicken](#thicken)
- - [Project Face to Shape](#project-face-to-shape)
- - [Emboss Face To Shape](#emboss-face-to-shape)
- - [Wire class extensions](#wire-class-extensions)
- - [Make Non Planar Face](#make-non-planar-face)
- - [Project Wire to Shape](#project-wire-to-shape)
- - [Emboss Wire to Shape](#emboss-wire-to-shape)
- - [Edge class extensions](#edge-class-extensions)
- - [Project Edge to Shape](#project-edge-to-shape)
- - [Emboss Edge to Shape](#emboss-edge-to-shape)
- - [Shape class extensions](#shape-class-extensions)
- - [Find Intersection](#find-intersection)
- - [Project Text on Shape](#project-text-on-shape)
- - [Emboss Text on Shape](#emboss-text-on-shape)
-# Installation
-Install from github:
-```
-python3 -m pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
-```
-Note that cq_warehouse requires the development version of cadquery (see [Installing CadQuery](https://cadquery.readthedocs.io/en/latest/installation.html)). Also note that cq_warehouse uses the pydantic package for input validation which requires keyword arguments (e.g. `num_teeth=16`).
-# Package Structure
-The cq_warehouse package contains the following sub-packages:
-- **sprocket** : a parametric sprocket generator
-- **chain** : a parametric chain generator
-- **drafting** : a set of methods used for documenting cadquery objects
-- **thread** : a parametric thread fastener generator
-- **fastener** : a parametric threaded fastener generator
-- **extensions** : a set of enhancements to the core cadquery system
-
-## sprocket sub-package
-A sprocket can be generated and saved to a STEP file with just four lines
-of python code using the `Sprocket` class:
-```python
-import cadquery as cq
-from cq_warehouse.sprocket import Sprocket
-
-sprocket32 = Sprocket(num_teeth=32)
-cq.exporters.export(sprocket32.cq_object,"sprocket.step")
-```
-How does this code work?
-1. The first line imports cadquery CAD system with the alias cq
-2. The second line imports the Sprocket class from the sprocket sub-package of the cq_warehouse package
-3. The third line instantiates a 32 tooth sprocket named sprocket32
-4. The fourth line uses the cadquery exporter functionality to save the generated
-sprocket object in STEP format
-
-Note that instead of exporting sprocket32, sprocket32.cq_object is exported as
-sprocket32 contains much more than just the raw CAD object - it contains all of
-the parameters used to generate this sprocket - such as the chain pitch - and some
-derived information that may be useful - such as the chain pitch radius.
-
-### Input Parameters
-Most of the Sprocket parameters are shown in the following diagram:
-
-![sprocket parameters](doc/sprocket_dimensions.png)
-
-The full set of Sprocket input parameters are as follows:
-- `num_teeth` (int) : the number of teeth on the perimeter of the sprocket (must be >= 3)
-- `chain_pitch` (float) : the distance between the centers of two adjacent rollers - default 1/2" - (pitch in the diagram)
-- `roller_diameter` (float) : the size of the cylindrical rollers within the chain - default 5/16" - (roller in the diagram)
-- `clearance` (float) : the size of the gap between the chain's rollers and the sprocket's teeth - default 0.0
-- `thickness` (float) : the thickness of the sprocket - default 0.084"
-- `bolt_circle_diameter` (float) : the diameter of the mounting bolt hole pattern - default 0.0 - (bcd in the diagram)
-- `num_mount_bolts` (int) : the number of bolt holes - default 0 - if 0, no bolt holes are added to the sprocket
-- `mount_bolt_diameter` (float) : the size of the bolt holes use to mount the sprocket - default 0.0 - (bolt in the diagram)
-- `bore_diameter` (float) : the size of the central hole in the sprocket - default 0.0 - if 0, no bore hole is added to the sprocket (bore in the diagram)
-
----
-**NOTE**
-Default parameters are for standard single sprocket bicycle chains.
-
----
-The sprocket in the diagram was generated as follows:
-```python
-MM = 1
-chain_ring = Sprocket(
- num_teeth = 32,
- clearance = 0.1*MM,
- bolt_circle_diameter = 104*MM,
- num_mount_bolts = 4,
- mount_bolt_diameter = 10*MM,
- bore_diameter = 80*MM
-)
-```
----
-**NOTE**
-Units in cadquery are defined so that 1 represents one millimeter but `MM = 1` makes this
-explicit.
-
----
-### Instance Variables
-In addition to all of the input parameters that are stored as instance variables
-within the Sprocket instance there are four derived instance variables:
-- `pitch_radius` (float) : the radius of the circle formed by the center of the chain rollers
-- `outer_radius` (float) : the size of the sprocket from center to tip of the teeth
-- `pitch_circumference` (float) : the circumference of the sprocket at the pitch rad
-- `cq_object` (cq.Workplane) : the cadquery sprocket object
-
-### Methods
-The Sprocket class defines two static methods that may be of use when designing systems with sprockets: calculation of the pitch radius and pitch circumference as follows:
-```python
-@staticmethod
-def sprocket_pitch_radius(num_teeth:int, chain_pitch:float) -> float:
- """
- Calculate and return the pitch radius of a sprocket with the given number of teeth
- and chain pitch
-
- Parameters
- ----------
- num_teeth : int
- the number of teeth on the perimeter of the sprocket
- chain_pitch : float
- the distance between two adjacent pins in a single link (default 1/2 INCH)
- """
-
-@staticmethod
-def sprocket_circumference(num_teeth:int, chain_pitch:float) -> float:
- """
- Calculate and return the pitch circumference of a sprocket with the given number of
- teeth and chain pitch
-
- Parameters
- ----------
- num_teeth : int
- the number of teeth on the perimeter of the sprocket
- chain_pitch : float
- the distance between two adjacent pins in a single link (default 1/2 INCH)
- """
-```
-### Tooth Tip Shape
-Normally the tip of a sprocket tooth has a circular section spanning the roller pin sockets
-on either side of the tooth tip. In this case, the tip is chamfered to allow the chain to
-easily slide over the tooth tip thus reducing the chances of derailing the chain in normal
-operation. However, it is valid to generate a sprocket without this flat
section by
-increasing the size of the rollers. In this case, the tooth tips will be spiky
and
-will not be chamfered.
-## chain sub-package
-A chain wrapped around a set of sprockets can be generated with the `Chain` class by providing
-the size and locations of the sprockets, how the chain wraps and optionally the chain parameters.
-
-For example, one can create the chain for a bicycle with a rear deraileur as follows:
-```python
-import cadquery as cq
-import cq_warehouse.chain as Chain
-
-derailleur_chain = Chain(
- spkt_teeth=[32, 10, 10, 16],
- positive_chain_wrap=[True, True, False, True],
- spkt_locations=[
- (0, 158.9*MM, 50*MM),
- (+190*MM, 0, 50*MM),
- (+190*MM, 78.9*MM, 50*MM),
- (+205*MM, 158.9*MM, 50*MM)
- ]
-)
-if "show_object" in locals():
- show_object(derailleur_chain.cq_object, name="derailleur_chain")
-```
-### Input Parameters
-The complete set of input parameters are:
-- `spkt_teeth` (list of int) : a list of the number of teeth on each sprocket the chain will wrap around
-- `spkt_locations` (list of cq.Vector or tuple(x,y) or tuple(x,y,z)) : the location of the sprocket centers
-- `positive_chain_wrap` (list of bool) : the direction chain wraps around the sprockets, True for counter clock wise viewed from positive Z
-- `chain_pitch` (float) : the distance between two adjacent pins in a single link - default 1/2"
-- `roller_diameter` (float) : the size of the cylindrical rollers within the chain - default 5/16"
-- `roller_length` (float) : the distance between the inner links, i.e. the length of the link rollers - default 3/32"
-- `link_plate_thickness` (float) : the thickness of the link plates (both inner and outer link plates) - default 1mm
-
-The chain is created on the XY plane (methods to move the chain are described below)
-with the sprocket centers being described by:
-- a two dimensional tuple (x,y)
-- a three dimensional tuple (x,y,z) which will result in the chain being created parallel
-to the XY plane, offset by z
-- the cadquery Vector class which will displace the chain by Vector.z
-
-To control the path of the chain between the sprockets, the user must indicate the desired
-direction for the chain to wrap around the sprocket. This is done with the `positive_chain_wrap`
-parameter which is a list of boolean values - one for each sprocket - indicating a counter
-clock wise or positive angle around the z-axis when viewed from the positive side of the XY
-plane. The following diagram illustrates the most complex chain path where the chain
-traverses wraps from positive to positive, positive to negative, negative to positive and
-negative to negative directions (`positive_chain_wrap` values are shown within the arrows
-starting from the largest sprocket):
-
-![chain direction](doc/chain_direction.png)
-
-Note that the chain is perfectly tight as it wraps around the sprockets and does not support any slack. Therefore, as the chain wraps back around to the first link it will either overlap or gap this link - this can be seen in the above figure at the top of the largest sprocket. Adjust the locations of the sprockets to control this value.
-
-### Instance Variables
-In addition to all of the input parameters that are stored as instance variables within the Chain instance there are seven derived instance variables:
-- `pitch_radii` (list of float) : the radius of the circle formed by the center of the chain rollers on each sprocket
-- `chain_links` (float) : the length of the chain in links
-- `num_rollers` (int) : the number of link rollers in the entire chain
-- `roller_loc` (list of cq.Vector) : the location of each roller in the chain
-- `chain_angles` (list of tuple(float,float)) : the chain entry and exit angles in degrees for each sprocket
-- `spkt_initial_rotation` (list of float) : angle in degrees to rotate each sprocket in-order to align the teeth with the gaps in the chain
-- `cq_object` (cq.Assembly) : the cadquery chain object
-
-### Methods
-The Chain class defines two methods:
-- a static method used to generate chain links cadquery objects, and
-- an instance method that will build a cadquery assembly for a chain given a set of sprocket
-cadquery objects.
-Note that the make_link instance method uses the @cache decorator to greatly improve the rate at
-links can be generated as a chain is composed of many copies of the links.
-
-```python
-def assemble_chain_transmission(self,spkts:list[Union[cq.Solid, cq.Workplane]]) -> cq.Assembly:
- """
- Create the transmission assembly from sprockets for a chain
-
- Parameters
- ----------
- spkts : list of cq.Solid or cq:Workplane
- the sprocket cadquery objects to combine with the chain to build a transmission
- """
-
-@staticmethod
-@cache
-def make_link(
- chain_pitch:float = 0.5*INCH,
- link_plate_thickness:float = 1*MM,
- inner:bool = True,
- roller_length:float = (3/32)*INCH,
- roller_diameter:float = (5/16)*INCH
- ) -> cq.Workplane:
- """
- Create either inner or outer link pairs. Inner links include rollers while
- outer links include fake roller pins.
-
- Parameters
- ----------
- chain_pitch : float = (1/2)*INCH
- # the distance between the centers of two adjacent rollers
- link_plate_thickness : float = 1*MM
- # the thickness of the plates which compose the chain links
- inner : bool = True
- # inner links include rollers while outer links include roller pins
- roller_length : float = (3/32)*INCH,
- # the spacing between the inner link plates
- roller_diameter : float = (5/16)*INCH
- # the size of the cylindrical rollers within the chain
- """
-```
-
-Once a chain or complete transmission has been generated it can be re-oriented as follows:
-```python
-two_sprocket_chain = Chain(
- spkt_teeth = [32, 32],
- positive_chain_wrap = [True, True],
- spkt_locations = [ (-5*INCH, 0), (+5*INCH, 0) ]
-)
-relocated_transmission = two_sprocket_chain.assemble_chain_transmission(
- spkts = [spkt32.cq_object, spkt32.cq_object]
-).rotate(axis=(0,1,1),angle=45).translate((20, 20, 20))
-```
-### Future Enhancements
-Two future enhancements are being considered:
-1. Non-planar chains - If the sprocket centers contain `z` values, the chain would follow the path of a spline between the sockets to approximate the path of a bicycle chain where the front and read sprockets are not in the same plane. Currently, the `z` values of the first sprocket define the `z` offset of the entire chain.
-2. Sprocket Location Slots - Typically on or more of the sprockets in a chain transmission will be adjustable to allow the chain to be tight around the
-sprockets. This could be implemented by allowing the user to specify a pair
-of locations defining a slot for a given sprocket indicating that the sprocket
-location should be selected somewhere along this slot to create a perfectly
-fitting chain.
-## drafting sub-package
-A class used to document cadquery designs by providing three methods that create objects that can be included into the design illustrating marked dimension_lines or notes.
-
-For example:
-```python
-import cadquery as cq
-from cq_warehouse.drafting import Draft
-
-# Import an object to be dimensioned
-mystery_object = cq.importers.importStep("mystery.step")
-
-# Create drawing instance with appropriate settings
-metric_drawing = Draft(decimal_precision=1)
-
-# Create an extension line from corners of the part
-length_dimension_line = metric_drawing.extension_line(
- object_edge=mystery_object.faces(" :hourglass: **CQ-editor** :hourglass: You can increase the Preferences :arrow_right: 3D Viewer :arrow_right: Deviation parameter to improve performance by slightly compromising accuracy.
-
-All of the fasteners default to right-handed thread but each of them provide a `hand` string parameter which can either be `"right"` or `"left"`.
-
-All of the fastener classes provide a `cq_object` instance variable which contains the cadquery object.
-
-The following sections describe each of the provided classes.
-
-### Nut
-As the base class of all other nut and bolt classes, all of the derived nut classes share the same interface as follows:
-- `fastener_type` (str) : type identifier - e.g. `"iso4032"`
-- `size` (str) : standard sizes - e.g. `"M6-1"`
-- `hand` (Literal["right", "left"] = "right") : thread direction
-- `simple` (bool = True) : simplify thread
-
-Each nut instance creates a set of properties that provide the CAD object as well as valuable parameters, as follows (values intended for internal use are not shown):
-
-- `clearance_drill_sizes` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `clearance_hole_diameters` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `cq_object` (cq.Compound) : cadquery Compound object
-- `nut_diameter` (float) : maximum diameter of the nut
-- `nut_thickness` (float) : maximum thickness of the nut
-- `nut_class` - (str) : display friendly class name
-- `tap_drill_sizes` - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `tap_hole_diameters` - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-
-#### Nut Selection
-As there are many classes and types of nuts to select from, the Nut class provides some methods that can help find the correct nut for your application. As a reminder, to find the subclasses of the Nut class, use `__subclasses__()`:
-```python
-Nut.__subclasses__() # [, ...]
-```
-Here is a summary of the class methods:
-- `types()` : (set{str}) - create a set of nut types, e.g.:
-```python
-HexNut.types() # {'iso4033', 'iso4032', 'iso4035'}
-```
-- `sizes(fastener_type:str)` : (list[str]) - create a list of nut sizes, e.g.:
-```python
-HexNut.sizes("iso4033") # ['M1.6-0.35', 'M1.8-0.35', 'M2-0.4', 'M2.5-0.45', 'M3-0.45', 'M3.5-0.6', 'M4-0.7', 'M5-0.8', 'M6-1', 'M8-1.25', 'M10-1.5', 'M12-1.75', 'M14-2', 'M16-2', 'M18-2.5', 'M20-2.5', 'M22-2.5', 'M24-3', 'M27-3', 'M30-3.5', 'M33-3.5', 'M36-4', 'M39-4', 'M42-4.5', 'M45-4.5', 'M48-5', 'M52-5']
-```
-- `select_by_size(size:str)` : (dict{class:[type,...],} - e.g.:
-```python
-Nut.select_by_size("M6-1") # {: ['din1587'], : ['iso4035', 'iso4032', 'iso4033'], : ['din1665'], : ['iso4036'], : ['din557']}
-```
-#### Derived Nut Classes
-The following is a list of the current nut classes derived from the base Nut class. Also listed is the type for each of these derived classes where the type refers to a standard that defines the nut parameters. All derived nuts inherit the same API as the base Nut class.
-- `DomedCapNut`: din1587
-- `HexNut`: iso4033, iso4035, iso4032
-- `HexNutWithFlange`: din1665
-- `UnchamferedHexagonNut`: iso4036
-- `SquareNut`: din557
-
-Detailed information about any of the nut types can be readily found on the internet from manufacture's websites or from the standard document itself.
-### Screw
-As the base class of all other screw and bolt classes, all of the derived screw classes share the same interface as follows:
-- `fastener_type` (str) : type identifier - e.g. `"iso4014"`
-- `size` (str) : standard sizes - e.g. `"M6-1"`
-- `length` (float) : distance from base of head to tip of thread
-- `hand` (Literal["right", "left"] = "right") : thread direction
-- `simple` (bool = True) : simplify thread
-
-In addition, to allow screws that have no recess (e.g. hex head bolts) to be countersunk the gap around the hex head which allows a socket wrench to be inserted can be specified with:
-- `socket_clearance` (float = 6 * MM)
-
-Each screw instance creates a set of properties that provide the Compound CAD object as well as valuable parameters, as follows (values intended for internal use are not shown):
-
-- `clearance_drill_sizes` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `clearance_hole_diameters` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `cq_object` (cq.Compound) : cadquery Compound object
-- `head_diameter` (float) : maximum diameter of the head
-- `head_height` (float) : maximum height of the head
-- `nominal_lengths` (list[float]) : nominal lengths values
-- `screw_class` - (str) : display friendly class name
-- `tap_drill_sizes` - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `tap_hole_diameters` - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-
-The following method helps with hole creation:
-
-- `min_hole_depth(counter_sunk: bool = True)` : distance from surface to tip of screw
-
-#### Screw Selection
-As there are many classes and types of screws to select from, the Screw class provides some methods that can help find the correct screw for your application. As a reminder, to find the subclasses of the Screw class, use `__subclasses__()`:
-```python
-Screw.__subclasses__() # [, ...]
-```
-Here is a summary of the class methods:
-- `types()` : (set{str}) - create a set of screw types, e.g.:
-```python
-CounterSunkScrew.types() # {'iso14582', 'iso10642', 'iso14581', 'iso2009', 'iso7046'}
-```
-- `sizes(fastener_type:str)` : (list[str]) - create a list of screw sizes, e.g.:
-```python
-CounterSunkScrew.sizes("iso7046") # ['M1.6-0.35', 'M2-0.4', 'M2.5-0.45', 'M3-0.5', 'M3.5-0.6', 'M4-0.7', 'M5-0.8', 'M6-1', 'M8-1.25', 'M10-1.5']
-```
-- `select_by_size(size:str)` : (dict{class:[type,...],} - e.g.:
-```python
-Screw.select_by_size("M6-1") # {: ['iso7380_1'], : ['iso7380_2'], ...}
-```
-To see if a given screw type has screws in the length you are looking for, each screw class provides a dictionary of available lengths, as follows:
-- `nominal_length_range[fastener_type:str]` : (list[float]) - all the nominal lengths for this screw type, e.g.:
-```python
-CounterSunkScrew.nominal_length_range["iso7046"] # [3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0]
-```
-During instantiation of a screw any value of `length` may be used; however, only a subset of the above nominal_length_range is valid for any given screw size. The valid sub-range is given with the `nominal_lengths` property as follows:
-```python
-screw = CounterSunkScrew(fastener_type="iso7046",size="M6-1",length=12*MM)
-screw.nominal_lengths # [8.0, 10.0, 12.0, 14.0, 16.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0]
-```
-#### Derived Screw Classes
-The following is a list of the current screw classes derived from the base Screw class. Also listed is the type for each of these derived classes where the type refers to a standard that defines the screw parameters. All derived screws inherit the same API as the base Screw class.
-- `ButtonHeadScrew`: iso7380_1
-- `ButtonHeadWithCollarScrew`: iso7380_2
-- `CheeseHeadScrew`: iso14580, iso7048, iso1207
-- `CounterSunkScrew`: iso2009, iso14582, iso14581, iso10642, iso7046
-- `HexHeadScrew`: iso4017, din931, iso4014
-- `HexHeadWithFlangeScrew`: din1662, din1665
-- `PanHeadScrew`: asme_b_18.6.3, iso1580, iso14583
-- `PanHeadWithCollarScrew`: din967
-- `RaisedCheeseHeadScrew`: iso7045
-- `RaisedCounterSunkOvalHeadScrew`: iso2010, iso7047, iso14584
-- `SetScrew`: iso4026
-- `SocketHeadCapScrew`: iso4762, asme_b18.3
-
-Detailed information about any of the screw types can be readily found on the internet from manufacture's websites or from the standard document itself.
-### Washer
-As the base class of all other washer and bolt classes, all of the derived washer classes share the same interface as follows:
-- `fastener_type` (str) : type identifier - e.g. `"iso4032"`
-- `size` (str) : standard sizes - e.g. `"M6-1"`
-
-Each washer instance creates a set of properties that provide the Compound CAD object as well as valuable parameters, as follows (values intended for internal use are not shown):
-
-- `clearance_drill_sizes` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `clearance_hole_diameters` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `cq_object` (cq.Compound) : cadquery Compound object
-- `washer_diameter` (float) : maximum diameter of the washer
-- `washer_thickness` (float) : maximum thickness of the washer
-- `washer_class` - (str) : display friendly class name
-
-#### Washer Selection
-As there are many classes and types of washers to select from, the Washer class provides some methods that can help find the correct washer for your application. As a reminder, to find the subclasses of the Washer class, use `__subclasses__()`:
-```python
-Washer.__subclasses__() # [, , ]
-```
-Here is a summary of the class methods:
-- `types()` : (set{str}) - create a set of washer types, e.g.:
-```python
-PlainWasher.types() # {'iso7091', 'iso7089', 'iso7093', 'iso7094'}
-```
-- `sizes(fastener_type:str)` : (list[str]) - create a list of washer sizes, e.g.:
-```python
-PlainWasher.sizes("iso7091") # ['M1.6', 'M1.7', 'M2', 'M2.3', 'M2.5', 'M2.6', 'M3', 'M3.5', 'M4', 'M5', 'M6', 'M7', 'M8', 'M10', 'M12', 'M14', 'M16', 'M18', 'M20', 'M22', 'M24', 'M26', 'M27', 'M28', 'M30', 'M32', 'M33', 'M35', 'M36']
-```
-- `select_by_size(size:str)` : (dict{class:[type,...],} - e.g.:
-```python
-Washer.select_by_size("M6") # {: ['iso7094', 'iso7093', 'iso7089', 'iso7091'], : ['iso7090'], : ['iso7092']}
-```
-
-#### Derived Washer Classes
-The following is a list of the current washer classes derived from the base Washer class. Also listed is the type for each of these derived classes where the type refers to a standard that defines the washer parameters. All derived washers inherit the same API as the base Washer class.
-- `PlainWasher`: iso7094, iso7093, iso7089, iso7091
-- `ChamferedWasher`: iso7090
-- `CheeseHeadWasher`: iso7092
-
-Detailed information about any of the washer types can be readily found on the internet from manufacture's websites or from the standard document itself.
-
-### Clearance, Tap and Threaded Holes
-When designing parts with CadQuery a common operation is to place holes appropriate to a specific fastener into the part. This operation is optimized with cq_warehouse by the following three new Workplane methods:
-- `cq.Workplane.clearanceHole`,
-- `cq.Workplane.tapHole`, and
-- `cq.Workplane.threadedHole`.
-
-These methods use data provided by a fastener instance (either a `Nut` or a `Screw`) to both create the appropriate hole (possibly countersunk) in your part as well as add the fastener to a CadQuery Assembly in the location of the hole. In addition, a list of washers can be provided which will get placed under the head of the screw or nut in the provided Assembly.
-
-For example, let's re-build the parametric bearing pillow block found in the [CadQuery Quickstart](https://cadquery.readthedocs.io/en/latest/quickstart.html):
-```python
-import cadquery as cq
-from cq_warehouse.fastener import SocketHeadCapScrew
-
-height = 60.0
-width = 80.0
-thickness = 10.0
-diameter = 22.0
-padding = 12.0
-
-# make the screw
-screw = SocketHeadCapScrew(fastener_type="iso4762", size="M2-0.4", length=16, simple=False)
-# make the assembly
-pillow_block = cq.Assembly(None, name="pillow_block")
-# make the base
-base = (
- cq.Workplane("XY")
- .box(height, width, thickness)
- .faces(">Z")
- .workplane()
- .hole(diameter)
- .faces(">Z")
- .workplane()
- .rect(height - padding, width - padding, forConstruction=True)
- .vertices()
- .clearanceHole(fastener=screw, baseAssembly=pillow_block)
- .edges("|Z")
- .fillet(2.0)
-)
-pillow_block.add(base)
-# Render the assembly
-show_object(pillow_block)
-```
-Which results in:
-![pillow_block](doc/pillow_block.png)
-The differences between this code and the Read the Docs version are:
-- screw dimensions aren't required
-- the screw is created during instantiation of the `SocketHeadCapScrew` class
-- an assembly is created and later the base is added to that assembly
-- the call to cskHole is replaced with clearanceHole
-
-Not only were the appropriate holes for M2-0.4 screws created but an assembly was created to store all of the parts in this project all without having to research the dimensions of M2 screws.
-
-Note: In this example the `simple=False` parameter creates accurate threads on each of the screws which significantly increases the complexity of the model. The default of simple is True which models the thread as a simple cylinder which is sufficient for most applications without the performance cost of accurate threads. Also note that the default color of the pillow block "base" was changed to better contrast the screws.
-#### API
-The APIs of these three methods are:
-
-`clearanceHole`: A hole that allows the screw to be inserted freely
-- `fastener`: Union[Nut, Screw],
-- `washers`: Optional[List[Washer]] = None,
-- `fit`: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
-- `depth`: Optional[float] = None,
-- `counterSunk`: Optional[bool] = True,
-- `baseAssembly`: Optional[cq.Assembly] = None,
-- `clean`: Optional[bool] = True,
-
-`tapHole`: A hole ready for a tap to cut a thread
-- `fastener`: Union[Nut, Screw],
-- `washers`: Optional[List[Washer]] = None,
-- `material`: Optional[Literal["Soft", "Hard"]] = "Soft",
-- `depth`: Optional[float] = None,
-- `counterSunk`: Optional[bool] = True,
-- `fit`: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
-- `baseAssembly`: Optional[cq.Assembly] = None,
-- `clean`: Optional[bool] = True,
-
-`threadedHole`: A hole with a integral thread
-- `fastener`: Screw,
-- `depth`: float,
-- `washers`: List[Washer],
-- `hand`: Literal["right", "left"] = "right",
-- `simple`: Optional[bool] = False,
-- `counterSunk`: Optional[bool] = True,
-- `fit`: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
-- `baseAssembly`: Optional[cq.Assembly] = None,
-- `clean`: Optional[bool] = True,
-
-One can see, the API for all three methods are very similar. The `fit` parameter is used for clearance hole dimensions and to calculate the gap around the head of a countersunk screw. The `material` parameter controls the size of the tap hole as they differ as a function of the material the part is made of. For clearance and tap holes, `depth` values of `None` are treated as thru holes. The threaded hole method requires that `depth` be specified as a consequence of how the thread is constructed.
+If you've ever wondered if there is a better alternative to doing mechanical CAD with proprietary software products,
+[CadQuery](https://cadquery.readthedocs.io/en/latest/index.html)
+and this package - **cq_warehouse** - and similar packages
+like [cq_gears](https://github.com/meadiode/cq_gears) might be what you've been looking for. CadQuery augments the Python programming language (the second most widely used programming language) with
+powerful capabilities enabling a wide variety of mechanical designs to be created in S/W with the same techniques that enable most of today's technology.
-The data used in the creation of these holes is available via three instance methods:
-```python
-screw = CounterSunkScrew(fastener_type="iso7046", size="M6-1", length=10)
-screw.clearance_hole_diameters # {'Close': 6.4, 'Normal': 6.6, 'Loose': 7.0}
-screw.clearance_drill_sizes # {'Close': '6.4', 'Normal': '6.6', 'Loose': '7'}
-screw.tap_hole_diameters # {'Soft': 5.0, 'Hard': 5.4}
-screw.tap_drill_sizes # {'Soft': '5', 'Hard': '5.4'}
-```
-Note that with imperial sized holes (e.g. 7/16), the drill sizes could be a fractional size (e.g. 25/64) or a numbered or lettered size (e.g. U). This information can be added to your designs with the [drafting sub-package](#drafting-sub-package).
-
-### Fastener Locations
-There are two methods that assist with the location of fastener holes relative to other parts: `cq.Assembly.fastenerLocations()` and `cq.Workplane.pushFastenerLocations()`.
-
-#### API
-The APIs of these three methods are:
-
-`fastenerLocations`: returns a list of `cq.Location` objects representing the position and orientation of a given fastener in this Assembly
-- `fastener`: Union[Nut, Screw]
-
-`pushFastenerLocations`: places the location(s) of fasteners on the stack ready for further CadQuery operations
-- `fastener`: Union[Nut, Screw], the fastener to locate
-- `baseAssembly`: cq.Assembly, the assembly that the fasteners are relative to
-
-The [align_fastener_holes.py](examples/align_fastener_holes.py) example shows how these methods can be used to align holes between parts in an assembly.
-
-### Bill of Materials
-As previously mentioned, when an assembly is passed into the three hole methods the fasteners referenced are added to the assembly. A new method has been added to the CadQuery Assembly class - `fastenerQuantities()` - which scans the assembly and returns a dictionary of either:
-- {fastener: count}, or
-- {fastener.info: count}
-
-For example, the values for the previous pillow block example are:
-```python
-print(pillow_block.fastenerQuantities())
-# {'SocketHeadCapScrew(iso4762): M2-0.4x16': 4}
-
-print(pillow_block.fastenerQuantities(bom=False))
-# {: 4}
-```
-Note that this method scans the given assembly and all its children for fasteners. To limit the scan to just the current Assembly, set the `deep=False` optional parameter).
-
-### Extending the fastener sub-package
-The fastener sub-package has been designed to be extended in the following two ways:
-- **Alternate Sizes** - As mentioned previously, the data used to guide the creation of fastener objects is derived from `.csv` files found in the same place as the source code. One can add to the set of standard sized fasteners by inserting appropriate data into the tables. There is a table for each fastener class; an example of the 'socket_head_cap_parameters.csv' is below:
-
-| Size | iso4762:dk | iso4762:k | ... | asme_b18.3:dk | asme_b18.3:k | asme_b18.3:s |
-| --------- | ---------- | --------- | --- | ------------- | ------------ | ------------ |
-| M1.6-0.35 | 3.14 | 1.6 |
-| M2-0.4 | 3.98 | 2 |
-| M2.5-0.45 | 4.68 | 2.5 |
-| M3-0.5 | 5.68 | 3 |
-| ... |
-| #0-80 | | | | 0.096 | 0.06 | 0.05 |
-| #1-64 | | | | 0.118 | 0.073 | 1/16 |
-| #1-72 | | | | 0.118 | 0.073 | 1/16 |
-| #2-56 | | | | 0.14 | 0.086 | 5/64 |
-
-The first row must contain a 'Size' and a set of '{fastener_type}:{parameter}' values. The parameters are taken from the ISO standards where 'k' represents the head height of a screw head, 'dk' is represents the head diameter, etc. Refer to the appropriate document for a complete description. The fastener 'Size' field has the format 'M{thread major diameter}-{thread pitch}' for metric fasteners or either '#{guage}-{TPI}' or '{fractional major diameter}-{TPI}' for imperial fasteners (TPI refers to Threads Per Inch). All the data for imperial fasteners must be entered as inch dimensions while metric data is in millimeters.
-
-There is also a 'nominal_screw_lengths.csv' file that contains a list of all the lengths supported by the standard, as follows:
+**cq_warehouse** augments CadQuery with parametric parts - generated on demand -
+and extensions to the core CadQuery capabilities. The resulting parts can be used within your
+projects or saved to a CAD file in STEP or STL format (among others) for use in a wide
+variety of CAD, CAM, or analytical systems.
-| Screw_Type | Unit | Nominal_Sizes |
-| ---------- | ---- | ------------------------ |
-| din931 | mm | 30,35,40,45,50,55,60,... |
-| ... | | |
+The documentation for **cq_warehouse** can found at [readthedocs](https://cq-warehouse.readthedocs.io/en/latest/index.html).
-The 'short' and 'long' values from the first table (not shown) control the minimum and maximum values in the nominal length ranges for each screw.
-- **New Fastener Types** - The base/derived class structure was designed to allow the creation of new fastener types/classes. For new fastener classes a 2D drawing of one half of the fastener profile is required. If the fastener has a non circular plan (e.g. a hex or a square) a 2D drawing of the plan is required. If the fastener contains a flange and a plan, a 2D profile of the flange is required. If these profiles or plans are present, the base class will use them to build the fastener. The Abstract Base Class technology ensures derived classes can't be created with missing components.
-## extensions sub-package
-This python module provides extensions to the native cadquery code base. Hopefully future generations of cadquery will incorporate this or similar functionality.
-
-Examples illustrating how to use much of the functionality of this sub-package can be found in the [extensions_examples.py](examples/extensions_examples.py) file.
-
-To help the user in debugging exceptions generated by the Opencascade core, the dreaded `StdFail_NotDone` exception is caught and augmented with a more meaningful exception where possible (a new exception is raised from StdFail_NotDone so no information is lost). In addition, python logging is used internally which can be enabled (currently by un-commenting the logging configuration code) to provide run-time information in a `cq_warehouse.log` file.
-### Assembly class extensions
-Two additional methods are added to the `Assembly` class which allow easy manipulation of an Assembly like the chain cadquery objects.
-#### Translate
-Move the current assembly (without making a copy) by the specified translation vector.
-```python
-Assembly.translate(self, vec: VectorLike)
- :param vec: The translation Vector or 3-tuple of floats
-```
-#### Rotate
-Rotate the current assembly (without making a copy) around the axis of rotation by the specified angle.
-```python
-Assembly.rotate(self, axis: VectorLike, angle: float):
- :param axis: The axis of rotation (starting at the origin)
- :type axis: a Vector or 3-tuple of floats
- :param angle: the rotation angle, in degrees
- :type angle: float
-```
-### Plane class extensions
-#### Transform to Local Coordinates
-Adding the ability to transform a Bounding Box.
-```python
-Plane.toLocalCoords(self, obj)
- :param obj: an object, vector, or bounding box to convert
- :type Vector, Shape, or BoundBox
- :return: an object of the same type, but converted to local coordinates
-```
-### Vector class extensions
-Methods to rotate a `Vector` about an axis and to convert a 2D point to 3D space.
-#### Rotate about X,Y and Z Axis
-Rotate a vector by an angle in degrees about x,y or z axis.
-```python
-Vector.rotateX(self, angle: float) -> cq.Vector
-Vector.rotateY(self, angle: float) -> cq.Vector
-Vector.rotateZ(self, angle: float) -> cq.Vector
-```
-
-#### Map 2D Vector to 3D Vector
-Map a 2D point on the XY plane to 3D space on the given plane at the offset.
-```python
-Vector.pointToVector(self, plane: str, offset: float = 0.0) -> cq.Vector
- :param plane: A string literal of ["XY", "XZ", "YZ"] representing the plane containing the 2D points
- :type plane: str
- :param offset: The distance from the origin to the provided plane
- :type offset: float
-```
-#### Translate to Vertex
-Convert a Vector to a Vertex
-```python
-Vector.toVertex() -> Vector
-```
-#### Get Signed Angle between Vectors
-Return the angle between two vectors on a plane with the given normal, where angle = atan2((Va x Vb) . Vn, Va . Vb) - in RADIANS.
-```python
-Vector.getSignedAngle(v: Vector, normal: Vector = None) -> float
-```
-
-### Vertex class extensions
-To facilitate placement of drafting objects within a design the cadquery `Vertex` class has been extended with addition and subtraction methods so control points can be defined as follows:
-```python
-part.faces(">Z").vertices(" cq.Vertex:
- :param other: vector to add
- :type other: Vertex, Vector or 3-tuple of float
-```
-#### Subtract
-Subtract a Vertex, Vector or tuple of floats to a Vertex
-```python
-Vertex.__sub__(
- self, other: Union[cq.Vertex, cq.Vector, Tuple[float, float, float]]
-) -> cq.Vertex:
- :param other: vector to add
- :type other: Vertex, Vector or 3-tuple of float
+To install **cq_warehouse** from github:
```
-#### Display
-Display a Vertex
-```python
-Vertex.__str__(self) -> str:
-```
-#### Convert to Vector
-Convert a Vertex to a Vector
-```python
-Vertex.toVector(self) -> cq.Vector:
-```
-
-### Workplane class extensions
-
-#### Text on 2D Path
-Place 2D/3D text on a 2D path as follows:
-![textOnPath](doc/textOnPath.png)
-
-```python
-Workplane.textOnPath(
- txt: str,
- fontsize: float,
- distance: float,
- start: float = 0.0,
- cut: bool = True,
- combine: bool = False,
- clean: bool = True,
- font: str = "Arial",
- fontPath: Optional[str] = None,
- kind: Literal["regular", "bold", "italic"] = "regular",
- valign: Literal["center", "top", "bottom"] = "center",
-)
-```
-The parameters are largely the same as the `Workplane.text` method. The `start` parameter (normally between 0.0 and 1.0) specify where on the path to start the text.
-
-Here are two examples:
-```python
-fox = (
- cq.Workplane("XZ")
- .threePointArc((50, 30), (100, 0))
- .textOnPath(
- txt="The quick brown fox jumped over the lazy dog",
- fontsize=5,
- distance=1,
- start=0.1,
- )
-)
-clover = (
- cq.Workplane("front")
- .moveTo(0, 10)
- .radiusArc((10, 0), 7.5)
- .radiusArc((0, -10), 7.5)
- .radiusArc((-10, 0), 7.5)
- .radiusArc((0, 10), 7.5)
- .consolidateWires()
- .textOnPath(
- txt=".x" * 102,
- fontsize=1,
- distance=1,
- )
-)
-```
-The path that the text follows is defined by the last Edge or Wire in the Workplane stack. Path's defined outside of the Workplane can be used with the `.add(path)` method.
-
-#### Hex Array
-Create a set of points on the stack which describe a hexagon array or honeycomb and push them onto the stack.
-```python
-Workplane.hexArray(
- diagonal: float,
- xCount: int,
- yCount: int,
- center: Union[bool, tuple[bool, bool]] = True,
-):
-:param diagonal: tip to tip size of hexagon ( must be > 0)
-:param xCount: number of points ( > 0 )
-:param yCount: number of points ( > 0 )
-:param center: If True, the array will be centered around the workplane center. If False, the lower corner will be on the reference point and the array will extend in the positive x and y directions. Can also use a 2-tuple to specify centering along each axis.
-```
-#### Thicken Non-Planar Face
-Find all of the faces on the stack and make them Solid objects by thickening along the normals.
-```python
-Workplane.thicken(depth: float, direction: cq.Vector = None)
-:param depth: the amount to thicken - can be positive or negative
-:param direction: an optional 'which way is up' parameter that ensures a set of faces are thickened in the same direction.
-```
-
-### Face class extensions
-
-#### Thicken
-Create a solid from a potentially non planar face by thickening along the normals. The direction vector can be used to indicate which way is 'up', potentially flipping the face normal direction such that many faces with different normals all go in the same direction (direction need only be +/- 90 degrees from the face normal.)
-
-In the following example, non-planar faces are thickened both towards and away from the center of the sphere.
-![thickenFace](doc/thickenFace.png)
-```python
-Face.thicken(depth: float, direction: cq.Vector = None) -> cq.Solid
-:param depth: the amount to thicken - can be positive or negative
-:param direction: an optional 'which way is up' parameter that ensures a set of faces are thickened in the same direction.
-```
-
-#### Project Face to Shape
-Project a Face onto a Shape generating new Face(s) on the surfaces of the object. Two types of projections are supported, a parallel or flat projection with a `direction` indicator or a conical projection where a `center` must be provided.
-
-The two types of projections are illustrated below:
-
-![flatProjection](doc/flatProjection.png)
-![conicalProjection](doc/conicalProjection.png)
-
-The API is as follows:
-```python
-Face.projectFaceToShape(
- targetObject: cq.Shape,
- direction: VectorLike = None,
- center: VectorLike = None,
- internalFacePoints: list[cq.Vector] = [],
-) -> list[cq.Face]
-```
-Note that an array of Faces is returned as the projection might result in faces on the "front" and "back" of the object (or even more if there are intermediate surfaces in the projection path). Faces "behind" the projection are not returned.
-
-To help refine the resulting face, a list of planar points can be passed to augment the surface definition. For example, when projecting a circle onto a sphere, a circle will result which will get converted to a planar circle face. If no points are provided, a single center point will be generated and used for this purpose.
-
-#### Emboss Face To Shape
-Emboss a Face defined on the XY plane to the Shape. Unlike projection, emboss attempts to maintain the lengths of the edges defining the face.
-
-```python
-Face.embossFaceToShape(
- targetObject: cq.Shape,
- surfacePoint: VectorLike,
- surfaceXDirection: VectorLike,
- internalFacePoints: list[cq.Vector] = [],
-) -> cq.Face
-```
-Unlike projection, a single Face is returned. The internalFacePoints parameter works as with projection.
-
-### Wire class extensions
-
-#### Make Non Planar Face
-Create a potentially non-planar face bounded by exterior (wire or edges), optionally refined by surfacePoints with optional holes defined by interiorWires.
-
-```python
-def makeNonPlanarFace(
- exterior: Union[cq.Wire, list[cq.Edge]],
- surfacePoints: list[VectorLike] = None,
- interiorWires: list[cq.Wire] = None,
-) -> cq.Face
-```
-or
-```python
-Wire.makeNonPlanarFace(
- surfacePoints: list[VectorLike] = None,
- interiorWires: list[cq.Wire] = None,
-) -> cq.Face
-```
-The `surfacePoints` parameter can be used to refine the resulting Face. If no points are provided a single central point will be used to help avoid the creation of a planar face.
-
-The `interiorWires` parameter can be used to pass one or more wires which define holes in the Face.
-
-#### Project Wire to Shape
-Project a Wire onto a Shape generating new Wires on the surfaces of the object one and only one of `direction` or `center` must be provided. Note that one more more wires may be generated depending on the topology of the target object and location/direction of projection.
-
-```python
-Wire.projectWireToShape(
- targetObject: cq.Shape,
- direction: VectorLike = None,
- center: VectorLike = None,
-) -> list[cq.Wire]
-```
-
-#### Emboss Wire to Shape
-Emboss a planar Wire defined on the XY plane to targetObject maintaining the length while doing so. An illustration follows:
-
-![embossWire](doc/embossWire.png)
-
-The embossed wire can be used to build features as:
-
-![embossFeature](doc/embossFeature.png)
-
-with the `sweep` method.
-
-```python
-Wire.embossWireToShape(
- targetObject: cq.Shape,
- surfacePoint: VectorLike,
- surfaceXDirection: VectorLike,
- tolerance: float = 0.01,
-) -> cq.Wire
-```
-The `surfacePoint` defines where on the target object the wire will be embossed while the `surfaceXDirection` controls the orientation of the wire on the surface. The `tolerance` parameter controls the accuracy of the embossed wire's length.
-
-### Edge class extensions
-
-#### Project Edge to Shape
-Same as [Project Wire To Shape](#project_wire-to-shape)
-
-
-#### Emboss Edge to Shape
-Same as [Emboss Wire To Shape](#emboss_wire-to-shape)
-
-### Shape class extensions
-
-#### Find Intersection
-Return both the point(s) and normal(s) of the intersection of the line (defined by a point and direction) and the shape.
-
-```python
-Shape.findIntersection(
- point: cq.Vector, direction: cq.Vector
-) -> list[tuple[cq.Vector, cq.Vector]]
-```
-
-#### Project Text on Shape
-Create 2D/3D text with a baseline following the given path on Shape as follows:
-![projectText](doc/projectText.png)
-
-```python
-Shape.projectText(
- txt: str,
- fontsize: float,
- depth: float,
- path: Union[cq.Wire, cq.Edge],
- font: str = "Arial",
- fontPath: Optional[str] = None,
- kind: Literal["regular", "bold", "italic"] = "regular",
- valign: Literal["center", "top", "bottom"] = "center",
- start: float = 0,
-) -> cq.Compound:
-```
-The `start` parameter normally ranges between 0.0 and 1.0 and represents how far along the path the text will start. If `depth` is zero, Faces will be returned instead of Solids.
-
-#### Emboss Text on Shape
-Create 3D text with a baseline following the given path on Shape as follows:
-![embossText](doc/embossText.png)
-
-```python
-Shape.embossText(
- txt: str,
- fontsize: float,
- depth: float,
- path: Union[cq.Wire, cq.Edge],
- font: str = "Arial",
- fontPath: Optional[str] = None,
- kind: Literal["regular", "bold", "italic"] = "regular",
- valign: Literal["center", "top", "bottom"] = "center",
- start: float = 0,
-) -> cq.Compound:
-```
-The `start` parameter normally ranges between 0.0 and 1.0 and represents how far along the path the text will start. If `depth` is zero, Faces will be returned instead of Solids.
+python3 -m pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
+```
\ No newline at end of file
diff --git a/doc/textOnPath.png b/doc/textOnPath.png
deleted file mode 100644
index 3c0dd64..0000000
Binary files a/doc/textOnPath.png and /dev/null differ
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..d4bb2cb
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/acme_thread.png b/docs/acme_thread.png
similarity index 100%
rename from doc/acme_thread.png
rename to docs/acme_thread.png
diff --git a/doc/buttonheadcapscrew.png b/docs/buttonheadcapscrew.png
similarity index 100%
rename from doc/buttonheadcapscrew.png
rename to docs/buttonheadcapscrew.png
diff --git a/docs/chain.rst b/docs/chain.rst
new file mode 100644
index 0000000..3203669
--- /dev/null
+++ b/docs/chain.rst
@@ -0,0 +1,92 @@
+
+####################################
+chain - a parametric chain generator
+####################################
+A chain wrapped around a set of sprockets can be generated with the :ref:`Chain` class by providing
+the size and locations of the sprockets, how the chain wraps and optionally the chain parameters.
+
+For example, one can create the chain for a bicycle with a rear deraileur as follows:
+
+.. code-block:: python
+
+ import cadquery as cq
+ import cq_warehouse.chain as Chain
+
+ derailleur_chain = Chain(
+ spkt_teeth=[32, 10, 10, 16],
+ positive_chain_wrap=[True, True, False, True],
+ spkt_locations=[
+ (0, 158.9 * MM, 50 * MM),
+ (+190 * MM, 0, 50 * MM),
+ (+190 * MM, 78.9 * MM, 50 * MM),
+ (+205 * MM, 158.9 * MM, 50 * MM)
+ ]
+ )
+ if "show_object" in locals():
+ show_object(derailleur_chain.cq_object, name="derailleur_chain")
+
+The chain is created on the XY plane (methods to move the chain are described below)
+with the sprocket centers being described by:
+
+* a two dimensional tuple (x,y)
+* a three dimensional tuple (x,y,z) which will result in the chain being created parallel
+ to the XY plane, offset by "z"
+* the cadquery Vector class which will displace the chain by Vector.z
+
+To control the path of the chain between the sprockets, the user must indicate the desired
+direction for the chain to wrap around the sprocket. This is done with the ``positive_chain_wrap``
+parameter which is a list of boolean values - one for each sprocket - indicating a counter
+clock wise or positive angle around the z-axis when viewed from the positive side of the XY
+plane. The following diagram illustrates the most complex chain path where the chain
+traverses wraps from positive to positive, positive to negative, negative to positive and
+negative to negative directions (`positive_chain_wrap` values are shown within the arrows
+starting from the largest sprocket):
+
+.. image:: chain_direction.png
+ :alt: chain direction
+
+.. py:module:: chain
+
+.. _chain:
+
+.. autoclass:: Chain
+ :members: assemble_chain_transmission, make_link
+
+Note that the chain is perfectly tight as it wraps around the sprockets and does
+not support any slack. Therefore, as the chain wraps back around to the first
+link it will either overlap or gap this link - this can be seen in the above
+figure at the top of the largest sprocket. Adjust the locations of the sprockets
+to control this value.
+
+Note that the make_link instance method uses the @cache decorator to greatly improve the rate at
+links can be generated as a chain is composed of many copies of the links.
+
+Once a chain or complete transmission has been generated it can be re-oriented as follows:
+
+.. code-block:: python
+
+ two_sprocket_chain = Chain(
+ spkt_teeth = [32, 32],
+ positive_chain_wrap = [True, True],
+ spkt_locations = [ (-5 * IN, 0), (+5 * IN, 0) ]
+ )
+ relocated_transmission = two_sprocket_chain.assemble_chain_transmission(
+ spkts = [spkt32.cq_object, spkt32.cq_object]
+ ).rotate(axis=(0,1,1),angle=45).translate((20, 20, 20))
+
+===================
+Future Enhancements
+===================
+Two future enhancements are being considered:
+
+#. Non-planar chains - If the sprocket centers contain ``z`` values, the chain
+ would follow the path of a spline between the sockets to approximate the path of
+ a bicycle chain where the front and rear sprockets are not in the same plane.
+ Currently, the ``z`` values of the first sprocket define the ``z`` offset of the
+ entire chain.
+#. Sprocket Location Slots - Typically on or more of the
+ sprockets in a chain transmission will be adjustable to allow the chain to be
+ tight around the sprockets. This could be implemented by allowing the user to
+ specify a pair of locations defining a slot for a given sprocket indicating that
+ the sprocket location should be selected somewhere along this slot to create a
+ perfectly fitting chain.
diff --git a/doc/chain_direction.png b/docs/chain_direction.png
similarity index 100%
rename from doc/chain_direction.png
rename to docs/chain_direction.png
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..177709b
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,93 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+
+cq_warehouse_path = os.path.dirname(os.path.abspath(os.getcwd()))
+source_files_path = os.path.join(cq_warehouse_path, "src/cq_warehouse")
+sys.path.insert(0, source_files_path)
+sys.path.append(os.path.abspath("sphinxext"))
+sys.path.insert(0, os.path.abspath("."))
+sys.path.insert(0, os.path.abspath("../"))
+
+# -- Project information -----------------------------------------------------
+
+project = "cq_warehouse"
+copyright = "2022, Gumyr"
+author = "Gumyr"
+
+# The full version, including alpha/beta/rc tags
+with open(os.path.join(cq_warehouse_path, "setup.cfg")) as f:
+ setup_cfg = f.readlines()
+for line in setup_cfg:
+ if "version =" in line:
+ release = line.split("=")[1].strip()
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "sphinx.ext.napoleon",
+ "sphinx.ext.autodoc",
+ "sphinx_autodoc_typehints",
+ "sphinx.ext.doctest",
+]
+
+# Napoleon settings
+napoleon_google_docstring = True
+napoleon_numpy_docstring = True
+napoleon_include_init_with_doc = False
+napoleon_include_private_with_doc = False
+napoleon_include_special_with_doc = False
+napoleon_use_admonition_for_examples = False
+napoleon_use_admonition_for_notes = False
+napoleon_use_admonition_for_references = False
+napoleon_use_ivar = True
+napoleon_use_param = True
+napoleon_use_rtype = True
+napoleon_use_keyword = True
+napoleon_custom_sections = None
+
+
+autodoc_typehints = ["description"]
+# autodoc_typehints = ["both"]
+autodoc_mock_imports = ["cadquery", "pkg_resources", "OCP"]
+
+# Sphinx settings
+add_module_names = False
+python_use_unqualified_type_names = True
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+# html_theme = "alabaster"
+html_theme = "sphinx_rtd_theme"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
diff --git a/doc/conicalProjection.png b/docs/conicalProjection.png
similarity index 100%
rename from doc/conicalProjection.png
rename to docs/conicalProjection.png
diff --git a/docs/cq_extensions_intellisense.png b/docs/cq_extensions_intellisense.png
new file mode 100644
index 0000000..ab618b0
Binary files /dev/null and b/docs/cq_extensions_intellisense.png differ
diff --git a/doc/cq_title_image.png b/docs/cq_title_image.png
similarity index 100%
rename from doc/cq_title_image.png
rename to docs/cq_title_image.png
diff --git a/doc/cq_title_image.py b/docs/cq_title_image.py
similarity index 100%
rename from doc/cq_title_image.py
rename to docs/cq_title_image.py
diff --git a/docs/drafting.rst b/docs/drafting.rst
new file mode 100644
index 0000000..514d4de
--- /dev/null
+++ b/docs/drafting.rst
@@ -0,0 +1,73 @@
+
+.. _drafting:
+
+#################################
+drafting - model-based definition
+#################################
+A class used to document cadquery designs by providing three methods that place
+the dimensions and notes right on the 3D model.
+
+For example:
+
+.. code-block:: python
+
+ import cadquery as cq
+ from cq_warehouse.drafting import Draft
+
+ # Import an object to be dimensioned
+ mystery_object = cq.importers.importStep("mystery.step")
+
+ # Create drawing instance with appropriate settings
+ metric_drawing = Draft(decimal_precision=1)
+
+ # Create an extension line from corners of the part
+ length_dimension_line = metric_drawing.extension_line(
+ object_edge=mystery_object.faces("`_
+file.
+
+To help the user in debugging exceptions generated by the Opencascade core, the
+dreaded ``StdFail_NotDone`` exception is caught and augmented with a more
+meaningful exception where possible (a new exception is raised from
+StdFail_NotDone so no information is lost). In addition, python logging is used
+internally which can be enabled (currently by un-commenting the logging
+configuration code) to provide run-time information in a ``cq_warehouse.log``
+file.
+
+.. py:module:: extensions_doc
+
+********************
+CadQuery Integration
+********************
+In order for cq_warehouse CadQuery extensions to be recognized by IDEs (e.g. Intellisense)
+the CadQuery source code needs to be updated. Although monkeypatching allows
+for the functionality of CadQuery to be extended, these extensions are not
+visible to the IDE which makes working with them more difficult.
+
+The `build_cadquery_patch.py `_
+script takes the cq_warehouse extensions.py file (from your pip install) and generates a patch file
+custom to the version of CadQuery that you're using (the version found with the python ``import cadquery``).
+The user needs to apply the patch for the changes to take effect. Reversing the patch will restore the four changed
+files. The patch command can also generate versioned backups of the changed files if
+the user wants even more security.
+
+Currently, cq_warehouse.extensions augments these four CadQuery source files:
+
+* assembly.py,
+* cq.py,
+* geom.py, and
+* shapes.py.
+
+Usage:
+
+.. doctest::
+
+ >>> python build_cadquery_patch.py
+ To apply the patch:
+ cd /home/gumyr/anaconda3/envs/cadquery-dev/lib/python3.9/site-packages/cadquery
+ patch -s -p4 < cadquery_extensions0.5.2.patch
+ To reverse the patch:
+ patch -R -p4 < cadquery_extensions0.5.2.patch
+
+.. warning::
+ This script is designed to work on all platforms that support the ``diff`` and ``patch``
+ commands but has only been tested on Ubuntu Linux.
+
+Once the patch has been applied, IDE's like Visual Studio Code will see all the cq_warehouse extensions
+as native CadQuery functionality and will show all the type hints and code completion available
+with core CadQuery methods. It will look something like this:
+
+.. image:: cq_extensions_intellisense.png
+
+*********
+Functions
+*********
+
+.. autofunction:: makeNonPlanarFace
+
+*************************
+Assembly class extensions
+*************************
+
+.. autoclass:: Assembly
+ :members:
+
+*********************
+Edge class extensions
+*********************
+
+.. autoclass:: Edge
+ :members:
+
+*********************
+Face class extensions
+*********************
+
+.. autoclass:: Face
+ :members:
+
+*************************
+Location class extensions
+*************************
+
+.. autoclass:: Location
+ :members: __str__
+
+**********************
+Plane class extensions
+**********************
+
+.. autoclass:: Plane
+ :members:
+
+**********************
+Shape class extensions
+**********************
+
+.. autoclass:: Shape
+ :members:
+
+***********************
+Vector class extensions
+***********************
+
+.. autoclass:: Vector
+ :members:
+
+***********************
+Vertex class extensions
+***********************
+
+.. autoclass:: Vertex
+ :members: __add__, __sub__, __str__, toVector
+
+*********************
+Wire class extensions
+*********************
+
+.. autoclass:: Wire
+ :members:
+
+.. _Workplane Extensions:
+
+**************************
+Workplane class extensions
+**************************
+
+.. autoclass:: Workplane
+ :members:
+
diff --git a/doc/externalthread.png b/docs/externalthread.png
similarity index 100%
rename from doc/externalthread.png
rename to docs/externalthread.png
diff --git a/docs/fastener.rst b/docs/fastener.rst
new file mode 100644
index 0000000..5072fcf
--- /dev/null
+++ b/docs/fastener.rst
@@ -0,0 +1,522 @@
+########################################
+fastener - parametric threaded fasteners
+########################################
+Many mechanical designs will contain threaded fasteners of some kind, either in a
+threaded hole or threaded screws or bolts holding two or more parts together. The
+fastener sub-package provides a set of classes with which raw threads can be created
+such that they can be integrated into other parts as well as a set of classes that
+create many different types of nuts, screws and washers - as follows:
+
+.. image:: fastener_disc.png
+ :alt: fastener_disc
+
+The holes for the screws in this figure were created with an extension of the Workplane
+class :meth:`~extensions_doc.Workplane.clearanceHole`, the nuts
+:meth:`~extensions_doc.Workplane.tapHole` and the central hole
+:meth:`~extensions_doc.Workplane.threadedHole`.
+The washers were automatically placed and all components were add to an Assembly in
+their correct position and orientations - see
+:ref:`Clearance, Tap and Threaded Holes ` for details.
+
+Here is a list of the classes (and fastener types) provided:
+
+* :ref:`Nut ` - the base nut class
+
+ * ``DomedCapNut``: din1587
+ * ``HexNut``: iso4033, iso4035, iso4032
+ * ``HexNutWithFlange``: din1665
+ * ``UnchamferedHexagonNut``: iso4036
+ * ``SquareNut``: din557
+
+* :ref:`Screw ` - the base screw class
+
+ * ``ButtonHeadScrew``: iso7380_1
+ * ``ButtonHeadWithCollarScrew``: iso7380_2
+ * ``CheeseHeadScrew``: iso14580, iso7048, iso1207
+ * ``CounterSunkScrew``: iso2009, iso14582, iso14581, iso10642, iso7046
+ * ``HexHeadScrew``: iso4017, din931, iso4014
+ * ``HexHeadWithFlangeScrew``: din1662, din1665
+ * ``PanHeadScrew``: asme_b_18.6.3, iso1580, iso14583
+ * ``PanHeadWithCollarScrew``: din967
+ * ``RaisedCheeseHeadScrew``: iso7045
+ * ``RaisedCounterSunkOvalHeadScrew``: iso2010, iso7047, iso14584
+ * ``SetScrew``: iso4026
+ * ``SocketHeadCapScrew``: iso4762, asme_b18.3
+
+* :ref:`Washer ` - the base washer class
+
+ * ``PlainWasher``: iso7094, iso7093, iso7089, iso7091
+ * ``ChamferedWasher``: iso7090
+ * ``CheeseHeadWasher``: iso7092
+
+See :ref:`Extending the fastener sub-package ` for guidance on how to easily
+add new sizes or entirely new types of fasteners.
+
+ The following example creates a variety of different sized fasteners:
+
+.. code-block:: python
+
+ import cadquery as cq
+ from cq_warehouse.fastener import HexNut, SocketHeadCapScrew, SetScrew
+ MM = 1
+ IN = 25.4 * MM
+
+ nut = HexNut(size="M3-0.5", fastener_type="iso4032")
+ setscrew = SetScrew(size="M6-1", fastener_type="iso4026",length=10 * MM)
+ capscrew = SocketHeadCapScrew(size="#6-32", fastener_type="asme_b18.3", length=(1/2) * IN)
+
+Both metric and imperial sized standard fasteners are directly supported by the fastener sub-package
+although the majority of the fasteners currently implemented are metric.
+
+Many of the fastener standards provide ranges for some of the dimensions - for example a minimum and
+maximum head diameter. This sub-package generally uses the maximum sizes when a range is available
+in-order to ensure clearance between a fastener and another part won't be compromised by a physical
+part that is within specification but larger than the CAD model.
+
+Threaded parts are complex for CAD systems to create and significantly increase the storage requirements
+thus making the system slow and difficult to use. To minimize these requirements all of the fastener
+classes have a ``simple`` boolean parameter that when ``True`` doesn't create actual threads at all.
+Such simple parts have the same overall dimensions and such that they can be used to check for fitment
+without dramatically impacting performance.
+
+.. hint::
+
+ ⌛CQ-editor⌛ You can increase the Preferences→3D Viewer→Deviation parameter to improve performance
+ by slightly compromising accuracy.
+
+All of the fasteners default to right-handed thread but each of them provide a ``hand`` string
+parameter which can either be ``"right"`` or ``"left"``.
+
+All of the fastener classes provide a ``cq_object`` instance variable which contains the cadquery
+object.
+
+The following sections describe each of the provided classes.
+
+.. _nut:
+
+***
+Nut
+***
+As the base class of all other nut and bolt classes, all of the derived nut classes share the same
+interface as follows:
+
+.. autoclass:: fastener.Nut
+
+
+Nut Selection
+=============
+As there are many classes and types of nuts to select from, the Nut class provides some methods
+that can help find the correct nut for your application. As a reminder, to find the subclasses of
+the Nut class, use ``__subclasses__()``:
+
+.. py:module:: fastener
+
+.. doctest::
+
+ >>> Nut.__subclasses__()
+ [, ...]
+
+Here is a summary of the class methods:
+
+.. automethod:: Nut.types
+
+.. doctest::
+
+ >>> HexNut.types()
+ {'iso4033', 'iso4032', 'iso4035'}
+
+.. automethod:: Nut.sizes
+
+.. doctest::
+
+ >>> HexNut.sizes("iso4033")
+ ['M1.6-0.35', 'M1.8-0.35', 'M2-0.4', 'M2.5-0.45', 'M3-0.45', 'M3.5-0.6', 'M4-0.7', 'M5-0.8', 'M6-1', 'M8-1.25', 'M10-1.5', 'M12-1.75', 'M14-2', 'M16-2', 'M18-2.5', 'M20-2.5', 'M22-2.5', 'M24-3', 'M27-3', 'M30-3.5', 'M33-3.5', 'M36-4', 'M39-4', 'M42-4.5', 'M45-4.5', 'M48-5', 'M52-5']
+
+.. automethod:: Nut.select_by_size
+
+.. doctest::
+
+ >>> Nut.select_by_size("M6-1")
+ {: ['din1587'], : ['iso4035', 'iso4032', 'iso4033'], : ['din1665'], : ['iso4036'], : ['din557']}
+
+
+Derived Nut Classes
+===================
+The following is a list of the current nut classes derived from the base Nut class. Also listed is
+the type for each of these derived classes where the type refers to a standard that defines the nut
+parameters. All derived nuts inherit the same API as the base Nut class.
+
+* ``DomedCapNut``: din1587
+* ``HexNut``: iso4033, iso4035, iso4032
+* ``HexNutWithFlange``: din1665
+* ``UnchamferedHexagonNut``: iso4036
+* ``SquareNut``: din557
+
+Detailed information about any of the nut types can be readily found on the internet from manufacture's
+websites or from the standard document itself.
+
+.. _screw:
+
+*****
+Screw
+*****
+As the base class of all other screw and bolt classes, all of the derived screw classes share the same
+interface as follows:
+
+.. autoclass:: fastener.Screw
+
+The following method helps with hole creation:
+
+
+.. automethod:: Screw.min_hole_depth
+
+
+Screw Selection
+===============
+As there are many classes and types of screws to select from, the Screw class provides some methods that
+can help find the correct screw for your application. As a reminder, to find the subclasses of the
+Screw class, use ``__subclasses__()``:
+
+.. doctest::
+
+ >>> Screw.__subclasses__()
+ [, ...]
+
+Here is a summary of the class methods:
+
+.. automethod:: Screw.types
+
+.. doctest::
+
+ >>> CounterSunkScrew.types()
+ {'iso14582', 'iso10642', 'iso14581', 'iso2009', 'iso7046'}
+
+.. automethod:: Screw.sizes
+
+.. doctest::
+
+ >>> CounterSunkScrew.sizes("iso7046")
+ ['M1.6-0.35', 'M2-0.4', 'M2.5-0.45', 'M3-0.5', 'M3.5-0.6', 'M4-0.7', 'M5-0.8', 'M6-1', 'M8-1.25', 'M10-1.5']
+
+.. automethod:: Screw.select_by_size
+
+* ``select_by_size(size:str)`` : (dict{class:[type,...],} - e.g.:
+
+.. doctest::
+
+ >>> Screw.select_by_size("M6-1")
+ {: ['iso7380_1'], : ['iso7380_2'], ...}
+
+To see if a given screw type has screws in the length you are looking for, each screw class
+provides a dictionary of available lengths, as follows:
+
+.. doctest::
+
+ >>> CounterSunkScrew.nominal_length_range["iso7046"]
+ [3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0]
+
+During instantiation of a screw any value of ``length`` may be used; however, only a subset of
+the above nominal_length_range is valid for any given screw size. The valid sub-range is given
+with the ``nominal_lengths`` property as follows:
+
+.. doctest::
+
+ >>> screw = CounterSunkScrew(fastener_type="iso7046",size="M6-1",length=12 * MM)
+ >>> screw.nominal_lengths
+ [8.0, 10.0, 12.0, 14.0, 16.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0]
+
+
+Derived Screw Classes
+=====================
+The following is a list of the current screw classes derived from the base Screw class. Also listed
+is the type for each of these derived classes where the type refers to a standard that defines the
+screw parameters. All derived screws inherit the same API as the base Screw class.
+
+* ``ButtonHeadScrew``: iso7380_1
+* ``ButtonHeadWithCollarScrew``: iso7380_2
+* ``CheeseHeadScrew``: iso14580, iso7048, iso1207
+* ``CounterSunkScrew``: iso2009, iso14582, iso14581, iso10642, iso7046
+* ``HexHeadScrew``: iso4017, din931, iso4014
+* ``HexHeadWithFlangeScrew``: din1662, din1665
+* ``PanHeadScrew``: asme_b_18.6.3, iso1580, iso14583
+* ``PanHeadWithCollarScrew``: din967
+* ``RaisedCheeseHeadScrew``: iso7045
+* ``RaisedCounterSunkOvalHeadScrew``: iso2010, iso7047, iso14584
+* ``SetScrew``: iso4026
+* ``SocketHeadCapScrew``: iso4762, asme_b18.3
+
+Detailed information about any of the screw types can be readily found on the internet from manufacture's
+websites or from the standard document itself.
+
+.. _washer:
+
+******
+Washer
+******
+As the base class of all other washer and bolt classes, all of the derived washer classes share
+the same interface as follows:
+
+.. autoclass:: fastener.Washer
+
+
+Washer Selection
+================
+As there are many classes and types of washers to select from, the Washer class provides some methods
+that can help find the correct washer for your application. As a reminder, to find the subclasses of
+the Washer class, use ``__subclasses__()``:
+
+.. doctest::
+
+ >>> Washer.__subclasses__()
+ [, , ]
+
+Here is a summary of the class methods:
+
+.. automethod:: Washer.types
+
+.. doctest::
+
+ >>> PlainWasher.types()
+ {'iso7091', 'iso7089', 'iso7093', 'iso7094'}
+
+.. automethod:: Washer.sizes
+
+.. doctest::
+
+ >>> PlainWasher.sizes("iso7091")
+ ['M1.6', 'M1.7', 'M2', 'M2.3', 'M2.5', 'M2.6', 'M3', 'M3.5', 'M4', 'M5', 'M6', 'M7', 'M8', 'M10', 'M12', 'M14', 'M16', 'M18', 'M20', 'M22', 'M24', 'M26', 'M27', 'M28', 'M30', 'M32', 'M33', 'M35', 'M36']
+
+.. automethod:: Washer.select_by_size
+
+.. doctest::
+
+ >>> Washer.select_by_size("M6")
+ {: ['iso7094', 'iso7093', 'iso7089', 'iso7091'], : ['iso7090'], : ['iso7092']}
+
+
+Derived Washer Classes
+======================
+The following is a list of the current washer classes derived from the base Washer class. Also listed
+is the type for each of these derived classes where the type refers to a standard that defines the washer
+parameters. All derived washers inherit the same API as the base Washer class.
+
+* ``PlainWasher``: iso7094, iso7093, iso7089, iso7091
+* ``ChamferedWasher``: iso7090
+* ``CheeseHeadWasher``: iso7092
+
+Detailed information about any of the washer types can be readily found on the internet from manufacture's
+websites or from the standard document itself.
+
+.. _clearance holes:
+
+*********************************
+Clearance, Tap and Threaded Holes
+*********************************
+When designing parts with CadQuery a common operation is to place holes appropriate to a specific fastener
+into the part. This operation is optimized with cq_warehouse by the following three new Workplane methods:
+
+* :meth:`~extensions_doc.Workplane.clearanceHole`,
+* :meth:`~extensions_doc.Workplane.tapHole`, and
+* :meth:`~extensions_doc.Workplane.threadedHole`.
+
+The API for all three methods are very similar. The ``fit`` parameter is used
+for clearance hole dimensions and to calculate the gap around the head of a countersunk screw.
+The ``material`` parameter controls the size of the tap hole as they differ as a function of the
+material the part is made of. For clearance and tap holes, ``depth`` values of ``None`` are treated
+as thru holes. The threaded hole method requires that ``depth`` be specified as a consequence of
+how the thread is constructed.
+
+These methods use data provided by a fastener instance (either a ``Nut`` or a ``Screw``) to both create
+the appropriate hole (possibly countersunk) in your part as well as add the fastener to a CadQuery Assembly
+in the location of the hole. In addition, a list of washers can be provided which will get placed under the
+head of the screw or nut in the provided Assembly.
+
+For example, let's re-build the parametric bearing pillow block found in
+the `CadQuery Quickstart `_:
+
+.. code-block:: python
+
+ import cadquery as cq
+ from cq_warehouse.fastener import SocketHeadCapScrew
+
+ height = 60.0
+ width = 80.0
+ thickness = 10.0
+ diameter = 22.0
+ padding = 12.0
+
+ # make the screw
+ screw = SocketHeadCapScrew(fastener_type="iso4762", size="M2-0.4", length=16, simple=False)
+ # make the assembly
+ pillow_block = cq.Assembly(None, name="pillow_block")
+ # make the base
+ base = (
+ cq.Workplane("XY")
+ .box(height, width, thickness)
+ .faces(">Z")
+ .workplane()
+ .hole(diameter)
+ .faces(">Z")
+ .workplane()
+ .rect(height - padding, width - padding, forConstruction=True)
+ .vertices()
+ .clearanceHole(fastener=screw, baseAssembly=pillow_block)
+ .edges("|Z")
+ )
+ pillow_block.add(base)
+ # Render the assembly
+ show_object(pillow_block)
+
+Which results in:
+
+.. image:: pillow_block.png
+ :alt: pillow_block
+
+The differences between this code and the Read the Docs version are:
+
+* screw dimensions aren't required
+* the screw is created during instantiation of the ``SocketHeadCapScrew`` class
+* an assembly is created and later the base is added to that assembly
+* the call to cskHole is replaced with clearanceHole
+
+Not only were the appropriate holes for M2-0.4 screws created but an assembly was created to
+store all of the parts in this project all without having to research the dimensions of M2 screws.
+
+Note: In this example the ``simple=False`` parameter creates accurate threads on each of the
+screws which significantly increases the complexity of the model. The default of simple is True
+which models the thread as a simple cylinder which is sufficient for most applications without
+the performance cost of accurate threads. Also note that the default color of the pillow block
+"base" was changed to better contrast the screws.
+
+The data used in the creation of these holes is available via three instance methods:
+
+.. doctest::
+
+ >>> screw = CounterSunkScrew(fastener_type="iso7046", size="M6-1", length=10)
+ >>> screw.clearance_hole_diameters
+ {'Close': 6.4, 'Normal': 6.6, 'Loose': 7.0}
+
+ >>> screw.clearance_drill_sizes
+ {'Close': '6.4', 'Normal': '6.6', 'Loose': '7'}
+ >>> screw.tap_hole_diameters
+ {'Soft': 5.0, 'Hard': 5.4}
+ >>> screw.tap_drill_sizes
+ {'Soft': '5', 'Hard': '5.4'}
+
+Note that with imperial sized holes (e.g. 7/16), the drill sizes could be a fractional size (e.g. 25/64)
+or a numbered or lettered size (e.g. U). This information can be added to your designs with the
+:ref:`drafting ` sub-package.
+
+
+******************
+Fastener Locations
+******************
+There are two methods that assist with the location of fastener holes relative to other
+parts: :meth:`~extensions_doc.Assembly.fastenerLocations` and :meth:`~extensions_doc.Workplane.pushFastenerLocations`.
+
+The `align_fastener_holes.py `_
+example shows how these methods can be used to align holes between parts in an assembly.
+
+.. literalinclude:: ../examples/align_fastener_holes.py
+ :language: python
+
+.. doctest::
+
+ ((25.0, 5.0, 12.0), (0.0, -0.0, 0.0))
+ ((15.0, 5.0, 12.0), (0.0, -0.0, 0.0))
+ ((20.0, 12.0, 5.0), (1.5707963267948966, -0.0, 3.141592653589793))
+ {'SocketHeadCapScrew(iso4762): M2-0.4x6': 3}
+
+.. image:: fastenerLocations.png
+
+*****************
+Bill of Materials
+*****************
+As previously mentioned, when an assembly is passed into the three hole methods the fasteners
+referenced are added to the assembly. A new method has been added to the CadQuery Assembly
+class - :meth:`~extensions_doc.Assembly.fastenerQuantities` - which scans the assembly and returns a dictionary of either:
+
+* {fastener: count}, or
+* {fastener.info: count}
+
+For example, the values for the previous pillow block example are:
+
+.. doctest::
+
+ >>> print(pillow_block.fastenerQuantities())
+ {'SocketHeadCapScrew(iso4762): M2-0.4x16': 4}
+
+ >>> print(pillow_block.fastenerQuantities(bom=False))
+ {: 4}
+
+Note that this method scans the given assembly and all its children for fasteners. To limit the
+scan to just the current Assembly, set the ``deep=False`` optional parameter).
+
+.. _extending:
+
+**********************************
+Extending the fastener sub-package
+**********************************
+The fastener sub-package has been designed to be extended in the following two ways:
+
+
+Alternate Sizes
+===============
+As mentioned previously, the data used to guide the creation of fastener objects is derived
+from ``.csv`` files found in the same place as the source code. One can add to the set of standard
+sized fasteners by inserting appropriate data into the tables. There is a table for each fastener
+class; an example of the 'socket_head_cap_parameters.csv' is below:
+
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+| Size | iso4762:dk | iso4762:k | ... | asme_b18.3:dk | asme_b18.3:k | | asme_b18.3:s |
++============+============+===========+=====+===============+==============+=====+==============+
+| M2-0.4 | 3.98 | 2 | | | | | |
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+| M2.5-0.45 | 4.68 | 2.5 | | 0.096 | 0.06 | | 0.05 |
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+| M3-0.5 | 5.68 | 3 | | 0.118 | 0.073 | | 1/16 |
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+| ... | | | | 0.118 | 0.073 | | 1/16 |
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+| #0-80 | | | | 0.14 | 0.086 | | 5/64 |
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+| #1-64 | | | | | | | |
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+| #1-72 | | | | | | | |
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+| #2-56 | | | | | | | |
++------------+------------+-----------+-----+---------------+--------------+-----+--------------+
+
+The first row must contain a 'Size' and a set of '{fastener_type}:{parameter}'
+values. The parameters are taken from the ISO standards where 'k' represents the
+head height of a screw head, 'dk' is represents the head diameter, etc. Refer to
+the appropriate document for a complete description. The fastener 'Size' field
+has the format 'M{thread major diameter}-{thread pitch}' for metric fasteners or
+either '#{guage}-{TPI}' or '{fractional major diameter}-{TPI}' for imperial
+fasteners (TPI refers to Threads Per Inch). All the data for imperial fasteners
+must be entered as inch dimensions while metric data is in millimeters.
+
+There is also a 'nominal_screw_lengths.csv' file that contains a list of all the
+lengths supported by the standard, as follows:
+
++------------+------+--------------------------+
+| Screw_Type | Unit | Nominal_Sizes |
++============+======+==========================+
+| din931 | mm | 30,35,40,45,50,55,60,... |
++------------+------+--------------------------+
+| ... | | |
++------------+------+--------------------------+
+
+The 'short' and 'long' values from the first table (not shown) control the
+minimum and maximum values in the nominal length ranges for each screw.
+
+New Fastener Types
+==================
+The base/derived class structure was designed to allow the creation of new
+fastener types/classes. For new fastener classes a 2D drawing of one half of the
+fastener profile is required. If the fastener has a non circular plan (e.g. a
+hex or a square) a 2D drawing of the plan is required. If the fastener contains
+a flange and a plan, a 2D profile of the flange is required. If these profiles
+or plans are present, the base class will use them to build the fastener. The
+Abstract Base Class technology ensures derived classes can't be created with
+missing components.
diff --git a/docs/fastenerLocations.png b/docs/fastenerLocations.png
new file mode 100644
index 0000000..a1b12a3
Binary files /dev/null and b/docs/fastenerLocations.png differ
diff --git a/doc/fastener_diagrams.py b/docs/fastener_diagrams.py
similarity index 100%
rename from doc/fastener_diagrams.py
rename to docs/fastener_diagrams.py
diff --git a/doc/fastener_disc.png b/docs/fastener_disc.png
similarity index 100%
rename from doc/fastener_disc.png
rename to docs/fastener_disc.png
diff --git a/doc/flatProjection.png b/docs/flatProjection.png
similarity index 100%
rename from doc/flatProjection.png
rename to docs/flatProjection.png
diff --git a/doc/hexbolt.png b/docs/hexbolt.png
similarity index 100%
rename from doc/hexbolt.png
rename to docs/hexbolt.png
diff --git a/doc/hexnut.png b/docs/hexnut.png
similarity index 100%
rename from doc/hexnut.png
rename to docs/hexnut.png
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..cc01c79
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,116 @@
+..
+ cq_warehouse readthedocs documentation
+
+ by: Gumyr
+ date: February 6th 2022
+
+ desc: This is the documentation for cq_warehouse on readthedocs
+
+ license:
+
+ Copyright 2021 Gumyr
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+.. highlight:: python
+
+.. image:: cq_title_image.png
+
+If you've ever wondered if there is a better alternative to doing mechanical CAD with
+proprietary software products, `CadQuery `_
+and this package - `cq_warehouse `_ - and similar packages
+like `cq_gears `_ might be what you've
+been looking for. CadQuery augments
+the Python programming language (the second most widely used programming language) with
+powerful capabilities enabling a wide variety of mechanical designs to be created
+in S/W with the same techniques that enable most of today's technology.
+
+**cq_warehouse** augments CadQuery with parametric parts - generated on demand -
+and extensions to the core CadQuery capabilities. The resulting parts can be used within your
+projects or saved to a CAD file in STEP or STL format (among others) for use in a wide
+variety of CAD, CAM, or analytical systems.
+
+As an example, consider the design of a simple bearing pillow block:
+
+.. code-block:: python
+
+ import cadquery as cq
+ from cq_warehouse.fastener import SocketHeadCapScrew
+
+ height = 60.0
+ width = 80.0
+ thickness = 10.0
+ diameter = 22.0
+ padding = 12.0
+
+ # make the screw
+ screw = SocketHeadCapScrew(fastener_type="iso4762", size="M2-0.4", length=16, simple=False)
+ # make the assembly
+ pillow_block = cq.Assembly(None, name="pillow_block")
+ # make the base
+ base = (
+ cq.Workplane("XY")
+ .box(height, width, thickness)
+ .faces(">Z")
+ .workplane()
+ .hole(diameter)
+ .faces(">Z")
+ .workplane()
+ .rect(height - padding, width - padding, forConstruction=True)
+ .vertices()
+ .clearanceHole(fastener=screw, baseAssembly=pillow_block)
+ .edges("|Z")
+ .fillet(2.0)
+ )
+ pillow_block.add(base)
+ # Render the assembly
+ show_object(pillow_block)
+
+Which results in:
+
+.. image:: pillow_block.png
+ :alt: pillow_block
+
+With just a few lines of code a parametric part can be created that can be easily
+reviewed and version controlled (with tools like `git `_ and
+`github `_). Documentation can be autogenerated - as with much
+of the documentation that you're reading now - from the source code of your designs.
+Parts can be validated automatically with comprehensive test suites to ensure
+flaws don't get introduced during the lifecycle of a part. The benefits of a full
+S/W development pipeline are too numerous to mention here but also consider that
+all of these tools are OpenSource and free to use - no more licenses -
+and modifiable if required. Take control of your CAD development tools.
+
+=================
+Table Of Contents
+=================
+
+.. toctree::
+ :maxdepth: 2
+
+ installation.rst
+ chain.rst
+ drafting.rst
+ extensions.rst
+ fastener.rst
+ sprocket.rst
+ thread.rst
+
+==================
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/installation.rst b/docs/installation.rst
new file mode 100644
index 0000000..e7ef434
--- /dev/null
+++ b/docs/installation.rst
@@ -0,0 +1,8 @@
+############
+Installation
+############
+Install from github:
+
+.. doctest::
+
+ >>> python3 -m pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
diff --git a/doc/internal_iso_thread.png b/docs/internal_iso_thread.png
similarity index 100%
rename from doc/internal_iso_thread.png
rename to docs/internal_iso_thread.png
diff --git a/doc/internalthread.png b/docs/internalthread.png
similarity index 100%
rename from doc/internalthread.png
rename to docs/internalthread.png
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..153be5e
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://www.sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/doc/pillow_block.png b/docs/pillow_block.png
similarity index 100%
rename from doc/pillow_block.png
rename to docs/pillow_block.png
diff --git a/doc/plasticThread.png b/docs/plasticThread.png
similarity index 100%
rename from doc/plasticThread.png
rename to docs/plasticThread.png
diff --git a/doc/projectText.png b/docs/projectText.png
similarity index 100%
rename from doc/projectText.png
rename to docs/projectText.png
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..e250d63
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,6 @@
+# Defining the exact version will make sure things don't break
+sphinx==4.2.0
+sphinx_rtd_theme==1.0.0
+readthedocs-sphinx-search==0.1.1
+sphinx_autodoc_typehints==1.12.0
+-e git+https://github.com/gumyr/cq_warehouse.git@2847710ff88561383b207b7252d27bb68508c650#egg=cq_warehouse
\ No newline at end of file
diff --git a/doc/setscrew.png b/docs/setscrew.png
similarity index 100%
rename from doc/setscrew.png
rename to docs/setscrew.png
diff --git a/doc/socketheadcapscrew.png b/docs/socketheadcapscrew.png
similarity index 100%
rename from doc/socketheadcapscrew.png
rename to docs/socketheadcapscrew.png
diff --git a/docs/sprocket.rst b/docs/sprocket.rst
new file mode 100644
index 0000000..f85b317
--- /dev/null
+++ b/docs/sprocket.rst
@@ -0,0 +1,70 @@
+###############################
+sprocket - parametric sprockets
+###############################
+A sprocket can be generated and saved to a STEP file with just four lines
+of python code using the :ref:`Sprocket ` class:
+
+.. code-block:: python
+
+ import cadquery as cq
+ from cq_warehouse.sprocket import Sprocket
+
+ sprocket32 = Sprocket(num_teeth=32)
+ cq.exporters.export(sprocket32.cq_object,"sprocket.step")
+
+
+How does this code work?
+
+#. The first line imports cadquery CAD system with the alias cq
+#. The second line imports the Sprocket class from the sprocket sub-package of the cq_warehouse package
+#. The third line instantiates a 32 tooth sprocket named "sprocket32"
+#. The fourth line uses the cadquery exporter functionality to save the generated
+ sprocket object in STEP format
+
+Note that instead of exporting ``sprocket32``, ``sprocket32.cq_object`` is exported as
+``sprocket32`` contains much more than just the raw CAD object - it contains all of
+the parameters used to generate this sprocket - such as the chain pitch - and some
+derived information that may be useful - such as the chain pitch radius.
+
+.. py:module:: sprocket
+
+.. _sprocket:
+
+.. autoclass:: Sprocket
+ :members: sprocket_pitch_radius, sprocket_circumference
+
+Most of the Sprocket parameters are shown in the following diagram:
+
+.. image:: sprocket_dimensions.png
+ :alt: sprocket parameters
+
+
+
+The sprocket in the diagram was generated as follows:
+
+.. code-block:: python
+
+ MM = 1
+ chain_ring = Sprocket(
+ num_teeth = 32,
+ clearance = 0.1 * MM,
+ bolt_circle_diameter = 104 * MM,
+ num_mount_bolts = 4,
+ mount_bolt_diameter = 10 * MM,
+ bore_diameter = 80 * MM
+ )
+
+.. note::
+
+ Units in CadQuery are defined so that 1 represents one millimeter but ``MM = 1`` makes this
+ explicit.
+
+
+Tooth Tip Shape
+===============
+Normally the tip of a sprocket tooth has a circular section spanning the roller pin sockets
+on either side of the tooth tip. In this case, the tip is chamfered to allow the chain to
+easily slide over the tooth tip thus reducing the chances of derailing the chain in normal
+operation. However, it is valid to generate a sprocket without this "flat" section by
+increasing the size of the rollers. In this case, the tooth tips will be "spiky" and
+will not be chamfered.
diff --git a/doc/sprocket_and_chain_diagrams.py b/docs/sprocket_and_chain_diagrams.py
similarity index 100%
rename from doc/sprocket_and_chain_diagrams.py
rename to docs/sprocket_and_chain_diagrams.py
diff --git a/doc/sprocket_dimensions.png b/docs/sprocket_dimensions.png
similarity index 100%
rename from doc/sprocket_dimensions.png
rename to docs/sprocket_dimensions.png
diff --git a/doc/squarenut.png b/docs/squarenut.png
similarity index 100%
rename from doc/squarenut.png
rename to docs/squarenut.png
diff --git a/docs/textOnPath.png b/docs/textOnPath.png
new file mode 100644
index 0000000..b8cc274
Binary files /dev/null and b/docs/textOnPath.png differ
diff --git a/doc/thickenFace.png b/docs/thickenFace.png
similarity index 100%
rename from doc/thickenFace.png
rename to docs/thickenFace.png
diff --git a/docs/thread.rst b/docs/thread.rst
new file mode 100644
index 0000000..40fc821
--- /dev/null
+++ b/docs/thread.rst
@@ -0,0 +1,82 @@
+
+###################################
+thread - parametric helical threads
+###################################
+Helical threads are very common in mechanical designs but can be tricky to
+create in a robust and efficient manner. This sub-package provides classes that
+create three common types of threads:
+
+* ISO Standard 60° threads found on most fasteners
+* Acme 29° threads found on imperial machine equipment
+* Metric Trapezoidal 30° thread found on metric machine equipment
+
+In addition, all threads support four different end finishes:
+
+* "raw" - where the thread extends beyond the desired length ready for integration into another part
+* "fade" - where the end of the thread spirals in - or out for internal threads
+* "square" - where the end of the thread is flat
+* "chamfer" - where the end of the thread is chamfered as commonly found on machine screws
+
+Here is what they look like (clockwise from the top: "fade", "chamfer", "square" and "raw"):
+
+.. image:: thread_end_finishes.png
+ :alt: EndFinishes
+
+When choosing between these four options, consider the performance differences
+between them. Here are some measurements that give a sense of the relative
+performance:
+
++-----------+--------+
+| Finish | Time |
++===========+========+
+| "raw" | 0.018s |
++-----------+--------+
+| "fade" | 0.087s |
++-----------+--------+
+| "square" | 0.370s |
++-----------+--------+
+| "chamfer" | 1.641s |
++-----------+--------+
+
+The "raw" and "fade" end finishes do not use any boolean operations which is why
+they are so fast. "square" does a cut() operation with a box while "chamfer"
+does an intersection() with a chamfered cylinder.
+
+The following sections describe the different thread classes.
+
+******
+Thread
+******
+
+.. autoclass:: thread.Thread
+
+*********
+IsoThread
+*********
+
+.. autoclass:: thread.IsoThread
+
+**********
+AcmeThread
+**********
+
+.. autoclass:: thread.AcmeThread
+
+***********************
+MetricTrapezoidalThread
+***********************
+
+.. autoclass:: thread.MetricTrapezoidalThread
+
+*****************
+TrapezoidalThread
+*****************
+The base class of the AcmeThread and MetricTrapezoidalThread classes.
+
+.. autoclass:: thread.TrapezoidalThread
+
+*******************
+PlasticBottleThread
+*******************
+
+.. autoclass:: thread.PlasticBottleThread
diff --git a/doc/thread_end_finishes.png b/docs/thread_end_finishes.png
similarity index 100%
rename from doc/thread_end_finishes.png
rename to docs/thread_end_finishes.png
diff --git a/examples/extensions_examples.py b/examples/extensions_examples.py
index 6ad8c7b..71426b5 100644
--- a/examples/extensions_examples.py
+++ b/examples/extensions_examples.py
@@ -25,6 +25,7 @@
limitations under the License.
"""
+from math import sin, cos, pi
import timeit
import random
import cadquery as cq
@@ -39,7 +40,7 @@
PROJECT_TEXT = 7
EMBOSS_WIRE = 8
-example = EMBOSS_TEXT
+example = EMBOSS_WIRE
# A sphere used as a projection target
sphere = cq.Solid.makeSphere(50, angleDegrees1=-90)
diff --git a/examples/pillow_block.py b/examples/pillow_block.py
index c0eb142..f7e0c0d 100644
--- a/examples/pillow_block.py
+++ b/examples/pillow_block.py
@@ -10,7 +10,7 @@
# make the screw
screw = SocketHeadCapScrew(
- fastener_type="iso4762", size="M2-0.4", length=16, simple=True
+ fastener_type="iso4762", size="M2-0.4", length=16, simple=False
)
# make the assembly
pillow_block = cq.Assembly(None, name="pillow_block")
@@ -30,7 +30,7 @@
.fillet(2.0)
)
pillow_block.add(base, name="base", color=cq.Color(162 / 255, 138 / 255, 255 / 255))
-print(pillow_block.fastener_quantities(bom=False))
+print(pillow_block.fastenerQuantities(bom=False))
# Render the assembly
if "show_object" in locals():
diff --git a/scripts/build_cadquery_patch.py b/scripts/build_cadquery_patch.py
new file mode 100644
index 0000000..9b9305b
--- /dev/null
+++ b/scripts/build_cadquery_patch.py
@@ -0,0 +1,445 @@
+"""
+Build CadQuery Patch
+
+name: build_cadquery_patch.py
+by: Gumyr
+date: January 27th 2022
+
+desc:
+
+ In order for cq_warehouse CadQuery extensions to be recognized by IDEs (e.g. Intellisense)
+ the CadQuery source code needs to be updated. Although monkeypatching allows
+ for the functionality of CadQuery to be extended, these extensions are not
+ visible to the IDE which makes working with them more difficult.
+
+ This code takes the cq_warehouse extensions.py file, reformats it to fit into
+ the CadQuery source code, applies changes to official Cadquery source files
+ and generates extended versions of these files:
+ - assembly.py,
+ - cq.py,
+ - geom.py, and
+ - shapes.py.
+ Finally, a diff is generated between the originals and extended files for use
+ with the patch command.
+
+ Usage:
+ >>> python build_cadquery_patch
+ To apply the patch:
+ cd /home/gumyr/anaconda3/envs/cadquery-dev/lib/python3.9/site-packages/cadquery
+ patch -s -p4 < cadquery_extensions0.5.2.patch
+ To reverse the patch:
+ patch -R -p4 < cadquery_extensions0.5.2.patch
+
+ Note: this code assumes black formatting of the python files
+
+todo: Add support for extension methods with decorators
+todo: Add an option to save the extended files
+
+license:
+
+ Copyright 2022 Gumyr
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+"""
+import sys
+import getopt
+import os
+import re
+from typing import Literal, Union
+import subprocess
+import tempfile
+import shutil
+import cadquery
+
+# Which CadQuery files define the Class
+# Note: Module defines where python functions go
+class_files = {
+ "occ_impl/shapes.py": ["Shape", "Vertex", "Edge", "Wire", "Face", "Module"],
+ "assembly.py": ["Assembly"],
+ "cq.py": ["Workplane"],
+ "occ_impl/geom.py": ["Plane", "Vector", "Location"],
+}
+
+
+def increase_indent(amount: int, python_code: list[str]) -> list[str]:
+ """Increase indentation
+
+ Increase the indentation of the code by a given number of spaces
+
+ Args:
+ amount (int): number of spaces to indent
+ python_code (list[str]): code to indent
+
+ Returns:
+ list[str]: indented code
+ """
+ return [" " * amount + line for line in python_code]
+
+
+def code_location(
+ object_name: str,
+ object_type: Literal["class", "method", "function"],
+ python_code: list[str],
+ range: tuple[int, int] = (0, 1000000),
+) -> Union[tuple[int, int], None]:
+ """locate python code within a module
+
+ Finds the start and end lines for a class, method or function within a
+ larger python module. Method names must be specificed as 'class.method'
+ to ensure they are unique.
+
+ Args:
+ object_name (str): name of python function - methods are specified as class.method
+ object_type (Literal["class","method","function"]): type of code object to extract
+ python_code (list[str]): python code
+ range (range: tuple[int, int]): search range. Defaults to entire module.
+
+ Raises:
+ ValueError: invalid object type
+ ValueError: badly formed method name
+
+ Returns:
+ Union[tuple[int, int],None]: either a (start,end) tuple or None if not found
+ """
+ if object_type not in ["class", "method", "function"]:
+ raise ValueError("object type must be one of 'class', 'method', or 'function'")
+
+ if object_type == "method" and len(object_name.split(".")) != 2:
+ raise ValueError("method names must be specified as 'class.method'")
+
+ object_dictionary = {"class": "class", "method": "def", "function": "def"}
+
+ # Methods are only unique within a class, so extract from just the class code
+ if object_type == "method":
+ class_name, object_to_find = tuple(object_name.split("."))
+ search_start, search_end = code_location(class_name, "class", python_code)
+ else:
+ object_to_find = object_name
+ search_start, search_end = range
+
+ object_key_word = object_dictionary[object_type]
+ if object_type == "function":
+ object_pattern = re.compile(rf"^{object_key_word}\s+{object_to_find}\(")
+ else:
+ object_pattern = re.compile(rf"^\s*{object_key_word}\s+{object_to_find}\(")
+
+ line_numbers = []
+ found = False
+ for line_number, line in enumerate(python_code):
+ # method are only unique within a class
+ if not (search_start < line_number < search_end):
+ continue
+ if not found:
+ found = object_pattern.match(line)
+ if found:
+ indent = re.match(r"^\s*", line).group()
+ # this regex is a negative lookahead assertion that looks
+ # for non white space or a non closing brace (from the input parameters)
+ end_of_function_pattern = re.compile(rf"^{indent}(?!\s|\))")
+ else:
+ found = not end_of_function_pattern.match(line)
+ if found:
+ line_numbers.append(line_number)
+
+ if line_numbers:
+ locations = (line_numbers[0], line_numbers[-1])
+ else:
+ locations = None
+ return locations
+
+
+def extract_code(
+ object_name: str,
+ object_type: Literal["class", "method", "function"],
+ python_code: list[str],
+) -> list[str]:
+ """Extract a class, method or function from python code
+
+ Args:
+ object_name (str): name of python function - methods are specified as class.method
+ object_type (Literal["class","method","function"]): type of code object to extract
+ python_code (list[str]): python code
+
+ Returns:
+ list[str]: code from just this object
+ """
+ code_range = code_location(object_name, object_type, python_code)
+ if code_range is None:
+ object_code = []
+ else:
+ object_code = python_code[code_range[0] : code_range[1]]
+ return object_code
+
+
+def prepare_extensions(python_code: list[str]) -> dict[list[dict]]:
+ """Prepare monkeypatched file
+
+ Return a data structure with the python code separated by class and method
+ with the monkeypatched method name replacing the function name.
+ dict[class:list[dict[method:list[str]]]]
+
+ Args:
+ python_code (list[str]): original python code
+
+ Returns:
+ dict[list[dict]]: converted python code organized by class and method
+ """
+ # Find all functions
+ all_functions = []
+ function_pattern = re.compile(r"^def\s+([a-zA-Z_]+)\(")
+ for line_num, line in enumerate(python_code):
+ function_match = function_pattern.match(line)
+ if function_match:
+ all_functions.append(function_match.group(1))
+
+ # Build a monkeypatch dictionary of {function: class.method}
+ monkeypatch_pattern = re.compile(
+ r"^([A-Z][a-zA-Z_]*.[a-zA-Z_]+)\s*=\s*([a-zA-Z_]+)\s*$"
+ )
+ monkeypatches = {}
+ monkeypatch_line_numbers = []
+ for line_num, line in enumerate(python_code):
+ monkeypatch_match = monkeypatch_pattern.match(line)
+ if monkeypatch_match:
+ monkeypatches[monkeypatch_match.group(2)] = monkeypatch_match.group(1)
+ monkeypatch_line_numbers.append(line_num)
+
+ # Find the real functions that aren't monkeypatched into a class
+ pure_functions = [f for f in all_functions if f not in list(monkeypatches.keys())]
+
+ # Remove the monkey patches from the code
+ monkeypatch_line_numbers.reverse()
+ for line_num in monkeypatch_line_numbers:
+ python_code.pop(line_num)
+
+ # Separate the code into return data structure
+ code_dictionary = {}
+ for function_name, class_method in monkeypatches.items():
+ method_code = {}
+ class_name, _sep, method_name = class_method.partition(".")
+ method_code[method_name] = extract_code(function_name, "function", python_code)
+ method_code[method_name][0] = method_code[method_name][0].replace(
+ function_name, method_name
+ )
+ # Due to differences in imports, these lines need to uncommented
+ if method_name == "toLocalCoords":
+ method_code[method_name] = [
+ line.replace(
+ "# from .shapes import Shape",
+ "from .shapes import Shape",
+ )
+ for line in method_code[method_name]
+ ]
+ if method_name == "textOnPath":
+ method_code[method_name] = [
+ line.replace(
+ "# from .selectors import DirectionMinMaxSelector",
+ "from .selectors import DirectionMinMaxSelector",
+ )
+ for line in method_code[method_name]
+ ]
+ # Now that the code has been modified, add it code dictionary
+ if class_name in code_dictionary:
+ code_dictionary[class_name].append(method_code)
+ else:
+ code_dictionary[class_name] = [method_code]
+
+ for function_name in pure_functions:
+ function_code = {}
+ function_code[function_name] = extract_code(
+ function_name, "function", python_code
+ )
+ if "Module" in code_dictionary:
+ code_dictionary["Module"].append(function_code)
+ else:
+ code_dictionary["Module"] = [function_code]
+
+ return code_dictionary
+
+
+def update_source_code(
+ extensions_code_dictionary: dict[list[dict]],
+ source_file_name: str,
+ source_file_location: str,
+):
+ """update_source_code
+
+ Insert the extensions source code into the cadquery source code
+
+ Args:
+ extensions_code_dictionary (dict[list[dict]]): extensions.py source code
+ source_file_name (str): name of source file
+ source_file_location (str): location of source file
+
+ Returns:
+ list(str): updated source code
+ """
+ with open(source_file_location) as f:
+ source_code = f.readlines()
+
+ for class_name in class_files[source_file_name]:
+ method_dictionaries = extensions_code_dictionary[class_name]
+ extension_methods = []
+ for method_dictionary in method_dictionaries:
+ for method_name, method_code in method_dictionary.items():
+ if class_name == "Module":
+ code_range = code_location(method_name, "function", source_code)
+ else:
+ code_range = code_location(
+ class_name + "." + method_name, "method", source_code
+ )
+ if code_range is None and class_name == "Module":
+ source_size = len(source_code)
+ source_code[source_size:source_size] = method_code
+ elif code_range is None:
+ extension_methods.append(method_name)
+ else:
+ # Delete the old code
+ del source_code[code_range[0] : code_range[1]]
+ # Insert the new code
+ source_code[code_range[0] : code_range[0]] = increase_indent(
+ 4, method_code
+ )
+
+ # Create the code block that needs to be inserted into this class
+ extension_code = []
+ for extension_method in extension_methods:
+ for method_dictionary in method_dictionaries:
+ if extension_method in method_dictionary.keys():
+ extension_code.extend(method_dictionary[extension_method])
+ if class_name != "Module":
+ extension_code = increase_indent(4, extension_code)
+
+ if class_name == "Module":
+ class_end = len(source_code) - 1
+ else:
+ _class_start, class_end = code_location(class_name, "class", source_code)
+ source_code[class_end + 1 : class_end + 1] = extension_code
+ return source_code
+
+
+def versiontuple(v):
+ return tuple(map(int, (v.split("."))))
+
+
+def main(argv):
+ # Find the location of cadquery
+ cadquery_path = os.path.dirname(cadquery.__file__)
+
+ # Does the cadquery path exist and point to cadquery
+ if not os.path.isfile(os.path.join(cadquery_path, "cq.py")):
+ print(f"{cadquery_path} is invalid - cq.py should be in this directory")
+ sys.exit(2)
+
+ # Find the location and version of cq_warehouse
+ pip_command = subprocess.run(
+ ["python", "-m", "pip", "show", "cq_warehouse"], capture_output=True
+ )
+ if pip_command.stderr:
+ raise RuntimeError(pip_command.stderr.decode("utf-8"))
+
+ pip_command_dictionary = dict(
+ entry.split(": ", 1)
+ for entry in pip_command.stdout.decode("utf-8").split("\n")
+ if ":" in entry
+ )
+
+ # Verify cq_warehouse version
+ extensions_version = pip_command_dictionary["Version"]
+ if versiontuple(extensions_version) < versiontuple("0.5.2"):
+ raise RuntimeError(
+ f"Version error - cq_warehouse version {extensions_version} must be >= 0.5.2"
+ )
+
+ # Read the cq_warehouse extensions.py file
+ extensions_path = os.path.join(
+ pip_command_dictionary["Location"], "cq_warehouse/extensions.py"
+ )
+ with open(extensions_path) as f:
+ extensions_python_code = f.readlines()
+
+ # Prepare a location to diff the original and extended versions
+ temp_directory = tempfile.TemporaryDirectory()
+ temp_directory_path = temp_directory.name
+ original_directory_path = os.path.join(temp_directory_path, "original")
+ extended_directory_path = os.path.join(temp_directory_path, "extensions")
+ shutil.copytree(cadquery_path, original_directory_path)
+ shutil.copytree(cadquery_path, extended_directory_path)
+
+ # Organize the extensions monkeypatched code into class(s), method(s)
+ extensions_code_dictionary = prepare_extensions(extensions_python_code)
+
+ # Update existing methods and add new ones for each of the source files
+ for source_file_name in class_files.keys():
+ source_file_location = os.path.join(cadquery_path, source_file_name)
+ source_code = update_source_code(
+ extensions_code_dictionary, source_file_name, source_file_location
+ )
+
+ # Write extended source file
+ extended_file_name = (
+ os.path.basename(source_file_name).split(".py")[0] + "_extended.py"
+ )
+ f = open(extended_file_name, "w")
+ f.writelines(source_code)
+ f.close()
+
+ # Run black on the resulting file to ensure formatting is correct
+ # .. danger of format changes polluting the patch
+ # subprocess.run(["black", output_file_name])
+
+ # Replace the original files in the extensions temp directory
+ shutil.copyfile(
+ extended_file_name, os.path.join(extended_directory_path, source_file_name)
+ )
+ # Copy the extended files into the cq_warehouse source directory for reference
+ shutil.copyfile(
+ extended_file_name,
+ os.path.join(
+ pip_command_dictionary["Location"], "cq_warehouse", extended_file_name
+ ),
+ )
+
+ # Create the patch file
+ patch_file_name = "cadquery_extensions" + extensions_version + ".patch"
+ with open(patch_file_name, "w") as patch_file:
+ subprocess.run(
+ [
+ "diff",
+ "-rN",
+ "-U",
+ "5",
+ original_directory_path,
+ extended_directory_path,
+ ],
+ stdout=patch_file,
+ )
+ # Copy the patch to the cadquery original source directory
+ shutil.copyfile(
+ patch_file_name,
+ os.path.join(cadquery_path, patch_file_name),
+ )
+
+ print(
+ f"Created the {patch_file_name} file and copied it to cadquery source directory"
+ )
+ print("To apply the patch:")
+ print(f" cd {cadquery_path}")
+ print(f" patch -s -p4 < {patch_file_name}")
+ print("To reverse the patch:")
+ print(f" patch -R -p4 < {patch_file_name}")
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/scripts/build_extensions_doc.py b/scripts/build_extensions_doc.py
new file mode 100644
index 0000000..e043181
--- /dev/null
+++ b/scripts/build_extensions_doc.py
@@ -0,0 +1,320 @@
+"""
+Build CadQuery Patch
+
+name: build_cadquery_patch.py
+by: Gumyr
+date: January 27th 2022
+
+desc:
+
+ In order for cq_warehouse CadQuery extensions to be recognized by IDEs (e.g. Intellisense)
+ the CadQuery source code needs to be updated. Although monkeypatching allows
+ for the functionality of CadQuery to be extended, these extensions are not
+ visible to the IDE which makes working with them more difficult.
+
+ This code takes the cq_warehouse extensions.py file, reformats it to fit into
+ the CadQuery source code, applies changes to official Cadquery source files
+ and generates extended versions of these files:
+ - assembly.py,
+ - cq.py,
+ - geom.py, and
+ - shapes.py.
+ Finally, a diff is generated between the originals and extended files for use
+ with the patch command.
+
+ Usage:
+ > python build_cadquery_patch
+
+ Note: this code assumes black formatting of the python files
+
+todo: Add support for extension methods with decorators
+todo: Add an option to save the extended files
+
+license:
+
+ Copyright 2022 Gumyr
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+"""
+import sys
+import getopt
+import os
+import re
+from tokenize import PlainToken
+from typing import Literal, Union
+import subprocess
+import tempfile
+import shutil
+
+
+def increase_indent(amount: int, python_code: list[str]) -> list[str]:
+ """Increase indentation
+
+ Increase the indentation of the code by a given number of spaces
+
+ Args:
+ amount (int): number of spaces to indent
+ python_code (list[str]): code to indent
+
+ Returns:
+ list[str]: indented code
+ """
+ return [" " * amount + line for line in python_code]
+
+
+def code_location(
+ object_name: str,
+ object_type: Literal["class", "method", "function"],
+ python_code: list[str],
+ range: tuple[int, int] = (0, 1000000),
+) -> Union[tuple[int, int], None]:
+ """locate python code within a module
+
+ Finds the start and end lines for a class, method or function within a
+ larger python module. Method names must be specificed as 'class.method'
+ to ensure they are unique.
+
+ Args:
+ object_name (str): name of python function - methods are specified as class.method
+ object_type (Literal["class","method","function"]): type of code object to extract
+ python_code (list[str]): python code
+ range (range: tuple[int, int]): search range. Defaults to entire module.
+
+ Raises:
+ ValueError: invalid object type
+ ValueError: badly formed method name
+
+ Returns:
+ Union[tuple[int, int],None]: either a (start,end) tuple or None if not found
+ """
+ if object_type not in ["class", "method", "function"]:
+ raise ValueError("object type must be one of 'class', 'method', or 'function'")
+
+ if object_type == "method" and len(object_name.split(".")) != 2:
+ raise ValueError("method names must be specified as 'class.method'")
+
+ object_dictionary = {"class": "class", "method": "def", "function": "def"}
+
+ # Methods are only unique within a class, so extract from just the class code
+ if object_type == "method":
+ class_name, object_to_find = tuple(object_name.split("."))
+ search_start, search_end = code_location(class_name, "class", python_code)
+ else:
+ object_to_find = object_name
+ search_start, search_end = range
+
+ object_key_word = object_dictionary[object_type]
+ if object_type == "function":
+ object_pattern = re.compile(rf"^{object_key_word}\s+{object_to_find}\(")
+ else:
+ object_pattern = re.compile(rf"^\s*{object_key_word}\s+{object_to_find}\(")
+
+ line_numbers = []
+ found = False
+ for line_number, line in enumerate(python_code):
+ # method are only unique within a class
+ if not (search_start < line_number < search_end):
+ continue
+ if not found:
+ found = object_pattern.match(line)
+ if found:
+ indent = re.match(r"^\s*", line).group()
+ # this regex is a negative lookahead assertion that looks
+ # for non white space or a non closing brace (from the input parameters)
+ end_of_function_pattern = re.compile(rf"^{indent}(?!\s|\))")
+ else:
+ found = not end_of_function_pattern.match(line)
+ if found:
+ line_numbers.append(line_number)
+
+ if line_numbers:
+ locations = (line_numbers[0], line_numbers[-1])
+ else:
+ locations = None
+ return locations
+
+
+def extract_code(
+ object_name: str,
+ object_type: Literal["class", "method", "function"],
+ python_code: list[str],
+) -> list[str]:
+ """Extract a class, method or function from python code
+
+ Args:
+ object_name (str): name of python function - methods are specified as class.method
+ object_type (Literal["class","method","function"]): type of code object to extract
+ python_code (list[str]): python code
+
+ Returns:
+ list[str]: code from just this object
+ """
+ code_range = code_location(object_name, object_type, python_code)
+ if code_range is None:
+ object_code = []
+ else:
+ object_code = python_code[code_range[0] : code_range[1]]
+ return object_code
+
+
+def prepare_extensions(python_code: list[str]) -> dict[list[dict]]:
+ """Prepare monkeypatched file
+
+ Return a data structure with the python code separated by class and method
+ with the monkeypatched method name replacing the function name.
+ dict[class:list[dict[method:list[str]]]]
+
+ Args:
+ python_code (list[str]): original python code
+
+ Returns:
+ dict[list[dict]]: converted python code organized by class and method
+ """
+ # Find all functions
+ all_functions = []
+ function_pattern = re.compile(r"^def\s+([a-zA-Z_]+)\(")
+ for line_num, line in enumerate(python_code):
+ function_match = function_pattern.match(line)
+ if function_match:
+ all_functions.append(function_match.group(1))
+
+ # Build a monkeypatch dictionary of {function: class.method}
+ monkeypatch_pattern = re.compile(
+ r"^([A-Z][a-zA-Z_]*.[a-zA-Z_]+)\s*=\s*([a-zA-Z_]+)\s*$"
+ )
+ monkeypatches = {}
+ monkeypatch_line_numbers = []
+ for line_num, line in enumerate(python_code):
+ monkeypatch_match = monkeypatch_pattern.match(line)
+ if monkeypatch_match:
+ monkeypatches[monkeypatch_match.group(2)] = monkeypatch_match.group(1)
+ monkeypatch_line_numbers.append(line_num)
+
+ # Find the real functions that aren't monkeypatched into a class
+ pure_functions = [f for f in all_functions if f not in list(monkeypatches.keys())]
+
+ # Remove the monkey patches from the code
+ monkeypatch_line_numbers.reverse()
+ for line_num in monkeypatch_line_numbers:
+ python_code.pop(line_num)
+
+ # Separate the code into return data structure
+ code_dictionary = {}
+ for function_name, class_method in monkeypatches.items():
+ method_code = {}
+ class_name, _sep, method_name = class_method.partition(".")
+ method_code[method_name] = extract_code(function_name, "function", python_code)
+ method_code[method_name][0] = method_code[method_name][0].replace(
+ function_name, method_name
+ )
+ # Due to differences in imports, these lines need to uncommented
+ if method_name == "toLocalCoords":
+ method_code[method_name] = [
+ line.replace(
+ "# from .shapes import Shape",
+ "from .shapes import Shape",
+ )
+ for line in method_code[method_name]
+ ]
+ if method_name == "textOnPath":
+ method_code[method_name] = [
+ line.replace(
+ "# from .selectors import DirectionMinMaxSelector",
+ "from .selectors import DirectionMinMaxSelector",
+ )
+ for line in method_code[method_name]
+ ]
+ # Now that the code has been modified, add it code dictionary
+ if class_name in code_dictionary:
+ code_dictionary[class_name].append(method_code)
+ else:
+ code_dictionary[class_name] = [method_code]
+
+ for function_name in pure_functions:
+ function_code = {}
+ function_code[function_name] = extract_code(
+ function_name, "function", python_code
+ )
+ if "Module" in code_dictionary:
+ code_dictionary["Module"].append(function_code)
+ else:
+ code_dictionary["Module"] = [function_code]
+
+ return code_dictionary
+
+
+def only_header(python_code: list[str]) -> list[str]:
+ docstring_count = 0
+ filtered_code = []
+ for line in python_code:
+ filtered_code.append(line)
+ if '"""' in line:
+ docstring_count += 1
+ if docstring_count == 2:
+ break
+ return filtered_code
+
+
+def main(argv):
+
+ # Find the cq_warehouse extensions.py file and read it
+ pip_command = subprocess.run(
+ ["python", "-m", "pip", "show", "cq_warehouse"], capture_output=True
+ )
+ pip_command_dictionary = dict(
+ entry.split(": ", 1)
+ for entry in pip_command.stdout.decode("utf-8").split("\n")
+ if ":" in entry
+ )
+ extensions_path = os.path.join(
+ pip_command_dictionary["Location"], "cq_warehouse/extensions.py"
+ )
+ with open(extensions_path) as doc_file:
+ extensions_python_code = doc_file.readlines()
+
+ # Organize the extensions monkeypatched code into class(s), method(s)
+ extensions_code_dictionary = prepare_extensions(extensions_python_code)
+
+ doc_file_path = os.path.join(
+ pip_command_dictionary["Location"], "cq_warehouse/extensions_doc.py"
+ )
+ print(f"Creating extensions documentation file: {doc_file_path}")
+ doc_file = open(doc_file_path, "w")
+ doc_file.writelines(
+ [
+ "from typing import Union, Tuple, Optional, Literal\n"
+ "from fastener import Screw, Nut, Washer\n"
+ "class gp_Ax1:\n pass\n",
+ "class T:\n pass\n",
+ "class VectorLike:\n pass\n",
+ "class BoundBox:\n pass\n",
+ "class Solid:\n pass\n",
+ "class Compound:\n pass\n",
+ "class Location:\n pass\n",
+ ]
+ )
+ for class_name, method_dictionaries in extensions_code_dictionary.items():
+ if class_name != "Module":
+ doc_file.writelines([f"class {class_name}(object):\n"])
+ for method_dictionary in method_dictionaries:
+ for method_name, method_code in method_dictionary.items():
+ if class_name == "Module":
+ doc_file.writelines(only_header(method_code))
+ else:
+ doc_file.writelines(only_header(increase_indent(4, method_code)))
+ doc_file.close()
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/setup.cfg b/setup.cfg
index 3104612..d96beb1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = cq_warehouse
-version = 0.5.1
+version = 0.5.2
author = Gumyr
author_email = gumyr9@gmail.com
description = A cadquery parametric part collection
@@ -20,7 +20,6 @@ package_dir =
packages = find:
python_requires = >=3.9
install_requires =
- pydantic
include_package_data = True
[options.packages.find]
where = src
diff --git a/src/cq_warehouse.egg-info/PKG-INFO b/src/cq_warehouse.egg-info/PKG-INFO
index 571c50e..80b70e6 100644
--- a/src/cq_warehouse.egg-info/PKG-INFO
+++ b/src/cq_warehouse.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: cq-warehouse
-Version: 0.5.1
+Version: 0.5.2
Summary: A cadquery parametric part collection
Home-page: https://github.com/gumyr/cq_warehouse
Author: Gumyr
@@ -16,1259 +16,25 @@ Description-Content-Type: text/markdown
License-File: LICENSE
-
-The cq_warehouse python/cadquery package contains a set of parametric parts which can
-be customized and used within your projects or saved to a CAD file
-in STEP or STL format for use in a wide variety of CAD
-or CAM systems.
+
-# Table of Contents
-- [Table of Contents](#table-of-contents)
-- [Installation](#installation)
-- [Package Structure](#package-structure)
- - [sprocket sub-package](#sprocket-sub-package)
- - [Input Parameters](#input-parameters)
- - [Instance Variables](#instance-variables)
- - [Methods](#methods)
- - [Tooth Tip Shape](#tooth-tip-shape)
- - [chain sub-package](#chain-sub-package)
- - [Input Parameters](#input-parameters-1)
- - [Instance Variables](#instance-variables-1)
- - [Methods](#methods-1)
- - [Future Enhancements](#future-enhancements)
- - [drafting sub-package](#drafting-sub-package)
- - [dimension_line](#dimension_line)
- - [extension_line](#extension_line)
- - [callout](#callout)
- - [thread sub-package](#thread-sub-package)
- - [Thread](#thread)
- - [IsoThread](#isothread)
- - [AcmeThread](#acmethread)
- - [MetricTrapezoidalThread](#metrictrapezoidalthread)
- - [TrapezoidalThread](#trapezoidalthread)
- - [PlasticBottleThread](#plasticbottlethread)
- - [fastener sub-package](#fastener-sub-package)
- - [Nut](#nut)
- - [Nut Selection](#nut-selection)
- - [Derived Nut Classes](#derived-nut-classes)
- - [Screw](#screw)
- - [Screw Selection](#screw-selection)
- - [Derived Screw Classes](#derived-screw-classes)
- - [Washer](#washer)
- - [Washer Selection](#washer-selection)
- - [Derived Washer Classes](#derived-washer-classes)
- - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
- - [API](#api)
- - [Fastener Locations](#fastener-locations)
- - [API](#api-1)
- - [Bill of Materials](#bill-of-materials)
- - [Extending the fastener sub-package](#extending-the-fastener-sub-package)
- - [extensions sub-package](#extensions-sub-package)
- - [Assembly class extensions](#assembly-class-extensions)
- - [Translate](#translate)
- - [Rotate](#rotate)
- - [Plane class extensions](#plane-class-extensions)
- - [Transform to Local Coordinates](#transform-to-local-coordinates)
- - [Vector class extensions](#vector-class-extensions)
- - [Rotate about X,Y and Z Axis](#rotate-about-xy-and-z-axis)
- - [Map 2D Vector to 3D Vector](#map-2d-vector-to-3d-vector)
- - [Translate to Vertex](#translate-to-vertex)
- - [Get Signed Angle between Vectors](#get-signed-angle-between-vectors)
- - [Vertex class extensions](#vertex-class-extensions)
- - [Add](#add)
- - [Subtract](#subtract)
- - [Display](#display)
- - [Convert to Vector](#convert-to-vector)
- - [Workplane class extensions](#workplane-class-extensions)
- - [Text on 2D Path](#text-on-2d-path)
- - [Hex Array](#hex-array)
- - [Thicken Non-Planar Face](#thicken-non-planar-face)
- - [Face class extensions](#face-class-extensions)
- - [Thicken](#thicken)
- - [Project Face to Shape](#project-face-to-shape)
- - [Emboss Face To Shape](#emboss-face-to-shape)
- - [Wire class extensions](#wire-class-extensions)
- - [Make Non Planar Face](#make-non-planar-face)
- - [Project Wire to Shape](#project-wire-to-shape)
- - [Emboss Wire to Shape](#emboss-wire-to-shape)
- - [Edge class extensions](#edge-class-extensions)
- - [Project Edge to Shape](#project-edge-to-shape)
- - [Emboss Edge to Shape](#emboss-edge-to-shape)
- - [Shape class extensions](#shape-class-extensions)
- - [Find Intersection](#find-intersection)
- - [Project Text on Shape](#project-text-on-shape)
- - [Emboss Text on Shape](#emboss-text-on-shape)
-# Installation
-Install from github:
-```
-python3 -m pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
-```
-Note that cq_warehouse requires the development version of cadquery (see [Installing CadQuery](https://cadquery.readthedocs.io/en/latest/installation.html)). Also note that cq_warehouse uses the pydantic package for input validation which requires keyword arguments (e.g. `num_teeth=16`).
-# Package Structure
-The cq_warehouse package contains the following sub-packages:
-- **sprocket** : a parametric sprocket generator
-- **chain** : a parametric chain generator
-- **drafting** : a set of methods used for documenting cadquery objects
-- **thread** : a parametric thread fastener generator
-- **fastener** : a parametric threaded fastener generator
-- **extensions** : a set of enhancements to the core cadquery system
-
-## sprocket sub-package
-A sprocket can be generated and saved to a STEP file with just four lines
-of python code using the `Sprocket` class:
-```python
-import cadquery as cq
-from cq_warehouse.sprocket import Sprocket
-
-sprocket32 = Sprocket(num_teeth=32)
-cq.exporters.export(sprocket32.cq_object,"sprocket.step")
-```
-How does this code work?
-1. The first line imports cadquery CAD system with the alias cq
-2. The second line imports the Sprocket class from the sprocket sub-package of the cq_warehouse package
-3. The third line instantiates a 32 tooth sprocket named sprocket32
-4. The fourth line uses the cadquery exporter functionality to save the generated
-sprocket object in STEP format
-
-Note that instead of exporting sprocket32, sprocket32.cq_object is exported as
-sprocket32 contains much more than just the raw CAD object - it contains all of
-the parameters used to generate this sprocket - such as the chain pitch - and some
-derived information that may be useful - such as the chain pitch radius.
-
-### Input Parameters
-Most of the Sprocket parameters are shown in the following diagram:
-
-![sprocket parameters](doc/sprocket_dimensions.png)
-
-The full set of Sprocket input parameters are as follows:
-- `num_teeth` (int) : the number of teeth on the perimeter of the sprocket (must be >= 3)
-- `chain_pitch` (float) : the distance between the centers of two adjacent rollers - default 1/2" - (pitch in the diagram)
-- `roller_diameter` (float) : the size of the cylindrical rollers within the chain - default 5/16" - (roller in the diagram)
-- `clearance` (float) : the size of the gap between the chain's rollers and the sprocket's teeth - default 0.0
-- `thickness` (float) : the thickness of the sprocket - default 0.084"
-- `bolt_circle_diameter` (float) : the diameter of the mounting bolt hole pattern - default 0.0 - (bcd in the diagram)
-- `num_mount_bolts` (int) : the number of bolt holes - default 0 - if 0, no bolt holes are added to the sprocket
-- `mount_bolt_diameter` (float) : the size of the bolt holes use to mount the sprocket - default 0.0 - (bolt in the diagram)
-- `bore_diameter` (float) : the size of the central hole in the sprocket - default 0.0 - if 0, no bore hole is added to the sprocket (bore in the diagram)
-
----
-**NOTE**
-Default parameters are for standard single sprocket bicycle chains.
-
----
-The sprocket in the diagram was generated as follows:
-```python
-MM = 1
-chain_ring = Sprocket(
- num_teeth = 32,
- clearance = 0.1*MM,
- bolt_circle_diameter = 104*MM,
- num_mount_bolts = 4,
- mount_bolt_diameter = 10*MM,
- bore_diameter = 80*MM
-)
-```
----
-**NOTE**
-Units in cadquery are defined so that 1 represents one millimeter but `MM = 1` makes this
-explicit.
-
----
-### Instance Variables
-In addition to all of the input parameters that are stored as instance variables
-within the Sprocket instance there are four derived instance variables:
-- `pitch_radius` (float) : the radius of the circle formed by the center of the chain rollers
-- `outer_radius` (float) : the size of the sprocket from center to tip of the teeth
-- `pitch_circumference` (float) : the circumference of the sprocket at the pitch rad
-- `cq_object` (cq.Workplane) : the cadquery sprocket object
-
-### Methods
-The Sprocket class defines two static methods that may be of use when designing systems with sprockets: calculation of the pitch radius and pitch circumference as follows:
-```python
-@staticmethod
-def sprocket_pitch_radius(num_teeth:int, chain_pitch:float) -> float:
- """
- Calculate and return the pitch radius of a sprocket with the given number of teeth
- and chain pitch
-
- Parameters
- ----------
- num_teeth : int
- the number of teeth on the perimeter of the sprocket
- chain_pitch : float
- the distance between two adjacent pins in a single link (default 1/2 INCH)
- """
-
-@staticmethod
-def sprocket_circumference(num_teeth:int, chain_pitch:float) -> float:
- """
- Calculate and return the pitch circumference of a sprocket with the given number of
- teeth and chain pitch
-
- Parameters
- ----------
- num_teeth : int
- the number of teeth on the perimeter of the sprocket
- chain_pitch : float
- the distance between two adjacent pins in a single link (default 1/2 INCH)
- """
-```
-### Tooth Tip Shape
-Normally the tip of a sprocket tooth has a circular section spanning the roller pin sockets
-on either side of the tooth tip. In this case, the tip is chamfered to allow the chain to
-easily slide over the tooth tip thus reducing the chances of derailing the chain in normal
-operation. However, it is valid to generate a sprocket without this flat
section by
-increasing the size of the rollers. In this case, the tooth tips will be spiky
and
-will not be chamfered.
-## chain sub-package
-A chain wrapped around a set of sprockets can be generated with the `Chain` class by providing
-the size and locations of the sprockets, how the chain wraps and optionally the chain parameters.
-
-For example, one can create the chain for a bicycle with a rear deraileur as follows:
-```python
-import cadquery as cq
-import cq_warehouse.chain as Chain
-
-derailleur_chain = Chain(
- spkt_teeth=[32, 10, 10, 16],
- positive_chain_wrap=[True, True, False, True],
- spkt_locations=[
- (0, 158.9*MM, 50*MM),
- (+190*MM, 0, 50*MM),
- (+190*MM, 78.9*MM, 50*MM),
- (+205*MM, 158.9*MM, 50*MM)
- ]
-)
-if "show_object" in locals():
- show_object(derailleur_chain.cq_object, name="derailleur_chain")
-```
-### Input Parameters
-The complete set of input parameters are:
-- `spkt_teeth` (list of int) : a list of the number of teeth on each sprocket the chain will wrap around
-- `spkt_locations` (list of cq.Vector or tuple(x,y) or tuple(x,y,z)) : the location of the sprocket centers
-- `positive_chain_wrap` (list of bool) : the direction chain wraps around the sprockets, True for counter clock wise viewed from positive Z
-- `chain_pitch` (float) : the distance between two adjacent pins in a single link - default 1/2"
-- `roller_diameter` (float) : the size of the cylindrical rollers within the chain - default 5/16"
-- `roller_length` (float) : the distance between the inner links, i.e. the length of the link rollers - default 3/32"
-- `link_plate_thickness` (float) : the thickness of the link plates (both inner and outer link plates) - default 1mm
-
-The chain is created on the XY plane (methods to move the chain are described below)
-with the sprocket centers being described by:
-- a two dimensional tuple (x,y)
-- a three dimensional tuple (x,y,z) which will result in the chain being created parallel
-to the XY plane, offset by z
-- the cadquery Vector class which will displace the chain by Vector.z
-
-To control the path of the chain between the sprockets, the user must indicate the desired
-direction for the chain to wrap around the sprocket. This is done with the `positive_chain_wrap`
-parameter which is a list of boolean values - one for each sprocket - indicating a counter
-clock wise or positive angle around the z-axis when viewed from the positive side of the XY
-plane. The following diagram illustrates the most complex chain path where the chain
-traverses wraps from positive to positive, positive to negative, negative to positive and
-negative to negative directions (`positive_chain_wrap` values are shown within the arrows
-starting from the largest sprocket):
-
-![chain direction](doc/chain_direction.png)
-
-Note that the chain is perfectly tight as it wraps around the sprockets and does not support any slack. Therefore, as the chain wraps back around to the first link it will either overlap or gap this link - this can be seen in the above figure at the top of the largest sprocket. Adjust the locations of the sprockets to control this value.
-
-### Instance Variables
-In addition to all of the input parameters that are stored as instance variables within the Chain instance there are seven derived instance variables:
-- `pitch_radii` (list of float) : the radius of the circle formed by the center of the chain rollers on each sprocket
-- `chain_links` (float) : the length of the chain in links
-- `num_rollers` (int) : the number of link rollers in the entire chain
-- `roller_loc` (list of cq.Vector) : the location of each roller in the chain
-- `chain_angles` (list of tuple(float,float)) : the chain entry and exit angles in degrees for each sprocket
-- `spkt_initial_rotation` (list of float) : angle in degrees to rotate each sprocket in-order to align the teeth with the gaps in the chain
-- `cq_object` (cq.Assembly) : the cadquery chain object
-
-### Methods
-The Chain class defines two methods:
-- a static method used to generate chain links cadquery objects, and
-- an instance method that will build a cadquery assembly for a chain given a set of sprocket
-cadquery objects.
-Note that the make_link instance method uses the @cache decorator to greatly improve the rate at
-links can be generated as a chain is composed of many copies of the links.
-
-```python
-def assemble_chain_transmission(self,spkts:list[Union[cq.Solid, cq.Workplane]]) -> cq.Assembly:
- """
- Create the transmission assembly from sprockets for a chain
-
- Parameters
- ----------
- spkts : list of cq.Solid or cq:Workplane
- the sprocket cadquery objects to combine with the chain to build a transmission
- """
-
-@staticmethod
-@cache
-def make_link(
- chain_pitch:float = 0.5*INCH,
- link_plate_thickness:float = 1*MM,
- inner:bool = True,
- roller_length:float = (3/32)*INCH,
- roller_diameter:float = (5/16)*INCH
- ) -> cq.Workplane:
- """
- Create either inner or outer link pairs. Inner links include rollers while
- outer links include fake roller pins.
-
- Parameters
- ----------
- chain_pitch : float = (1/2)*INCH
- # the distance between the centers of two adjacent rollers
- link_plate_thickness : float = 1*MM
- # the thickness of the plates which compose the chain links
- inner : bool = True
- # inner links include rollers while outer links include roller pins
- roller_length : float = (3/32)*INCH,
- # the spacing between the inner link plates
- roller_diameter : float = (5/16)*INCH
- # the size of the cylindrical rollers within the chain
- """
-```
-
-Once a chain or complete transmission has been generated it can be re-oriented as follows:
-```python
-two_sprocket_chain = Chain(
- spkt_teeth = [32, 32],
- positive_chain_wrap = [True, True],
- spkt_locations = [ (-5*INCH, 0), (+5*INCH, 0) ]
-)
-relocated_transmission = two_sprocket_chain.assemble_chain_transmission(
- spkts = [spkt32.cq_object, spkt32.cq_object]
-).rotate(axis=(0,1,1),angle=45).translate((20, 20, 20))
-```
-### Future Enhancements
-Two future enhancements are being considered:
-1. Non-planar chains - If the sprocket centers contain `z` values, the chain would follow the path of a spline between the sockets to approximate the path of a bicycle chain where the front and read sprockets are not in the same plane. Currently, the `z` values of the first sprocket define the `z` offset of the entire chain.
-2. Sprocket Location Slots - Typically on or more of the sprockets in a chain transmission will be adjustable to allow the chain to be tight around the
-sprockets. This could be implemented by allowing the user to specify a pair
-of locations defining a slot for a given sprocket indicating that the sprocket
-location should be selected somewhere along this slot to create a perfectly
-fitting chain.
-## drafting sub-package
-A class used to document cadquery designs by providing three methods that create objects that can be included into the design illustrating marked dimension_lines or notes.
-
-For example:
-```python
-import cadquery as cq
-from cq_warehouse.drafting import Draft
-
-# Import an object to be dimensioned
-mystery_object = cq.importers.importStep("mystery.step")
-
-# Create drawing instance with appropriate settings
-metric_drawing = Draft(decimal_precision=1)
-
-# Create an extension line from corners of the part
-length_dimension_line = metric_drawing.extension_line(
- object_edge=mystery_object.faces(" :hourglass: **CQ-editor** :hourglass: You can increase the Preferences :arrow_right: 3D Viewer :arrow_right: Deviation parameter to improve performance by slightly compromising accuracy.
-
-All of the fasteners default to right-handed thread but each of them provide a `hand` string parameter which can either be `"right"` or `"left"`.
-
-All of the fastener classes provide a `cq_object` instance variable which contains the cadquery object.
-
-The following sections describe each of the provided classes.
-
-### Nut
-As the base class of all other nut and bolt classes, all of the derived nut classes share the same interface as follows:
-- `fastener_type` (str) : type identifier - e.g. `"iso4032"`
-- `size` (str) : standard sizes - e.g. `"M6-1"`
-- `hand` (Literal["right", "left"] = "right") : thread direction
-- `simple` (bool = True) : simplify thread
-
-Each nut instance creates a set of properties that provide the CAD object as well as valuable parameters, as follows (values intended for internal use are not shown):
-
-- `clearance_drill_sizes` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `clearance_hole_diameters` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `cq_object` (cq.Compound) : cadquery Compound object
-- `nut_diameter` (float) : maximum diameter of the nut
-- `nut_thickness` (float) : maximum thickness of the nut
-- `nut_class` - (str) : display friendly class name
-- `tap_drill_sizes` - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `tap_hole_diameters` - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-
-#### Nut Selection
-As there are many classes and types of nuts to select from, the Nut class provides some methods that can help find the correct nut for your application. As a reminder, to find the subclasses of the Nut class, use `__subclasses__()`:
-```python
-Nut.__subclasses__() # [, ...]
-```
-Here is a summary of the class methods:
-- `types()` : (set{str}) - create a set of nut types, e.g.:
-```python
-HexNut.types() # {'iso4033', 'iso4032', 'iso4035'}
-```
-- `sizes(fastener_type:str)` : (list[str]) - create a list of nut sizes, e.g.:
-```python
-HexNut.sizes("iso4033") # ['M1.6-0.35', 'M1.8-0.35', 'M2-0.4', 'M2.5-0.45', 'M3-0.45', 'M3.5-0.6', 'M4-0.7', 'M5-0.8', 'M6-1', 'M8-1.25', 'M10-1.5', 'M12-1.75', 'M14-2', 'M16-2', 'M18-2.5', 'M20-2.5', 'M22-2.5', 'M24-3', 'M27-3', 'M30-3.5', 'M33-3.5', 'M36-4', 'M39-4', 'M42-4.5', 'M45-4.5', 'M48-5', 'M52-5']
-```
-- `select_by_size(size:str)` : (dict{class:[type,...],} - e.g.:
-```python
-Nut.select_by_size("M6-1") # {: ['din1587'], : ['iso4035', 'iso4032', 'iso4033'], : ['din1665'], : ['iso4036'], : ['din557']}
-```
-#### Derived Nut Classes
-The following is a list of the current nut classes derived from the base Nut class. Also listed is the type for each of these derived classes where the type refers to a standard that defines the nut parameters. All derived nuts inherit the same API as the base Nut class.
-- `DomedCapNut`: din1587
-- `HexNut`: iso4033, iso4035, iso4032
-- `HexNutWithFlange`: din1665
-- `UnchamferedHexagonNut`: iso4036
-- `SquareNut`: din557
-
-Detailed information about any of the nut types can be readily found on the internet from manufacture's websites or from the standard document itself.
-### Screw
-As the base class of all other screw and bolt classes, all of the derived screw classes share the same interface as follows:
-- `fastener_type` (str) : type identifier - e.g. `"iso4014"`
-- `size` (str) : standard sizes - e.g. `"M6-1"`
-- `length` (float) : distance from base of head to tip of thread
-- `hand` (Literal["right", "left"] = "right") : thread direction
-- `simple` (bool = True) : simplify thread
-
-In addition, to allow screws that have no recess (e.g. hex head bolts) to be countersunk the gap around the hex head which allows a socket wrench to be inserted can be specified with:
-- `socket_clearance` (float = 6 * MM)
-
-Each screw instance creates a set of properties that provide the Compound CAD object as well as valuable parameters, as follows (values intended for internal use are not shown):
-
-- `clearance_drill_sizes` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `clearance_hole_diameters` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `cq_object` (cq.Compound) : cadquery Compound object
-- `head_diameter` (float) : maximum diameter of the head
-- `head_height` (float) : maximum height of the head
-- `nominal_lengths` (list[float]) : nominal lengths values
-- `screw_class` - (str) : display friendly class name
-- `tap_drill_sizes` - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `tap_hole_diameters` - [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-
-The following method helps with hole creation:
-
-- `min_hole_depth(counter_sunk: bool = True)` : distance from surface to tip of screw
-
-#### Screw Selection
-As there are many classes and types of screws to select from, the Screw class provides some methods that can help find the correct screw for your application. As a reminder, to find the subclasses of the Screw class, use `__subclasses__()`:
-```python
-Screw.__subclasses__() # [, ...]
-```
-Here is a summary of the class methods:
-- `types()` : (set{str}) - create a set of screw types, e.g.:
-```python
-CounterSunkScrew.types() # {'iso14582', 'iso10642', 'iso14581', 'iso2009', 'iso7046'}
-```
-- `sizes(fastener_type:str)` : (list[str]) - create a list of screw sizes, e.g.:
-```python
-CounterSunkScrew.sizes("iso7046") # ['M1.6-0.35', 'M2-0.4', 'M2.5-0.45', 'M3-0.5', 'M3.5-0.6', 'M4-0.7', 'M5-0.8', 'M6-1', 'M8-1.25', 'M10-1.5']
-```
-- `select_by_size(size:str)` : (dict{class:[type,...],} - e.g.:
-```python
-Screw.select_by_size("M6-1") # {: ['iso7380_1'], : ['iso7380_2'], ...}
-```
-To see if a given screw type has screws in the length you are looking for, each screw class provides a dictionary of available lengths, as follows:
-- `nominal_length_range[fastener_type:str]` : (list[float]) - all the nominal lengths for this screw type, e.g.:
-```python
-CounterSunkScrew.nominal_length_range["iso7046"] # [3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0]
-```
-During instantiation of a screw any value of `length` may be used; however, only a subset of the above nominal_length_range is valid for any given screw size. The valid sub-range is given with the `nominal_lengths` property as follows:
-```python
-screw = CounterSunkScrew(fastener_type="iso7046",size="M6-1",length=12*MM)
-screw.nominal_lengths # [8.0, 10.0, 12.0, 14.0, 16.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0]
-```
-#### Derived Screw Classes
-The following is a list of the current screw classes derived from the base Screw class. Also listed is the type for each of these derived classes where the type refers to a standard that defines the screw parameters. All derived screws inherit the same API as the base Screw class.
-- `ButtonHeadScrew`: iso7380_1
-- `ButtonHeadWithCollarScrew`: iso7380_2
-- `CheeseHeadScrew`: iso14580, iso7048, iso1207
-- `CounterSunkScrew`: iso2009, iso14582, iso14581, iso10642, iso7046
-- `HexHeadScrew`: iso4017, din931, iso4014
-- `HexHeadWithFlangeScrew`: din1662, din1665
-- `PanHeadScrew`: asme_b_18.6.3, iso1580, iso14583
-- `PanHeadWithCollarScrew`: din967
-- `RaisedCheeseHeadScrew`: iso7045
-- `RaisedCounterSunkOvalHeadScrew`: iso2010, iso7047, iso14584
-- `SetScrew`: iso4026
-- `SocketHeadCapScrew`: iso4762, asme_b18.3
-
-Detailed information about any of the screw types can be readily found on the internet from manufacture's websites or from the standard document itself.
-### Washer
-As the base class of all other washer and bolt classes, all of the derived washer classes share the same interface as follows:
-- `fastener_type` (str) : type identifier - e.g. `"iso4032"`
-- `size` (str) : standard sizes - e.g. `"M6-1"`
-
-Each washer instance creates a set of properties that provide the Compound CAD object as well as valuable parameters, as follows (values intended for internal use are not shown):
-
-- `clearance_drill_sizes` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `clearance_hole_diameters` - see [Clearance, Tap and Threaded Holes](#clearance-tap-and-threaded-holes)
-- `cq_object` (cq.Compound) : cadquery Compound object
-- `washer_diameter` (float) : maximum diameter of the washer
-- `washer_thickness` (float) : maximum thickness of the washer
-- `washer_class` - (str) : display friendly class name
-
-#### Washer Selection
-As there are many classes and types of washers to select from, the Washer class provides some methods that can help find the correct washer for your application. As a reminder, to find the subclasses of the Washer class, use `__subclasses__()`:
-```python
-Washer.__subclasses__() # [, , ]
-```
-Here is a summary of the class methods:
-- `types()` : (set{str}) - create a set of washer types, e.g.:
-```python
-PlainWasher.types() # {'iso7091', 'iso7089', 'iso7093', 'iso7094'}
-```
-- `sizes(fastener_type:str)` : (list[str]) - create a list of washer sizes, e.g.:
-```python
-PlainWasher.sizes("iso7091") # ['M1.6', 'M1.7', 'M2', 'M2.3', 'M2.5', 'M2.6', 'M3', 'M3.5', 'M4', 'M5', 'M6', 'M7', 'M8', 'M10', 'M12', 'M14', 'M16', 'M18', 'M20', 'M22', 'M24', 'M26', 'M27', 'M28', 'M30', 'M32', 'M33', 'M35', 'M36']
-```
-- `select_by_size(size:str)` : (dict{class:[type,...],} - e.g.:
-```python
-Washer.select_by_size("M6") # {: ['iso7094', 'iso7093', 'iso7089', 'iso7091'], : ['iso7090'], : ['iso7092']}
-```
-
-#### Derived Washer Classes
-The following is a list of the current washer classes derived from the base Washer class. Also listed is the type for each of these derived classes where the type refers to a standard that defines the washer parameters. All derived washers inherit the same API as the base Washer class.
-- `PlainWasher`: iso7094, iso7093, iso7089, iso7091
-- `ChamferedWasher`: iso7090
-- `CheeseHeadWasher`: iso7092
-
-Detailed information about any of the washer types can be readily found on the internet from manufacture's websites or from the standard document itself.
-
-### Clearance, Tap and Threaded Holes
-When designing parts with CadQuery a common operation is to place holes appropriate to a specific fastener into the part. This operation is optimized with cq_warehouse by the following three new Workplane methods:
-- `cq.Workplane.clearanceHole`,
-- `cq.Workplane.tapHole`, and
-- `cq.Workplane.threadedHole`.
-
-These methods use data provided by a fastener instance (either a `Nut` or a `Screw`) to both create the appropriate hole (possibly countersunk) in your part as well as add the fastener to a CadQuery Assembly in the location of the hole. In addition, a list of washers can be provided which will get placed under the head of the screw or nut in the provided Assembly.
-
-For example, let's re-build the parametric bearing pillow block found in the [CadQuery Quickstart](https://cadquery.readthedocs.io/en/latest/quickstart.html):
-```python
-import cadquery as cq
-from cq_warehouse.fastener import SocketHeadCapScrew
-
-height = 60.0
-width = 80.0
-thickness = 10.0
-diameter = 22.0
-padding = 12.0
-
-# make the screw
-screw = SocketHeadCapScrew(fastener_type="iso4762", size="M2-0.4", length=16, simple=False)
-# make the assembly
-pillow_block = cq.Assembly(None, name="pillow_block")
-# make the base
-base = (
- cq.Workplane("XY")
- .box(height, width, thickness)
- .faces(">Z")
- .workplane()
- .hole(diameter)
- .faces(">Z")
- .workplane()
- .rect(height - padding, width - padding, forConstruction=True)
- .vertices()
- .clearanceHole(fastener=screw, baseAssembly=pillow_block)
- .edges("|Z")
- .fillet(2.0)
-)
-pillow_block.add(base)
-# Render the assembly
-show_object(pillow_block)
-```
-Which results in:
-![pillow_block](doc/pillow_block.png)
-The differences between this code and the Read the Docs version are:
-- screw dimensions aren't required
-- the screw is created during instantiation of the `SocketHeadCapScrew` class
-- an assembly is created and later the base is added to that assembly
-- the call to cskHole is replaced with clearanceHole
-
-Not only were the appropriate holes for M2-0.4 screws created but an assembly was created to store all of the parts in this project all without having to research the dimensions of M2 screws.
-
-Note: In this example the `simple=False` parameter creates accurate threads on each of the screws which significantly increases the complexity of the model. The default of simple is True which models the thread as a simple cylinder which is sufficient for most applications without the performance cost of accurate threads. Also note that the default color of the pillow block "base" was changed to better contrast the screws.
-#### API
-The APIs of these three methods are:
-
-`clearanceHole`: A hole that allows the screw to be inserted freely
-- `fastener`: Union[Nut, Screw],
-- `washers`: Optional[List[Washer]] = None,
-- `fit`: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
-- `depth`: Optional[float] = None,
-- `counterSunk`: Optional[bool] = True,
-- `baseAssembly`: Optional[cq.Assembly] = None,
-- `clean`: Optional[bool] = True,
-
-`tapHole`: A hole ready for a tap to cut a thread
-- `fastener`: Union[Nut, Screw],
-- `washers`: Optional[List[Washer]] = None,
-- `material`: Optional[Literal["Soft", "Hard"]] = "Soft",
-- `depth`: Optional[float] = None,
-- `counterSunk`: Optional[bool] = True,
-- `fit`: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
-- `baseAssembly`: Optional[cq.Assembly] = None,
-- `clean`: Optional[bool] = True,
-
-`threadedHole`: A hole with a integral thread
-- `fastener`: Screw,
-- `depth`: float,
-- `washers`: List[Washer],
-- `hand`: Literal["right", "left"] = "right",
-- `simple`: Optional[bool] = False,
-- `counterSunk`: Optional[bool] = True,
-- `fit`: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
-- `baseAssembly`: Optional[cq.Assembly] = None,
-- `clean`: Optional[bool] = True,
-
-One can see, the API for all three methods are very similar. The `fit` parameter is used for clearance hole dimensions and to calculate the gap around the head of a countersunk screw. The `material` parameter controls the size of the tap hole as they differ as a function of the material the part is made of. For clearance and tap holes, `depth` values of `None` are treated as thru holes. The threaded hole method requires that `depth` be specified as a consequence of how the thread is constructed.
+If you've ever wondered if there is a better alternative to doing mechanical CAD with proprietary software products,
+[CadQuery](https://cadquery.readthedocs.io/en/latest/index.html)
+and this package - **cq_warehouse** - and similar packages
+like [cq_gears](https://github.com/meadiode/cq_gears) might be what you've been looking for. CadQuery augments the Python programming language (the second most widely used programming language) with
+powerful capabilities enabling a wide variety of mechanical designs to be created in S/W with the same techniques that enable most of today's technology.
-The data used in the creation of these holes is available via three instance methods:
-```python
-screw = CounterSunkScrew(fastener_type="iso7046", size="M6-1", length=10)
-screw.clearance_hole_diameters # {'Close': 6.4, 'Normal': 6.6, 'Loose': 7.0}
-screw.clearance_drill_sizes # {'Close': '6.4', 'Normal': '6.6', 'Loose': '7'}
-screw.tap_hole_diameters # {'Soft': 5.0, 'Hard': 5.4}
-screw.tap_drill_sizes # {'Soft': '5', 'Hard': '5.4'}
-```
-Note that with imperial sized holes (e.g. 7/16), the drill sizes could be a fractional size (e.g. 25/64) or a numbered or lettered size (e.g. U). This information can be added to your designs with the [drafting sub-package](#drafting-sub-package).
-
-### Fastener Locations
-There are two methods that assist with the location of fastener holes relative to other parts: `cq.Assembly.fastenerLocations()` and `cq.Workplane.pushFastenerLocations()`.
-
-#### API
-The APIs of these three methods are:
-
-`fastenerLocations`: returns a list of `cq.Location` objects representing the position and orientation of a given fastener in this Assembly
-- `fastener`: Union[Nut, Screw]
-
-`pushFastenerLocations`: places the location(s) of fasteners on the stack ready for further CadQuery operations
-- `fastener`: Union[Nut, Screw], the fastener to locate
-- `baseAssembly`: cq.Assembly, the assembly that the fasteners are relative to
-
-The [align_fastener_holes.py](examples/align_fastener_holes.py) example shows how these methods can be used to align holes between parts in an assembly.
-
-### Bill of Materials
-As previously mentioned, when an assembly is passed into the three hole methods the fasteners referenced are added to the assembly. A new method has been added to the CadQuery Assembly class - `fastenerQuantities()` - which scans the assembly and returns a dictionary of either:
-- {fastener: count}, or
-- {fastener.info: count}
-
-For example, the values for the previous pillow block example are:
-```python
-print(pillow_block.fastenerQuantities())
-# {'SocketHeadCapScrew(iso4762): M2-0.4x16': 4}
-
-print(pillow_block.fastenerQuantities(bom=False))
-# {: 4}
-```
-Note that this method scans the given assembly and all its children for fasteners. To limit the scan to just the current Assembly, set the `deep=False` optional parameter).
-
-### Extending the fastener sub-package
-The fastener sub-package has been designed to be extended in the following two ways:
-- **Alternate Sizes** - As mentioned previously, the data used to guide the creation of fastener objects is derived from `.csv` files found in the same place as the source code. One can add to the set of standard sized fasteners by inserting appropriate data into the tables. There is a table for each fastener class; an example of the 'socket_head_cap_parameters.csv' is below:
-
-| Size | iso4762:dk | iso4762:k | ... | asme_b18.3:dk | asme_b18.3:k | asme_b18.3:s |
-| --------- | ---------- | --------- | --- | ------------- | ------------ | ------------ |
-| M1.6-0.35 | 3.14 | 1.6 |
-| M2-0.4 | 3.98 | 2 |
-| M2.5-0.45 | 4.68 | 2.5 |
-| M3-0.5 | 5.68 | 3 |
-| ... |
-| #0-80 | | | | 0.096 | 0.06 | 0.05 |
-| #1-64 | | | | 0.118 | 0.073 | 1/16 |
-| #1-72 | | | | 0.118 | 0.073 | 1/16 |
-| #2-56 | | | | 0.14 | 0.086 | 5/64 |
-
-The first row must contain a 'Size' and a set of '{fastener_type}:{parameter}' values. The parameters are taken from the ISO standards where 'k' represents the head height of a screw head, 'dk' is represents the head diameter, etc. Refer to the appropriate document for a complete description. The fastener 'Size' field has the format 'M{thread major diameter}-{thread pitch}' for metric fasteners or either '#{guage}-{TPI}' or '{fractional major diameter}-{TPI}' for imperial fasteners (TPI refers to Threads Per Inch). All the data for imperial fasteners must be entered as inch dimensions while metric data is in millimeters.
-
-There is also a 'nominal_screw_lengths.csv' file that contains a list of all the lengths supported by the standard, as follows:
+**cq_warehouse** augments CadQuery with parametric parts - generated on demand -
+and extensions to the core CadQuery capabilities. The resulting parts can be used within your
+projects or saved to a CAD file in STEP or STL format (among others) for use in a wide
+variety of CAD, CAM, or analytical systems.
-| Screw_Type | Unit | Nominal_Sizes |
-| ---------- | ---- | ------------------------ |
-| din931 | mm | 30,35,40,45,50,55,60,... |
-| ... | | |
+The documentation for **cq_warehouse** can found at [readthedocs](https://cq-warehouse.readthedocs.io/en/latest/index.html).
-The 'short' and 'long' values from the first table (not shown) control the minimum and maximum values in the nominal length ranges for each screw.
-- **New Fastener Types** - The base/derived class structure was designed to allow the creation of new fastener types/classes. For new fastener classes a 2D drawing of one half of the fastener profile is required. If the fastener has a non circular plan (e.g. a hex or a square) a 2D drawing of the plan is required. If the fastener contains a flange and a plan, a 2D profile of the flange is required. If these profiles or plans are present, the base class will use them to build the fastener. The Abstract Base Class technology ensures derived classes can't be created with missing components.
-## extensions sub-package
-This python module provides extensions to the native cadquery code base. Hopefully future generations of cadquery will incorporate this or similar functionality.
-
-Examples illustrating how to use much of the functionality of this sub-package can be found in the [extensions_examples.py](examples/extensions_examples.py) file.
-
-To help the user in debugging exceptions generated by the Opencascade core, the dreaded `StdFail_NotDone` exception is caught and augmented with a more meaningful exception where possible (a new exception is raised from StdFail_NotDone so no information is lost). In addition, python logging is used internally which can be enabled (currently by un-commenting the logging configuration code) to provide run-time information in a `cq_warehouse.log` file.
-### Assembly class extensions
-Two additional methods are added to the `Assembly` class which allow easy manipulation of an Assembly like the chain cadquery objects.
-#### Translate
-Move the current assembly (without making a copy) by the specified translation vector.
-```python
-Assembly.translate(self, vec: VectorLike)
- :param vec: The translation Vector or 3-tuple of floats
-```
-#### Rotate
-Rotate the current assembly (without making a copy) around the axis of rotation by the specified angle.
-```python
-Assembly.rotate(self, axis: VectorLike, angle: float):
- :param axis: The axis of rotation (starting at the origin)
- :type axis: a Vector or 3-tuple of floats
- :param angle: the rotation angle, in degrees
- :type angle: float
-```
-### Plane class extensions
-#### Transform to Local Coordinates
-Adding the ability to transform a Bounding Box.
-```python
-Plane.toLocalCoords(self, obj)
- :param obj: an object, vector, or bounding box to convert
- :type Vector, Shape, or BoundBox
- :return: an object of the same type, but converted to local coordinates
-```
-### Vector class extensions
-Methods to rotate a `Vector` about an axis and to convert a 2D point to 3D space.
-#### Rotate about X,Y and Z Axis
-Rotate a vector by an angle in degrees about x,y or z axis.
-```python
-Vector.rotateX(self, angle: float) -> cq.Vector
-Vector.rotateY(self, angle: float) -> cq.Vector
-Vector.rotateZ(self, angle: float) -> cq.Vector
-```
-
-#### Map 2D Vector to 3D Vector
-Map a 2D point on the XY plane to 3D space on the given plane at the offset.
-```python
-Vector.pointToVector(self, plane: str, offset: float = 0.0) -> cq.Vector
- :param plane: A string literal of ["XY", "XZ", "YZ"] representing the plane containing the 2D points
- :type plane: str
- :param offset: The distance from the origin to the provided plane
- :type offset: float
-```
-#### Translate to Vertex
-Convert a Vector to a Vertex
-```python
-Vector.toVertex() -> Vector
-```
-#### Get Signed Angle between Vectors
-Return the angle between two vectors on a plane with the given normal, where angle = atan2((Va x Vb) . Vn, Va . Vb) - in RADIANS.
-```python
-Vector.getSignedAngle(v: Vector, normal: Vector = None) -> float
-```
-
-### Vertex class extensions
-To facilitate placement of drafting objects within a design the cadquery `Vertex` class has been extended with addition and subtraction methods so control points can be defined as follows:
-```python
-part.faces(">Z").vertices(" cq.Vertex:
- :param other: vector to add
- :type other: Vertex, Vector or 3-tuple of float
-```
-#### Subtract
-Subtract a Vertex, Vector or tuple of floats to a Vertex
-```python
-Vertex.__sub__(
- self, other: Union[cq.Vertex, cq.Vector, Tuple[float, float, float]]
-) -> cq.Vertex:
- :param other: vector to add
- :type other: Vertex, Vector or 3-tuple of float
+To install **cq_warehouse** from github:
```
-#### Display
-Display a Vertex
-```python
-Vertex.__str__(self) -> str:
-```
-#### Convert to Vector
-Convert a Vertex to a Vector
-```python
-Vertex.toVector(self) -> cq.Vector:
-```
-
-### Workplane class extensions
-
-#### Text on 2D Path
-Place 2D/3D text on a 2D path as follows:
-![textOnPath](doc/textOnPath.png)
-
-```python
-Workplane.textOnPath(
- txt: str,
- fontsize: float,
- distance: float,
- start: float = 0.0,
- cut: bool = True,
- combine: bool = False,
- clean: bool = True,
- font: str = "Arial",
- fontPath: Optional[str] = None,
- kind: Literal["regular", "bold", "italic"] = "regular",
- valign: Literal["center", "top", "bottom"] = "center",
-)
-```
-The parameters are largely the same as the `Workplane.text` method. The `start` parameter (normally between 0.0 and 1.0) specify where on the path to start the text.
-
-Here are two examples:
-```python
-fox = (
- cq.Workplane("XZ")
- .threePointArc((50, 30), (100, 0))
- .textOnPath(
- txt="The quick brown fox jumped over the lazy dog",
- fontsize=5,
- distance=1,
- start=0.1,
- )
-)
-clover = (
- cq.Workplane("front")
- .moveTo(0, 10)
- .radiusArc((10, 0), 7.5)
- .radiusArc((0, -10), 7.5)
- .radiusArc((-10, 0), 7.5)
- .radiusArc((0, 10), 7.5)
- .consolidateWires()
- .textOnPath(
- txt=".x" * 102,
- fontsize=1,
- distance=1,
- )
-)
-```
-The path that the text follows is defined by the last Edge or Wire in the Workplane stack. Path's defined outside of the Workplane can be used with the `.add(path)` method.
-
-#### Hex Array
-Create a set of points on the stack which describe a hexagon array or honeycomb and push them onto the stack.
-```python
-Workplane.hexArray(
- diagonal: float,
- xCount: int,
- yCount: int,
- center: Union[bool, tuple[bool, bool]] = True,
-):
-:param diagonal: tip to tip size of hexagon ( must be > 0)
-:param xCount: number of points ( > 0 )
-:param yCount: number of points ( > 0 )
-:param center: If True, the array will be centered around the workplane center. If False, the lower corner will be on the reference point and the array will extend in the positive x and y directions. Can also use a 2-tuple to specify centering along each axis.
-```
-#### Thicken Non-Planar Face
-Find all of the faces on the stack and make them Solid objects by thickening along the normals.
-```python
-Workplane.thicken(depth: float, direction: cq.Vector = None)
-:param depth: the amount to thicken - can be positive or negative
-:param direction: an optional 'which way is up' parameter that ensures a set of faces are thickened in the same direction.
-```
-
-### Face class extensions
-
-#### Thicken
-Create a solid from a potentially non planar face by thickening along the normals. The direction vector can be used to indicate which way is 'up', potentially flipping the face normal direction such that many faces with different normals all go in the same direction (direction need only be +/- 90 degrees from the face normal.)
-
-In the following example, non-planar faces are thickened both towards and away from the center of the sphere.
-![thickenFace](doc/thickenFace.png)
-```python
-Face.thicken(depth: float, direction: cq.Vector = None) -> cq.Solid
-:param depth: the amount to thicken - can be positive or negative
-:param direction: an optional 'which way is up' parameter that ensures a set of faces are thickened in the same direction.
-```
-
-#### Project Face to Shape
-Project a Face onto a Shape generating new Face(s) on the surfaces of the object. Two types of projections are supported, a parallel or flat projection with a `direction` indicator or a conical projection where a `center` must be provided.
-
-The two types of projections are illustrated below:
-
-![flatProjection](doc/flatProjection.png)
-![conicalProjection](doc/conicalProjection.png)
-
-The API is as follows:
-```python
-Face.projectFaceToShape(
- targetObject: cq.Shape,
- direction: VectorLike = None,
- center: VectorLike = None,
- internalFacePoints: list[cq.Vector] = [],
-) -> list[cq.Face]
-```
-Note that an array of Faces is returned as the projection might result in faces on the "front" and "back" of the object (or even more if there are intermediate surfaces in the projection path). Faces "behind" the projection are not returned.
-
-To help refine the resulting face, a list of planar points can be passed to augment the surface definition. For example, when projecting a circle onto a sphere, a circle will result which will get converted to a planar circle face. If no points are provided, a single center point will be generated and used for this purpose.
-
-#### Emboss Face To Shape
-Emboss a Face defined on the XY plane to the Shape. Unlike projection, emboss attempts to maintain the lengths of the edges defining the face.
-
-```python
-Face.embossFaceToShape(
- targetObject: cq.Shape,
- surfacePoint: VectorLike,
- surfaceXDirection: VectorLike,
- internalFacePoints: list[cq.Vector] = [],
-) -> cq.Face
-```
-Unlike projection, a single Face is returned. The internalFacePoints parameter works as with projection.
-
-### Wire class extensions
-
-#### Make Non Planar Face
-Create a potentially non-planar face bounded by exterior (wire or edges), optionally refined by surfacePoints with optional holes defined by interiorWires.
-
-```python
-def makeNonPlanarFace(
- exterior: Union[cq.Wire, list[cq.Edge]],
- surfacePoints: list[VectorLike] = None,
- interiorWires: list[cq.Wire] = None,
-) -> cq.Face
-```
-or
-```python
-Wire.makeNonPlanarFace(
- surfacePoints: list[VectorLike] = None,
- interiorWires: list[cq.Wire] = None,
-) -> cq.Face
-```
-The `surfacePoints` parameter can be used to refine the resulting Face. If no points are provided a single central point will be used to help avoid the creation of a planar face.
-
-The `interiorWires` parameter can be used to pass one or more wires which define holes in the Face.
-
-#### Project Wire to Shape
-Project a Wire onto a Shape generating new Wires on the surfaces of the object one and only one of `direction` or `center` must be provided. Note that one more more wires may be generated depending on the topology of the target object and location/direction of projection.
-
-```python
-Wire.projectWireToShape(
- targetObject: cq.Shape,
- direction: VectorLike = None,
- center: VectorLike = None,
-) -> list[cq.Wire]
-```
-
-#### Emboss Wire to Shape
-Emboss a planar Wire defined on the XY plane to targetObject maintaining the length while doing so. An illustration follows:
-
-![embossWire](doc/embossWire.png)
-
-The embossed wire can be used to build features as:
-
-![embossFeature](doc/embossFeature.png)
-
-with the `sweep` method.
-
-```python
-Wire.embossWireToShape(
- targetObject: cq.Shape,
- surfacePoint: VectorLike,
- surfaceXDirection: VectorLike,
- tolerance: float = 0.01,
-) -> cq.Wire
-```
-The `surfacePoint` defines where on the target object the wire will be embossed while the `surfaceXDirection` controls the orientation of the wire on the surface. The `tolerance` parameter controls the accuracy of the embossed wire's length.
-
-### Edge class extensions
-
-#### Project Edge to Shape
-Same as [Project Wire To Shape](#project_wire-to-shape)
-
-
-#### Emboss Edge to Shape
-Same as [Emboss Wire To Shape](#emboss_wire-to-shape)
-
-### Shape class extensions
-
-#### Find Intersection
-Return both the point(s) and normal(s) of the intersection of the line (defined by a point and direction) and the shape.
-
-```python
-Shape.findIntersection(
- point: cq.Vector, direction: cq.Vector
-) -> list[tuple[cq.Vector, cq.Vector]]
-```
-
-#### Project Text on Shape
-Create 2D/3D text with a baseline following the given path on Shape as follows:
-![projectText](doc/projectText.png)
-
-```python
-Shape.projectText(
- txt: str,
- fontsize: float,
- depth: float,
- path: Union[cq.Wire, cq.Edge],
- font: str = "Arial",
- fontPath: Optional[str] = None,
- kind: Literal["regular", "bold", "italic"] = "regular",
- valign: Literal["center", "top", "bottom"] = "center",
- start: float = 0,
-) -> cq.Compound:
-```
-The `start` parameter normally ranges between 0.0 and 1.0 and represents how far along the path the text will start. If `depth` is zero, Faces will be returned instead of Solids.
-
-#### Emboss Text on Shape
-Create 3D text with a baseline following the given path on Shape as follows:
-![embossText](doc/embossText.png)
-
-```python
-Shape.embossText(
- txt: str,
- fontsize: float,
- depth: float,
- path: Union[cq.Wire, cq.Edge],
- font: str = "Arial",
- fontPath: Optional[str] = None,
- kind: Literal["regular", "bold", "italic"] = "regular",
- valign: Literal["center", "top", "bottom"] = "center",
- start: float = 0,
-) -> cq.Compound:
+python3 -m pip install git+https://github.com/gumyr/cq_warehouse.git#egg=cq_warehouse
```
-The `start` parameter normally ranges between 0.0 and 1.0 and represents how far along the path the text will start. If `depth` is zero, Faces will be returned instead of Solids.
-
diff --git a/src/cq_warehouse.egg-info/SOURCES.txt b/src/cq_warehouse.egg-info/SOURCES.txt
index 321eeae..390263c 100644
--- a/src/cq_warehouse.egg-info/SOURCES.txt
+++ b/src/cq_warehouse.egg-info/SOURCES.txt
@@ -3,6 +3,7 @@ README.md
pyproject.toml
setup.cfg
src/cq_warehouse/__init__.py
+src/cq_warehouse/assembly_extended.py
src/cq_warehouse/button_head_parameters.csv
src/cq_warehouse/button_head_with_collar_parameters.csv
src/cq_warehouse/chain.py
@@ -11,11 +12,14 @@ src/cq_warehouse/cheese_head_parameters.csv
src/cq_warehouse/cheese_head_washer_parameters.csv
src/cq_warehouse/clearance_hole_sizes.csv
src/cq_warehouse/countersunk_head_parameters.csv
+src/cq_warehouse/cq_extended.py
src/cq_warehouse/domed_cap_nut_parameters.csv
src/cq_warehouse/drafting.py
src/cq_warehouse/drill_sizes.csv
src/cq_warehouse/extensions.py
+src/cq_warehouse/extensions_doc.py
src/cq_warehouse/fastener.py
+src/cq_warehouse/geom_extended.py
src/cq_warehouse/hex_head_parameters.csv
src/cq_warehouse/hex_head_with_flange_parameters.csv
src/cq_warehouse/hex_nut_parameters.csv
@@ -30,6 +34,7 @@ src/cq_warehouse/plain_washer_parameters.csv
src/cq_warehouse/raised_cheese_head_parameters.csv
src/cq_warehouse/raised_countersunk_oval_head_parameters.csv
src/cq_warehouse/setscrew_parameters.csv
+src/cq_warehouse/shapes_extended.py
src/cq_warehouse/socket_head_cap_parameters.csv
src/cq_warehouse/sprocket.py
src/cq_warehouse/square_nut_parameters.csv
@@ -39,5 +44,4 @@ src/cq_warehouse/unchamfered_hex_nut_parameters.csv
src/cq_warehouse.egg-info/PKG-INFO
src/cq_warehouse.egg-info/SOURCES.txt
src/cq_warehouse.egg-info/dependency_links.txt
-src/cq_warehouse.egg-info/requires.txt
src/cq_warehouse.egg-info/top_level.txt
\ No newline at end of file
diff --git a/src/cq_warehouse.egg-info/requires.txt b/src/cq_warehouse.egg-info/requires.txt
deleted file mode 100644
index 572b352..0000000
--- a/src/cq_warehouse.egg-info/requires.txt
+++ /dev/null
@@ -1 +0,0 @@
-pydantic
diff --git a/src/cq_warehouse/chain.py b/src/cq_warehouse/chain.py
index 8cd4962..ca373df 100644
--- a/src/cq_warehouse/chain.py
+++ b/src/cq_warehouse/chain.py
@@ -40,19 +40,11 @@
import warnings
from functools import cache
from typing import Union, Tuple, List
-from pydantic import (
- BaseModel,
- PrivateAttr,
- validator,
- validate_arguments,
- conlist,
- root_validator,
-)
-import cadquery as cq
+from cadquery import Vector, Location, Solid, Workplane, Assembly
+from cadquery.occ_impl.shapes import VectorLike
import cq_warehouse.extensions
from cq_warehouse.sprocket import Sprocket
-VectorLike = Union[Tuple[float, float], Tuple[float, float, float], cq.Vector]
MM = 1
INCH = 25.4 * MM
@@ -62,176 +54,146 @@
#
# =============================== CLASSES ===============================
#
-class Chain(BaseModel):
- """
+class Chain:
+ """Roller Chain
+
Create a new chain object as defined by the given parameters. The input parameter
defaults are appropriate for a standard bicycle chain.
- Usage:
- c = Chain(
- spkt_teeth=[32, 32],
- spkt_locations=[(-300, 0), (300, 0)],
- positive_chain_wrap=[True, True]
- )
- print(c.spkt_initial_rotation) # [5.625, 193.82627377380086]
- c.cq_object.save('chain.step') # save the cadquery assembly as a STEP file
-
- Attributes
- ----------
- spkt_teeth : list of int
- a list of the number of teeth on each sprocket the chain will wrap around
- spkt_locations : list of cq.Vector or tuple(x,y) or tuple(x,y,z)
- the location of the sprocket centers
- positive_chain_wrap : list Boolean
- the direction chain wraps around the sprockets, True for counter clock wise viewed
- from positive Z
- chain_pitch : float
- the distance between two adjacent pins in a single link (default 1/2 INCH)
- roller_diameter : float
- the size of the cylindrical rollers within the chain (default 5/16 INCH)
- roller_length : float
- the distance between the inner links, i.e. the length of the link rollers
- link_plate_thickness : float
- the thickness of the link plates (both inner and outer link plates)
- pitch_radii : list of float
- the radius of the circle formed by the center of the chain rollers on each sprocket
- chain_links : float
- the length of the chain in links
- num_rollers : int
- the number of link rollers in the entire chain
- roller_loc : list[cq.Vector]
- the location of each roller in the chain
- chain_angles : list[(float,float)]
- the chain entry and exit angles in degrees for each sprocket
- spkt_initial_rotation : list of float
- a in degrees to rotate each sprocket in-order to align the teeth with the gaps
- in the chain
- cq_object : cadquery.Assembly
- the cadquery chain object
-
- Methods
- -------
-
- assemble_chain_transmission(spkts:list[cq.Solid or cq.Workplane],chain:Chain) -> cq.Assembly:
- Create a cq.Assembly from an array of sprockets and a chain object
-
- make_link(chain_pitch:float,link_plate_thickness:float,inner:bool,roller_length:float,
- roller_diameter:float) -> cq.Workplane:
- Create either an internal or external chain link as a cq.Workplane object
+ Args:
+ spkt_teeth (list[int]): list of the number of teeth on each sprocket the chain will wrap around
+ spkt_locations (list[VectorLike]): location of the sprocket centers
+ positive_chain_wrap (list[bool]): the direction chain wraps around the sprockets, True for counter
+ clockwise viewed from positive Z
+ chain_pitch (float): distance between two adjacent pins in a single link. Defaults to 1/2 inch.
+ roller_diameter (float): size of the cylindrical rollers within the chain. Defaults to 5/16 inch.
+ roller_length (float): distance between the inner links, i.e. the length of the link rollers.
+ Defaults to 3/32 inch.
+ link_plate_thickness (float): thickness of the link plates (both inner and outer link plates).
+ Defaults to 1 mm.
+
+ Attributes:
+ pitch_radii (float): radius of the circle formed by the center of the chain rollers on each sprocket
+ chain_links (int): length of the chain in links
+ num_rollers (int): number of link rollers in the entire chain
+ roller_loc (list[Vector]): location of each roller in the chain
+ chain_angles (list[tuple[float,float]]): chain entry and exit angles in degrees for each sprocket
+ spkt_initial_rotation (list[float]): angle in degrees to rotate each sprocket in-order to align the teeth with the gaps in the chain
+ cq_object: cadquery chain object
+
+ Raises:
+ ValueError: invalid roller diameter
+ ValueError: length of spkt_teeth, spkt_locations, positive_chain_wrap not equal
+ ValueError: sprockets in the same location
+
+ Examples:
+
+ .. code-block:: python
+
+ c = Chain(
+ spkt_teeth=[32, 32],
+ spkt_locations=[(-300, 0), (300, 0)],
+ positive_chain_wrap=[True, True]
+ )
- """
+ print(c.spkt_initial_rotation) # [5.625, 193.82627377380086]
- # Instance Attributes
- spkt_teeth: conlist(item_type=int, min_items=2)
- spkt_locations: conlist(item_type=VectorLike, min_items=2)
- positive_chain_wrap: conlist(item_type=bool, min_items=2)
- chain_pitch: float = (1 / 2) * INCH
- roller_diameter: float = (5 / 16) * INCH
- roller_length: float = (3 / 32) * INCH
- link_plate_thickness: float = 1.0 * MM
+ c.cq_object.save('chain.step') # save the cadquery assembly as a STEP file
+
+ """
@property
def pitch_radii(self) -> List[float]:
- """ The radius of the circle formed by the center of the chain rollers on each sprocket """
+ """The radius of the circle formed by the center of the chain rollers on each sprocket"""
return [
Sprocket.sprocket_pitch_radius(n, self.chain_pitch) for n in self.spkt_teeth
]
@property
def chain_links(self) -> float:
- """ the length of the chain in links """
+ """the length of the chain in links"""
return self._chain_links
@property
def num_rollers(self) -> int:
- """ the number of link rollers in the entire chain """
+ """the number of link rollers in the entire chain"""
return self._num_rollers
@property
- def roller_loc(self) -> List[cq.Vector]:
- """ the location of each roller in the chain """
+ def roller_loc(self) -> List[Vector]:
+ """the location of each roller in the chain"""
return self._roller_loc
@property
def chain_angles(self) -> "List[Tuple(float,float)]":
- """ the chain entry and exit angles in degrees for each sprocket """
+ """the chain entry and exit angles in degrees for each sprocket"""
return self._chain_angles
@property
def spkt_initial_rotation(self) -> List[float]:
- """ a in degrees to rotate each sprocket in-order to align the teeth with the gaps
- in the chain """
+ """a in degrees to rotate each sprocket in-order to align the teeth with the gaps
+ in the chain"""
return self._spkt_initial_rotation
@property
- def cq_object(self) -> cq.Assembly:
- """ the cadquery chain object """
+ def cq_object(self) -> Assembly:
+ """the cadquery chain object"""
return self._cq_object
- # Private Attributes
- _flat_teeth: bool = PrivateAttr()
- _num_spkts: int = PrivateAttr()
- _spkt_locs: List[cq.Vector] = PrivateAttr()
- _plane_offset: float = PrivateAttr()
- _chain_links: float = PrivateAttr()
- _num_rollers: float = PrivateAttr()
- _arc_a: List[float] = PrivateAttr()
- _segment_lengths: List[float] = PrivateAttr()
- _segment_sums: List[float] = PrivateAttr()
- _chain_length: float = PrivateAttr()
- _roller_loc: List[cq.Vector] = PrivateAttr()
- _chain_angles: List[float] = PrivateAttr()
- _spkt_initial_rotation: List[float] = PrivateAttr()
- _cq_object: cq.Assembly = PrivateAttr()
-
- # pylint: disable=too-few-public-methods
- class Config:
- """ Configurate pydantic to allow cadquery native types """
-
- arbitrary_types_allowed = True
-
- # pylint: disable=no-self-argument
- # pylint: disable=no-self-use
- @validator("roller_diameter")
- def is_roller_too_large(cls, v, values):
- """ Ensure that the roller would fit in the chain """
- if v >= values["chain_pitch"]:
- raise ValueError(
- f"roller_diameter {v} is too large for chain_pitch {values['chain_pitch']}"
- )
- return v
-
- @root_validator(pre=True)
- def equal_length_lists(cls, values):
- """ Ensure equal length of sprockets, locations and wrap lists """
- if (
- not len(values["spkt_teeth"])
- == len(values["spkt_locations"])
- == len(values["positive_chain_wrap"])
+ def __init__(
+ self,
+ spkt_teeth: list[int],
+ spkt_locations: list[VectorLike],
+ positive_chain_wrap: list[bool],
+ chain_pitch: float = (1 / 2) * INCH,
+ roller_diameter: float = (5 / 16) * INCH,
+ roller_length: float = (3 / 32) * INCH,
+ link_plate_thickness: float = 1.0 * MM,
+ ):
+ """Validate inputs and create the chain assembly object"""
+ self.spkt_teeth = spkt_teeth
+ self.spkt_locations = spkt_locations
+ self.positive_chain_wrap = positive_chain_wrap
+ self.chain_pitch = chain_pitch
+ self.roller_diameter = roller_diameter
+ self.roller_length = roller_length
+ self.link_plate_thickness = link_plate_thickness
+
+ if not (
+ isinstance(spkt_teeth, list) and all(isinstance(s, int) for s in spkt_teeth)
+ ):
+ raise ValueError("spkt_teeth must be a list of int")
+ if not (
+ isinstance(spkt_locations, list)
+ and all(isinstance(v, (Vector, tuple)) for v in spkt_locations)
):
+ raise ValueError("spkt_locations must be a list")
+ if not (
+ isinstance(positive_chain_wrap, list)
+ and all(isinstance(b, bool) for b in positive_chain_wrap)
+ ):
+ raise ValueError("positive_chain_wrap must be a list")
+ if not (len(spkt_teeth) == len(spkt_locations) == len(positive_chain_wrap)):
raise ValueError(
"Length of spkt_teeth, spkt_locations, positive_chain_wrap not equal"
)
- return values
-
- def __init__(self, **data):
- """ Validate inputs and create the chain assembly object """
- # Use the BaseModel initializer to validate the attributes
- super().__init__(**data)
+ """Ensure that the roller would fit in the chain"""
+ if self.roller_diameter >= self.chain_pitch:
+ raise ValueError(
+ f"roller_diameter {self.roller_diameter} is too large for chain_pitch {self.chain_pitch}"
+ )
# Store the number of sprockets in this chain
self._num_spkts = len(self.spkt_teeth)
- # Store the locations of the sprockets as a list of cq.Vector independent of the inputs
+ # Store the locations of the sprockets as a list of Vector independent of the inputs
self._spkt_locs = [
- cq.Vector(l.x, l.y, 0)
- if isinstance(l, cq.Vector)
- else cq.Vector(l[0], l[1], 0)
+ Vector(l.x, l.y, 0) if isinstance(l, Vector) else Vector(l[0], l[1], 0)
for l in self.spkt_locations
]
# Store the elevation of the chain plane from the XY plane
- if isinstance(self.spkt_locations[0], cq.Vector):
+ if isinstance(self.spkt_locations[0], Vector):
self._plane_offset = self.spkt_locations[0].z
else:
self._plane_offset = (
@@ -243,10 +205,10 @@ def __init__(self, **data):
self._calc_entry_exit_angles() # Determine critical chain angles
self._calc_segment_lengths() # Determine the chain segment lengths
self._calc_roller_locations() # Determine the location of each chain roller
- self._assemble_chain() # Build the cq.Assembly for the chain
+ self._assemble_chain() # Build the Assembly for the chain
def _calc_spkt_separation(self) -> List[float]:
- """ Determine the distance between sprockets """
+ """Determine the distance between sprockets"""
return [
(self._spkt_locs[(s + 1) % self._num_spkts] - self._spkt_locs[s]).Length
for s in range(self._num_spkts)
@@ -355,7 +317,7 @@ def _calc_entry_exit_angles(self):
self._chain_angles = [*zip(entry_a, exit_a)]
def _calc_segment_lengths(self):
- """ Determine the length of the chain between and in contact with the sprockets """
+ """Determine the length of the chain between and in contact with the sprockets"""
# Determine the distance between sprockets
spkt_sep = self._calc_spkt_separation()
@@ -416,19 +378,15 @@ def _calc_segment_lengths(self):
self._num_rollers = floor(self._chain_length / self.chain_pitch)
def _calc_roller_locations(self):
- """ Determine the location of all the chain rollers """
+ """Determine the location of all the chain rollers"""
# Calculate the 2D point where the chain enters and exits the sprockets
spkt_entry_exit_loc = [
[
self._spkt_locs[s]
- + cq.Vector(0, self.pitch_radii[s]).rotateZ(
- self._chain_angles[s][ENTRY]
- ),
+ + Vector(0, self.pitch_radii[s]).rotateZ(self._chain_angles[s][ENTRY]),
self._spkt_locs[s]
- + cq.Vector(0, self.pitch_radii[s]).rotateZ(
- self._chain_angles[s][EXIT]
- ),
+ + Vector(0, self.pitch_radii[s]).rotateZ(self._chain_angles[s][EXIT]),
]
for s in range(self._num_spkts)
]
@@ -460,7 +418,7 @@ def _calc_roller_locations(self):
if roller_segment % 2 == 0: # on a sprocket
self._roller_loc.append(
self._spkt_locs[roller_spkt]
- + cq.Vector(0, self.pitch_radii[roller_spkt]).rotateZ(roller_a)
+ + Vector(0, self.pitch_radii[roller_spkt]).rotateZ(roller_a)
)
else: # between two sprockets
self._roller_loc.append(
@@ -492,10 +450,10 @@ def _calc_roller_locations(self):
]
def _assemble_chain(self):
- """ Given the roller locations assemble the chain """
+ """Given the roller locations assemble the chain"""
#
# Initialize the chain assembly
- self._cq_object = cq.Assembly(None, name="chain_links")
+ self._cq_object = Assembly(None, name="chain_links")
#
# Add the links to the chain assembly
@@ -509,25 +467,36 @@ def _assemble_chain(self):
- self._roller_loc[i].x,
)
)
- link_location = cq.Location(
- self._roller_loc[i].pointToVector("XY", self._plane_offset)
+ link_location = Location(
+ self._roller_loc[i] + Vector(0, 0, self._plane_offset)
)
self._cq_object.add(
Chain.make_link(inner=i % 2 == 0).rotate(
- (0, 0, 0), cq.Vector(0, 0, 1), link_rotation_a_d
+ (0, 0, 0), Vector(0, 0, 1), link_rotation_a_d
),
name="link" + str(i),
loc=link_location,
)
- @validate_arguments(config=dict(arbitrary_types_allowed=True))
def assemble_chain_transmission(
- self, spkts: list[Union[cq.Solid, cq.Workplane]]
- ) -> cq.Assembly:
- """
+ self, spkts: list[Union[Solid, Workplane]]
+ ) -> Assembly:
+ """Build Socket/Chain Assembly
+
Create the transmission assembly from sprockets for a chain
+
+ Args:
+ spkts: sprockets to include in transmission
+
+ Returns:
+ Chain wrapped around sprockets
"""
- transmission = cq.Assembly(None, name="transmission")
+ if not isinstance(spkts, list) or not all(
+ isinstance(s, (Solid, Workplane)) for s in spkts
+ ):
+ raise ValueError("spkts must be a list of Solid or Workplane")
+
+ transmission = Assembly(None, name="transmission")
for spkt_num, spkt in enumerate(spkts):
spktname = "spkt" + str(spkt_num)
@@ -542,37 +511,47 @@ def assemble_chain_transmission(
@staticmethod
@cache
- @validate_arguments
def make_link(
chain_pitch: float = 0.5 * INCH,
link_plate_thickness: float = 1 * MM,
inner: bool = True,
roller_length: float = (3 / 32) * INCH,
roller_diameter: float = (5 / 16) * INCH,
- ) -> cq.Workplane:
- """
+ ) -> Workplane:
+ """Create roller chain link pair
+
Create either inner or outer link pairs. Inner links include rollers while
outer links include fake roller pins.
+
+ Args:
+ chain_pitch: distance between roller pin centers. Defaults to 0.5*INCH.
+ link_plate_thickness: thickness of single plate connecting rollers. Defaults to 1*MM.
+ inner: is this an inner (or outer) chain link?. Defaults to True.
+ roller_length: length of the internal roller. Defaults to (3 / 32)*INCH.
+ roller_diameter: diameter of the internal roller. Defaults to (5 / 16)*INCH.
+
+ Returns:
+ A single link pair
"""
def link_plates(chain_pitch, thickness, inner=False):
- """ Create a single chain link, either inner or outer """
+ """Create a single chain link, either inner or outer"""
plate_scale = chain_pitch / (0.5 * INCH)
neck = plate_scale * 4.5 * MM / 2
plate_r = plate_scale * 8.5 * MM / 2
neck_r = (pow(chain_pitch / 2, 2) + pow(neck, 2) - pow(plate_r, 2)) / (
2 * plate_r - 2 * neck
)
- plate_cen_pt = cq.Vector(chain_pitch / 2, 0)
+ plate_cen_pt = Vector(chain_pitch / 2, 0)
plate_neck_intersection_a = degrees(atan2(neck + neck_r, chain_pitch / 2))
neck_tangent_pt = (
- cq.Vector(plate_r, 0).rotateZ(180 - plate_neck_intersection_a)
+ Vector(plate_r, 0).rotateZ(180 - plate_neck_intersection_a)
+ plate_cen_pt
)
# Create a dog boned link plate
plate = (
- cq.Workplane("XY")
+ Workplane("XY")
.hLine(chain_pitch / 2 + plate_r, forConstruction=True)
.threePointArc((chain_pitch / 2, plate_r), neck_tangent_pt.toTuple())
.radiusArc((0, neck), neck_r)
@@ -598,7 +577,7 @@ def link_plates(chain_pitch, thickness, inner=False):
def roller(roller_diameter=(5 / 16) * INCH, roller_length=(3 / 32) * INCH):
roller = (
- cq.Workplane("XY")
+ Workplane("XY")
.circle(roller_diameter / 2)
.extrude(roller_length / 2, both=True)
)
diff --git a/src/cq_warehouse/drafting.py b/src/cq_warehouse/drafting.py
index 52cf670..c291710 100644
--- a/src/cq_warehouse/drafting.py
+++ b/src/cq_warehouse/drafting.py
@@ -27,165 +27,141 @@
limitations under the License.
"""
-from math import floor, log2, gcd, pi
-from typing import Union, Tuple, Literal, Optional, ClassVar, Any, List
-
-"""# pylint: disable=no-name-in-module"""
-from pydantic import BaseModel, PrivateAttr, validator, validate_arguments
-from numpy import arange, sign
-import cadquery as cq
+from math import floor, log2, gcd, pi, copysign
+from typing import Union, Tuple, Literal, Optional, ClassVar, List
+from cadquery import (
+ Wire,
+ Edge,
+ Vector,
+ Vertex,
+ Color,
+ Assembly,
+ Solid,
+ Workplane,
+ Plane,
+)
+from cadquery.occ_impl.shapes import VectorLike
import cq_warehouse.extensions
MM = 1
INCH = 25.4 * MM
-VectorLike = Union[Tuple[float, float, float], cq.Vector]
PathDescriptor = Union[
- cq.Wire, cq.Edge, List[Union[cq.Vector, cq.Vertex, Tuple[float, float, float]]],
+ Wire,
+ Edge,
+ List[Union[Vector, Vertex, Tuple[float, float, float]]],
]
-PointDescriptor = Union[cq.Vector, cq.Vertex, Tuple[float, float, float]]
+PointDescriptor = Union[Vector, Vertex, Tuple[float, float, float]]
-class Draft(BaseModel):
- """
+class Draft:
+ """Draft
+
Documenting cadquery designs with dimension and extension lines as well as callouts.
- Usage:
- metric_drawing = Draft(decimal_precision=1)
- length_dimension_line = metric_drawing.extension_line(
- object_edge=mystery_object.faces(" cq.Assembly:
- Create a dimension line between points or along path
+ Args:
+ font_size (float): size of the text in dimension lines and callouts. Defaults to 5.0.
+ color (Color, optional): color of text, extension lines and arrows. Defaults to Color(0.25, 0.25, 0.25).
+ arrow_diameter (float): maximum diameter of arrow heads. Defaults to 1.0.
+ arrow_length (float): arrow head length. Defaults to 3.0.
+ label_normal (VectorLike, optional): text and extension line plane normal. Defaults to XY plane.
+ units (Literal["metric", "imperial"]): unit of measurement. Defaults to "metric".
+ number_display (Literal["decimal", "fraction"]): display numbers as decimals or fractions. Defaults to "decimal".
+ display_units (bool): control the display of units with numbers. Defaults to True.
+ decimal_precision (int): number of decimal places when displaying numbers. Defaults to 2.
+ fractional_precision (int): maximum fraction denominator - must be a factor of 2. Defaults to 64.
- extension_line(
- object_edge: PathDescriptor,
- offset: float,
- label: str = None,
- tolerance: Optional[Union[float, Tuple[float, float]]] = None,
- label_angle: bool = False,
- )-> cq.Assembly:
- Create an extension line - a dimension line offset from the object
- with lines extending from the object - between points or along path
+ Example:
- callout(
- label: str,
- tail: Optional[PathDescriptor] = None,
- origin: Optional[PointDescriptor] = None,
- justify: Literal["left", "center", "right"] = "left",
- ) -> cq.Assembly:
- Create a callout at the origin with no tail, or at the root of the given tail
+ .. code-block:: python
+
+ metric_drawing = Draft(decimal_precision=1)
+ length_dimension_line = metric_drawing.extension_line(
+ object_edge=mystery_object.faces(">> color: cq.Color = cq.Color(0.25,0.25,0.25)
+ # >>> color: Color = Color(0.25,0.25,0.25)
# results in
# >>> TypeError: cannot pickle 'OCP.Quantity.Quantity_ColorRGBA' object
- def __init__(self, **data: Any):
- super().__init__(**data)
- self._label_normal = (
- cq.Vector(0, 0, 1)
- if self.label_normal is None
- else cq.Vector(self.label_normal).normalized()
- )
- self._label_x_dir = (
- cq.Vector(0, 1, 0)
- if self._label_normal == cq.Vector(1, 0, 0)
- else cq.Vector(1, 0, 0)
- )
- self.color = cq.Color(0.25, 0.25, 0.25) if self.color is None else self.color
-
- # pylint: disable=too-few-public-methods
- class Config:
- """ Configurate pydantic to allow cadquery native types """
-
- arbitrary_types_allowed = True
+ def __init__(
+ self,
+ font_size: float = 5.0,
+ color: Optional[Color] = Color(0.25, 0.25, 0.25),
+ arrow_diameter: float = 1.0,
+ arrow_length: float = 3.0,
+ label_normal: Optional[VectorLike] = None,
+ units: Literal["metric", "imperial"] = "metric",
+ number_display: Literal["decimal", "fraction"] = "decimal",
+ display_units: bool = True,
+ decimal_precision: int = 2,
+ fractional_precision: int = 64,
+ ):
+ self.font_size = font_size
+ self.color = color
+ self.arrow_diameter = arrow_diameter
+ self.arrow_length = arrow_length
+ self.label_normal = label_normal
+ self.units = units
+ self.number_display = number_display
+ self.display_units = display_units
+ self.decimal_precision = decimal_precision
+ self.fractional_precision = fractional_precision
- @validator("fractional_precision")
- @classmethod
- def fractional_precision_power_two(cls, fractional_precision):
- """ Fraction denominator must be a power of two """
if not log2(fractional_precision).is_integer():
raise ValueError(
f"fractional_precision values must be a factor of 2; provided {fractional_precision}"
)
- return fractional_precision
+ if units not in ["metric", "imperial"]:
+ raise ValueError(f"units must be one of 'metric' or 'imperial' not {units}")
+ if number_display not in ["decimal", "fraction"]:
+ raise ValueError(
+ f"number_display must be one of 'decimal' or 'fraction' not {number_display}"
+ )
+ self._label_normal = (
+ Vector(0, 0, 1)
+ if self.label_normal is None
+ else Vector(self.label_normal).normalized()
+ )
+ self._label_x_dir = (
+ Vector(0, 1, 0)
+ if self._label_normal == Vector(1, 0, 0)
+ else Vector(1, 0, 0)
+ )
+ # self.color = Color(0.25, 0.25, 0.25) if self.color is None else self.color
def round_to_str(self, number: float) -> str:
- """ Round a float but remove decimal if appropriate and convert to str """
+ """Round a float but remove decimal if appropriate and convert to str"""
return (
f"{round(number, self.decimal_precision):.{self.decimal_precision}f}"
if self.decimal_precision > 0
else str(int(round(number, self.decimal_precision)))
)
- @validate_arguments
def _number_with_units(
self,
number: float,
tolerance: Union[float, Tuple[float, float]] = None,
display_units: Optional[bool] = None,
) -> str:
- """ Convert a raw number to a unit of measurement string based on the class settings """
+ """Convert a raw number to a unit of measurement string based on the class settings"""
def simplify_fraction(numerator: int, denominator: int) -> Tuple[int, int]:
- """ Mathematically simplify a fraction given a numerator and demoninator """
+ """Mathematically simplify a fraction given a numerator and demoninator"""
greatest_common_demoninator = gcd(numerator, denominator)
return (
int(numerator / greatest_common_demoninator),
@@ -227,26 +203,24 @@ def simplify_fraction(numerator: int, denominator: int) -> Tuple[int, int]:
return return_value
- @validate_arguments(config=dict(arbitrary_types_allowed=True))
def _make_arrow(
- self, path: Union[cq.Edge, cq.Wire], tip_pos: Literal["start", "end"] = "start"
- ) -> cq.Solid:
- """ Create an arrow head which follows the provided path """
+ self, path: Union[Edge, Wire], tip_pos: Literal["start", "end"] = "start"
+ ) -> Solid:
+ """Create an arrow head which follows the provided path"""
# Calculate the position along the path to create the arrow cross-sections
loft_pos = [0.0 if tip_pos == "start" else 1.0]
for i in [2, 1]:
loft_pos.append(
- self.arrow_length / (i * cq.Wire.assembleEdges([path]).Length())
+ self.arrow_length / (i * Wire.assembleEdges([path]).Length())
if tip_pos == "start"
- else 1.0
- - self.arrow_length / (i * cq.Wire.assembleEdges([path]).Length())
+ else 1.0 - self.arrow_length / (i * Wire.assembleEdges([path]).Length())
)
radius_lut = {0: 0.0001, 1: 0.2, 2: 0.5}
arrow_cross_sections = [
- cq.Wire.assembleEdges(
+ Wire.assembleEdges(
[
- cq.Edge.makeCircle(
+ Edge.makeCircle(
radius=radius_lut[i] * self.arrow_diameter,
pnt=path.positionAt(loft_pos[i]),
dir=path.tangentAt(loft_pos[i]),
@@ -255,46 +229,43 @@ def _make_arrow(
)
for i in range(3)
]
- arrow = cq.Assembly(None, name="arrow")
+ arrow = Assembly(None, name="arrow")
arrow.add(
- cq.Solid.makeLoft(arrow_cross_sections), name="arrow_and_shaft",
+ Solid.makeLoft(arrow_cross_sections),
+ name="arrow_and_shaft",
)
arrow.add(path, name="arrow_shaft")
return arrow
@staticmethod
- def _segment_line(
- path: Union[cq.Edge, cq.Wire], tip_pos: float, tail_pos: float
- ) -> cq.Edge:
- """ Create a segment of a path between tip and tail (inclusive) """
+ def _segment_line(path: Union[Edge, Wire], tip_pos: float, tail_pos: float) -> Edge:
+ """Create a segment of a path between tip and tail (inclusive)"""
if not 0.0 <= tip_pos <= 1.0:
raise ValueError(f"tip_pos value of {tip_pos} is not between 0.0 and 1.0")
if not 0.0 <= tail_pos <= 1.0:
raise ValueError(f"tail_pos value of {tail_pos} is not between 0.0 and 1.0")
- sub_path = cq.Edge.makeSpline(
+ sub_path = Edge.makeSpline(
listOfVector=[
- path.positionAt(t)
- for t in arange(tip_pos, tail_pos + 0.00001, (tail_pos - tip_pos) / 16)
+ path.positionAt(tip_pos + i * (tail_pos - tip_pos) / 16)
+ for i in range(17)
],
tangents=[path.tangentAt(t) for t in [tip_pos, tail_pos]],
)
return sub_path
@staticmethod
- def _path_to_wire(path: PathDescriptor) -> cq.Wire:
- """ Convert a PathDescriptor into a cq.Wire """
- if isinstance(path, (cq.Edge, cq.Wire)):
- path_as_wire = cq.Wire.assembleEdges([path])
+ def _path_to_wire(path: PathDescriptor) -> Wire:
+ """Convert a PathDescriptor into a Wire"""
+ if isinstance(path, (Edge, Wire)):
+ path_as_wire = Wire.assembleEdges([path])
else:
- path_as_wire = cq.Wire.assembleEdges(
- cq.Workplane()
+ path_as_wire = Wire.assembleEdges(
+ Workplane()
.polyline(
[
- cq.Vector(p.toTuple())
- if isinstance(p, cq.Vertex)
- else cq.Vector(p)
+ Vector(p.toTuple()) if isinstance(p, Vertex) else Vector(p)
for p in path
]
)
@@ -303,8 +274,8 @@ def _path_to_wire(path: PathDescriptor) -> cq.Wire:
return path_as_wire
def _label_size(self, label_str: str) -> float:
- """ Return the length of a text string given class parameters """
- label_xy_object = cq.Workplane("XY").text(
+ """Return the length of a text string given class parameters"""
+ label_xy_object = Workplane("XY").text(
txt=label_str,
fontsize=self.font_size,
distance=self.font_size / 20,
@@ -315,30 +286,30 @@ def _label_size(self, label_str: str) -> float:
return label_length
@staticmethod
- def _find_center_of_arc(arc: cq.Edge) -> cq.Vector:
- """ Given an arc find the center of the circle """
+ def _find_center_of_arc(arc: Edge) -> Vector:
+ """Given an arc find the center of the circle"""
arc_radius = arc.radius()
arc_pnt = arc.positionAt(0.25)
chord_end_points = [arc.positionAt(t) for t in [0.0, 0.5]]
- chord_line = cq.Edge.makeLine(*chord_end_points)
+ chord_line = Edge.makeLine(*chord_end_points)
chord_center_pnt = chord_line.positionAt(0.5)
- radial_tangent = cq.Edge.makeLine(arc_pnt, chord_center_pnt).tangentAt(0)
+ radial_tangent = Edge.makeLine(arc_pnt, chord_center_pnt).tangentAt(0)
center = arc_pnt + radial_tangent * arc_radius
return center
def _label_to_str(
self,
label: str,
- line_wire: cq.Wire,
+ line_wire: Wire,
label_angle: bool,
tolerance: Optional[Union[float, Tuple[float, float]]],
) -> str:
- """ Create the str to use as the label text """
+ """Create the str to use as the label text"""
line_length = line_wire.Length()
if label is not None:
label_str = label
elif label_angle:
- arc_edge = cq.Workplane(line_wire).edges("%circle").val()
+ arc_edge = Workplane(line_wire).edges("%circle").val()
try:
arc_radius = arc_edge.radius()
except AttributeError as not_an_arc_error:
@@ -354,10 +325,10 @@ def _label_to_str(
def _make_arrow_shaft(
self,
label_length: float,
- line_wire: cq.Wire,
+ line_wire: Wire,
internal: bool,
arrow_pos: Literal["start", "end"],
- ) -> cq.Edge:
+ ) -> Edge:
line_length = line_wire.Length()
# Calculate the relative positions along the dimension_line line of the key features
@@ -385,8 +356,8 @@ def _make_arrow_shaft(
)
else:
arrow_shaft = (
- cq.Workplane(
- cq.Plane(
+ Workplane(
+ Plane(
origin=line_wire.positionAt(line_wire_pos),
xDir=line_wire.tangentAt(line_wire_pos),
normal=self._label_normal,
@@ -403,38 +374,38 @@ def _str_to_object(
self,
position: Literal["start", "center", "end"],
label_str: str,
- location_wire: cq.Wire,
- ) -> cq.Solid:
+ location_wire: Wire,
+ ) -> Solid:
if position == "center":
- text_plane = cq.Plane(
+ text_plane = Plane(
origin=location_wire.positionAt(0.5),
xDir=location_wire.tangentAt(0.5),
normal=self._label_normal,
)
- label_object = cq.Workplane(text_plane).text(
+ label_object = Workplane(text_plane).text(
txt=label_str, fontsize=self.font_size, distance=self.font_size / 100
)
elif position == "end":
- text_plane = cq.Plane(
+ text_plane = Plane(
origin=location_wire.tangentAt(0.0) * -1.5 * MM
+ location_wire.positionAt(0.0),
xDir=location_wire.tangentAt(0.0) * -1,
normal=self._label_normal,
)
- label_object = cq.Workplane(text_plane).text(
+ label_object = Workplane(text_plane).text(
txt=label_str,
fontsize=self.font_size,
distance=self.font_size / 100,
halign="left",
)
else: # position=="start"
- text_plane = cq.Plane(
+ text_plane = Plane(
origin=location_wire.tangentAt(1.0) * 1.5 * MM
+ location_wire.positionAt(1.0),
xDir=location_wire.tangentAt(1.0) * -1,
normal=self._label_normal,
)
- label_object = cq.Workplane(text_plane).text(
+ label_object = Workplane(text_plane).text(
txt=label_str,
fontsize=self.font_size,
distance=self.font_size / 100,
@@ -442,7 +413,6 @@ def _str_to_object(
)
return label_object
- @validate_arguments(config=dict(arbitrary_types_allowed=True))
def dimension_line(
self,
path: PathDescriptor,
@@ -450,15 +420,40 @@ def dimension_line(
arrows: Tuple[bool, bool] = (True, True),
tolerance: Optional[Union[float, Tuple[float, float]]] = None,
label_angle: bool = False,
- ) -> cq.Assembly:
- """
- Create a dimension line typically for internal measurements
+ ) -> Assembly:
+ """Dimension Line
+
+ Create a dimension line typically for internal measurements.
+ Typically used for (but not restricted to) inside dimensions, a dimension line often
+ as arrows on either side of a dimension or label.
There are three options depending on the size of the text and length
of the dimension line:
Type 1) The label and arrows fit within the length of the path
Type 2) The text fit within the path and the arrows go outside
Type 3) Neither the text nor the arrows fit within the path
+
+ Args:
+ path (PathDescriptor): a very general type of input used to describe the path the
+ dimension line will follow .
+ label (Optional[str], optional): a text string which will replace the length (or
+ arc length) that would otherwise be extracted from the provided path. Providing
+ a label is useful when illustrating a parameterized input where the name of an
+ argument is desired not an actual measurement. Defaults to None.
+ arrows (Tuple[bool, bool], optional): a pair of boolean values controlling the placement
+ of the start and end arrows. Defaults to (True, True).
+ tolerance (Optional[Union[float, Tuple[float, float]]], optional): an optional tolerance
+ value to add to the extracted length value. If a single tolerance value is provided
+ it is shown as ± the provided value while a pair of values are shown as
+ separate + and - values. Defaults to None.
+ label_angle (bool, optional): a flag indicating that instead of an extracted length value,
+ the size of the circular arc extracted from the path should be displayed in degrees.
+
+ Raises:
+ ValueError: No output - insufficient space for labels and no arrows selected
+
+ Returns:
+ Assembly: the dimension line
"""
# Create a wire modelling the path of the dimension lines from a variety of input types
@@ -482,7 +477,7 @@ def dimension_line(
)
# Compose an assembly with the component parts of the dimension_line line
- d_line = cq.Assembly(None, name=label_str + "_dimension_line", color=self.color)
+ d_line = Assembly(None, name=label_str + "_dimension_line", color=self.color)
# For the start and end arrow generate complete arrows from shafts and the label object
for i, arrow_pos in enumerate(["start", "end"]):
@@ -505,7 +500,6 @@ def dimension_line(
return d_line
- @validate_arguments(config=dict(arbitrary_types_allowed=True))
def extension_line(
self,
object_edge: PathDescriptor,
@@ -514,15 +508,42 @@ def extension_line(
arrows: Tuple[bool, bool] = (True, True),
tolerance: Optional[Union[float, Tuple[float, float]]] = None,
label_angle: bool = False,
- ) -> cq.Assembly:
- """ Create a dimension line with two lines extending outward from the part to dimension """
+ ) -> Assembly:
+ """Extension Line
+
+ Create a dimension line with two lines extending outward from the part to dimension.
+ Typically used for (but not restricted to) outside dimensions, with a pair of lines
+ extending from the edge of a part to a dimension line.
+
+ Args:
+ object_edge (PathDescriptor): a very general type of input defining the object to
+ be dimensioned. Typically this value would be extracted from the part but is
+ not restricted to this use.
+ offset (float): a distance to displace the dimension line from the edge of the object
+ label (str, optional): a text string which will replace the length (or arc length)
+ that would otherwise be extracted from the provided path. Providing a label is
+ useful when illustrating a parameterized input where the name of an argument
+ is desired not an actual measurement. Defaults to None.
+ arrows (Tuple[bool, bool], optional): a pair of boolean values controlling the placement
+ of the start and end arrows. Defaults to (True, True).
+ tolerance (Optional[Union[float, Tuple[float, float]]], optional): an optional tolerance
+ value to add to the extracted length value. If a single tolerance value is provided
+ it is shown as ± the provided value while a pair of values are shown as
+ separate + and - values. Defaults to None.
+ label_angle (bool, optional): a flag indicating that instead of an extracted length
+ value, the size of the circular arc extracted from the path should be displayed
+ in degrees. Defaults to False.
+
+ Returns:
+ Assembly: the extension line
+ """
# Create a wire modelling the path of the dimension lines from a variety of input types
object_path = Draft._path_to_wire(object_edge)
object_length = object_path.Length()
# Determine if the provided object edge is a circular arc and if so extract its radius
- arc_edge = cq.Workplane(object_path).edges("%circle").val()
+ arc_edge = Workplane(object_path).edges("%circle").val()
try:
arc_radius = arc_edge.radius()
except AttributeError:
@@ -534,36 +555,37 @@ def extension_line(
# Create a new arc for the dimension line offset from the given one
arc_center = Draft._find_center_of_arc(arc_edge)
radial_directions = [
- cq.Edge.makeLine(arc_center, object_path.positionAt(i)).tangentAt(1.0)
+ Edge.makeLine(arc_center, object_path.positionAt(i)).tangentAt(1.0)
for i in [0.0, 0.5, 1.0]
]
offset_arc_pts = [
arc_center + radial_directions[i] * (arc_radius + offset)
for i in range(3)
]
- extension_path = cq.Edge.makeThreePointArc(*offset_arc_pts)
+ extension_path = Edge.makeThreePointArc(*offset_arc_pts)
# Create radial extension lines
ext_line = [
- cq.Edge.makeLine(
+ Edge.makeLine(
object_path.positionAt(i)
- + radial_directions[i * 2] * sign(offset) * 1.5 * MM,
+ + radial_directions[i * 2] * copysign(1, offset) * 1.5 * MM,
object_path.positionAt(i)
- + radial_directions[i * 2] * (offset + sign(offset) * 3.0 * MM),
+ + radial_directions[i * 2]
+ * (offset + copysign(1, offset) * 3.0 * MM),
)
for i in range(2)
]
else:
extension_tangent = object_path.tangentAt(0).cross(self._label_normal)
- dimension_plane = cq.Plane(
+ dimension_plane = Plane(
origin=object_path.positionAt(0),
xDir=extension_tangent,
normal=self._label_normal,
)
ext_line = [
(
- cq.Workplane(dimension_plane)
- .moveTo(sign(offset) * 1.5 * MM, l)
- .lineTo(offset + sign(offset) * 3 * MM, l)
+ Workplane(dimension_plane)
+ .moveTo(copysign(1, offset) * 1.5 * MM, l)
+ .lineTo(offset + copysign(1, offset) * 3 * MM, l)
)
for l in [0, object_length]
]
@@ -579,7 +601,7 @@ def extension_line(
tolerance=tolerance,
label_angle=label_angle,
)
- e_line = cq.Assembly(
+ e_line = Assembly(
None, name=d_line.name.replace("dimension", "extension"), color=self.color
)
e_line.add(ext_line[0], name="extension_line0")
@@ -587,21 +609,43 @@ def extension_line(
e_line.add(d_line, name="dimension_line")
return e_line
- @validate_arguments(config=dict(arbitrary_types_allowed=True))
def callout(
self,
label: str,
tail: Optional[PathDescriptor] = None,
origin: Optional[PointDescriptor] = None,
justify: Literal["left", "center", "right"] = "left",
- ) -> cq.Assembly:
- """ Create a text box that optionally points at something """
+ ) -> Assembly:
+ """Callout
+
+ A text box with or without a tail pointing to another object used to provide
+ extra information to the reader.
+
+ Args:
+ label (str): the text to place within the callout - note that including a ``\\n``
+ in the text string will split the text over multiple lines.
+ tail (Optional[PathDescriptor], optional): an optional tail defined as above -
+ note that if provided the text origin will be the start of the tail.
+ Defaults to None.
+ origin (Optional[PointDescriptor], optional): a very general definition of anchor
+ point of the text defined as
+ ``PointDescriptor = Union[Vector, Vertex, Tuple[float, float, float]]``
+ Defaults to None.
+ justify (Literal[, optional): text alignment. Defaults to "left".
+
+ Raises:
+ ValueError: Either origin or tail must be provided
+
+ Returns:
+ Assembly: the callout
+
+ """
if origin is not None:
text_origin = (
- cq.Vector(origin)
- if isinstance(origin, (cq.Vector, tuple))
- else cq.Vector(origin.toTuple())
+ Vector(origin)
+ if isinstance(origin, (Vector, tuple))
+ else Vector(origin.toTuple())
)
elif tail is not None:
line_wire = Draft._path_to_wire(tail)
@@ -609,11 +653,11 @@ def callout(
else:
raise ValueError("Either origin or tail must be provided")
- text_plane = cq.Plane(
+ text_plane = Plane(
origin=text_origin, xDir=self._label_x_dir, normal=self._label_normal
)
- t_box = cq.Assembly(None, name=label + "_callout", color=self.color)
- label_text = cq.Workplane(text_plane).text(
+ t_box = Assembly(None, name=label + "_callout", color=self.color)
+ label_text = Workplane(text_plane).text(
txt=label,
fontsize=self.font_size,
distance=self.font_size / 100,
@@ -624,7 +668,8 @@ def callout(
t_box.add(label_text, name="callout_label")
if tail is not None:
t_box.add(
- self._make_arrow(line_wire, tip_pos="end"), name="callout_tail",
+ self._make_arrow(line_wire, tip_pos="end"),
+ name="callout_tail",
)
return t_box
diff --git a/src/cq_warehouse/extensions.py b/src/cq_warehouse/extensions.py
index 94e2b75..39caecb 100644
--- a/src/cq_warehouse/extensions.py
+++ b/src/cq_warehouse/extensions.py
@@ -33,18 +33,36 @@
"""
import sys
import logging
-from math import pi, sin, cos, radians, sqrt, degrees
+import math
+from functools import reduce
from typing import Optional, Literal, Union, Tuple
import cadquery as cq
from cadquery.occ_impl.shapes import VectorLike
from cadquery.cq import T
+from cadquery import (
+ Assembly,
+ BoundBox,
+ Compound,
+ Edge,
+ Face,
+ Plane,
+ Location,
+ Shape,
+ Solid,
+ Vector,
+ Vertex,
+ Wire,
+ Workplane,
+ DirectionMinMaxSelector,
+)
+from cq_warehouse.fastener import Screw, Nut, Washer
+from cq_warehouse.thread import IsoThread
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds
from OCP.TopTools import TopTools_HSequenceOfShape
from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin, BRepOffset_RectoVerso
from OCP.BRepProj import BRepProj_Projection
-from OCP.gp import gp_Pnt, gp_Dir
from OCP.gce import gce_MakeLin
from OCP.GeomAbs import (
GeomAbs_C0,
@@ -58,6 +76,7 @@
from OCP.StdFail import StdFail_NotDone
from OCP.Standard import Standard_NoSuchObject
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
+from OCP.gp import gp_Vec, gp_Pnt, gp_Ax1, gp_Dir, gp_Trsf, gp, gp_GTrsf
# Logging configuration - uncomment to enable logs
# logging.basicConfig(
@@ -69,38 +88,137 @@
"""
-Assembly extensions: rotate(), translate()
+Assembly extensions: rotate(), translate(), fastenerQuantities(), fastenerLocations()
"""
-def _translate(self, vec: VectorLike):
+def _assembly_translate(self, vec: "VectorLike") -> "Assembly":
"""
Moves the current assembly (without making a copy) by the specified translation vector
- :param vec: The translation vector
+
+ Args:
+ vec: The translation vector
+
+ Returns:
+ The translated Assembly
+
+ Example:
+ car_assembly.translate((1,2,3))
"""
- self.loc = self.loc * cq.Location(cq.Vector(vec))
+ self.loc = self.loc * Location(Vector(vec))
return self
-cq.Assembly.translate = _translate
+Assembly.translate = _assembly_translate
-def _rotate(self, axis: VectorLike, angle: float):
- """
+def _assembly_rotate(self, axis: "VectorLike", angle: float) -> "Assembly":
+ """Rotate Assembly
+
Rotates the current assembly (without making a copy) around the axis of rotation
by the specified angle
- :param axis: The axis of rotation (starting at the origin)
- :type axis: a 3-tuple of floats
- :param angle: the rotation angle, in degrees
- :type angle: float
+ Args:
+ axis: The axis of rotation (starting at the origin)
+ angle: The rotation angle, in degrees
+
+ Returns:
+ The rotated Assembly
+
+ Example:
+ car_assembly.rotate((0,0,1),90)
"""
- self.loc = self.loc * cq.Location(cq.Vector(0, 0, 0), cq.Vector(axis), angle)
+ self.loc = self.loc * Location(Vector(0, 0, 0), Vector(axis), angle)
return self
-cq.Assembly.rotate = _rotate
+Assembly.rotate = _assembly_rotate
+
+
+def _fastener_quantities(self, bom: bool = True, deep: bool = True) -> dict:
+ """Fastener Quantities
+
+ Generate a bill of materials of the fasteners in an assembly augmented by the hole methods
+ bom: returns fastener.info if True else counts fastener instances
+
+ Args:
+ bom (bool, optional): Select a Bill of Materials or raw fastener instance count. Defaults to True.
+ deep (bool, optional): Scan the entire Assembly. Defaults to True.
+
+ Returns:
+ fastener usage summary
+ """
+ from cq_warehouse.fastener import Screw, Nut, Washer
+
+ assembly_list = []
+ if deep:
+ for _name, sub_assembly in self.traverse():
+ assembly_list.append(sub_assembly)
+ else:
+ assembly_list.append(self)
+
+ fasteners = []
+ for sub_assembly in assembly_list:
+ for value in sub_assembly.metadata.values():
+ if isinstance(value, (Screw, Nut, Washer)):
+ fasteners.append(value)
+
+ unique_fasteners = set(fasteners)
+ if bom:
+ quantities = {f.info: fasteners.count(f) for f in unique_fasteners}
+ else:
+ quantities = {f: fasteners.count(f) for f in unique_fasteners}
+ return quantities
+
+
+Assembly.fastenerQuantities = _fastener_quantities
+
+
+def _fastener_locations(self, fastener: Union["Nut", "Screw"]) -> list[Location]:
+ """Return location(s) of fastener
+
+ Generate a list of cadquery Locations for the given fastener relative to the Assembly
+
+ Args:
+ fastener: fastener to search for
+
+ Returns:
+ a list of cadquery Location objects for each fastener instance
+ """
+
+ name_to_fastener = {}
+ base_assembly_structure = {}
+ # Extract a list of only the fasteners from the metadata
+ for (name, a) in self.traverse():
+ base_assembly_structure[name] = a
+ if a.metadata is None:
+ continue
+
+ for key, value in a.metadata.items():
+ if value == fastener:
+ name_to_fastener[key] = value
+
+ fastener_path_locations = {}
+ base_assembly_path = self._flatten()
+ for assembly_name, _assembly_pointer in base_assembly_path.items():
+ for fastener_name in name_to_fastener.keys():
+ if fastener_name in assembly_name:
+ parents = assembly_name.split("/")
+ fastener_path_locations[fastener_name] = [
+ base_assembly_structure[name].loc for name in parents
+ ]
+
+ fastener_locations = [
+ reduce(lambda l1, l2: l1 * l2, locs)
+ for locs in fastener_path_locations.values()
+ ]
+
+ return fastener_locations
+
+
+Assembly.fastenerLocations = _fastener_locations
+
"""
@@ -109,119 +227,129 @@ def _rotate(self, axis: VectorLike, angle: float):
"""
-def _toLocalCoords(self, obj):
+def _toLocalCoords(self, obj: Union["Vector", "Shape", "BoundBox"]):
"""Project the provided coordinates onto this plane
- :param obj: an object, vector, or bounding box to convert
- :type Vector, Shape, or BoundBox
- :return: an object of the same type, but converted to local coordinates
+ Args:
+ obj: an object, vector, or bounding box to convert
+
+ Returns:
+ an object of the same type, but converted to local coordinates
Most of the time, the z-coordinate returned will be zero, because most
operations based on a plane are all 2D. Occasionally, though, 3D
points outside of the current plane are transformed. One such example is
:py:meth:`Workplane.box`, where 3D corners of a box are transformed to
orient the box in space correctly.
-
"""
# from .shapes import Shape
- if isinstance(obj, cq.Vector):
+ if isinstance(obj, Vector):
return obj.transform(self.fG)
- elif isinstance(obj, cq.Shape):
+ elif isinstance(obj, Shape):
return obj.transformShape(self.fG)
- elif isinstance(obj, cq.BoundBox):
- global_bottom_left = cq.Vector(obj.xmin, obj.ymin, obj.zmin)
- global_top_right = cq.Vector(obj.xmax, obj.ymax, obj.zmax)
+ elif isinstance(obj, BoundBox):
+ global_bottom_left = Vector(obj.xmin, obj.ymin, obj.zmin)
+ global_top_right = Vector(obj.xmax, obj.ymax, obj.zmax)
local_bottom_left = global_bottom_left.transform(self.fG)
local_top_right = global_top_right.transform(self.fG)
local_bbox = Bnd_Box(
gp_Pnt(*local_bottom_left.toTuple()), gp_Pnt(*local_top_right.toTuple())
)
- return cq.BoundBox(local_bbox)
+ return BoundBox(local_bbox)
else:
raise ValueError(
f"Don't know how to convert type {type(obj)} to local coordinates"
)
-cq.Plane.toLocalCoords = _toLocalCoords
+Plane.toLocalCoords = _toLocalCoords
"""
-Vector extensions: rotateX(), rotateY(), rotateZ(), pointToVector(), toVertex(), getSignedAngle()
+Vector extensions: rotateX(), rotateY(), rotateZ(), toVertex(), getSignedAngle()
"""
-def _vector_rotate_x(self, angle: float) -> cq.Vector:
- """cq.Vector rotate angle in degrees about x-axis"""
- return cq.Vector(
- self.x,
- self.y * cos(radians(angle)) - self.z * sin(radians(angle)),
- self.y * sin(radians(angle)) + self.z * cos(radians(angle)),
- )
+def _vector_rotate_x(self, angle: float) -> "Vector":
+ """Rotate Vector about X-Axis
+ Args:
+ angle: Angle in degrees
-cq.Vector.rotateX = _vector_rotate_x
+ Returns:
+ Rotated Vector
+ """
+ return Vector(
+ gp_Vec(self.x, self.y, self.z).Rotated(gp.OX_s(), math.pi * angle / 180)
+ )
-def _vector_rotate_y(self, angle: float) -> cq.Vector:
- """cq.Vector rotate angle in degrees about y-axis"""
- return cq.Vector(
- self.x * cos(radians(angle)) + self.z * sin(radians(angle)),
- self.y,
- -self.x * sin(radians(angle)) + self.z * cos(radians(angle)),
- )
+Vector.rotateX = _vector_rotate_x
-cq.Vector.rotateY = _vector_rotate_y
+def _vector_rotate_y(self, angle: float) -> "Vector":
+ """Rotate Vector about Y-Axis
+ Args:
+ angle: Angle in degrees
-def _vector_rotate_z(self, angle: float) -> cq.Vector:
- """cq.Vector rotate angle in degrees about z-axis"""
- return cq.Vector(
- self.x * cos(radians(angle)) - self.y * sin(radians(angle)),
- self.x * sin(radians(angle)) + self.y * cos(radians(angle)),
- self.z,
+ Returns:
+ Rotated Vector
+ """
+ return Vector(
+ gp_Vec(self.x, self.y, self.z).Rotated(gp.OY_s(), math.pi * angle / 180)
)
-cq.Vector.rotateZ = _vector_rotate_z
+Vector.rotateY = _vector_rotate_y
+
+
+def _vector_rotate_z(self, angle: float) -> "Vector":
+ """Rotate Vector about Z-Axis
+
+ Args:
+ angle: Angle in degrees
+ Returns:
+ Rotated Vector
+ """
+ return Vector(
+ gp_Vec(self.x, self.y, self.z).Rotated(gp.OZ_s(), math.pi * angle / 180)
+ )
-def _point_to_vector(self, plane: str, offset: float = 0.0) -> cq.Vector:
- """map a 2D point on the XY plane to 3D space on the given plane at the offset"""
- if not isinstance(plane, str) or plane not in ["XY", "XZ", "YZ"]:
- raise ValueError("plane " + str(plane) + " must be one of: XY,XZ,YZ")
- if plane == "XY":
- mapped_point = cq.Vector(self.x, self.y, offset)
- elif plane == "XZ":
- mapped_point = cq.Vector(self.x, offset, self.y)
- else: # YZ
- mapped_point = cq.Vector(offset, self.x, self.y)
- return mapped_point
+Vector.rotateZ = _vector_rotate_z
-cq.Vector.pointToVector = _point_to_vector
+def _vector_to_vertex(self) -> "Vertex":
+ """Convert to Vector to Vertex
-def _toVertex(self):
- """Convert a Vector to a Vertex"""
- return cq.Vertex.makeVertex(*self.toTuple())
+ Returns:
+ Vertex equivalent of Vector
+ """
+ return Vertex.makeVertex(*self.toTuple())
-cq.Vector.toVertex = _toVertex
+Vector.toVertex = _vector_to_vertex
def _getSignedAngle(self, v: "Vector", normal: "Vector" = None) -> float:
+ """Signed Angle Between Vectors
- """
Return the signed angle in RADIANS between two vectors with the given normal
- based on this math:
- angle = atan2((Va x Vb) . Vn, Va . Vb)
- """
+ based on this math: angle = atan2((Va × Vb) ⋅ Vn, Va ⋅ Vb)
+
+ Args:
+ v: Second Vector.
+
+ normal: Vector's Normal. Defaults to -Z Axis.
+ Returns:
+ Angle between vectors
+ """
if normal is None:
gp_normal = gp_Vec(0, 0, -1)
else:
@@ -229,27 +357,46 @@ def _getSignedAngle(self, v: "Vector", normal: "Vector" = None) -> float:
return self.wrapped.AngleWithRef(v.wrapped, gp_normal)
-cq.Vector.getSignedAngle = _getSignedAngle
+Vector.getSignedAngle = _getSignedAngle
"""
-Vertex extensions: __add__(), __sub__(), __str__()
+Vertex extensions: __add__(), __sub__(), __str__(), toVector
"""
-def __vertex_add__(
- self, other: Union[cq.Vertex, cq.Vector, Tuple[float, float, float]]
-) -> cq.Vertex:
- """Add a Vector or tuple of floats to a Vertex"""
- if isinstance(other, cq.Vertex):
- new_vertex = cq.Vertex.makeVertex(
+def _vertex_add__(
+ self, other: Union["Vertex", "Vector", Tuple[float, float, float]]
+) -> "Vertex":
+ """Add
+
+ Add to a Vertex with a Vertex, Vector or Tuple
+
+ Args:
+ other: Value to add
+
+ Raises:
+ TypeError: other not in [Tuple,Vector,Vertex]
+
+ Returns:
+ Result
+
+ Example:
+ part.faces(">Z").vertices(" "Vertex":
+ """Subtract
+
+ Substract a Vertex with a Vertex, Vector or Tuple from self
+ Args:
+ other: Value to add
-def __vertex_sub__(self, other: Union[cq.Vertex, cq.Vector, tuple]) -> cq.Vertex:
- """Subtract a Vector or tuple of floats to a Vertex"""
- if isinstance(other, cq.Vertex):
- new_vertex = cq.Vertex.makeVertex(
+ Raises:
+ TypeError: other not in [Tuple,Vector,Vertex]
+
+ Returns:
+ Result
+
+ Example:
+ part.faces(">Z").vertices(" cq.Vertex
return new_vertex
-cq.Vertex.__sub__ = __vertex_sub__
+Vertex.__sub__ = _vertex_sub__
-def __vertex_str__(self) -> str:
- """Display a Vertex"""
+def _vertex_str__(self) -> str:
+ """To String
+
+ Convert Vertex to String for display
+
+ Returns:
+ Vertex as String
+ """
return f"Vertex: ({self.X}, {self.Y}, {self.Z})"
-cq.Vertex.__str__ = __vertex_str__
+Vertex.__str__ = _vertex_str__
-def _vertex_to_vector(self) -> cq.Vector:
- """Convert a Vertex to a Vector"""
- return cq.Vector(self.toTuple())
+def _vertex_to_vector(self) -> "Vector":
+ """To Vector
+ Convert a Vertex to Vector
-cq.Vertex.toVector = _vertex_to_vector
+ Returns:
+ Vector representation of Vertex
+ """
+ return Vector(self.toTuple())
-"""
+Vertex.toVector = _vertex_to_vector
-Workplane extensions: textOnPath(), hexArray(), thicken()
"""
-cq.Workplane.text
+Workplane extensions: textOnPath(), hexArray(), thicken(), fastenerHole(), clearanceHole(),
+ tapHole(), threadedHole(), pushFastenerLocations()
+
+"""
-def textOnPath(
+def _textOnPath(
self: T,
txt: str,
fontsize: float,
@@ -325,19 +498,32 @@ def textOnPath(
"""
Returns 3D text with the baseline following the given path.
- :param txt: text to be rendered
- :param fontsize: size of the font in model units
- :param distance: the distance to extrude or cut, normal to the workplane plane
- :type distance: float, negative means opposite the normal direction
- :param start: the relative location on path to start the text
- :type start: float, values must be between 0.0 and 1.0
- :param cut: True to cut the resulting solid from the parent solids if found
- :param combine: True to combine the resulting solid with parent solids if found
- :param clean: call :py:meth:`clean` afterwards to have a clean shape
- :param font: font name
- :param fontPath: path to font file
- :param kind: font type
- :return: a CQ object with the resulting solid selected
+ The parameters are largely the same as the
+ `Workplane.text() `_
+ method. The **start** parameter (normally between 0.0 and 1.0) specify where on the path to
+ start the text.
+
+ The path that the text follows is defined by the last Edge or Wire in the
+ Workplane stack. Path's defined outside of the Workplane can be used with the
+ `add() `_
+ method.
+
+ .. image:: textOnPath.png
+
+ Args:
+ txt: text to be rendered
+ fontsize: size of the font in model units
+ distance: the distance to extrude or cut, normal to the workplane plane, negative means opposite the normal direction
+ start: the relative location on path to start the text, values must be between 0.0 and 1.0
+ cut: True to cut the resulting solid from the parent solids if found
+ combine: True to combine the resulting solid with parent solids if found
+ clean: call :py:meth:`clean` afterwards to have a clean shape
+ font: font name
+ fontPath: path to font file
+ kind: font type
+
+ Returns:
+ a CQ object with the resulting solid selected
The returned object is always a Workplane object, and depends on whether combine is True, and
whether a context solid is already defined:
@@ -349,7 +535,7 @@ def textOnPath(
Examples::
fox = (
- cq.Workplane("XZ")
+ Workplane("XZ")
.threePointArc((50, 30), (100, 0))
.textOnPath(
txt="The quick brown fox jumped over the lazy dog",
@@ -360,7 +546,7 @@ def textOnPath(
)
clover = (
- cq.Workplane("front")
+ Workplane("front")
.moveTo(0, 10)
.radiusArc((10, 0), 7.5)
.radiusArc((0, -10), 7.5)
@@ -375,7 +561,9 @@ def textOnPath(
)
"""
- def position_face(orig_face: cq.Face) -> cq.Face:
+ # from .selectors import DirectionMinMaxSelector
+
+ def position_face(orig_face: "Face") -> "Face":
"""
Reposition a face to the provided path
@@ -383,12 +571,15 @@ def position_face(orig_face: cq.Face) -> cq.Face:
relative to the path. Global coordinates to position the face.
"""
bbox = self.plane.toLocalCoords(orig_face.BoundingBox())
- face_bottom_center = cq.Vector((bbox.xmin + bbox.xmax) / 2, 0, 0)
+ face_bottom_center = Vector((bbox.xmin + bbox.xmax) / 2, 0, 0)
relative_position_on_wire = start + face_bottom_center.x / path_length
wire_tangent = path.tangentAt(relative_position_on_wire)
- wire_angle = degrees(
- self.plane.xDir.getSignedAngle(wire_tangent, self.plane.zDir)
+ wire_angle = (
+ 180
+ * self.plane.xDir.getSignedAngle(wire_tangent, self.plane.zDir)
+ / math.pi
)
+
wire_position = path.positionAt(relative_position_on_wire)
global_face_bottom_center = self.plane.toWorldCoords(face_bottom_center)
return orig_face.translate(wire_position - global_face_bottom_center).rotate(
@@ -401,15 +592,15 @@ def position_face(orig_face: cq.Face) -> cq.Face:
if not self.ctx.pendingWires and not self.ctx.pendingEdges:
raise Exception("A pending edge or wire must be present to define the path")
for stack_object in self.vals():
- if type(stack_object) == cq.Edge:
+ if type(stack_object) == Edge:
path = self.ctx.pendingEdges.pop(0)
break
- if type(stack_object) == cq.Wire:
+ if type(stack_object) == Wire:
path = self.ctx.pendingWires.pop(0)
break
# Create text on the current workplane
- raw_text = cq.Compound.makeText(
+ raw_text = Compound.makeText(
txt,
fontsize,
distance,
@@ -422,16 +613,16 @@ def position_face(orig_face: cq.Face) -> cq.Face:
)
# Extract just the faces on the workplane
text_faces = (
- cq.Workplane(raw_text)
- .faces(cq.DirectionMinMaxSelector(self.plane.zDir, False))
+ Workplane(raw_text)
+ .faces(DirectionMinMaxSelector(self.plane.zDir, False))
.vals()
)
path_length = path.Length()
# Reposition all of the text faces and re-create 3D text
faces_on_path = [position_face(f) for f in text_faces]
- result = cq.Compound.makeCompound(
- [cq.Solid.extrudeLinear(f, self.plane.zDir) for f in faces_on_path]
+ result = Compound.makeCompound(
+ [Solid.extrudeLinear(f, self.plane.zDir) for f in faces_on_path]
)
if cut:
new_solid = self._cutFromBase(result)
@@ -444,7 +635,7 @@ def position_face(orig_face: cq.Face) -> cq.Face:
return new_solid
-cq.Workplane.textOnPath = textOnPath
+Workplane.textOnPath = _textOnPath
def _hexArray(
@@ -454,21 +645,26 @@ def _hexArray(
yCount: int,
center: Union[bool, tuple[bool, bool]] = True,
):
- """
+ """Create Hex Array
+
Creates a hexagon array of points and pushes them onto the stack.
If you want to position the array at another point, create another workplane
that is shifted to the position you would like to use as a reference
- :param diagonal: tip to tip size of hexagon ( must be > 0)
- :param xCount: number of points ( > 0 )
- :param yCount: number of points ( > 0 )
- :param center: If True, the array will be centered around the workplane center.
- If False, the lower corner will be on the reference point and the array will
- extend in the positive x and y directions. Can also use a 2-tuple to specify
- centering along each axis.
+ Args:
+ diagonal: tip to tip size of hexagon ( must be > 0)
+ xCount: number of points ( > 0 )
+ yCount: number of points ( > 0 )
+ center: If True, the array will be centered around the workplane center.
+ If False, the lower corner will be on the reference point and the array will
+ extend in the positive x and y directions. Can also use a 2-tuple to specify
+ centering along each axis.
+
+ Returns:
+ Places points on the Workplane stack
"""
xSpacing = 3 * diagonal / 4
- ySpacing = diagonal * sqrt(3) / 2
+ ySpacing = diagonal * math.sqrt(3) / 2
if xSpacing <= 0 or ySpacing <= 0 or xCount < 1 or yCount < 1:
raise ValueError("Spacing and count must be > 0 ")
@@ -478,33 +674,361 @@ def _hexArray(
lpoints = [] # coordinates relative to bottom left point
for x in range(0, xCount, 2):
for y in range(yCount):
- lpoints.append(cq.Vector(xSpacing * x, ySpacing * y + ySpacing / 2))
+ lpoints.append(Vector(xSpacing * x, ySpacing * y + ySpacing / 2))
for x in range(1, xCount, 2):
for y in range(yCount):
- lpoints.append(cq.Vector(xSpacing * x, ySpacing * y + ySpacing))
+ lpoints.append(Vector(xSpacing * x, ySpacing * y + ySpacing))
# shift points down and left relative to origin if requested
- offset = cq.Vector()
+ offset = Vector()
if center[0]:
- offset += cq.Vector(-xSpacing * (xCount - 1) * 0.5, 0)
+ offset += Vector(-xSpacing * (xCount - 1) * 0.5, 0)
if center[1]:
- offset += cq.Vector(0, -ySpacing * (yCount - 1) * 0.5)
+ offset += Vector(0, -ySpacing * (yCount - 1) * 0.5)
lpoints = [x + offset for x in lpoints]
return self.pushPoints(lpoints)
-cq.Workplane.hexArray = _hexArray
+Workplane.hexArray = _hexArray
+
+
+def _workplane_thicken(self, depth: float, direction: "Vector" = None):
+ """Thicken Face
+
+ Find all of the faces on the stack and make them Solid objects by thickening
+ along the normals.
+ Args:
+ depth: Amount to thicken face(s), can be positive or negative.
+ direction: The direction vector can be used to
+ indicate which way is 'up', potentially flipping the face normal direction
+ such that many faces with different normals all go in the same direction
+ (direction need only be +/- 90 degrees from the face normal). Defaults to None.
-def _workplaneThicken(self, depth: float, direction: cq.Vector = None):
- """Find all of the faces on the stack and make them Solid objects by thickening along the normals"""
+ Returns:
+ A set of new objects on the Workplane stack
+ """
return self.newObject([f.thicken(depth, direction) for f in self.faces().vals()])
-cq.Workplane.thicken = _workplaneThicken
+Workplane.thicken = _workplane_thicken
+
+
+def _fastenerHole(
+ self: T,
+ hole_diameters: dict,
+ fastener: Union["Nut", "Screw"],
+ depth: float,
+ washers: list["Washer"],
+ fit: Optional[Literal["Close", "Normal", "Loose"]] = None,
+ material: Optional[Literal["Soft", "Hard"]] = None,
+ counterSunk: Optional[bool] = True,
+ baseAssembly: Optional["Assembly"] = None,
+ hand: Optional[Literal["right", "left"]] = None,
+ simple: Optional[bool] = False,
+ clean: Optional[bool] = True,
+) -> T:
+ """Fastener Specific Hole
+
+ Makes a counterbore clearance, tap or threaded hole for the given screw for each item
+ on the stack. The surface of the hole is at the current workplane.
+
+ Args:
+ hole_diameters: either clearance or tap hole diameter specifications
+ fastener: A nut or screw instance
+ depth: hole depth
+ washers: A list of washer instances, can be empty
+ fit: one of "Close", "Normal", "Loose" which determines clearance hole diameter. Defaults to None.
+ material: on of "Soft", "Hard" which determines tap hole size. Defaults to None.
+ counterSunk: Is the fastener countersunk into the part?. Defaults to True.
+ baseAssembly: Assembly to add faster to. Defaults to None.
+ hand: tap hole twist direction either "right" or "left". Defaults to None.
+ simple: tap hole thread complexity selector. Defaults to False.
+ clean: execute a clean operation remove extraneous internal features. Defaults to True.
+
+ Raises:
+ ValueError: fit or material not in hole_diameters dictionary
+
+ Returns:
+ the shape on the workplane stack with a new hole
+ """
+ from cq_warehouse.thread import IsoThread
+
+ # If there is a thread direction, this is a threaded hole
+ threaded_hole = not hand is None
+
+ bore_direction = Vector(0, 0, -1)
+ origin = Vector(0, 0, 0)
+
+ # Setscrews' countersink_profile is None so check if it exists
+ countersink_profile = fastener.countersink_profile(fit)
+ if counterSunk and not countersink_profile is None:
+ head_offset = countersink_profile.vertices(">Z").val().Z
+ countersink_cutter = (
+ countersink_profile.revolve().translate((0, 0, -head_offset)).val()
+ )
+ else:
+ head_offset = 0
+
+ if threaded_hole:
+ hole_radius = fastener.thread_diameter / 2
+ else:
+ key = fit if material is None else material
+ try:
+ hole_radius = hole_diameters[key] / 2
+ except KeyError as e:
+ raise ValueError(
+ f"{key} invalid, must be one of {list(hole_diameters.keys())}"
+ ) from e
+
+ shank_hole = Solid.makeCylinder(
+ radius=hole_radius,
+ height=depth,
+ pnt=origin,
+ dir=bore_direction,
+ )
+ if counterSunk and not countersink_profile is None:
+ fastener_hole = countersink_cutter.fuse(shank_hole)
+ else:
+ fastener_hole = shank_hole
+
+ cskAngle = 82 # Common tip angle
+ h = hole_radius / math.tan(math.radians(cskAngle / 2.0))
+ drill_tip = Solid.makeCone(
+ hole_radius, 0.0, h, bore_direction * depth, bore_direction
+ )
+ fastener_hole = fastener_hole.fuse(drill_tip)
+
+ # Record the location of each hole for use in the assembly
+ null_object = Solid.makeBox(1, 1, 1)
+ relocated_test_objects = self.eachpoint(lambda loc: null_object.moved(loc), True)
+ hole_locations = [loc.location() for loc in relocated_test_objects.vals()]
+
+ # Add fasteners and washers to the base assembly if it was provided
+ if baseAssembly is not None:
+ for hole_loc in hole_locations:
+ washer_thicknesses = 0
+ if not washers is None:
+ for washer in washers:
+ baseAssembly.add(
+ washer.cq_object,
+ loc=hole_loc
+ * Location(
+ bore_direction
+ * (
+ head_offset
+ - fastener.length_offset()
+ - washer_thicknesses
+ )
+ ),
+ )
+ washer_thicknesses += washer.washer_thickness
+ # Create a metadata entry associating the auto-generated name & fastener
+ baseAssembly.metadata[baseAssembly.children[-1].name] = washer
+
+ baseAssembly.add(
+ fastener.cq_object,
+ loc=hole_loc
+ * Location(
+ bore_direction
+ * (head_offset - fastener.length_offset() - washer_thicknesses)
+ ),
+ )
+ # Create a metadata entry associating the auto-generated name & fastener
+ baseAssembly.metadata[baseAssembly.children[-1].name] = fastener
+
+ # Make holes in the stack solid object
+ part = self.cutEach(lambda loc: fastener_hole.moved(loc), True, False)
+
+ # Add threaded inserts
+ if threaded_hole and not simple:
+ thread = IsoThread(
+ major_diameter=fastener.thread_diameter,
+ pitch=fastener.thread_pitch,
+ length=depth - head_offset,
+ external=False,
+ hand=hand,
+ )
+ for hole_loc in hole_locations:
+ part = part.union(
+ thread.cq_object.moved(hole_loc * Location(bore_direction * depth))
+ )
+ if clean:
+ part = part.clean()
+ return part
+
+
+Workplane.fastenerHole = _fastenerHole
+def _clearanceHole(
+ self: T,
+ fastener: Union["Nut", "Screw"],
+ washers: Optional[list["Washer"]] = None,
+ fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
+ depth: Optional[float] = None,
+ counterSunk: Optional[bool] = True,
+ baseAssembly: Optional["Assembly"] = None,
+ clean: Optional[bool] = True,
+) -> T:
+ """Clearance Hole
+
+ Put a clearance hole in a shape at the provided location
+
+ For more information on how to use clearanceHole() see
+ :ref:`Clearance, Tap and Threaded Holes `.
+
+ Args:
+ fastener: A nut or screw instance
+ washers: A list of washer instances, can be empty
+ fit: one of "Close", "Normal", "Loose" which determines clearance hole diameter. Defaults to None.
+ depth: hole depth. Defaults to through part.
+ counterSunk: Is the fastener countersunk into the part?. Defaults to True.
+ baseAssembly: Assembly to add faster to. Defaults to None.
+ clean: execute a clean operation remove extraneous internal features. Defaults to True.
+
+ Returns:
+ the shape on the workplane stack with a new clearance hole
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ return self.fastenerHole(
+ hole_diameters=fastener.clearance_hole_diameters,
+ fastener=fastener,
+ washers=washers,
+ fit=fit,
+ depth=depth,
+ counterSunk=counterSunk,
+ baseAssembly=baseAssembly,
+ clean=clean,
+ )
+
+
+def _tapHole(
+ self: T,
+ fastener: Union["Nut", "Screw"],
+ washers: Optional[list["Washer"]] = None,
+ material: Optional[Literal["Soft", "Hard"]] = "Soft",
+ depth: Optional[float] = None,
+ counterSunk: Optional[bool] = True,
+ fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
+ baseAssembly: Optional["Assembly"] = None,
+ clean: Optional[bool] = True,
+) -> T:
+ """Tap Hole
+
+ Put a tap hole in a shape at the provided location
+
+ For more information on how to use tapHole() see
+ :ref:`Clearance, Tap and Threaded Holes `.
+
+ Args:
+ fastener: A nut or screw instance
+ washers: A list of washer instances, can be empty
+ material: on of "Soft", "Hard" which determines tap hole size. Defaults to None.
+ depth: hole depth. Defaults to through part.
+ counterSunk: Is the fastener countersunk into the part?. Defaults to True.
+ fit: one of "Close", "Normal", "Loose" which determines clearance hole diameter. Defaults to None.
+ baseAssembly: Assembly to add faster to. Defaults to None.
+ clean: execute a clean operation remove extraneous internal features. Defaults to True.
+
+ Returns:
+ the shape on the workplane stack with a new tap hole
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ return self.fastenerHole(
+ hole_diameters=fastener.tap_hole_diameters,
+ fastener=fastener,
+ washers=washers,
+ fit=fit,
+ material=material,
+ depth=depth,
+ counterSunk=counterSunk,
+ baseAssembly=baseAssembly,
+ clean=clean,
+ )
+
+
+def _threadedHole(
+ self: T,
+ fastener: "Screw",
+ depth: float,
+ washers: Optional[list["Washer"]] = None,
+ hand: Literal["right", "left"] = "right",
+ simple: Optional[bool] = False,
+ counterSunk: Optional[bool] = True,
+ fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
+ baseAssembly: Optional["Assembly"] = None,
+ clean: Optional[bool] = True,
+) -> T:
+ """Threaded Hole
+
+ Put a threaded hole in a shape at the provided location
+
+ For more information on how to use threadedHole() see
+ :ref:`Clearance, Tap and Threaded Holes `.
+
+ Args:
+ fastener: A nut or screw instance
+ depth: hole depth. Defaults to through part.
+ washers: A list of washer instances, can be empty
+ hand: tap hole twist direction either "right" or "left". Defaults to None.
+ simple (Optional[bool], optional): [description]. Defaults to False.
+ counterSunk: Is the fastener countersunk into the part?. Defaults to True.
+ fit: one of "Close", "Normal", "Loose" which determines clearance hole diameter. Defaults to None.
+ baseAssembly: Assembly to add faster to. Defaults to None.
+ clean: execute a clean operation remove extraneous internal features. Defaults to True.
+
+ Returns:
+ the shape on the workplane stack with a new threaded hole
+ """
+ return self.fastenerHole(
+ hole_diameters=fastener.clearance_hole_diameters,
+ fastener=fastener,
+ washers=washers,
+ fit=fit,
+ depth=depth,
+ counterSunk=counterSunk,
+ baseAssembly=baseAssembly,
+ hand=hand,
+ simple=simple,
+ clean=clean,
+ )
+
+
+Workplane.clearanceHole = _clearanceHole
+Workplane.tapHole = _tapHole
+Workplane.threadedHole = _threadedHole
+
+
+def _push_fastener_locations(
+ self: T,
+ fastener: Union["Nut", "Screw"],
+ baseAssembly: "Assembly",
+):
+ """Push Fastener Locations
+
+ Push the Location(s) of the given fastener relative to the given Assembly onto the workplane stack.
+
+ Returns:
+ Location objects on the workplane stack
+ """
+
+ # The locations need to be pushed as global not local object locations
+ ns = self.__class__()
+ ns.plane = Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1))
+ ns.parent = self
+ ns.objects = baseAssembly.fastenerLocations(fastener)
+ ns.ctx = self.ctx
+ return ns
+
+
+Workplane.pushFastenerLocations = _push_fastener_locations
+
"""
Face extensions: thicken(), projectToShape(), embossToShape()
@@ -512,12 +1036,27 @@ def _workplaneThicken(self, depth: float, direction: cq.Vector = None):
"""
-def _faceThicken(self, depth: float, direction: cq.Vector = None) -> cq.Solid:
- """
+def _face_thicken(self, depth: float, direction: "Vector" = None) -> "Solid":
+ """Thicken Face
+
Create a solid from a potentially non planar face by thickening along the normals.
- The direction vector can be used to indicate which way is 'up', potentially flipping the
- face normal direction such that many faces with different normals all go in the same
- direction (direction need only be +/- 90 degrees from the face normal.)
+
+ .. image:: thickenFace.png
+
+ Non-planar faces are thickened both towards and away from the center of the sphere.
+
+ Args:
+ depth: Amount to thicken face(s), can be positive or negative.
+ direction: The direction vector can be used to
+ indicate which way is 'up', potentially flipping the face normal direction
+ such that many faces with different normals all go in the same direction
+ (direction need only be +/- 90 degrees from the face normal). Defaults to None.
+
+ Raises:
+ RuntimeError: Opencascade internal failures
+
+ Returns:
+ The resulting Solid object
"""
# Check to see if the normal needs to be flipped
@@ -544,42 +1083,74 @@ def _faceThicken(self, depth: float, direction: cq.Vector = None) -> cq.Solid:
)
solid.MakeOffsetShape()
try:
- result = cq.Solid(solid.Shape())
+ result = Solid(solid.Shape())
except StdFail_NotDone as e:
raise RuntimeError("Error applying thicken to given Face") from e
return result
-cq.Face.thicken = _faceThicken
+Face.thicken = _face_thicken
-def _projectFaceToShape(
- self: cq.Face,
- targetObject: cq.Shape,
- direction: VectorLike = None,
- center: VectorLike = None,
- internalFacePoints: list[cq.Vector] = [],
-) -> list[cq.Face]:
- """
+def _face_projectToShape(
+ self,
+ targetObject: "Shape",
+ direction: "VectorLike" = None,
+ center: "VectorLike" = None,
+ internalFacePoints: list["Vector"] = [],
+) -> list["Face"]:
+ """Project Face to target Object
+
Project a Face onto a Shape generating new Face(s) on the surfaces of the object
one and only one of `direction` or `center` must be provided.
- There are four phase to creation of the projected face:
- 1- extract the outer wire and project
- 2- extract the inner wires and project
- 3- extract surface points within the outer wire
- 4- build a non planar face
+ The two types of projections are illustrated below:
+
+ .. image:: flatProjection.png
+ :alt: flatProjection
+
+ .. image:: conicalProjection.png
+ :alt: conicalProjection
+
+ Note that an array of Faces is returned as the projection might result in faces
+ on the "front" and "back" of the object (or even more if there are intermediate
+ surfaces in the projection path). Faces "behind" the projection are not
+ returned.
+
+ To help refine the resulting face, a list of planar points can be passed to
+ augment the surface definition. For example, when projecting a circle onto a
+ sphere, a circle will result which will get converted to a planar circle face.
+ If no points are provided, a single center point will be generated and used for
+ this purpose.
+
+ Args:
+ targetObject: Object to project onto
+ direction: Parallel projection direction. Defaults to None.
+ center: Conical center of projection. Defaults to None.
+ internalFacePoints: Points refining shape. Defaults to [].
+
+ Raises:
+ ValueError: Only one of direction or center must be provided
+
+ Returns:
+ Face(s) projected on target object
"""
+ # There are four phase to creation of the projected face:
+ # 1- extract the outer wire and project
+ # 2- extract the inner wires and project
+ # 3- extract surface points within the outer wire
+ # 4- build a non planar face
+
if not (direction is None) ^ (center is None):
raise ValueError("One of either direction or center must be provided")
if direction is not None:
- direction_vector = cq.Vector(direction)
+ direction_vector = Vector(direction)
center_point = None
else:
direction_vector = None
- center_point = cq.Vector(center)
+ center_point = Vector(center)
# Phase 1 - outer wire
planar_outer_wire = self.outerWire()
@@ -594,7 +1165,7 @@ def _projectFaceToShape(
planar_inner_wire_list = [
w
if w.wrapped.Orientation() != planar_outer_wire_orientation
- else cq.Wire(w.wrapped.Reversed())
+ else Wire(w.wrapped.Reversed())
for w in self.innerWires()
]
# Project inner wires on to potentially multiple surfaces
@@ -625,19 +1196,16 @@ def _projectFaceToShape(
projected_grid_points = []
else:
if len(internalFacePoints) == 1:
- planar_grid = cq.Edge.makeLine(
+ planar_grid = Edge.makeLine(
planar_outer_wire.positionAt(0), internalFacePoints[0]
)
else:
- planar_grid = cq.Wire.makePolygon(
- [cq.Vector(v) for v in internalFacePoints]
- )
+ planar_grid = Wire.makePolygon([Vector(v) for v in internalFacePoints])
projected_grids = planar_grid.projectToShape(
targetObject, direction_vector, center_point
)
projected_grid_points = [
- [cq.Vector(*v.toTuple()) for v in grid.Vertices()]
- for grid in projected_grids
+ [Vector(*v.toTuple()) for v in grid.Vertices()] for grid in projected_grids
]
logging.debug(f"projecting grid resulted in {len(projected_grid_points)} points")
@@ -653,25 +1221,38 @@ def _projectFaceToShape(
return projected_faces
-cq.Face.projectToShape = _projectFaceToShape
-
+Face.projectToShape = _face_projectToShape
-def _embossFaceToShape(
- self: cq.Face,
- targetObject: cq.Shape,
- surfacePoint: VectorLike,
- surfaceXDirection: VectorLike,
- internalFacePoints: list[cq.Vector] = [],
-) -> cq.Face:
- """
- Emboss a Face onto a Shape
- There are four phase to creation of the projected face:
- 1- extract the outer wire and project
- 2- extract the inner wires and project
- 3- extract surface points within the outer wire
- 4- build a non planar face
+def _face_embossToShape(
+ self,
+ targetObject: "Shape",
+ surfacePoint: "VectorLike",
+ surfaceXDirection: "VectorLike",
+ internalFacePoints: list["Vector"] = [],
+) -> "Face":
+ """Emboss Face on target object
+
+ Emboss a Face on the XY plane onto a Shape while maintaining
+ original face dimensions where possible.
+
+ Unlike projection, a single Face is returned. The internalFacePoints
+ parameter works as with projection.
+
+ Args:
+ targetObject: Object to emboss onto
+ surfacePoint: Point on target object to start embossing
+ surfaceXDirection: Direction of X-Axis on target object
+ internalFacePoints: Surface refinement points. Defaults to [].
+
+ Returns:
+ Face: Embossed face
"""
+ # There are four phase to creation of the projected face:
+ # 1- extract the outer wire and project
+ # 2- extract the inner wires and project
+ # 3- extract surface points within the outer wire
+ # 4- build a non planar face
# Phase 1 - outer wire
planar_outer_wire = self.outerWire()
@@ -684,7 +1265,7 @@ def _embossFaceToShape(
planar_inner_wires = [
w
if w.wrapped.Orientation() != planar_outer_wire_orientation
- else cq.Wire(w.wrapped.Reversed())
+ else Wire(w.wrapped.Reversed())
for w in self.innerWires()
]
embossed_inner_wires = [
@@ -703,19 +1284,17 @@ def _embossFaceToShape(
embossed_surface_points = []
else:
if len(internalFacePoints) == 1:
- planar_grid = cq.Edge.makeLine(
+ planar_grid = Edge.makeLine(
planar_outer_wire.positionAt(0), internalFacePoints[0]
)
else:
- planar_grid = cq.Wire.makePolygon(
- [cq.Vector(v) for v in internalFacePoints]
- )
+ planar_grid = Wire.makePolygon([Vector(v) for v in internalFacePoints])
embossed_grid = planar_grid.embossToShape(
targetObject, surfacePoint, surfaceXDirection
)
embossed_surface_points = [
- cq.Vector(*v.toTuple()) for v in embossed_grid.Vertices()
+ Vector(*v.toTuple()) for v in embossed_grid.Vertices()
]
# Phase 4 - Build the faces
@@ -726,8 +1305,7 @@ def _embossFaceToShape(
return embossed_face
-cq.Face.embossToShape = _embossFaceToShape
-
+Face.embossToShape = _face_embossToShape
"""
@@ -737,14 +1315,29 @@ def _embossFaceToShape(
def makeNonPlanarFace(
- exterior: Union[cq.Wire, list[cq.Edge]],
- surfacePoints: list[VectorLike] = None,
- interiorWires: list[cq.Wire] = None,
-) -> cq.Face:
- """Create a potentially non-planar face bounded by exterior (wire or edges),
- optionally refined by surfacePoints with optional holes defined by interiorWires"""
+ exterior: Union["Wire", list["Edge"]],
+ surfacePoints: list["VectorLike"] = None,
+ interiorWires: list["Wire"] = None,
+) -> "Face":
+ """Create Non-Planar Face
+
+ Create a potentially non-planar face bounded by exterior (wire or edges),
+ optionally refined by surfacePoints with optional holes defined by
+ interiorWires.
+
+ Args:
+ exterior: Perimeter of face
+ surfacePoints: Points on the surface that refine the shape. Defaults to None.
+ interiorWires: Hole(s) in the face. Defaults to None.
+
+ Raises:
+ RuntimeError: Opencascade core exceptions building face
+
+ Returns:
+ Non planar face
+ """
- surface_points = [cq.Vector(p) for p in surfacePoints]
+ surface_points = [Vector(p) for p in surfacePoints]
# First, create the non-planar surface
surface = BRepOffsetAPI_MakeFilling(
@@ -759,7 +1352,7 @@ def makeNonPlanarFace(
MaxDeg=8, # the highest degree which the polynomial defining the filling surface can have
MaxSegments=9, # the greatest number of segments which the filling surface can have
)
- if isinstance(exterior, cq.Wire):
+ if isinstance(exterior, Wire):
outside_edges = exterior.Edges()
else:
outside_edges = [e.Edge() for e in exterior]
@@ -768,7 +1361,7 @@ def makeNonPlanarFace(
try:
surface.Build()
- surface_face = cq.Face(surface.Shape())
+ surface_face = Face(surface.Shape())
except (StdFail_NotDone, Standard_NoSuchObject) as e:
raise RuntimeError(
"Error building non-planar face with provided exterior"
@@ -778,7 +1371,7 @@ def makeNonPlanarFace(
surface.Add(gp_Pnt(*pt.toTuple()))
try:
surface.Build()
- surface_face = cq.Face(surface.Shape())
+ surface_face = Face(surface.Shape())
except StdFail_NotDone as e:
raise RuntimeError(
"Error building non-planar face with provided surfacePoints"
@@ -790,7 +1383,7 @@ def makeNonPlanarFace(
for w in interiorWires:
makeface_object.Add(w.wrapped)
try:
- surface_face = cq.Face(makeface_object.Face())
+ surface_face = Face(makeface_object.Face())
except StdFail_NotDone as e:
raise RuntimeError(
"Error adding interior hole in non-planar face with provided interiorWires"
@@ -803,52 +1396,84 @@ def makeNonPlanarFace(
return surface_face
-def _makeNonPlanarFace(
+def _wire_makeNonPlanarFace(
self,
- surfacePoints: list[cq.Vector] = None,
- interiorWires: list[cq.Wire] = None,
-) -> cq.Face:
+ surfacePoints: list["Vector"] = None,
+ interiorWires: list["Wire"] = None,
+) -> "Face":
+ """Create Non-Planar Face with perimeter Wire
+
+ Create a potentially non-planar face bounded by exterior Wire,
+ optionally refined by surfacePoints with optional holes defined by
+ interiorWires.
+
+ The **surfacePoints** parameter can be used to refine the resulting Face. If no
+ points are provided a single central point will be used to help avoid the
+ creation of a planar face.
+
+ Args:
+ surfacePoints: Points on the surface that refine the shape. Defaults to None.
+ interiorWires: Hole(s) in the face. Defaults to None.
+
+ Raises:
+ RuntimeError: Opencascade core exceptions building face
+
+ Returns:
+ Non planar face
+ """
return makeNonPlanarFace(self, surfacePoints, interiorWires)
-cq.Wire.makeNonPlanarFace = _makeNonPlanarFace
+Wire.makeNonPlanarFace = _wire_makeNonPlanarFace
def _projectWireToShape(
- self: Union[cq.Wire, cq.Edge],
- targetObject: cq.Shape,
- direction: VectorLike = None,
- center: VectorLike = None,
-) -> list[cq.Wire]:
- """
+ self,
+ targetObject: "Shape",
+ direction: "VectorLike" = None,
+ center: "VectorLike" = None,
+) -> list["Wire"]:
+ """Project Wire
+
Project a Wire onto a Shape generating new Wires on the surfaces of the object
- one and only one of `direction` or `center` must be provided. Note that one more
+ one and only one of `direction` or `center` must be provided. Note that one or
more wires may be generated depending on the topology of the target object and
location/direction of projection.
To avoid flipping the normal of a face built with the projected wire the orientation
of the output wires are forced to be the same as self.
+
+ Args:
+ targetObject: Object to project onto
+ direction: Parallel projection direction. Defaults to None.
+ center: Conical center of projection. Defaults to None.
+
+ Raises:
+ ValueError: Only one of direction or center must be provided
+
+ Returns:
+ Projected wire(s)
"""
if not (direction is None) ^ (center is None):
raise ValueError("One of either direction or center must be provided")
if direction is not None:
- direction_vector = cq.Vector(direction).normalized()
+ direction_vector = Vector(direction).normalized()
center_point = None
else:
direction_vector = None
- center_point = cq.Vector(center)
+ center_point = Vector(center)
# Project the wire on the target object
if not direction_vector is None:
projection_object = BRepProj_Projection(
self.wrapped,
- cq.Shape.cast(targetObject.wrapped).wrapped,
+ Shape.cast(targetObject.wrapped).wrapped,
gp_Dir(*direction_vector.toTuple()),
)
else:
projection_object = BRepProj_Projection(
self.wrapped,
- cq.Shape.cast(targetObject.wrapped).wrapped,
+ Shape.cast(targetObject.wrapped).wrapped,
gp_Pnt(*center_point.toTuple()),
)
@@ -858,9 +1483,9 @@ def _projectWireToShape(
while projection_object.More():
projected_wire = projection_object.Current()
if target_orientation == projected_wire.Orientation():
- output_wires.append(cq.Wire(projected_wire))
+ output_wires.append(Wire(projected_wire))
else:
- output_wires.append(cq.Wire(projected_wire.Reversed()))
+ output_wires.append(Wire(projected_wire.Reversed()))
projection_object.Next()
logging.debug(f"wire generated {len(output_wires)} projected wires")
@@ -898,18 +1523,41 @@ def _projectWireToShape(
return output_wires
-cq.Wire.projectToShape = _projectWireToShape
+Wire.projectToShape = _projectWireToShape
def _embossWireToShape(
- self: cq.Wire,
- targetObject: cq.Shape,
- surfacePoint: VectorLike,
- surfaceXDirection: VectorLike,
+ self,
+ targetObject: "Shape",
+ surfacePoint: "VectorLike",
+ surfaceXDirection: "VectorLike",
tolerance: float = 0.01,
-) -> cq.Wire:
- """Emboss a planar Wire to targetObject maintaining the length while doing so"""
+) -> "Wire":
+ """Emboss Wire on target object
+
+ Emboss an Wire on the XY plane onto a Shape while maintaining
+ original wire dimensions where possible.
+ .. image:: embossWire.png
+
+ The embossed wire can be used to build features as:
+
+ .. image:: embossFeature.png
+
+ with the `sweep() `_ method.
+
+ Args:
+ targetObject: Object to emboss onto
+ surfacePoint: Point on target object to start embossing
+ surfaceXDirection: Direction of X-Axis on target object
+ tolerance: maximum allowed error in embossed wire length. Defaults to 0.01.
+
+ Raises:
+ RuntimeError: Embosses wire is invalid
+
+ Returns:
+ Embossed wire
+ """
planar_edges = self.Edges()
planar_closed = self.IsClosed()
logging.debug(f"embossing wire with {len(planar_edges)} edges")
@@ -920,17 +1568,17 @@ def _embossWireToShape(
first_start_point = None
last_end_point = None
edge_separatons = []
- surface_point = cq.Vector(surfacePoint)
- surface_x_direction = cq.Vector(surfaceXDirection)
+ surface_point = Vector(surfacePoint)
+ surface_x_direction = Vector(surfaceXDirection)
# If the wire doesn't start at the origin, create an embossed construction line to get
# to the beginning of the first edge
- if planar_edges[0].positionAt(0) == cq.Vector(0, 0, 0):
+ if planar_edges[0].positionAt(0) == Vector(0, 0, 0):
edge_surface_point = surface_point
- planar_edge_end_point = cq.Vector(0, 0, 0)
+ planar_edge_end_point = Vector(0, 0, 0)
else:
- construction_line = cq.Edge.makeLine(
- cq.Vector(0, 0, 0), planar_edges[0].positionAt(0)
+ construction_line = Edge.makeLine(
+ Vector(0, 0, 0), planar_edges[0].positionAt(0)
)
embossed_construction_line = construction_line.embossToShape(
targetObject, surface_point, surface_x_direction, tolerance
@@ -962,7 +1610,7 @@ def _embossWireToShape(
logging.debug(f"embossed wire closure gap {closure_gap:0.3f}")
if planar_closed and closure_gap > tolerance:
logging.debug(f"closing gap in embossed wire of size {closure_gap}")
- gap_edge = cq.Edge.makeSpline(
+ gap_edge = Edge.makeSpline(
[last_end_point, first_start_point],
tangents=[embossed_edge.tangentAt(1), first_edge.tangentAt(0)],
)
@@ -976,7 +1624,7 @@ def _embossWireToShape(
)
# Note: wires_out is an OCP.TopTools.TopTools_HSequenceOfShape not a simple list
embossed_wires = [w for w in wires_out]
- embossed_wire = cq.Wire(embossed_wires[0])
+ embossed_wire = Wire(embossed_wires[0])
if planar_closed and not embossed_wire.IsClosed():
embossed_wire.close()
@@ -992,48 +1640,92 @@ def _embossWireToShape(
return embossed_wire
-cq.Wire.embossToShape = _embossWireToShape
+Wire.embossToShape = _embossWireToShape
"""
Edge extensions: projectToShape(), embossToShape()
"""
-cq.Edge.projectToShape = _projectWireToShape
+
+
+def _projectEdgeToShape(
+ self,
+ targetObject: "Shape",
+ direction: "VectorLike" = None,
+ center: "VectorLike" = None,
+) -> list["Edge"]:
+ """Project Edge
+
+ Project an Edge onto a Shape generating new Wires on the surfaces of the object
+ one and only one of `direction` or `center` must be provided. Note that one or
+ more wires may be generated depending on the topology of the target object and
+ location/direction of projection.
+
+ To avoid flipping the normal of a face built with the projected wire the orientation
+ of the output wires are forced to be the same as self.
+
+ Args:
+ targetObject: Object to project onto
+ direction: Parallel projection direction. Defaults to None.
+ center: Conical center of projection. Defaults to None.
+
+ Raises:
+ ValueError: Only one of direction or center must be provided
+
+ Returns:
+ Projected Edge(s)
+ """
+ wire = cq.Wire.assembleEdges([self])
+ projected_wires = wire.projectToShape(targetObject, direction, center)
+ projected_edges = [w.Edges()[0] for w in projected_wires]
+ return projected_edges
+
+
+Edge.projectToShape = _projectEdgeToShape
def _embossEdgeToShape(
- self: cq.Edge,
- targetObject: cq.Shape,
- surfacePoint: VectorLike,
- surfaceXDirection: VectorLike,
+ self,
+ targetObject: "Shape",
+ surfacePoint: "VectorLike",
+ surfaceXDirection: "VectorLike",
tolerance: float = 0.01,
-) -> cq.Edge:
- """
- Emboss a planar Edge to targetObject while maintaining edge length
+) -> "Edge":
+ """Emboss Edge on target object
- Algorithm - piecewise approximation of points on surface -> generate spline:
+ Emboss an Edge on the XY plane onto a Shape while maintaining
+ original edge dimensions where possible.
- - successively increasing the number of points to emboss
- - create local plane at current point given surface normal and surface x direction
- - create new approximate point on local plane from next planar point
- - get global position of next approximate point
- - using current normal and next approximate point find next surface intersection point and normal
- - create spline from points
- - measure length of spline
- - repeat with more points unless within target tolerance
+ Args:
+ targetObject: Object to emboss onto
+ surfacePoint: Point on target object to start embossing
+ surfaceXDirection: Direction of X-Axis on target object
+ tolerance: maximum allowed error in embossed edge length
+ Returns:
+ Embossed edge
"""
+ # Algorithm - piecewise approximation of points on surface -> generate spline:
+ # - successively increasing the number of points to emboss
+ # - create local plane at current point given surface normal and surface x direction
+ # - create new approximate point on local plane from next planar point
+ # - get global position of next approximate point
+ # - using current normal and next approximate point find next surface intersection point and normal
+ # - create spline from points
+ # - measure length of spline
+ # - repeat with more points unless within target tolerance
+
def find_point_on_surface(
- current_surface_point: cq.Vector,
- current_surface_normal: cq.Vector,
- planar_relative_position: cq.Vector,
- ) -> cq.Vector:
+ current_surface_point: Vector,
+ current_surface_normal: Vector,
+ planar_relative_position: Vector,
+ ) -> Vector:
"""
Given a 2D relative position from a surface point, find the closest point on the surface.
"""
- segment_plane = cq.Plane(
+ segment_plane = Plane(
origin=current_surface_point,
xDir=surface_x_direction,
normal=current_surface_normal,
@@ -1044,7 +1736,7 @@ def find_point_on_surface(
)[0]
return (next_surface_point, next_surface_normal)
- surface_x_direction = cq.Vector(surfaceXDirection)
+ surface_x_direction = Vector(surfaceXDirection)
planar_edge_length = self.Length()
planar_edge_closed = self.IsClosed()
@@ -1056,7 +1748,7 @@ def find_point_on_surface(
while length_error > tolerance and loop_count < 8:
# Initialize the algorithm by priming it with the start of Edge self
- surface_origin = cq.Vector(surfacePoint)
+ surface_origin = Vector(surfacePoint)
(surface_origin_point, surface_origin_normal) = targetObject.findIntersection(
point=surface_origin,
direction=surface_origin - target_object_center,
@@ -1082,7 +1774,7 @@ def find_point_on_surface(
embossed_edge_points.append(current_surface_point)
# Create a spline through the points and determine length difference from target
- embossed_edge = cq.Edge.makeSpline(
+ embossed_edge = Edge.makeSpline(
embossed_edge_points, periodic=planar_edge_closed
)
length_error = planar_edge_length - embossed_edge.Length()
@@ -1099,7 +1791,7 @@ def find_point_on_surface(
return embossed_edge
-cq.Edge.embossToShape = _embossEdgeToShape
+Edge.embossToShape = _embossEdgeToShape
"""
@@ -1109,10 +1801,19 @@ def find_point_on_surface(
def _findIntersection(
- self: cq.Shape, point: cq.Vector, direction: cq.Vector
-) -> list[tuple[cq.Vector, cq.Vector]]:
- """Return both the point(s) and normal(s) of the intersection of the line and the shape"""
+ self, point: "Vector", direction: "Vector"
+) -> list[tuple["Vector", "Vector"]]:
+ """Find point and normal at intersection
+
+ Return both the point(s) and normal(s) of the intersection of the line and the shape
+ Args:
+ point: point on intersecting line
+ direction: direction of intersecting line
+
+ Returns:
+ Point and normal of intersection
+ """
oc_point = gp_Pnt(*point.toTuple())
oc_axis = gp_Dir(*direction.toTuple())
oc_shape = self.wrapped
@@ -1125,9 +1826,7 @@ def _findIntersection(
while intersectMaker.More():
interPt = intersectMaker.Pnt()
distance = oc_point.Distance(interPt)
- intersections.append(
- (cq.Face(intersectMaker.Face()), cq.Vector(interPt), distance)
- )
+ intersections.append((Face(intersectMaker.Face()), Vector(interPt), distance))
intersectMaker.Next()
intersections.sort(key=lambda x: x[2])
@@ -1144,7 +1843,7 @@ def _findIntersection(
return result
-cq.Shape.findIntersection = _findIntersection
+Shape.findIntersection = _findIntersection
def _projectText(
@@ -1152,21 +1851,46 @@ def _projectText(
txt: str,
fontsize: float,
depth: float,
- path: Union[cq.Wire, cq.Edge],
+ path: Union["Wire", "Edge"],
font: str = "Arial",
fontPath: Optional[str] = None,
kind: Literal["regular", "bold", "italic"] = "regular",
valign: Literal["center", "top", "bottom"] = "center",
start: float = 0,
-) -> cq.Compound:
- """Create 3D text with a baseline following the given path on Shape"""
+) -> "Compound":
+ """Projected 3D text following the given path on Shape
+
+ Create 3D text using projection by positioning each face of
+ the planar text normal to the shape along the path and projecting
+ onto the surface. If depth is not zero, the resulting face is
+ thickened to the provided depth.
+
+ Note that projection may result in text distortion depending on
+ the shape at a position along the path.
+
+ .. image:: projectText.png
+
+ Args:
+ txt: Text to be rendered
+ fontsize: Size of the font in model units
+ depth: Thickness of text, 0 returns a Face object
+ path: Path on the Shape to follow
+ font: Font name. Defaults to "Arial".
+ fontPath: Path to font file. Defaults to None.
+ kind: Font type - one of "regular", "bold", "italic". Defaults to "regular".
+ valign: Vertical Alignment - one of "center", "top", "bottom". Defaults to "center".
+ start: Relative location on path to start the text. Defaults to 0.
+
+ Returns:
+ The projected text
+ """
path_length = path.Length()
shape_center = self.Center()
# Create text faces
text_faces = (
- cq.Workplane("XY")
+ Workplane("XY")
.text(
txt,
fontsize,
@@ -1194,7 +1918,7 @@ def _projectText(
path_position,
path_position - shape_center,
)[0]
- surface_normal_plane = cq.Plane(
+ surface_normal_plane = Plane(
origin=surface_point, xDir=path_tangent, normal=surface_normal
)
projection_face = text_face.translate((-face_center_x, 0, 0)).transformShape(
@@ -1215,10 +1939,10 @@ def _projectText(
logging.debug(f"finished projecting text sting '{txt}'")
- return cq.Compound.makeCompound(projected_text)
+ return Compound.makeCompound(projected_text)
-cq.Shape.projectText = _projectText
+Shape.projectText = _projectText
def _embossText(
@@ -1226,21 +1950,42 @@ def _embossText(
txt: str,
fontsize: float,
depth: float,
- path: Union[cq.Wire, cq.Edge],
+ path: Union["Wire", "Edge"],
font: str = "Arial",
fontPath: Optional[str] = None,
kind: Literal["regular", "bold", "italic"] = "regular",
valign: Literal["center", "top", "bottom"] = "center",
start: float = 0,
-) -> cq.Compound:
- """Create 3D text with a baseline following the given path on Shape"""
+) -> "Compound":
+ """Embossed 3D text following the given path on Shape
+
+ Create 3D text by embossing each face of the planar text onto
+ the shape along the path. If depth is not zero, the resulting
+ face is thickened to the provided depth.
+
+ .. image:: embossText.png
+
+ Args:
+ txt: Text to be rendered
+ fontsize: Size of the font in model units
+ depth: Thickness of text, 0 returns a Face object
+ path: Path on the Shape to follow
+ font: Font name. Defaults to "Arial".
+ fontPath: Path to font file. Defaults to None.
+ kind: Font type - one of "regular", "bold", "italic". Defaults to "regular".
+ valign: Vertical Alignment - one of "center", "top", "bottom". Defaults to "center".
+ start: Relative location on path to start the text. Defaults to 0.
+
+ Returns:
+ The embossed text
+ """
path_length = path.Length()
shape_center = self.Center()
# Create text faces
text_faces = (
- cq.Workplane("XY")
+ Workplane("XY")
.text(
txt,
fontsize,
@@ -1282,7 +2027,28 @@ def _embossText(
logging.debug(f"finished embossing text sting '{txt}'")
- return cq.Compound.makeCompound(embossed_text)
+ return Compound.makeCompound(embossed_text)
+
+
+Shape.embossText = _embossText
+
+"""
+
+Location extensions: __str__()
+
+"""
+
+
+def _location_str(self):
+ """To String
+
+ Convert Location to String for display
+
+ Returns:
+ Location as String
+ """
+ loc_tuple = self.toTuple()
+ return f"({str(loc_tuple[0])}, {str(loc_tuple[1])})"
-cq.Shape.embossText = _embossText
+Location.__str__ = _location_str
diff --git a/src/cq_warehouse/extensions_doc.py b/src/cq_warehouse/extensions_doc.py
new file mode 100644
index 0000000..e6de391
--- /dev/null
+++ b/src/cq_warehouse/extensions_doc.py
@@ -0,0 +1,797 @@
+from typing import Union, Tuple, Optional, Literal
+from fastener import Screw, Nut, Washer
+class gp_Ax1:
+ pass
+class T:
+ pass
+class VectorLike:
+ pass
+class BoundBox:
+ pass
+class Solid:
+ pass
+class Compound:
+ pass
+class Location:
+ pass
+class Assembly(object):
+ def translate(self, vec: "VectorLike") -> "Assembly":
+ """
+ Moves the current assembly (without making a copy) by the specified translation vector
+
+ Args:
+ vec: The translation vector
+
+ Returns:
+ The translated Assembly
+
+ Example:
+ car_assembly.translate((1,2,3))
+ """
+ def rotate(self, axis: "VectorLike", angle: float) -> "Assembly":
+ """Rotate Assembly
+
+ Rotates the current assembly (without making a copy) around the axis of rotation
+ by the specified angle
+
+ Args:
+ axis: The axis of rotation (starting at the origin)
+ angle: The rotation angle, in degrees
+
+ Returns:
+ The rotated Assembly
+
+ Example:
+ car_assembly.rotate((0,0,1),90)
+ """
+ def fastenerQuantities(self, bom: bool = True, deep: bool = True) -> dict:
+ """Fastener Quantities
+
+ Generate a bill of materials of the fasteners in an assembly augmented by the hole methods
+ bom: returns fastener.info if True else counts fastener instances
+
+ Args:
+ bom (bool, optional): Select a Bill of Materials or raw fastener instance count. Defaults to True.
+ deep (bool, optional): Scan the entire Assembly. Defaults to True.
+
+ Returns:
+ fastener usage summary
+ """
+ def fastenerLocations(self, fastener: Union["Nut", "Screw"]) -> list[Location]:
+ """Return location(s) of fastener
+
+ Generate a list of cadquery Locations for the given fastener relative to the Assembly
+
+ Args:
+ fastener: fastener to search for
+
+ Returns:
+ a list of cadquery Location objects for each fastener instance
+ """
+class Plane(object):
+ def toLocalCoords(self, obj: Union["Vector", "Shape", "BoundBox"]):
+ """Project the provided coordinates onto this plane
+
+ Args:
+ obj: an object, vector, or bounding box to convert
+
+ Returns:
+ an object of the same type, but converted to local coordinates
+
+ Most of the time, the z-coordinate returned will be zero, because most
+ operations based on a plane are all 2D. Occasionally, though, 3D
+ points outside of the current plane are transformed. One such example is
+ :py:meth:`Workplane.box`, where 3D corners of a box are transformed to
+ orient the box in space correctly.
+ """
+class Vector(object):
+ def rotateX(self, angle: float) -> "Vector":
+ """Rotate Vector about X-Axis
+
+ Args:
+ angle: Angle in degrees
+
+ Returns:
+ Rotated Vector
+ """
+ def rotateY(self, angle: float) -> "Vector":
+ """Rotate Vector about Y-Axis
+
+ Args:
+ angle: Angle in degrees
+
+ Returns:
+ Rotated Vector
+ """
+ def rotateZ(self, angle: float) -> "Vector":
+ """Rotate Vector about Z-Axis
+
+ Args:
+ angle: Angle in degrees
+
+ Returns:
+ Rotated Vector
+ """
+ def toVertex(self) -> "Vertex":
+ """Convert to Vector to Vertex
+
+ Returns:
+ Vertex equivalent of Vector
+ """
+ def getSignedAngle(self, v: "Vector", normal: "Vector" = None) -> float:
+ """Signed Angle Between Vectors
+
+ Return the signed angle in RADIANS between two vectors with the given normal
+ based on this math: angle = atan2((Va × Vb) ⋅ Vn, Va ⋅ Vb)
+
+ Args:
+ v: Second Vector.
+
+ normal: Vector's Normal. Defaults to -Z Axis.
+
+ Returns:
+ Angle between vectors
+ """
+class Vertex(object):
+ def __add__(
+ self, other: Union["Vertex", "Vector", Tuple[float, float, float]]
+ ) -> "Vertex":
+ """Add
+
+ Add to a Vertex with a Vertex, Vector or Tuple
+
+ Args:
+ other: Value to add
+
+ Raises:
+ TypeError: other not in [Tuple,Vector,Vertex]
+
+ Returns:
+ Result
+
+ Example:
+ part.faces(">Z").vertices(" "Vertex":
+ """Subtract
+
+ Substract a Vertex with a Vertex, Vector or Tuple from self
+
+ Args:
+ other: Value to add
+
+ Raises:
+ TypeError: other not in [Tuple,Vector,Vertex]
+
+ Returns:
+ Result
+
+ Example:
+ part.faces(">Z").vertices(" str:
+ """To String
+
+ Convert Vertex to String for display
+
+ Returns:
+ Vertex as String
+ """
+ def toVector(self) -> "Vector":
+ """To Vector
+
+ Convert a Vertex to Vector
+
+ Returns:
+ Vector representation of Vertex
+ """
+class Workplane(object):
+ def textOnPath(
+ self: T,
+ txt: str,
+ fontsize: float,
+ distance: float,
+ start: float = 0.0,
+ cut: bool = True,
+ combine: bool = False,
+ clean: bool = True,
+ font: str = "Arial",
+ fontPath: Optional[str] = None,
+ kind: Literal["regular", "bold", "italic"] = "regular",
+ valign: Literal["center", "top", "bottom"] = "center",
+ ) -> T:
+ """
+ Returns 3D text with the baseline following the given path.
+
+ The parameters are largely the same as the
+ `Workplane.text() `_
+ method. The **start** parameter (normally between 0.0 and 1.0) specify where on the path to
+ start the text.
+
+ The path that the text follows is defined by the last Edge or Wire in the
+ Workplane stack. Path's defined outside of the Workplane can be used with the
+ `add() `_
+ method.
+
+ .. image:: textOnPath.png
+
+ Args:
+ txt: text to be rendered
+ fontsize: size of the font in model units
+ distance: the distance to extrude or cut, normal to the workplane plane, negative means opposite the normal direction
+ start: the relative location on path to start the text, values must be between 0.0 and 1.0
+ cut: True to cut the resulting solid from the parent solids if found
+ combine: True to combine the resulting solid with parent solids if found
+ clean: call :py:meth:`clean` afterwards to have a clean shape
+ font: font name
+ fontPath: path to font file
+ kind: font type
+
+ Returns:
+ a CQ object with the resulting solid selected
+
+ The returned object is always a Workplane object, and depends on whether combine is True, and
+ whether a context solid is already defined:
+
+ * if combine is False, the new value is pushed onto the stack.
+ * if combine is true, the value is combined with the context solid if it exists,
+ and the resulting solid becomes the new context solid.
+
+ Examples::
+
+ fox = (
+ Workplane("XZ")
+ .threePointArc((50, 30), (100, 0))
+ .textOnPath(
+ txt="The quick brown fox jumped over the lazy dog",
+ fontsize=5,
+ distance=1,
+ start=0.1,
+ )
+ )
+
+ clover = (
+ Workplane("front")
+ .moveTo(0, 10)
+ .radiusArc((10, 0), 7.5)
+ .radiusArc((0, -10), 7.5)
+ .radiusArc((-10, 0), 7.5)
+ .radiusArc((0, 10), 7.5)
+ .consolidateWires()
+ .textOnPath(
+ txt=".x" * 102,
+ fontsize=1,
+ distance=1,
+ )
+ )
+ """
+ def hexArray(
+ self,
+ diagonal: float,
+ xCount: int,
+ yCount: int,
+ center: Union[bool, tuple[bool, bool]] = True,
+ ):
+ """Create Hex Array
+
+ Creates a hexagon array of points and pushes them onto the stack.
+ If you want to position the array at another point, create another workplane
+ that is shifted to the position you would like to use as a reference
+
+ Args:
+ diagonal: tip to tip size of hexagon ( must be > 0)
+ xCount: number of points ( > 0 )
+ yCount: number of points ( > 0 )
+ center: If True, the array will be centered around the workplane center.
+ If False, the lower corner will be on the reference point and the array will
+ extend in the positive x and y directions. Can also use a 2-tuple to specify
+ centering along each axis.
+
+ Returns:
+ Places points on the Workplane stack
+ """
+ def thicken(self, depth: float, direction: "Vector" = None):
+ """Thicken Face
+
+ Find all of the faces on the stack and make them Solid objects by thickening
+ along the normals.
+
+ Args:
+ depth: Amount to thicken face(s), can be positive or negative.
+ direction: The direction vector can be used to
+ indicate which way is 'up', potentially flipping the face normal direction
+ such that many faces with different normals all go in the same direction
+ (direction need only be +/- 90 degrees from the face normal). Defaults to None.
+
+ Returns:
+ A set of new objects on the Workplane stack
+ """
+ def fastenerHole(
+ self: T,
+ hole_diameters: dict,
+ fastener: Union["Nut", "Screw"],
+ depth: float,
+ washers: list["Washer"],
+ fit: Optional[Literal["Close", "Normal", "Loose"]] = None,
+ material: Optional[Literal["Soft", "Hard"]] = None,
+ counterSunk: Optional[bool] = True,
+ baseAssembly: Optional["Assembly"] = None,
+ hand: Optional[Literal["right", "left"]] = None,
+ simple: Optional[bool] = False,
+ clean: Optional[bool] = True,
+ ) -> T:
+ """Fastener Specific Hole
+
+ Makes a counterbore clearance, tap or threaded hole for the given screw for each item
+ on the stack. The surface of the hole is at the current workplane.
+
+ Args:
+ hole_diameters: either clearance or tap hole diameter specifications
+ fastener: A nut or screw instance
+ depth: hole depth
+ washers: A list of washer instances, can be empty
+ fit: one of "Close", "Normal", "Loose" which determines clearance hole diameter. Defaults to None.
+ material: on of "Soft", "Hard" which determines tap hole size. Defaults to None.
+ counterSunk: Is the fastener countersunk into the part?. Defaults to True.
+ baseAssembly: Assembly to add faster to. Defaults to None.
+ hand: tap hole twist direction either "right" or "left". Defaults to None.
+ simple: tap hole thread complexity selector. Defaults to False.
+ clean: execute a clean operation remove extraneous internal features. Defaults to True.
+
+ Raises:
+ ValueError: fit or material not in hole_diameters dictionary
+
+ Returns:
+ the shape on the workplane stack with a new hole
+ """
+ def clearanceHole(
+ self: T,
+ fastener: Union["Nut", "Screw"],
+ washers: Optional[list["Washer"]] = None,
+ fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
+ depth: Optional[float] = None,
+ counterSunk: Optional[bool] = True,
+ baseAssembly: Optional["Assembly"] = None,
+ clean: Optional[bool] = True,
+ ) -> T:
+ """Clearance Hole
+
+ Put a clearance hole in a shape at the provided location
+
+ For more information on how to use clearanceHole() see
+ :ref:`Clearance, Tap and Threaded Holes `.
+
+ Args:
+ fastener: A nut or screw instance
+ washers: A list of washer instances, can be empty
+ fit: one of "Close", "Normal", "Loose" which determines clearance hole diameter. Defaults to None.
+ depth: hole depth. Defaults to through part.
+ counterSunk: Is the fastener countersunk into the part?. Defaults to True.
+ baseAssembly: Assembly to add faster to. Defaults to None.
+ clean: execute a clean operation remove extraneous internal features. Defaults to True.
+
+ Returns:
+ the shape on the workplane stack with a new clearance hole
+ """
+ def tapHole(
+ self: T,
+ fastener: Union["Nut", "Screw"],
+ washers: Optional[list["Washer"]] = None,
+ material: Optional[Literal["Soft", "Hard"]] = "Soft",
+ depth: Optional[float] = None,
+ counterSunk: Optional[bool] = True,
+ fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
+ baseAssembly: Optional["Assembly"] = None,
+ clean: Optional[bool] = True,
+ ) -> T:
+ """Tap Hole
+
+ Put a tap hole in a shape at the provided location
+
+ For more information on how to use tapHole() see
+ :ref:`Clearance, Tap and Threaded Holes `.
+
+ Args:
+ fastener: A nut or screw instance
+ washers: A list of washer instances, can be empty
+ material: on of "Soft", "Hard" which determines tap hole size. Defaults to None.
+ depth: hole depth. Defaults to through part.
+ counterSunk: Is the fastener countersunk into the part?. Defaults to True.
+ fit: one of "Close", "Normal", "Loose" which determines clearance hole diameter. Defaults to None.
+ baseAssembly: Assembly to add faster to. Defaults to None.
+ clean: execute a clean operation remove extraneous internal features. Defaults to True.
+
+ Returns:
+ the shape on the workplane stack with a new tap hole
+ """
+ def threadedHole(
+ self: T,
+ fastener: "Screw",
+ depth: float,
+ washers: Optional[list["Washer"]] = None,
+ hand: Literal["right", "left"] = "right",
+ simple: Optional[bool] = False,
+ counterSunk: Optional[bool] = True,
+ fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
+ baseAssembly: Optional["Assembly"] = None,
+ clean: Optional[bool] = True,
+ ) -> T:
+ """Threaded Hole
+
+ Put a threaded hole in a shape at the provided location
+
+ For more information on how to use threadedHole() see
+ :ref:`Clearance, Tap and Threaded Holes `.
+
+ Args:
+ fastener: A nut or screw instance
+ depth: hole depth. Defaults to through part.
+ washers: A list of washer instances, can be empty
+ hand: tap hole twist direction either "right" or "left". Defaults to None.
+ simple (Optional[bool], optional): [description]. Defaults to False.
+ counterSunk: Is the fastener countersunk into the part?. Defaults to True.
+ fit: one of "Close", "Normal", "Loose" which determines clearance hole diameter. Defaults to None.
+ baseAssembly: Assembly to add faster to. Defaults to None.
+ clean: execute a clean operation remove extraneous internal features. Defaults to True.
+
+ Returns:
+ the shape on the workplane stack with a new threaded hole
+ """
+ def pushFastenerLocations(
+ self: T,
+ fastener: Union["Nut", "Screw"],
+ baseAssembly: "Assembly",
+ ):
+ """Push Fastener Locations
+
+ Push the Location(s) of the given fastener relative to the given Assembly onto the workplane stack.
+
+ Returns:
+ Location objects on the workplane stack
+ """
+class Face(object):
+ def thicken(self, depth: float, direction: "Vector" = None) -> "Solid":
+ """Thicken Face
+
+ Create a solid from a potentially non planar face by thickening along the normals.
+
+ .. image:: thickenFace.png
+
+ Non-planar faces are thickened both towards and away from the center of the sphere.
+
+ Args:
+ depth: Amount to thicken face(s), can be positive or negative.
+ direction: The direction vector can be used to
+ indicate which way is 'up', potentially flipping the face normal direction
+ such that many faces with different normals all go in the same direction
+ (direction need only be +/- 90 degrees from the face normal). Defaults to None.
+
+ Raises:
+ RuntimeError: Opencascade internal failures
+
+ Returns:
+ The resulting Solid object
+ """
+ def projectToShape(
+ self,
+ targetObject: "Shape",
+ direction: "VectorLike" = None,
+ center: "VectorLike" = None,
+ internalFacePoints: list["Vector"] = [],
+ ) -> list["Face"]:
+ """Project Face to target Object
+
+ Project a Face onto a Shape generating new Face(s) on the surfaces of the object
+ one and only one of `direction` or `center` must be provided.
+
+ The two types of projections are illustrated below:
+
+ .. image:: flatProjection.png
+ :alt: flatProjection
+
+ .. image:: conicalProjection.png
+ :alt: conicalProjection
+
+ Note that an array of Faces is returned as the projection might result in faces
+ on the "front" and "back" of the object (or even more if there are intermediate
+ surfaces in the projection path). Faces "behind" the projection are not
+ returned.
+
+ To help refine the resulting face, a list of planar points can be passed to
+ augment the surface definition. For example, when projecting a circle onto a
+ sphere, a circle will result which will get converted to a planar circle face.
+ If no points are provided, a single center point will be generated and used for
+ this purpose.
+
+ Args:
+ targetObject: Object to project onto
+ direction: Parallel projection direction. Defaults to None.
+ center: Conical center of projection. Defaults to None.
+ internalFacePoints: Points refining shape. Defaults to [].
+
+ Raises:
+ ValueError: Only one of direction or center must be provided
+
+ Returns:
+ Face(s) projected on target object
+ """
+ def embossToShape(
+ self,
+ targetObject: "Shape",
+ surfacePoint: "VectorLike",
+ surfaceXDirection: "VectorLike",
+ internalFacePoints: list["Vector"] = [],
+ ) -> "Face":
+ """Emboss Face on target object
+
+ Emboss a Face on the XY plane onto a Shape while maintaining
+ original face dimensions where possible.
+
+ Unlike projection, a single Face is returned. The internalFacePoints
+ parameter works as with projection.
+
+ Args:
+ targetObject: Object to emboss onto
+ surfacePoint: Point on target object to start embossing
+ surfaceXDirection: Direction of X-Axis on target object
+ internalFacePoints: Surface refinement points. Defaults to [].
+
+ Returns:
+ Face: Embossed face
+ """
+class Wire(object):
+ def makeNonPlanarFace(
+ self,
+ surfacePoints: list["Vector"] = None,
+ interiorWires: list["Wire"] = None,
+ ) -> "Face":
+ """Create Non-Planar Face with perimeter Wire
+
+ Create a potentially non-planar face bounded by exterior Wire,
+ optionally refined by surfacePoints with optional holes defined by
+ interiorWires.
+
+ The **surfacePoints** parameter can be used to refine the resulting Face. If no
+ points are provided a single central point will be used to help avoid the
+ creation of a planar face.
+
+ Args:
+ surfacePoints: Points on the surface that refine the shape. Defaults to None.
+ interiorWires: Hole(s) in the face. Defaults to None.
+
+ Raises:
+ RuntimeError: Opencascade core exceptions building face
+
+ Returns:
+ Non planar face
+ """
+ def projectToShape(
+ self,
+ targetObject: "Shape",
+ direction: "VectorLike" = None,
+ center: "VectorLike" = None,
+ ) -> list["Wire"]:
+ """Project Wire
+
+ Project a Wire onto a Shape generating new Wires on the surfaces of the object
+ one and only one of `direction` or `center` must be provided. Note that one or
+ more wires may be generated depending on the topology of the target object and
+ location/direction of projection.
+
+ To avoid flipping the normal of a face built with the projected wire the orientation
+ of the output wires are forced to be the same as self.
+
+ Args:
+ targetObject: Object to project onto
+ direction: Parallel projection direction. Defaults to None.
+ center: Conical center of projection. Defaults to None.
+
+ Raises:
+ ValueError: Only one of direction or center must be provided
+
+ Returns:
+ Projected wire(s)
+ """
+ def embossToShape(
+ self,
+ targetObject: "Shape",
+ surfacePoint: "VectorLike",
+ surfaceXDirection: "VectorLike",
+ tolerance: float = 0.01,
+ ) -> "Wire":
+ """Emboss Wire on target object
+
+ Emboss an Wire on the XY plane onto a Shape while maintaining
+ original wire dimensions where possible.
+
+ .. image:: embossWire.png
+
+ The embossed wire can be used to build features as:
+
+ .. image:: embossFeature.png
+
+ with the `sweep() `_ method.
+
+ Args:
+ targetObject: Object to emboss onto
+ surfacePoint: Point on target object to start embossing
+ surfaceXDirection: Direction of X-Axis on target object
+ tolerance: maximum allowed error in embossed wire length. Defaults to 0.01.
+
+ Raises:
+ RuntimeError: Embosses wire is invalid
+
+ Returns:
+ Embossed wire
+ """
+class Edge(object):
+ def projectToShape(
+ self,
+ targetObject: "Shape",
+ direction: "VectorLike" = None,
+ center: "VectorLike" = None,
+ ) -> list["Edge"]:
+ """Project Edge
+
+ Project an Edge onto a Shape generating new Wires on the surfaces of the object
+ one and only one of `direction` or `center` must be provided. Note that one or
+ more wires may be generated depending on the topology of the target object and
+ location/direction of projection.
+
+ To avoid flipping the normal of a face built with the projected wire the orientation
+ of the output wires are forced to be the same as self.
+
+ Args:
+ targetObject: Object to project onto
+ direction: Parallel projection direction. Defaults to None.
+ center: Conical center of projection. Defaults to None.
+
+ Raises:
+ ValueError: Only one of direction or center must be provided
+
+ Returns:
+ Projected Edge(s)
+ """
+ def embossToShape(
+ self,
+ targetObject: "Shape",
+ surfacePoint: "VectorLike",
+ surfaceXDirection: "VectorLike",
+ tolerance: float = 0.01,
+ ) -> "Edge":
+ """Emboss Edge on target object
+
+ Emboss an Edge on the XY plane onto a Shape while maintaining
+ original edge dimensions where possible.
+
+ Args:
+ targetObject: Object to emboss onto
+ surfacePoint: Point on target object to start embossing
+ surfaceXDirection: Direction of X-Axis on target object
+ tolerance: maximum allowed error in embossed edge length
+
+ Returns:
+ Embossed edge
+ """
+class Shape(object):
+ def findIntersection(
+ self, point: "Vector", direction: "Vector"
+ ) -> list[tuple["Vector", "Vector"]]:
+ """Find point and normal at intersection
+
+ Return both the point(s) and normal(s) of the intersection of the line and the shape
+
+ Args:
+ point: point on intersecting line
+ direction: direction of intersecting line
+
+ Returns:
+ Point and normal of intersection
+ """
+ def projectText(
+ self,
+ txt: str,
+ fontsize: float,
+ depth: float,
+ path: Union["Wire", "Edge"],
+ font: str = "Arial",
+ fontPath: Optional[str] = None,
+ kind: Literal["regular", "bold", "italic"] = "regular",
+ valign: Literal["center", "top", "bottom"] = "center",
+ start: float = 0,
+ ) -> "Compound":
+ """Projected 3D text following the given path on Shape
+
+ Create 3D text using projection by positioning each face of
+ the planar text normal to the shape along the path and projecting
+ onto the surface. If depth is not zero, the resulting face is
+ thickened to the provided depth.
+
+ Note that projection may result in text distortion depending on
+ the shape at a position along the path.
+
+ .. image:: projectText.png
+
+ Args:
+ txt: Text to be rendered
+ fontsize: Size of the font in model units
+ depth: Thickness of text, 0 returns a Face object
+ path: Path on the Shape to follow
+ font: Font name. Defaults to "Arial".
+ fontPath: Path to font file. Defaults to None.
+ kind: Font type - one of "regular", "bold", "italic". Defaults to "regular".
+ valign: Vertical Alignment - one of "center", "top", "bottom". Defaults to "center".
+ start: Relative location on path to start the text. Defaults to 0.
+
+ Returns:
+ The projected text
+ """
+ def embossText(
+ self,
+ txt: str,
+ fontsize: float,
+ depth: float,
+ path: Union["Wire", "Edge"],
+ font: str = "Arial",
+ fontPath: Optional[str] = None,
+ kind: Literal["regular", "bold", "italic"] = "regular",
+ valign: Literal["center", "top", "bottom"] = "center",
+ start: float = 0,
+ ) -> "Compound":
+ """Embossed 3D text following the given path on Shape
+
+ Create 3D text by embossing each face of the planar text onto
+ the shape along the path. If depth is not zero, the resulting
+ face is thickened to the provided depth.
+
+ .. image:: embossText.png
+
+ Args:
+ txt: Text to be rendered
+ fontsize: Size of the font in model units
+ depth: Thickness of text, 0 returns a Face object
+ path: Path on the Shape to follow
+ font: Font name. Defaults to "Arial".
+ fontPath: Path to font file. Defaults to None.
+ kind: Font type - one of "regular", "bold", "italic". Defaults to "regular".
+ valign: Vertical Alignment - one of "center", "top", "bottom". Defaults to "center".
+ start: Relative location on path to start the text. Defaults to 0.
+
+ Returns:
+ The embossed text
+ """
+class Location(object):
+ def __str__(self):
+ """To String
+
+ Convert Location to String for display
+
+ Returns:
+ Location as String
+ """
+def makeNonPlanarFace(
+ exterior: Union["Wire", list["Edge"]],
+ surfacePoints: list["VectorLike"] = None,
+ interiorWires: list["Wire"] = None,
+) -> "Face":
+ """Create Non-Planar Face
+
+ Create a potentially non-planar face bounded by exterior (wire or edges),
+ optionally refined by surfacePoints with optional holes defined by
+ interiorWires.
+
+ Args:
+ exterior: Perimeter of face
+ surfacePoints: Points on the surface that refine the shape. Defaults to None.
+ interiorWires: Hole(s) in the face. Defaults to None.
+
+ Raises:
+ RuntimeError: Opencascade core exceptions building face
+
+ Returns:
+ Non planar face
+ """
diff --git a/src/cq_warehouse/fastener.py b/src/cq_warehouse/fastener.py
index a1d8685..d28e662 100644
--- a/src/cq_warehouse/fastener.py
+++ b/src/cq_warehouse/fastener.py
@@ -30,9 +30,8 @@
limitations under the License.
"""
-from functools import reduce
from abc import ABC, abstractmethod
-from typing import Literal, Tuple, Optional, List, TypeVar, Union, cast
+from typing import Literal, Tuple, Optional, List
from math import sin, cos, tan, radians, pi, degrees, sqrt
import csv
import importlib.resources as pkg_resources
@@ -48,10 +47,6 @@
# ISO standards use single variable dimension labels which are used extensively
# pylint: disable=invalid-name
-# lambdas are only used in Workplane methods which cycle over multiple locations
-# and are required
-# pylint: disable=unnecessary-lambda
-
def polygon_diagonal(width: float, num_sides: Optional[int] = 6) -> float:
"""Distance across polygon diagonals given width across flats"""
@@ -203,6 +198,17 @@ def lookup_nominal_screw_lengths() -> dict:
def _fillet2D(self, radius: float, vertices: List[cq.Vertex]) -> cq.Wire:
+ """Fillet 2D
+
+ Fillet wires in a workplane
+
+ Args:
+ radius: fillet radius
+ vertices: vertices to fillet
+
+ Returns:
+ The filleted result
+ """
return cq.Workplane(self.val().fillet2D(radius, vertices))
@@ -342,7 +348,37 @@ def select_by_size_fn(cls, size: str) -> dict:
class Nut(ABC):
- """Base Class used to create standard threaded nuts"""
+ """Parametric Nut
+
+ Base Class used to create standard threaded nuts
+
+ Args:
+ size (str): standard sizes - e.g. "M6-1"
+ fastener_type (str): type identifier - e.g. "iso4032"
+ hand (Literal["right","left"], optional): thread direction. Defaults to "right".
+ simple (bool, optional): simplify by not creating thread. Defaults to True.
+
+ Raises:
+ ValueError: invalid size, must be formatted as size-pitch or size-TPI
+ ValueError: invalid fastener_type
+ ValueError: invalid hand, must be one of 'left' or 'right'
+ ValueError: invalid size
+
+ Each nut instance creates a set of instance variables that provide the CAD object as well as valuable
+ parameters, as follows (values intended for internal use are not shown):
+
+ Attributes:
+ tap_drill_sizes (dict): dictionary of drill sizes for tapped holes
+ tap_hole_diameters (dict): dictionary of drill diameters for tapped holes
+ clearance_drill_sizes (dict): dictionary of drill sizes for clearance holes
+ clearance_hole_diameters (dict): dictionary of drill diameters for clearance holes
+ info (str): identifying information
+ nut_class (class): the derived class that created this nut
+ nut_thickness (float): maximum thickness of the nut
+ nut_diameter (float): maximum diameter of the nut
+ cq_object (Compound): cadquery Compound nut as defined by class attributes
+
+ """
# Read clearance and tap hole dimesions tables
# Close, Medium, Loose
@@ -454,7 +490,7 @@ def nut_diameter(self):
@property
def cq_object(self):
- """A cadquery Compound screw as defined by class attributes"""
+ """A cadquery Compound nut as defined by class attributes"""
return self._cq_object
def length_offset(self):
@@ -790,7 +826,42 @@ def countersink_profile(self, fit) -> cq.Workplane:
class Screw(ABC):
- """Base class for a set of threaded screws or bolts"""
+ """Parametric Screw
+
+ Base class for a set of threaded screws or bolts
+
+ Args:
+ size (str): standard sizes - e.g. "M6-1"
+ length (float): distance from base of head to tip of thread
+ fastener_type (str): type identifier - e.g. "iso4014"
+ hand (Literal["right","left"], optional): thread direction. Defaults to "right".
+ simple (bool, optional): simplify by not creating thread. Defaults to True.
+ socket_clearance (float, optional): gap around screw with no recess (e.g. hex head)
+ which allows a socket wrench to be inserted. Defaults to 6mm.
+
+ Raises:
+ ValueError: invalid size, must be formatted as size-pitch or size-TPI
+ ValueError: invalid fastener_type
+ ValueError: invalid hand, must be one of 'left' or 'right'
+ ValueError: invalid size
+
+ Each screw instance creates a set of properties that provide the Compound CAD object as
+ well as valuable parameters, as follows (values intended for internal use are not shown):
+
+ Attributes:
+ tap_drill_sizes (dict): dictionary of drill sizes for tapped holes
+ tap_hole_diameters (dict): dictionary of drill diameters for tapped holes
+ clearance_drill_sizes (dict): dictionary of drill sizes for clearance holes
+ clearance_hole_diameters (dict): dictionary of drill diameters for clearance holes
+ nominal_lengths (list[float]): list of nominal screw lengths
+ info (str): identifying information
+ screw_class (class): the derived class that created this screw
+ head_height (float): maximum height of the screw head
+ head_diameter (float): maximum diameter of the screw head
+ head (Solid): cadquery Solid screw head as defined by class attributes
+ cq_object (Compound): cadquery Compound nut as defined by class attributes
+
+ """
# Read clearance and tap hole dimesions tables
# Close, Medium, Loose
@@ -944,7 +1015,7 @@ def head_diameter(self):
@property
def head(self):
- """A cadquery Solid thread as defined by class attributes"""
+ """cadquery Solid screw head as defined by class attributes"""
return self._head
@property
@@ -1667,7 +1738,32 @@ def head_profile(self):
class Washer(ABC):
- """Base Class used to create standard washers"""
+ """Parametric Washer
+
+ Base class used to create standard washers
+
+ Args:
+ size (str): standard sizes - e.g. "M6-1"
+ fastener_type (str): type identifier - e.g. "iso4032"
+
+ Raises:
+ ValueError: invalid fastener_type
+ ValueError: invalid size
+
+ Each washer instance creates a set of properties that provide the Compound CAD object
+ as well as valuable parameters, as follows (values intended for internal use are not shown):
+
+ Attributes:
+ clearance_drill_sizes (dict): dictionary of drill sizes for clearance holes
+ clearance_hole_diameters (dict): dictionary of drill diameters for clearance holes
+ nominal_lengths (list[float]): list of nominal screw lengths
+ info (str): identifying information
+ washer_class (str): display friendly class name
+ washer_diameter (float): maximum diameter of the washer
+ washer_thickness (float): maximum thickness of the washer
+ cq_object (Compound): cadquery Compound washer as defined by class attributes
+
+ """
# Read clearance and tap hole dimesions tables
# Close, Normal, Loose
@@ -1873,310 +1969,3 @@ def washer_profile(self):
return profile
washer_countersink_profile = Washer.default_countersink_profile
-
-
-T = TypeVar("T", bound="Workplane")
-
-
-def _fastenerHole(
- self: T,
- hole_diameters: dict,
- fastener: Union[Nut, Screw],
- depth: float,
- washers: List[Washer],
- fit: Optional[Literal["Close", "Normal", "Loose"]] = None,
- material: Optional[Literal["Soft", "Hard"]] = None,
- counterSunk: Optional[bool] = True,
- baseAssembly: Optional[cq.Assembly] = None,
- hand: Optional[Literal["right", "left"]] = None,
- simple: Optional[bool] = False,
- clean: Optional[bool] = True,
-) -> T:
- """
- Makes a counterbore clearance, tap or threaded hole for the given screw for each item
- on the stack. The surface of the hole is at the current workplane.
- """
-
- # If there is a thread direction, this is a threaded hole
- threaded_hole = not hand is None
-
- bore_direction = cq.Vector(0, 0, -1)
- origin = cq.Vector(0, 0, 0)
-
- # Setscrews' countersink_profile is None so check if it exists
- countersink_profile = fastener.countersink_profile(fit)
- if counterSunk and not countersink_profile is None:
- head_offset = countersink_profile.vertices(">Z").val().Z
- countersink_cutter = (
- countersink_profile.revolve().translate((0, 0, -head_offset)).val()
- )
- else:
- head_offset = 0
-
- if threaded_hole:
- hole_radius = fastener.thread_diameter / 2
- else:
- key = fit if material is None else material
- try:
- hole_radius = hole_diameters[key] / 2
- except KeyError as e:
- raise ValueError(
- f"{key} invalid, must be one of {list(hole_diameters.keys())}"
- ) from e
-
- shank_hole = cq.Solid.makeCylinder(
- radius=hole_radius,
- height=depth,
- pnt=origin,
- dir=bore_direction,
- )
- if counterSunk and not countersink_profile is None:
- fastener_hole = countersink_cutter.fuse(shank_hole)
- else:
- fastener_hole = shank_hole
-
- cskAngle = 82 # Common tip angle
- h = hole_radius / tan(radians(cskAngle / 2.0))
- drill_tip = cq.Solid.makeCone(
- hole_radius, 0.0, h, bore_direction * depth, bore_direction
- )
- fastener_hole = fastener_hole.fuse(drill_tip)
-
- # Record the location of each hole for use in the assembly
- null_object = cq.Solid.makeBox(1, 1, 1)
- relocated_test_objects = self.eachpoint(lambda loc: null_object.moved(loc), True)
- hole_locations = [loc.location() for loc in relocated_test_objects.vals()]
-
- # Add fasteners and washers to the base assembly if it was provided
- if baseAssembly is not None:
- for hole_loc in hole_locations:
- washer_thicknesses = 0
- if not washers is None:
- for washer in washers:
- baseAssembly.add(
- washer.cq_object,
- loc=hole_loc
- * cq.Location(
- bore_direction
- * (
- head_offset
- - fastener.length_offset()
- - washer_thicknesses
- )
- ),
- )
- washer_thicknesses += washer.washer_thickness
- # Create a metadata entry associating the auto-generated name & fastener
- baseAssembly.metadata[baseAssembly.children[-1].name] = washer
-
- baseAssembly.add(
- fastener.cq_object,
- loc=hole_loc
- * cq.Location(
- bore_direction
- * (head_offset - fastener.length_offset() - washer_thicknesses)
- ),
- )
- # Create a metadata entry associating the auto-generated name & fastener
- baseAssembly.metadata[baseAssembly.children[-1].name] = fastener
-
- # Make holes in the stack solid object
- part = self.cutEach(lambda loc: fastener_hole.moved(loc), True, False)
-
- # Add threaded inserts
- if threaded_hole and not simple:
- thread = IsoThread(
- major_diameter=fastener.thread_diameter,
- pitch=fastener.thread_pitch,
- length=depth - head_offset,
- external=False,
- hand=hand,
- )
- for hole_loc in hole_locations:
- part = part.union(
- thread.cq_object.moved(hole_loc * cq.Location(bore_direction * depth))
- )
- if clean:
- part = part.clean()
- return part
-
-
-cq.Workplane.fastenerHole = _fastenerHole
-
-
-def _clearanceHole(
- self: T,
- fastener: Union[Nut, Screw],
- washers: Optional[List[Washer]] = None,
- fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
- depth: Optional[float] = None,
- counterSunk: Optional[bool] = True,
- baseAssembly: Optional[cq.Assembly] = None,
- clean: Optional[bool] = True,
-) -> T:
- """Clearance hole front end to fastener hole"""
- if depth is None:
- depth = self.largestDimension()
-
- return self.fastenerHole(
- hole_diameters=fastener.clearance_hole_diameters,
- fastener=fastener,
- washers=washers,
- fit=fit,
- depth=depth,
- counterSunk=counterSunk,
- baseAssembly=baseAssembly,
- clean=clean,
- )
-
-
-def _tapHole(
- self: T,
- fastener: Union[Nut, Screw],
- washers: Optional[List[Washer]] = None,
- material: Optional[Literal["Soft", "Hard"]] = "Soft",
- depth: Optional[float] = None,
- counterSunk: Optional[bool] = True,
- fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
- baseAssembly: Optional[cq.Assembly] = None,
- clean: Optional[bool] = True,
-) -> T:
- """Tap hole front end to fastener hole"""
- if depth is None:
- depth = self.largestDimension()
-
- return self.fastenerHole(
- hole_diameters=fastener.tap_hole_diameters,
- fastener=fastener,
- washers=washers,
- fit=fit,
- material=material,
- depth=depth,
- counterSunk=counterSunk,
- baseAssembly=baseAssembly,
- clean=clean,
- )
-
-
-def _threadedHole(
- self: T,
- fastener: Screw,
- depth: float,
- washers: Optional[List[Washer]] = None,
- hand: Literal["right", "left"] = "right",
- simple: Optional[bool] = False,
- counterSunk: Optional[bool] = True,
- fit: Optional[Literal["Close", "Normal", "Loose"]] = "Normal",
- baseAssembly: Optional[cq.Assembly] = None,
- clean: Optional[bool] = True,
-) -> T:
- """Threaded hole front end to fastener hole"""
- return self.fastenerHole(
- hole_diameters=fastener.clearance_hole_diameters,
- fastener=fastener,
- washers=washers,
- fit=fit,
- depth=depth,
- counterSunk=counterSunk,
- baseAssembly=baseAssembly,
- hand=hand,
- simple=simple,
- clean=clean,
- )
-
-
-cq.Workplane.clearanceHole = _clearanceHole
-cq.Workplane.tapHole = _tapHole
-cq.Workplane.threadedHole = _threadedHole
-
-
-def _fastener_quantities(self, bom: bool = True, deep: bool = True) -> dict:
- """Generate a bill of materials of the fasteners in an assembly augmented by the hole methods
- bom: returns fastener.info if True else fastener
- """
- assembly_list = []
- if deep:
- for _name, sub_assembly in self.traverse():
- assembly_list.append(sub_assembly)
- else:
- assembly_list.append(self)
-
- fasteners = []
- for sub_assembly in assembly_list:
- for value in sub_assembly.metadata.values():
- if isinstance(value, (Screw, Nut, Washer)):
- fasteners.append(value)
-
- unique_fasteners = set(fasteners)
- if bom:
- quantities = {f.info: fasteners.count(f) for f in unique_fasteners}
- else:
- quantities = {f: fasteners.count(f) for f in unique_fasteners}
- return quantities
-
-
-cq.Assembly.fastenerQuantities = _fastener_quantities
-
-
-def _location_str(self):
- """A __str__ method to the Location class"""
- loc_tuple = self.toTuple()
- return f"({str(loc_tuple[0])}, {str(loc_tuple[1])})"
-
-
-cq.Location.__str__ = _location_str
-
-
-def _fastener_locations(self, fastener: Union[Nut, Screw]) -> list[cq.Location]:
- """Generate a list of cadquery Locations for the given fastener relative to the Assembly"""
-
- name_to_fastener = {}
- base_assembly_structure = {}
- # Extract a list of only the fasteners from the metadata
- for (name, a) in self.traverse():
- base_assembly_structure[name] = a
- if a.metadata is None:
- continue
-
- for key, value in a.metadata.items():
- if value == fastener:
- name_to_fastener[key] = value
-
- fastener_path_locations = {}
- base_assembly_path = self._flatten()
- for assembly_name, _assembly_pointer in base_assembly_path.items():
- for fastener_name in name_to_fastener.keys():
- if fastener_name in assembly_name:
- parents = assembly_name.split("/")
- fastener_path_locations[fastener_name] = [
- base_assembly_structure[name].loc for name in parents
- ]
-
- fastener_locations = [
- reduce(lambda l1, l2: l1 * l2, locs)
- for locs in fastener_path_locations.values()
- ]
-
- return fastener_locations
-
-
-cq.Assembly.fastenerLocations = _fastener_locations
-
-
-def _push_fastener_locations(
- self: T,
- fastener: Union[Nut, Screw],
- baseAssembly: cq.Assembly,
-):
- """Push the Location(s) of the given fastener relative to the given Assembly onto the stack"""
-
- # The locations need to be pushed as global not local object locations
- ns = self.__class__()
- ns.plane = cq.Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1))
- ns.parent = self
- ns.objects = baseAssembly.fastenerLocations(fastener)
- ns.ctx = self.ctx
- return ns
-
-
-cq.Workplane.pushFastenerLocations = _push_fastener_locations
diff --git a/src/cq_warehouse/sprocket.py b/src/cq_warehouse/sprocket.py
index 120065c..7f073a7 100644
--- a/src/cq_warehouse/sprocket.py
+++ b/src/cq_warehouse/sprocket.py
@@ -30,106 +30,67 @@
"""
from math import sin, asin, cos, pi, radians, sqrt
-from typing import Union, Tuple
-from pydantic import BaseModel, PrivateAttr, validator, validate_arguments, Field
import cadquery as cq
+from cadquery import Vector, Workplane, Wire
import cq_warehouse.extensions
-VectorLike = Union[Tuple[float, float], Tuple[float, float, float], cq.Vector]
-
-VERSION = 1.0
MM = 1
INCH = 25.4 * MM
#
# =============================== CLASSES ===============================
#
-class Sprocket(BaseModel):
+class Sprocket:
"""
Create a new sprocket object as defined by the given parameters. The input parameter
defaults are appropriate for a standard bicycle chain.
- Usage:
- s = Sprocket(num_teeth=32)
- print(s.pitch_radius) # 64.78458745735234
- s.cq_object.rotate((0,0,0),(0,0,1),10)
-
- Attributes
- ----------
- num_teeth : int
- the number of teeth on the perimeter of the sprocket
- chain_pitch : float
- the distance between the centers of two adjacent rollers (default 1/2 INCH)
- roller_diameter : float
- the size of the cylindrical rollers within the chain (default 5/16 INCH)
- clearance : float
- the size of the gap between the chain's rollers and the sprocket's teeth (default 0)
- thickness : float
- the thickness of the sprocket (default 0.084 INCH)
- bolt_circle_diameter : float
- the diameter of the mounting bolt hole pattern (default 0)
- num_mount_bolts : int
- the number of bolt holes (default 0) - if 0, no bolt holes are added to the sprocket
- mount_bolt_diameter : float
- the size of the bolt holes use to mount the sprocket (default 0)
- bore_diameter : float
- the size of the central hole in the sprocket (default 0) - if 0, no bore hole is added
- to the sprocket
- pitch_radius : float
- the radius of the circle formed by the center of the chain rollers
- outer_radius : float
- the size of the sprocket from center to tip of the teeth
- pitch_circumference : float
- the circumference of the sprocket at the pitch radius
- cq_object : cq.Workplane
- the cadquery sprocket object
-
- Methods
- -------
-
- sprocket_pitch_radius(num_teeth,chain_pitch) -> float:
- Calculate and return the pitch radius of a sprocket with the given number of teeth
- and chain pitch
+ Args:
+ num_teeth (int): number of teeth on the perimeter of the sprocket
+ chain_pitch (float): distance between the centers of two adjacent rollers.
+ Defaults to 1/2 inch.
+ roller_diameter (float): size of the cylindrical rollers within the chain.
+ Defaults to 5/16 inch.
+ clearance (float): size of the gap between the chain's rollers and the sprocket's teeth.
+ Defaults to 0.
+ thickness (float): thickness of the sprocket.
+ Defaults to 0.084 inch.
+ bolt_circle_diameter (float): diameter of the mounting bolt hole pattern.
+ Defaults to 0.
+ num_mount_bolts (int): number of bolt holes (default 0) - if 0, no bolt holes
+ are added to the sprocket
+ mount_bolt_diameter (float): size of the bolt holes use to mount the sprocket.
+ Defaults to 0.
+ bore_diameter (float): size of the central hole in the sprocket (default 0) - if 0,
+ no bore hole is added to the sprocket
+
+ **NOTE**: Default parameters are for standard single sprocket bicycle chains.
+
+ Attributes:
+ pitch_radius (float): radius of the circle formed by the center of the chain rollers
+ outer_radius (float): size of the sprocket from center to tip of the teeth
+ pitch_circumference (float): circumference of the sprocket at the pitch radius
+ cq_object (Workplane): cadquery sprocket object
+
+ Example:
+
+ .. doctest::
+
+ >>> s = Sprocket(num_teeth=32)
+ >>> print(s.pitch_radius)
+ 64.78458745735234
+ >>> s.cq_object.rotate((0,0,0),(0,0,1),10)
- sprocket_circumference(num_teeth,chain_pitch) -> float:
- Calculate and return the pitch circumference of a sprocket with the given number
- of teeth and chain pitch
"""
- # Instance Attributes
- num_teeth: int = Field(..., gt=2)
- chain_pitch: float = (1 / 2) * INCH
- roller_diameter: float = (5 / 16) * INCH
- clearance: float = 0.0
- thickness: float = 0.084 * INCH
- bolt_circle_diameter: float = 0.0
- num_mount_bolts: int = 0
- mount_bolt_diameter: float = 0.0
- bore_diameter: float = 0.0
-
- # Private Attributes
- _flat_teeth: bool = PrivateAttr()
- _cq_object: cq.Workplane = PrivateAttr()
-
- # pylint: disable=no-self-argument
- # pylint: disable=no-self-use
- @validator("roller_diameter")
- def is_roller_too_large(cls, i, values):
- """ Ensure that the roller would fit in the chain """
- if i >= values["chain_pitch"]:
- raise ValueError(
- f"roller_diameter {i} is too large for chain_pitch {values['chain_pitch']}"
- )
- return i
-
@property
def pitch_radius(self):
- """ The radius of the circle formed by the center of the chain rollers """
+ """The radius of the circle formed by the center of the chain rollers"""
return Sprocket.sprocket_pitch_radius(self.num_teeth, self.chain_pitch)
@property
def outer_radius(self):
- """ The size of the sprocket from center to tip of the teeth """
+ """The size of the sprocket from center to tip of the teeth"""
if self._flat_teeth:
o_radius = self.pitch_radius + self.roller_diameter / 4
else:
@@ -143,25 +104,54 @@ def outer_radius(self):
@property
def pitch_circumference(self):
- """ The circumference of the sprocket at the pitch radius """
+ """The circumference of the sprocket at the pitch radius"""
return Sprocket.sprocket_circumference(self.num_teeth, self.chain_pitch)
@property
def cq_object(self):
- """ A cadquery Workplane sprocket as defined by class attributes """
+ """A cadquery Workplane sprocket as defined by class attributes"""
return self._cq_object
- def __init__(self, **data):
- """ Validate inputs and create the chain assembly object """
- # Use the BaseModel initializer to validate the attributes
- super().__init__(**data)
+ def __init__(
+ self,
+ num_teeth: int,
+ chain_pitch: float = (1 / 2) * INCH,
+ roller_diameter: float = (5 / 16) * INCH,
+ clearance: float = 0.0,
+ thickness: float = 0.084 * INCH,
+ bolt_circle_diameter: float = 0.0,
+ num_mount_bolts: int = 0,
+ mount_bolt_diameter: float = 0.0,
+ bore_diameter: float = 0.0,
+ ):
+ """Validate inputs and create the chain assembly object"""
+ self.num_teeth = num_teeth
+ self.chain_pitch = chain_pitch
+ self.roller_diameter = roller_diameter
+ self.clearance = clearance
+ self.thickness = thickness
+ self.bolt_circle_diameter = bolt_circle_diameter
+ self.num_mount_bolts = num_mount_bolts
+ self.mount_bolt_diameter = mount_bolt_diameter
+ self.bore_diameter = bore_diameter
+
+ # Validate inputs
+ """Ensure that the roller would fit in the chain"""
+ if self.roller_diameter >= self.chain_pitch:
+ raise ValueError(
+ f"roller_diameter {self.roller_diameter} is too large for chain_pitch {self.chain_pitch}"
+ )
+ if not isinstance(num_teeth, int) or num_teeth <= 2:
+ raise ValueError(
+ f"num_teeth must be an integer greater than 2 not {num_teeth}"
+ )
# Create the sprocket
self._cq_object = self._make_sprocket()
- def _make_sprocket(self) -> cq.Workplane:
- """ Create a new sprocket object as defined by the class attributes """
+ def _make_sprocket(self) -> Workplane:
+ """Create a new sprocket object as defined by the class attributes"""
sprocket = (
- cq.Workplane("XY")
+ Workplane("XY")
.polarArray(self.pitch_radius, 0, 360, self.num_teeth)
.tooth_outline(
self.num_teeth, self.chain_pitch, self.roller_diameter, self.clearance
@@ -204,11 +194,10 @@ def _make_sprocket(self) -> cq.Workplane:
return sprocket
@staticmethod
- @validate_arguments
def sprocket_pitch_radius(num_teeth: int, chain_pitch: float) -> float:
"""
Calculate and return the pitch radius of a sprocket with the given number of teeth
- and chain pitch
+ and chain pitch
Parameters
----------
@@ -220,11 +209,10 @@ def sprocket_pitch_radius(num_teeth: int, chain_pitch: float) -> float:
return sqrt(chain_pitch * chain_pitch / (2 * (1 - cos(2 * pi / num_teeth))))
@staticmethod
- @validate_arguments
def sprocket_circumference(num_teeth: int, chain_pitch: float) -> float:
"""
Calculate and return the pitch circumference of a sprocket with the given number of
- teeth and chain pitch
+ teeth and chain pitch
Parameters
----------
@@ -245,12 +233,11 @@ def sprocket_circumference(num_teeth: int, chain_pitch: float) -> float:
#
-@validate_arguments
def make_tooth_outline(
num_teeth: int, chain_pitch: float, roller_diameter: float, clearance: float = 0.0
-) -> cq.Wire:
+) -> Wire:
"""
- Create a cq.Wire in the shape of a single tooth of the sprocket defined by the input parameters
+ Create a Wire in the shape of a single tooth of the sprocket defined by the input parameters
There are two different shapes that the tooth could take:
1) "Spiky" teeth: given sufficiently large rollers, there is no circular top
@@ -318,17 +305,17 @@ def make_tooth_outline(
)
# Bottom of the roller arc
- start_pt = cq.Vector(pitch_rad - roller_rad, 0).rotateZ(tooth_a_degrees / 2)
+ start_pt = Vector(pitch_rad - roller_rad, 0).rotateZ(tooth_a_degrees / 2)
# Where the roller arc meets transitions to the top half of the tooth
- tangent_pt = cq.Vector(0, -roller_rad).rotateZ(-tooth_a_degrees / 2) + cq.Vector(
+ tangent_pt = Vector(0, -roller_rad).rotateZ(-tooth_a_degrees / 2) + Vector(
pitch_rad, 0
).rotateZ(tooth_a_degrees / 2)
# The intersection point of the tooth and the outer rad
- outer_pt = cq.Vector(
+ outer_pt = Vector(
outer_rad * cos(outer_intersect_a_r), outer_rad * sin(outer_intersect_a_r)
)
# The location of the tip of the spike if there is no "flat" section
- spike_pt = cq.Vector(
+ spike_pt = Vector(
sqrt(pitch_rad ** 2 - (chain_pitch / 2) ** 2)
+ sqrt((chain_pitch - roller_rad) ** 2 - (chain_pitch / 2) ** 2),
0,
@@ -337,7 +324,7 @@ def make_tooth_outline(
# Generate the tooth outline
if outer_pt.y > 0: # "Flat" topped sprockets
tooth = (
- cq.Workplane("XY")
+ Workplane("XY")
.moveTo(start_pt.x, start_pt.y)
.radiusArc(tangent_pt.toTuple(), -roller_rad)
.radiusArc(outer_pt.toTuple(), chain_pitch - roller_rad)
@@ -350,7 +337,7 @@ def make_tooth_outline(
)
else: # "Spiky" sprockets
tooth = (
- cq.Workplane("XY")
+ Workplane("XY")
.moveTo(start_pt.x, start_pt.y)
.radiusArc(tangent_pt.toTuple(), -roller_rad)
.radiusArc(spike_pt.toTuple(), chain_pitch - roller_rad)
@@ -365,20 +352,20 @@ def make_tooth_outline(
def _tooth_outline(
self, num_teeth, chain_pitch, roller_diameter, clearance
-) -> cq.Workplane:
- """ Wrap make_tooth_outline for use within cq.Workplane with multiple sprocket teeth """
+) -> Workplane:
+ """Wrap make_tooth_outline for use within Workplane with multiple sprocket teeth"""
# pylint: disable=unnecessary-lambda
tooth = make_tooth_outline(num_teeth, chain_pitch, roller_diameter, clearance)
return self.eachpoint(lambda loc: tooth.moved(loc), True)
-cq.Workplane.tooth_outline = _tooth_outline
+Workplane.tooth_outline = _tooth_outline
#
# Extensions to the Vector class
-def _vector_flip_y(self) -> cq.Vector:
- """ cq.Vector reflect across the XZ plane """
- return cq.Vector(self.x, -self.y, self.z)
+def _vector_flip_y(self) -> Vector:
+ """Vector reflect across the XZ plane"""
+ return Vector(self.x, -self.y, self.z)
-cq.Vector.flipY = _vector_flip_y
+Vector.flipY = _vector_flip_y
diff --git a/src/cq_warehouse/thread.py b/src/cq_warehouse/thread.py
index 96c662b..6aa028e 100644
--- a/src/cq_warehouse/thread.py
+++ b/src/cq_warehouse/thread.py
@@ -53,23 +53,47 @@ def imperial_str_to_float(measure: str) -> float:
return result
-class Thread:
- """Create a helical thread
- Each end of the thread can finished as follows:
- - "raw" unfinished which typically results in the thread extended below
- z=0 or above z=length
- - "fade" the thread height drops to zero over 90° of arc (or 1/4 pitch)
- - "square" clipped by the z=0 or z=length plane
- - "chamfer" conical ends which facilitates alignment of a bolt into a nut
-
- Note that the performance of this Thread class varies significantly by end
- finish. Here are some sample measurements (both ends finished) to illustate
- how the time required to create the thread varies:
- - "raw" 0.018s
- - "fade" 0.087s
- - "square" 0.370s
- - "chamfer" 1.641s
+cq.Shape
+
+class Thread:
+ """Helical thread
+
+ The most general thread class used to build all of the other threads.
+ Creates right or left hand helical thread with the given
+ root and apex radii.
+
+ Args:
+ apex_radius: Radius at the narrow tip of the thread.
+ apex_width: Radius at the wide base of the thread.
+ root_radius: Radius at the wide base of the thread.
+ root_width: Thread base width.
+ pitch: Length of 360° of thread rotation.
+ length: End to end length of the thread.
+ apex_offset: Asymmetric thread apex offset from center. Defaults to 0.0.
+ hand: Twist direction. Defaults to "right".
+ taper_angle: Cone angle for tapered thread. Defaults to None.
+ end_finishes: Profile of each end, one of:
+
+ "raw"
+ unfinished which typically results in the thread
+ extended below z=0 or above z=length
+ "fade"
+ the thread height drops to zero over 90° of arc
+ (or 1/4 pitch)
+ "square"
+ clipped by the z=0 or z=length plane
+ "chamfer"
+ conical ends which facilitates alignment of a bolt
+ into a nut
+
+ Defaults to ("raw","raw").
+
+ Attributes:
+ cq_object: cadquery Solid object
+
+ Raises:
+ ValueError: if end_finishes not in ["raw", "square", "fade", "chamfer"]:
"""
def fade_helix(
@@ -120,7 +144,7 @@ def __init__(
):
"""Store the parameters and create the thread object"""
for finish in end_finishes:
- if not finish in ["raw", "square", "fade", "chamfer"]:
+ if finish not in ["raw", "square", "fade", "chamfer"]:
raise ValueError(
'end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer"'
)
@@ -369,9 +393,49 @@ def make_thread_solid(
class IsoThread:
- """
- ISO standard threads as shown in the following diagram:
- https://en.wikipedia.org/wiki/ISO_metric_screw_thread#/media/File:ISO_and_UTS_Thread_Dimensions.svg
+ """ISO Standard Thread
+
+ Both external and internal ISO standard 60° threads as shown in
+ the following diagram (from https://en.wikipedia.org/wiki/ISO_metric_screw_thread):
+
+ .. image:: https://upload.wikimedia.org/wikipedia/commons/4/4b/ISO_and_UTS_Thread_Dimensions.svg
+
+ The following is an example of an internal thread with a chamfered end as might be found inside a nut:
+
+ .. image:: internal_iso_thread.png
+
+ Args:
+ major_diameter (float): Primary thread diameter
+ pitch (float): Length of 360° of thread rotation
+ length (float): End to end length of the thread
+ external (bool, optional): External or internal thread selector. Defaults to True.
+ hand (Literal[, optional): Twist direction. Defaults to "right".
+ end_finishes (Tuple[ Literal[, optional): Profile of each end, one of:
+
+ "raw"
+ unfinished which typically results in the thread
+ extended below z=0 or above z=length
+ "fade"
+ the thread height drops to zero over 90° of arc
+ (or 1/4 pitch)
+ "square"
+ clipped by the z=0 or z=length plane
+ "chamfer"
+ conical ends which facilitates alignment of a bolt
+ into a nut
+
+ Defaults to ("fade", "square").
+
+ Attributes:
+ thread_angle (int): 60 degrees
+ h_parameter (float): Value of `h` as shown in the thread diagram
+ min_radius (float): Inside radius of the thread diagram
+ cq_object (Solid): The generated cadquery Solid thread object
+
+ Raises:
+ ValueError: if hand not in ["right", "left"]:
+ ValueError: end_finishes not in ["raw", "square", "fade", "chamfer"]
+
"""
@property
@@ -411,7 +475,7 @@ def __init__(
raise ValueError(f'hand must be one of "right" or "left" not {hand}')
self.hand = hand
for finish in end_finishes:
- if not finish in ["raw", "square", "fade", "chamfer"]:
+ if finish not in ["raw", "square", "fade", "chamfer"]:
raise ValueError(
'end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer"'
)
@@ -433,13 +497,46 @@ def __init__(
class TrapezoidalThread(ABC):
- """
+ """Trapezoidal Thread Base Class
+
Trapezoidal Thread base class for Metric and Acme derived classes
Trapezoidal thread forms are screw thread profiles with trapezoidal outlines. They are
the most common forms used for leadscrews (power screws). They offer high strength
and ease of manufacture. They are typically found where large loads are required, as
in a vise or the leadscrew of a lathe.
+
+ Args:
+ size (str): specified by derived class
+ length (float): thread length
+ external (bool, optional): external or internal thread selector. Defaults to True.
+ hand (Literal[, optional): twist direction. Defaults to "right".
+ end_finishes (Tuple[ Literal[, optional): Profile of each end, one of:
+
+ "raw"
+ unfinished which typically results in the thread
+ extended below z=0 or above z=length
+ "fade"
+ the thread height drops to zero over 90° of arc
+ (or 1/4 pitch)
+ "square"
+ clipped by the z=0 or z=length plane
+ "chamfer"
+ conical ends which facilitates alignment of a bolt
+ into a nut
+
+ Defaults to ("fade", "fade").
+
+ Raises:
+ ValueError: hand must be one of "right" or "left"
+ ValueError: end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer"
+
+ Attributes:
+ thread_angle (int): thread angle in degrees
+ diameter (float): thread diameter
+ pitch (float): thread pitch
+ cq_object (Solid): The generated cadquery Solid thread object
+
"""
@property
@@ -506,11 +603,46 @@ def __init__(
class AcmeThread(TrapezoidalThread):
- """
+ """ACME Thread
+
The original trapezoidal thread form, and still probably the one most commonly encountered
worldwide, with a 29° thread angle, is the Acme thread form.
- size is specified as a string (i.e. "3/4" or "1 1/4")
+ The following is the acme thread with faded ends:
+
+ .. image:: acme_thread.png
+
+ Args:
+ size (str): size as a string (i.e. "3/4" or "1 1/4")
+ length (float): thread length
+ external (bool, optional): external or internal thread selector. Defaults to True.
+ hand (Literal[, optional): twist direction. Defaults to "right".
+ end_finishes (Tuple[ Literal[, optional): Profile of each end, one of:
+
+ "raw"
+ unfinished which typically results in the thread
+ extended below z=0 or above z=length
+ "fade"
+ the thread height drops to zero over 90° of arc
+ (or 1/4 pitch)
+ "square"
+ clipped by the z=0 or z=length plane
+ "chamfer"
+ conical ends which facilitates alignment of a bolt
+ into a nut
+
+ Defaults to ("fade", "fade").
+
+ Raises:
+ ValueError: hand must be one of "right" or "left"
+ ValueError: end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer"
+
+ Attributes:
+ thread_angle (int): thread angle in degrees
+ diameter (float): thread diameter
+ pitch (float): thread pitch
+ cq_object (Solid): The generated cadquery Solid thread object
+
"""
acme_pitch = {
@@ -550,10 +682,40 @@ def parse_size(cls, size: str) -> Tuple[float, float]:
class MetricTrapezoidalThread(TrapezoidalThread):
- """
+ """Metric Trapezoidal Thread
+
The ISO 2904 standard metric trapezoidal thread with a thread angle of 30°
- size is specified as a sting with diameter x pitch in mm (i.e. "8x1.5")
+ Args:
+ size (str): specified as a sting with diameter x pitch in mm (i.e. "8x1.5")
+ length (float): End to end length of the thread
+ external (bool, optional): external or internal thread selector. Defaults to True.
+ hand (Literal[, optional): twist direction. Defaults to "right".
+ end_finishes (Tuple[ Literal[, optional): Profile of each end, one of:
+
+ "raw"
+ unfinished which typically results in the thread
+ extended below z=0 or above z=length
+ "fade"
+ the thread height drops to zero over 90° of arc
+ (or 1/4 pitch)
+ "square"
+ clipped by the z=0 or z=length plane
+ "chamfer"
+ conical ends which facilitates alignment of a bolt
+ into a nut
+
+ Defaults to ("fade", "fade").
+
+ Raises:
+ ValueError: hand must be one of "right" or "left"
+ ValueError: end_finishes invalid, must be tuple() of "raw, square, taper, or chamfer"
+
+ Attributes:
+ thread_angle (int): thread angle in degrees
+ diameter (float): thread diameter
+ pitch (float): thread pitch
+ cq_object (Solid): The generated cadquery Solid thread object
"""
# Turn off black auto-format for this array as it will be spread over hundreds of lines
@@ -610,16 +772,40 @@ def parse_size(cls, size: str) -> Tuple[float, float]:
class PlasticBottleThread:
"""ASTM D2911 Plastic Bottle Thread
- size: [L|M][diameter(mm)]SP[100|103|110|200|400|410|415:425|444]
- e.g. M15SP425
+ The `ASTM D2911 Standard `_ Plastic Bottle Thread.
- L Style: All-Purpose Thread - trapezoidal shape with 30° shoulders, metal or platsic closures
- M Style: Modified Buttress Thread - asymmetric shape with 10° and 40/45/50° shoulders, plastic closures
+ L Style:
+ All-Purpose Thread - trapezoidal shape with 30° shoulders, metal or platsic closures
+ M Style:
+ Modified Buttress Thread - asymmetric shape with 10° and 40/45/50° shoulders, plastic closures
+
+ .. image:: plasticThread.png
+
+ Args:
+ size (str): as defined by the ASTM is specified as [L|M][diameter(mm)]SP[100|103|110|200|400|410|415|425|444]
+ external (bool, optional): external or internal thread selector. Defaults to True.
+ hand (Literal[, optional): twist direction. Defaults to "right".
+ manufacturingCompensation (float, optional): used to compensate for over-extrusion of 3D
+ printers. A value of 0.2mm will reduce the radius of an external thread by 0.2mm (and
+ increase the radius of an internal thread) such that the resulting 3D printed part
+ matches the target dimensions. Defaults to 0.0.
+
+ Raises:
+ ValueError: hand must be one of "right" or "left"
+ ValueError: size invalid, must match [L|M][diameter(mm)]SP[100|103|110|200|400|410|415:425|444]
+ ValueError: finish invalid
+ ValueError: diameter invalid
+
+ Attributes:
+ cq_object (Solid): cadquery thread object
+
+ Example:
+ .. code-block:: python
+
+ thread = PlasticBottleThread(
+ size="M38SP444", external=False, manufacturingCompensation=0.2 * MM
+ )
- example:
- thread = PlasticBottleThread(
- size="M38SP444", external=False, manufacturingCompensation=0.2 * MM
- )
"""
# {TPI: [root_width,thread_height]}
diff --git a/tests/drafting_tests.py b/tests/drafting_tests.py
index 9e0a57b..9e62a22 100644
--- a/tests/drafting_tests.py
+++ b/tests/drafting_tests.py
@@ -48,7 +48,7 @@ class TestClassInstantiation(unittest.TestCase):
"""Test Draft class instantiation"""
def test_draft_instantiation(self):
- """Parameter parsing is mostly covered by pydantic, except these"""
+ """Parameter parsing"""
with self.assertRaises(ValueError):
Draft(units="normal")
with self.assertRaises(ValueError):
@@ -218,7 +218,7 @@ def test_callout(self):
with self.assertRaises(ValueError):
metric_drawing.callout(label="error")
- with self.assertRaises(ValueError):
+ with self.assertRaises(TypeError):
metric_drawing.callout(label="test", location=(0, 0, 0), justify="centre")
diff --git a/tests/extensions_tests.py b/tests/extensions_tests.py
index 0358143..7f539ba 100644
--- a/tests/extensions_tests.py
+++ b/tests/extensions_tests.py
@@ -29,6 +29,7 @@
import unittest
import cadquery as cq
from cq_warehouse.extensions import *
+from cq_warehouse.fastener import SocketHeadCapScrew, DomedCapNut, ChamferedWasher
def _assertTupleAlmostEquals(self, expected, actual, places, msg=None):
@@ -186,24 +187,6 @@ def test_vector_rotate(self):
self.assertTupleAlmostEquals(vector_y.toTuple(), (math.sqrt(2), 2, 0), 7)
self.assertTupleAlmostEquals(vector_z.toTuple(), (0, -math.sqrt(2), 3), 7)
- def test_point_to_vector(self):
- """Validate conversion of 2D points to 3D vectors"""
- point = cq.Vector(1, 2)
- with self.assertRaises(ValueError):
- point.pointToVector((1, 0, 0))
- with self.assertRaises(ValueError):
- point.pointToVector("x")
- self.assertTupleAlmostEquals(point.pointToVector("XY").toTuple(), (1, 2, 0), 7)
- self.assertTupleAlmostEquals(
- point.pointToVector("XY", 4).toTuple(), (1, 2, 4), 7
- )
- self.assertTupleAlmostEquals(
- point.pointToVector("XZ", 3).toTuple(), (1, 3, 2), 7
- )
- self.assertTupleAlmostEquals(
- point.pointToVector("YZ", 5).toTuple(), (5, 1, 2), 7
- )
-
def testGetSignedAngle(self):
"""Verify getSignedAngle calculations with and without a provided normal"""
a = math.pi / 3
@@ -362,5 +345,181 @@ def test_vertex_to_vector(self):
)
+class TestFastenerMethods(unittest.TestCase):
+ def test_clearance_hole(self):
+ screw = SocketHeadCapScrew(size="M6-1", fastener_type="iso4762", length=40)
+ depth = screw.min_hole_depth()
+ pillow_block = cq.Assembly(None, name="pillow_block")
+ box = (
+ cq.Workplane("XY")
+ .box(10, 10, 10)
+ .faces(">Z")
+ .workplane()
+ .clearanceHole(fastener=screw, baseAssembly=pillow_block, depth=depth)
+ .val()
+ )
+ self.assertLess(box.Volume(), 999.99)
+ self.assertEqual(len(pillow_block.children), 1)
+ self.assertEqual(pillow_block.fastenerQuantities(bom=False)[screw], 1)
+ self.assertEqual(len(pillow_block.fastenerQuantities(bom=True)), 1)
+
+ def test_invalid_clearance_hole(self):
+ for fastener_class in Screw.__subclasses__() + Nut.__subclasses__():
+ fastener_type = list(fastener_class.types())[0]
+ fastener_size = fastener_class.sizes(fastener_type=fastener_type)[0]
+ if fastener_class in Screw.__subclasses__():
+ fastener = fastener_class(
+ size=fastener_size,
+ fastener_type=fastener_type,
+ length=15,
+ simple=True,
+ )
+ else:
+ fastener = fastener_class(
+ size=fastener_size, fastener_type=fastener_type, simple=True
+ )
+ with self.assertRaises(ValueError):
+ (
+ cq.Workplane("XY")
+ .box(10, 10, 10)
+ .faces(">Z")
+ .workplane()
+ .clearanceHole(fastener=fastener, depth=40, fit="Bad")
+ )
+
+ def test_tap_hole(self):
+ nut = DomedCapNut(size="M6-1", fastener_type="din1587")
+ washer = ChamferedWasher(size="M6", fastener_type="iso7090")
+ pillow_block = cq.Assembly(None, name="pillow_block")
+ box = (
+ cq.Workplane("XY")
+ .box(10, 10, 10)
+ .faces(">Z")
+ .workplane()
+ .tapHole(fastener=nut, baseAssembly=pillow_block, washers=[washer])
+ .val()
+ )
+ self.assertLess(box.Volume(), 999.99)
+ self.assertEqual(len(pillow_block.children), 2)
+
+ def test_invalid_tap_hole(self):
+ for fastener_class in Screw.__subclasses__() + Nut.__subclasses__():
+ fastener_type = list(fastener_class.types())[0]
+ fastener_size = fastener_class.sizes(fastener_type=fastener_type)[0]
+ if fastener_class in Screw.__subclasses__():
+ fastener = fastener_class(
+ size=fastener_size,
+ fastener_type=fastener_type,
+ length=15,
+ simple=True,
+ )
+ else:
+ fastener = fastener_class(
+ size=fastener_size, fastener_type=fastener_type, simple=True
+ )
+ with self.assertRaises(ValueError):
+ (
+ cq.Workplane("XY")
+ .box(10, 10, 10)
+ .faces(">Z")
+ .workplane()
+ .tapHole(fastener=fastener, depth=40, material="Bad")
+ )
+
+ def test_threaded_hole(self):
+ screw = SocketHeadCapScrew(size="M6-1", fastener_type="iso4762", length=40)
+ washer = ChamferedWasher(size="M6", fastener_type="iso7090")
+ pillow_block = cq.Assembly(None, name="pillow_block")
+ box = (
+ cq.Workplane("XY")
+ .box(20, 20, 20)
+ .faces(">Z")
+ .workplane()
+ .threadedHole(
+ fastener=screw,
+ depth=20,
+ baseAssembly=pillow_block,
+ washers=[washer, washer],
+ simple=False,
+ counterSunk=False,
+ )
+ .faces("Z")
+ .workplane()
+ .pushPoints([(5, -5), (-5, -5)])
+ .clearanceHole(
+ fastener=cap_screw, counterSunk=False, baseAssembly=bracket_assembly
+ )
+ .faces(">Y")
+ .workplane()
+ .pushPoints([(0, -7)])
+ .clearanceHole(
+ fastener=cap_screw, counterSunk=False, baseAssembly=bracket_assembly
+ )
+ )
+ # Add the top plate to the top assembly so it can be placed with the screws
+ bracket_assembly.add(angle_bracket, name="angle_bracket")
+ # Add the top plate and screws to the base assembly
+ square_tube_assembly.add(
+ bracket_assembly,
+ name="top_plate_assembly",
+ loc=cq.Location(cq.Vector(20, 10, 10)),
+ )
+
+ # Create the square tube
+ square_tube = (
+ cq.Workplane("YZ")
+ .rect(18, 18)
+ .rect(14, 14)
+ .offset2D(1)
+ .extrude(30, both=True)
+ )
+ original_tube_volume = square_tube.val().Volume()
+ # Complete the square tube assembly by adding the square tube
+ square_tube_assembly.add(square_tube, name="square_tube")
+ # Add tap holes to the square tube that align with the angle bracket
+ square_tube = square_tube.pushFastenerLocations(
+ cap_screw, square_tube_assembly
+ ).tapHole(fastener=cap_screw, counterSunk=False, depth=10)
+ self.assertLess(square_tube.val().Volume(), original_tube_volume)
+
+ # Where are the cap screw holes in the square tube?
+ fastener_positions = [(25.0, 5.0, 12.0), (15.0, 5.0, 12.0), (20.0, 12.0, 5.0)]
+ for i, loc in enumerate(square_tube_assembly.fastenerLocations(cap_screw)):
+ self.assertTupleAlmostEquals(loc.toTuple()[0], fastener_positions[i], 7)
+ self.assertTrue(str(fastener_positions[i]) in str(loc))
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/fastener_tests.py b/tests/fastener_tests.py
index eeb369f..99c2e33 100644
--- a/tests/fastener_tests.py
+++ b/tests/fastener_tests.py
@@ -28,6 +28,7 @@
import unittest
import cadquery as cq
from cq_warehouse.fastener import *
+import cq_warehouse.extensions
MM = 1
IN = 25.4 * MM
@@ -326,181 +327,5 @@ def test_missing_hole_data(self):
self.assertIsNone(screw.nominal_lengths)
-class TestWorkplaneMethods(unittest.TestCase):
- def test_clearance_hole(self):
- screw = SocketHeadCapScrew(size="M6-1", fastener_type="iso4762", length=40)
- depth = screw.min_hole_depth()
- pillow_block = cq.Assembly(None, name="pillow_block")
- box = (
- cq.Workplane("XY")
- .box(10, 10, 10)
- .faces(">Z")
- .workplane()
- .clearanceHole(fastener=screw, baseAssembly=pillow_block, depth=depth)
- .val()
- )
- self.assertLess(box.Volume(), 999.99)
- self.assertEqual(len(pillow_block.children), 1)
- self.assertEqual(pillow_block.fastenerQuantities(bom=False)[screw], 1)
- self.assertEqual(len(pillow_block.fastenerQuantities(bom=True)), 1)
-
- def test_invalid_clearance_hole(self):
- for fastener_class in Screw.__subclasses__() + Nut.__subclasses__():
- fastener_type = list(fastener_class.types())[0]
- fastener_size = fastener_class.sizes(fastener_type=fastener_type)[0]
- if fastener_class in Screw.__subclasses__():
- fastener = fastener_class(
- size=fastener_size,
- fastener_type=fastener_type,
- length=15,
- simple=True,
- )
- else:
- fastener = fastener_class(
- size=fastener_size, fastener_type=fastener_type, simple=True
- )
- with self.assertRaises(ValueError):
- (
- cq.Workplane("XY")
- .box(10, 10, 10)
- .faces(">Z")
- .workplane()
- .clearanceHole(fastener=fastener, depth=40, fit="Bad")
- )
-
- def test_tap_hole(self):
- nut = DomedCapNut(size="M6-1", fastener_type="din1587")
- washer = ChamferedWasher(size="M6", fastener_type="iso7090")
- pillow_block = cq.Assembly(None, name="pillow_block")
- box = (
- cq.Workplane("XY")
- .box(10, 10, 10)
- .faces(">Z")
- .workplane()
- .tapHole(fastener=nut, baseAssembly=pillow_block, washers=[washer])
- .val()
- )
- self.assertLess(box.Volume(), 999.99)
- self.assertEqual(len(pillow_block.children), 2)
-
- def test_invalid_tap_hole(self):
- for fastener_class in Screw.__subclasses__() + Nut.__subclasses__():
- fastener_type = list(fastener_class.types())[0]
- fastener_size = fastener_class.sizes(fastener_type=fastener_type)[0]
- if fastener_class in Screw.__subclasses__():
- fastener = fastener_class(
- size=fastener_size,
- fastener_type=fastener_type,
- length=15,
- simple=True,
- )
- else:
- fastener = fastener_class(
- size=fastener_size, fastener_type=fastener_type, simple=True
- )
- with self.assertRaises(ValueError):
- (
- cq.Workplane("XY")
- .box(10, 10, 10)
- .faces(">Z")
- .workplane()
- .tapHole(fastener=fastener, depth=40, material="Bad")
- )
-
- def test_threaded_hole(self):
- screw = SocketHeadCapScrew(size="M6-1", fastener_type="iso4762", length=40)
- washer = ChamferedWasher(size="M6", fastener_type="iso7090")
- pillow_block = cq.Assembly(None, name="pillow_block")
- box = (
- cq.Workplane("XY")
- .box(20, 20, 20)
- .faces(">Z")
- .workplane()
- .threadedHole(
- fastener=screw,
- depth=20,
- baseAssembly=pillow_block,
- washers=[washer, washer],
- simple=False,
- counterSunk=False,
- )
- .faces("Z")
- .workplane()
- .pushPoints([(5, -5), (-5, -5)])
- .clearanceHole(
- fastener=cap_screw, counterSunk=False, baseAssembly=bracket_assembly
- )
- .faces(">Y")
- .workplane()
- .pushPoints([(0, -7)])
- .clearanceHole(
- fastener=cap_screw, counterSunk=False, baseAssembly=bracket_assembly
- )
- )
- # Add the top plate to the top assembly so it can be placed with the screws
- bracket_assembly.add(angle_bracket, name="angle_bracket")
- # Add the top plate and screws to the base assembly
- square_tube_assembly.add(
- bracket_assembly,
- name="top_plate_assembly",
- loc=cq.Location(cq.Vector(20, 10, 10)),
- )
-
- # Create the square tube
- square_tube = (
- cq.Workplane("YZ")
- .rect(18, 18)
- .rect(14, 14)
- .offset2D(1)
- .extrude(30, both=True)
- )
- original_tube_volume = square_tube.val().Volume()
- # Complete the square tube assembly by adding the square tube
- square_tube_assembly.add(square_tube, name="square_tube")
- # Add tap holes to the square tube that align with the angle bracket
- square_tube = square_tube.pushFastenerLocations(
- cap_screw, square_tube_assembly
- ).tapHole(fastener=cap_screw, counterSunk=False, depth=10)
- self.assertLess(square_tube.val().Volume(), original_tube_volume)
-
- # Where are the cap screw holes in the square tube?
- fastener_positions = [(25.0, 5.0, 12.0), (15.0, 5.0, 12.0), (20.0, 12.0, 5.0)]
- for i, loc in enumerate(square_tube_assembly.fastenerLocations(cap_screw)):
- self.assertTupleAlmostEquals(loc.toTuple()[0], fastener_positions[i], 7)
- self.assertTrue(str(fastener_positions[i]) in str(loc))
-
-
if __name__ == "__main__":
unittest.main()
diff --git a/tests/map_texture_tests.py b/tests/map_texture_tests.py
deleted file mode 100644
index 02e265c..0000000
--- a/tests/map_texture_tests.py
+++ /dev/null
@@ -1,157 +0,0 @@
-"""
-Unit tests for the cq_warehouse map_texture sub-package
-
-name: map_texture_tests.py
-by: Gumyr
-date: January 10th 2022
-
-desc: Unit tests for the map_texture sub-package of cq_warehouse
-
-license:
-
- Copyright 2022 Gumyr
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
-
-"""
-import unittest
-import math
-import cadquery as cq
-from cq_warehouse.map_texture import *
-
-
-def _assertTupleAlmostEquals(self, expected, actual, places, msg=None):
- """Check Tuples"""
- for i, j in zip(actual, expected):
- self.assertAlmostEqual(i, j, places, msg=msg)
-
-
-unittest.TestCase.assertTupleAlmostEquals = _assertTupleAlmostEquals
-
-
-class TestSupportFunctions(unittest.TestCase):
- def testToLocalWorldCoords(self):
- """Tests the toLocalCoords and toGlocalCoords methods"""
-
- # Test vector translation
- v1 = cq.Vector(1, 2, 0)
- p1 = cq.Plane.named("XZ")
- self.assertTupleAlmostEquals(
- p1.toLocalCoords(v1).toTuple(), (v1.x, v1.z, -v1.y), 3
- )
-
- # Test shape translation
- box1 = cq.Workplane("XY").box(2, 4, 8)
- box1_max_v = box1.vertices(">X and >Y and >Z").val() # (1.0, 2.0, 4.0)
- box2_max_v = (
- cq.Workplane(p1)
- .add(p1.toLocalCoords(box1.solids().val()))
- .vertices(">X and >Y and >Z")
- .val()
- ) # (1.0, 4.0, 2.0)
- self.assertTupleAlmostEquals(
- (box1_max_v.X, box1_max_v.Y, box1_max_v.Z),
- (box2_max_v.X, box2_max_v.Z, box2_max_v.Y),
- 3,
- )
-
- # Test bounding box translation
- bb1 = box1.solids().val().BoundingBox()
- bb2 = p1.toLocalCoords(bb1)
- self.assertTupleAlmostEquals((bb2.xmax, bb2.ymax, bb2.zmax), (1, 4, -2), 3)
-
- # Test for unsupported type (Location unsupported)
- with self.assertRaises(ValueError):
- p1.toLocalCoords(cq.Location(cq.Vector(1, 1, 1)))
-
- # Test vector translation back to world coordinates
- v2 = (
- cq.Workplane(p1)
- .lineTo(1, 2, 4)
- .vertices(">X and >Y and >Z")
- .val()
- .toTuple()
- ) # (1.0, 2.0, 4.0)
- v3 = p1.toWorldCoords(v2) # (1.0, 4.0, -2.0)
- self.assertTupleAlmostEquals(v2, (v3.x, v3.z, -v3.y), 3)
-
- def testGetSignedAngle(self):
- """Verify getSignedAngle calculations with and without a provided normal"""
- a = math.pi / 3
- v1 = cq.Vector(1, 0, 0)
- v2 = cq.Vector(math.cos(a), -math.sin(a), 0)
- d1 = v1.getSignedAngle(v2)
- d2 = v1.getSignedAngle(v2, cq.Vector(0, 0, 1))
- self.assertAlmostEqual(d1, a)
- self.assertAlmostEqual(d2, -a)
-
-
-class TestTextOnPath(unittest.TestCase):
- def testTextOnPath(self):
-
- # Verify a wire path on a object with cut
- box = cq.Workplane("XY").box(4, 4, 0.5)
- obj1 = (
- box.faces(">Z")
- .workplane()
- .circle(1.5)
- .textOnPath(
- "text on a circle", fontsize=0.5, distance=-0.05, cut=True, clean=False
- )
- )
- # combined object should have smaller volume
- self.assertGreater(box.val().Volume(), obj1.val().Volume())
-
- # Verify a wire path on a object with combine
- obj2 = (
- box.faces(">Z")
- .workplane()
- .circle(1.5)
- .textOnPath(
- "text on a circle",
- fontsize=0.5,
- distance=0.05,
- font="Sans",
- cut=False,
- combine=True,
- )
- )
- # combined object should have bigger volume
- self.assertLess(box.val().Volume(), obj2.val().Volume())
-
- # verify that the number of top faces & solids is correct (NB: this is font specific)
- self.assertEqual(len(obj2.faces(">Z").vals()), 14)
-
- # verify that the fox jumps over the dog
- dog = cq.Workplane("XY", origin=(50, 0, 0)).box(30, 30, 30, centered=True)
- fox = (
- cq.Workplane("XZ")
- .threePointArc((50, 30), (100, 0))
- .textOnPath(
- txt="The quick brown fox jumped over the lazy dog",
- fontsize=5,
- distance=1,
- start=0.1,
- cut=False,
- )
- )
- self.assertEqual(fox.val().intersect(dog.val()).Volume(), 0)
-
- # Verify that an edge or wire must be present
- with self.assertRaises(Exception):
- cq.Workplane("XY").textOnPath("error", 5, 1, 1)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/sprocket_and_chain_tests.py b/tests/sprocket_and_chain_tests.py
index fc87225..f603f7e 100644
--- a/tests/sprocket_and_chain_tests.py
+++ b/tests/sprocket_and_chain_tests.py
@@ -15,8 +15,7 @@
"""
import math
import unittest
-import pydantic
-import cadquery as cq
+from cadquery import Vector
from cq_warehouse.sprocket import Sprocket
from cq_warehouse.chain import Chain
@@ -59,9 +58,7 @@ def test_chain_input_parsing(self):
spkt_locations=[(0, 0), (1, 1)],
positive_chain_wrap=[True],
)
- with self.assertRaises(
- pydantic.error_wrappers.ValidationError
- ): # Invalid locations
+ with self.assertRaises(ValueError): # Invalid locations
Chain(
spkt_teeth=[10, 10],
spkt_locations=[(0, 0), 1],
@@ -73,28 +70,54 @@ def test_chain_input_parsing(self):
spkt_locations=[(0, 0), (0, 0)],
positive_chain_wrap=[True, False],
)
- with self.assertRaises(KeyError): # Invalid teeth
- Chain(spkt_teeth=[12.5, 6])
- with self.assertRaises(KeyError): # Too few sprockets
- Chain(spkt_teeth=[16])
- with self.assertRaises(
- pydantic.error_wrappers.ValidationError
- ): # Teeth not list
- Chain(spkt_teeth=12)
- with self.assertRaises(KeyError): # Too few locations
- Chain(spkt_locations=cq.Vector(0, 0, 0))
- with self.assertRaises(KeyError): # Wrap not a list
- Chain(positive_chain_wrap=True)
- with self.assertRaises(KeyError): # Wrap not bool
- Chain(positive_chain_wrap=["yes", "no"])
+ with self.assertRaises(ValueError): # Invalid teeth
+ Chain(
+ spkt_teeth=[12.5, 6],
+ spkt_locations=[(0, 0), (1, 0)],
+ positive_chain_wrap=[True, False],
+ )
+ with self.assertRaises(ValueError): # Too few sprockets
+ Chain(
+ spkt_teeth=[16],
+ spkt_locations=[(0, 0), (1, 0)],
+ positive_chain_wrap=[True, False],
+ )
+ with self.assertRaises(ValueError): # Teeth not list
+ Chain(
+ spkt_teeth=12,
+ spkt_locations=[(0, 0), (1, 0)],
+ positive_chain_wrap=[True, False],
+ )
+ with self.assertRaises(ValueError): # Too few locations
+ Chain(
+ spkt_teeth=[12, 12],
+ spkt_locations=Vector(0, 0, 0),
+ positive_chain_wrap=[True, False],
+ )
+ with self.assertRaises(ValueError): # Wrap not a list
+ Chain(
+ spkt_teeth=[12, 12],
+ spkt_locations=Vector(0, 0, 0),
+ positive_chain_wrap=True,
+ )
+ with self.assertRaises(ValueError): # Wrap not bool
+ Chain(
+ spkt_teeth=[12, 12],
+ spkt_locations=Vector(0, 0, 0),
+ positive_chain_wrap=["yes", "no"],
+ )
with self.assertRaises(ValueError): # Length mismatch
Chain(
spkt_teeth=[20, 20],
- spkt_locations=[cq.Vector(0, 0, 0), cq.Vector(20, 0, 0)],
+ spkt_locations=[Vector(0, 0, 0), Vector(20, 0, 0)],
positive_chain_wrap=[True],
)
- with self.assertRaises(KeyError): # Overlapping sprockets
- Chain(spkt_locations=[cq.Vector(0, 0, 0), cq.Vector(0, 0, 0)])
+ with self.assertRaises(ValueError): # Overlapping sprockets
+ Chain(
+ spkt_teeth=[12, 12],
+ spkt_locations=[Vector(0, 0, 0), Vector(0, 0, 0)],
+ positive_chain_wrap=[True, True],
+ )
class TestSprocketShape(unittest.TestCase):
@@ -110,7 +133,6 @@ def test_flat_sprocket_shape(self):
bore_diameter=80 * MM,
)
spkt_object = spkt.cq_object
- # cq.exporters.export(spkt_object,"sprocket_flat.step")
self.assertTrue(spkt_object.val().isValid())
# self.assertEqual(spkt.hashCode(),2035876455) # hashCode() isn't consistent
self.assertAlmostEqual(spkt_object.val().Area(), 16935.40667143173)
@@ -126,7 +148,6 @@ def test_spiky_sprocket_shape(self):
num_teeth=16, chain_pitch=0.5 * INCH, roller_diameter=0.49 * INCH
)
spkt_object = spkt.cq_object
- # cq.exporters.export(spkt_object,"sprocket_spiky.step")
self.assertTrue(spkt_object.val().isValid())
self.assertAlmostEqual(spkt_object.val().Area(), 5475.870128515104)
self.assertAlmostEqual(spkt_object.val().Volume(), 5124.246302618558)
@@ -234,11 +255,11 @@ def test_five_sprocket_chain(self):
spkt_teeth=[32, 10, 10, 10, 16],
positive_chain_wrap=[True, True, False, False, True],
spkt_locations=[
- cq.Vector(0, 158.9 * MM, 0),
- cq.Vector(+190 * MM, -50 * MM, 0),
- cq.Vector(+140 * MM, 20 * MM, 0),
- cq.Vector(+120 * MM, 90 * MM, 0),
- cq.Vector(+205 * MM, 158.9 * MM, 0),
+ Vector(0, 158.9 * MM, 0),
+ Vector(+190 * MM, -50 * MM, 0),
+ Vector(+140 * MM, 20 * MM, 0),
+ Vector(+120 * MM, 90 * MM, 0),
+ Vector(+205 * MM, 158.9 * MM, 0),
],
)
self.assertEqual(chain.num_rollers, len(roller_pos))
@@ -251,8 +272,8 @@ def test_missing_link(self):
Chain(
spkt_teeth=[32, 32],
spkt_locations=[
- cq.Vector(-4.9 * INCH, 0, 0),
- cq.Vector(+5 * INCH, 0, 0),
+ Vector(-4.9 * INCH, 0, 0),
+ Vector(+5 * INCH, 0, 0),
],
positive_chain_wrap=[True, True],
)
@@ -320,12 +341,12 @@ def test_assemble_chain_transmission(self):
chain = Chain(
spkt_teeth=[16, 16],
- spkt_locations=[cq.Vector(-3 * INCH, 40, 50), cq.Vector(+3 * INCH, 40, 50)],
+ spkt_locations=[Vector(-3 * INCH, 40, 50), Vector(+3 * INCH, 40, 50)],
positive_chain_wrap=[True, True],
)
- with self.assertRaises(pydantic.error_wrappers.ValidationError):
+ with self.assertRaises(ValueError):
chain.assemble_chain_transmission(spkt0.cq_object)
- with self.assertRaises(pydantic.error_wrappers.ValidationError):
+ with self.assertRaises(ValueError):
chain.assemble_chain_transmission([spkt0, spkt1.cq_object])
""" Validate a transmission assembly composed of two sprockets and a chain """
@@ -351,11 +372,6 @@ def test_assemble_chain_transmission(self):
self.assertEqual(transmission.children[0].name, "spkt0")
self.assertEqual(transmission.children[1].name, "spkt1")
self.assertEqual(transmission.children[2].name, "chain")
- # self.assertEqual(
- # transmission.children[0].loc,
- # cq.Location((-3*INCH,0,0),(1,0,0),90),
- # 7
- # )
class TestVectorMethods(unittest.TestCase):
@@ -363,9 +379,9 @@ class TestVectorMethods(unittest.TestCase):
def test_vector_rotate(self):
"""Validate vector rotate methods"""
- vector_x = cq.Vector(1, 0, 1).rotateX(45)
- vector_y = cq.Vector(1, 2, 1).rotateY(45)
- vector_z = cq.Vector(-1, -1, 3).rotateZ(45)
+ vector_x = Vector(1, 0, 1).rotateX(45)
+ vector_y = Vector(1, 2, 1).rotateY(45)
+ vector_z = Vector(-1, -1, 3).rotateZ(45)
self.assertTupleAlmostEquals(
vector_x.toTuple(), (1, -math.sqrt(2) / 2, math.sqrt(2) / 2), 7
)
@@ -374,27 +390,7 @@ def test_vector_rotate(self):
def test_vector_flip_y(self):
"""Validate vector flip of the xz plane method"""
- self.assertTupleAlmostEquals(
- cq.Vector(1, 2, 3).flipY().toTuple(), (1, -2, 3), 7
- )
-
- def test_point_to_vector(self):
- """Validate conversion of 2D points to 3D vectors"""
- point = cq.Vector(1, 2)
- with self.assertRaises(ValueError):
- point.pointToVector((1, 0, 0))
- with self.assertRaises(ValueError):
- point.pointToVector("x")
- self.assertTupleAlmostEquals(point.pointToVector("XY").toTuple(), (1, 2, 0), 7)
- self.assertTupleAlmostEquals(
- point.pointToVector("XY", 4).toTuple(), (1, 2, 4), 7
- )
- self.assertTupleAlmostEquals(
- point.pointToVector("XZ", 3).toTuple(), (1, 3, 2), 7
- )
- self.assertTupleAlmostEquals(
- point.pointToVector("YZ", 5).toTuple(), (5, 1, 2), 7
- )
+ self.assertTupleAlmostEquals(Vector(1, 2, 3).flipY().toTuple(), (1, -2, 3), 7)
if __name__ == "__main__":