ktongue's picture
download
raw
8.04 kB
import numpy as np
import cadquery as cq
from cadquery_massembly import MAssembly, relocate
from jupyter_cadquery import web_color
from jupyter_cadquery.viewer.client import show
from jupyter_cadquery.animation import Animation
# Parts
thickness = 2
height = 40
width = 65
length = 100
diam = 4
tol = 0.05
def create_base(rotate=False):
x1, x2 = 0.63, 0.87
base_holes = {
"right_back": (-x1 * length, -x1 * width),
"right_middle": (0, -x2 * width),
"right_front": (x1 * length, -x1 * width),
"left_back": (-x1 * length, x1 * width),
"left_middle": (0, x2 * width),
"left_front": (x1 * length, x1 * width),
}
stand_holes = {"front_stand": (0.75 * length, 0), "back_stand": (-0.8 * length, 0)}
workplane = cq.Workplane()
if rotate:
workplane = workplane.transformed(rotate=(30, 45, 60))
base = (
workplane.ellipseArc(length, width, 25, -25, startAtCurrent=False)
.close()
.pushPoints(list(base_holes.values()))
.circle(diam / 2 + tol)
.moveTo(*stand_holes["back_stand"])
.rect(thickness + 2 * tol, width / 2 + 2 * tol)
.moveTo(*stand_holes["front_stand"])
.rect(thickness + 2 * tol, width / 2 + 2 * tol)
.extrude(thickness)
)
base
# tag mating points
if rotate:
l_coord = lambda vec2d: workplane.plane.toWorldCoords(vec2d).toTuple()
l_nps = lambda vec2d: cq.NearestToPointSelector(l_coord(vec2d))
base.faces(f"<{l_coord((0,0,1))}").tag("bottom")
base.faces(f">{l_coord((0,0,1))}").tag("top")
for name, hole in base_holes.items():
base.faces(f"<{l_coord((0,0,1))}").edges(l_nps(hole)).tag(name)
for name, hole in stand_holes.items():
base.faces(f"<{l_coord((0,0,1))}").wires(l_nps(hole)).tag(name)
else:
base.faces("<Z").tag("bottom")
base.faces(">Z").tag("top")
for name, hole in base_holes.items():
base.faces("<Z").wires(cq.NearestToPointSelector(hole)).tag(name)
for name, hole in stand_holes.items():
base.faces("<Z").wires(cq.NearestToPointSelector(hole)).tag(name)
return base
base_holes_names = {
"right_back",
"right_middle",
"right_front",
"left_back",
"left_middle",
"left_front",
}
def create_stand():
stand = cq.Workplane().box(height, width / 2 + 10, thickness)
inset = cq.Workplane().box(thickness, width / 2, thickness)
backing = cq.Workplane("ZX").polyline([(10, 0), (0, 0), (0, 10)]).close().extrude(thickness)
stand = (
stand.union(inset.translate(((height + thickness) / 2, 0, 0)))
.union(inset.translate((-(height + thickness) / 2, 0, 0)))
.union(backing.translate((-height / 2, -thickness / 2, thickness / 2)))
.union(backing.rotate((0, 0, 0), (0, 1, 0), -90).translate((height / 2, -thickness / 2, thickness / 2)))
)
return stand
stand_names = ("front_stand", "back_stand")
def create_upper_leg():
l1, l2 = 50, 80
pts = [(0, 0), (0, height / 2), (l1, height / 2 - 5), (l2, 0)]
upper_leg_hole = (l2 - 10, 0)
upper_leg = (
cq.Workplane()
.polyline(pts)
.mirrorX()
.pushPoints([upper_leg_hole])
.circle(diam / 2 + tol)
.extrude(thickness)
.edges("|Z and (not <X)")
.fillet(4)
)
axle = (
cq.Workplane("XZ", origin=(0, height / 2 + thickness + tol, thickness / 2))
.circle(diam / 2)
.extrude(2 * (height / 2 + thickness + tol))
)
upper_leg = upper_leg.union(axle)
# tag mating points
upper_leg.faces(">Z").edges(cq.NearestToPointSelector(upper_leg_hole)).tag("top")
upper_leg.faces("<Z").edges(cq.NearestToPointSelector(upper_leg_hole)).tag("bottom")
return upper_leg
def create_lower_leg():
w, l1, l2 = 15, 20, 120
pts = [(0, 0), (l1, w), (l2, 0)]
lower_leg_hole = (l1 - 10, 0)
lower_leg = (
cq.Workplane()
.polyline(pts)
.mirrorX()
.pushPoints([lower_leg_hole])
.circle(diam / 2 + tol)
.extrude(thickness)
.edges("|Z")
.fillet(5)
)
# tag mating points
lower_leg.faces(">Z").edges(cq.NearestToPointSelector(lower_leg_hole)).tag("top"),
lower_leg.faces("<Z").edges(cq.NearestToPointSelector(lower_leg_hole)).tag("bottom")
return lower_leg
leg_angles = {
"right_back": -105,
"right_middle": -90,
"right_front": -75,
"left_back": 105,
"left_middle": 90,
"left_front": 75,
}
leg_names = list(leg_angles.keys())
base = create_base(rotate=False)
stand = create_stand()
upper_leg = create_upper_leg()
lower_leg = create_lower_leg()
# Assembly
def create_hexapod():
# Some shortcuts
L = lambda *args: cq.Location(cq.Vector(*args))
C = lambda name: web_color(name)
# Leg assembly
leg = MAssembly(upper_leg, name="upper", color=C("orange")).add(
lower_leg, name="lower", color=C("orange"), loc=L(80, 0, 0)
)
# Hexapod assembly
hexapod = (
MAssembly(base, name="bottom", color=C("silver"), loc=L(0, 1.1 * width, 0))
.add(base, name="top", color=C("gainsboro"), loc=L(0, -2.2 * width, 0))
.add(stand, name="front_stand", color=C("SkyBlue"), loc=L(40, 100, 0))
.add(stand, name="back_stand", color=C("SkyBlue"), loc=L(-40, 100, 0))
)
for i, name in enumerate(leg_names):
hexapod.add(leg, name=name, loc=L(100, -55 * (i - 1.7), 0))
return hexapod
# Mates
from collections import OrderedDict as odict
hexapod = create_hexapod()
# show(hexapod)
hexapod.mate("bottom?top", name="bottom", origin=True)
hexapod.mate("top?bottom", name="top", origin=True, transforms=odict(rx=180, tz=-(height + 2 * tol)))
for name in stand_names:
hexapod.mate(f"bottom?{name}", name=f"{name}_bottom", transforms=odict(rz=-90 if "f" in name else 90))
hexapod.mate(f"{name}@faces@<X", name=name, origin=True, transforms=odict(rx=180))
for name in base_holes_names:
hexapod.mate(f"bottom?{name}", name=f"{name}_hole", transforms=odict(rz=leg_angles[name]))
for name in leg_names:
lower, upper, angle = ("top", "bottom", -75) if "left" in name else ("bottom", "top", -75)
hexapod.mate(f"{name}?{upper}", name=f"leg_{name}_hole", transforms=odict(rz=angle))
hexapod.mate(f"{name}@faces@<Y", name=f"leg_{name}_hinge", origin=True, transforms=odict(rx=180, rz=-90))
hexapod.mate(f"{name}/lower?{lower}", name=f"leg_{name}_lower_hole", origin=True)
# show(hexapod, reset_camera=False)
relocate(hexapod)
# Assemble the parts
for leg in leg_names:
hexapod.assemble(f"leg_{leg}_lower_hole", f"leg_{leg}_hole")
hexapod.assemble(f"leg_{leg}_hinge", f"{leg}_hole")
hexapod.assemble("top", "bottom")
for stand_name in stand_names:
hexapod.assemble(f"{stand_name}", f"{stand_name}_bottom")
show(hexapod, render_mates=True, mate_scale=5)
# Animation
horizontal_angle = 25
def intervals(count):
r = [min(180, (90 + i * (360 // count)) % 360) for i in range(count)]
return r
def times(end, count):
return np.linspace(0, end, count + 1)
def vertical(count, end, offset, reverse):
ints = intervals(count)
heights = [round(35 * np.sin(np.deg2rad(x)) - 15, 1) for x in ints]
heights.append(heights[0])
return times(end, count), heights[offset:] + heights[1 : offset + 1]
def horizontal(end, reverse):
factor = 1 if reverse else -1
return times(end, 4), [0, factor * horizontal_angle, 0, -factor * horizontal_angle, 0]
leg_group = ("left_front", "right_middle", "left_back")
animation = Animation()
for name in leg_names:
# move upper leg
animation.add_track(f"/bottom/{name}", "rz", *horizontal(4, "middle" in name))
# move lower leg
animation.add_track(f"/bottom/{name}/lower", "rz", *vertical(8, 4, 0 if name in leg_group else 4, "left" in name))
# lift hexapod to run on grid
# animation.add_track(f"bottom", "tz", [0, 4], [61.25] * 2)
animation.animate(speed=3)

Xet Storage Details

Size:
8.04 kB
·
Xet hash:
677a062b8554bb8a8a5abfff44f2b8e19c1ef68263394139cedd17079cf7ecf2

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.