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