Created
November 27, 2019 07:25
-
-
Save abudden/37e7e6a3c647b58a0646b047f40862d0 to your computer and use it in GitHub Desktop.
cadquery model of a welding trolley, with error lines included.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# vim: set fileencoding=utf-8 : | |
import itertools | |
import cadquery as cq | |
def exportStep(object_list, filename): | |
""" | |
Simple worker function to take a list of objects and export a single step file with all objects in place. | |
""" | |
vals = list(itertools.chain(*[o.vals() for obj in object_list for o in obj.all()])) | |
compound = cq.Compound.makeCompound(vals) | |
compound.exportStep(filename) | |
def draw_angle(obj, angle_size, angle_thickness, orientation): | |
""" | |
Attempt to make a generic function for drawing the cross section of some equal-angle. | |
""" | |
if orientation.lower() == 'southeast': | |
result = (obj.hLine(angle_size).vLine(-angle_thickness) | |
.hLine(-(angle_size-angle_thickness)).vLine(-(angle_size-angle_thickness)) | |
.hLine(-angle_thickness).vLine(angle_size) | |
.close() | |
) | |
elif orientation.lower() == 'southwest': | |
result = (obj.hLine(-angle_size).vLine(-angle_thickness) | |
.hLine((angle_size-angle_thickness)).vLine(-(angle_size-angle_thickness)) | |
.hLine(angle_thickness).vLine(angle_size) | |
.close() | |
) | |
elif orientation.lower() == 'northeast': | |
result = (obj.hLine(angle_size).vLine(angle_thickness) | |
.hLine(-(angle_size-angle_thickness)).vLine((angle_size-angle_thickness)) | |
.hLine(-angle_thickness).vLine(-angle_size) | |
.close() | |
) | |
elif orientation.lower() == 'northwest': | |
result = (obj.hLine(-angle_size).vLine(angle_thickness) | |
.hLine((angle_size-angle_thickness)).vLine((angle_size-angle_thickness)) | |
.hLine(angle_thickness).vLine(-angle_size) | |
.close() | |
) | |
else: | |
raise Exception("Unknown orientation") | |
return result | |
# Dimensions of various parts of the system | |
# Frame constructed from box section steel: | |
box_size, box_thickness = 30.0, 2.0 | |
# General use angle-iron size | |
top_angle_size, top_angle_thickness = 30.0, 3.0 | |
# Lower profile angle-iron for supporting the water cooler | |
cooler_support_angle_size, cooler_support_angle_thickness = 25.0, 3.0 | |
# Allow this much space when butting anything up to the inside corner of angle-iron as it's usually filleted | |
angle_corner_allowance = 3.0 | |
# Dimensions of the TIG welder | |
welder_h, welder_w, welder_d = 438.0, 240.0, 550.0 | |
# Dimensions of the water cooler | |
cooler_h, cooler_w, cooler_d = 370.0, 260.0, 495.0 # includes handle, feet and connectors | |
# Dimensions of the feet of the water cooler | |
cooler_feet_from_front = 28.0 | |
cooler_feet_from_sides = 4.0 | |
cooler_feet_from_back = 75.0 | |
cooler_feet_size = 33.0 | |
cooler_feet_height = 25.0 | |
cooler_clearance = 5.0 # Clearance around cooler for box section so it can be inserted through the gap | |
# Minimum clearance above the handle of the water to the TIG welder | |
cooler_top_clearance = 15.0 | |
# Centre of the rectangle forming the feet (which are offset from the centre of the water cooler) | |
cooler_offset_centre = (cooler_feet_from_front-cooler_feet_from_back)/2.0 | |
# Trolley width is cooler width plus clearance plus the box size | |
trolley_w = cooler_w+2*(cooler_clearance+box_size) | |
# Ignore the above, just make it the maximum width that will fit under my bench | |
trolley_w = 460.0 | |
# Depth needs to be the bigger of: | |
# - Cooler depth plus clearance plus the box section size front and back | |
# - The TIG welder depth plus the size of the angle on which it sits | |
trolley_d = max(cooler_d+2*(cooler_clearance+box_size), welder_d+2*(top_angle_thickness+angle_corner_allowance)) | |
# Height is calculated from the cooler height + clearance top and bottom assuming | |
# the cooler sits within the box section. | |
trolley_h = cooler_h + cooler_top_clearance + cooler_support_angle_thickness | |
############################# | |
# Start of the actual model # | |
############################# | |
###### Model the Water Cooler ###### | |
# Length is in X (object width), Width in Y (object depth), Height in Z (object height) | |
# Cooler is offset to the right-hand side of the trolley | |
cooler = (cq.Workplane('XY', origin=((trolley_w/2.0)-(box_size+(cooler_w/2.0)), box_size+cooler_clearance, -(cooler_h+cooler_top_clearance-cooler_feet_height))) | |
# Create the initial box shape, excluding the feet: | |
.box(length=cooler_w, width=cooler_d, height=cooler_h-cooler_feet_height, centered=(True, False, False)) | |
# Get the face that's as far as possible in the Z plane | |
.faces('<Z').workplane(centerOption='CenterOfBoundBox') | |
# Offset the centre of the sketch to compensate for the offset feet | |
.center(0, -cooler_offset_centre) | |
# Draw a rectangle for construction | |
.rect(xLen=cooler_w-(2*(cooler_feet_from_sides+(cooler_feet_size/2.0))), | |
yLen=cooler_d-(cooler_feet_size+cooler_feet_from_front+cooler_feet_from_back), | |
forConstruction=True) | |
# At each vertex of the rectangle | |
.vertices() | |
# Draw a box to represent the feet | |
.box(length=cooler_feet_size, | |
width=cooler_feet_size, | |
height=cooler_feet_height, | |
centered=(True, True, False)) | |
) | |
###### Model the TIG Welder ###### | |
# The welder is a simple box, but offset its position so that it's up against the | |
# right-hand box section. Could move it further if using angle for the right-hand support | |
welder = (cq.Workplane('XY', origin=(trolley_w/2.0-(welder_w+box_size), top_angle_thickness+angle_corner_allowance, top_angle_thickness)) | |
.box(length=welder_w, width=welder_d, height=welder_h, | |
centered=(False, False, False)) | |
) | |
###### Model the Trolley ###### | |
## Start with the basic box-section frame, made up of 12 pieces of box-section steel to form a cuboid | |
# For simplicity, model the box section as solid square bar | |
trolley = (cq.Workplane('XY').center(0, trolley_d/2.0) | |
# Start with two rectangles to extrude the top frame | |
.rect(xLen=trolley_w, yLen=trolley_d) | |
.rect(xLen=trolley_w-2*box_size, yLen=trolley_d-2*box_size) | |
.extrude(-box_size) | |
# Now get the bottom face | |
.faces('<Z').workplane(centerOption='CenterOfBoundBox') | |
# Draw a construction rectangle to get the vertices at the middle of each upright | |
.rect(xLen=trolley_w-box_size, yLen=trolley_d-box_size, forConstruction=True) | |
.vertices() | |
# Draw the box section (as square bar) for the uprights | |
.box(box_size, box_size, trolley_h-2*box_size, centered=(True, True, False)) | |
# Get the bottom face and draw another two rectangles for the bottom frame | |
.faces('<Z') | |
# centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'" | |
.workplane(centerOption='CenterOfBoundBox') | |
.rect(xLen=trolley_w, yLen=trolley_d) | |
.rect(xLen=trolley_w-2*box_size, yLen=trolley_d-2*box_size) | |
.extrude(box_size) | |
# Fillet the edges to represent the box form | |
.fillet(4) | |
) | |
# When drawing the angle-iron polyline, it's useful to have the far edge of the | |
# feet relative to the mid-point of the two feet | |
foot_edge = (cooler_d/2.0) - (cooler_feet_from_back + cooler_offset_centre) | |
# Now add some angle-iron supports for the water cooler feet to sit on. | |
trolley = (trolley | |
# Get an inside face with normal in the +X direction. This should get four | |
# faces, so filter for the one with the lowest Z midpoint. | |
.faces("+X").faces('<X').faces('<Z').workplane(centerOption='CenterOfBoundBox') | |
# Offset the centre for the corner of the feet and the bottom of the box section | |
.center(cooler_offset_centre, -box_size/2.0) | |
# Draw two angle iron cross-sections and close the polyline | |
.polyline([ | |
(foot_edge+angle_corner_allowance+cooler_support_angle_thickness, 0), | |
(foot_edge+angle_corner_allowance+cooler_support_angle_thickness, cooler_support_angle_size), | |
(foot_edge+angle_corner_allowance, cooler_support_angle_size), | |
(foot_edge+angle_corner_allowance, cooler_support_angle_thickness), | |
(foot_edge+angle_corner_allowance-(cooler_support_angle_size-cooler_support_angle_thickness), cooler_support_angle_thickness), | |
(foot_edge+angle_corner_allowance-(cooler_support_angle_size-cooler_support_angle_thickness), 0), | |
(foot_edge+angle_corner_allowance+cooler_support_angle_thickness, 0), | |
(-(foot_edge+angle_corner_allowance+cooler_support_angle_thickness), 0), | |
(-(foot_edge+angle_corner_allowance+cooler_support_angle_thickness), cooler_support_angle_size), | |
(-(foot_edge+angle_corner_allowance), cooler_support_angle_size), | |
(-(foot_edge+angle_corner_allowance), cooler_support_angle_thickness), | |
(-(foot_edge+angle_corner_allowance-(cooler_support_angle_size-cooler_support_angle_thickness)), cooler_support_angle_thickness), | |
(-(foot_edge+angle_corner_allowance-(cooler_support_angle_size-cooler_support_angle_thickness)), 0), | |
(-(foot_edge+angle_corner_allowance+cooler_support_angle_thickness), 0), | |
]).close() | |
# Extrude to join the opposite wall | |
.extrude(trolley_w-(2.0*box_size)) | |
) | |
# Now we can add some arms at the top of the trolley | |
trolley_support_height = 370.0 | |
trolley_support_outer_radius = 99.0 # Chosen to suit material available | |
trolley_support_inner_radius = trolley_support_outer_radius-box_size | |
# Add a support on the RHS of the welder, which can be copied for the same on the LHS of the trolley | |
right_arm = (trolley | |
# Select the face of the right-hand side of the upper right beam | |
.faces('>X').workplane(centerOption='CenterOfBoundBox') | |
# Make the centre (start point) as the top left-corner of the face | |
.center(-trolley_d/2.0, trolley_h/2.0) | |
# Now draw a line point-by-point to produce the shape of the arm | |
.moveTo(0.0, 0.0) | |
.lineTo(0.0, trolley_support_height-trolley_support_outer_radius) | |
.radiusArc((trolley_support_outer_radius, trolley_support_height), trolley_support_outer_radius) | |
.lineTo(trolley_d, trolley_support_height) | |
.lineTo(trolley_d, 0.0) | |
.lineTo(trolley_d-box_size, 0.0) | |
.lineTo(trolley_d-box_size, trolley_support_height-box_size) | |
.lineTo(trolley_support_inner_radius+box_size, trolley_support_height-box_size) | |
# Go back to the start so we can draw the second radius in a clockwise dimension | |
.moveTo(0.0, 0.0) | |
.lineTo(box_size, 0.0) | |
.lineTo(box_size, trolley_support_height - (box_size + trolley_support_inner_radius)) | |
.radiusArc((box_size+trolley_support_inner_radius, trolley_support_height-box_size), trolley_support_inner_radius) | |
.moveTo(0.0, 0.0) | |
# Now extrude | |
.close() | |
.extrude(-box_size, combine=False) | |
) | |
# Duplicate the right arm, translated to the other side of the trolley | |
left_arm = (right_arm | |
.translate((-(trolley_w-box_size), 0.0, 0.0)) | |
) | |
# Merge with the trolley | |
trolley = trolley.union(right_arm).union(left_arm) | |
# Add angle section to restrain the welder front-to-back; this is just the one at the front of the trolley | |
welder_contain_angle = (trolley | |
# This is a really nasty way to select the correct face, but I can't get the '<Z[0]' thing working | |
.faces('-X').faces('>X').faces('not (>Z)').faces('>Z').workplane(centerOption='CenterOfBoundBox') | |
# At the front: | |
.moveTo(trolley_d/2.0, box_size/2.0) | |
#.each(lambda x: draw_angle(x, top_angle_size, top_angle_thickness, 'southeast'), useLocalCoordinates=True) | |
.line(0.0, top_angle_size) | |
.line(-top_angle_thickness, 0.0) | |
.line(0.0, top_angle_thickness-top_angle_size) | |
.line(top_angle_thickness-top_angle_size, 0.0) | |
.line(0.0, -top_angle_thickness) | |
.line(top_angle_size, 0.0) | |
.close() | |
# And extrude... | |
.extrude(welder_w, combine=False) | |
) | |
# End plate on the end of the angle to constrain the welder in X | |
plate_thickness = 3.0 | |
welder_contain_angle = (welder_contain_angle | |
.faces('-X') | |
.faces('<Y') | |
.workplane(centerOption='CenterOfBoundBox') | |
.rect(xLen=top_angle_size, yLen=top_angle_size) | |
.extrude(plate_thickness) | |
) | |
# Now mirror the angle and end-plate to make a second instance at the back of the trolley | |
welder_contain_angle_2 = (welder_contain_angle | |
.mirror('XZ', (0.0, trolley_d/2.0, 0.0)) | |
) | |
# Merge with the trolley | |
trolley = trolley.union(welder_contain_angle).union(welder_contain_angle_2) | |
###### Removable storage shelf unit for top of trolley ###### | |
# Now model something that uses the storage area beside the welder. | |
storage_gap_d = trolley_d | |
storage_gap_w = trolley_w - ((2*box_size) + welder_w + plate_thickness) | |
# Make a frame out of angle-iron with the front and back angles facing down (to hold the unit in place on the trolley) | |
storage_unit = (trolley | |
# This is a really nasty way to select the correct face, but I can't get the '<Z[0]' thing working | |
.faces('+Z').faces('not (<Z)').faces('not (<Z)').faces('not (<Z)').faces('<Z').faces('not (<X)').faces('<X') | |
# centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'" | |
.workplane(centerOption='CenterOfBoundBox') | |
# Draw the frame made by four lengths of angle, two upwards, two downwards | |
.rect(xLen=storage_gap_w, yLen=storage_gap_d+(2*top_angle_thickness)) | |
.rect(xLen=storage_gap_w - (2*top_angle_size), yLen=storage_gap_d + (2*top_angle_thickness) - (2*top_angle_size)) | |
.extrude(top_angle_thickness, combine=False) | |
# Select the bottom face and complete the downward-facing angle | |
.faces('<Z') | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(0, (storage_gap_d/2.0) + (top_angle_thickness/2.0)) | |
.lineTo(0, -((storage_gap_d/2.0) + (top_angle_thickness/2.0)), forConstruction=True) | |
.vertices() | |
.rect(xLen=storage_gap_w, yLen=top_angle_thickness) | |
.extrude((top_angle_size-top_angle_thickness)) | |
# Select the top face and complete the upward-facing angle | |
.faces('>Z') | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(-((storage_gap_w/2.0)-(top_angle_thickness/2.0)), 0.0) | |
.lineTo(+((storage_gap_w/2.0)-(top_angle_thickness/2.0)), 0.0, forConstruction=True) | |
.vertices() | |
.rect(xLen=top_angle_thickness, yLen=storage_gap_d) | |
.extrude(top_angle_size-top_angle_thickness) | |
) | |
# Make uprights at the rear of the storage unit out of angle-iron. | |
upright_left = (storage_unit | |
# Now draw angle uprights at the back | |
.faces('+Z').faces('not (>Z)').faces('>Z') | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(-storage_gap_w/2.0, (storage_gap_d/2.0)) | |
.hLine(top_angle_size).vLine(-top_angle_thickness) | |
.hLine(-(top_angle_size-top_angle_thickness)).vLine(-(top_angle_size-top_angle_thickness)) | |
.hLine(-top_angle_thickness).vLine(top_angle_size) | |
.close() | |
.extrude(trolley_support_height-top_angle_thickness, combine=False) | |
) | |
# Possibly could have done this by using "mirror", but selecting the correct mirror plane (the midpoint of the storage unit) felt too difficult | |
upright_right = (storage_unit | |
# Now draw angle uprights at the back | |
.faces('+Z').faces('not (>Z)').faces('>Z') | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(+storage_gap_w/2.0, (storage_gap_d/2.0))#+top_angle_thickness) | |
.hLine(-top_angle_size).vLine(-top_angle_thickness) | |
.hLine((top_angle_size-top_angle_thickness)).vLine(-(top_angle_size-top_angle_thickness)) | |
.hLine(top_angle_thickness).vLine(top_angle_size) | |
.close() | |
.extrude(trolley_support_height-top_angle_thickness, combine=False) | |
) | |
storage_unit = storage_unit.union(upright_left).union(upright_right) | |
# Now add some uprights at the front; make these out of flat bar to ensure access to the full width of the storage unit | |
flat_bar_t = 3.0 | |
flat_bar_w = 25.0 | |
storage_unit = (storage_unit | |
.faces('-Y').faces('not (<Y)').faces('<Y') | |
# centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'" | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(-((storage_gap_w/2.0)-(flat_bar_t/2.0)), | |
(trolley_support_height-top_angle_thickness)/2.0) | |
.hLine(storage_gap_w-flat_bar_t, forConstruction=True) | |
.vertices() | |
.rect(xLen=flat_bar_t, yLen=trolley_support_height-(top_angle_size)) | |
.extrude(-flat_bar_w) | |
) | |
# Use angle along the left and right edge at the top: | |
# Front-to-back angle at top | |
su_top_left = (storage_unit | |
.faces('-Y').faces('not (<Y)').faces('<Y') | |
# centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'" | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(0.0, (trolley_support_height-top_angle_thickness)/2.0) | |
.move(-storage_gap_w/2.0, 0.0) | |
.hLine(top_angle_size).vLine(-top_angle_thickness) | |
.hLine(-(top_angle_size-top_angle_thickness)).vLine(-(top_angle_size-top_angle_thickness)) | |
.hLine(-top_angle_thickness).vLine(top_angle_size) | |
.close() | |
.extrude(-storage_gap_d, combine=False) | |
) | |
# Again could have used mirror if I better understood plane selection | |
su_top_right = (storage_unit | |
.faces('-Y').faces('not (<Y)').faces('<Y') | |
# centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'" | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(0.0, (trolley_support_height-top_angle_thickness)/2.0) | |
.move(storage_gap_w/2.0, 0.0) | |
.hLine(-top_angle_size).vLine(-top_angle_thickness) | |
.hLine((top_angle_size-top_angle_thickness)).vLine(-(top_angle_size-top_angle_thickness)) | |
.hLine(top_angle_thickness).vLine(top_angle_size) | |
.close() | |
.extrude(-storage_gap_d, combine=False) | |
) | |
storage_unit = storage_unit.union(su_top_left).union(su_top_right) | |
# Now some angle sections along the front and back at the top | |
su_back = (storage_unit | |
.faces('<X') | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(-storage_gap_d/2.0, (trolley_support_height+top_angle_size-top_angle_thickness)/2.0) | |
.hLine(top_angle_size).vLine(-top_angle_thickness) | |
.hLine(-(top_angle_size-top_angle_thickness)).vLine(-(top_angle_size-top_angle_thickness)) | |
.hLine(-top_angle_thickness).vLine(top_angle_size) | |
.close() | |
.extrude(-storage_gap_w, combine=False) | |
) | |
# Again could have used mirror if I better understood plane selection | |
su_front = (storage_unit | |
.faces('<X') | |
.workplane(centerOption='CenterOfBoundBox') | |
.moveTo(storage_gap_d/2.0, (trolley_support_height+top_angle_size-top_angle_thickness)/2.0) | |
.hLine(-top_angle_size).vLine(-top_angle_thickness) | |
.hLine((top_angle_size-top_angle_thickness)).vLine(-(top_angle_size-top_angle_thickness)) | |
.hLine(top_angle_thickness).vLine(top_angle_size) | |
.close() | |
.extrude(-storage_gap_w, combine=False) | |
) | |
storage_unit = storage_unit.union(su_back).union(su_front) | |
# If in CQ-Editor GUI: | |
if 'show_object' in globals(): | |
show_object = globals()['show_object'] # Does nothing, but gets rid of a pylint error | |
show_object(cooler) | |
show_object(welder) | |
show_object(trolley) | |
show_object(storage_unit) | |
else: | |
# Otherwise just export the various objects | |
exportStep([cooler, welder, trolley, storage_unit], 'weldingtrolley2.step') | |
print("") | |
print("Trolley Base Frame: %d mm W × %d mm D × %d mm H" % (trolley_w, trolley_d, trolley_h)) | |
print("Storage frame size: %d mm W × %d mm D" % (storage_gap_w, storage_gap_d)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment