cosplay: Touhou/Houjuu Nue #4
|
@ -1 +1,2 @@
|
|||
from nhf.materials import Material, Role
|
||||
from nhf.parts.item import Item
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
18
nhf/utils.py
18
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
|
||||
|
|
Loading…
Reference in New Issue