# cadquery
from cadquery.occ_impl import shapes as occ_shapes
from pathlib import Path
import cadquery as cq
import numpy as np
def loft_faces(f1: occ_shapes.Face, f2: occ_shapes.Face) -> occ_shapes.Solid:
solid = cq.Solid.makeLoft([f1.outerWire(), f2.outerWire()])
for inner1, inner2 in zip(f1.innerWires(), f2.innerWires()):
solid_inner = cq.Solid.makeLoft([inner1, inner2])
solid = solid.cut(solid_inner)
return solid
def container(
width,
depth,
height,
thickness,
ledge,
exterior_ledge,
fillet,
angle,
clearance,
ridge,
):
radians = np.deg2rad(angle)
throw = depth * np.arctan(radians)
height = height + throw / 2
# create base box
outer = cq.Workplane("XY").rect(width, depth).extrude(height)
inner = (
cq.Workplane("XY")
.rect(width - thickness * 2, depth - thickness * 2)
.extrude(height)
.translate([0, 0, thickness])
)
box = outer.cut(inner)
# cut angle
if radians > 0:
side_cut = (
outer.faces(">X")
.workplane()
.moveTo(depth / 2, height)
.lineTo(-depth / 2, height - throw)
.lineTo(-depth / 2, height)
.close()
.extrude(-width, combine=False)
)
box = box.cut(side_cut)
# cut ledge
ledge_face = box.faces(">Z").val()
outer_wire = ledge_face.outerWire()
inner_wire = ledge_face.innerWires()[0]
if exterior_ledge:
tolerance_thickness = thickness - clearance
offset_wire = inner_wire.offset2D(tolerance_thickness / 2)[0]
cut_face = occ_shapes.Face.makeFromWires(outer_wire, [offset_wire])
else:
tolerance_thickness = thickness + clearance
offset_wire = inner_wire.offset2D(tolerance_thickness / 2)[0]
cut_face = occ_shapes.Face.makeFromWires(offset_wire, [inner_wire])
cut_solid = loft_faces(cut_face, cut_face.translate([0, 0, -ledge]))
# fillet vertical edges
box = box.cut(cq.Workplane(obj=cut_solid)).edges("|Z").fillet(fillet)
# fillet mating edges
normal = cq.Vector(0, -np.sin(radians), np.cos(radians))
box = (
box.cut(cq.Workplane(obj=cut_solid))
.faces(cq.selectors.ParallelDirSelector(normal, tolerance=0.1))
.faces(cq.selectors.AreaNthSelector(0))
.edges(cq.selectors.LengthNthSelector(0))
.fillet(thickness / 4)
)
# extrude retention ridge
edge: cq.Edge
for edge in offset_wire.edges():
if not edge.geomType() == "LINE":
continue
tangent: cq.Vector = edge.Center() - offset_wire.Center()
tangent.z = 0
x_dir = edge.positionAt(0.45) - edge.positionAt(0.55)
upward_direction = cq.Vector(0, 0, 1)
cross_product = tangent.cross(upward_direction)
if not cross_product.dot(x_dir) < 0:
x_dir = -x_dir
plane = cq.Plane(
origin=edge.Center(),
xDir=x_dir,
normal=tangent,
)
plane2 = plane.rotated(cq.Vector(0, 1, 0) * -90)
a = ridge
b = ledge / 8
arc = (
cq.Workplane(plane2)
.moveTo(-thickness / 4, -b)
.lineTo(0, -b)
.ellipseArc(a, b, -90, 90, startAtCurrent=True)
.lineTo(-thickness / 4, b)
.close()
)
arc = arc.extrude(edge.Length() / 2 - 2, both=True)
arc = arc.translate(-cq.Vector(0, 0, 1) * ledge / 2.0)
if exterior_ledge:
box = box.union(arc)
else:
box = box.cut(arc)
return box.val()
def container_with_lid(
width: float = 20.0,
depth: float = 20.0,
height: float = 20.0,
thickness: float = 2.0,
ledge: float = 5.0,
fillet: float = 2.0,
angle: float = 5.0,
clearance: float = 0.1,
top_ratio: float = 0.3,
ridge_top: float = 2.0,
ridge_bot: float = 3.0,
) -> Path:
width = width + thickness * 2
depth = depth + thickness * 2
height = height + thickness * 2 + ledge
lower_height = height * (1 - top_ratio)
upper_height = height * top_ratio
ledge = ledge
fillet = fillet
angle = angle
clearance = clearance
ridge_top = clearance * ridge_top
ridge_bot = clearance * ridge_bot
lower = container(
width,
depth,
lower_height,
thickness,
ledge,
True,
fillet,
angle,
clearance,
ridge_bot,
)
higher = (
container(
width,
depth,
upper_height,
thickness,
ledge,
False,
fillet,
angle,
clearance,
ridge_top,
)
.rotate([0, 0, 0], [0, 0, 1], 0)
.translate([width * 1.2, 0, 0])
)
compound = cq.Compound.makeCompound([lower, higher])
compound_path = "container_with_lid.stl"
compound.exportStl(compound_path)
return Path(compound_path)