Skip to content

Commit

Permalink
Merge pull request #64 from gumyr/subclass
Browse files Browse the repository at this point in the history
Subclass
  • Loading branch information
gumyr authored Sep 4, 2022
2 parents 580833a + 8ce5dd5 commit a307044
Show file tree
Hide file tree
Showing 26 changed files with 1,011 additions and 324 deletions.
8 changes: 6 additions & 2 deletions docs/bearing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ add new sizes or entirely new types of bearings.
Both metric and imperial sized standard bearings are directly supported by the bearing sub-package
although the majority of the bearings currently implemented are metric.

All of the fastener classes provide a ``cq_object`` instance variable which contains the cadquery
object.
.. deprecated:: 0.8.0

Previous versions of cq_warehouse required the used of a ``cq_object`` instance variable to access
the CadQuery cad object. Currently all bearing objects are a sub-class of the CadQuery Compound
object and therefore can be used as any other Compound object without referencing ``cq_object``.
Future versions of cq_warehouse will remove ``cq_object`` entirely.

The following sections describe each of the provided classes.

Expand Down
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"sphinx.ext.autodoc",
"sphinx_autodoc_typehints",
"sphinx.ext.doctest",
"sphinx.ext.graphviz",
]

# Napoleon settings
Expand Down Expand Up @@ -78,6 +79,9 @@
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]

# -- GraphViz configuration ----------------------------------
graphviz_output_format = "svg"


# -- Options for HTML output -------------------------------------------------

Expand Down
8 changes: 6 additions & 2 deletions docs/fastener.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,12 @@ without dramatically impacting performance.
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.
.. deprecated:: 0.8.0

Previous versions of cq_warehouse required the used of a ``cq_object`` instance variable to access
the CadQuery cad object. Currently all fastener objects are a sub-class of the CadQuery Solid
object and therefore can be used as any other Solid object without referencing ``cq_object``.
Future versions of cq_warehouse will remove ``cq_object`` entirely.

The following sections describe each of the provided classes.

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Table Of Contents
:maxdepth: 2

installation.rst
subclassing.rst
bearing.rst
chain.rst
drafting.rst
Expand Down
12 changes: 7 additions & 5 deletions docs/sprocket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ of python code using the :ref:`Sprocket <sprocket>` class:
from cq_warehouse.sprocket import Sprocket
sprocket32 = Sprocket(num_teeth=32)
cq.exporters.export(sprocket32.cq_object,"sprocket.step")
cq.exporters.export(sprocket32,"sprocket.step")
How does this code work?
Expand All @@ -21,10 +21,12 @@ How does this code work?
#. 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.
.. deprecated:: 0.8.0

Previous versions of cq_warehouse required the used of a ``cq_object`` instance variable to access
the CadQuery cad object. Currently sprocket objects are a sub-class of the CadQuery Solid
object and therefore can be used as any other Solid object without referencing ``cq_object``.
Future versions of cq_warehouse will remove ``cq_object`` entirely.

.. py:module:: sprocket
Expand Down
170 changes: 170 additions & 0 deletions docs/subclassing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#############################
Sub-classing CadQuery Objects
#############################
As CadQuery is a python CAD library it is based on the concept of classes. One
of the most important CadQuery base class is Shape which provides both an
OpenCascade object contained in the ``wrapped`` attribute and pure python
attributes such as ``forConstruction`` (a bool) and ``label`` (a str). Shape provides
a wealth of python methods to manipulate Shapes, such as ``rotate()`` and ``mirror()``.

Solid and Edge are two of the many sub-classes of Shape - more formally
Solid is a derived class of the Shape base class - which inherit the
methods of Shape. The cq_warehouse.extensions package contains
changes to the CadQuery core functionality which allows users to also create
derived classes from the Shape base class. These user derived classes also
inherit the wealth of Shape methods (also note that future additions to Shape
will automatically apply to user derived classes).

The cq_warehouse.fastener ``Nut`` class is an example of a derived class - specifically
of Solid and is defined as follows:

.. code-block:: python
class Nut(ABC, Solid):
and therefore inherits the methods of Solid and Shape. ``Nut`` is itself
a base class for a whole series of different classes of nuts like ``DomedCadNut``
which is defined as follows:

.. code-block:: python
class DomedCapNut(Nut):
Expressed as a diagram, the inheritance looks like this:

.. graphviz::

digraph {
splines=false;
Shape -> Solid;
Mixin3D -> Solid;
Solid -> Nut;
ABC -> Nut;
Nut -> BradTeeNut;
Nut -> DomedCapNut;
Nut -> HexNut;
Ellipsis [shape=none label="&#8230;" fontsize=30];
Nut -> SquareNut;
{rank=same BradTeeNut DomedCapNut HexNut Ellipsis SquareNut} // align on same rank
// invisible edges to order nodes
edge[style=invis]
BradTeeNut->DomedCapNut->HexNut->Ellipsis->SquareNut
}

.. note::
``Nut`` is also a sub-class of ``ABC`` the Abstract Base Class which provides
many useful features when creating derived classes. ``Mixin3D`` provides
a set of methods specific to 3D objects like ``chamfer()``.

Instantiation of one of the nut sub-classes is done as follows:

.. code-block:: python
nut = DomedCapNut(size="M6-1", fastener_type="din1587")
where ``nut`` is a subclass of Solid and therefore the many Solid methods
apply to ``nut`` like this:

.. code-block:: python
nut_translated = nut.translate((20, 20, 10))
Creating Custom Sub-Classes
===========================
To create custom sub-classes of Shape, there are three necessary steps and
one extra step for complex classes:

1. The class definition must include the base class, e.g: ``class MyCustomShape(Solid):``.
2. All parameters must be stored as instance attributes so a copy can be created.
3. The class' ``__init__`` method must initialize the base class
object: ``super().__init__(obj.wrapped)``. Recall that the Shape
class stores the OpenCascade CAD object in the ``wrapped`` attribute
which is what is passed into the ``__init__`` method.
4. Create a custom ``copy()`` method for complex classes only (see below).

Here is a working example of a ``FilletBox`` (i.e.a box with rounded corners)
that is a sub-class of Solid:

.. code-block:: python
class FilletBox(Solid):
"""A filleted box
A box of the given dimensions with all of the edges filleted.
Args:
length (float): box length
width (float): box width
height (float): box height
radius (float): edge radius
pnt (VectorLike, optional): minimum x,y,z point. Defaults to (0, 0, 0).
dir (VectorLike, optional): direction of height. Defaults to (0, 0, 1).
"""
def __init__(
self,
length: float,
width: float,
height: float,
radius: float,
pnt: VectorLike = (0, 0, 0),
dir: VectorLike = (0, 0, 1),
):
# Store the attributes so the object can be copied
self.length = length
self.width = width
self.height = height
self.radius = radius
self.pnt = pnt
self.dir = dir
# Create the object
obj = Solid.makeBox(length, width, height, pnt, dir)
obj = obj.fillet(radius, obj.Edges())
# Initialize the Solid class with the new OCCT object
super().__init__(obj.wrapped)
Internally, Shape has a ``copy()`` method that is able copy derived
classes with a single OpenCascade object stored in the ``wrapped`` attribute.
If a custom class contains attributes that can't be copied with the python
``copy.deepcopy()`` method, that class will need to contain a custom ``copy()``
method. This custom copy method can be based off the cq_warehouse extended
copy method shown here:

.. code-block:: python
from OCP.BRepBuilderAPI import BRepBuilderAPI_Copy
def copy(self: "Shape") -> "Shape":
"""
Creates a new object that is a copy of this object.
"""
# The wrapped object is a OCCT TopoDS_Shape which can't be pickled or copied
# with the standard python copy/deepcopy, so create a deepcopy 'memo' with this
# value already copied which causes deepcopy to skip it.
memo = {id(self.wrapped): BRepBuilderAPI_Copy(self.wrapped).Shape()}
copy_of_shape = copy.deepcopy(self, memo)
return copy_of_shape
Converting Compound to Solid
============================
When creating custom Solid sub-classed objects one my find that a Compound object
has been created instead of the desired Solid object (use the ``type(<my_object>)``
function to find the class of an object). As a Compound object is a fancy list
of other Shapes it is often possible to extract the desired Solid from the
Compound. The following code will check for this condition and extract the
Solid object for initialization of the base class:

.. code-block:: python
if isinstance(obj, Compound) and len(obj.Solids()) == 1:
super().__init__(obj.Solids()[0].wrapped)
else:
super().__init__(obj.wrapped)
where ``obj`` is the custom object created by this sub-class. If the Compound is
always generated by the custom class, the ``if`` check can be eliminated.

If the desired object is a Compound (e.g. cq_warehouse bearings) the class
should sub-class Compound and initialize the base (Compound) class in the
normal way.
22 changes: 6 additions & 16 deletions examples/chain_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ class TestCases(Enum):
spkt_locations=[Vector(-5 * INCH, 0, 0), Vector(+5 * INCH, 0, 0)],
)
sprocket_transmission = two_sprocket_chain.assemble_chain_transmission(
spkts=[spkt32.cq_object, spkt32.cq_object]
spkts=[spkt32, spkt32]
)

elif test_case == TestCases.TWO_SPROCKETS_ON_YZ:
#
# Create a set of example transmissions
print("Two sprockets on YZ plane example...")
spkt32_y = spkt32.cq_object.rotate((0, 0, 0), (0, 1, 0), 90)
spkt32_y = spkt32.rotate((0, 0, 0), (0, 1, 0), 90)
two_sprocket_chain = Chain(
spkt_teeth=[32, 32],
positive_chain_wrap=[True, True],
Expand Down Expand Up @@ -108,7 +108,7 @@ class TestCases(Enum):
],
)
sprocket_transmission = derailleur_chain.assemble_chain_transmission(
spkts=[spkt32.cq_object, spkt10.cq_object, spkt10.cq_object, spkt16.cq_object]
spkts=[spkt32, spkt10, spkt10, spkt16]
)
# sprocket_transmission.save("deraileur.step")

Expand All @@ -125,9 +125,7 @@ class TestCases(Enum):
)
# Align the sprockets to the oblique plane defined by the spkt locations
spkts_aligned = [
s.cq_object.val()._apply_transform(
derailleur_chain.chain_plane.rG.wrapped.Trsf()
)
s._apply_transform(derailleur_chain.chain_plane.rG.wrapped.Trsf())
for s in [spkt32, spkt10, spkt16]
]
sprocket_transmission = derailleur_chain.assemble_chain_transmission(
Expand All @@ -151,13 +149,7 @@ class TestCases(Enum):
],
)
sprocket_transmission = five_sprocket_chain.assemble_chain_transmission(
spkts=[
spkt32.cq_object,
spkt10.cq_object,
spkt10.cq_object,
spkt10.cq_object,
spkt16.cq_object,
]
spkts=[spkt32, spkt10, spkt10, spkt10, spkt16]
)
# sprocket_transmission.save("five_sprocket.step")

Expand All @@ -170,9 +162,7 @@ class TestCases(Enum):
spkt_locations=[(-5 * INCH, 0), (+5 * INCH, 0)],
)
sprocket_transmission = (
two_sprocket_chain.assemble_chain_transmission(
spkts=[spkt32.cq_object, spkt32.cq_object]
)
two_sprocket_chain.assemble_chain_transmission(spkts=[spkt32, spkt32])
.rotate(axis=(0, 1, 1), angle=45)
.translate((20, 20, 20))
)
Expand Down
45 changes: 23 additions & 22 deletions examples/sprocket_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,40 @@
from cq_warehouse.sprocket import Sprocket

MM = 1
INCH = 25.4*MM
INCH = 25.4 * MM
#
# Create a set of sprockets for these examples
print("Creating sprockets...")
sprocket32 = Sprocket(
num_teeth=32,
clearance = 0.05,
bolt_circle_diameter = 104*MM,
num_mount_bolts = 4,
mount_bolt_diameter = 10*MM,
bore_diameter = 80*MM
clearance=0.05,
bolt_circle_diameter=104 * MM,
num_mount_bolts=4,
mount_bolt_diameter=10 * MM,
bore_diameter=80 * MM,
)
sprocket10 = Sprocket(
num_teeth=10,
clearance = 0.05,
num_mount_bolts = 0,
bore_diameter = 5*MM
num_teeth=10, clearance=0.05, num_mount_bolts=0, bore_diameter=5 * MM
)
sprocket16 = Sprocket(
num_teeth=16,
num_mount_bolts = 6,
bolt_circle_diameter = 44*MM,
mount_bolt_diameter = 5*MM,
bore_diameter = 0
num_mount_bolts=6,
bolt_circle_diameter=44 * MM,
mount_bolt_diameter=5 * MM,
bore_diameter=0,
)
cq.exporters.export(sprocket32, "sprocket32.step")
cq.exporters.export(sprocket10, "sprocket10.step")
cq.exporters.export(sprocket16, "sprocket16.step")
print(
f"The first sprocket has {sprocket32.num_teeth} teeth and a pitch radius of {round(sprocket32.pitch_radius,1)}mm"
)
print(
f"The pitch radius of a bicycle sprocket with 48 teeth is {round(Sprocket.sprocket_pitch_radius(48,(1/2)*INCH),1)}mm"
)
cq.exporters.export(sprocket32.cq_object,'sprocket32.step')
cq.exporters.export(sprocket10.cq_object,'sprocket10.step')
cq.exporters.export(sprocket16.cq_object,'sprocket16.step')
print(f"The first sprocket has {sprocket32.num_teeth} teeth and a pitch radius of {round(sprocket32.pitch_radius,1)}mm")
print(f"The pitch radius of a bicycle sprocket with 48 teeth is {round(Sprocket.sprocket_pitch_radius(48,(1/2)*INCH),1)}mm")

# If running from within the cq-editor, show the sprockets
if "show_object" in locals():
show_object(sprocket32.cq_object,name="sprocket32")
show_object(sprocket10.cq_object,name="sprocket10")
show_object(sprocket16.cq_object,name="sprocket16")
show_object(sprocket32, name="sprocket32")
show_object(sprocket10, name="sprocket10")
show_object(sprocket16, name="sprocket16")
Loading

0 comments on commit a307044

Please sign in to comment.