Skip to content

Instantly share code, notes, and snippets.

@abudden
Created November 26, 2019 16:55
Show Gist options
  • Save abudden/66b5d8fa66fb40d4e3a379559499d371 to your computer and use it in GitHub Desktop.
Save abudden/66b5d8fa66fb40d4e3a379559499d371 to your computer and use it in GitHub Desktop.
cadquery model of a welding trolley
#!/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.
Not currently used.
"""
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')
.workplane() # centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'"
# thankfully in this case, the centre of mass is in the same place as the centre of the bounding box
.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')
.workplane() # centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'"
# thankfully in this case, the centre of mass is in the same place as the centre of the bounding box
# 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')
.workplane() # centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'"
# thankfully in this case, the centre of mass is in the same place as the centre of the bounding box
.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')
.workplane() # centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'"
# thankfully in this case, the centre of mass is in the same place as the centre of the bounding box
.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')
.workplane() # centerOption='CenterOfBoundBox' produces "AttributeError: 'TopoDS_Face' object has no attribute 'tesselate'"
# thankfully in this case, the centre of mass is in the same place as the centre of the bounding box
.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