feat: Item baseclass, and fasteners

This commit is contained in:
Leni Aniva 2024-07-19 00:58:10 -07:00
parent dccae49b9d
commit 7cb00c0794
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
5 changed files with 193 additions and 20 deletions

View File

@ -1 +1,2 @@
from nhf.materials import Material, Role
from nhf.parts.item import Item

View File

@ -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

91
nhf/parts/fasteners.py Normal file
View File

@ -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("tip")
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("bot")
result.faces(">Z").tag("top")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X")
return result

67
nhf/parts/item.py Normal file
View File

@ -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

View File

@ -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