From 7cb00c07940053cb0e28e18fa5f2920820b194f7 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 00:58:10 -0700 Subject: [PATCH] feat: Item baseclass, and fasteners --- nhf/__init__.py | 1 + nhf/materials.py | 36 ++++++++--------- nhf/parts/fasteners.py | 91 ++++++++++++++++++++++++++++++++++++++++++ nhf/parts/item.py | 67 +++++++++++++++++++++++++++++++ nhf/utils.py | 18 +++++++++ 5 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 nhf/parts/fasteners.py create mode 100644 nhf/parts/item.py diff --git a/nhf/__init__.py b/nhf/__init__.py index b582e6a..70b975b 100644 --- a/nhf/__init__.py +++ b/nhf/__init__.py @@ -1 +1,2 @@ from nhf.materials import Material, Role +from nhf.parts.item import Item diff --git a/nhf/materials.py b/nhf/materials.py index c4c4e49..af336da 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -9,6 +9,11 @@ def _color(name: str, alpha: float) -> Cq.Color: r, g, b, _ = Cq.Color(name).toTuple() return Cq.Color(r, g, b, alpha) + +KEY_ROLE = 'role' +KEY_MATERIAL = 'material' +KEY_ITEM = 'item' + class Role(Flag): """ Describes the role of a part @@ -26,7 +31,10 @@ class Role(Flag): STRUCTURE = auto() DECORATION = auto() ELECTRONIC = auto() + + # Fasteners, etc. CONNECTION = auto() + HANDLE = auto() # Parent and child components in a load bearing joint @@ -54,7 +62,8 @@ ROLE_COLOR_MAP = { Role.STRUCTURE: _color('gray', 0.4), Role.DECORATION: _color('lightseagreen', 0.4), Role.ELECTRONIC: _color('mediumorchid', 0.5), - Role.CONNECTION: _color('steelblue3', 0.8) + Role.CONNECTION: _color('steelblue3', 0.8), + Role.HANDLE: _color('tomato4', 0.8), } @@ -91,10 +100,10 @@ def add_with_material_role( metadata = {} color = None if material: - metadata["material"] = material + metadata[KEY_MATERIAL] = material color = material.color if role: - metadata["role"] = role + metadata[KEY_ROLE] = role color = role.color_avg() if len(metadata) == 0: metadata = None @@ -106,29 +115,16 @@ Cq.Assembly.addS = add_with_material_role def color_by_material(self: Cq.Assembly) -> Cq.Assembly: for _, a in self.traverse(): - if 'material' not in a.metadata: + if KEY_MATERIAL not in a.metadata: continue - a.color = a.metadata["material"].color + a.color = a.metadata[KEY_MATERIAL].color return self Cq.Assembly.color_by_material = color_by_material def color_by_role(self: Cq.Assembly, avg: bool = True) -> Cq.Assembly: for _, a in self.traverse(): - if 'role' not in a.metadata: + if KEY_ROLE not in a.metadata: continue - role = a.metadata["role"] + role = a.metadata[KEY_ROLE] a.color = role.color_avg() if avg else role.color_head() return self Cq.Assembly.color_by_role = color_by_role - -def total_mass(self: Cq.Assembly) -> float: - """ - Calculates the total mass in units of g - """ - total = 0.0 - for _, a in self.traverse(): - if 'material' not in a.metadata: - continue - vol = a.toCompound().Volume() - total += vol * a.metadata['material'].density - return total / 1000.0 -Cq.Assembly.total_mass = total_mass diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py new file mode 100644 index 0000000..c6a0afe --- /dev/null +++ b/nhf/parts/fasteners.py @@ -0,0 +1,91 @@ +from dataclasses import dataclass +import math +import cadquery as Cq +from nhf import Item, Role +import nhf.utils + +@dataclass(frozen=True) +class ThreaddedKnob(Item): + """ + Sourced from: + + > Othmro Black 12mm(M12) x 50mm Thread Replacement Star Hand Knob Tightening + > Screws + """ + diam_rod: float + height_rod: float + diam_knob: float + + diam_neck: float + height_neck: float + height_knob: float + + @property + def name(self) -> str: + return f"Knob-M{int(self.diam_rod)}-{int(self.height_rod)}mm" + + def generate(self) -> Cq.Assembly: + print(self.name) + knob = Cq.Solid.makeCylinder( + radius=self.diam_knob / 2, + height=self.height_knob, + ) + neck = Cq.Solid.makeCylinder( + radius=self.diam_neck / 2, + height=self.height_neck, + ) + rod = ( + Cq.Workplane('XY') + .cylinder( + radius=self.diam_rod / 2, + height=self.height_rod, + centered=(True, True, False)) + ) + rod.faces("Z").tag("root") + + return ( + Cq.Assembly() + .addS(rod, name="rod", role=Role.CONNECTION) + .addS(neck, name="neck", role=Role.HANDLE, + loc=Cq.Location((0, 0, self.height_rod))) + .addS(knob, name="knob", role=Role.HANDLE, + loc=Cq.Location((0, 0, self.height_rod + self.height_neck))) + ) + + +@dataclass(frozen=True) +class HexNut(Item): + diam: float + pitch: float + + # FIXME: Measure these + m: float + s: float + + def __post_init__(self): + assert self.s > self.diam + + @property + def name(self): + return f"HexNut-M{int(self.diam)}-{self.pitch}" + + @property + def role(self): + return Role.CONNECTION + + def generate(self) -> Cq.Workplane: + print(self.name) + r = self.s / math.sqrt(3) + result = ( + Cq.Workplane("XY") + .sketch() + .regularPolygon(r=r, n=6) + .circle(r=self.diam/2, mode='s') + .finalize() + .extrude(self.m) + ) + result.faces("Z").tag("top") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") + return result diff --git a/nhf/parts/item.py b/nhf/parts/item.py new file mode 100644 index 0000000..16fbef7 --- /dev/null +++ b/nhf/parts/item.py @@ -0,0 +1,67 @@ +from typing import Union, Optional +from collections import Counter +from dataclasses import dataclass +import cadquery as Cq +from nhf.materials import Role, KEY_ROLE, KEY_ITEM + +@dataclass(frozen=True) +class Item: + """ + A pre-fabricated item + """ + mass: float + + #@property + #def mass(self) -> float: + # """ + # Mass, in grams + # """ + # return self._mass + + #@mass.setter + #def mass(self, value): + # assert value >= 0, "Mass cannot be negative" + # self._mass = value + + @property + def name(self) -> str: + pass + + @property + def role(self) -> Optional[Role]: + return None + + def generate(self) -> Union[Cq.Assembly, Cq.Workplane]: + """ + Creates an assembly for this item. Subclass should implement this + """ + return Cq.Assembly() + + def assembly(self) -> Cq.Assembly: + """ + Interface for creating assembly with the necessary metadata + """ + a = self.generate() + if isinstance(a, Cq.Workplane): + a = Cq.Assembly(a) + if role := self.role: + a.metadata[KEY_ROLE] = role + a.color = role.color_avg() + assert isinstance(a, Cq.Assembly) + assert KEY_ITEM not in a.metadata + a.metadata[KEY_ITEM] = self + return a + + @staticmethod + def count(a: Cq.Assembly) -> Counter: + """ + Counts the number of items + """ + occ = Counter() + for _, obj in a.traverse(): + if KEY_ITEM not in obj.metadata: + continue + item = obj.metadata[KEY_ITEM] + assert isinstance(item, Item) + occ[item.name] += 1 + return occ diff --git a/nhf/utils.py b/nhf/utils.py index f542632..e999e85 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -6,6 +6,7 @@ import functools import cadquery as Cq from nhf import Role from typing import Union, Tuple, cast +from nhf.materials import KEY_ITEM, KEY_MATERIAL # Bug fixes def _subloc(self, name: str) -> Tuple[Cq.Location, str]: @@ -204,3 +205,20 @@ def get_abs_location(self: Cq.Assembly, return loc Cq.Assembly.get_abs_location = get_abs_location + + +# Tallying functions + +def total_mass(self: Cq.Assembly) -> float: + """ + Calculates the total mass in units of g + """ + total = 0.0 + for _, a in self.traverse(): + if item := a.metadata.get(KEY_ITEM): + total += item.mass + elif material := a.metadata.get(KEY_MATERIAL): + vol = a.toCompound().Volume() + total += (vol / 1000) * material.density + return total +Cq.Assembly.total_mass = total_mass