Cosplay/nhf/parts/box.py

157 lines
5.0 KiB
Python

import cadquery as Cq
from dataclasses import dataclass, field
from typing import Tuple, Optional, Union, Callable
from nhf.build import Model, TargetKind, target
import nhf.utils
def box_with_centre_holes(
length: float,
width: float,
height: float,
hole_loc: list[float],
hole_diam: float = 6.0,
) -> Cq.Workplane:
"""
Creates a box with holes along the X axis, marked `conn0, conn1, ...`. The
box's y axis is centred
"""
result = (
Cq.Workplane('XY')
.box(length, width, height, centered=(False, True, False))
.faces(">Z")
.workplane()
)
plane = result
for i, x in enumerate(hole_loc):
result = result.moveTo(x, 0).hole(hole_diam)
plane.moveTo(x, 0).tagPlane(f"conn{i}")
return result
@dataclass
class Hole:
x: float
y: float = 0.0
diam: Optional[float] = None
tag: Optional[str] = None
face: Optional[Cq.Face] = None
@property
def rev_tag(self) -> str:
assert self.tag is not None
return self.tag + "_rev"
def cutting_geometry(self, default_diam: Optional[float]=None) -> Cq.Face:
if self.face is not None:
return self.face
diam = self.diam if self.diam is not None else default_diam
assert diam is not None
return Cq.Face.makeFromWires(Cq.Wire.makeCircle(diam/2, Cq.Vector(), Cq.Vector(0,0,1)))
@dataclass
class MountingBox(Model):
"""
Create a box with marked holes
"""
length: float = 100.0
width: float = 60.0
thickness: float = 1.0
# List of (x, y), diam
holes: list[Hole] = field(default_factory=lambda: [])
hole_diam: Optional[float] = None
centred: Tuple[bool, bool] = (False, True)
generate_side_tags: bool = True
# Generate tags on the opposite side
generate_reverse_tags: bool = False
centre_bot_top_tags: bool = False
centre_left_right_tags: bool = False
# Determines the position of side tags
flip_y: bool = False
profile_callback: Optional[Callable[[Cq.Sketch], Cq.Sketch]] = None
def __post_init__(self):
for i, hole in enumerate(self.holes):
if hole.tag is None:
hole.tag = f"conn{i}"
@target(kind=TargetKind.DXF)
def profile(self) -> Cq.Sketch:
bx, by = 0, 0
if not self.centred[0]:
bx = self.length / 2
if not self.centred[1]:
by = self.width / 2
result = (
Cq.Sketch()
.push([(bx, by)])
.rect(self.length, self.width)
)
for hole in self.holes:
face = hole.cutting_geometry(default_diam=self.hole_diam)
result.push([(hole.x, hole.y)]).each(lambda l:face.moved(l), mode='s')
if self.profile_callback:
result = self.profile_callback(result)
return result
def generate(self) -> Cq.Workplane:
"""
Creates box shape with markers
"""
result = (
Cq.Workplane('XY')
.placeSketch(self.profile())
.extrude(self.thickness)
)
plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness)
reverse_plane = result.copyWorkplane(Cq.Workplane('XY'))
for hole in self.holes:
assert hole.tag
plane.moveTo(hole.x, hole.y).tagPlane(hole.tag)
if self.generate_reverse_tags:
reverse_plane.moveTo(hole.x, hole.y).tagPlane(hole.rev_tag, '-Z')
if self.generate_side_tags:
xn, xp = 0, self.length
if self.centred[0]:
xn -= self.length/2
xp -= self.length/2
yn, yp = 0, self.width
if self.centred[1]:
yn -= self.width/2
yp -= self.width/2
tag_x = xn + (self.length/2 if self.centre_left_right_tags else 0)
result.copyWorkplane(Cq.Workplane('XZ', origin=(tag_x, yn, self.thickness))).tagPlane("left")
result.copyWorkplane(Cq.Workplane('ZX', origin=(tag_x, yp, self.thickness))).tagPlane("right")
tag_y = yn + (self.width/2 if self.centre_bot_top_tags else 0)
result.copyWorkplane(Cq.Workplane('ZY', origin=(xn, tag_y, self.thickness))).tagPlane("bot")
result.copyWorkplane(Cq.Workplane('YZ', origin=(xp, tag_y, self.thickness))).tagPlane("top")
result.faces(">Z").tag("dir")
return result
def marked_assembly(self) -> Cq.Assembly:
result = (
Cq.Assembly()
.add(self.generate(), name="box")
)
for hole in self.holes:
result.markPlane(f"box?{hole.tag}")
if self.generate_reverse_tags:
result.markPlane(f"box?{hole.rev_tag}")
if self.generate_side_tags:
(
result
.markPlane("box?left")
.markPlane("box?right")
.markPlane("box?dir")
.markPlane("box?top")
.markPlane("box?bot")
)
return result.solve()