cosplay: Touhou/Houjuu Nue #4

Open
aniva wants to merge 189 commits from touhou/houjuu-nue into main
5 changed files with 193 additions and 20 deletions
Showing only changes of commit 7cb00c0794 - Show all commits

View File

@ -1 +1,2 @@
from nhf.materials import Material, Role 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() r, g, b, _ = Cq.Color(name).toTuple()
return Cq.Color(r, g, b, alpha) return Cq.Color(r, g, b, alpha)
KEY_ROLE = 'role'
KEY_MATERIAL = 'material'
KEY_ITEM = 'item'
class Role(Flag): class Role(Flag):
""" """
Describes the role of a part Describes the role of a part
@ -26,7 +31,10 @@ class Role(Flag):
STRUCTURE = auto() STRUCTURE = auto()
DECORATION = auto() DECORATION = auto()
ELECTRONIC = auto() ELECTRONIC = auto()
# Fasteners, etc.
CONNECTION = auto() CONNECTION = auto()
HANDLE = auto()
# Parent and child components in a load bearing joint # Parent and child components in a load bearing joint
@ -54,7 +62,8 @@ ROLE_COLOR_MAP = {
Role.STRUCTURE: _color('gray', 0.4), Role.STRUCTURE: _color('gray', 0.4),
Role.DECORATION: _color('lightseagreen', 0.4), Role.DECORATION: _color('lightseagreen', 0.4),
Role.ELECTRONIC: _color('mediumorchid', 0.5), 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 = {} metadata = {}
color = None color = None
if material: if material:
metadata["material"] = material metadata[KEY_MATERIAL] = material
color = material.color color = material.color
if role: if role:
metadata["role"] = role metadata[KEY_ROLE] = role
color = role.color_avg() color = role.color_avg()
if len(metadata) == 0: if len(metadata) == 0:
metadata = None metadata = None
@ -106,29 +115,16 @@ Cq.Assembly.addS = add_with_material_role
def color_by_material(self: Cq.Assembly) -> Cq.Assembly: def color_by_material(self: Cq.Assembly) -> Cq.Assembly:
for _, a in self.traverse(): for _, a in self.traverse():
if 'material' not in a.metadata: if KEY_MATERIAL not in a.metadata:
continue continue
a.color = a.metadata["material"].color a.color = a.metadata[KEY_MATERIAL].color
return self return self
Cq.Assembly.color_by_material = color_by_material Cq.Assembly.color_by_material = color_by_material
def color_by_role(self: Cq.Assembly, avg: bool = True) -> Cq.Assembly: def color_by_role(self: Cq.Assembly, avg: bool = True) -> Cq.Assembly:
for _, a in self.traverse(): for _, a in self.traverse():
if 'role' not in a.metadata: if KEY_ROLE not in a.metadata:
continue continue
role = a.metadata["role"] role = a.metadata[KEY_ROLE]
a.color = role.color_avg() if avg else role.color_head() a.color = role.color_avg() if avg else role.color_head()
return self return self
Cq.Assembly.color_by_role = color_by_role 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 import cadquery as Cq
from nhf import Role from nhf import Role
from typing import Union, Tuple, cast from typing import Union, Tuple, cast
from nhf.materials import KEY_ITEM, KEY_MATERIAL
# Bug fixes # Bug fixes
def _subloc(self, name: str) -> Tuple[Cq.Location, str]: def _subloc(self, name: str) -> Tuple[Cq.Location, str]:
@ -204,3 +205,20 @@ def get_abs_location(self: Cq.Assembly,
return loc return loc
Cq.Assembly.get_abs_location = get_abs_location 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