Make your own Assembly
¶
An Assembly
is a container for 1 to many Part
and/or
nested Assembly
instances.
Today we’re going to make a toy car, and put it into an assembly.
This tutorial assumes you’ve been through the Make your own Part tutorial.
Its structure will look something like this:
car
├○ chassis
├─ front_axle
│ ├○ axle
│ ├○ left_wheel
│ └○ right_wheel
└─ rear_axle
├○ axle
├○ left_wheel
└○ right_wheel
Parts¶
For this assembly we’re going to need the following parts
- axle : a simple long thin cylinder
- wheel : another simple cylinder
- chassis : an extruded
cadquery.Workplane.polyline()
While making these parts, we’re going to design them based on how we want to use them, but also on how others may want to use them.
Wheel¶
The wheel’s origin will be the point it meets the axle.
It’s just a cylinder with a couterbore hole for a screw to hold the wheel onto its axle.
import cadquery
import cqparts
from cqparts.params import *
from cqparts.display import render_props, display
class Wheel(cqparts.Part):
# Parameters
width = PositiveFloat(10, doc="width of wheel")
diameter = PositiveFloat(30, doc="wheel diameter")
# default appearance
_render = render_props(template='wood_dark')
def make(self):
wheel = cadquery.Workplane('XY') \
.circle(self.diameter / 2).extrude(self.width)
hole = cadquery.Workplane('XY') \
.circle(2).extrude(self.width/2).faces(">Z") \
.circle(4).extrude(self.width/2)
wheel = wheel.cut(hole)
return wheel
def get_cutout(self, clearance=0):
# A cylinder with a equal clearance on every face
return cadquery.Workplane('XY', origin=(0, 0, -clearance)) \
.circle((self.diameter / 2) + clearance) \
.extrude(self.width + (2 * clearance))
Extra Function
The get_cutout
will be used later to alter the chassis to make room for
the wheel, it serves as a negative that will be cut away from another solid.
Tip
Remember: this is just a python class, so we can add any functions, or attributes that make our design better.
Result
wheel = Wheel()
display(wheel)
Axle¶
The vehicle’s axle is a cylinder with a diameter
and length
.
We’ll put a pilot hole in each end for a screw to hold the wheel on.
from cqparts.constraint import Mate
from cqparts.utils.geometry import CoordSystem
class Axle(cqparts.Part):
# Parameters
length = PositiveFloat(50, doc="axle length")
diameter = PositiveFloat(10, doc="axle diameter")
# default appearance
_render = render_props(color=(50, 50, 50)) # dark grey
def make(self):
axle = cadquery.Workplane('ZX', origin=(0, -self.length/2, 0)) \
.circle(self.diameter / 2).extrude(self.length)
cutout = cadquery.Workplane('ZX', origin=(0, -self.length/2, 0)) \
.circle(1.5).extrude(10)
axle = axle.cut(cutout)
cutout = cadquery.Workplane('XZ', origin=(0, self.length/2, 0)) \
.circle(1.5).extrude(10)
axle = axle.cut(cutout)
return axle
# wheel mates, assuming they rotate around z-axis
@property
def mate_left(self):
return Mate(self, CoordSystem(
origin=(0, -self.length / 2, 0),
xDir=(1, 0, 0), normal=(0, -1, 0),
))
@property
def mate_right(self):
return Mate(self, CoordSystem(
origin=(0, self.length / 2, 0),
xDir=(1, 0, 0), normal=(0, 1, 0),
))
def get_cutout(self, clearance=0):
return cadquery.Workplane('ZX', origin=(0, -self.length/2 - clearance, 0)) \
.circle((self.diameter / 2) + clearance) \
.extrude(self.length + (2 * clearance))
We could have simply drawn a circle and extrude it, right?
The problem with that design is its implementation will involve translating it by \(\frac{length}{2}\), and rotating \(90°\) so the \(Z\) axis is along the world \(Y\) axis.
Although that would not be difficult to do, it is messy.
Instead we’ve aligned the axle along the \(Y\) axis, with the origin at the
cylinder’s center, implemented in the make()
function above.
Mates
A Mate
is a relative coordinate system
used to connect parts in an assembly. We’ll see how that works when we make
our first assembly later in this tutorial.
When assembling an axle we’ll want to attach a wheel to each end. They’re defined
above as mate_left
and mate_right
.
Note that the normal
for a Mate
is (0, 0, 1)
by default \(+Z\) axis. Changing this to the \(\pm Y\)
axis for left/right rotates the wheel so that wheel’s \(+Z\) axis is
aligned accordingly.
Extra Functions
like the Wheel
, we’ve added a get_cutout
which will be used later.
Result
axle = Axle()
display(axle)
Chassis¶
The chassis is a relatively complex shape that could have many
parameters. For the sake of simplicity in this tutorial, we’re going to make
it a static face using polyline()
with a
variable extrude()
width.
class Chassis(cqparts.Part):
# Parameters
width = PositiveFloat(50, doc="chassis width")
_render = render_props(template='wood_light')
def make(self):
points = [ # chassis outline
(-60,0),(-60,22),(-47,23),(-37,40),
(5,40),(23,25),(60,22),(60,0),
]
return cadquery.Workplane('XZ', origin=(0,self.width/2,0)) \
.moveTo(*points[0]).polyline(points[1:]).close() \
.extrude(self.width)
Result
chassis = Chassis()
display(chassis)
Wheel Assembly¶
We finally have all the parts we’ll need, let’s make our first assembly.
from cqparts.constraint import Fixed, Coincident
class WheeledAxle(cqparts.Assembly):
left_width = PositiveFloat(7, doc="left wheel width")
right_width = PositiveFloat(7, doc="right wheel width")
left_diam = PositiveFloat(25, doc="left wheel diameter")
right_diam = PositiveFloat(25, doc="right wheel diameter")
axle_diam = PositiveFloat(8, doc="axel diameter")
axle_track = PositiveFloat(50, doc="distance between wheel tread midlines")
wheel_clearance = PositiveFloat(3, doc="distance between wheel and chassis")
def make_components(self):
axel_length = self.axle_track - (self.left_width + self.right_width) / 2
return {
'axle': Axle(length=axel_length, diameter=self.axle_diam),
'left_wheel': Wheel(
width=self.left_width, diameter=self.left_diam,
),
'right_wheel': Wheel(
width=self.right_width, diameter=self.right_diam,
),
}
def make_constraints(self):
return [
Fixed(self.components['axle'].mate_origin, CoordSystem()),
Coincident(
self.components['left_wheel'].mate_origin,
self.components['axle'].mate_left
),
Coincident(
self.components['right_wheel'].mate_origin,
self.components['axle'].mate_right
),
]
def apply_cutout(self, part):
# Cut wheel & axle from given part
axle = self.components['axle']
left_wheel = self.components['left_wheel']
right_wheel = self.components['right_wheel']
local_obj = part.local_obj
local_obj = local_obj \
.cut((axle.world_coords - part.world_coords) + axle.get_cutout()) \
.cut((left_wheel.world_coords - part.world_coords) + left_wheel.get_cutout(self.wheel_clearance)) \
.cut((right_wheel.world_coords - part.world_coords) + right_wheel.get_cutout(self.wheel_clearance))
part.local_obj = local_obj
Parameters
Just like a Part
, the Assembly
class makes use of parameters.
These parameters can be passed directly or indirectly to sub-assemblies, or
to child parts. Note how axel_track
is used to define the axle’s length.
Components
The Assembly.make_components()
method is overridden above, to return
a dict
of named Component
instances.
Each component requires a name that’s unique for the particular assembly.
Components may be added, or omitted based on the assembly’s parameters if necessary.
Constraints
The Assembly.make_constraints()
method is overridden to return a
list
of Constraint
instances.
Each constraint references, at least:
- the component being constrained.
- parameter(s) defining how it is to be constrained.
see Constraints for more details.
Extra Functions
We’ve added apply_cutout
as a utility for the next stage of assembly.
It will subtract geometry away from a given Part
to allow placement
of the axle, and freedom for the wheels to spin.
The apply_cutout
method is not part of the normal assembly build cycle; at
this stage it’s simply another method in the class.
Result
# wheels made asymmetrical to show that the
# 2 wheels are independently created.
wheeled_axle = WheeledAxle(right_width=2)
display(wheeled_axle)
Composition Tree
To see the hierarchy of what we’ve just made, we can also run:
>>> print(wheeled_axle.tree_str(name='wheel_assembly'))
wheel_assembly
├○ axle
├○ left_wheel
└○ right_wheel
Car Assembly¶
And finally we combine eveything above into a car!
class Car(cqparts.Assembly):
# Parameters
wheelbase = PositiveFloat(70, "distance between front and rear axles")
axle_track = PositiveFloat(60, "distance between tread midlines")
# wheels
wheel_width = PositiveFloat(10, doc="width of all wheels")
front_wheel_diam = PositiveFloat(30, doc="front wheel diameter")
rear_wheel_diam = PositiveFloat(30, doc="rear wheel diameter")
axle_diam = PositiveFloat(10, doc="axle diameter")
def make_components(self):
return {
'chassis': Chassis(width=self.axle_track),
'front_axle': WheeledAxle(
left_width=self.wheel_width,
right_width=self.wheel_width,
left_diam=self.front_wheel_diam,
right_diam=self.front_wheel_diam,
axle_diam=self.axle_diam,
axle_track=self.axle_track,
),
'rear_axle': WheeledAxle(
left_width=self.wheel_width,
right_width=self.wheel_width,
left_diam=self.rear_wheel_diam,
right_diam=self.rear_wheel_diam,
axle_diam=self.axle_diam,
axle_track=self.axle_track,
),
}
def make_constraints(self):
return [
Fixed(self.components['chassis'].mate_origin),
Coincident(
self.components['front_axle'].mate_origin,
Mate(self.components['chassis'], CoordSystem((self.wheelbase/2,0,0))),
),
Coincident(
self.components['rear_axle'].mate_origin,
Mate(self.components['chassis'], CoordSystem((-self.wheelbase/2,0,0))),
),
]
def make_alterations(self):
# cut out wheel wells
chassis = self.components['chassis']
self.components['front_axle'].apply_cutout(chassis)
self.components['rear_axle'].apply_cutout(chassis)
car = Car()
display(car)
And we can also view its hierarchy with:
>>> print(car.tree_str(name='car'))
car
├○ chassis
├─ front_axle
│ ├○ axle
│ ├○ left_wheel
│ └○ right_wheel
└─ rear_axle
├○ axle
├○ left_wheel
└○ right_wheel
Note that the components in this assembly are both Part
and
Assembly
instances. The nested assemblies gives us the 2nd layer of
parts.
Also note that we have 2 of the same Assembly
class, but 2 different
instances, much like the 2 wheels in the previous WheeledAxle
assembly.
The chassis was altered after the WeeledAxle
assemblies were placed using
the apply_cutout
utility methods build into WeeledAxle
class. The
finished result being:
display(car.find('chassis'))