158 lines
5.0 KiB
Python
158 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):
|
||
|
assert self.thickness > 0
|
||
|
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()
|