feat: Submodel in build system

This commit is contained in:
Leni Aniva 2024-07-16 15:42:39 -07:00
parent 0cc6100d0e
commit 66b26fa056
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
6 changed files with 239 additions and 163 deletions

View File

@ -17,11 +17,13 @@ class BuildScaffold(Model):
"""
from enum import Enum
from pathlib import Path
from typing import Union
from typing import Union, Optional
from functools import wraps
import traceback
from colorama import Fore, Style
import cadquery as Cq
import nhf.checks as NC
import nhf.utils
TOL=1e-6
@ -40,7 +42,7 @@ class Target:
def __init__(self,
method,
name: str,
name: Optional[str] = None,
prototype: bool = False,
kind: TargetKind = TargetKind.STL,
**kwargs):
@ -58,11 +60,14 @@ class Target:
return self._method(obj, *args, **kwargs)
@property
def file_name(self):
def file_name(self) -> Optional[str]:
"""
Output file name
"""
if self.name:
return f"{self.name}.{self.kind.ext}"
else:
return None
def write_to(self, obj, path: str):
x = self._method(obj)
@ -75,6 +80,14 @@ class Target:
x = x.toCompound().fuse(tol=TOL)
x.exportStl(path, **self.kwargs)
elif self.kind == TargetKind.DXF:
if isinstance(x, Cq.Sketch):
# https://github.com/CadQuery/cadquery/issues/1575
x = (
Cq.Workplane()
.add(x._faces)
.add(x._wires)
.add(x._edges)
)
assert isinstance(x, Cq.Workplane)
Cq.exporters.exportDXF(x, path, **self.kwargs)
else:
@ -95,7 +108,7 @@ class Target:
return {method.name: method for method in g()}
def target(name, **deco_kwargs):
def target(**deco_kwargs):
"""
Decorator for annotating a build output
"""
@ -103,7 +116,7 @@ def target(name, **deco_kwargs):
@wraps(method)
def wrapper(self, *args, **kwargs):
return method(self, *args, **kwargs)
wrapper._target = Target(method, name, **deco_kwargs)
wrapper._target = Target(method, **deco_kwargs)
return wrapper
return f
@ -161,11 +174,74 @@ def assembly(**deco_kwargs):
return wrapper
return f
class Submodel:
"""
Marks a function's output as a submodel
"""
def __init__(self,
method,
name: str,
prototype: bool = False,
**kwargs):
self._method = method
self.name = name
self.prototype = prototype
self.kwargs = kwargs
def __str__(self):
return f"<submodel {self.name} {self._method}>"
def __call__(self, obj, *args, **kwargs):
"""
Raw call function which passes arguments directly to `_method`
"""
return self._method(obj, *args, **kwargs)
@property
def file_name(self):
"""
Output file name
"""
return self.name
def write_to(self, obj, path: str):
x = self._method(obj)
assert isinstance(x, Model), f"Unexpected type: {type(x)}"
x.build_all(path)
@classmethod
def methods(cls, subject):
"""
List of all methods of a class or objects annotated with this decorator.
"""
def g():
for name in dir(subject):
if name == 'target_names':
continue
method = getattr(subject, name)
if hasattr(method, '_submodel'):
yield method._submodel
return {method.name: method for method in g()}
def submodel(name, **deco_kwargs):
"""
Decorator for annotating a build output
"""
def f(method):
@wraps(method)
def wrapper(self, *args, **kwargs):
return method(self, *args, **kwargs)
wrapper._submodel = Submodel(method, name, **deco_kwargs)
return wrapper
return f
class Model:
"""
Base class for a parametric assembly
"""
def __init__(self, name: str):
def __init__(self, name: Optional[str] = None):
self.name = name
@property
@ -193,8 +269,15 @@ class Model:
Build all targets in this model and write the results to file
"""
output_dir = Path(output_dir)
for t in Target.methods(self).values():
output_file = output_dir / self.name / t.file_name
targets = Target.methods(self)
for t in targets.values():
file_name = t.file_name
if file_name is None:
assert len(targets) == 1, "Only one anonymous target is permitted"
output_file = output_dir.with_suffix('.' + t.kind.ext)
else:
output_file = output_dir / file_name
if output_file.is_file():
if verbose >= 1:
print(f"{Fore.GREEN}Skipping{Style.RESET_ALL} {output_file}")
@ -210,3 +293,8 @@ class Model:
print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}")
except Exception as e:
print(f"{Fore.RED}Failed to build{Style.RESET_ALL} {output_file}: {e}")
traceback.print_exc()
for t in Submodel.methods(self).values():
d = output_dir / t.name
t.write_to(self, str(d))

View File

@ -1,6 +1,7 @@
import cadquery as Cq
from dataclasses import dataclass, field
from typing import Tuple, Optional, Union
from nhf.build import Model, TargetKind, target
import nhf.utils
def box_with_centre_holes(
@ -34,7 +35,7 @@ class Hole:
tag: Optional[str] = None
@dataclass
class MountingBox:
class MountingBox(Model):
"""
Create a box with marked holes
"""
@ -53,6 +54,7 @@ class MountingBox:
generate_side_tags: bool = True
@target(kind=TargetKind.DXF)
def profile(self) -> Cq.Sketch:
bx, by = 0, 0
if not self.centred[0]:

View File

@ -32,9 +32,8 @@ shoulder, elbow, wrist in analogy with human anatomy.
from dataclasses import dataclass, field
from typing import Optional
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.parts.joints import HirthJoint, TorsionJoint
from nhf.parts.handle import Handle, BayonetMount
import nhf.touhou.houjuu_nue.wing as MW
import nhf.touhou.houjuu_nue.trident as MT
import nhf.touhou.houjuu_nue.joints as MJ
@ -47,9 +46,6 @@ class Parameters(Model):
Defines dimensions for the Houjuu Nue cosplay
"""
# Thickness of the exoskeleton panel in millimetres
panel_thickness: float = 25.4 / 16
# Harness
harness: MH.Harness = field(default_factory=lambda: MH.Harness())
@ -61,120 +57,53 @@ class Parameters(Model):
n_tooth=24
))
wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile(
shoulder_joint=MJ.ShoulderJoint(
height=100.0,
),
elbow_height=110.0,
))
wing_r1: MW.WingProfile = field(default_factory=lambda: MW.WingProfile(name="r1"))
wing_r2: MW.WingProfile = field(default_factory=lambda: MW.WingProfile(name="r2"))
wing_r3: MW.WingProfile = field(default_factory=lambda: MW.WingProfile(name="r3"))
# Exterior radius of the wing root assembly
wing_root_radius: float = 40
wing_root_wall_thickness: float = 8
"""
Heights for various wing joints, where the numbers start from the first
joint.
"""
wing_s0_thickness: float = 40
# Length of the spacer
wing_s1_thickness: float = 20
wing_s1_spacer_thickness: float = 25.4 / 8
wing_s1_spacer_width: float = 20
wing_s1_spacer_hole_diam: float = 8
wing_s1_shoulder_spacer_hole_dist: float = 20
wing_s1_shoulder_spacer_width: float = 60
trident_handle: Handle = field(default_factory=lambda: Handle(
diam=38,
diam_inner=38-2 * 25.4/8,
diam_connector_internal=18,
simplify_geometry=False,
mount=BayonetMount(n_pin=3),
))
trident_terminal_height: float = 80
trident_terminal_hole_diam: float = 24
trident_terminal_bottom_thickness: float = 10
trident: MT.Trident = field(default_factory=lambda: MT.Trident())
def __post_init__(self):
super().__init__(name="houjuu-nue")
self.harness.hs_hirth_joint = self.hs_hirth_joint
self.wing_profile.base_joint = self.hs_hirth_joint
assert self.wing_root_radius > self.hs_hirth_joint.radius, \
"Wing root must be large enough to accomodate joint"
assert self.wing_s1_shoulder_spacer_hole_dist > self.wing_s1_spacer_hole_diam, \
"Spacer holes are too close to each other"
self.wing_r1.base_joint = self.hs_hirth_joint
self.wing_r2.base_joint = self.hs_hirth_joint
self.wing_r3.base_joint = self.hs_hirth_joint
@target(name="trident/handle-connector")
def handle_connector(self):
return self.trident_handle.connector()
@target(name="trident/handle-insertion")
def handle_insertion(self):
return self.trident_handle.insertion()
@target(name="trident/proto-handle-connector", prototype=True)
def proto_handle_connector(self):
return self.trident_handle.one_side_connector(height=15)
@target(name="trident/handle-terminal-connector")
def handle_terminal_connector(self):
result = self.trident_handle.one_side_connector(height=self.trident_terminal_height)
#result.faces("<Z").circle(radius=25/2).cutThruAll()
h = self.trident_terminal_height + self.trident_handle.insertion_length - self.trident_terminal_bottom_thickness
result = result.faces(">Z").hole(self.trident_terminal_hole_diam, depth=h)
return result
@submodel(name="harness")
def submodel_harness(self) -> Model:
return self.harness
@target(name="harness", kind=TargetKind.DXF)
def harness_profile(self) -> Cq.Sketch:
return self.harness.profile()
def harness_surface(self) -> Cq.Workplane:
return self.harness.surface()
def hs_joint_parent(self) -> Cq.Workplane:
return self.harness.hs_joint_parent()
@assembly()
def harness_assembly(self) -> Cq.Assembly:
return self.harness.assembly()
@target(name="wing/proto-shoulder-joint-parent", prototype=True)
def proto_shoulder_joint_parent(self):
return self.wing_profile.shoulder_joint.torsion_joint.track()
@target(name="wing/proto-shoulder-joint-child", prototype=True)
def proto_shoulder_joint_child(self):
return self.wing_profile.shoulder_joint.torsion_joint.rider()
@assembly()
def wing_r1_assembly(self, parts: Optional[list[str]] = None) -> Cq.Assembly:
return self.wing_profile.assembly(parts)
@submodel(name="wing-r1")
def submodel_wing_r1(self) -> Model:
return self.wing_r1
@submodel(name="wing-r2")
def submodel_wing_r2(self) -> Model:
return self.wing_r2
@submodel(name="wing-r3")
def submodel_wing_r3(self) -> Model:
return self.wing_r3
@assembly()
def wings_harness_assembly(self, parts: Optional[list[str]] = None) -> Cq.Assembly:
"""
Assembly of harness with all the wings
"""
a_tooth = self.hs_hirth_joint.tooth_angle
result = (
Cq.Assembly()
.add(self.harness_assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
.add(self.wing_r1_assembly(parts), name="wing_r1")
.add(self.wing_r1_assembly(parts), name="wing_r2")
.add(self.wing_r1_assembly(parts), name="wing_r3")
.add(self.harness.assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
.add(self.wing_r1.assembly(parts), name="wing_r1")
.add(self.wing_r2.assembly(parts), name="wing_r2")
.add(self.wing_r3.assembly(parts), name="wing_r3")
)
self.hs_hirth_joint.add_constraints(result, "harness/r1", "wing_r1/s0/hs", offset=9)
self.hs_hirth_joint.add_constraints(result, "harness/r2", "wing_r2/s0/hs", offset=8)
self.hs_hirth_joint.add_constraints(result, "harness/r3", "wing_r3/s0/hs", offset=7)
return result.solve()
@assembly(collision_check=False)
def trident_assembly(self) -> Cq.Assembly:
"""
Disable collision check since the threads may not align.
"""
return MT.trident_assembly(self.trident_handle)
@submodel(name="trident")
def submodel_trident(self) -> Model:
return self.trident
if __name__ == '__main__':

View File

@ -2,10 +2,11 @@ from dataclasses import dataclass, field
import cadquery as Cq
from nhf.parts.joints import HirthJoint
from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly
import nhf.utils
@dataclass
class Harness:
class Harness(Model):
thickness: float = 25.4 / 8
width: float = 300.0
height: float = 400.0
@ -40,6 +41,10 @@ class Harness:
hs_joint_axis_cbore_diam: float = 20
hs_joint_axis_cbore_depth: float = 3
def __post_init__(self):
super().__init__(name="harness")
@target(name="profile", kind=TargetKind.DXF)
def profile(self) -> Cq.Sketch:
"""
Creates the harness shape
@ -100,6 +105,7 @@ class Harness:
(-dx, dx),
]
@target(name="hs-joint-parent")
def hs_joint_parent(self):
"""
Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth
@ -150,6 +156,7 @@ class Harness:
result.faces("<Z").tag("base")
return result
@assembly()
def assembly(self) -> Cq.Assembly:
harness = self.surface()
result = (

View File

@ -1,20 +1,53 @@
import math
from dataclasses import dataclass, field
import cadquery as Cq
from nhf import Material, Role
from nhf.parts.handle import Handle
from nhf.parts.handle import Handle, BayonetMount
from nhf.build import Model, target, assembly
import nhf.utils
def trident_assembly(
handle: Handle,
handle_segment_length: float = 24*25.4,
terminal_height=100):
@dataclass
class Trident(Model):
handle: Handle = field(default_factory=lambda: Handle(
diam=38,
diam_inner=38-2 * 25.4/8,
diam_connector_internal=18,
simplify_geometry=False,
mount=BayonetMount(n_pin=3),
))
terminal_height: float = 80
terminal_hole_diam: float = 24
terminal_bottom_thickness: float = 10
segment_length: float = 24 * 25.4
@target(name="handle-connector")
def handle_connector(self):
return self.handle.connector()
@target(name="handle-insertion")
def handle_insertion(self):
return self.handle.insertion()
@target(name="proto-handle-terminal-connector", prototype=True)
def proto_handle_connector(self):
return self.handle.one_side_connector(height=15)
@target(name="handle-terminal-connector")
def handle_terminal_connector(self):
result = self.handle.one_side_connector(height=self.terminal_height)
#result.faces("<Z").circle(radius=25/2).cutThruAll()
h = self.terminal_height + self.handle.insertion_length - self.terminal_bottom_thickness
result = result.faces(">Z").hole(self.terminal_hole_diam, depth=h)
return result
@assembly()
def assembly(self):
def segment():
return handle.segment(handle_segment_length)
return self.handle.segment(self.segment_length)
terminal = (
handle
.one_side_connector(height=terminal_height)
self.handle
.one_side_connector(height=self.terminal_height)
.faces(">Z")
.hole(15, terminal_height + handle.insertion_length - 10)
.hole(15, self.terminal_height + self.handle.insertion_length - 10)
)
mat_c = Material.PLASTIC_PLA
mat_i = Material.RESIN_TOUGH_1500
@ -22,19 +55,19 @@ def trident_assembly(
role_i = Role.CONNECTION
role_c = Role.CONNECTION
role_s = Role.STRUCTURE
assembly = (
a = (
Cq.Assembly()
.addS(handle.insertion(), name="i0",
.addS(self.handle.insertion(), name="i0",
material=mat_i, role=role_i)
.constrain("i0", "Fixed")
.addS(segment(), name="s1",
material=mat_s, role=role_s)
.constrain("i0?rim", "s1?mate1", "Plane", param=0)
.addS(handle.insertion(), name="i1",
.addS(self.handle.insertion(), name="i1",
material=mat_i, role=role_i)
.addS(handle.connector(), name="c1",
.addS(self.handle.connector(), name="c1",
material=mat_c, role=role_c)
.addS(handle.insertion(), name="i2",
.addS(self.handle.insertion(), name="i2",
material=mat_i, role=role_i)
.constrain("s1?mate2", "i1?rim", "Plane", param=0)
.constrain("i1?mate", "c1?mate1", "Plane")
@ -42,14 +75,14 @@ def trident_assembly(
.addS(segment(), name="s2",
material=mat_s, role=role_s)
.constrain("i2?rim", "s2?mate1", "Plane", param=0)
.addS(handle.insertion(), name="i3",
.addS(self.handle.insertion(), name="i3",
material=mat_i, role=role_i)
.constrain("s2?mate2", "i3?rim", "Plane", param=0)
.addS(handle.one_side_connector(), name="head",
.addS(self.handle.one_side_connector(), name="head",
material=mat_c, role=role_c)
.constrain("i3?mate", "head?mate", "Plane")
.addS(terminal, name="terminal",
material=mat_c, role=role_c)
.constrain("i0?mate", "terminal?mate", "Plane")
)
return assembly.solve()
return a.solve()

View File

@ -8,7 +8,7 @@ from dataclasses import dataclass, field
from typing import Mapping, Tuple, Optional
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.parts.box import box_with_centre_holes, MountingBox, Hole
from nhf.parts.joints import HirthJoint
from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint, DiskJoint
@ -27,7 +27,9 @@ class WingProfile(Model):
panel_thickness: float = 25.4 / 16
spacer_thickness: float = 25.4 / 8
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint())
shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint(
height=100.0,
))
shoulder_width: float = 30.0
shoulder_tip_x: float = -200.0
shoulder_tip_y: float = 160.0
@ -40,7 +42,7 @@ class WingProfile(Model):
),
flip=False,
))
elbow_height: float = 100
elbow_height: float = 110
elbow_x: float = 240
elbow_y: float = 30
# Tilt of elbow w.r.t. shoulder
@ -104,6 +106,7 @@ class WingProfile(Model):
def root_height(self) -> float:
return self.shoulder_joint.height
@target(name="profile-s0", kind=TargetKind.DXF)
def profile_s0(self) -> Cq.Sketch:
tip_x = self.shoulder_tip_x
tip_y = self.shoulder_tip_y
@ -127,6 +130,7 @@ class WingProfile(Model):
)
return sketch
@submodel(name="spacer-s0-shoulder")
def spacer_s0_shoulder(self) -> MountingBox:
"""
Should be cut
@ -147,6 +151,7 @@ class WingProfile(Model):
hole_diam=self.shoulder_joint.parent_conn_hole_diam,
centred=(True, True),
)
@submodel(name="spacer-s0-shoulder")
def spacer_s0_base(self) -> MountingBox:
"""
Should be cut
@ -179,6 +184,7 @@ class WingProfile(Model):
reverse=not top,
)
@assembly()
def assembly_s0(self) -> Cq.Assembly:
result = (
Cq.Assembly()
@ -335,6 +341,7 @@ class WingProfile(Model):
]
@target(name="profile-s1", kind=TargetKind.DXF)
def profile_s1(self) -> Cq.Sketch:
profile = (
self.profile()
@ -365,6 +372,7 @@ class WingProfile(Model):
profile = self.profile_s1()
tags = tags_shoulder + tags_elbow
return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
@submodel(name="spacer-s1-shoulder")
def spacer_s1_shoulder(self) -> MountingBox:
holes = [
Hole(x)
@ -377,6 +385,7 @@ class WingProfile(Model):
holes=holes,
hole_diam=self.shoulder_joint.child_conn_hole_diam,
)
@submodel(name="spacer-s1-elbow")
def spacer_s1_elbow(self) -> MountingBox:
holes = [
Hole(x)
@ -389,6 +398,7 @@ class WingProfile(Model):
holes=holes,
hole_diam=self.elbow_joint.hole_diam,
)
@assembly()
def assembly_s1(self) -> Cq.Assembly:
result = (
Cq.Assembly()
@ -412,7 +422,7 @@ class WingProfile(Model):
)
return result.solve()
@target(name="profile-s2", kind=TargetKind.DXF)
def profile_s2(self) -> Cq.Sketch:
profile = (
self.profile()
@ -450,6 +460,7 @@ class WingProfile(Model):
profile = self.profile_s2()
tags = tags_elbow + tags_wrist
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
@submodel(name="spacer-s2-elbow")
def spacer_s2_elbow(self) -> MountingBox:
holes = [
Hole(x)
@ -462,6 +473,7 @@ class WingProfile(Model):
holes=holes,
hole_diam=self.elbow_joint.hole_diam,
)
@submodel(name="spacer-s2-wrist")
def spacer_s2_wrist(self) -> MountingBox:
holes = [
Hole(x)
@ -474,6 +486,7 @@ class WingProfile(Model):
holes=holes,
hole_diam=self.wrist_joint.hole_diam,
)
@assembly()
def assembly_s2(self) -> Cq.Assembly:
result = (
Cq.Assembly()
@ -497,6 +510,7 @@ class WingProfile(Model):
)
return result.solve()
@target(name="profile-s3", kind=TargetKind.DXF)
def profile_s3(self) -> Cq.Sketch:
profile = (
self.profile()
@ -519,6 +533,7 @@ class WingProfile(Model):
]
profile = self.profile_s3()
return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front)
@submodel(name="spacer-s3-wrist")
def spacer_s3_wrist(self) -> MountingBox:
holes = [
Hole(x)
@ -531,6 +546,7 @@ class WingProfile(Model):
holes=holes,
hole_diam=self.wrist_joint.hole_diam
)
@assembly()
def assembly_s3(self) -> Cq.Assembly:
result = (
Cq.Assembly()
@ -554,6 +570,7 @@ class WingProfile(Model):
)
return result.solve()
@assembly()
def assembly(self,
parts: Optional[list[str]] = None,
angle_elbow_wrist: float = 0.0,