Compare commits
5 Commits
6cccd6f212
...
cc5a537893
Author | SHA1 | Date | |
---|---|---|---|
cc5a537893 | |||
e305925dc6 | |||
88d17aae8d | |||
7ebe77d879 | |||
60335aedec |
163
src/case.py
163
src/case.py
|
@ -23,12 +23,12 @@ button_seat_margin_x = 0.2
|
|||
# N.B.: keep this >= button_inset_gap to keep the part easily printable
|
||||
button_seat_margin_yz = 1.5
|
||||
|
||||
# make the walls near the buttons this much thinner
|
||||
# make the walls near the buttons this much thinner (by cutting away the *exterior*
|
||||
button_inset_x = 0.4
|
||||
button_inset_margin_yz = 5.5
|
||||
# length of the segment we cut away entirely from the body bordering each button
|
||||
button_inset_gap = 1.4
|
||||
button_rocker_width = 0.8
|
||||
button_rocker_width = 1.4
|
||||
button_rocker_length = 3.0
|
||||
|
||||
# TODO: this should be relative to the bottom of the case, not the center
|
||||
|
@ -44,7 +44,7 @@ battery_harness_shell_thickness = 0.8
|
|||
battery_harness_margin_bottom = 21.0
|
||||
battery_harness_margin_top = 2.0
|
||||
|
||||
def _makeShell(solid, thickness: float=1, combine=False, **kwargs):
|
||||
def _thicken(solid, thickness: float=1, combine=False, **kwargs):
|
||||
"""
|
||||
dilate the solid in all dimensions by `thickness` and then subtract the original.
|
||||
this implementation only behaves as expected if the solid has no cusps (i.e. is smoothed).
|
||||
|
@ -55,6 +55,36 @@ def _makeShell(solid, thickness: float=1, combine=False, **kwargs):
|
|||
# alternatively, to perform an inset, set thickness negative and use combine="cut"
|
||||
return solid.faces().each(lambda f: f.thicken(thickness), combine=combine)
|
||||
|
||||
def make_shell(body):
|
||||
shell = _thicken(body, thickness)
|
||||
# a true fillet on the print-bed side of the print isn't possible (it's an overhang),
|
||||
# so turn that into a chamfer, which can be printed
|
||||
body_slice = body.faces("not |Z").edges(">>Z[-3]")
|
||||
body_slice_z = body_slice.vals()[0].BoundingBox().zmax
|
||||
body_max_z = body.faces(tag="body_back").vals()[0].BoundingBox().zmax
|
||||
body_thick = body_slice \
|
||||
.toPending().offset2D(thickness) \
|
||||
.extrude(body_max_z - body_slice_z + thickness, combine=False)
|
||||
body_thin = body_slice.toPending().consolidateWires() \
|
||||
.extrude(body_max_z - body_slice_z + thickness, combine=False)
|
||||
|
||||
sides = body_thick.cut(body_thin)
|
||||
backing = _thicken(body_thick.faces(">Z"), -thickness, combine=False)
|
||||
shell = shell.union(sides).union(backing)
|
||||
|
||||
shell = shell.edges(">Z").chamfer(thickness)
|
||||
shell = shell.tag("shell")
|
||||
return shell
|
||||
|
||||
def _mask_to_main_body(case, feature):
|
||||
# if we cut too far into the screen encasing, then we create an overhang which might not print well.
|
||||
# and if we cut into the backing of the case, that's not great either
|
||||
# instead, we accept a non-arch, and rely that the printer can bridge the top.
|
||||
shell_main_bbox = case.faces("|Y and >Y", tag="shell").vals()[0].BoundingBox()
|
||||
mask = cq.Workplane("XY").box(1000, 1000, shell_main_bbox.zlen, centered=True) \
|
||||
.translate(shell_main_bbox.center)
|
||||
return feature.intersect(mask)
|
||||
|
||||
def front_cutaway(case):
|
||||
# split the case into the front part, which covers the screen, and the rear
|
||||
split_case = case.faces("<<Z").workplane(offset=-thickness).split(keepTop=True, keepBottom=True)
|
||||
|
@ -74,40 +104,35 @@ def front_cutaway(case):
|
|||
.edges("|Z").fillet(overhang_radius)
|
||||
# return front_face_cutaway.faces("|Z").each(lambda f: f.thicken(thickness).translate((0, 0, -thickness)), combine=False)
|
||||
|
||||
def camera_cutaway(phone):
|
||||
cutout = phone.solids(tag="camera")
|
||||
def camera_cutaway(camera):
|
||||
# take the face which attaches the camera to the phone, and extrude that.
|
||||
# cq imprecision requires i extrude > thickness to get a margin that's actually recognized as a complete cut
|
||||
cutout = cutout.faces("<Z").each(lambda f: f.thicken(3*thickness), combine=False)\
|
||||
cutout = camera.faces("<Z").each(lambda f: f.thicken(3*thickness), combine=False)\
|
||||
.translate((0, 0, 2*thickness))
|
||||
cutout = cutout.faces("not |Z").each(lambda f: f.thicken(camera_cut_margin), combine=True)
|
||||
return cutout
|
||||
|
||||
def aux_cutaway(phone):
|
||||
cutout = phone.solids(tag="aux")
|
||||
# extrude only from the surface of the phone out through the case
|
||||
# -- no reason to cutaway the interior of the aux port from the case
|
||||
cutout = cutout.faces("<Y").each(lambda f: f.thicken(thickness), combine=False)
|
||||
cutout = cutout.faces("not |Y").each(lambda f: f.thicken(aux_cut_margin), combine=True)
|
||||
def aux_cutaway(case, aux):
|
||||
# extrude through the case
|
||||
cutout = _thicken(aux.faces("<Y"), thickness, combine=True)
|
||||
# thicken
|
||||
cutout = _thicken(cutout.faces("not |Y"), aux_cut_margin, combine=True)
|
||||
|
||||
# if we cut too far into the screen encasing, then we create an overhang which might not print well.
|
||||
body_top = phone.faces("<<Y").vals()[0].BoundingBox().zmin
|
||||
shell_main_bbox = case.faces("|Y and <Y", tag="shell").vals()[0].BoundingBox()
|
||||
cutout_top = cutout.vals()[0].BoundingBox().zmin
|
||||
return cutout.translate((0, 0, max(0, body_top - cutout_top)))
|
||||
cutout = cutout.translate((0, 0, max(0, shell_main_bbox.zmin - cutout_top)))
|
||||
|
||||
def usb_cutaway(phone):
|
||||
cutout = phone.solids(tag="usb")
|
||||
cutout = cq.Workplane("XZ").add(cutout) \
|
||||
.edges(">Y") \
|
||||
cutout = _mask_to_main_body(case, cutout)
|
||||
|
||||
return cutout
|
||||
|
||||
def usb_cutaway(case, usb):
|
||||
cutout = cq.Workplane("XZ").add(usb.edges(">Y")) \
|
||||
.toPending().offset2D(usb_cut_margin) \
|
||||
.extrude(-thickness)
|
||||
.extrude(2*thickness, both=True)
|
||||
|
||||
# if we cut too far into the screen encasing, then we create an overhang which might not print well.
|
||||
# unlike with e.g. the aux port, shaping the cutout so its top is an arch (and therefore more printable), would destroy its function.
|
||||
# instead, we accept a non-arch, and rely that the printer can bridge the top.
|
||||
mask = phone.faces(">>Y", tag="body").each(lambda f: f.thicken(1000), combine=False)
|
||||
mask = mask.faces(">Z").box(1000, 1000, 1000, centered=(True, True, False))
|
||||
cutout = cutout.intersect(mask)
|
||||
cutout = _mask_to_main_body(case, cutout)
|
||||
|
||||
return cutout
|
||||
|
||||
|
@ -116,12 +141,11 @@ def usb_cutaway(phone):
|
|||
# cutout, = cutout.faces("<Z").workplane(offset=-3).split(keepBottom=True, keepTop=False).all()
|
||||
# return cutout.translate((0, 0, max(0, body_top - cutout_top)))
|
||||
|
||||
def button_seat_cutaway(phone):
|
||||
buttons = phone.solids(tag="volume").union(phone.solids(tag="power"))
|
||||
def button_seat_cutaway(phone, buttons):
|
||||
# cutout = cq.Workplane("YZ").add(buttons.faces("<X")).edges() \
|
||||
# .toPending().offset2D(button_seat_margin_yz) \
|
||||
# .extrude(thickness)
|
||||
cutout = buttons.faces().each(lambda f: f.thicken(button_seat_margin_yz), combine=False)
|
||||
cutout = _thicken(buttons, button_seat_margin_yz, combine=False)
|
||||
mask = buttons.faces(">X").box(100, 100, 100, centered=(True, True, True), combine=False) \
|
||||
.translate((-50 + button_seat_margin_x, 0, 0))
|
||||
cutout = cutout.intersect(mask)
|
||||
|
@ -161,26 +185,46 @@ def button_gap_cutaway(phone, buttons):
|
|||
cutout = cutout_top.union(cutout_bot)
|
||||
return cutout
|
||||
|
||||
def button_rockers(phone, buttons):
|
||||
buttons2d = buttons.faces("<X")
|
||||
buttons = cq.Workplane("YZ").add(buttons2d) \
|
||||
.edges().toPending().consolidateWires() \
|
||||
.extrude(button_rocker_width) \
|
||||
.translate((thickness-button_inset_x, 0, 0))
|
||||
# def button_rockers(phone, buttons):
|
||||
# buttons2d = buttons.faces("<X")
|
||||
# buttons = cq.Workplane("YZ").add(buttons2d) \
|
||||
# .edges().toPending().consolidateWires() \
|
||||
# .extrude(button_rocker_width) \
|
||||
# .translate((thickness-button_inset_x, 0, 0))
|
||||
#
|
||||
# rocker_top = buttons.faces("<Y").workplane(offset=-button_rocker_length).split(keepTop=True)
|
||||
# rocker_top = rocker_top.edges("not <X").fillet(button_rocker_width * 2/3)
|
||||
# rocker_bot = buttons.faces(">Y").workplane(offset=-button_rocker_length).split(keepTop=True)
|
||||
# rocker_bot = rocker_bot.edges("not <X").fillet(button_rocker_width * 2/3)
|
||||
#
|
||||
# rockers = rocker_top.union(rocker_bot)
|
||||
# return rockers
|
||||
#
|
||||
# def button_bump(phone, button):
|
||||
# button2d = button.faces("<X")
|
||||
# bump = button2d.sphere(radius=button_rocker_width, angle3=180, direct=(0, 1, 0), combine=False)
|
||||
# bump = bump.translate((thickness-button_inset_x, 0, 0))
|
||||
# return bump
|
||||
|
||||
rocker_top = buttons.faces("<Y").workplane(offset=-button_rocker_length).split(keepTop=True)
|
||||
rocker_top = rocker_top.edges("not <X").fillet(button_rocker_width * 2/3)
|
||||
rocker_bot = buttons.faces(">Y").workplane(offset=-button_rocker_length).split(keepTop=True)
|
||||
rocker_bot = rocker_bot.edges("not <X").fillet(button_rocker_width * 2/3)
|
||||
|
||||
rockers = rocker_top.union(rocker_bot)
|
||||
return rockers
|
||||
|
||||
def button_bump(phone, button):
|
||||
def button_column(phone, button, align="center"):
|
||||
"""
|
||||
make a column on the exterior of the phone, so that pressing it in anywhere should activation the adjacent button.
|
||||
"""
|
||||
button2d = button.faces("<X")
|
||||
rocker = button2d.sphere(radius=button_rocker_width, angle3=180, direct=(0, 1, 0), combine=False)
|
||||
rocker = rocker.translate((thickness-button_inset_x, 0, 0))
|
||||
return rocker
|
||||
column = button2d.box(button_rocker_width, button_rocker_length, 20, centered=(False, True, True), combine=False)
|
||||
column = column.translate((thickness-button_inset_x, 0, 0))
|
||||
|
||||
mask = _thicken(phone.faces(">>X", tag="body"), 100, combine=False)
|
||||
column = column.intersect(mask)
|
||||
|
||||
column = column.edges(">X").chamfer(button_rocker_width * 3/4)
|
||||
|
||||
button_bbox = button.vals()[0].BoundingBox()
|
||||
if align == "up":
|
||||
column = column.translate((0, -0.5 * button_bbox.ylen + 2*button_inset_gap, 0))
|
||||
elif align == "down":
|
||||
column = column.translate((0, 0.5 * button_bbox.ylen - 2*button_inset_gap, 0))
|
||||
return column
|
||||
|
||||
def battery_cutaway(phone, battery):
|
||||
body_back_bb = phone.faces(tag="body_back") \
|
||||
|
@ -290,24 +334,35 @@ def orient_for_printing(case):
|
|||
|
||||
def case(phone, battery=None):
|
||||
body = phone.solids(tag="body")
|
||||
volume = phone.solids(tag="volume")
|
||||
power = phone.solids(tag="power")
|
||||
case = _makeShell(body, thickness)
|
||||
volume = phone.solids(tag="volume")
|
||||
aux = phone.solids(tag="aux")
|
||||
camera = phone.solids(tag="camera")
|
||||
usb = phone.solids(tag="usb")
|
||||
|
||||
case = make_shell(body)
|
||||
case = case.cut(front_cutaway(case))
|
||||
case = case.cut(camera_cutaway(phone))
|
||||
case = case.cut(aux_cutaway(phone))
|
||||
case = case.cut(usb_cutaway(phone))
|
||||
case = case.cut(button_seat_cutaway(phone))
|
||||
case = case.cut(aux_cutaway(case, aux))
|
||||
case = case.cut(camera_cutaway(camera))
|
||||
case = case.cut(usb_cutaway(case, usb))
|
||||
|
||||
case = case.cut(button_seat_cutaway(phone, volume))
|
||||
case = case.cut(button_seat_cutaway(phone, power))
|
||||
case = case.cut(button_gap_cutaway(phone, volume))
|
||||
case = case.cut(button_gap_cutaway(phone, power))
|
||||
case = case.cut(button_inset_cutaway(phone, volume))
|
||||
case = case.cut(button_inset_cutaway(phone, power))
|
||||
case = case.add(button_rockers(phone, volume))
|
||||
case = case.add(button_bump(phone, power))
|
||||
case = case.add(button_column(phone, volume, "up"))
|
||||
case = case.add(button_column(phone, volume, "down"))
|
||||
# case = case.add(button_rockers(phone, volume))
|
||||
# case = case.add(button_bump(phone, power))
|
||||
case = case.add(button_column(phone, power))
|
||||
|
||||
if battery:
|
||||
case = case.cut(battery_cutaway(phone, battery))
|
||||
# case = case.add(battery_straps(phone, battery))
|
||||
case = case.add(battery_harness(phone, battery))
|
||||
|
||||
# TODO: compress the case along the Z axis, to give a snugger fit (0.8mm is a good compression)
|
||||
case = orient_for_printing(case)
|
||||
return case
|
||||
|
|
Loading…
Reference in New Issue
Block a user