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()