From 4613247e1b2453d3c46d18025598769c02d32ec8 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 19 Jun 2024 15:54:09 -0700 Subject: [PATCH 001/187] feat: Hirth Joint for wing root --- nhf/diag.py | 42 +++++++ nhf/joints.py | 106 ++++++++++++++++++ nhf/touhou/__init__.py | 0 nhf/touhou/houjuu_nue/README.org | 14 +++ nhf/touhou/houjuu_nue/__init__.py | 17 +++ .../houjuu_nue/controller/controller.ino | 68 +++++++++++ 6 files changed, 247 insertions(+) create mode 100644 nhf/diag.py create mode 100644 nhf/joints.py create mode 100644 nhf/touhou/__init__.py create mode 100644 nhf/touhou/houjuu_nue/README.org create mode 100644 nhf/touhou/houjuu_nue/__init__.py create mode 100644 nhf/touhou/houjuu_nue/controller/controller.ino diff --git a/nhf/diag.py b/nhf/diag.py new file mode 100644 index 0000000..1648383 --- /dev/null +++ b/nhf/diag.py @@ -0,0 +1,42 @@ +import cadquery as Cq + +def tidy_repr(obj): + """Shortens a default repr string""" + return repr(obj).split(".")[-1].rstrip(">") + + +def _ctx_str(self): + return ( + tidy_repr(self) + + ":\n" + + f" pendingWires: {self.pendingWires}\n" + + f" pendingEdges: {self.pendingEdges}\n" + + f" tags: {self.tags}" + ) + + +Cq.cq.CQContext.__str__ = _ctx_str + + +def _plane_str(self): + return ( + tidy_repr(self) + + ":\n" + + f" origin: {self.origin.toTuple()}\n" + + f" z direction: {self.zDir.toTuple()}" + ) + + +Cq.occ_impl.geom.Plane.__str__ = _plane_str + + +def _wp_str(self): + out = tidy_repr(self) + ":\n" + out += f" parent: {tidy_repr(self.parent)}\n" if self.parent else " no parent\n" + out += f" plane: {self.plane}\n" + out += f" objects: {self.objects}\n" + out += f" modelling context: {self.ctx}" + return out + + +Cq.Workplane.__str__ = _wp_str diff --git a/nhf/joints.py b/nhf/joints.py new file mode 100644 index 0000000..805d89e --- /dev/null +++ b/nhf/joints.py @@ -0,0 +1,106 @@ +import cadquery as Cq +import math +import unittest + +def hirth_joint(radius=60, + radius_inner=40, + radius_centre=30, + base_height=20, + n_tooth=16, + tooth_height=16, + tooth_height_inner=2): + """ + Creates a cylindrical Hirth Joint + """ + # ensures secant doesn't blow up + assert n_tooth >= 5 + + # angle of half of a single tooth + theta = math.pi / n_tooth + + # Generate a tooth by lofting between two curves + + inner_raise = (tooth_height - tooth_height_inner) / 2 + # Outer tooth triangle spans a curve of length `2 pi r / n_tooth`. This + # creates the side profile (looking radially inwards) of each of the + # triangles. + outer = [ + (radius * math.tan(theta), 0), + (0, tooth_height), + (-radius * math.tan(theta), 0), + ] + inner = [ + (radius_inner * math.sin(theta), 0), + (radius_inner * math.sin(theta), inner_raise - tooth_height_inner / 2), + (0, inner_raise + tooth_height_inner / 2), + (-radius_inner * math.sin(theta), inner_raise - tooth_height_inner / 2), + (-radius_inner * math.sin(theta), 0), + ] + tooth = ( + Cq.Workplane('YZ') + .polyline(inner) + .close() + .workplane(offset=radius - radius_inner) + .polyline(outer) + .close() + .loft(combine=True) + .val() + ) + tooth_centre_radius = radius_inner * math.cos(theta) + teeth = ( + Cq.Workplane('XY') + .polarArray(radius=radius_inner, startAngle=0, angle=360, count=n_tooth) + .eachpoint(lambda loc: tooth.located(loc)) + .intersect(Cq.Solid.makeCylinder( + height=base_height + tooth_height, + radius=radius, + )) + ) + base = ( + Cq.Workplane('XY') + .cylinder( + height=base_height, + radius=radius, + centered=(True, True, False)) + .faces(">Z").tag("bore") + .union(teeth.val().move(Cq.Location((0,0,base_height)))) + .clean() + ) + #base.workplane(offset=tooth_height/2).circle(radius=radius,forConstruction=True).tag("mate") + base.polyline([(0, 0, 0), (0, 0, 1)], forConstruction=True).tag("mate") + return base + +def hirth_assembly(): + """ + Example assembly of two Hirth joints + """ + rotate = 180 / 16 + obj1 = hirth_joint().faces(tag="bore").cboreHole( + diameter=10, + cboreDiameter=20, + cboreDepth=3) + obj2 = ( + hirth_joint() + .rotate( + axisStartPoint=(0,0,0), + axisEndPoint=(0,0,1), + angleDegrees=rotate + ) + ) + result = ( + Cq.Assembly() + .add(obj1, name="obj1", color=Cq.Color(0.8,0.8,0.5,0.3)) + .add(obj2, name="obj2", color=Cq.Color(0.5,0.5,0.5,0.3), loc=Cq.Location((0,0,80))) + .constrain("obj1?mate", "obj2?mate", "Axis") + .solve() + ) + return result + +class TestJoints(unittest.TestCase): + + def test_hirth_assembly(self): + print(Cq.__version__) + hirth_assembly() + +if __name__ == '__main__': + unittest.main() diff --git a/nhf/touhou/__init__.py b/nhf/touhou/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nhf/touhou/houjuu_nue/README.org b/nhf/touhou/houjuu_nue/README.org new file mode 100644 index 0000000..1649107 --- /dev/null +++ b/nhf/touhou/houjuu_nue/README.org @@ -0,0 +1,14 @@ +#+title: Cosplay: Houjuu Nue + +* Controller + +This part describes the electrical connections and the microcontroller code. + +* Structure + +This part describes the 3d printed and laser cut structures. ~structure.blend~ +is an overall sketch of the shapes and looks of the wing. + +* Pattern + +This part describes the sewing patterns. diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py new file mode 100644 index 0000000..f91d538 --- /dev/null +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -0,0 +1,17 @@ +import cadquery as Cq + +def mystery(): + return ( + Cq.Workplane() + .box(1, 1, 1) + .tag("base") + .wires(">Z") + .toPending() + .translate((0.1, 0.1, 1.0)) + .toPending() + .loft() + .faces(">>X", tag="base") + .workplane(centerOption="CenterOfMass") + .circle(0.2) + .extrude(3) + ) diff --git a/nhf/touhou/houjuu_nue/controller/controller.ino b/nhf/touhou/houjuu_nue/controller/controller.ino new file mode 100644 index 0000000..245d39d --- /dev/null +++ b/nhf/touhou/houjuu_nue/controller/controller.ino @@ -0,0 +1,68 @@ +#include + +// Main LED strip setup +#define LED_PIN 5 +#define NUM_LEDS 100 +#define LED_PART 50 +#define BRIGHTNESS 250 +#define LED_TYPE WS2811 +CRGB leds[NUM_LEDS]; + +CRGB color_red; +CRGB color_blue; +CRGB color_green; + +#define DIAG_PIN 6 + + +void setup() { + // Calculate colors + hsv2rgb_spectrum(CHSV(4, 255, 100), color_red); + hsv2rgb_spectrum(CHSV(170, 255, 100), color_blue); + hsv2rgb_spectrum(CHSV(90, 255, 100), color_green); + pinMode(LED_BUILTIN, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(DIAG_PIN, OUTPUT); + + // Main LED strip + FastLED.addLeds(leds, NUM_LEDS); +} + +void loop() { + fill_segmented(CRGB::Green, CRGB::Orange); + delay(500); + + flash(leds, NUM_LEDS, color_red, 10, 20); + delay(500); + flash(leds, NUM_LEDS, color_blue, 10, 20); + delay(500); +} + +void fill_segmented(CRGB c1, CRGB c2) +{ + //fill_solid(leds, LED_PART, c1); + fill_gradient_RGB(leds, LED_PART, CRGB::Black ,c1); + fill_gradient_RGB(leds + LED_PART, NUM_LEDS - LED_PART, CRGB::Black, c2); + FastLED.show(); +} +void flash(CRGB *ptr, uint16_t num, CRGB const& lead, int steps, int step_time) +{ + digitalWrite(LED_BUILTIN, LOW); + + //fill_solid(leds, NUM_LEDS, CRGB::Black); + for (int i = 0; i < steps; ++i) + { + uint8_t factor = 255 * i / steps; + analogWrite(DIAG_PIN, factor); + CRGB tail = blend(lead, CRGB::Black, factor); + uint16_t front = factor * (int) num / 255; + fill_solid(ptr, front, tail); + //fill_gradient_RGB(ptr, front, tail, lead); + //fill_solid(leds + front, NUM_LEDS - front, CRGB::Black); + FastLED.show(); + delay(step_time); + } + fill_gradient_RGB(ptr, num, CRGB::Black, lead); + FastLED.show(); + analogWrite(DIAG_PIN, LOW); +} \ No newline at end of file -- 2.44.1 From a3f2b01b8c783976c4f7aed842531573e619c90e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 19 Jun 2024 15:54:42 -0700 Subject: [PATCH 002/187] fix: Extraneous printing --- nhf/joints.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nhf/joints.py b/nhf/joints.py index 805d89e..b066c77 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -99,7 +99,6 @@ def hirth_assembly(): class TestJoints(unittest.TestCase): def test_hirth_assembly(self): - print(Cq.__version__) hirth_assembly() if __name__ == '__main__': -- 2.44.1 From 75c06585ed2a8050cabecca237bd9823c3039b13 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 19 Jun 2024 16:14:49 -0700 Subject: [PATCH 003/187] fix: Hirth joint mating line --- nhf/joints.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/nhf/joints.py b/nhf/joints.py index b066c77..9cdd0ad 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -8,7 +8,8 @@ def hirth_joint(radius=60, base_height=20, n_tooth=16, tooth_height=16, - tooth_height_inner=2): + tooth_height_inner=2, + tol=0.01): """ Creates a cylindrical Hirth Joint """ @@ -26,14 +27,16 @@ def hirth_joint(radius=60, # triangles. outer = [ (radius * math.tan(theta), 0), + (radius * math.tan(theta) - tol, 0), (0, tooth_height), + (-radius * math.tan(theta) + tol, 0), (-radius * math.tan(theta), 0), ] inner = [ (radius_inner * math.sin(theta), 0), - (radius_inner * math.sin(theta), inner_raise - tooth_height_inner / 2), - (0, inner_raise + tooth_height_inner / 2), - (-radius_inner * math.sin(theta), inner_raise - tooth_height_inner / 2), + (radius_inner * math.sin(theta), inner_raise), + (0, inner_raise + tooth_height_inner), + (-radius_inner * math.sin(theta), inner_raise), (-radius_inner * math.sin(theta), 0), ] tooth = ( @@ -43,13 +46,13 @@ def hirth_joint(radius=60, .workplane(offset=radius - radius_inner) .polyline(outer) .close() - .loft(combine=True) + .loft(ruled=True, combine=True) .val() ) tooth_centre_radius = radius_inner * math.cos(theta) teeth = ( Cq.Workplane('XY') - .polarArray(radius=radius_inner, startAngle=0, angle=360, count=n_tooth) + .polarArray(radius=tooth_centre_radius, startAngle=0, angle=360, count=n_tooth) .eachpoint(lambda loc: tooth.located(loc)) .intersect(Cq.Solid.makeCylinder( height=base_height + tooth_height, @@ -67,7 +70,7 @@ def hirth_joint(radius=60, .clean() ) #base.workplane(offset=tooth_height/2).circle(radius=radius,forConstruction=True).tag("mate") - base.polyline([(0, 0, 0), (0, 0, 1)], forConstruction=True).tag("mate") + base.polyline([(0, 0, base_height), (0, 0, base_height+tooth_height)], forConstruction=True).tag("mate") return base def hirth_assembly(): @@ -90,8 +93,8 @@ def hirth_assembly(): result = ( Cq.Assembly() .add(obj1, name="obj1", color=Cq.Color(0.8,0.8,0.5,0.3)) - .add(obj2, name="obj2", color=Cq.Color(0.5,0.5,0.5,0.3), loc=Cq.Location((0,0,80))) - .constrain("obj1?mate", "obj2?mate", "Axis") + .add(obj2, name="obj2", color=Cq.Color(0.5,0.5,0.5,0.3)) + .constrain("obj1?mate", "obj2?mate", "Plane") .solve() ) return result -- 2.44.1 From 8ad5eb9fe6c6668b420013b13d1a2f064166df2d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 19 Jun 2024 21:23:41 -0700 Subject: [PATCH 004/187] feat: Comma joint, Nue wing root stub --- nhf/joints.py | 129 ++++++++++++++++++++++++++++++ nhf/touhou/houjuu_nue/__init__.py | 47 +++++++---- 2 files changed, 161 insertions(+), 15 deletions(-) diff --git a/nhf/joints.py b/nhf/joints.py index 9cdd0ad..02c7527 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -99,10 +99,139 @@ def hirth_assembly(): ) return result +def comma_joint(radius=30, + shaft_radius=10, + height=10, + flange=10, + flange_thickness=25, + n_serration=16, + serration_angle_offset=0, + serration_height=5, + serration_inner_radius=20, + serration_theta=2 * math.pi / 48, + serration_tilt=-30, + right_handed=False): + """ + Produces a "o_" shaped joint, with serrations to accomodate a torsion spring + """ + assert flange_thickness <= radius + flange_poly = [ + (0, radius - flange_thickness), + (0, radius), + (flange + radius, radius), + (flange + radius, radius - flange_thickness) + ] + if right_handed: + flange_poly = [(x, -y) for x,y in flange_poly] + sketch = ( + Cq.Sketch() + .circle(radius) + .polygon(flange_poly, mode='a') + .circle(shaft_radius, mode='s') + ) + serration_poly = [ + (0, 0), (radius, 0), + (radius, radius * math.tan(serration_theta)) + ] + serration = ( + Cq.Workplane('XY') + .sketch() + .polygon(serration_poly) + .circle(radius, mode='i') + .circle(serration_inner_radius, mode='s') + .finalize() + .extrude(serration_height) + .translate(Cq.Vector((-serration_inner_radius, 0, height))) + .rotate( + axisStartPoint=(0, 0, 0), + axisEndPoint=(0, 0, height), + angleDegrees=serration_tilt) + .val() + ) + serrations = ( + Cq.Workplane('XY') + .polarArray(radius=serration_inner_radius, + startAngle=0+serration_angle_offset, + angle=360+serration_angle_offset, + count=n_serration) + .eachpoint(lambda loc: serration.located(loc)) + ) + result = ( + Cq.Workplane() + .add(sketch) + .extrude(height) + .union(serrations) + .clean() + ) + + result.polyline([ + (0, 0, height - serration_height), + (0, 0, height + serration_height)], + forConstruction=True).tag("serrated") + result.polyline([ + (0, radius, 0), + (flange + radius, radius, 0)], + forConstruction=True).tag("tail") + result.faces('>X').tag("tail_end") + return result + +def torsion_spring(radius=12, + height=20, + thickness=2, + omega=90, + tail_length=25): + """ + Produces a torsion spring with abridged geometry since sweep is very slow in + cq-editor. + """ + base = ( + Cq.Workplane('XY') + .cylinder(height=height, radius=radius, + centered=(True, True, False)) + ) + base.faces(">Z").tag("mate_top") + base.faces("Z") - .toPending() - .translate((0.1, 0.1, 1.0)) - .toPending() - .loft() - .faces(">>X", tag="base") - .workplane(centerOption="CenterOfMass") - .circle(0.2) - .extrude(3) - ) +@dataclass(frozen=True) +class Parameters: + + """ + Thickness of the exoskeleton panel in millimetres + """ + panel_thickness: float = 25.4 / 16 + + # Wing root properties + """ + Radius of the mounting mechanism of the wing root. This is constrained by + the size of the harness. + """ + root_radius: float = 60 + + def wing_root(self, + side_width=30, + side_height=100): + """ + Generate the wing root which contains a Hirth joint at its base and a + rectangular opening on its side, with the necessary interfaces. + """ + result = ( + Cq.Workplane("XY") + .circle(self.root_radius) + .transformed(offset=Cq.Vector(80, 0, 80), + rotate=Cq.Vector(0, 45, 0)) + .rect(side_width, side_height) + .loft() + ) + return result -- 2.44.1 From 133c69b846d77133a2edac792f9a90a801b2aaa1 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 20 Jun 2024 23:29:18 -0700 Subject: [PATCH 005/187] feat: Wing profile, unit testing --- README.md | 7 +++++ nhf/joints.py | 11 -------- nhf/test.py | 12 +++++++++ nhf/touhou/houjuu_nue/__init__.py | 44 +++++++++++++++++++++++++++++-- nhf/touhou/houjuu_nue/test.py | 14 ++++++++++ 5 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 nhf/test.py create mode 100644 nhf/touhou/houjuu_nue/test.py diff --git a/README.md b/README.md index 282f834..ae83ed0 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,10 @@ and this should succeed python3 -c "import nhf" ``` +## Testing + +Run all tests with +``` sh +python3 -m unittest +``` + diff --git a/nhf/joints.py b/nhf/joints.py index 02c7527..f809989 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -1,10 +1,8 @@ import cadquery as Cq import math -import unittest def hirth_joint(radius=60, radius_inner=40, - radius_centre=30, base_height=20, n_tooth=16, tooth_height=16, @@ -226,12 +224,3 @@ def comma_assembly(): ) return result -class TestJoints(unittest.TestCase): - - def test_hirth_assembly(self): - hirth_assembly() - def test_comma_assembly(self): - comma_assembly() - -if __name__ == '__main__': - unittest.main() diff --git a/nhf/test.py b/nhf/test.py new file mode 100644 index 0000000..03015ad --- /dev/null +++ b/nhf/test.py @@ -0,0 +1,12 @@ +import unittest +import nhf.joints + +class TestJoints(unittest.TestCase): + + def test_joints_hirth_assembly(self): + nhf.joints.hirth_assembly() + def test_joints_comma_assembly(self): + nhf.joints.comma_assembly() + +if __name__ == '__main__': + unittest.main() diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 8eac42c..f9ee0ee 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -1,5 +1,6 @@ -import cadquery as Cq from dataclasses import dataclass +import unittest +import cadquery as Cq @dataclass(frozen=True) class Parameters: @@ -16,9 +17,46 @@ class Parameters: """ root_radius: float = 60 + """ + Heights for various wing joints, where the numbers start from the first joint. + """ + wing_r1_height = 100 + wing_r1_width = 400 + wing_r2_height = 100 + wing_r3_height = 100 + + def wing_r1_profile(self) -> Cq.Sketch: + """ + Generates the first wing segment profile, with the wing root pointing in + the positive x axis. + """ + # Depression of the wing middle + bend = 200 + factor = 0.7 + result = ( + Cq.Sketch() + .segment((0, 0), (0, self.wing_r1_height)) + .spline([ + (0, self.wing_r1_height), + (0.5 * self.wing_r1_width, self.wing_r1_height - factor * bend), + (self.wing_r1_width, self.wing_r1_height - bend), + ]) + .segment( + (self.wing_r1_width, self.wing_r1_height - bend), + (self.wing_r1_width, -bend), + ) + .spline([ + (self.wing_r1_width, - bend), + (0.5 * self.wing_r1_width, - factor * bend), + (0, 0), + ]) + .assemble() + ) + return result + def wing_root(self, side_width=30, - side_height=100): + side_height=100) -> Cq.Shape: """ Generate the wing root which contains a Hirth joint at its base and a rectangular opening on its side, with the necessary interfaces. @@ -30,5 +68,7 @@ class Parameters: rotate=Cq.Vector(0, 45, 0)) .rect(side_width, side_height) .loft() + .val() ) return result + diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py new file mode 100644 index 0000000..0a9f148 --- /dev/null +++ b/nhf/touhou/houjuu_nue/test.py @@ -0,0 +1,14 @@ +import unittest +import nhf.touhou.houjuu_nue as M + +class Test(unittest.TestCase): + + def test_wing_root(self): + p = M.Parameters() + p.wing_root() + def test_wing_profiles(self): + p = M.Parameters() + p.wing_r1_profile() + +if __name__ == '__main__': + unittest.main() -- 2.44.1 From 0e5445ebb52a0ac8a5f6b0226b6c8e6135477af1 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 20 Jun 2024 23:45:24 -0700 Subject: [PATCH 006/187] feat: Nue wing R1 --- nhf/touhou/houjuu_nue/__init__.py | 10 ++++++++++ nhf/touhou/houjuu_nue/test.py | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index f9ee0ee..c4813ee 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -54,6 +54,16 @@ class Parameters: ) return result + def wing_r1(self) -> Cq.Solid: + profile = self.wing_r1_profile() + result = ( + Cq.Workplane("XY") + .placeSketch(profile) + .extrude(self.panel_thickness) + .val() + ) + return result + def wing_root(self, side_width=30, side_height=100) -> Cq.Shape: diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 0a9f148..7124936 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -6,9 +6,9 @@ class Test(unittest.TestCase): def test_wing_root(self): p = M.Parameters() p.wing_root() - def test_wing_profiles(self): + def test_wings(self): p = M.Parameters() - p.wing_r1_profile() + p.wing_r1() if __name__ == '__main__': unittest.main() -- 2.44.1 From 376580003eb33e1fbf08d353e5b2ae04303cb0d0 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 22 Jun 2024 13:40:06 -0700 Subject: [PATCH 007/187] feat: Base of Houjuu-Scarlett joint --- nhf/joints.py | 3 +- nhf/touhou/houjuu_nue/__init__.py | 70 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/nhf/joints.py b/nhf/joints.py index f809989..554d7b6 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -13,6 +13,7 @@ def hirth_joint(radius=60, """ # ensures secant doesn't blow up assert n_tooth >= 5 + assert radius > radius_inner # angle of half of a single tooth theta = math.pi / n_tooth @@ -64,7 +65,7 @@ def hirth_joint(radius=60, radius=radius, centered=(True, True, False)) .faces(">Z").tag("bore") - .union(teeth.val().move(Cq.Location((0,0,base_height)))) + .union(teeth.val().move(Cq.Location((0,0,base_height))), tol=tol) .clean() ) #base.workplane(offset=tooth_height/2).circle(radius=radius,forConstruction=True).tag("mate") diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index c4813ee..8d2c0c8 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -1,6 +1,7 @@ from dataclasses import dataclass import unittest import cadquery as Cq +import nhf.joints @dataclass(frozen=True) class Parameters: @@ -25,6 +26,75 @@ class Parameters: wing_r2_height = 100 wing_r3_height = 100 + """ + The Houjuu-Scarlett joint mechanism at the base of the wing + """ + hs_joint_base_width = 85 + hs_joint_base_thickness = 10 + hs_joint_ring_thickness = 10 + hs_joint_tooth_height = 10 + hs_joint_radius = 30 + hs_joint_radius_inner = 20 + hs_joint_corner_fillet = 5 + hs_joint_corner_cbore_diam = 12 + hs_joint_corner_cbore_depth = 12 + hs_joint_corner_diam = 15 + hs_joint_corner_inset = 15 + + def print_geometries(self): + return [ + self.hs_joint_parent() + ] + + def hs_joint_parent(self): + """ + Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth + coupling, a cylindrical base, and a mounting base. + """ + hirth = nhf.joints.hirth_joint( + radius=self.hs_joint_radius, + radius_inner=self.hs_joint_radius_inner, + tooth_height=self.hs_joint_tooth_height, + base_height=self.hs_joint_ring_thickness) + hole_rect_width = self.hs_joint_base_width - 2 * self.hs_joint_corner_inset + hirth = ( + hirth + .faces("Z") + .workplane() + .rect(hole_rect_width, hole_rect_width, forConstruction=True) + .vertices() + .cboreHole( + diameter=self.hs_joint_corner_diam, + cboreDiameter=self.hs_joint_corner_cbore_diam, + cboreDepth=self.hs_joint_corner_cbore_depth) + .faces(">Z") + .workplane() + .union(hirth.move((0, 0, self.hs_joint_base_thickness)), tol=0.1) + .clean() + ) + return result + + + def wing_r1_profile(self) -> Cq.Sketch: """ Generates the first wing segment profile, with the wing root pointing in -- 2.44.1 From eb8a48fe7789b57d03a32ce7ff9de2e2f0087111 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 23 Jun 2024 22:27:15 -0700 Subject: [PATCH 008/187] feat: Harness assembly --- nhf/__init__.py | 1 + nhf/materials.py | 18 ++++ nhf/touhou/houjuu_nue/__init__.py | 152 ++++++++++++++++++++++++++---- nhf/touhou/houjuu_nue/test.py | 3 + 4 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 nhf/materials.py diff --git a/nhf/__init__.py b/nhf/__init__.py index e69de29..2175f24 100644 --- a/nhf/__init__.py +++ b/nhf/__init__.py @@ -0,0 +1 @@ +from nhf.materials import Material diff --git a/nhf/materials.py b/nhf/materials.py new file mode 100644 index 0000000..7ca621b --- /dev/null +++ b/nhf/materials.py @@ -0,0 +1,18 @@ +""" +A catalog of material properties +""" +from enum import Enum +import cadquery as Cq + +def _color(name: str, alpha: float) -> Cq.Color: + r, g, b, _ = Cq.Color(name).toTuple() + return Cq.Color(r, g, b, alpha) + +class Material(Enum): + + WOOD_BIRCH = 0.8, _color('bisque', 0.9) + PLASTIC_PLA = 0.5, _color('azure3', 0.6) + + def __init__(self, density: float, color: Cq.Color): + self.density = density + self.color = color diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 8d2c0c8..7fd08e7 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -2,6 +2,7 @@ from dataclasses import dataclass import unittest import cadquery as Cq import nhf.joints +from nhf import Material @dataclass(frozen=True) class Parameters: @@ -11,8 +12,39 @@ class Parameters: """ panel_thickness: float = 25.4 / 16 + # Harness + harness_thickness: float = 25.4 / 8 + harness_width = 300 + harness_height = 400 + harness_fillet = 10 + + harness_wing_base_pos = [ + ("r1", 70, 150), + ("l1", -70, 150), + ("r2", 100, 0), + ("l2", -100, 0), + ("r3", 70, -150), + ("l3", -70, -150), + ] + + # Holes drilled onto harness for attachment with HS joint + harness_to_wing_base_hole_diam = 6 + # Wing root properties """ + The Houjuu-Scarlett joint mechanism at the base of the wing + """ + hs_joint_base_width = 85 + hs_joint_base_thickness = 10 + hs_joint_ring_thickness = 5 + hs_joint_tooth_height = 10 + hs_joint_radius = 30 + hs_joint_radius_inner = 20 + hs_joint_corner_fillet = 5 + hs_joint_corner_cbore_diam = 12 + hs_joint_corner_cbore_depth = 12 + hs_joint_corner_inset = 15 + """ Radius of the mounting mechanism of the wing root. This is constrained by the size of the harness. """ @@ -26,26 +58,77 @@ class Parameters: wing_r2_height = 100 wing_r3_height = 100 - """ - The Houjuu-Scarlett joint mechanism at the base of the wing - """ - hs_joint_base_width = 85 - hs_joint_base_thickness = 10 - hs_joint_ring_thickness = 10 - hs_joint_tooth_height = 10 - hs_joint_radius = 30 - hs_joint_radius_inner = 20 - hs_joint_corner_fillet = 5 - hs_joint_corner_cbore_diam = 12 - hs_joint_corner_cbore_depth = 12 - hs_joint_corner_diam = 15 - hs_joint_corner_inset = 15 def print_geometries(self): return [ self.hs_joint_parent() ] + def harness_profile(self) -> Cq.Sketch: + """ + Creates the harness shape + """ + w, h = self.harness_width / 2, self.harness_height / 2 + sketch = ( + Cq.Sketch() + .polygon([ + (0.7 * w, h), + (w, 0), + (0.7 * w, -h), + (0.7 * -w, -h), + (-w, 0), + (0.7 * -w, h), + ]) + #.rect(self.harness_width, self.harness_height) + .vertices() + .fillet(self.harness_fillet) + ) + for tag, x, y in self.harness_wing_base_pos: + conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] + sketch = ( + sketch + .push(conn) + .tag(tag) + .circle(self.harness_to_wing_base_hole_diam / 2, mode='s') + .reset() + ) + return sketch + + def harness(self) -> Cq.Shape: + """ + Creates the harness shape + """ + result = ( + Cq.Workplane('XZ') + .placeSketch(self.harness_profile()) + .extrude(self.harness_thickness) + ) + result.faces(">Y").tag("mount") + plane = result.faces(">Y").workplane() + for tag, x, y in self.harness_wing_base_pos: + conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] + for i, (px, py) in enumerate(conn): + ( + plane + .moveTo(px, py) + .circle(1, forConstruction='True') + .edges() + .tag(f"{tag}_{i}") + ) + return result + + def hs_joint_harness_conn(self) -> list[tuple[int, int]]: + """ + Generates a set of points corresponding to the connectorss + """ + dx = self.hs_joint_base_width / 2 - self.hs_joint_corner_inset + return [ + (dx, dx), + (dx, -dx), + (-dx, -dx), + (-dx, dx), + ] + def hs_joint_parent(self): """ Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth @@ -56,7 +139,6 @@ class Parameters: radius_inner=self.hs_joint_radius_inner, tooth_height=self.hs_joint_tooth_height, base_height=self.hs_joint_ring_thickness) - hole_rect_width = self.hs_joint_base_width - 2 * self.hs_joint_corner_inset hirth = ( hirth .faces("Z") .workplane() - .rect(hole_rect_width, hole_rect_width, forConstruction=True) - .vertices() + .pushPoints(conn) .cboreHole( - diameter=self.hs_joint_corner_diam, + diameter=self.harness_to_wing_base_hole_diam, cboreDiameter=self.hs_joint_corner_cbore_diam, cboreDepth=self.hs_joint_corner_cbore_depth) + ) + plane = result.faces(">Z").workplane(offset=-self.hs_joint_base_thickness) + for i, (px, py) in enumerate(conn): + ( + plane + .moveTo(px, py) + .circle(1, forConstruction='True') + .edges() + .tag(f"h{i}") + ) + result = ( + result .faces(">Z") .workplane() - .union(hirth.move((0, 0, self.hs_joint_base_thickness)), tol=0.1) + .union(hirth.move(Cq.Location((0, 0, self.hs_joint_base_thickness))), tol=0.1) .clean() ) + result.faces(" Date: Mon, 24 Jun 2024 11:05:03 -0700 Subject: [PATCH 009/187] feat: Use M12 centre hole for H-S joint --- nhf/touhou/houjuu_nue/__init__.py | 46 +++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 7fd08e7..ede9d9f 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -42,8 +42,14 @@ class Parameters: hs_joint_radius_inner = 20 hs_joint_corner_fillet = 5 hs_joint_corner_cbore_diam = 12 - hs_joint_corner_cbore_depth = 12 - hs_joint_corner_inset = 15 + hs_joint_corner_cbore_depth = 2 + hs_joint_corner_inset = 12 + + hs_joint_axis_diam = 12 + hs_joint_axis_cbore_diam = 20 + hs_joint_axis_cbore_depth = 3 + + """ Radius of the mounting mechanism of the wing root. This is constrained by the size of the harness. @@ -138,19 +144,19 @@ class Parameters: radius=self.hs_joint_radius, radius_inner=self.hs_joint_radius_inner, tooth_height=self.hs_joint_tooth_height, - base_height=self.hs_joint_ring_thickness) - hirth = ( - hirth - .faces(" Date: Mon, 24 Jun 2024 11:13:11 -0700 Subject: [PATCH 010/187] test: Joint integrity --- nhf/test.py | 5 +++++ nhf/touhou/houjuu_nue/test.py | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nhf/test.py b/nhf/test.py index 03015ad..9c215ff 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -1,8 +1,13 @@ import unittest +import cadquery as Cq import nhf.joints class TestJoints(unittest.TestCase): + def test_joint_hirth(self): + j = nhf.joints.hirth_joint() + assert isinstance(j.val().solids(), Cq.Solid),\ + "Hirth joint must be in one piece" def test_joints_hirth_assembly(self): nhf.joints.hirth_assembly() def test_joints_comma_assembly(self): diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 435d7e8..91808a7 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -1,11 +1,17 @@ import unittest +import cadquery as Cq import nhf.touhou.houjuu_nue as M class Test(unittest.TestCase): + def test_hs_joint_parent(self): + p = M.Parameters() + obj = p.hs_joint_parent() + assert isinstance(obj.val().solids(), Cq.Solid), "H-S joint must be in one piece" def test_wing_root(self): p = M.Parameters() - p.wing_root() + obj = p.wing_root() + assert isinstance(obj.solids(), Cq.Solid), "Wing root must be in one piece" def test_wings(self): p = M.Parameters() p.wing_r1() -- 2.44.1 From 32e5f543d905e09b4c85e34c8c6648d080bd6ec0 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 24 Jun 2024 16:13:15 -0700 Subject: [PATCH 011/187] feat: 2 segment wing root --- nhf/touhou/houjuu_nue/__init__.py | 75 ++++++++------ nhf/touhou/houjuu_nue/wing.py | 162 ++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 28 deletions(-) create mode 100644 nhf/touhou/houjuu_nue/wing.py diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index ede9d9f..8f2d8a3 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -1,15 +1,42 @@ +""" +This cosplay consists of 3 components: + +## Trident + +The trident is composed of individual segments, made of acrylic, and a 3D +printed head (convention rule prohibits metal) with a metallic paint. To ease +transportation, the trident handle has individual segments with threads and can +be assembled on site. + +## Snake + +A 3D printed snake with a soft material so it can wrap around and bend + +## Wings + +This is the crux of the cosplay and the most complex component. The wings mount +on a wearable harness. Each wing consists of 4 segments with 3 joints. Parts of +the wing which demands transluscency are created from 1/16" acrylic panels. +These panels serve double duty as the exoskeleton. + +The wings are labeled r1,r2,r3,l1,l2,l3. The segments of the wings are labeled +from root to tip s0 (root), s1, s2, s3. The joints are named (from root to tip) +shoulder, elbow, wrist in analogy with human anatomy. +""" from dataclasses import dataclass import unittest import cadquery as Cq import nhf.joints from nhf import Material +import nhf.touhou.houjuu_nue.wing as MW @dataclass(frozen=True) class Parameters: + """ + Defines dimensions for the Houjuu Nue cosplay + """ - """ - Thickness of the exoskeleton panel in millimetres - """ + # Thickness of the exoskeleton panel in millimetres panel_thickness: float = 25.4 / 16 # Harness @@ -28,12 +55,11 @@ class Parameters: ] # Holes drilled onto harness for attachment with HS joint - harness_to_wing_base_hole_diam = 6 + harness_to_root_conn_diam = 6 # Wing root properties - """ - The Houjuu-Scarlett joint mechanism at the base of the wing - """ + # + # The Houjuu-Scarlett joint mechanism at the base of the wing hs_joint_base_width = 85 hs_joint_base_thickness = 10 hs_joint_ring_thickness = 5 @@ -49,12 +75,8 @@ class Parameters: hs_joint_axis_cbore_diam = 20 hs_joint_axis_cbore_depth = 3 - - """ - Radius of the mounting mechanism of the wing root. This is constrained by - the size of the harness. - """ - root_radius: float = 60 + # Exterior radius of the wing root assembly + wing_root_radius = 40 """ Heights for various wing joints, where the numbers start from the first joint. @@ -64,6 +86,10 @@ class Parameters: wing_r2_height = 100 wing_r3_height = 100 + def __post_init__(self): + assert self.wing_root_radius > self.hs_joint_radius,\ + "Wing root must be large enough to accomodate joint" + def print_geometries(self): return [ @@ -95,7 +121,7 @@ class Parameters: sketch .push(conn) .tag(tag) - .circle(self.harness_to_wing_base_hole_diam / 2, mode='s') + .circle(self.harness_to_root_conn_diam / 2, mode='s') .reset() ) return sketch @@ -171,7 +197,7 @@ class Parameters: .workplane() .pushPoints(conn) .cboreHole( - diameter=self.harness_to_wing_base_hole_diam, + diameter=self.harness_to_root_conn_diam, cboreDiameter=self.hs_joint_corner_cbore_diam, cboreDepth=self.hs_joint_corner_cbore_depth) ) @@ -245,23 +271,16 @@ class Parameters: ) return result - def wing_root(self, - side_width=30, - side_height=100) -> Cq.Shape: + def wing_root(self) -> Cq.Shape: """ Generate the wing root which contains a Hirth joint at its base and a rectangular opening on its side, with the necessary interfaces. """ - result = ( - Cq.Workplane("XY") - .circle(self.root_radius) - .transformed(offset=Cq.Vector(80, 0, 80), - rotate=Cq.Vector(0, 45, 0)) - .rect(side_width, side_height) - .loft() - .val() - ) - return result + return MW.wing_base().val()#self.wing_root_radius) + + ###################### + # Assemblies # + ###################### def harness_assembly(self): harness = self.harness() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py new file mode 100644 index 0000000..a6fa011 --- /dev/null +++ b/nhf/touhou/houjuu_nue/wing.py @@ -0,0 +1,162 @@ +import math +import cadquery as Cq + +def wing_root_profiles( + base_sweep=150, + wall_thickness=8, + base_radius=60, + middle_offset=30, + conn_width=40, + conn_height=100) -> tuple[Cq.Wire, Cq.Wire]: + assert base_sweep < 180 + assert middle_offset > 0 + theta = math.pi * base_sweep / 180 + c, s = math.cos(theta), math.sin(theta) + c_1, s_1 = math.cos(theta * 0.75), math.sin(theta * 0.75) + c_2, s_2 = math.cos(theta / 2), math.sin(theta / 2) + r1 = base_radius + r2 = base_radius - wall_thickness + base = ( + Cq.Sketch() + .arc( + (c * r1, s * r1), + (c_1 * r1, s_1 * r1), + (c_2 * r1, s_2 * r1), + ) + .arc( + (c_2 * r1, s_2 * r1), + (r1, 0), + (c_2 * r1, -s_2 * r1), + ) + .arc( + (c_2 * r1, -s_2 * r1), + (c_1 * r1, -s_1 * r1), + (c * r1, -s * r1), + ) + .segment( + (c * r1, -s * r1), + (c * r2, -s * r2), + ) + .arc( + (c * r2, -s * r2), + (c_1 * r2, -s_1 * r2), + (c_2 * r2, -s_2 * r2), + ) + .arc( + (c_2 * r2, -s_2 * r2), + (r2, 0), + (c_2 * r2, s_2 * r2), + ) + .arc( + (c_2 * r2, s_2 * r2), + (c_1 * r2, s_1 * r2), + (c * r2, s * r2), + ) + .segment( + (c * r2, s * r2), + (c * r1, s * r1), + ) + .assemble(tag="wire") + .wires().val() + ) + assert isinstance(base, Cq.Wire) + + # The interior sweep is given by theta, but the exterior sweep exceeds the + # interior sweep so the wall does not become thinner towards the edges. + # If the exterior sweep is theta', it has to satisfy + # + # sin(theta) * r2 + wall_thickness = sin(theta') * r1 + x, y = conn_width / 2, conn_height / 2 + t = wall_thickness + dx = middle_offset + middle = ( + Cq.Sketch() + # Interior arc, top point + .arc( + (x - t, y - t), + (x - t + dx, 0), + (x - t, -y + t), + ) + .segment( + (x - t, -y + t), + (-x, -y+t) + ) + .segment((-x, -y)) + .segment((x, -y)) + # Outer arc, bottom point + .arc( + (x, -y), + (x + dx, 0), + (x, y), + ) + .segment( + (x, y), + (-x, y) + ) + .segment((-x, y-t)) + #.segment((x2, a)) + .close() + .assemble(tag="wire") + .wires().val() + ) + assert isinstance(middle, Cq.Wire) + + x, y = conn_width / 2, conn_height / 2 + t = wall_thickness + tip = ( + Cq.Sketch() + .segment((-x, y), (x, y)) + .segment((x, -y)) + .segment((-x, -y)) + .segment((-x, -y+t)) + .segment((x-t, -y+t)) + .segment((x-t, y-t)) + .segment((-x, y-t)) + .close() + .assemble(tag="wire") + .wires().val() + ) + return base, middle, tip +def wing_base(): + root_profile, middle_profile, tip_profile = wing_root_profiles() + + rotate_centre = Cq.Vector(-200, 0, -25) + rotate_axis = Cq.Vector(0, 1, 0) + terminal_offset = Cq.Vector(-80, 0, 80) + terminal_rotate = Cq.Vector(0, -45, 0) + + #middle_profile = middle_profile.moved(Cq.Location((0, 0, -100))) + #tip_profile = tip_profile.moved(Cq.Location((0, 0, -200))) + middle_profile = middle_profile.rotate( + startVector=rotate_centre, + endVector=rotate_centre + rotate_axis, + angleDegrees = 35, + ) + tip_profile = tip_profile.rotate( + startVector=rotate_centre, + endVector=rotate_centre + rotate_axis, + angleDegrees = 70, + ) + seg1 = ( + Cq.Workplane('XY') + .add(root_profile) + .toPending() + .transformed( + offset=terminal_offset, + rotate=terminal_rotate) + #.add(middle_profile.moved(Cq.Location((-15, 0, 15)))) + .add(middle_profile) + .toPending() + .loft() + ) + seg2 = ( + Cq.Workplane('XY') + .add(middle_profile) + .toPending() + .workplane() + .add(tip_profile) + .toPending() + .loft() + ) + seg1 = seg1.union(seg2) + return seg1 -- 2.44.1 From a41906d13011b2fcfbaa9fa0693cf884243d9cf3 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 25 Jun 2024 09:11:48 -0400 Subject: [PATCH 012/187] feat: Trident handle --- nhf/handle.py | 156 ++++++++++++++++++++++++++++++ nhf/materials.py | 1 + nhf/test.py | 9 ++ nhf/touhou/houjuu_nue/__init__.py | 10 +- nhf/touhou/houjuu_nue/test.py | 3 + nhf/touhou/houjuu_nue/trident.py | 30 ++++++ 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 nhf/handle.py create mode 100644 nhf/touhou/houjuu_nue/trident.py diff --git a/nhf/handle.py b/nhf/handle.py new file mode 100644 index 0000000..0509897 --- /dev/null +++ b/nhf/handle.py @@ -0,0 +1,156 @@ +""" +This schematics file contains all designs related to tool handles +""" +from dataclasses import dataclass +import cadquery as Cq + +@dataclass(frozen=True) +class Handle: + """ + Characteristic of a tool handle + """ + + # Outer radius for the handle + radius: float = 38 / 2 + + # Inner radius + radius_inner: float = 33 / 2 + + # Wall thickness for the connector + insertion_thickness: float = 4 + + # The connector goes in the insertion + connector_thickness: float = 4 + + # Length for the rim on the female connector + rim_length: float = 5 + + insertion_length: float = 60 + + connector_length: float = 60 + + def __post_init__(self): + assert self.radius > self.radius_inner + assert self.radius_inner > self.insertion_thickness + self.connector_thickness + assert self.insertion_length > self.rim_length + + @property + def _r1(self): + """ + Radius of inside of insertion + """ + return self.radius_inner - self.insertion_thickness + @property + def _r2(self): + """ + Radius of inside of connector + """ + return self._r1 - self.connector_thickness + + def segment(self, length: float): + result = ( + Cq.Workplane() + .cylinder(radius=self.radius, height=length) + ) + result.faces("Z").tag("mate2") + return result + + def insertion(self): + """ + This type of joint is used to connect two handlebar pieces. Each handlebar + piece is a tube which cannot be machined, so the joint connects to the + handle by glue. + + Tags: + * lip: Co-planar Mates to the rod + * mate: Mates to the connector + """ + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.radius_inner, + height=self.insertion_length - self.rim_length, + centered=[True, True, False]) + ) + result.faces(">Z").tag("lip") + result = ( + result.faces(">Z") + .workplane() + .circle(self.radius) + .extrude(self.rim_length) + .faces(">Z") + .hole(2 * self._r1) + ) + result.faces(">Z").tag("mate") + return result + + def connector(self, solid: bool = False): + """ + Tags: + * mate{1,2}: Mates to the connector + """ + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.radius, + height=self.connector_length, + ) + ) + for (tag, selector) in [("mate1", "Z")]: + result.faces(selector).tag(tag) + r1 = self.radius_inner + result = ( + result + .faces(selector) + .workplane() + .circle(self._r1) + .extrude(self.insertion_length) + ) + if not solid: + result = result.faces(">Z").hole(2 * self._r2) + return result + + def one_side_connector(self): + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.radius, + height=self.rim_length, + ) + ) + result.faces("Z").tag("base") + result = ( + result + .faces(" self.hs_joint_radius,\ "Wing root must be large enough to accomodate joint" @@ -301,3 +306,6 @@ class Parameters: result.constrain(f"harness?{name}_{i}", f"hs_{name}p?h{i}", "Point") result.solve() return result + + def trident_assembly(self): + return MT.trident_assembly(self.trident_handle) diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 91808a7..2eb8cd7 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -18,6 +18,9 @@ class Test(unittest.TestCase): def test_harness(self): p = M.Parameters() p.harness_assembly() + def test_trident(): + p = M.Parameters() + p.trident_assembly() if __name__ == '__main__': unittest.main() diff --git a/nhf/touhou/houjuu_nue/trident.py b/nhf/touhou/houjuu_nue/trident.py new file mode 100644 index 0000000..e270e74 --- /dev/null +++ b/nhf/touhou/houjuu_nue/trident.py @@ -0,0 +1,30 @@ +import math +import cadquery as Cq +from nhf import Material +from nhf.handle import Handle + +def trident_assembly( + handle: Handle, + handle_segment_length: float = 24*25.4): + def segment(): + return handle.segment(handle_segment_length) + mat_i = Material.PLASTIC_PLA + mat_s = Material.ACRYLIC_BLACK + assembly = ( + Cq.Assembly() + .add(handle.insertion(), name="i0", color=mat_i.color) + .constrain("i0", "Fixed") + .add(segment(), name="s1", color=mat_s.color) + .constrain("i0?lip", "s1?mate1", "Plane", param=0) + .add(handle.insertion(), name="i1", color=mat_i.color) + .add(handle.connector(), name="c1", color=mat_i.color) + .add(handle.insertion(), name="i2", color=mat_i.color) + .constrain("s1?mate2", "i1?lip", "Plane", param=0) + .constrain("i1?mate", "c1?mate1", "Plane") + .constrain("i2?mate", "c1?mate2", "Plane") + .add(segment(), name="s2", color=mat_s.color) + .constrain("i2?lip", "s2?mate1", "Plane", param=0) + .add(handle.insertion(), name="i3", color=mat_i.color) + .constrain("s2?mate2", "i3?lip", "Plane", param=0) + ) + return assembly.solve() -- 2.44.1 From cec2f4da5527a5e9ca955aac50db893794d533b6 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 26 Jun 2024 09:42:50 -0400 Subject: [PATCH 013/187] test: Trident length --- nhf/touhou/houjuu_nue/test.py | 12 ++++++++---- nhf/touhou/houjuu_nue/trident.py | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 2eb8cd7..1f6265a 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -7,20 +7,24 @@ class Test(unittest.TestCase): def test_hs_joint_parent(self): p = M.Parameters() obj = p.hs_joint_parent() - assert isinstance(obj.val().solids(), Cq.Solid), "H-S joint must be in one piece" + self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece") def test_wing_root(self): p = M.Parameters() obj = p.wing_root() - assert isinstance(obj.solids(), Cq.Solid), "Wing root must be in one piece" + self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece") def test_wings(self): p = M.Parameters() p.wing_r1() def test_harness(self): p = M.Parameters() p.harness_assembly() - def test_trident(): + def test_trident(self): p = M.Parameters() - p.trident_assembly() + assembly = p.trident_assembly() + bbox = assembly.toCompound().BoundingBox() + length = bbox.zlen + self.assertGreater(length, 1300) + self.assertLess(length, 1700) if __name__ == '__main__': unittest.main() diff --git a/nhf/touhou/houjuu_nue/trident.py b/nhf/touhou/houjuu_nue/trident.py index e270e74..75e4d73 100644 --- a/nhf/touhou/houjuu_nue/trident.py +++ b/nhf/touhou/houjuu_nue/trident.py @@ -26,5 +26,7 @@ def trident_assembly( .constrain("i2?lip", "s2?mate1", "Plane", param=0) .add(handle.insertion(), name="i3", color=mat_i.color) .constrain("s2?mate2", "i3?lip", "Plane", param=0) + .add(handle.one_side_connector(), name="head", color=mat_i.color) + .constrain("i3?mate", "head?mate", "Plane") ) return assembly.solve() -- 2.44.1 From 0c42f71c9fa3f05c91c3ede704bce7e9536a60d3 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 26 Jun 2024 09:44:02 -0400 Subject: [PATCH 014/187] fix: Don't user `assert` in unit tests --- nhf/test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nhf/test.py b/nhf/test.py index 37a750d..5695c41 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -7,8 +7,9 @@ class TestJoints(unittest.TestCase): def test_joint_hirth(self): j = nhf.joints.hirth_joint() - assert isinstance(j.val().solids(), Cq.Solid),\ - "Hirth joint must be in one piece" + self.assertIsInstance( + j.val().solids(), Cq.Solid, + msg="Hirth joint must be in one piece") def test_joints_hirth_assembly(self): nhf.joints.hirth_assembly() def test_joints_comma_assembly(self): -- 2.44.1 From d823a58d8865a5a0e6ee9a43ba39ef799785bfea Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 26 Jun 2024 11:28:25 -0400 Subject: [PATCH 015/187] feat: Metric threads on handle --- nhf/handle.py | 119 ++++++--- nhf/metric_threads.py | 422 ++++++++++++++++++++++++++++++ nhf/test.py | 22 +- nhf/touhou/houjuu_nue/__init__.py | 10 +- nhf/touhou/houjuu_nue/trident.py | 8 +- 5 files changed, 537 insertions(+), 44 deletions(-) create mode 100644 nhf/metric_threads.py diff --git a/nhf/handle.py b/nhf/handle.py index 0509897..fbd8c56 100644 --- a/nhf/handle.py +++ b/nhf/handle.py @@ -3,59 +3,89 @@ This schematics file contains all designs related to tool handles """ from dataclasses import dataclass import cadquery as Cq +import nhf.metric_threads as NMt @dataclass(frozen=True) class Handle: """ Characteristic of a tool handle + + This assumes the handle segment material does not have threads. Each segment + attaches to two insertions, which have threads on the inside. A connector + has threads on the outside and joints two insertions. + + Note that all the radial sizes are diameters (in mm). """ - # Outer radius for the handle - radius: float = 38 / 2 + # Outer and inner radius for the handle usually come in standard sizes + diam: float = 38 + diam_inner: float = 33 - # Inner radius - radius_inner: float = 33 / 2 + # Major diameter of the internal threads, following ISO metric screw thread + # standard. This determines the wall thickness of the insertion. + diam_threading: float = 27.0 - # Wall thickness for the connector - insertion_thickness: float = 4 + thread_pitch: float = 3.0 - # The connector goes in the insertion - connector_thickness: float = 4 + # Internal cavity diameter. This determines the wall thickness of the connector + diam_connector_internal: float = 18.0 + + # If set to true, do not generate threads + simplify_geometry: bool = True # Length for the rim on the female connector rim_length: float = 5 insertion_length: float = 60 + # Amount by which the connector goes into the segment connector_length: float = 60 def __post_init__(self): - assert self.radius > self.radius_inner - assert self.radius_inner > self.insertion_thickness + self.connector_thickness + assert self.diam > self.diam_inner, "Material thickness cannot be <= 0" + assert self.diam_inner > self.diam_insertion_internal, "Threading radius is too big" + assert self.diam_insertion_internal > self.diam_connector_external + assert self.diam_connector_external > self.diam_connector_internal, "Internal diameter is too large" assert self.insertion_length > self.rim_length @property - def _r1(self): - """ - Radius of inside of insertion - """ - return self.radius_inner - self.insertion_thickness + def diam_insertion_internal(self): + r = NMt.metric_thread_major_radius( + self.diam_threading, + self.thread_pitch, + internal=True) + return r * 2 + @property - def _r2(self): - """ - Radius of inside of connector - """ - return self._r1 - self.connector_thickness + def diam_connector_external(self): + r = NMt.metric_thread_minor_radius( + self.diam_threading, + self.thread_pitch) + return r * 2 def segment(self, length: float): result = ( Cq.Workplane() - .cylinder(radius=self.radius, height=length) + .cylinder( + radius=self.diam / 2, + height=length) ) result.faces("Z").tag("mate2") return result + def _external_thread(self): + return NMt.external_metric_thread( + self.diam_threading, + self.thread_pitch, + self.insertion_length, + top_lead_in=True) + def _internal_thread(self): + return NMt.internal_metric_thread( + self.diam_threading, + self.thread_pitch, + self.insertion_length) + def insertion(self): """ This type of joint is used to connect two handlebar pieces. Each handlebar @@ -69,20 +99,24 @@ class Handle: result = ( Cq.Workplane('XY') .cylinder( - radius=self.radius_inner, + radius=self.diam_inner / 2, height=self.insertion_length - self.rim_length, centered=[True, True, False]) ) - result.faces(">Z").tag("lip") - result = ( - result.faces(">Z") - .workplane() - .circle(self.radius) - .extrude(self.rim_length) - .faces(">Z") - .hole(2 * self._r1) - ) + result.faces(">Z").tag("rim") + if self.rim_length > 0: + result = ( + result.faces(">Z") + .workplane() + .circle(self.diam / 2) + .extrude(self.rim_length) + .faces(">Z") + .hole(self.diam_insertion_internal) + ) result.faces(">Z").tag("mate") + if not self.simplify_geometry: + thread = self._internal_thread().val() + result = result.union(thread) return result def connector(self, solid: bool = False): @@ -93,29 +127,40 @@ class Handle: result = ( Cq.Workplane('XY') .cylinder( - radius=self.radius, + radius=self.diam / 2, height=self.connector_length, ) ) for (tag, selector) in [("mate1", "Z")]: result.faces(selector).tag(tag) - r1 = self.radius_inner result = ( result .faces(selector) .workplane() - .circle(self._r1) + .circle(self.diam_connector_external / 2) .extrude(self.insertion_length) ) if not solid: - result = result.faces(">Z").hole(2 * self._r2) + result = result.faces(">Z").hole(self.diam_connector_internal) + if not self.simplify_geometry: + thread = self._external_thread().val() + result = ( + result + .union( + thread + .moved(Cq.Vector(0, 0, self.connector_length / 2))) + .union( + thread + .rotate((0,0,0), (1,0,0), angleDegrees=90) + .moved(Cq.Vector(0, 0, -self.connector_length / 2))) + ) return result def one_side_connector(self): result = ( Cq.Workplane('XY') .cylinder( - radius=self.radius, + radius=self.diam / 2, height=self.rim_length, ) ) @@ -125,7 +170,7 @@ class Handle: result .faces(" radians. +def __deg2rad(degrees): + return degrees * math.pi / 180 + +# In the absence of flat thread valley and flattened thread tip, returns the +# amount by which the thread "triangle" protrudes outwards (radially) from base +# cylinder in the case of external thread, or the amount by which the thread +# "triangle" protrudes inwards from base tube in the case of internal thread. +def metric_thread_perfect_height(pitch): + return pitch / (2 * math.tan(__deg2rad(metric_thread_angle()))) + +# Up the radii of internal (female) thread in order to provide a little bit of +# wiggle room around male thread. Right now input parameter 'diameter' is +# ignored. This function is only used for internal/female threads. Currently +# there is no practical way to adjust the male/female thread clearance besides +# to manually edit this function. This design route was chosen for the sake of +# code simplicity. +def __metric_thread_internal_radius_increase(diameter, pitch): + return 0.1 * metric_thread_perfect_height(pitch) + +# Returns the major radius of thread, which is always the greater of the two. +def metric_thread_major_radius(diameter, pitch, internal=False): + return (__metric_thread_internal_radius_increase(diameter, pitch) if + internal else 0.0) + (diameter / 2) + +# What portion of the total pitch is taken up by the angled thread section (and +# not the squared off valley and tip). The remaining portion (1 minus ratio) +# will be divided equally between the flattened valley and flattened tip. +def __metric_thread_effective_ratio(): + return 0.7 + +# Returns the minor radius of thread, which is always the lesser of the two. +def metric_thread_minor_radius(diameter, pitch, internal=False): + return (metric_thread_major_radius(diameter, pitch, internal) + - (__metric_thread_effective_ratio() * + metric_thread_perfect_height(pitch))) + +# What the major radius would be if the cuts were perfectly triangular, without +# flat spots in the valleys and without flattened tips. +def metric_thread_perfect_major_radius(diameter, pitch, internal=False): + return (metric_thread_major_radius(diameter, pitch, internal) + + ((1.0 - __metric_thread_effective_ratio()) * + metric_thread_perfect_height(pitch) / 2)) + +# What the minor radius would be if the cuts were perfectly triangular, without +# flat spots in the valleys and without flattened tips. +def metric_thread_perfect_minor_radius(diameter, pitch, internal=False): + return (metric_thread_perfect_major_radius(diameter, pitch, internal) + - metric_thread_perfect_height(pitch)) + +# Returns the lead-in and/or chamfer distance along the z axis of rotation. +# The lead-in/chamfer only depends on the pitch and is made with the same angle +# as the thread, that being 30 degrees offset from radial. +def metric_thread_lead_in(pitch, internal=False): + return (math.tan(__deg2rad(metric_thread_angle())) + * (metric_thread_major_radius(256.0, pitch, internal) + - metric_thread_minor_radius(256.0, pitch, internal))) + +# Returns the width of the flat spot in thread valley of a standard thread. +# This is also equal to the width of the flat spot on thread tip, on a standard +# thread. +def metric_thread_relief(pitch): + return (1.0 - __metric_thread_effective_ratio()) * pitch / 2 + + +############################################################################### +# A few words on modules external_metric_thread() and internal_metric_thread(). +# The parameter 'z_start' is added as a convenience in order to make the male +# and female threads align perfectly. When male and female threads are created +# having the same diameter, pitch, and n_starts (usually 1), then so long as +# they are not translated or rotated (or so long as they are subjected to the +# same exact translation and rotation), they will intermesh perfectly, +# regardless of the value of 'z_start' used on each. This is in order that +# assemblies be able to depict perfectly aligning threads. + +# Generates threads with base cylinder unless 'base_cylinder' is overridden. +# Please note that 'use_epsilon' is activated by default, which causes a slight +# budge in the minor radius, inwards, so that overlaps would be created with +# inner cylinders. (Does not affect thread profile outside of cylinder.) +############################################################################### +def external_metric_thread(diameter, # Required parameter, e.g. 3.0 for M3x0.5 + pitch, # Required parameter, e.g. 0.5 for M3x0.5 + length, # Required parameter, e.g. 2.0 + z_start=0.0, + n_starts=1, + bottom_lead_in=False, # Lead-in is at same angle as + top_lead_in =False, # thread, namely 30 degrees. + bottom_relief=False, # Add relief groove to start or + top_relief =False, # end of threads (shorten). + force_outer_radius=-1.0, # Set close to diameter/2. + use_epsilon=True, # For inner cylinder overlap. + base_cylinder=True, # Whether to include base cyl. + cyl_extend_bottom=-1.0, + cyl_extend_top=-1.0, + envelope=False): # Draw only envelope, don't cut. + + cyl_extend_bottom = max(0.0, cyl_extend_bottom) + cyl_extend_top = max(0.0, cyl_extend_top) + + z_off = (1.0 - __metric_thread_effective_ratio()) * pitch / 4 + t_start = z_start + t_length = length + if bottom_relief: + t_start = t_start + (2 * z_off) + t_length = t_length - (2 * z_off) + if top_relief: + t_length = t_length - (2 * z_off) + outer_r = (force_outer_radius if (force_outer_radius > 0.0) else + metric_thread_major_radius(diameter,pitch)) + inner_r = metric_thread_minor_radius(diameter,pitch) + epsilon = 0 + inner_r_adj = inner_r + inner_z_budge = 0 + if use_epsilon: + epsilon = (z_off/3) / math.tan(__deg2rad(metric_thread_angle())) + inner_r_adj = inner_r - epsilon + inner_z_budge = math.tan(__deg2rad(metric_thread_angle())) * epsilon + + if envelope: + threads = cq.Workplane("XZ") + threads = threads.moveTo(inner_r_adj, -pitch) + threads = threads.lineTo(outer_r, -pitch) + threads = threads.lineTo(outer_r, t_length + pitch) + threads = threads.lineTo(inner_r_adj, t_length + pitch) + threads = threads.close() + threads = threads.revolve() + + else: # Not envelope, cut the threads. + wire = cq.Wire.makeHelix(pitch=pitch*n_starts, + height=t_length+pitch, + radius=inner_r) + wire = wire.translate((0,0,-pitch/2)) + wire = wire.rotate(startVector=(0,0,0), endVector=(0,0,1), + angleDegrees=360*(-pitch/2)/(pitch*n_starts)) + d_mid = ((metric_thread_major_radius(diameter,pitch) - outer_r) + * math.tan(__deg2rad(metric_thread_angle()))) + thread = cq.Workplane("XZ") + thread = thread.moveTo(inner_r_adj, -pitch/2 + z_off - inner_z_budge) + thread = thread.lineTo(outer_r, -(z_off + d_mid)) + thread = thread.lineTo(outer_r, z_off + d_mid) + thread = thread.lineTo(inner_r_adj, pitch/2 - z_off + inner_z_budge) + thread = thread.close() + thread = thread.sweep(wire, isFrenet=True) + threads = thread + for addl_start in range(1, n_starts): + # TODO: Incremental/cumulative rotation may not be as accurate as + # keeping 'thread' intact and rotating it by correct amount + # on each iteration. However, changing the code in that + # regard may disrupt the delicate nature of workarounds + # with repsect to quirks in the underlying B-rep library. + thread = thread.rotate(axisStartPoint=(0,0,0), + axisEndPoint=(0,0,1), + angleDegrees=360/n_starts) + threads = threads.union(thread) + + square_shave = cq.Workplane("XY") + square_shave = square_shave.box(length=outer_r*3, width=outer_r*3, + height=pitch*2, centered=True) + square_shave = square_shave.translate((0,0,-pitch)) # Because centered. + # Always cut the top and bottom square. Otherwise things don't play nice. + threads = threads.cut(square_shave) + + if bottom_lead_in: + delta_r = outer_r - inner_r + rise = math.tan(__deg2rad(metric_thread_angle())) * delta_r + lead_in = cq.Workplane("XZ") + lead_in = lead_in.moveTo(inner_r - delta_r, -rise) + lead_in = lead_in.lineTo(outer_r + delta_r, 2 * rise) + lead_in = lead_in.lineTo(outer_r + delta_r, -pitch - rise) + lead_in = lead_in.lineTo(inner_r - delta_r, -pitch - rise) + lead_in = lead_in.close() + lead_in = lead_in.revolve() + threads = threads.cut(lead_in) + + # This was originally a workaround to the anomalous B-rep computation where + # the top of base cylinder is flush with top of threads, without the use of + # lead-in. It turns out that preferring the use of the 'render_cyl_early' + # strategy alleviates other problems as well. + render_cyl_early = (base_cylinder and ((not top_relief) and + (not (cyl_extend_top > 0.0)) and + (not envelope))) + render_cyl_late = (base_cylinder and (not render_cyl_early)) + if render_cyl_early: + cyl = cq.Workplane("XY") + cyl = cyl.circle(radius=inner_r) + cyl = cyl.extrude(until=length+pitch+cyl_extend_bottom) + # Make rotation of cylinder consistent with non-workaround case. + cyl = cyl.rotate(axisStartPoint=(0,0,0), axisEndPoint=(0,0,1), + angleDegrees=-(360*t_start/(pitch*n_starts))) + cyl = cyl.translate((0,0,-t_start+(z_start-cyl_extend_bottom))) + threads = threads.union(cyl) + + # Next, make cuts at the top. + square_shave = square_shave.translate((0,0,pitch*2+t_length)) + threads = threads.cut(square_shave) + + if top_lead_in: + delta_r = outer_r - inner_r + rise = math.tan(__deg2rad(metric_thread_angle())) * delta_r + lead_in = cq.Workplane("XZ") + lead_in = lead_in.moveTo(inner_r - delta_r, t_length + rise) + lead_in = lead_in.lineTo(outer_r + delta_r, t_length - (2 * rise)) + lead_in = lead_in.lineTo(outer_r + delta_r, t_length + pitch + rise) + lead_in = lead_in.lineTo(inner_r - delta_r, t_length + pitch + rise) + lead_in = lead_in.close() + lead_in = lead_in.revolve() + threads = threads.cut(lead_in) + + # Place the threads into position. + threads = threads.translate((0,0,t_start)) + if (not envelope): + threads = threads.rotate(axisStartPoint=(0,0,0), axisEndPoint=(0,0,1), + angleDegrees=360*t_start/(pitch*n_starts)) + + if render_cyl_late: + cyl = cq.Workplane("XY") + cyl = cyl.circle(radius=inner_r) + cyl = cyl.extrude(until=length+cyl_extend_bottom+cyl_extend_top) + cyl = cyl.translate((0,0,z_start-cyl_extend_bottom)) + threads = threads.union(cyl) + + return threads + + +############################################################################### +# Generates female threads without a base tube, unless 'base_tube_od' is set to +# something which is sufficiently greater than 'diameter' parameter. Please +# note that 'use_epsilon' is activated by default, which causes a slight budge +# in the major radius, outwards, so that overlaps would be created with outer +# tubes. (Does not affect thread profile inside of tube or beyond extents.) +############################################################################### +def internal_metric_thread(diameter, # Required parameter, e.g. 3.0 for M3x0.5 + pitch, # Required parameter, e.g. 0.5 for M3x0.5 + length, # Required parameter, e.g. 2.0. + z_start=0.0, + n_starts=1, + bottom_chamfer=False, # Chamfer is at same angle as + top_chamfer =False, # thread, namely 30 degrees. + bottom_relief=False, # Add relief groove to start or + top_relief =False, # end of threads (shorten). + use_epsilon=True, # For outer cylinder overlap. + # The base tube outer diameter must be sufficiently + # large for tube to be rendered. Otherwise ignored. + base_tube_od=-1.0, + tube_extend_bottom=-1.0, + tube_extend_top=-1.0, + envelope=False): # Draw only envelope, don't cut. + + tube_extend_bottom = max(0.0, tube_extend_bottom) + tube_extend_top = max(0.0, tube_extend_top) + + z_off = (1.0 - __metric_thread_effective_ratio()) * pitch / 4 + t_start = z_start + t_length = length + if bottom_relief: + t_start = t_start + (2 * z_off) + t_length = t_length - (2 * z_off) + if top_relief: + t_length = t_length - (2 * z_off) + outer_r = metric_thread_major_radius(diameter,pitch, + internal=True) + inner_r = metric_thread_minor_radius(diameter,pitch, + internal=True) + epsilon = 0 + outer_r_adj = outer_r + outer_z_budge = 0 + if use_epsilon: + # High values of 'epsilon' sometimes cause entire starts to disappear. + epsilon = (z_off/5) / math.tan(__deg2rad(metric_thread_angle())) + outer_r_adj = outer_r + epsilon + outer_z_budge = math.tan(__deg2rad(metric_thread_angle())) * epsilon + + if envelope: + threads = cq.Workplane("XZ") + threads = threads.moveTo(outer_r_adj, -pitch) + threads = threads.lineTo(inner_r, -pitch) + threads = threads.lineTo(inner_r, t_length + pitch) + threads = threads.lineTo(outer_r_adj, t_length + pitch) + threads = threads.close() + threads = threads.revolve() + + else: # Not envelope, cut the threads. + wire = cq.Wire.makeHelix(pitch=pitch*n_starts, + height=t_length+pitch, + radius=inner_r) + wire = wire.translate((0,0,-pitch/2)) + wire = wire.rotate(startVector=(0,0,0), endVector=(0,0,1), + angleDegrees=360*(-pitch/2)/(pitch*n_starts)) + thread = cq.Workplane("XZ") + thread = thread.moveTo(outer_r_adj, -pitch/2 + z_off - outer_z_budge) + thread = thread.lineTo(inner_r, -z_off) + thread = thread.lineTo(inner_r, z_off) + thread = thread.lineTo(outer_r_adj, pitch/2 - z_off + outer_z_budge) + thread = thread.close() + thread = thread.sweep(wire, isFrenet=True) + threads = thread + for addl_start in range(1, n_starts): + # TODO: Incremental/cumulative rotation may not be as accurate as + # keeping 'thread' intact and rotating it by correct amount + # on each iteration. However, changing the code in that + # regard may disrupt the delicate nature of workarounds + # with repsect to quirks in the underlying B-rep library. + thread = thread.rotate(axisStartPoint=(0,0,0), + axisEndPoint=(0,0,1), + angleDegrees=360/n_starts) + threads = threads.union(thread) + # Rotate so that the external threads would align. + threads = threads.rotate(axisStartPoint=(0,0,0), axisEndPoint=(0,0,1), + angleDegrees=180/n_starts) + + square_len = max(outer_r*3, base_tube_od*1.125) + square_shave = cq.Workplane("XY") + square_shave = square_shave.box(length=square_len, width=square_len, + height=pitch*2, centered=True) + square_shave = square_shave.translate((0,0,-pitch)) # Because centered. + # Always cut the top and bottom square. Otherwise things don't play nice. + threads = threads.cut(square_shave) + + if bottom_chamfer: + delta_r = outer_r - inner_r + rise = math.tan(__deg2rad(metric_thread_angle())) * delta_r + chamfer = cq.Workplane("XZ") + chamfer = chamfer.moveTo(inner_r - delta_r, 2 * rise) + chamfer = chamfer.lineTo(outer_r + delta_r, -rise) + chamfer = chamfer.lineTo(outer_r + delta_r, -pitch - rise) + chamfer = chamfer.lineTo(inner_r - delta_r, -pitch - rise) + chamfer = chamfer.close() + chamfer = chamfer.revolve() + threads = threads.cut(chamfer) + + # This was originally a workaround to the anomalous B-rep computation where + # the top of base tube is flush with top of threads w/o the use of chamfer. + # This is now being made consistent with the 'render_cyl_early' strategy in + # external_metric_thread() whereby we prefer the "render early" plan of + # action even in cases where a top chamfer or lead-in is used. + render_tube_early = ((base_tube_od > (outer_r * 2)) and + (not top_relief) and + (not (tube_extend_top > 0.0)) and + (not envelope)) + render_tube_late = ((base_tube_od > (outer_r * 2)) and + (not render_tube_early)) + if render_tube_early: + tube = cq.Workplane("XY") + tube = tube.circle(radius=base_tube_od/2) + tube = tube.circle(radius=outer_r) + tube = tube.extrude(until=length+pitch+tube_extend_bottom) + # Make rotation of cylinder consistent with non-workaround case. + tube = tube.rotate(axisStartPoint=(0,0,0), axisEndPoint=(0,0,1), + angleDegrees=-(360*t_start/(pitch*n_starts))) + tube = tube.translate((0,0,-t_start+(z_start-tube_extend_bottom))) + threads = threads.union(tube) + + # Next, make cuts at the top. + square_shave = square_shave.translate((0,0,pitch*2+t_length)) + threads = threads.cut(square_shave) + + if top_chamfer: + delta_r = outer_r - inner_r + rise = math.tan(__deg2rad(metric_thread_angle())) * delta_r + chamfer = cq.Workplane("XZ") + chamfer = chamfer.moveTo(inner_r - delta_r, t_length - (2 * rise)) + chamfer = chamfer.lineTo(outer_r + delta_r, t_length + rise) + chamfer = chamfer.lineTo(outer_r + delta_r, t_length + pitch + rise) + chamfer = chamfer.lineTo(inner_r - delta_r, t_length + pitch + rise) + chamfer = chamfer.close() + chamfer = chamfer.revolve() + threads = threads.cut(chamfer) + + # Place the threads into position. + threads = threads.translate((0,0,t_start)) + if (not envelope): + threads = threads.rotate(axisStartPoint=(0,0,0), axisEndPoint=(0,0,1), + angleDegrees=360*t_start/(pitch*n_starts)) + + if render_tube_late: + tube = cq.Workplane("XY") + tube = tube.circle(radius=base_tube_od/2) + tube = tube.circle(radius=outer_r) + tube = tube.extrude(until=length+tube_extend_bottom+tube_extend_top) + tube = tube.translate((0,0,z_start-tube_extend_bottom)) + threads = threads.union(tube) + + return threads diff --git a/nhf/test.py b/nhf/test.py index 5695c41..ce795bd 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -2,6 +2,7 @@ import unittest import cadquery as Cq import nhf.joints import nhf.handle +import nhf.metric_threads as NMt class TestJoints(unittest.TestCase): @@ -20,8 +21,25 @@ class TestHandle(unittest.TestCase): def test_handle_assembly(self): h = nhf.handle.Handle() - h.connector_insertion_assembly() - h.connector_one_side_insertion_assembly() + assembly = h.connector_insertion_assembly() + bbox = assembly.toCompound().BoundingBox() + self.assertAlmostEqual(bbox.xlen, h.diam) + self.assertAlmostEqual(bbox.ylen, h.diam) + assembly = h.connector_one_side_insertion_assembly() + bbox = assembly.toCompound().BoundingBox() + self.assertAlmostEqual(bbox.xlen, h.diam) + self.assertAlmostEqual(bbox.ylen, h.diam) + + +class TestMetricThreads(unittest.TestCase): + + def test_major_radius(self): + major = 3.0 + t = NMt.external_metric_thread(major, 0.5, 4.0, z_start= -0.85, top_lead_in=True) + bbox = t.val().BoundingBox() + self.assertAlmostEqual(bbox.xlen, major, places=3) + self.assertAlmostEqual(bbox.ylen, major, places=3) + if __name__ == '__main__': unittest.main() diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 6aad524..fbdc328 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -89,7 +89,15 @@ class Parameters: wing_r2_height = 100 wing_r3_height = 100 - trident_handle: nhf.handle.Handle = nhf.handle.Handle() + trident_handle: nhf.handle.Handle = nhf.handle.Handle( + diam=38, + diam_inner=33, + # M27-3 + diam_threading=27, + thread_pitch=3, + diam_connector_internal=18, + simplify_geometry=False, + ) def __post_init__(self): assert self.wing_root_radius > self.hs_joint_radius,\ diff --git a/nhf/touhou/houjuu_nue/trident.py b/nhf/touhou/houjuu_nue/trident.py index 75e4d73..769213c 100644 --- a/nhf/touhou/houjuu_nue/trident.py +++ b/nhf/touhou/houjuu_nue/trident.py @@ -15,17 +15,17 @@ def trident_assembly( .add(handle.insertion(), name="i0", color=mat_i.color) .constrain("i0", "Fixed") .add(segment(), name="s1", color=mat_s.color) - .constrain("i0?lip", "s1?mate1", "Plane", param=0) + .constrain("i0?rim", "s1?mate1", "Plane", param=0) .add(handle.insertion(), name="i1", color=mat_i.color) .add(handle.connector(), name="c1", color=mat_i.color) .add(handle.insertion(), name="i2", color=mat_i.color) - .constrain("s1?mate2", "i1?lip", "Plane", param=0) + .constrain("s1?mate2", "i1?rim", "Plane", param=0) .constrain("i1?mate", "c1?mate1", "Plane") .constrain("i2?mate", "c1?mate2", "Plane") .add(segment(), name="s2", color=mat_s.color) - .constrain("i2?lip", "s2?mate1", "Plane", param=0) + .constrain("i2?rim", "s2?mate1", "Plane", param=0) .add(handle.insertion(), name="i3", color=mat_i.color) - .constrain("s2?mate2", "i3?lip", "Plane", param=0) + .constrain("s2?mate2", "i3?rim", "Plane", param=0) .add(handle.one_side_connector(), name="head", color=mat_i.color) .constrain("i3?mate", "head?mate", "Plane") ) -- 2.44.1 From 9fda02ed9dc5e7b3b3941a4e782452411cf78623 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 26 Jun 2024 12:01:01 -0400 Subject: [PATCH 016/187] feat: Thread on handle terminal piece --- nhf/handle.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nhf/handle.py b/nhf/handle.py index fbd8c56..3aef3bb 100644 --- a/nhf/handle.py +++ b/nhf/handle.py @@ -164,15 +164,23 @@ class Handle: height=self.rim_length, ) ) - result.faces("Z").tag("base") + result.faces(">Z").tag("mate") + result.faces("Z") .workplane() .circle(self.diam_connector_external / 2) .extrude(self.insertion_length) ) + if not self.simplify_geometry: + thread = self._external_thread().val() + result = ( + result + .union( + thread + .moved(Cq.Vector(0, 0, self.connector_length / 2))) + ) return result def connector_insertion_assembly(self): -- 2.44.1 From 53c204eb208a835ba2d3d3a72e370897d33cda7e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 26 Jun 2024 15:57:22 -0400 Subject: [PATCH 017/187] feat: Torsion joint --- nhf/joints.py | 229 +++++++++++++++++++++++++++++++++++++++++-------- nhf/springs.py | 42 +++++++++ nhf/test.py | 5 ++ 3 files changed, 238 insertions(+), 38 deletions(-) create mode 100644 nhf/springs.py diff --git a/nhf/joints.py b/nhf/joints.py index 554d7b6..e7df7e9 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -1,5 +1,7 @@ -import cadquery as Cq +from dataclasses import dataclass import math +import cadquery as Cq +import nhf.springs as NS def hirth_joint(radius=60, radius_inner=40, @@ -174,54 +176,205 @@ def comma_joint(radius=30, result.faces('>X').tag("tail_end") return result -def torsion_spring(radius=12, - height=20, - thickness=2, - omega=90, - tail_length=25): - """ - Produces a torsion spring with abridged geometry since sweep is very slow in - cq-editor. - """ - base = ( - Cq.Workplane('XY') - .cylinder(height=height, radius=radius, - centered=(True, True, False)) - ) - base.faces(">Z").tag("mate_top") - base.faces(" self.spring_hole_depth + assert self.radius > self.groove_radius_outer + assert self.groove_radius_outer > self.groove_radius_inner + assert self.groove_radius_inner > self.radius_spring + assert self.spring_height > self.groove_depth, "Groove is too deep" + + @property + def total_height(self): + return 2 * self.disk_height + self.spring_height + + @property + def _radius_spring_internal(self): + return self.radius_spring - self.spring_thickness + + def _slot_polygon(self, flip: bool=False): + r1 = self.radius_spring - self.spring_thickness + r2 = self.radius_spring + flip = flip != self.right_handed + if flip: + r1 = -r1 + r2 = -r2 + return [ + (0, r2), + (self.spring_tail_length, r2), + (self.spring_tail_length, r1), + (0, r1), + ] + def _directrix(self, height, theta=0): + c, s = math.cos(theta), math.sin(theta) + r2 = self.radius_spring + l = self.spring_tail_length + if self.right_handed: + r2 = -r2 + # This is (0, r2) and (l, r2) transformed by rotation matrix + # [[c, s], [-s, c]] + return [ + (s * r2, -s * l + c * r2, height), + (c * l + s * r2, -s * l + c * r2, height), + ] + + + def spring(self): + return NS.torsion_spring( + radius=self.radius_spring, + height=self.spring_height, + thickness=self.spring_thickness, + tail_length=self.spring_tail_length, + ) + + def track(self): + groove_profile = ( + Cq.Sketch() + .circle(self.radius) + .circle(self.groove_radius_outer, mode='s') + .circle(self.groove_radius_inner, mode='a') + .circle(self.radius_spring, mode='s') + ) + spring_hole_profile = ( + Cq.Sketch() + .circle(self.radius) + .polygon(self._slot_polygon(flip=False), mode='s') + .circle(self.radius_spring, mode='s') + ) + result = ( + Cq.Workplane('XY') + .cylinder(radius=self.radius, height=self.disk_height) + .faces('>Z') + .tag("spring") + .placeSketch(spring_hole_profile) + .extrude(self.spring_thickness) + # If the spring hole profile is not simply connected, this workplane + # will have to be created from the `spring-mate` face. + .faces('>Z') + .placeSketch(groove_profile) + .extrude(self.groove_depth) + .faces('>Z') + .hole(self.radius_axle) + ) + # Insert directrix` + result.polyline(self._directrix(self.disk_height), + forConstruction=True).tag("directrix") + return result + + def rider(self): + def slot(loc): + wire = Cq.Wire.makePolygon(self._slot_polygon(flip=False)) + face = Cq.Face.makeFromWires(wire) + return face.located(loc) + wall_profile = ( + Cq.Sketch() + .circle(self.radius, mode='a') + .circle(self.radius_spring, mode='s') + .parray( + r=0, + a1=0, + da=360, + n=self.n_slots) + .each(slot, mode='s') + #.circle(self._radius_wall, mode='a') + ) + contact_profile = ( + Cq.Sketch() + .circle(self.groove_radius_outer, mode='a') + .circle(self.groove_radius_inner, mode='s') + #.circle(self._radius_wall, mode='a') + .parray( + r=0, + a1=0, + da=360, + n=self.n_slots) + .each(slot, mode='s') + ) + middle_height = self.spring_height - self.groove_depth - self.rider_gap + result = ( + Cq.Workplane('XY') + .cylinder(radius=self.radius, height=self.disk_height) + .faces('>Z') + .tag("spring") + .placeSketch(wall_profile) + .extrude(middle_height) + # The top face might not be in one piece. + #.faces('>Z') + .workplane(offset=middle_height) + .placeSketch(contact_profile) + .extrude(self.groove_depth + self.rider_gap) + .faces(tag="spring") + .circle(self._radius_spring_internal) + .extrude(self.spring_height) + .faces('>Z') + .hole(self.radius_axle) + ) + for i in range(self.n_slots): + theta = 2 * math.pi * i / self.n_slots + result.polyline(self._directrix(self.disk_height, theta), + forConstruction=True).tag(f"directrix{i}") + return result + + def rider_track_assembly(self): + rider = self.rider() + track = self.track() + spring = self.spring() + result = ( + Cq.Assembly() + .add(spring, name="spring", color=Cq.Color(0.5,0.5,0.5,1)) + .add(track, name="track", color=Cq.Color(0.5,0.5,0.8,0.3)) + .constrain("track?spring", "spring?top", "Plane") + .add(rider, name="rider", color=Cq.Color(0.8,0.8,0.5,0.3)) + .constrain("rider?spring", "spring?bot", "Plane") + .constrain("track?directrix", "spring?directrix_bot", "Axis") + .constrain("rider?directrix0", "spring?directrix_top", "Axis") + .solve() + ) + return result diff --git a/nhf/springs.py b/nhf/springs.py new file mode 100644 index 0000000..06e14b5 --- /dev/null +++ b/nhf/springs.py @@ -0,0 +1,42 @@ +import math +import cadquery as Cq + +def torsion_spring(radius=12, + height=20, + thickness=2, + omega=90, + tail_length=25): + """ + Produces a torsion spring with abridged geometry since sweep is very slow in + cq-editor. + """ + base = ( + Cq.Workplane('XY') + .cylinder(height=height, radius=radius, + centered=(True, True, False)) + ) + base.faces(">Z").tag("top") + base.faces(" Date: Wed, 26 Jun 2024 19:27:36 -0400 Subject: [PATCH 018/187] fix: use `.located` to move threads --- nhf/handle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nhf/handle.py b/nhf/handle.py index 3aef3bb..0424569 100644 --- a/nhf/handle.py +++ b/nhf/handle.py @@ -148,11 +148,11 @@ class Handle: result .union( thread - .moved(Cq.Vector(0, 0, self.connector_length / 2))) + .located(Cq.Location((0, 0, self.connector_length / 2)))) .union( thread .rotate((0,0,0), (1,0,0), angleDegrees=90) - .moved(Cq.Vector(0, 0, -self.connector_length / 2))) + .located(Cq.Location((0, 0, -self.connector_length / 2)))) ) return result @@ -179,7 +179,7 @@ class Handle: result .union( thread - .moved(Cq.Vector(0, 0, self.connector_length / 2))) + .located(Cq.Location((0, 0, self.connector_length / 2)))) ) return result -- 2.44.1 From 4dd43f7151d497270b22352d9312eeb9ce981d37 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 27 Jun 2024 23:22:54 -0400 Subject: [PATCH 019/187] refactor: Separate H-S joint component --- nhf/touhou/houjuu_nue/__init__.py | 16 ++++++++++------ nhf/touhou/houjuu_nue/test.py | 6 ++++++ nhf/touhou/houjuu_nue/wing.py | 26 +++++++++++++++++++++----- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index fbdc328..38e4f81 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -174,16 +174,20 @@ class Parameters: (-dx, dx), ] + def hs_joint_component(self): + hirth = nhf.joints.hirth_joint( + radius=self.hs_joint_radius, + radius_inner=self.hs_joint_radius_inner, + tooth_height=self.hs_joint_tooth_height, + base_height=self.hs_joint_ring_thickness) + return hirth + def hs_joint_parent(self): """ Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth coupling, a cylindrical base, and a mounting base. """ - hirth = nhf.joints.hirth_joint( - radius=self.hs_joint_radius, - radius_inner=self.hs_joint_radius_inner, - tooth_height=self.hs_joint_tooth_height, - base_height=self.hs_joint_ring_thickness).val() + hirth = self.hs_joint_component().val() #hirth = ( # hirth # .faces("X").tag("conn") + return result -- 2.44.1 From 914bc23582f6c23344e3cc8a6513a89e95b283ff Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 28 Jun 2024 17:21:30 -0400 Subject: [PATCH 020/187] feat: Add directrix tag to hirth joint --- nhf/__init__.py | 2 +- nhf/joints.py | 72 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/nhf/__init__.py b/nhf/__init__.py index 2175f24..b582e6a 100644 --- a/nhf/__init__.py +++ b/nhf/__init__.py @@ -1 +1 @@ -from nhf.materials import Material +from nhf.materials import Material, Role diff --git a/nhf/joints.py b/nhf/joints.py index e7df7e9..7c227b4 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -2,6 +2,13 @@ from dataclasses import dataclass import math import cadquery as Cq import nhf.springs as NS +from nhf import Role + +def hirth_tooth_angle(n_tooth): + """ + Angle of one whole tooth + """ + return 360 / n_tooth def hirth_joint(radius=60, radius_inner=40, @@ -9,9 +16,13 @@ def hirth_joint(radius=60, n_tooth=16, tooth_height=16, tooth_height_inner=2, - tol=0.01): + tol=0.01, + tag_prefix="", + is_mated=False): """ Creates a cylindrical Hirth Joint + + is_mated: If set to true, rotate the teeth so they line up at 0 degrees. """ # ensures secant doesn't blow up assert n_tooth >= 5 @@ -51,9 +62,14 @@ def hirth_joint(radius=60, .val() ) tooth_centre_radius = radius_inner * math.cos(theta) + angle_offset = hirth_tooth_angle(n_tooth) / 2 if is_mated else 0 teeth = ( Cq.Workplane('XY') - .polarArray(radius=tooth_centre_radius, startAngle=0, angle=360, count=n_tooth) + .polarArray( + radius=tooth_centre_radius, + startAngle=angle_offset, + angle=360, + count=n_tooth) .eachpoint(lambda loc: tooth.located(loc)) .intersect(Cq.Solid.makeCylinder( height=base_height + tooth_height, @@ -66,36 +82,54 @@ def hirth_joint(radius=60, height=base_height, radius=radius, centered=(True, True, False)) - .faces(">Z").tag("bore") + .faces(">Z").tag(f"{tag_prefix}bore") .union(teeth.val().move(Cq.Location((0,0,base_height))), tol=tol) .clean() ) #base.workplane(offset=tooth_height/2).circle(radius=radius,forConstruction=True).tag("mate") - base.polyline([(0, 0, base_height), (0, 0, base_height+tooth_height)], forConstruction=True).tag("mate") + ( + base + .polyline([(0, 0, base_height), (0, 0, base_height+tooth_height)], forConstruction=True) + .tag(f"{tag_prefix}mate") + ) + ( + base + .polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True) + .tag(f"{tag_prefix}directrix") + ) return base -def hirth_assembly(): +def hirth_assembly(n_tooth=12): """ Example assembly of two Hirth joints """ - rotate = 180 / 16 - obj1 = hirth_joint().faces(tag="bore").cboreHole( - diameter=10, - cboreDiameter=20, - cboreDepth=3) - obj2 = ( - hirth_joint() - .rotate( - axisStartPoint=(0,0,0), - axisEndPoint=(0,0,1), - angleDegrees=rotate - ) + #rotate = 180 / 16 + + tab = ( + Cq.Workplane('XY') + .box(100, 10, 2, centered=False) ) + obj1 = ( + hirth_joint(n_tooth=n_tooth) + .faces(tag="bore") + .cboreHole( + diameter=10, + cboreDiameter=20, + cboreDepth=3) + .union(tab) + ) + obj2 = ( + hirth_joint(n_tooth=n_tooth, is_mated=True) + .union(tab) + ) + angle = hirth_tooth_angle(n_tooth) result = ( Cq.Assembly() - .add(obj1, name="obj1", color=Cq.Color(0.8,0.8,0.5,0.3)) - .add(obj2, name="obj2", color=Cq.Color(0.5,0.5,0.5,0.3)) + .add(obj1, name="obj1", color=Role.PARENT.color) + .add(obj2, name="obj2", color=Role.CHILD.color) + .constrain("obj1", "Fixed") .constrain("obj1?mate", "obj2?mate", "Plane") + .constrain("obj1?directrix", "obj2?directrix", "Axis", param=angle) .solve() ) return result -- 2.44.1 From 87e99ac4ceab6d6daecb600d1c25d208934379e7 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 28 Jun 2024 21:36:33 -0400 Subject: [PATCH 021/187] fix: Collision problem with Hirth joints --- nhf/joints.py | 65 ++++++++++++++++++++++++++++++++++++--------------- nhf/test.py | 6 ++++- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/nhf/joints.py b/nhf/joints.py index 7c227b4..43c5169 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -23,50 +23,69 @@ def hirth_joint(radius=60, Creates a cylindrical Hirth Joint is_mated: If set to true, rotate the teeth so they line up at 0 degrees. + + FIXME: The curves don't mate perfectly. See if non-planar lofts can solve + this issue. """ - # ensures secant doesn't blow up + # ensures tangent doesn't blow up assert n_tooth >= 5 assert radius > radius_inner + assert tooth_height >= tooth_height_inner # angle of half of a single tooth theta = math.pi / n_tooth - # Generate a tooth by lofting between two curves - + c, s, t = math.cos(theta), math.sin(theta), math.tan(theta) + span = radius * t + radius_proj = radius / c + span_inner = radius_inner * s + # 2 * raise + (inner tooth height) = (tooth height) inner_raise = (tooth_height - tooth_height_inner) / 2 - # Outer tooth triangle spans a curve of length `2 pi r / n_tooth`. This - # creates the side profile (looking radially inwards) of each of the - # triangles. + # Outer tooth triangle spans 2*theta radians. This profile is the radial + # profile projected onto a plane `radius` away from the centre of the + # cylinder. The y coordinates on the edge must drop to compensate. + + # The drop is equal to, via similar triangles + drop = inner_raise * (radius_proj - radius) / (radius - radius_inner) outer = [ - (radius * math.tan(theta), 0), - (radius * math.tan(theta) - tol, 0), + (span, -tol - drop), + (span, -drop), (0, tooth_height), - (-radius * math.tan(theta) + tol, 0), - (-radius * math.tan(theta), 0), + (-span, -drop), + (-span, -tol - drop), ] + adj = radius_inner * c + # In the case of the inner triangle, it is projected onto a plane `adj` away + # from the centre. The apex must extrapolate + + # Via similar triangles + # + # (inner_raise + tooth_height_inner) - + # (tooth_height - inner_raise - tooth_height_inner) * ((radius_inner - adj) / (radius - radius_inner)) + apex = (inner_raise + tooth_height_inner) - \ + inner_raise * (radius_inner - adj) / (radius - radius_inner) inner = [ - (radius_inner * math.sin(theta), 0), - (radius_inner * math.sin(theta), inner_raise), - (0, inner_raise + tooth_height_inner), - (-radius_inner * math.sin(theta), inner_raise), - (-radius_inner * math.sin(theta), 0), + (span_inner, -tol), + (span_inner, inner_raise), + (0, apex), + (-span_inner, inner_raise), + (-span_inner, -tol), ] tooth = ( Cq.Workplane('YZ') .polyline(inner) .close() - .workplane(offset=radius - radius_inner) + .workplane(offset=radius - adj) .polyline(outer) .close() - .loft(ruled=True, combine=True) + .loft(ruled=False, combine=True) .val() ) - tooth_centre_radius = radius_inner * math.cos(theta) angle_offset = hirth_tooth_angle(n_tooth) / 2 if is_mated else 0 teeth = ( Cq.Workplane('XY') .polarArray( - radius=tooth_centre_radius, + radius=adj, startAngle=angle_offset, angle=360, count=n_tooth) @@ -75,6 +94,14 @@ def hirth_joint(radius=60, height=base_height + tooth_height, radius=radius, )) + .intersect(Cq.Solid.makeCylinder( + height=base_height + tooth_height, + radius=radius, + )) + .cut(Cq.Solid.makeCylinder( + height=base_height + tooth_height, + radius=radius_inner, + )) ) base = ( Cq.Workplane('XY') diff --git a/nhf/test.py b/nhf/test.py index e29b781..b0ab29a 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -3,6 +3,7 @@ import cadquery as Cq import nhf.joints import nhf.handle import nhf.metric_threads as NMt +from nhf.checks import binary_intersection class TestJoints(unittest.TestCase): @@ -12,7 +13,10 @@ class TestJoints(unittest.TestCase): j.val().solids(), Cq.Solid, msg="Hirth joint must be in one piece") def test_joints_hirth_assembly(self): - nhf.joints.hirth_assembly() + assembly = nhf.joints.hirth_assembly() + isect = binary_intersection(assembly) + self.assertLess(isect.Volume(), 1e-6, + "Hirth joint assembly must not have intersection") def test_joints_comma_assembly(self): nhf.joints.comma_assembly() def test_torsion_joint(self): -- 2.44.1 From 3170a025a1acde9fc4d2b08bb84389c5174be470 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 28 Jun 2024 23:07:37 -0400 Subject: [PATCH 022/187] refactor: Combine Hirth Joint into one class --- nhf/checks.py | 6 + nhf/joints.py | 299 +++++++++++++++--------------- nhf/materials.py | 1 + nhf/test.py | 8 +- nhf/touhou/houjuu_nue/__init__.py | 91 ++++----- nhf/touhou/houjuu_nue/test.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 25 ++- 7 files changed, 237 insertions(+), 195 deletions(-) create mode 100644 nhf/checks.py diff --git a/nhf/checks.py b/nhf/checks.py new file mode 100644 index 0000000..468cc8e --- /dev/null +++ b/nhf/checks.py @@ -0,0 +1,6 @@ +import cadquery as Cq + +def binary_intersection(a: Cq.Assembly) -> Cq.Shape: + objs = [s.toCompound() for _, s in a.traverse() if isinstance(s, Cq.Assembly)] + obj1, obj2 = objs[:2] + return obj1.intersect(obj2) diff --git a/nhf/joints.py b/nhf/joints.py index 43c5169..3327307 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -10,156 +10,165 @@ def hirth_tooth_angle(n_tooth): """ return 360 / n_tooth -def hirth_joint(radius=60, - radius_inner=40, - base_height=20, - n_tooth=16, - tooth_height=16, - tooth_height_inner=2, - tol=0.01, - tag_prefix="", - is_mated=False): +@dataclass(frozen=True) +class HirthJoint: """ - Creates a cylindrical Hirth Joint - - is_mated: If set to true, rotate the teeth so they line up at 0 degrees. - - FIXME: The curves don't mate perfectly. See if non-planar lofts can solve - this issue. + A Hirth joint attached to a cylindrical base """ - # ensures tangent doesn't blow up - assert n_tooth >= 5 - assert radius > radius_inner - assert tooth_height >= tooth_height_inner + radius: float = 60 + radius_inner: float = 40 + base_height: float = 20 + n_tooth: float = 16 + tooth_height: float = 16 + tooth_height_inner: float = 2 - # angle of half of a single tooth - theta = math.pi / n_tooth + def __post_init__(self): + # Ensures tangent doesn't blow up + assert self.n_tooth >= 5 + assert self.radius > self.radius_inner + assert self.tooth_height >= self.tooth_height_inner - c, s, t = math.cos(theta), math.sin(theta), math.tan(theta) - span = radius * t - radius_proj = radius / c - span_inner = radius_inner * s - # 2 * raise + (inner tooth height) = (tooth height) - inner_raise = (tooth_height - tooth_height_inner) / 2 - # Outer tooth triangle spans 2*theta radians. This profile is the radial - # profile projected onto a plane `radius` away from the centre of the - # cylinder. The y coordinates on the edge must drop to compensate. + @property + def _theta(self): + return math.pi / self.n_tooth - # The drop is equal to, via similar triangles - drop = inner_raise * (radius_proj - radius) / (radius - radius_inner) - outer = [ - (span, -tol - drop), - (span, -drop), - (0, tooth_height), - (-span, -drop), - (-span, -tol - drop), - ] - adj = radius_inner * c - # In the case of the inner triangle, it is projected onto a plane `adj` away - # from the centre. The apex must extrapolate + @property + def tooth_angle(self): + return hirth_tooth_angle(self.n_tooth) - # Via similar triangles - # - # (inner_raise + tooth_height_inner) - - # (tooth_height - inner_raise - tooth_height_inner) * ((radius_inner - adj) / (radius - radius_inner)) - apex = (inner_raise + tooth_height_inner) - \ - inner_raise * (radius_inner - adj) / (radius - radius_inner) - inner = [ - (span_inner, -tol), - (span_inner, inner_raise), - (0, apex), - (-span_inner, inner_raise), - (-span_inner, -tol), - ] - tooth = ( - Cq.Workplane('YZ') - .polyline(inner) - .close() - .workplane(offset=radius - adj) - .polyline(outer) - .close() - .loft(ruled=False, combine=True) - .val() - ) - angle_offset = hirth_tooth_angle(n_tooth) / 2 if is_mated else 0 - teeth = ( - Cq.Workplane('XY') - .polarArray( - radius=adj, - startAngle=angle_offset, - angle=360, - count=n_tooth) - .eachpoint(lambda loc: tooth.located(loc)) - .intersect(Cq.Solid.makeCylinder( - height=base_height + tooth_height, - radius=radius, - )) - .intersect(Cq.Solid.makeCylinder( - height=base_height + tooth_height, - radius=radius, - )) - .cut(Cq.Solid.makeCylinder( - height=base_height + tooth_height, - radius=radius_inner, - )) - ) - base = ( - Cq.Workplane('XY') - .cylinder( - height=base_height, - radius=radius, - centered=(True, True, False)) - .faces(">Z").tag(f"{tag_prefix}bore") - .union(teeth.val().move(Cq.Location((0,0,base_height))), tol=tol) - .clean() - ) - #base.workplane(offset=tooth_height/2).circle(radius=radius,forConstruction=True).tag("mate") - ( - base - .polyline([(0, 0, base_height), (0, 0, base_height+tooth_height)], forConstruction=True) - .tag(f"{tag_prefix}mate") - ) - ( - base - .polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True) - .tag(f"{tag_prefix}directrix") - ) - return base -def hirth_assembly(n_tooth=12): - """ - Example assembly of two Hirth joints - """ - #rotate = 180 / 16 + def generate(self, tag_prefix="", is_mated=False, tol=0.01): + """ + is_mated: If set to true, rotate the teeth so they line up at 0 degrees. - tab = ( - Cq.Workplane('XY') - .box(100, 10, 2, centered=False) - ) - obj1 = ( - hirth_joint(n_tooth=n_tooth) - .faces(tag="bore") - .cboreHole( - diameter=10, - cboreDiameter=20, - cboreDepth=3) - .union(tab) - ) - obj2 = ( - hirth_joint(n_tooth=n_tooth, is_mated=True) - .union(tab) - ) - angle = hirth_tooth_angle(n_tooth) - result = ( - Cq.Assembly() - .add(obj1, name="obj1", color=Role.PARENT.color) - .add(obj2, name="obj2", color=Role.CHILD.color) - .constrain("obj1", "Fixed") - .constrain("obj1?mate", "obj2?mate", "Plane") - .constrain("obj1?directrix", "obj2?directrix", "Axis", param=angle) - .solve() - ) - return result + FIXME: The curves don't mate perfectly. See if non-planar lofts can solve + this issue. + """ + c, s, t = math.cos(self._theta), math.sin(self._theta), math.tan(self._theta) + span = self.radius * t + radius_proj = self.radius / c + span_inner = self.radius_inner * s + # 2 * raise + (inner tooth height) = (tooth height) + inner_raise = (self.tooth_height - self.tooth_height_inner) / 2 + # Outer tooth triangle spans 2*theta radians. This profile is the radial + # profile projected onto a plane `radius` away from the centre of the + # cylinder. The y coordinates on the edge must drop to compensate. + + # The drop is equal to, via similar triangles + drop = inner_raise * (radius_proj - self.radius) / (self.radius - self.radius_inner) + outer = [ + (span, -tol - drop), + (span, -drop), + (0, self.tooth_height), + (-span, -drop), + (-span, -tol - drop), + ] + adj = self.radius_inner * c + # In the case of the inner triangle, it is projected onto a plane `adj` away + # from the centre. The apex must extrapolate + + # Via similar triangles + # + # (inner_raise + tooth_height_inner) - + # (tooth_height - inner_raise - tooth_height_inner) * ((radius_inner - adj) / (radius - radius_inner)) + apex = (inner_raise + self.tooth_height_inner) - \ + inner_raise * (self.radius_inner - adj) / (self.radius - self.radius_inner) + inner = [ + (span_inner, -tol), + (span_inner, inner_raise), + (0, apex), + (-span_inner, inner_raise), + (-span_inner, -tol), + ] + tooth = ( + Cq.Workplane('YZ') + .polyline(inner) + .close() + .workplane(offset=self.radius - adj) + .polyline(outer) + .close() + .loft(ruled=False, combine=True) + .val() + ) + angle_offset = hirth_tooth_angle(self.n_tooth) / 2 if is_mated else 0 + h = self.base_height + self.tooth_height + teeth = ( + Cq.Workplane('XY') + .polarArray( + radius=adj, + startAngle=angle_offset, + angle=360, + count=self.n_tooth) + .eachpoint(lambda loc: tooth.located(loc)) + .intersect(Cq.Solid.makeCylinder( + height=h, + radius=self.radius, + )) + .cut(Cq.Solid.makeCylinder( + height=h, + radius=self.radius_inner, + )) + ) + base = ( + Cq.Workplane('XY') + .cylinder( + height=self.base_height, + radius=self.radius, + centered=(True, True, False)) + .faces(">Z").tag(f"{tag_prefix}bore") + .union( + teeth.val().move(Cq.Location((0,0,self.base_height))), + tol=tol) + .clean() + ) + #base.workplane(offset=tooth_height/2).circle(radius=radius,forConstruction=True).tag("mate") + ( + base + .polyline([ + (0, 0, self.base_height), + (0, 0, self.base_height + self.tooth_height) + ], forConstruction=True) + .tag(f"{tag_prefix}mate") + ) + ( + base + .polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True) + .tag(f"{tag_prefix}directrix") + ) + return base + + def assembly(self): + """ + Generate an example assembly + """ + tab = ( + Cq.Workplane('XY') + .box(100, 10, 2, centered=False) + ) + obj1 = ( + self.generate() + .faces(tag="bore") + .cboreHole( + diameter=10, + cboreDiameter=20, + cboreDepth=3) + .union(tab) + ) + obj2 = ( + self.generate(is_mated=True) + .union(tab) + ) + angle = 1 * self.tooth_angle + result = ( + Cq.Assembly() + .add(obj1, name="obj1", color=Role.PARENT.color) + .add(obj2, name="obj2", color=Role.CHILD.color) + .constrain("obj1", "Fixed") + .constrain("obj1?mate", "obj2?mate", "Plane") + .constrain("obj1?directrix", "obj2?directrix", "Axis", param=angle) + .solve() + ) + return result def comma_joint(radius=30, shaft_radius=10, @@ -429,10 +438,10 @@ class TorsionJoint: spring = self.spring() result = ( Cq.Assembly() - .add(spring, name="spring", color=Cq.Color(0.5,0.5,0.5,1)) - .add(track, name="track", color=Cq.Color(0.5,0.5,0.8,0.3)) + .add(spring, name="spring", color=Role.DAMPING.color) + .add(track, name="track", color=Role.PARENT.color) .constrain("track?spring", "spring?top", "Plane") - .add(rider, name="rider", color=Cq.Color(0.8,0.8,0.5,0.3)) + .add(rider, name="rider", color=Role.CHILD.color) .constrain("rider?spring", "spring?bot", "Plane") .constrain("track?directrix", "spring?directrix_bot", "Axis") .constrain("rider?directrix0", "spring?directrix_top", "Axis") diff --git a/nhf/materials.py b/nhf/materials.py index 7ac4c26..c0adbde 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -16,6 +16,7 @@ class Role(Enum): # Parent and child components in a load bearing joint PARENT = _color('blue4', 0.6) CHILD = _color('darkorange2', 0.6) + DAMPING = _color('springgreen', 0.5) STRUCTURE = _color('gray', 0.4) DECORATION = _color('lightseagreen', 0.4) ELECTRONIC = _color('mediumorchid', 0.5) diff --git a/nhf/test.py b/nhf/test.py index b0ab29a..ed85cfd 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -8,12 +8,14 @@ from nhf.checks import binary_intersection class TestJoints(unittest.TestCase): def test_joint_hirth(self): - j = nhf.joints.hirth_joint() + j = nhf.joints.HirthJoint() + obj = j.generate() self.assertIsInstance( - j.val().solids(), Cq.Solid, + obj.val().solids(), Cq.Solid, msg="Hirth joint must be in one piece") def test_joints_hirth_assembly(self): - assembly = nhf.joints.hirth_assembly() + j = nhf.joints.HirthJoint() + assembly = j.assembly() isect = binary_intersection(assembly) self.assertLess(isect.Volume(), 1e-6, "Hirth joint assembly must not have intersection") diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 38e4f81..f059eca 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -30,10 +30,12 @@ import cadquery as Cq import nhf.joints import nhf.handle from nhf import Material +from nhf.joints import HirthJoint +from nhf.handle import Handle import nhf.touhou.houjuu_nue.wing as MW import nhf.touhou.houjuu_nue.trident as MT -@dataclass(frozen=True) +@dataclass class Parameters: """ Defines dimensions for the Houjuu Nue cosplay @@ -44,9 +46,9 @@ class Parameters: # Harness harness_thickness: float = 25.4 / 8 - harness_width = 300 - harness_height = 400 - harness_fillet = 10 + harness_width: float = 300 + harness_height: float = 400 + harness_fillet: float = 10 harness_wing_base_pos = [ ("r1", 70, 150), @@ -60,36 +62,39 @@ class Parameters: # Holes drilled onto harness for attachment with HS joint harness_to_root_conn_diam = 6 + hs_hirth_joint: HirthJoint = HirthJoint( + radius=30, + radius_inner=20, + tooth_height=10, + base_height=5 + ) + # Wing root properties # # The Houjuu-Scarlett joint mechanism at the base of the wing - hs_joint_base_width = 85 - hs_joint_base_thickness = 10 - hs_joint_ring_thickness = 5 - hs_joint_tooth_height = 10 - hs_joint_radius = 30 - hs_joint_radius_inner = 20 - hs_joint_corner_fillet = 5 - hs_joint_corner_cbore_diam = 12 - hs_joint_corner_cbore_depth = 2 - hs_joint_corner_inset = 12 + hs_joint_base_width: float = 85 + hs_joint_base_thickness: float = 10 + hs_joint_corner_fillet: float = 5 + hs_joint_corner_cbore_diam: float = 12 + hs_joint_corner_cbore_depth: float = 2 + hs_joint_corner_inset: float = 12 - hs_joint_axis_diam = 12 - hs_joint_axis_cbore_diam = 20 - hs_joint_axis_cbore_depth = 3 + hs_joint_axis_diam: float = 12 + hs_joint_axis_cbore_diam: float = 20 + hs_joint_axis_cbore_depth: float = 3 # Exterior radius of the wing root assembly - wing_root_radius = 40 + wing_root_radius: float = 40 """ Heights for various wing joints, where the numbers start from the first joint. """ - wing_r1_height = 100 - wing_r1_width = 400 - wing_r2_height = 100 - wing_r3_height = 100 + wing_r1_height: float = 100 + wing_r1_width: float = 400 + wing_r2_height: float = 100 + wing_r3_height: float = 100 - trident_handle: nhf.handle.Handle = nhf.handle.Handle( + trident_handle: Handle = Handle( diam=38, diam_inner=33, # M27-3 @@ -100,7 +105,7 @@ class Parameters: ) def __post_init__(self): - assert self.wing_root_radius > self.hs_joint_radius,\ + assert self.wing_root_radius > self.hs_hirth_joint.radius,\ "Wing root must be large enough to accomodate joint" @@ -174,20 +179,12 @@ class Parameters: (-dx, dx), ] - def hs_joint_component(self): - hirth = nhf.joints.hirth_joint( - radius=self.hs_joint_radius, - radius_inner=self.hs_joint_radius_inner, - tooth_height=self.hs_joint_tooth_height, - base_height=self.hs_joint_ring_thickness) - return hirth - def hs_joint_parent(self): """ Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth coupling, a cylindrical base, and a mounting base. """ - hirth = self.hs_joint_component().val() + hirth = self.hs_hirth_joint.generate() #hirth = ( # hirth # .faces("Z") .workplane() - .union(hirth.move(Cq.Location((0, 0, self.hs_joint_base_thickness))), tol=0.1) + .union(hirth.translate((0, 0, self.hs_joint_base_thickness)), tol=0.1) .clean() ) result = ( @@ -247,7 +244,12 @@ class Parameters: result.faces(" Cq.Shape: + """ + Generate the wing root which contains a Hirth joint at its base and a + rectangular opening on its side, with the necessary interfaces. + """ + return MW.wing_root(joint=self.hs_hirth_joint) def wing_r1_profile(self) -> Cq.Sketch: """ @@ -288,13 +290,6 @@ class Parameters: ) return result - def wing_root(self) -> Cq.Shape: - """ - Generate the wing root which contains a Hirth joint at its base and a - rectangular opening on its side, with the necessary interfaces. - """ - return MW.wing_root() - ###################### # Assemblies # ###################### @@ -310,12 +305,18 @@ class Parameters: j = self.hs_joint_parent() ( result - .add(j, name=f"hs_{name}p", color=Material.PLASTIC_PLA.color) + .add(j, name=f"hs_{name}", color=Material.PLASTIC_PLA.color) #.constrain(f"harness?{name}", f"hs_{name}p?mate", "Point") - .constrain("harness?mount", f"hs_{name}p?base", "Axis") + .constrain("harness?mount", f"hs_{name}?base", "Axis") ) for i in range(4): - result.constrain(f"harness?{name}_{i}", f"hs_{name}p?h{i}", "Point") + result.constrain(f"harness?{name}_{i}", f"hs_{name}?h{i}", "Point") + angle = 6 * self.hs_hirth_joint.tooth_angle + ( + result.add(self.wing_root(), name="w0_r1", color=Material.PLASTIC_PLA.color) + .constrain("w0_r1?mate", "hs_r1?mate", "Plane") + .constrain("w0_r1?directrix", "hs_r1?directrix", "Axis", param=angle) + ) result.solve() return result diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 7b22e46..42b3006 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -20,7 +20,7 @@ class Test(unittest.TestCase): self.assertLess(bbox.zlen, 255, msg=msg) def test_wings(self): p = M.Parameters() - p.wing_r1() + p.wing_root() def test_harness(self): p = M.Parameters() p.harness_assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 8d36658..90ffb47 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -1,5 +1,10 @@ +""" +This file describes the shapes of the wing shells. The joints are defined in +`__init__.py`. +""" import math import cadquery as Cq +from nhf.joints import HirthJoint def wing_root_profiles( base_sweep=150, @@ -117,7 +122,13 @@ def wing_root_profiles( .wires().val() ) return base, middle, tip -def wing_root(): + + +def wing_root(joint: HirthJoint, + bolt_diam: int = 12): + """ + Generate the contiguous components of the root wing segment + """ root_profile, middle_profile, tip_profile = wing_root_profiles() rotate_centre = Cq.Vector(-200, 0, -25) @@ -175,4 +186,16 @@ def wing_root(): result = seg1.union(seg2).union(seg3) result.faces("X").tag("conn") + + j = ( + joint.generate(is_mated=True) + .faces(" Date: Sun, 30 Jun 2024 14:28:42 -0700 Subject: [PATCH 023/187] fix: Hirth joint mating --- nhf/joints.py | 132 ++++++++++++++++++-------------------------------- nhf/test.py | 13 +++-- 2 files changed, 54 insertions(+), 91 deletions(-) diff --git a/nhf/joints.py b/nhf/joints.py index 3327307..91ae4c2 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -4,126 +4,86 @@ import cadquery as Cq import nhf.springs as NS from nhf import Role -def hirth_tooth_angle(n_tooth): - """ - Angle of one whole tooth - """ - return 360 / n_tooth - @dataclass(frozen=True) class HirthJoint: """ A Hirth joint attached to a cylindrical base """ + + # r radius: float = 60 + # r_i radius_inner: float = 40 base_height: float = 20 n_tooth: float = 16 + # h_o tooth_height: float = 16 - tooth_height_inner: float = 2 def __post_init__(self): # Ensures tangent doesn't blow up assert self.n_tooth >= 5 assert self.radius > self.radius_inner - assert self.tooth_height >= self.tooth_height_inner - - @property - def _theta(self): - return math.pi / self.n_tooth @property def tooth_angle(self): - return hirth_tooth_angle(self.n_tooth) + return 360 / self.n_tooth def generate(self, tag_prefix="", is_mated=False, tol=0.01): """ is_mated: If set to true, rotate the teeth so they line up at 0 degrees. - FIXME: The curves don't mate perfectly. See if non-planar lofts can solve - this issue. + FIXME: Mate is not exact when number of tooth is low """ - c, s, t = math.cos(self._theta), math.sin(self._theta), math.tan(self._theta) - span = self.radius * t - radius_proj = self.radius / c - span_inner = self.radius_inner * s - # 2 * raise + (inner tooth height) = (tooth height) - inner_raise = (self.tooth_height - self.tooth_height_inner) / 2 - # Outer tooth triangle spans 2*theta radians. This profile is the radial - # profile projected onto a plane `radius` away from the centre of the - # cylinder. The y coordinates on the edge must drop to compensate. - - # The drop is equal to, via similar triangles - drop = inner_raise * (radius_proj - self.radius) / (self.radius - self.radius_inner) - outer = [ - (span, -tol - drop), - (span, -drop), - (0, self.tooth_height), - (-span, -drop), - (-span, -tol - drop), - ] - adj = self.radius_inner * c - # In the case of the inner triangle, it is projected onto a plane `adj` away - # from the centre. The apex must extrapolate - - # Via similar triangles - # - # (inner_raise + tooth_height_inner) - - # (tooth_height - inner_raise - tooth_height_inner) * ((radius_inner - adj) / (radius - radius_inner)) - apex = (inner_raise + self.tooth_height_inner) - \ - inner_raise * (self.radius_inner - adj) / (self.radius - self.radius_inner) - inner = [ - (span_inner, -tol), - (span_inner, inner_raise), - (0, apex), - (-span_inner, inner_raise), - (-span_inner, -tol), - ] - tooth = ( + phi = math.radians(self.tooth_angle) + alpha = 2 * math.atan(self.radius / self.tooth_height * math.tan(phi/2)) + #alpha = math.atan(self.radius * math.radians(180 / self.n_tooth) / self.tooth_height) + gamma = math.radians(90 / self.n_tooth) + # Tooth half height + l = self.radius * math.cos(gamma) + a = self.radius * math.sin(gamma) + t = a / math.tan(alpha / 2) + beta = math.asin(t / l) + dx = self.tooth_height * math.tan(alpha / 2) + profile = ( Cq.Workplane('YZ') - .polyline(inner) + .polyline([ + (0, 0), + (dx, self.tooth_height), + (-dx, self.tooth_height), + ]) .close() - .workplane(offset=self.radius - adj) - .polyline(outer) - .close() - .loft(ruled=False, combine=True) + .extrude(-self.radius) .val() + .rotate((0, 0, 0), (0, 1, 0), math.degrees(beta)) + .moved(Cq.Location((0, 0, self.base_height))) ) - angle_offset = hirth_tooth_angle(self.n_tooth) / 2 if is_mated else 0 - h = self.base_height + self.tooth_height - teeth = ( + core = Cq.Solid.makeCylinder( + radius=self.radius_inner, + height=self.tooth_height, + pnt=(0, 0, self.base_height), + ) + angle_offset = self.tooth_angle / 2 if is_mated else 0 + result = ( Cq.Workplane('XY') + .cylinder( + radius=self.radius, + height=self.base_height + self.tooth_height, + centered=(True, True, False)) + .faces(">Z") + .tag(f"{tag_prefix}bore") + .cut(core) .polarArray( - radius=adj, + radius=self.radius, startAngle=angle_offset, angle=360, count=self.n_tooth) - .eachpoint(lambda loc: tooth.located(loc)) - .intersect(Cq.Solid.makeCylinder( - height=h, - radius=self.radius, - )) - .cut(Cq.Solid.makeCylinder( - height=h, - radius=self.radius_inner, - )) + .cutEach( + lambda loc: profile.moved(loc), + ) ) - base = ( - Cq.Workplane('XY') - .cylinder( - height=self.base_height, - radius=self.radius, - centered=(True, True, False)) - .faces(">Z").tag(f"{tag_prefix}bore") - .union( - teeth.val().move(Cq.Location((0,0,self.base_height))), - tol=tol) - .clean() - ) - #base.workplane(offset=tooth_height/2).circle(radius=radius,forConstruction=True).tag("mate") ( - base + result .polyline([ (0, 0, self.base_height), (0, 0, self.base_height + self.tooth_height) @@ -131,11 +91,11 @@ class HirthJoint: .tag(f"{tag_prefix}mate") ) ( - base + result .polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True) .tag(f"{tag_prefix}directrix") ) - return base + return result def assembly(self): """ diff --git a/nhf/test.py b/nhf/test.py index ed85cfd..4f97602 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -13,12 +13,15 @@ class TestJoints(unittest.TestCase): self.assertIsInstance( obj.val().solids(), Cq.Solid, msg="Hirth joint must be in one piece") + def test_joints_hirth_assembly(self): - j = nhf.joints.HirthJoint() - assembly = j.assembly() - isect = binary_intersection(assembly) - self.assertLess(isect.Volume(), 1e-6, - "Hirth joint assembly must not have intersection") + for n_tooth in [16, 20, 24]: + with self.subTest(n_tooth=n_tooth): + j = nhf.joints.HirthJoint() + assembly = j.assembly() + isect = binary_intersection(assembly) + self.assertLess(isect.Volume(), 1e-6, + "Hirth joint assembly must not have intersection") def test_joints_comma_assembly(self): nhf.joints.comma_assembly() def test_torsion_joint(self): -- 2.44.1 From 59bcc9914cdf7eda5bd5bf7cce90e4df5b8249e4 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 30 Jun 2024 19:03:16 -0700 Subject: [PATCH 024/187] fix: Remove tag prefix in favour of subassembly --- nhf/joints.py | 24 +++++++++++++++--------- nhf/springs.py | 12 ++++++++++-- nhf/touhou/houjuu_nue/test.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 3 ++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/nhf/joints.py b/nhf/joints.py index 91ae4c2..29ff40b 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -29,7 +29,7 @@ class HirthJoint: return 360 / self.n_tooth - def generate(self, tag_prefix="", is_mated=False, tol=0.01): + def generate(self, is_mated=False, tol=0.01): """ is_mated: If set to true, rotate the teeth so they line up at 0 degrees. @@ -71,7 +71,7 @@ class HirthJoint: height=self.base_height + self.tooth_height, centered=(True, True, False)) .faces(">Z") - .tag(f"{tag_prefix}bore") + .tag("bore") .cut(core) .polarArray( radius=self.radius, @@ -88,12 +88,12 @@ class HirthJoint: (0, 0, self.base_height), (0, 0, self.base_height + self.tooth_height) ], forConstruction=True) - .tag(f"{tag_prefix}mate") + .tag("mate") ) ( result .polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True) - .tag(f"{tag_prefix}directrix") + .tag("directrix") ) return result @@ -235,7 +235,7 @@ class TorsionJoint: disk_height = 10 radius_spring = 15 - radius_axle = 10 + radius_axle = 6 # Offset of the spring hole w.r.t. surface spring_hole_depth = 4 @@ -320,7 +320,10 @@ class TorsionJoint: ) result = ( Cq.Workplane('XY') - .cylinder(radius=self.radius, height=self.disk_height) + .cylinder( + radius=self.radius, + height=self.disk_height, + centered=(True, True, False)) .faces('>Z') .tag("spring") .placeSketch(spring_hole_profile) @@ -331,7 +334,7 @@ class TorsionJoint: .placeSketch(groove_profile) .extrude(self.groove_depth) .faces('>Z') - .hole(self.radius_axle) + .hole(self.radius_axle * 2) ) # Insert directrix` result.polyline(self._directrix(self.disk_height), @@ -370,7 +373,10 @@ class TorsionJoint: middle_height = self.spring_height - self.groove_depth - self.rider_gap result = ( Cq.Workplane('XY') - .cylinder(radius=self.radius, height=self.disk_height) + .cylinder( + radius=self.radius, + height=self.disk_height, + centered=(True, True, False)) .faces('>Z') .tag("spring") .placeSketch(wall_profile) @@ -384,7 +390,7 @@ class TorsionJoint: .circle(self._radius_spring_internal) .extrude(self.spring_height) .faces('>Z') - .hole(self.radius_axle) + .hole(self.radius_axle * 2) ) for i in range(self.n_slots): theta = 2 * math.pi * i / self.n_slots diff --git a/nhf/springs.py b/nhf/springs.py index 06e14b5..31fcf51 100644 --- a/nhf/springs.py +++ b/nhf/springs.py @@ -24,13 +24,21 @@ def torsion_spring(radius=12, .transformed( offset=(0, radius-thickness), rotate=(0, 0, 0)) - .box(length=tail_length, width=thickness, height=thickness, centered=False) + .box( + length=tail_length, + width=thickness, + height=thickness, + centered=False) .copyWorkplane(Cq.Workplane('XY')) .transformed( offset=(0, 0, height - thickness), rotate=(0, 0, omega)) .center(-tail_length, radius-thickness) - .box(length=tail_length, width=thickness, height=thickness, centered=False) + .box( + length=tail_length, + width=thickness, + height=thickness, + centered=False) ) result.polyline([(0, radius, 0), (tail_length, radius, 0)], forConstruction=True).tag("directrix_bot") diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 42b3006..cd0d17b 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -11,7 +11,7 @@ class Test(unittest.TestCase): def test_wing_root(self): p = M.Parameters() obj = p.wing_root() - self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece") + #self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece") bbox = obj.BoundingBox() msg = "Must fix 256^3 bbox" diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 90ffb47..04de65a 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -195,7 +195,8 @@ def wing_root(joint: HirthJoint, result = ( result .union(j.translate((0, 0, -10))) - .union(Cq.Solid.makeCylinder(57, 5).moved(Cq.Location((0, 0, -10)))) + #.union(Cq.Solid.makeCylinder(57, 5).moved(Cq.Location((0, 0, -10)))) + .union(Cq.Solid.makeCylinder(20, 5).moved(Cq.Location((0, 0, -10)))) .clean() ) return result -- 2.44.1 From 1710f0db36b49533807efdc2965a6b45a08e010f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 1 Jul 2024 17:59:42 -0700 Subject: [PATCH 025/187] feat: Improve model for printing --- nhf/handle.py | 50 +++++++++++++++++++++++++++---- nhf/touhou/houjuu_nue/__init__.py | 2 +- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/nhf/handle.py b/nhf/handle.py index 0424569..5bdd311 100644 --- a/nhf/handle.py +++ b/nhf/handle.py @@ -74,11 +74,13 @@ class Handle: result.faces(">Z").tag("mate2") return result - def _external_thread(self): + def _external_thread(self, length=None): + if length is None: + length = self.insertion_length return NMt.external_metric_thread( self.diam_threading, self.thread_pitch, - self.insertion_length, + length, top_lead_in=True) def _internal_thread(self): return NMt.internal_metric_thread( @@ -86,7 +88,7 @@ class Handle: self.thread_pitch, self.insertion_length) - def insertion(self): + def insertion(self, holes=[]): """ This type of joint is used to connect two handlebar pieces. Each handlebar piece is a tube which cannot be machined, so the joint connects to the @@ -95,6 +97,12 @@ class Handle: Tags: * lip: Co-planar Mates to the rod * mate: Mates to the connector + + WARNING: A tolerance lower than the defualt (maybe 5e-4) is required for + STL export. + + Set `holes` to the heights for drilling holes into the model for resin + to flow out. """ result = ( Cq.Workplane('XY') @@ -117,12 +125,22 @@ class Handle: if not self.simplify_geometry: thread = self._internal_thread().val() result = result.union(thread) + for h in holes: + cyl = Cq.Solid.makeCylinder( + radius=2, + height=self.diam * 2, + pnt=(-self.diam, 0, h), + dir=(1, 0, 0)) + result = result.cut(cyl) return result - def connector(self, solid: bool = False): + def connector(self, solid: bool = True): """ Tags: * mate{1,2}: Mates to the connector + + WARNING: A tolerance lower than the defualt (maybe 2e-4) is required for + STL export. """ result = ( Cq.Workplane('XY') @@ -151,7 +169,7 @@ class Handle: .located(Cq.Location((0, 0, self.connector_length / 2)))) .union( thread - .rotate((0,0,0), (1,0,0), angleDegrees=90) + .rotate((0,0,0), (1,0,0), angleDegrees=180) .located(Cq.Location((0, 0, -self.connector_length / 2)))) ) return result @@ -183,6 +201,28 @@ class Handle: ) return result + def threaded_core(self, length): + """ + Generates a threaded core for unioning with other components + """ + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.diam_connector_external / 2, + height=length, + centered=(True, True, False), + ) + ) + result.faces(">Z").tag("mate") + result.faces(" Date: Tue, 2 Jul 2024 19:59:09 -0700 Subject: [PATCH 026/187] fix: One sided connector --- nhf/handle.py | 11 +++++++---- nhf/joints.py | 3 ++- nhf/touhou/houjuu_nue/trident.py | 12 +++++++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/nhf/handle.py b/nhf/handle.py index 5bdd311..36462b8 100644 --- a/nhf/handle.py +++ b/nhf/handle.py @@ -36,7 +36,7 @@ class Handle: # Length for the rim on the female connector rim_length: float = 5 - insertion_length: float = 60 + insertion_length: float = 30 # Amount by which the connector goes into the segment connector_length: float = 60 @@ -174,12 +174,15 @@ class Handle: ) return result - def one_side_connector(self): + def one_side_connector(self, height=None): + if height is None: + height = self.rim_length result = ( Cq.Workplane('XY') .cylinder( radius=self.diam / 2, - height=self.rim_length, + height=height, + centered=(True, True, False) ) ) result.faces(">Z").tag("mate") @@ -197,7 +200,7 @@ class Handle: result .union( thread - .located(Cq.Location((0, 0, self.connector_length / 2)))) + .located(Cq.Location((0, 0, height)))) ) return result diff --git a/nhf/joints.py b/nhf/joints.py index 29ff40b..1fb938b 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -223,7 +223,7 @@ def comma_assembly(): ) return result -@dataclass(frozen=True) +@dataclass class TorsionJoint: """ This jonit consists of a rider puck on a track puck. IT is best suited if @@ -260,6 +260,7 @@ class TorsionJoint: assert self.groove_radius_outer > self.groove_radius_inner assert self.groove_radius_inner > self.radius_spring assert self.spring_height > self.groove_depth, "Groove is too deep" + assert self.radius_spring > self.radius_axle @property def total_height(self): diff --git a/nhf/touhou/houjuu_nue/trident.py b/nhf/touhou/houjuu_nue/trident.py index 769213c..71901ab 100644 --- a/nhf/touhou/houjuu_nue/trident.py +++ b/nhf/touhou/houjuu_nue/trident.py @@ -5,9 +5,17 @@ from nhf.handle import Handle def trident_assembly( handle: Handle, - handle_segment_length: float = 24*25.4): + handle_segment_length: float = 24*25.4, + terminal_height=100): def segment(): return handle.segment(handle_segment_length) + + terminal = ( + handle + .one_side_connector(height=terminal_height) + .faces(">Z") + .hole(15, terminal_height + handle.insertion_length - 10) + ) mat_i = Material.PLASTIC_PLA mat_s = Material.ACRYLIC_BLACK assembly = ( @@ -28,5 +36,7 @@ def trident_assembly( .constrain("s2?mate2", "i3?rim", "Plane", param=0) .add(handle.one_side_connector(), name="head", color=mat_i.color) .constrain("i3?mate", "head?mate", "Plane") + .add(terminal, name="terminal", color=mat_i.color) + .constrain("i0?mate", "terminal?mate", "Plane") ) return assembly.solve() -- 2.44.1 From e75e640623faa8a0939d8db333f9f91c4b9bb8b0 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 3 Jul 2024 18:45:16 -0700 Subject: [PATCH 027/187] fix: Type missing in dataclass --- nhf/handle.py | 2 +- nhf/joints.py | 28 ++++++++++++++-------------- nhf/touhou/houjuu_nue/__init__.py | 16 ++++++++-------- nhf/touhou/houjuu_nue/test.py | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/nhf/handle.py b/nhf/handle.py index 36462b8..36c41a7 100644 --- a/nhf/handle.py +++ b/nhf/handle.py @@ -5,7 +5,7 @@ from dataclasses import dataclass import cadquery as Cq import nhf.metric_threads as NMt -@dataclass(frozen=True) +@dataclass class Handle: """ Characteristic of a tool handle diff --git a/nhf/joints.py b/nhf/joints.py index 1fb938b..f8e5f8f 100644 --- a/nhf/joints.py +++ b/nhf/joints.py @@ -4,7 +4,7 @@ import cadquery as Cq import nhf.springs as NS from nhf import Role -@dataclass(frozen=True) +@dataclass class HirthJoint: """ A Hirth joint attached to a cylindrical base @@ -231,26 +231,26 @@ class TorsionJoint: """ # Radius limit for rotating components - radius = 40 - disk_height = 10 + radius: float = 40 + disk_height: float = 10 - radius_spring = 15 - radius_axle = 6 + radius_spring: float = 15 + radius_axle: float = 6 # Offset of the spring hole w.r.t. surface - spring_hole_depth = 4 + spring_hole_depth: float = 4 # Also used for the height of the hole for the spring - spring_thickness = 2 - spring_height = 15 + spring_thickness: float = 2 + spring_height: float = 15 - spring_tail_length = 40 + spring_tail_length: float = 40 - groove_radius_outer = 35 - groove_radius_inner = 20 - groove_depth = 5 - rider_gap = 2 - n_slots = 8 + groove_radius_outer: float = 35 + groove_radius_inner: float = 20 + groove_depth: float = 5 + rider_gap: float = 2 + n_slots: float = 8 right_handed: bool = False diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 43d8b44..55f3614 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -24,7 +24,7 @@ from root to tip s0 (root), s1, s2, s3. The joints are named (from root to tip) shoulder, elbow, wrist in analogy with human anatomy. """ -from dataclasses import dataclass +from dataclasses import dataclass, field import unittest import cadquery as Cq import nhf.joints @@ -50,24 +50,24 @@ class Parameters: harness_height: float = 400 harness_fillet: float = 10 - harness_wing_base_pos = [ + harness_wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [ ("r1", 70, 150), ("l1", -70, 150), ("r2", 100, 0), ("l2", -100, 0), ("r3", 70, -150), ("l3", -70, -150), - ] + ]) # Holes drilled onto harness for attachment with HS joint - harness_to_root_conn_diam = 6 + harness_to_root_conn_diam: float = 6 - hs_hirth_joint: HirthJoint = HirthJoint( + hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=30, radius_inner=20, tooth_height=10, base_height=5 - ) + )) # Wing root properties # @@ -94,7 +94,7 @@ class Parameters: wing_r2_height: float = 100 wing_r3_height: float = 100 - trident_handle: Handle = Handle( + trident_handle: Handle = field(default_factory=lambda: Handle( diam=38, diam_inner=38-2 * 25.4/8, # M27-3 @@ -102,7 +102,7 @@ class Parameters: thread_pitch=3, diam_connector_internal=18, simplify_geometry=False, - ) + )) def __post_init__(self): assert self.wing_root_radius > self.hs_hirth_joint.radius,\ diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index cd0d17b..57d1aaa 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -12,7 +12,7 @@ class Test(unittest.TestCase): p = M.Parameters() obj = p.wing_root() #self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece") - bbox = obj.BoundingBox() + bbox = obj.val().BoundingBox() msg = "Must fix 256^3 bbox" self.assertLess(bbox.xlen, 255, msg=msg) -- 2.44.1 From 6201683c00a21ec3d1e2650ebf3429c5700f7e5b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 3 Jul 2024 23:15:39 -0700 Subject: [PATCH 028/187] feat: Add build system --- .gitignore | 3 ++ nhf/build.py | 81 +++++++++++++++++++++++++++++++ nhf/touhou/houjuu_nue/__init__.py | 14 +++++- poetry.lock | 2 +- pyproject.toml | 1 + 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 nhf/build.py diff --git a/.gitignore b/.gitignore index 3226ba4..182aa47 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ result # Python __pycache__ *.py[cod] + +# Model build output +/build diff --git a/nhf/build.py b/nhf/build.py new file mode 100644 index 0000000..5ac9bd2 --- /dev/null +++ b/nhf/build.py @@ -0,0 +1,81 @@ +""" +The NHF build system + +Usage: For any parametric assembly, inherit the `Model` class, and mark the +output objects with the `@target` decorator +""" +from pathlib import Path +from typing import Union +import cadquery as Cq +from colorama import Fore, Style + +class Target: + + def __init__(self, + method, + name: str): + self._method = method + self.name = name + def __call__(self, obj, *args, **kwargs): + return self._method(obj, *args, **kwargs) + + @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): + method = getattr(subject, name) + if isinstance(method, Target): + yield name, method + return {name: method for name, method in g()} + + +def target(name, **kwargs): + """ + Decorator for annotating a build output + """ + def f(method): + return Target(method, name=name, **kwargs) + return f + +def _to_shape(x: Union[Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly]) -> Cq.Shape: + if isinstance(x, Cq.Workplane): + x = x.val() + if isinstance(x, Cq.Assembly): + x = x.toCompound() + return x + + +class Model: + """ + Base class for a parametric assembly + """ + + def target_names(self) -> list[str]: + """ + List of decorated target functions + """ + return list(Target.methods(self).keys()) + + def build_all(self, output_dir: Union[Path, str] ="build", verbose=1): + """ + Build all targets in this model + """ + output_dir = Path(output_dir) + output_dir.mkdir(exist_ok=True, parents=True) + for k, f in Target.methods(self).items(): + output_file = output_dir / f"{k}.stl" + if output_file.is_file(): + if verbose >= 1: + print(f"{Fore.GREEN}Skipping{Style.RESET_ALL} {output_file}") + continue + + model = f(self) + if verbose >= 1: + print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}", end="") + _to_shape(model).exportStl(str(output_file)) + if verbose >= 1: + print("\r", end="") + print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}") diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 55f3614..3d00652 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -1,4 +1,9 @@ """ +To build, execute +``` +python3 nhf/touhou/houjuu_nue/__init__.py +``` + This cosplay consists of 3 components: ## Trident @@ -32,11 +37,12 @@ import nhf.handle from nhf import Material from nhf.joints import HirthJoint from nhf.handle import Handle +from nhf.build import Model, target import nhf.touhou.houjuu_nue.wing as MW import nhf.touhou.houjuu_nue.trident as MT @dataclass -class Parameters: +class Parameters(Model): """ Defines dimensions for the Houjuu Nue cosplay """ @@ -179,6 +185,7 @@ class Parameters: (-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 @@ -322,3 +329,8 @@ class Parameters: def trident_assembly(self): return MT.trident_assembly(self.trident_handle) + + +if __name__ == '__main__': + p = Parameters() + p.build_all() diff --git a/poetry.lock b/poetry.lock index 69302dd..756343c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -857,4 +857,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "caf46b526858dbf2960b0204782140ad072d96f7b3f161ac7e9db0d9b709b25a" +content-hash = "ec47ccffd60fbda610a5c3725fc064a08b1b794f23084672bd62beb20b1b19f7" diff --git a/pyproject.toml b/pyproject.toml index 3431c0e..8ca0117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ python = "^3.10" cadquery = "^2.4.0" build123d = "^0.5.0" numpy = "^1.26.4" +colorama = "^0.4.6" [build-system] requires = ["poetry-core"] -- 2.44.1 From 46161ba82ecf19b90c54bba1e61426e5a4b30a14 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 4 Jul 2024 00:24:14 -0700 Subject: [PATCH 029/187] fix: Decorated target not directly callable --- nhf/build.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index 5ac9bd2..f11b9c8 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -6,8 +6,9 @@ output objects with the `@target` decorator """ from pathlib import Path from typing import Union -import cadquery as Cq +from functools import wraps from colorama import Fore, Style +import cadquery as Cq class Target: @@ -16,6 +17,8 @@ class Target: name: str): self._method = method self.name = name + def __str__(self): + return f"" def __call__(self, obj, *args, **kwargs): return self._method(obj, *args, **kwargs) @@ -27,17 +30,21 @@ class Target: def g(): for name in dir(subject): method = getattr(subject, name) - if isinstance(method, Target): - yield name, method - return {name: method for name, method in g()} + if hasattr(method, 'target'): + yield method.target + return {method.name: method for method in g()} -def target(name, **kwargs): +def target(name, **deco_kwargs): """ Decorator for annotating a build output """ def f(method): - return Target(method, name=name, **kwargs) + @wraps(method) + def wrapper(self, *args, **kwargs): + return method(self, *args, **kwargs) + wrapper.target = Target(method, name, **deco_kwargs) + return wrapper return f def _to_shape(x: Union[Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly]) -> Cq.Shape: @@ -74,8 +81,7 @@ class Model: model = f(self) if verbose >= 1: - print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}", end="") + print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}") _to_shape(model).exportStl(str(output_file)) if verbose >= 1: - print("\r", end="") print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}") -- 2.44.1 From 5bceb6180e31b86b2397d78a642e69f557224c08 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 4 Jul 2024 00:42:14 -0700 Subject: [PATCH 030/187] refactor: Move parts into nhf.parts --- nhf/build.py | 16 ++++++-- nhf/parts/__init__.py | 0 nhf/{ => parts}/handle.py | 10 ++--- nhf/{ => parts}/joints.py | 6 +-- nhf/{ => parts}/metric_threads.py | 0 nhf/{ => parts}/springs.py | 0 nhf/parts/test.py | 59 ++++++++++++++++++++++++++++++ nhf/primitive.py | 12 ------ nhf/test.py | 61 +++++++------------------------ nhf/touhou/houjuu_nue/__init__.py | 6 +-- nhf/touhou/houjuu_nue/trident.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 2 +- 12 files changed, 97 insertions(+), 77 deletions(-) create mode 100644 nhf/parts/__init__.py rename nhf/{ => parts}/handle.py (96%) rename nhf/{ => parts}/joints.py (99%) rename nhf/{ => parts}/metric_threads.py (100%) rename nhf/{ => parts}/springs.py (100%) create mode 100644 nhf/parts/test.py delete mode 100644 nhf/primitive.py diff --git a/nhf/build.py b/nhf/build.py index f11b9c8..5da3d01 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -29,9 +29,11 @@ class Target: """ def g(): for name in dir(subject): + if name == 'target_names': + continue method = getattr(subject, name) - if hasattr(method, 'target'): - yield method.target + if hasattr(method, '_target'): + yield method._target return {method.name: method for method in g()} @@ -43,7 +45,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, name, **deco_kwargs) return wrapper return f @@ -60,12 +62,20 @@ class Model: Base class for a parametric assembly """ + @property def target_names(self) -> list[str]: """ List of decorated target functions """ return list(Target.methods(self).keys()) + def check_all(self) -> int: + total = 0 + for k, f in Target.methods(self).items(): + f(self) + total += 1 + return total + def build_all(self, output_dir: Union[Path, str] ="build", verbose=1): """ Build all targets in this model diff --git a/nhf/parts/__init__.py b/nhf/parts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nhf/handle.py b/nhf/parts/handle.py similarity index 96% rename from nhf/handle.py rename to nhf/parts/handle.py index 36c41a7..c6845ef 100644 --- a/nhf/handle.py +++ b/nhf/parts/handle.py @@ -3,7 +3,7 @@ This schematics file contains all designs related to tool handles """ from dataclasses import dataclass import cadquery as Cq -import nhf.metric_threads as NMt +import nhf.parts.metric_threads as metric_threads @dataclass class Handle: @@ -50,7 +50,7 @@ class Handle: @property def diam_insertion_internal(self): - r = NMt.metric_thread_major_radius( + r = metric_threads.metric_thread_major_radius( self.diam_threading, self.thread_pitch, internal=True) @@ -58,7 +58,7 @@ class Handle: @property def diam_connector_external(self): - r = NMt.metric_thread_minor_radius( + r = metric_threads.metric_thread_minor_radius( self.diam_threading, self.thread_pitch) return r * 2 @@ -77,13 +77,13 @@ class Handle: def _external_thread(self, length=None): if length is None: length = self.insertion_length - return NMt.external_metric_thread( + return metric_threads.external_metric_thread( self.diam_threading, self.thread_pitch, length, top_lead_in=True) def _internal_thread(self): - return NMt.internal_metric_thread( + return metric_threads.internal_metric_thread( self.diam_threading, self.thread_pitch, self.insertion_length) diff --git a/nhf/joints.py b/nhf/parts/joints.py similarity index 99% rename from nhf/joints.py rename to nhf/parts/joints.py index f8e5f8f..286776d 100644 --- a/nhf/joints.py +++ b/nhf/parts/joints.py @@ -1,7 +1,7 @@ from dataclasses import dataclass import math import cadquery as Cq -import nhf.springs as NS +import nhf.parts.springs as springs from nhf import Role @dataclass @@ -209,7 +209,7 @@ def comma_joint(radius=30, def comma_assembly(): joint1 = comma_joint() joint2 = comma_joint() - spring = NS.torsion_spring() + spring = springs.torsion_spring() result = ( Cq.Assembly() .add(joint1, name="joint1", color=Cq.Color(0.8,0.8,0.5,0.3)) @@ -298,7 +298,7 @@ class TorsionJoint: def spring(self): - return NS.torsion_spring( + return springs.torsion_spring( radius=self.radius_spring, height=self.spring_height, thickness=self.spring_thickness, diff --git a/nhf/metric_threads.py b/nhf/parts/metric_threads.py similarity index 100% rename from nhf/metric_threads.py rename to nhf/parts/metric_threads.py diff --git a/nhf/springs.py b/nhf/parts/springs.py similarity index 100% rename from nhf/springs.py rename to nhf/parts/springs.py diff --git a/nhf/parts/test.py b/nhf/parts/test.py new file mode 100644 index 0000000..d1e1768 --- /dev/null +++ b/nhf/parts/test.py @@ -0,0 +1,59 @@ +import unittest +import cadquery as Cq +from nhf.checks import binary_intersection +import nhf.parts.joints as joints +import nhf.parts.handle as handle +import nhf.parts.metric_threads as metric_threads + +class TestJoints(unittest.TestCase): + + def test_joint_hirth(self): + j = joints.HirthJoint() + obj = j.generate() + self.assertIsInstance( + obj.val().solids(), Cq.Solid, + msg="Hirth joint must be in one piece") + + def test_joints_hirth_assembly(self): + for n_tooth in [16, 20, 24]: + with self.subTest(n_tooth=n_tooth): + j = joints.HirthJoint() + assembly = j.assembly() + isect = binary_intersection(assembly) + self.assertLess(isect.Volume(), 1e-6, + "Hirth joint assembly must not have intersection") + def test_joints_comma_assembly(self): + joints.comma_assembly() + def test_torsion_joint(self): + j = joints.TorsionJoint() + assembly = j.rider_track_assembly() + bbox = assembly.toCompound().BoundingBox() + self.assertAlmostEqual(bbox.zlen, j.total_height) + + +class TestHandle(unittest.TestCase): + + def test_handle_assembly(self): + h = handle.Handle() + assembly = h.connector_insertion_assembly() + bbox = assembly.toCompound().BoundingBox() + self.assertAlmostEqual(bbox.xlen, h.diam) + self.assertAlmostEqual(bbox.ylen, h.diam) + assembly = h.connector_one_side_insertion_assembly() + bbox = assembly.toCompound().BoundingBox() + self.assertAlmostEqual(bbox.xlen, h.diam) + self.assertAlmostEqual(bbox.ylen, h.diam) + + +class TestMetricThreads(unittest.TestCase): + + def test_major_radius(self): + major = 3.0 + t = metric_threads.external_metric_thread(major, 0.5, 4.0, z_start= -0.85, top_lead_in=True) + bbox = t.val().BoundingBox() + self.assertAlmostEqual(bbox.xlen, major, places=3) + self.assertAlmostEqual(bbox.ylen, major, places=3) + + +if __name__ == '__main__': + unittest.main() diff --git a/nhf/primitive.py b/nhf/primitive.py deleted file mode 100644 index 35e1565..0000000 --- a/nhf/primitive.py +++ /dev/null @@ -1,12 +0,0 @@ -import cadquery as Cq - -def mystery(): - return ( - Cq.Workplane("XY") - .box(10, 5, 5) - .faces(">Z") - .workplane() - .hole(1) - .edges("|Z") - .fillet(2) - ) diff --git a/nhf/test.py b/nhf/test.py index 4f97602..c6e94a6 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -1,58 +1,23 @@ import unittest import cadquery as Cq -import nhf.joints -import nhf.handle -import nhf.metric_threads as NMt -from nhf.checks import binary_intersection +from nhf.build import Model, target -class TestJoints(unittest.TestCase): +class BuildScaffold(Model): - def test_joint_hirth(self): - j = nhf.joints.HirthJoint() - obj = j.generate() - self.assertIsInstance( - obj.val().solids(), Cq.Solid, - msg="Hirth joint must be in one piece") + @target(name="obj1") + def o1(self): + return Cq.Solid.makeBox(10, 10, 10) - def test_joints_hirth_assembly(self): - for n_tooth in [16, 20, 24]: - with self.subTest(n_tooth=n_tooth): - j = nhf.joints.HirthJoint() - assembly = j.assembly() - isect = binary_intersection(assembly) - self.assertLess(isect.Volume(), 1e-6, - "Hirth joint assembly must not have intersection") - def test_joints_comma_assembly(self): - nhf.joints.comma_assembly() - def test_torsion_joint(self): - j = nhf.joints.TorsionJoint() - assembly = j.rider_track_assembly() - bbox = assembly.toCompound().BoundingBox() - self.assertAlmostEqual(bbox.zlen, j.total_height) + def o2(self): + return Cq.Solid.makeCylinder(10, 20) +class TestBuild(unittest.TestCase): -class TestHandle(unittest.TestCase): - - def test_handle_assembly(self): - h = nhf.handle.Handle() - assembly = h.connector_insertion_assembly() - bbox = assembly.toCompound().BoundingBox() - self.assertAlmostEqual(bbox.xlen, h.diam) - self.assertAlmostEqual(bbox.ylen, h.diam) - assembly = h.connector_one_side_insertion_assembly() - bbox = assembly.toCompound().BoundingBox() - self.assertAlmostEqual(bbox.xlen, h.diam) - self.assertAlmostEqual(bbox.ylen, h.diam) - - -class TestMetricThreads(unittest.TestCase): - - def test_major_radius(self): - major = 3.0 - t = NMt.external_metric_thread(major, 0.5, 4.0, z_start= -0.85, top_lead_in=True) - bbox = t.val().BoundingBox() - self.assertAlmostEqual(bbox.xlen, major, places=3) - self.assertAlmostEqual(bbox.ylen, major, places=3) + def test_build_scaffold(self): + s = BuildScaffold() + names = ["obj1"] + self.assertEqual(s.target_names, names) + self.assertEqual(s.check_all(), len(names)) if __name__ == '__main__': diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 3d00652..37a91a4 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -32,12 +32,10 @@ shoulder, elbow, wrist in analogy with human anatomy. from dataclasses import dataclass, field import unittest import cadquery as Cq -import nhf.joints -import nhf.handle from nhf import Material -from nhf.joints import HirthJoint -from nhf.handle import Handle from nhf.build import Model, target +from nhf.parts.joints import HirthJoint +from nhf.parts.handle import Handle import nhf.touhou.houjuu_nue.wing as MW import nhf.touhou.houjuu_nue.trident as MT diff --git a/nhf/touhou/houjuu_nue/trident.py b/nhf/touhou/houjuu_nue/trident.py index 71901ab..312ff85 100644 --- a/nhf/touhou/houjuu_nue/trident.py +++ b/nhf/touhou/houjuu_nue/trident.py @@ -1,7 +1,7 @@ import math import cadquery as Cq from nhf import Material -from nhf.handle import Handle +from nhf.parts.handle import Handle def trident_assembly( handle: Handle, diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 04de65a..224183d 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -4,7 +4,7 @@ This file describes the shapes of the wing shells. The joints are defined in """ import math import cadquery as Cq -from nhf.joints import HirthJoint +from nhf.parts.joints import HirthJoint def wing_root_profiles( base_sweep=150, -- 2.44.1 From 66fc02ef44dd7c36fb1398d408d516f56198f7af Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 4 Jul 2024 01:11:16 -0700 Subject: [PATCH 031/187] feat: Export DXF in build system --- nhf/build.py | 64 ++++++++++++++++++++++++------- nhf/touhou/houjuu_nue/__init__.py | 11 ++---- 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index 5da3d01..c3b0e4a 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -2,25 +2,69 @@ The NHF build system Usage: For any parametric assembly, inherit the `Model` class, and mark the -output objects with the `@target` decorator +output objects with the `@target` decorator. Each marked function should only +take `self` as an argument. +```python +class BuildScaffold(Model): + + @target(name="obj1") + def o1(self): + return Cq.Solid.makeBox(10, 10, 10) + + def o2(self): + return Cq.Solid.makeCylinder(10, 20) +``` """ +from enum import Enum from pathlib import Path from typing import Union from functools import wraps from colorama import Fore, Style import cadquery as Cq +class TargetKind(Enum): + + STL = "stl", + DXF = "dxf", + + def __init__(self, ext: str): + self.ext = ext + class Target: def __init__(self, method, - name: str): + name: str, + kind: TargetKind = TargetKind.STL, + **kwargs): self._method = method self.name = name + self.kind = kind + self.kwargs = kwargs def __str__(self): return f"" def __call__(self, obj, *args, **kwargs): + """ + Raw call function which passes arguments directly to `_method` + """ return self._method(obj, *args, **kwargs) + def file_name(self, file_name): + return f"{file_name}.{self.kind.ext}" + def write_to(self, obj, path: str): + x = self._method(obj) + if self.kind == TargetKind.STL: + assert isinstance(x, Union[ + Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly]) + if isinstance(x, Cq.Workplane): + x = x.val() + if isinstance(x, Cq.Assembly): + x = x.toCompound() + x.exportStl(path, **self.kwargs) + elif self.kind == TargetKind.DXF: + assert isinstance(x, Cq.Workplane) + Cq.exporters.exportDXF(x, path, **self.kwargs) + else: + assert False, f"Invalid kind: {self.kind}" @classmethod def methods(cls, subject): @@ -49,13 +93,6 @@ def target(name, **deco_kwargs): return wrapper return f -def _to_shape(x: Union[Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly]) -> Cq.Shape: - if isinstance(x, Cq.Workplane): - x = x.val() - if isinstance(x, Cq.Assembly): - x = x.toCompound() - return x - class Model: """ @@ -76,22 +113,21 @@ class Model: total += 1 return total - def build_all(self, output_dir: Union[Path, str] ="build", verbose=1): + def build_all(self, output_dir: Union[Path, str] = "build", verbose=1): """ Build all targets in this model """ output_dir = Path(output_dir) output_dir.mkdir(exist_ok=True, parents=True) - for k, f in Target.methods(self).items(): - output_file = output_dir / f"{k}.stl" + for k, target in Target.methods(self).items(): + output_file = output_dir / target.file_name(k) if output_file.is_file(): if verbose >= 1: print(f"{Fore.GREEN}Skipping{Style.RESET_ALL} {output_file}") continue - model = f(self) if verbose >= 1: print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}") - _to_shape(model).exportStl(str(output_file)) + target.write_to(self, str(output_file)) if verbose >= 1: print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}") diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 37a91a4..ba99b00 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -33,7 +33,7 @@ from dataclasses import dataclass, field import unittest import cadquery as Cq from nhf import Material -from nhf.build import Model, target +from nhf.build import Model, TargetKind, target from nhf.parts.joints import HirthJoint from nhf.parts.handle import Handle import nhf.touhou.houjuu_nue.wing as MW @@ -113,11 +113,6 @@ class Parameters(Model): "Wing root must be large enough to accomodate joint" - def print_geometries(self): - return [ - self.hs_joint_parent() - ] - def harness_profile(self) -> Cq.Sketch: """ Creates the harness shape @@ -148,6 +143,7 @@ class Parameters(Model): ) return sketch + @target(name="harness", kind=TargetKind.DXF) def harness(self) -> Cq.Shape: """ Creates the harness shape @@ -249,7 +245,8 @@ class Parameters(Model): result.faces(" Cq.Shape: + @target(name="wing_root") + def wing_root(self) -> Cq.Assembly: """ Generate the wing root which contains a Hirth joint at its base and a rectangular opening on its side, with the necessary interfaces. -- 2.44.1 From 80fb2e997d0e8122cb96999c323ba6433bf9b7e5 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 4 Jul 2024 01:13:22 -0700 Subject: [PATCH 032/187] feat: Build trident handle --- nhf/build.py | 2 +- nhf/touhou/houjuu_nue/__init__.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nhf/build.py b/nhf/build.py index c3b0e4a..7b2531d 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -118,13 +118,13 @@ class Model: Build all targets in this model """ output_dir = Path(output_dir) - output_dir.mkdir(exist_ok=True, parents=True) for k, target in Target.methods(self).items(): output_file = output_dir / target.file_name(k) if output_file.is_file(): if verbose >= 1: print(f"{Fore.GREEN}Skipping{Style.RESET_ALL} {output_file}") continue + output_file.parent.mkdir(exist_ok=True, parents=True) if verbose >= 1: print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}") diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index ba99b00..8f5679e 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -112,6 +112,13 @@ class Parameters(Model): assert self.wing_root_radius > self.hs_hirth_joint.radius,\ "Wing root must be large enough to accomodate 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() + def harness_profile(self) -> Cq.Sketch: """ -- 2.44.1 From d69cf014a10998c4691bc20fe88ff7d9ddd756a6 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 4 Jul 2024 01:16:01 -0700 Subject: [PATCH 033/187] chore: Clean up import --- nhf/parts/test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nhf/parts/test.py b/nhf/parts/test.py index d1e1768..1e70ba2 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -1,9 +1,7 @@ import unittest import cadquery as Cq from nhf.checks import binary_intersection -import nhf.parts.joints as joints -import nhf.parts.handle as handle -import nhf.parts.metric_threads as metric_threads +from nhf.parts import joints, handle, metric_threads class TestJoints(unittest.TestCase): -- 2.44.1 From 89c6a39c2f92ee4716d42266aecc010f3273fcf3 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 4 Jul 2024 10:02:58 -0700 Subject: [PATCH 034/187] feat: Name in target --- nhf/build.py | 37 ++++++++++++++++++++++--------- nhf/touhou/houjuu_nue/__init__.py | 1 + 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index 7b2531d..4435508 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -42,14 +42,20 @@ class Target: self.kind = kind self.kwargs = kwargs def __str__(self): - return f"" + return f"" def __call__(self, obj, *args, **kwargs): """ Raw call function which passes arguments directly to `_method` """ return self._method(obj, *args, **kwargs) - def file_name(self, file_name): - return f"{file_name}.{self.kind.ext}" + + @property + def file_name(self): + """ + Output file name + """ + return f"{self.name}.{self.kind.ext}" + def write_to(self, obj, path: str): x = self._method(obj) if self.kind == TargetKind.STL: @@ -98,6 +104,8 @@ class Model: """ Base class for a parametric assembly """ + def __init__(self, name: str): + self.name = name @property def target_names(self) -> list[str]: @@ -107,19 +115,22 @@ class Model: return list(Target.methods(self).keys()) def check_all(self) -> int: + """ + Builds all targets but do not output them + """ total = 0 - for k, f in Target.methods(self).items(): - f(self) + for t in Target.methods(self).values(): + t(self) total += 1 return total def build_all(self, output_dir: Union[Path, str] = "build", verbose=1): """ - Build all targets in this model + Build all targets in this model and write the results to file """ output_dir = Path(output_dir) - for k, target in Target.methods(self).items(): - output_file = output_dir / target.file_name(k) + for t in Target.methods(self).values(): + output_file = output_dir / self.name / t.file_name if output_file.is_file(): if verbose >= 1: print(f"{Fore.GREEN}Skipping{Style.RESET_ALL} {output_file}") @@ -128,6 +139,10 @@ class Model: if verbose >= 1: print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}") - target.write_to(self, str(output_file)) - if verbose >= 1: - print(f"{Fore.GREEN}Built{Style.RESET_ALL} {output_file}") + + try: + t.write_to(self, str(output_file)) + if verbose >= 1: + 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}") diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 8f5679e..b90eb8c 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -109,6 +109,7 @@ class Parameters(Model): )) def __post_init__(self): + super().__init__(name="houjuu-nue") assert self.wing_root_radius > self.hs_hirth_joint.radius,\ "Wing root must be large enough to accomodate joint" -- 2.44.1 From 1794729890e587e5aad693e1e774d6cbcbc3d044 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 4 Jul 2024 12:03:38 -0700 Subject: [PATCH 035/187] fix: Use subassemblies for wings and harnesses --- nhf/parts/joints.py | 8 +++- nhf/touhou/houjuu_nue/__init__.py | 64 +++++++++++++++++-------------- nhf/touhou/houjuu_nue/test.py | 9 +++-- nhf/touhou/houjuu_nue/wing.py | 14 ++++--- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 286776d..036a4f4 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -28,6 +28,10 @@ class HirthJoint: def tooth_angle(self): return 360 / self.n_tooth + @property + def total_height(self): + return self.base_height + self.tooth_height + def generate(self, is_mated=False, tol=0.01): """ @@ -97,7 +101,7 @@ class HirthJoint: ) return result - def assembly(self): + def assembly(self, offset: int = 1): """ Generate an example assembly """ @@ -118,7 +122,7 @@ class HirthJoint: self.generate(is_mated=True) .union(tab) ) - angle = 1 * self.tooth_angle + angle = offset * self.tooth_angle result = ( Cq.Assembly() .add(obj1, name="obj1", color=Role.PARENT.color) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index b90eb8c..fe26116 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -32,7 +32,7 @@ shoulder, elbow, wrist in analogy with human anatomy. from dataclasses import dataclass, field import unittest import cadquery as Cq -from nhf import Material +from nhf import Material, Role from nhf.build import Model, TargetKind, target from nhf.parts.joints import HirthJoint from nhf.parts.handle import Handle @@ -194,18 +194,6 @@ class Parameters(Model): coupling, a cylindrical base, and a mounting base. """ hirth = self.hs_hirth_joint.generate() - #hirth = ( - # hirth - # .faces("Z") @@ -224,11 +213,13 @@ class Parameters(Model): cboreDiameter=self.hs_joint_corner_cbore_diam, cboreDepth=self.hs_joint_corner_cbore_depth) ) + # Creates a plane parallel to the holes but shifted to the base plane = result.faces(">Z").workplane(offset=-self.hs_joint_base_thickness) + for i, (px, py) in enumerate(conn): ( plane - .moveTo(px, py) + .pushPoints([(px, py)]) .circle(1, forConstruction='True') .edges() .tag(f"h{i}") @@ -237,7 +228,7 @@ class Parameters(Model): result .faces(">Z") .workplane() - .union(hirth.translate((0, 0, self.hs_joint_base_thickness)), tol=0.1) + .union(hirth, tol=0.1) .clean() ) result = ( @@ -304,34 +295,49 @@ class Parameters(Model): # Assemblies # ###################### + def trident_assembly(self): + return MT.trident_assembly(self.trident_handle) + def harness_assembly(self): harness = self.harness() result = ( Cq.Assembly() - .add(harness, name="harness", color=Material.WOOD_BIRCH.color) - .constrain("harness", "Fixed") + .add(harness, name="base", color=Material.WOOD_BIRCH.color) + .constrain("base", "Fixed") ) for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: j = self.hs_joint_parent() ( result - .add(j, name=f"hs_{name}", color=Material.PLASTIC_PLA.color) - #.constrain(f"harness?{name}", f"hs_{name}p?mate", "Point") - .constrain("harness?mount", f"hs_{name}?base", "Axis") + .add(j, name=name, color=Role.PARENT.color) + .constrain("base?mount", f"{name}?base", "Axis") ) for i in range(4): - result.constrain(f"harness?{name}_{i}", f"hs_{name}?h{i}", "Point") - angle = 6 * self.hs_hirth_joint.tooth_angle - ( - result.add(self.wing_root(), name="w0_r1", color=Material.PLASTIC_PLA.color) - .constrain("w0_r1?mate", "hs_r1?mate", "Plane") - .constrain("w0_r1?directrix", "hs_r1?directrix", "Axis", param=angle) - ) + result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") result.solve() return result - def trident_assembly(self): - return MT.trident_assembly(self.trident_handle) + def wings_assembly(self): + """ + 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_root(), name="w0_r1") + .add(self.wing_root(), name="w0_l1") + .constrain("harness/base", "Fixed") + .constrain("w0_r1/joint?mate", "harness/r1?mate", "Plane") + .constrain("w0_r1/joint?directrix", "harness/r1?directrix", + "Axis", param=7 * a_tooth) + .constrain("w0_l1/joint?mate", "harness/l1?mate", "Plane") + .constrain("w0_l1/joint?directrix", "harness/l1?directrix", + "Axis", param=-1 * a_tooth) + .solve() + ) + return result if __name__ == '__main__': diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 57d1aaa..0b63ecd 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -18,13 +18,16 @@ class Test(unittest.TestCase): self.assertLess(bbox.xlen, 255, msg=msg) self.assertLess(bbox.ylen, 255, msg=msg) self.assertLess(bbox.zlen, 255, msg=msg) - def test_wings(self): + def test_wing_root(self): p = M.Parameters() p.wing_root() - def test_harness(self): + def test_wings_assembly(self): + p = M.Parameters() + p.wings_assembly() + def test_harness_assembly(self): p = M.Parameters() p.harness_assembly() - def test_trident(self): + def test_trident_assembly(self): p = M.Parameters() assembly = p.trident_assembly() bbox = assembly.toCompound().BoundingBox() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 224183d..5f9daf9 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -4,6 +4,7 @@ This file describes the shapes of the wing shells. The joints are defined in """ import math import cadquery as Cq +from nhf import Material, Role from nhf.parts.joints import HirthJoint def wing_root_profiles( @@ -125,7 +126,7 @@ def wing_root_profiles( def wing_root(joint: HirthJoint, - bolt_diam: int = 12): + bolt_diam: int = 12) -> Cq.Assembly: """ Generate the contiguous components of the root wing segment """ @@ -192,11 +193,12 @@ def wing_root(joint: HirthJoint, .faces(" Date: Thu, 4 Jul 2024 17:50:11 -0700 Subject: [PATCH 036/187] feat: Connectors on wing root --- nhf/touhou/houjuu_nue/wing.py | 134 ++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 5f9daf9..ea3a5c3 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -10,8 +10,9 @@ from nhf.parts.joints import HirthJoint def wing_root_profiles( base_sweep=150, wall_thickness=8, - base_radius=60, + base_radius=40, middle_offset=30, + middle_height=80, conn_width=40, conn_height=100) -> tuple[Cq.Wire, Cq.Wire]: assert base_sweep < 180 @@ -72,7 +73,7 @@ def wing_root_profiles( # If the exterior sweep is theta', it has to satisfy # # sin(theta) * r2 + wall_thickness = sin(theta') * r1 - x, y = conn_width / 2, conn_height / 2 + x, y = conn_width / 2, middle_height / 2 t = wall_thickness dx = middle_offset middle = ( @@ -126,65 +127,86 @@ def wing_root_profiles( def wing_root(joint: HirthJoint, - bolt_diam: int = 12) -> Cq.Assembly: + bolt_diam: int = 12, + union_tol=1e-4, + attach_diam=8, + conn_width=40, + conn_height=100, + wall_thickness=8) -> Cq.Assembly: """ Generate the contiguous components of the root wing segment """ - root_profile, middle_profile, tip_profile = wing_root_profiles() + tip_centre = Cq.Vector((-150, 0, -80)) + attach_points = [ + (15, 0), + (40, 0), + ] + root_profile, middle_profile, tip_profile = wing_root_profiles( + conn_width=conn_width, + conn_height=conn_height, + wall_thickness=8, + ) + middle_profile = middle_profile.located(Cq.Location( + (-40, 0, -40), (0, 30, 0) + )) + antetip_profile = tip_profile.located(Cq.Location( + (-95, 0, -75), (0, 60, 0) + )) + tip_profile = tip_profile.located(Cq.Location( + tip_centre, (0, 90, 0) + )) + profiles = [ + root_profile, + middle_profile, + antetip_profile, + tip_profile, + ] + result = None + for p1, p2 in zip(profiles[:-1], profiles[1:]): + seg = ( + Cq.Workplane('XY') + .add(p1) + .toPending() + .workplane() # This call is necessary + .add(p2) + .toPending() + .loft() + ) + if result: + result = result.union(seg, tol=union_tol) + else: + result = seg + result = ( + result + # Create connector holes + .copyWorkplane( + Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0))) + ) + .pushPoints(attach_points) + .hole(attach_diam) + ) + # Generate attach point tags - rotate_centre = Cq.Vector(-200, 0, -25) - rotate_axis = Cq.Vector(0, 1, 0) - terminal_offset = Cq.Vector(-80, 0, 80) - terminal_rotate = Cq.Vector(0, -45, 0) + for sign in [False, True]: + y = conn_height / 2 - wall_thickness + side = "bottom" if sign else "top" + y = y if sign else -y + plane = ( + result + # Create connector holes + .copyWorkplane( + Cq.Workplane(side, origin=tip_centre + + Cq.Vector((0, y, 0))) + ) + ) + for i, (px, py) in enumerate(attach_points): + ( + plane + .moveTo(px, py) + .eachpoint(Cq.Vertex.makeVertex(0, 0, 0)) + .tag(f"conn_{side}{i}") + ) - #middle_profile = middle_profile.moved(Cq.Location((0, 0, -100))) - #tip_profile = tip_profile.moved(Cq.Location((0, 0, -200))) - middle_profile = middle_profile.rotate( - startVector=rotate_centre, - endVector=rotate_centre + rotate_axis, - angleDegrees = 30, - ) - antetip_profile = tip_profile.rotate( - startVector=rotate_centre, - endVector=rotate_centre + rotate_axis, - angleDegrees = 60, - ) - tip_profile = tip_profile.rotate( - startVector=rotate_centre, - endVector=rotate_centre + rotate_axis, - angleDegrees = 90, - ) - seg1 = ( - Cq.Workplane('XY') - .add(root_profile) - .toPending() - .transformed( - offset=terminal_offset, - rotate=terminal_rotate) - #.add(middle_profile.moved(Cq.Location((-15, 0, 15)))) - .add(middle_profile) - .toPending() - .loft() - ) - seg2 = ( - Cq.Workplane('XY') - .add(middle_profile) - .toPending() - .workplane() - .add(antetip_profile) - .toPending() - .loft() - ) - seg3 = ( - Cq.Workplane('XY') - .add(antetip_profile) - .toPending() - .workplane() - .add(tip_profile) - .toPending() - .loft() - ) - result = seg1.union(seg2).union(seg3) result.faces("X").tag("conn") -- 2.44.1 From 9e7369c6f8c9266d727e6de9bf0225a6999ffadc Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 6 Jul 2024 16:41:13 -0700 Subject: [PATCH 037/187] feat: Tag point and tag plane for mating --- nhf/checks.py | 26 +++++- nhf/materials.py | 1 + nhf/test.py | 127 ++++++++++++++++++++++++++++++ nhf/touhou/houjuu_nue/__init__.py | 59 ++++++++++++-- nhf/touhou/houjuu_nue/wing.py | 16 ++-- nhf/utils.py | 38 +++++++++ 6 files changed, 251 insertions(+), 16 deletions(-) create mode 100644 nhf/utils.py diff --git a/nhf/checks.py b/nhf/checks.py index 468cc8e..19914ab 100644 --- a/nhf/checks.py +++ b/nhf/checks.py @@ -1,6 +1,30 @@ import cadquery as Cq def binary_intersection(a: Cq.Assembly) -> Cq.Shape: - objs = [s.toCompound() for _, s in a.traverse() if isinstance(s, Cq.Assembly)] + objs = [s.toCompound() for _, s in a.traverse() + if isinstance(s, Cq.Assembly)] obj1, obj2 = objs[:2] return obj1.intersect(obj2) + + +def pairwise_intersection(assembly: Cq.Assembly, tol: float=1e-6) -> list[(str, str, float)]: + """ + Given an assembly, test the pairwise intersection volume of its components. + Return the pairs whose intersection volume exceeds `tol`. + """ + m = {name: (i, shape.moved(loc)) + for i, (shape, name, loc, _) + in enumerate(assembly)} + result = [] + for name, (i1, sh1) in m.items(): + for name2, (i2, sh2) in m.items(): + if name == name2: + assert i1 == i2 + continue + if i2 <= i1: + # Remove the upper diagonal + continue + vol = sh1.intersect(sh2).Volume() + if vol > tol: + result.append((name, name2, vol)) + return result diff --git a/nhf/materials.py b/nhf/materials.py index c0adbde..0cb266d 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -31,6 +31,7 @@ class Material(Enum): WOOD_BIRCH = 0.8, _color('bisque', 0.9) PLASTIC_PLA = 0.5, _color('azure3', 0.6) + RESIN_TRANSPARENT = 1.1, _color('cadetblue2', 0.6) ACRYLIC_BLACK = 0.5, _color('gray50', 0.6) def __init__(self, density: float, color: Cq.Color): diff --git a/nhf/test.py b/nhf/test.py index c6e94a6..fc9d15b 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -1,9 +1,27 @@ +""" +Unit tests for tooling +""" import unittest import cadquery as Cq from nhf.build import Model, target +import nhf.checks +import nhf.utils + +# Color presets for testing purposes +color_parent = Cq.Color(0.7, 0.7, 0.5, 0.5) +color_child = Cq.Color(0.5, 0.7, 0.7, 0.5) + +def makeSphere(r: float) -> Cq.Solid: + """ + Makes a full sphere. The default function makes a hemisphere + """ + return Cq.Solid.makeSphere(r, angleDegrees1=-90) class BuildScaffold(Model): + def __init__(self): + super().__init__(name="scaffold") + @target(name="obj1") def o1(self): return Cq.Solid.makeBox(10, 10, 10) @@ -19,6 +37,115 @@ class TestBuild(unittest.TestCase): self.assertEqual(s.target_names, names) self.assertEqual(s.check_all(), len(names)) +class TestChecks(unittest.TestCase): + + def intersect_test_case(self, offset): + assembly = ( + Cq.Assembly() + .add(Cq.Solid.makeBox(10, 10, 10), + name="c1", + loc=Cq.Location((0, 0, 0))) + .add(Cq.Solid.makeBox(10, 10, 10), + name="c2", + loc=Cq.Location((0, 0, offset))) + ) + coll = nhf.checks.pairwise_intersection(assembly) + if -10 < offset and offset < 10: + self.assertEqual(len(coll), 1) + else: + self.assertEqual(coll, []) + + def test_intersect(self): + for offset in [9, 10, 11, -10]: + with self.subTest(offset=offset): + self.intersect_test_case(offset) + +class TestUtils(unittest.TestCase): + + def test_tag_point(self): + """ + A board with 3 holes of unequal sizes. Each hole is marked + """ + p4x, p4y = 5, 5 + p3x, p3y = 0, 0 + p2x, p2y = -5, 0 + board = ( + Cq.Workplane('XY') + .box(15, 15, 5) + .faces("Y").tag("mount") plane = result.faces(">Y").workplane() for tag, x, y in self.harness_wing_base_pos: - conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] + conn = [(px + x, py + y) for px, py + in self.hs_joint_harness_conn()] for i, (px, py) in enumerate(conn): ( plane @@ -250,7 +262,30 @@ class Parameters(Model): Generate the wing root which contains a Hirth joint at its base and a rectangular opening on its side, with the necessary interfaces. """ - return MW.wing_root(joint=self.hs_hirth_joint) + return MW.wing_root( + joint=self.hs_hirth_joint, + shoulder_attach_dist=self.shoulder_attach_dist, + shoulder_attach_diam=self.shoulder_attach_diam, + wall_thickness=self.wing_root_wall_thickness, + ) + + @target(name="shoulder") + def shoulder_parent_joint(self) -> Cq.Assembly: + result = ( + self.shoulder_joint.rider() + .copyWorkplane(Cq.Workplane( + 'YZ', origin=Cq.Vector((100, 0, self.wing_root_wall_thickness)))) + .rect(30, 10, centered=(True, False)) + .extrude("next") + .copyWorkplane(Cq.Workplane( + 'YX', origin=Cq.Vector((60, 0, self.wing_root_wall_thickness)))) + .hole(self.shoulder_attach_diam) + .moveTo(0, self.shoulder_attach_dist) + .hole(self.shoulder_attach_diam) + ) + result.moveTo(0, 0).tagPlane('conn0') + result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1') + return result def wing_r1_profile(self) -> Cq.Sketch: """ @@ -295,10 +330,10 @@ class Parameters(Model): # Assemblies # ###################### - def trident_assembly(self): + def trident_assembly(self) -> Cq.Assembly: return MT.trident_assembly(self.trident_handle) - def harness_assembly(self): + def harness_assembly(self) -> Cq.Assembly: harness = self.harness() result = ( Cq.Assembly() @@ -317,7 +352,19 @@ class Parameters(Model): result.solve() return result - def wings_assembly(self): + def wing_r1_assembly(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.wing_root(), name="r1") + .add(self.shoulder_parent_joint(), name="shoulder_parent_top", + color=Material.RESIN_TRANSPARENT.color) + .constrain("r1/scaffold?conn_top0", "shoulder_parent_top?conn0", "Plane") + .constrain("r1/scaffold?conn_top1", "shoulder_parent_top?conn1", "Plane") + .solve() + ) + return result + + def wings_assembly(self) -> Cq.Assembly: """ Assembly of harness with all the wings """ diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index ea3a5c3..d9ab898 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -6,6 +6,7 @@ import math import cadquery as Cq from nhf import Material, Role from nhf.parts.joints import HirthJoint +import nhf.utils def wing_root_profiles( base_sweep=150, @@ -129,7 +130,8 @@ def wing_root_profiles( def wing_root(joint: HirthJoint, bolt_diam: int = 12, union_tol=1e-4, - attach_diam=8, + shoulder_attach_diam=8, + shoulder_attach_dist=25, conn_width=40, conn_height=100, wall_thickness=8) -> Cq.Assembly: @@ -139,7 +141,7 @@ def wing_root(joint: HirthJoint, tip_centre = Cq.Vector((-150, 0, -80)) attach_points = [ (15, 0), - (40, 0), + (15 + shoulder_attach_dist, 0), ] root_profile, middle_profile, tip_profile = wing_root_profiles( conn_width=conn_width, @@ -183,7 +185,7 @@ def wing_root(joint: HirthJoint, Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0))) ) .pushPoints(attach_points) - .hole(attach_diam) + .hole(shoulder_attach_diam) ) # Generate attach point tags @@ -200,12 +202,8 @@ def wing_root(joint: HirthJoint, ) ) for i, (px, py) in enumerate(attach_points): - ( - plane - .moveTo(px, py) - .eachpoint(Cq.Vertex.makeVertex(0, 0, 0)) - .tag(f"conn_{side}{i}") - ) + tag = f"conn_{side}{i}" + plane.moveTo(px, py).tagPlane(tag) result.faces("X").tag("conn") diff --git a/nhf/utils.py b/nhf/utils.py new file mode 100644 index 0000000..7cc020d --- /dev/null +++ b/nhf/utils.py @@ -0,0 +1,38 @@ +""" +Marking utilities for `Cq.Workplane` + +Adds the functions to `Cq.Workplane`: +1. `tagPoint` +2. `tagPlane` +""" +import cadquery as Cq + + +def tagPoint(self, tag: str): + """ + Adds a vertex that can be used in `Point` constraints. + """ + vertex = Cq.Vertex.makeVertex(0, 0, 0) + self.eachpoint(vertex.moved, useLocalCoordinates=True).tag(tag) + +Cq.Workplane.tagPoint = tagPoint + + +def tagPlane(self, tag: str, axis='Z'): + """ + Adds a phantom `Cq.Edge` in the given location which can be referenced in a + `Axis`, `Point`, or `Plane` constraint. + """ + x, y, z = 0, 0, 0 + if axis in ('z', 'Z'): + z = 1 + elif axis in ('y', 'Y'): + y = 1 + elif axis in ('x', 'X'): + x = 1 + else: + assert False, "Axis must be one of x,y,z" + edge = Cq.Edge.makeLine((-x, -y, -z), (x, y, z)) + self.eachpoint(edge.moved, useLocalCoordinates=True).tag(tag) + +Cq.Workplane.tagPlane = tagPlane -- 2.44.1 From 58028579a9e2ac37212d124e62d47ed41820eba7 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 6 Jul 2024 23:43:55 -0700 Subject: [PATCH 038/187] fix: Torsion joint directrix and collision problem --- nhf/parts/joints.py | 111 ++++++++++++++++++++++++++++++++------------ nhf/parts/test.py | 15 +++++- 2 files changed, 95 insertions(+), 31 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 036a4f4..b69fb67 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -3,6 +3,9 @@ import math import cadquery as Cq import nhf.parts.springs as springs from nhf import Role +import nhf.utils + +TOL = 1e-6 @dataclass class HirthJoint: @@ -232,17 +235,31 @@ class TorsionJoint: """ This jonit consists of a rider puck on a track puck. IT is best suited if the radius has to be small and vertical space is abundant. + + The rider part consists of: + 1. A cylinderical base + 2. A annular extrusion with the same radius as the base, but with slots + carved in + 3. An annular rider + + The track part consists of: + 1. A cylindrical base + 2. A slotted annular extrusion where the slot allows the spring to rest + 3. An outer and an inner annuli which forms a track the rider can move on """ # Radius limit for rotating components radius: float = 40 - disk_height: float = 10 + track_disk_height: float = 10 + rider_disk_height: float = 8 radius_spring: float = 15 radius_axle: float = 6 - # Offset of the spring hole w.r.t. surface - spring_hole_depth: float = 4 + # If true, cover the spring hole. May make it difficult to insert the spring + # considering the stiffness of torsion spring steel. + spring_hole_cover_track: bool = False + spring_hole_cover_rider: bool = False # Also used for the height of the hole for the spring spring_thickness: float = 2 @@ -253,13 +270,17 @@ class TorsionJoint: groove_radius_outer: float = 35 groove_radius_inner: float = 20 groove_depth: float = 5 - rider_gap: float = 2 - n_slots: float = 8 + rider_gap: float = 1 + rider_n_slots: float = 4 + + # Degrees of the first and last rider slots + rider_slot_begin: float = 0 + rider_slot_span: float = 90 right_handed: bool = False + def __post_init__(self): - assert self.disk_height > self.spring_hole_depth assert self.radius > self.groove_radius_outer assert self.groove_radius_outer > self.groove_radius_inner assert self.groove_radius_inner > self.radius_spring @@ -268,7 +289,7 @@ class TorsionJoint: @property def total_height(self): - return 2 * self.disk_height + self.spring_height + return self.track_disk_height + self.rider_disk_height + self.spring_height @property def _radius_spring_internal(self): @@ -295,6 +316,10 @@ class TorsionJoint: r2 = -r2 # This is (0, r2) and (l, r2) transformed by rotation matrix # [[c, s], [-s, c]] + return [ + (0, 0, height), + (c, s, height) + ] return [ (s * r2, -s * l + c * r2, height), (c * l + s * r2, -s * l + c * r2, height), @@ -320,14 +345,24 @@ class TorsionJoint: spring_hole_profile = ( Cq.Sketch() .circle(self.radius) - .polygon(self._slot_polygon(flip=False), mode='s') .circle(self.radius_spring, mode='s') ) + slot_height = self.spring_thickness + if not self.spring_hole_cover_track: + slot_height += self.groove_depth + slot = ( + Cq.Workplane('XY') + .sketch() + .polygon(self._slot_polygon(flip=False)) + .finalize() + .extrude(slot_height) + .val() + ) result = ( Cq.Workplane('XY') .cylinder( radius=self.radius, - height=self.disk_height, + height=self.track_disk_height, centered=(True, True, False)) .faces('>Z') .tag("spring") @@ -340,9 +375,10 @@ class TorsionJoint: .extrude(self.groove_depth) .faces('>Z') .hole(self.radius_axle * 2) + .cut(slot.moved(Cq.Location((0, 0, self.track_disk_height)))) ) # Insert directrix` - result.polyline(self._directrix(self.disk_height), + result.polyline(self._directrix(self.track_disk_height), forConstruction=True).tag("directrix") return result @@ -357,9 +393,9 @@ class TorsionJoint: .circle(self.radius_spring, mode='s') .parray( r=0, - a1=0, - da=360, - n=self.n_slots) + a1=self.rider_slot_begin, + da=self.rider_slot_span, + n=self.rider_n_slots) .each(slot, mode='s') #.circle(self._radius_wall, mode='a') ) @@ -367,43 +403,58 @@ class TorsionJoint: Cq.Sketch() .circle(self.groove_radius_outer, mode='a') .circle(self.groove_radius_inner, mode='s') - #.circle(self._radius_wall, mode='a') - .parray( - r=0, - a1=0, - da=360, - n=self.n_slots) - .each(slot, mode='s') ) - middle_height = self.spring_height - self.groove_depth - self.rider_gap + if not self.spring_hole_cover_rider: + contact_profile = ( + contact_profile + .parray( + r=0, + a1=self.rider_slot_begin, + da=self.rider_slot_span, + n=self.rider_n_slots) + .each(slot, mode='s') + .reset() + ) + #.circle(self._radius_wall, mode='a') + middle_height = self.spring_height - self.groove_depth - self.rider_gap - self.spring_thickness result = ( Cq.Workplane('XY') .cylinder( radius=self.radius, - height=self.disk_height, + height=self.rider_disk_height, centered=(True, True, False)) .faces('>Z') .tag("spring") + .workplane() .placeSketch(wall_profile) .extrude(middle_height) + .faces(tag="spring") + .workplane() # The top face might not be in one piece. - #.faces('>Z') .workplane(offset=middle_height) .placeSketch(contact_profile) .extrude(self.groove_depth + self.rider_gap) .faces(tag="spring") + .workplane() .circle(self._radius_spring_internal) .extrude(self.spring_height) - .faces('>Z') + #.faces(tag="spring") + #.workplane() .hole(self.radius_axle * 2) ) - for i in range(self.n_slots): - theta = 2 * math.pi * i / self.n_slots - result.polyline(self._directrix(self.disk_height, theta), + theta_begin = math.radians(self.rider_slot_begin) + math.pi + theta_span = math.radians(self.rider_slot_span) + if abs(math.remainder(self.rider_slot_span, 360)) < TOL: + theta_step = theta_span / self.rider_n_slots + else: + theta_step = theta_span / (self.rider_n_slots - 1) + for i in range(self.rider_n_slots): + theta = theta_begin - i * theta_step + result.polyline(self._directrix(self.rider_disk_height, theta), forConstruction=True).tag(f"directrix{i}") return result - def rider_track_assembly(self): + def rider_track_assembly(self, directrix=0): rider = self.rider() track = self.track() spring = self.spring() @@ -412,10 +463,10 @@ class TorsionJoint: .add(spring, name="spring", color=Role.DAMPING.color) .add(track, name="track", color=Role.PARENT.color) .constrain("track?spring", "spring?top", "Plane") + .constrain("track?directrix", "spring?directrix_bot", "Axis") .add(rider, name="rider", color=Role.CHILD.color) .constrain("rider?spring", "spring?bot", "Plane") - .constrain("track?directrix", "spring?directrix_bot", "Axis") - .constrain("rider?directrix0", "spring?directrix_top", "Axis") + .constrain(f"rider?directrix{directrix}", "spring?directrix_top", "Axis") .solve() ) return result diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 1e70ba2..73b4cd7 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -1,6 +1,6 @@ import unittest import cadquery as Cq -from nhf.checks import binary_intersection +from nhf.checks import binary_intersection, pairwise_intersection from nhf.parts import joints, handle, metric_threads class TestJoints(unittest.TestCase): @@ -22,12 +22,25 @@ class TestJoints(unittest.TestCase): "Hirth joint assembly must not have intersection") def test_joints_comma_assembly(self): joints.comma_assembly() + def test_torsion_joint(self): j = joints.TorsionJoint() assembly = j.rider_track_assembly() bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.zlen, j.total_height) + def torsion_joint_collision_case(self, joint: joints.TorsionJoint, slot: int): + assembly = joint.rider_track_assembly(slot) + bbox = assembly.toCompound().BoundingBox() + self.assertAlmostEqual(bbox.zlen, joint.total_height) + self.assertEqual(pairwise_intersection(assembly), []) + + def test_torsion_joint_collision(self): + j = joints.TorsionJoint() + for slot in range(j.rider_n_slots): + with self.subTest(slot=slot): + self.torsion_joint_collision_case(j, slot) + class TestHandle(unittest.TestCase): -- 2.44.1 From 800b658410bfc54b4ffc26b48b829e2c3b402d1f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 6 Jul 2024 23:50:10 -0700 Subject: [PATCH 039/187] feat: Right-handed spring --- nhf/parts/joints.py | 5 +++-- nhf/parts/springs.py | 18 ++++++++++++------ nhf/parts/test.py | 3 +++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index b69fb67..0a4b47a 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -314,12 +314,12 @@ class TorsionJoint: l = self.spring_tail_length if self.right_handed: r2 = -r2 - # This is (0, r2) and (l, r2) transformed by rotation matrix - # [[c, s], [-s, c]] return [ (0, 0, height), (c, s, height) ] + # This is (0, r2) and (l, r2) transformed by rotation matrix + # [[c, s], [-s, c]] return [ (s * r2, -s * l + c * r2, height), (c * l + s * r2, -s * l + c * r2, height), @@ -332,6 +332,7 @@ class TorsionJoint: height=self.spring_height, thickness=self.spring_thickness, tail_length=self.spring_tail_length, + right_handed=self.right_handed, ) def track(self): diff --git a/nhf/parts/springs.py b/nhf/parts/springs.py index 31fcf51..7ce0530 100644 --- a/nhf/parts/springs.py +++ b/nhf/parts/springs.py @@ -5,11 +5,14 @@ def torsion_spring(radius=12, height=20, thickness=2, omega=90, - tail_length=25): + tail_length=25, + right_handed: bool = False): """ Produces a torsion spring with abridged geometry since sweep is very slow in cq-editor. """ + if right_handed: + omega = -omega base = ( Cq.Workplane('XY') .cylinder(height=height, radius=radius, @@ -17,12 +20,14 @@ def torsion_spring(radius=12, ) base.faces(">Z").tag("top") base.faces(" Date: Sat, 6 Jul 2024 23:53:57 -0700 Subject: [PATCH 040/187] test: Torsion joint covered --- nhf/parts/test.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 5685978..0610f8f 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -23,26 +23,27 @@ class TestJoints(unittest.TestCase): def test_joints_comma_assembly(self): joints.comma_assembly() - def test_torsion_joint(self): - j = joints.TorsionJoint() - assembly = j.rider_track_assembly() - bbox = assembly.toCompound().BoundingBox() - self.assertAlmostEqual(bbox.zlen, j.total_height) - - def torsion_joint_collision_case(self, joint: joints.TorsionJoint, slot: int): + def torsion_joint_case(self, joint: joints.TorsionJoint, slot: int): assembly = joint.rider_track_assembly(slot) bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.zlen, joint.total_height) self.assertEqual(pairwise_intersection(assembly), []) - def test_torsion_joint_collision(self): + def test_torsion_joint(self): j = joints.TorsionJoint() for slot in range(j.rider_n_slots): with self.subTest(slot=slot): - self.torsion_joint_collision_case(j, slot) - def test_torsion_joint_collision_right_handed(self): + self.torsion_joint_case(j, slot) + def test_torsion_joint_right_handed(self): j = joints.TorsionJoint(right_handed=True) - self.torsion_joint_collision_case(j, 1) + self.torsion_joint_case(j, 1) + def test_torsion_joint_covered(self): + j = joints.TorsionJoint( + spring_hole_cover_track=True, + spring_hole_cover_rider=True, + ) + self.torsion_joint_case(j, 1) + class TestHandle(unittest.TestCase): @@ -63,7 +64,7 @@ class TestMetricThreads(unittest.TestCase): def test_major_radius(self): major = 3.0 - t = metric_threads.external_metric_thread(major, 0.5, 4.0, z_start= -0.85, top_lead_in=True) + t = metric_threads.external_metric_thread(major, 0.5, 4.0, z_start=-0.85, top_lead_in=True) bbox = t.val().BoundingBox() self.assertAlmostEqual(bbox.xlen, major, places=3) self.assertAlmostEqual(bbox.ylen, major, places=3) -- 2.44.1 From fc0edd995b51c6c337aed21904efb96bc9fbce83 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 7 Jul 2024 09:44:54 -0700 Subject: [PATCH 041/187] fix: Torsion joint directrix --- nhf/parts/joints.py | 14 +++++--------- nhf/parts/test.py | 2 ++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 0a4b47a..2ebb543 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -265,7 +265,7 @@ class TorsionJoint: spring_thickness: float = 2 spring_height: float = 15 - spring_tail_length: float = 40 + spring_tail_length: float = 35 groove_radius_outer: float = 35 groove_radius_inner: float = 20 @@ -314,15 +314,11 @@ class TorsionJoint: l = self.spring_tail_length if self.right_handed: r2 = -r2 + # This is (0, r2) and (l, r2) transformed by right handed rotation + # matrix `[[c, -s], [s, c]]` return [ - (0, 0, height), - (c, s, height) - ] - # This is (0, r2) and (l, r2) transformed by rotation matrix - # [[c, s], [-s, c]] - return [ - (s * r2, -s * l + c * r2, height), - (c * l + s * r2, -s * l + c * r2, height), + (-s * r2, c * r2, height), + (c * l - s * r2, s * l + c * r2, height), ] diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 0610f8f..bb05e20 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -27,6 +27,8 @@ class TestJoints(unittest.TestCase): assembly = joint.rider_track_assembly(slot) bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.zlen, joint.total_height) + self.assertAlmostEqual(bbox.xlen, joint.radius * 2) + self.assertAlmostEqual(bbox.ylen, joint.radius * 2) self.assertEqual(pairwise_intersection(assembly), []) def test_torsion_joint(self): -- 2.44.1 From 54593b9a4ef3eef93d525d21f1b55c7a57bb3718 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 7 Jul 2024 12:15:47 -0700 Subject: [PATCH 042/187] feat: Shoulder parent joint --- nhf/parts/joints.py | 4 +- nhf/parts/test.py | 6 +++ nhf/touhou/houjuu_nue/__init__.py | 83 +++++++++++++++++++++++++------ nhf/touhou/houjuu_nue/test.py | 13 +++-- nhf/touhou/houjuu_nue/wing.py | 26 ++++++---- 5 files changed, 102 insertions(+), 30 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 2ebb543..a74b7fe 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -379,7 +379,9 @@ class TorsionJoint: forConstruction=True).tag("directrix") return result - def rider(self): + def rider(self, rider_slot_begin=None): + if not rider_slot_begin: + rider_slot_begin = self.rider_slot_begin def slot(loc): wire = Cq.Wire.makePolygon(self._slot_polygon(flip=False)) face = Cq.Face.makeFromWires(wire) diff --git a/nhf/parts/test.py b/nhf/parts/test.py index bb05e20..93ab7c1 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -24,6 +24,7 @@ class TestJoints(unittest.TestCase): joints.comma_assembly() def torsion_joint_case(self, joint: joints.TorsionJoint, slot: int): + assert 0 <= slot and slot < joint.rider_n_slots assembly = joint.rider_track_assembly(slot) bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.zlen, joint.total_height) @@ -45,6 +46,11 @@ class TestJoints(unittest.TestCase): spring_hole_cover_rider=True, ) self.torsion_joint_case(j, 1) + def test_torsion_joint_slot(self): + j = joints.TorsionJoint( + rider_slot_begin=90, + ) + self.torsion_joint_case(j, 1) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 9b5af33..32954d1 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -92,8 +92,10 @@ class Parameters(Model): wing_root_radius: float = 40 wing_root_wall_thickness: float = 8 - shoulder_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( - radius_axle=8 + shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( + track_disk_height=5.0, + rider_disk_height=7.0, + radius_axle=8.0, )) # Two holes on each side (top and bottom) are used to attach the shoulder @@ -104,10 +106,10 @@ class Parameters(Model): """ Heights for various wing joints, where the numbers start from the first joint. """ + wing_s0_thickness: float = 40 + wing_s0_height: float = 100 wing_r1_height: float = 100 wing_r1_width: float = 400 - wing_r2_height: float = 100 - wing_r3_height: float = 100 trident_handle: Handle = field(default_factory=lambda: Handle( diam=38, @@ -267,18 +269,20 @@ class Parameters(Model): shoulder_attach_dist=self.shoulder_attach_dist, shoulder_attach_diam=self.shoulder_attach_diam, wall_thickness=self.wing_root_wall_thickness, + conn_height=self.wing_s0_height, + conn_thickness=self.wing_s0_thickness, ) - @target(name="shoulder") - def shoulder_parent_joint(self) -> Cq.Assembly: + @target(name="shoulder_parent") + def shoulder_parent_joint(self) -> Cq.Workplane: result = ( - self.shoulder_joint.rider() + self.shoulder_torsion_joint.rider() .copyWorkplane(Cq.Workplane( - 'YZ', origin=Cq.Vector((100, 0, self.wing_root_wall_thickness)))) - .rect(30, 10, centered=(True, False)) + 'YZ', origin=Cq.Vector((90, 0, self.wing_root_wall_thickness)))) + .rect(25, 7, centered=(True, False)) .extrude("next") .copyWorkplane(Cq.Workplane( - 'YX', origin=Cq.Vector((60, 0, self.wing_root_wall_thickness)))) + 'YX', origin=Cq.Vector((55, 0, self.wing_root_wall_thickness)))) .hole(self.shoulder_attach_diam) .moveTo(0, self.shoulder_attach_dist) .hole(self.shoulder_attach_diam) @@ -287,6 +291,27 @@ class Parameters(Model): result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1') return result + @target(name="shoulder_child") + def shoulder_child_joint(self) -> Cq.Assembly: + # FIXME: half of conn_height + h = 100 / 2 + dh = h - self.shoulder_torsion_joint.total_height + core = ( + Cq.Workplane('XY') + .moveTo(0, 15) + .box(50, 40, 2 * dh, centered=(True, False, True)) + ) + loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) + result = ( + Cq.Assembly() + .add(core, name="core", loc=Cq.Location()) + .add(self.shoulder_torsion_joint.track(), name="track_top", + loc=Cq.Location((0, 0, dh), (0, 0, 1), -90)) + .add(self.shoulder_torsion_joint.track(), name="track_bot", + loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) + ) + return result + def wing_r1_profile(self) -> Cq.Sketch: """ Generates the first wing segment profile, with the wing root pointing in @@ -352,14 +377,44 @@ class Parameters(Model): result.solve() return result + def shoulder_assembly(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.shoulder_child_joint(), name="child", + color=Role.CHILD.color) + .constrain("child/core", "Fixed") + # Top parent joint + .add(self.shoulder_torsion_joint.spring(), name="spring_top", + color=Role.DAMPING.color) + .constrain("child/track_top?spring", "spring_top?top", "Plane") + .constrain("child/track_top?directrix", "spring_top?directrix_bot", "Axis") + .add(self.shoulder_parent_joint(), name="parent_top", + color=Role.PARENT.color) + .constrain("parent_top?spring", "spring_top?bot", "Plane") + .constrain("parent_top?directrix0", "spring_top?directrix_top", "Axis") + # Bottom parent joint + .add(self.shoulder_torsion_joint.spring(), name="spring_bot", + color=Role.DAMPING.color) + .constrain("child/track_bot?spring", "spring_bot?top", "Plane") + .constrain("child/track_bot?directrix", "spring_bot?directrix_bot", "Axis") + .add(self.shoulder_parent_joint(), name="parent_bot", + color=Role.PARENT.color) + .constrain("parent_bot?spring", "spring_bot?bot", "Plane") + .constrain("parent_bot?directrix0", "spring_bot?directrix_top", "Axis") + .solve() + ) + return result + def wing_r1_assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() .add(self.wing_root(), name="r1") - .add(self.shoulder_parent_joint(), name="shoulder_parent_top", - color=Material.RESIN_TRANSPARENT.color) - .constrain("r1/scaffold?conn_top0", "shoulder_parent_top?conn0", "Plane") - .constrain("r1/scaffold?conn_top1", "shoulder_parent_top?conn1", "Plane") + .add(self.shoulder_assembly(), name="shoulder") + .constrain("r1/scaffold", "Fixed") + .constrain("r1/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") + .constrain("r1/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") + .constrain("r1/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") + .constrain("r1/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") .solve() ) return result diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 0b63ecd..512b95b 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -1,6 +1,7 @@ import unittest import cadquery as Cq import nhf.touhou.houjuu_nue as M +from nhf.checks import pairwise_intersection class Test(unittest.TestCase): @@ -8,19 +9,23 @@ class Test(unittest.TestCase): p = M.Parameters() obj = p.hs_joint_parent() self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece") + def test_shoulder_joint(self): + p = M.Parameters() + shoulder = p.shoulder_assembly() + assert isinstance(shoulder, Cq.Assembly) + self.assertEqual(pairwise_intersection(shoulder), []) + def test_wing_root(self): p = M.Parameters() obj = p.wing_root() + assert isinstance(obj, Cq.Assembly) #self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece") - bbox = obj.val().BoundingBox() + bbox = obj.toCompound().BoundingBox() msg = "Must fix 256^3 bbox" self.assertLess(bbox.xlen, 255, msg=msg) self.assertLess(bbox.ylen, 255, msg=msg) self.assertLess(bbox.zlen, 255, msg=msg) - def test_wing_root(self): - p = M.Parameters() - p.wing_root() def test_wings_assembly(self): p = M.Parameters() p.wings_assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index d9ab898..fe9c9e4 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -14,7 +14,7 @@ def wing_root_profiles( base_radius=40, middle_offset=30, middle_height=80, - conn_width=40, + conn_thickness=40, conn_height=100) -> tuple[Cq.Wire, Cq.Wire]: assert base_sweep < 180 assert middle_offset > 0 @@ -74,7 +74,7 @@ def wing_root_profiles( # If the exterior sweep is theta', it has to satisfy # # sin(theta) * r2 + wall_thickness = sin(theta') * r1 - x, y = conn_width / 2, middle_height / 2 + x, y = conn_thickness / 2, middle_height / 2 t = wall_thickness dx = middle_offset middle = ( @@ -109,7 +109,7 @@ def wing_root_profiles( ) assert isinstance(middle, Cq.Wire) - x, y = conn_width / 2, conn_height / 2 + x, y = conn_thickness / 2, conn_height / 2 t = wall_thickness tip = ( Cq.Sketch() @@ -132,30 +132,32 @@ def wing_root(joint: HirthJoint, union_tol=1e-4, shoulder_attach_diam=8, shoulder_attach_dist=25, - conn_width=40, + conn_thickness=40, conn_height=100, wall_thickness=8) -> Cq.Assembly: """ Generate the contiguous components of the root wing segment """ tip_centre = Cq.Vector((-150, 0, -80)) + attach_theta = math.radians(5) + c, s = math.cos(attach_theta), math.sin(attach_theta) attach_points = [ - (15, 0), - (15 + shoulder_attach_dist, 0), + (15, 4), + (15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s), ] root_profile, middle_profile, tip_profile = wing_root_profiles( - conn_width=conn_width, + conn_thickness=conn_thickness, conn_height=conn_height, wall_thickness=8, ) middle_profile = middle_profile.located(Cq.Location( - (-40, 0, -40), (0, 30, 0) + (-40, 0, -40), (0, 1, 0), 30 )) antetip_profile = tip_profile.located(Cq.Location( - (-95, 0, -75), (0, 60, 0) + (-95, 0, -75), (0, 1, 0), 60 )) tip_profile = tip_profile.located(Cq.Location( - tip_centre, (0, 90, 0) + tip_centre, (0, 1, 0), 90 )) profiles = [ root_profile, @@ -201,9 +203,11 @@ def wing_root(joint: HirthJoint, Cq.Vector((0, y, 0))) ) ) + if side == "bottom": + side = "bot" for i, (px, py) in enumerate(attach_points): tag = f"conn_{side}{i}" - plane.moveTo(px, py).tagPlane(tag) + plane.moveTo(px, -py if side == "top" else py).tagPlane(tag) result.faces("X").tag("conn") -- 2.44.1 From d43482f77d85055b50541c3fcb36f4b9fc42a70d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 7 Jul 2024 21:01:40 -0700 Subject: [PATCH 043/187] feat: 2nd wing segment with spacer --- nhf/materials.py | 1 + nhf/parts/joints.py | 1 + nhf/touhou/houjuu_nue/__init__.py | 95 +++++++++++++++++++++---------- nhf/touhou/houjuu_nue/wing.py | 31 ++++++++++ nhf/utils.py | 34 +++++++---- 5 files changed, 121 insertions(+), 41 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index 0cb266d..863ee3e 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -33,6 +33,7 @@ class Material(Enum): PLASTIC_PLA = 0.5, _color('azure3', 0.6) RESIN_TRANSPARENT = 1.1, _color('cadetblue2', 0.6) ACRYLIC_BLACK = 0.5, _color('gray50', 0.6) + ACRYLIC_TRANSPARENT = 0.5, _color('ghostwhite', 0.5) def __init__(self, density: float, color: Cq.Color): self.density = density diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index a74b7fe..5990e8a 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -332,6 +332,7 @@ class TorsionJoint: ) def track(self): + # TODO: Cover outer part of track only. Can we do this? groove_profile = ( Cq.Sketch() .circle(self.radius) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 32954d1..db534ec 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -104,12 +104,16 @@ class Parameters(Model): shoulder_attach_diam: float = 8 """ - Heights for various wing joints, where the numbers start from the first joint. + Heights for various wing joints, where the numbers start from the first + joint. """ wing_s0_thickness: float = 40 wing_s0_height: float = 100 - wing_r1_height: float = 100 - wing_r1_width: float = 400 + + # Length of the spacer + wing_s1_thickness: float = 20 + wing_s1_spacer_thickness: float = 25.4 / 8 + wing_s1_spacer_width: float = 20 trident_handle: Handle = field(default_factory=lambda: Handle( diam=38, @@ -121,6 +125,9 @@ class Parameters(Model): simplify_geometry=False, )) + material_panel: Material = Material.ACRYLIC_TRANSPARENT + material_bracket: Material = Material.ACRYLIC_TRANSPARENT + def __post_init__(self): super().__init__(name="houjuu-nue") assert self.wing_root_radius > self.hs_hirth_joint.radius,\ @@ -258,6 +265,10 @@ class Parameters(Model): result.faces(" Cq.Workplane: + # return self.wing_joining_plate.plate() + @target(name="wing_root") def wing_root(self) -> Cq.Assembly: """ @@ -312,43 +323,42 @@ class Parameters(Model): ) return result - def wing_r1_profile(self) -> Cq.Sketch: - """ - Generates the first wing segment profile, with the wing root pointing in - the positive x axis. - """ - # Depression of the wing middle - bend = 200 - factor = 0.7 + @target(name="wing/s1-spacer", kind=TargetKind.DXF) + def wing_s1_spacer(self) -> Cq.Workplane: result = ( - Cq.Sketch() - .segment((0, 0), (0, self.wing_r1_height)) - .spline([ - (0, self.wing_r1_height), - (0.5 * self.wing_r1_width, self.wing_r1_height - factor * bend), - (self.wing_r1_width, self.wing_r1_height - bend), - ]) - .segment( - (self.wing_r1_width, self.wing_r1_height - bend), - (self.wing_r1_width, -bend), - ) - .spline([ - (self.wing_r1_width, - bend), - (0.5 * self.wing_r1_width, - factor * bend), - (0, 0), - ]) - .assemble() + Cq.Workplane('XZ') + .sketch() + .rect(self.wing_s1_spacer_width, self.wing_s1_thickness) + .finalize() + .extrude(self.wing_s1_spacer_thickness) ) + result.faces("Z").tag("mate2") + result.faces(">Y").tag("dir") return result - def wing_r1(self) -> Cq.Solid: - profile = self.wing_r1_profile() + @target(name="wing/r1s1", kind=TargetKind.DXF) + def wing_r1s1_profile(self) -> Cq.Sketch: + return MW.wing_r1s1_profile() + + def wing_r1s1_panel(self, front=True) -> Cq.Workplane: + profile = self.wing_r1s1_profile() + anchors = [ + ("shoulder_bot", 10, 10), + ("middle", 50, -20), + ("tip", 390, -150), + ] result = ( Cq.Workplane("XY") .placeSketch(profile) .extrude(self.panel_thickness) - .val() ) + plane = result.faces(">Z" if front else " Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.wing_r1s1_panel(front=True), name="panel_front", + color=self.material_panel.color) + .add(self.wing_r1s1_panel(front=False), name="panel_back", + color=self.material_panel.color) + .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: result = ( Cq.Assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index fe9c9e4..61a535f 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -226,3 +226,34 @@ def wing_root(joint: HirthJoint, loc=Cq.Location((0, 0, -joint.total_height))) ) return result + +def wing_r1s1_profile() -> Cq.Sketch: + """ + Generates the first wing segment profile, with the wing root pointing in + the positive x axis. + """ + # Depression of the wing middle + h = 100 + w = 400 + bend = 200 + factor = 0.7 + result = ( + Cq.Sketch() + .segment((0, 0), (0, h)) + .spline([ + (0, h), + (0.5 * w, h - factor * bend), + (w, h - bend), + ]) + .segment( + (w, h - bend), + (w, -bend), + ) + .spline([ + (w, - bend), + (0.5 * w, - factor * bend), + (0, 0), + ]) + .assemble() + ) + return result diff --git a/nhf/utils.py b/nhf/utils.py index 7cc020d..f375c63 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -6,6 +6,7 @@ Adds the functions to `Cq.Workplane`: 2. `tagPlane` """ import cadquery as Cq +from typing import Union, Tuple def tagPoint(self, tag: str): @@ -18,21 +19,34 @@ def tagPoint(self, tag: str): Cq.Workplane.tagPoint = tagPoint -def tagPlane(self, tag: str, axis='Z'): +def tagPlane(self, tag: str, + direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'): """ Adds a phantom `Cq.Edge` in the given location which can be referenced in a `Axis`, `Point`, or `Plane` constraint. """ - x, y, z = 0, 0, 0 - if axis in ('z', 'Z'): - z = 1 - elif axis in ('y', 'Y'): - y = 1 - elif axis in ('x', 'X'): - x = 1 + if isinstance(direction, str): + x, y, z = 0, 0, 0 + assert len(direction) == 2 + sign, axis = direction + if axis in ('z', 'Z'): + z = 1 + elif axis in ('y', 'Y'): + y = 1 + elif axis in ('x', 'X'): + x = 1 + else: + assert False, "Axis must be one of x,y,z" + if sign == '+': + sign = 1 + elif sign == '-': + sign = -1 + else: + assert False, "Sign must be one of +/-" + v = Cq.Vector(x, y, z) * sign else: - assert False, "Axis must be one of x,y,z" - edge = Cq.Edge.makeLine((-x, -y, -z), (x, y, z)) + v = Cq.Vector(direction) + edge = Cq.Edge.makeLine(v * (-1), v) self.eachpoint(edge.moved, useLocalCoordinates=True).tag(tag) Cq.Workplane.tagPlane = tagPlane -- 2.44.1 From 53c143e0b751b2af3907547ad6a05f6d9cab8e00 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 7 Jul 2024 21:45:10 -0700 Subject: [PATCH 044/187] test: Check assembly collision --- nhf/build.py | 62 ++++++++++++++++++++++++++++- nhf/checks.py | 7 ++++ nhf/parts/handle.py | 4 ++ nhf/parts/test.py | 8 ++++ nhf/touhou/houjuu_nue/__init__.py | 66 ++++++++++++++++--------------- nhf/touhou/houjuu_nue/test.py | 14 ++----- 6 files changed, 118 insertions(+), 43 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index 4435508..25e957f 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -21,6 +21,7 @@ from typing import Union from functools import wraps from colorama import Fore, Style import cadquery as Cq +import nhf.checks as NC class TargetKind(Enum): @@ -31,6 +32,9 @@ class TargetKind(Enum): self.ext = ext class Target: + """ + Marks a function's output for serialization + """ def __init__(self, method, @@ -99,6 +103,59 @@ def target(name, **deco_kwargs): return wrapper return f +class Assembly: + """ + Marks a function's output for assembly property checking + """ + + def __init__(self, + method, + collision_check: bool = True, + **kwargs): + self._method = method + self.name = method.__name__ + self.collision_check = collision_check + self.kwargs = kwargs + def __str__(self): + return f"" + def __call__(self, obj, *args, **kwargs): + """ + Raw call function which passes arguments directly to `_method` + """ + return self._method(obj, *args, **kwargs) + + def check(self, obj): + x = self._method(obj) + assert isinstance(x, Cq.Assembly) + if self.collision_check: + intersections = NC.pairwise_intersection(x) + assert not intersections, f"In {self}, collision detected: {intersections}" + + @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, '_assembly'): + yield method._assembly + return {method.name: method for method in g()} + +def assembly(**deco_kwargs): + """ + Decorator for annotating an assembly output + """ + def f(method): + @wraps(method) + def wrapper(self, *args, **kwargs): + return method(self, *args, **kwargs) + wrapper._assembly = Assembly(method, **deco_kwargs) + return wrapper + return f class Model: """ @@ -116,12 +173,15 @@ class Model: def check_all(self) -> int: """ - Builds all targets but do not output them + Build all models and run all the checks. Return number of checks passed """ total = 0 for t in Target.methods(self).values(): t(self) total += 1 + for t in Assembly.methods(self).values(): + t.check(self) + total += 1 return total def build_all(self, output_dir: Union[Path, str] = "build", verbose=1): diff --git a/nhf/checks.py b/nhf/checks.py index 19914ab..a5a413d 100644 --- a/nhf/checks.py +++ b/nhf/checks.py @@ -24,6 +24,13 @@ def pairwise_intersection(assembly: Cq.Assembly, tol: float=1e-6) -> list[(str, if i2 <= i1: # Remove the upper diagonal continue + head = name.split('/', 2)[1] + head2 = name2.split('/', 2)[1] + if head == head2: + # Do not test into subassemblies + continue + + vol = sh1.intersect(sh2).Volume() if vol > tol: result.append((name, name2, vol)) diff --git a/nhf/parts/handle.py b/nhf/parts/handle.py index c6845ef..bdeb459 100644 --- a/nhf/parts/handle.py +++ b/nhf/parts/handle.py @@ -69,6 +69,8 @@ class Handle: .cylinder( radius=self.diam / 2, height=length) + .faces(">Z") + .hole(self.diam_inner) ) result.faces("Z").tag("mate2") @@ -200,6 +202,8 @@ class Handle: result .union( thread + # Avoids collision in some mating cases + .rotate((0,0,0), (1,0,0), angleDegrees=180) .located(Cq.Location((0, 0, height)))) ) return result diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 93ab7c1..4445d57 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -56,16 +56,24 @@ class TestJoints(unittest.TestCase): class TestHandle(unittest.TestCase): + def test_handle_collision(self): + h = handle.Handle() + assembly = h.connector_insertion_assembly() + self.assertEqual(pairwise_intersection(assembly), []) def test_handle_assembly(self): h = handle.Handle() assembly = h.connector_insertion_assembly() bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.xlen, h.diam) self.assertAlmostEqual(bbox.ylen, h.diam) + + def test_one_sided_insertion(self): + h = handle.Handle() assembly = h.connector_one_side_insertion_assembly() bbox = assembly.toCompound().BoundingBox() self.assertAlmostEqual(bbox.xlen, h.diam) self.assertAlmostEqual(bbox.ylen, h.diam) + self.assertEqual(pairwise_intersection(assembly), []) class TestMetricThreads(unittest.TestCase): diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index db534ec..564a7fc 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -33,7 +33,7 @@ from dataclasses import dataclass, field import unittest import cadquery as Cq from nhf import Material, Role -from nhf.build import Model, TargetKind, target +from nhf.build import Model, TargetKind, target, assembly from nhf.parts.joints import HirthJoint, TorsionJoint from nhf.parts.handle import Handle import nhf.touhou.houjuu_nue.wing as MW @@ -265,6 +265,26 @@ class Parameters(Model): result.faces(" Cq.Assembly: + harness = self.harness() + result = ( + Cq.Assembly() + .add(harness, name="base", color=Material.WOOD_BIRCH.color) + .constrain("base", "Fixed") + ) + for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: + j = self.hs_joint_parent() + ( + result + .add(j, name=name, color=Role.PARENT.color) + .constrain("base?mount", f"{name}?base", "Axis") + ) + for i in range(4): + result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") + result.solve() + return result + #@target(name="wing/joining-plate", kind=TargetKind.DXF) #def joining_plate(self) -> Cq.Workplane: # return self.wing_joining_plate.plate() @@ -289,11 +309,11 @@ class Parameters(Model): result = ( self.shoulder_torsion_joint.rider() .copyWorkplane(Cq.Workplane( - 'YZ', origin=Cq.Vector((90, 0, self.wing_root_wall_thickness)))) + 'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness)))) .rect(25, 7, centered=(True, False)) .extrude("next") .copyWorkplane(Cq.Workplane( - 'YX', origin=Cq.Vector((55, 0, self.wing_root_wall_thickness)))) + 'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness)))) .hole(self.shoulder_attach_diam) .moveTo(0, self.shoulder_attach_dist) .hole(self.shoulder_attach_diam) @@ -355,38 +375,11 @@ class Parameters(Model): ) plane = result.faces(">Z" if front else " Cq.Assembly: - return MT.trident_assembly(self.trident_handle) - - def harness_assembly(self) -> Cq.Assembly: - harness = self.harness() - result = ( - Cq.Assembly() - .add(harness, name="base", color=Material.WOOD_BIRCH.color) - .constrain("base", "Fixed") - ) - for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: - j = self.hs_joint_parent() - ( - result - .add(j, name=name, color=Role.PARENT.color) - .constrain("base?mount", f"{name}?base", "Axis") - ) - for i in range(4): - result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") - result.solve() return result + @assembly() def shoulder_assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() @@ -415,6 +408,7 @@ class Parameters(Model): ) return result + @assembly() def wing_r1s1_assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() @@ -438,6 +432,7 @@ class Parameters(Model): return result + @assembly() def wing_r1_assembly(self) -> Cq.Assembly: result = ( Cq.Assembly() @@ -452,6 +447,7 @@ class Parameters(Model): ) return result + @assembly() def wings_assembly(self) -> Cq.Assembly: """ Assembly of harness with all the wings @@ -474,6 +470,14 @@ class Parameters(Model): ) return result + @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) + + if __name__ == '__main__': p = Parameters() diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 512b95b..42bd607 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -9,11 +9,6 @@ class Test(unittest.TestCase): p = M.Parameters() obj = p.hs_joint_parent() self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece") - def test_shoulder_joint(self): - p = M.Parameters() - shoulder = p.shoulder_assembly() - assert isinstance(shoulder, Cq.Assembly) - self.assertEqual(pairwise_intersection(shoulder), []) def test_wing_root(self): p = M.Parameters() @@ -26,12 +21,6 @@ class Test(unittest.TestCase): self.assertLess(bbox.xlen, 255, msg=msg) self.assertLess(bbox.ylen, 255, msg=msg) self.assertLess(bbox.zlen, 255, msg=msg) - def test_wings_assembly(self): - p = M.Parameters() - p.wings_assembly() - def test_harness_assembly(self): - p = M.Parameters() - p.harness_assembly() def test_trident_assembly(self): p = M.Parameters() assembly = p.trident_assembly() @@ -39,6 +28,9 @@ class Test(unittest.TestCase): length = bbox.zlen self.assertGreater(length, 1300) self.assertLess(length, 1700) + #def test_assemblies(self): + # p = M.Parameters() + # p.check_all() if __name__ == '__main__': unittest.main() -- 2.44.1 From 876571418c23166ab3f3bffcd1466b4db8d9d819 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 8 Jul 2024 21:46:35 -0700 Subject: [PATCH 045/187] fix: Directrix labeling in torsion joint --- nhf/parts/joints.py | 76 +++++++---- nhf/parts/springs.py | 17 ++- nhf/parts/test.py | 6 +- nhf/touhou/houjuu_nue/__init__.py | 209 ++++++++++++++++++++++++------ 4 files changed, 232 insertions(+), 76 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 5990e8a..c37e0a9 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -100,7 +100,7 @@ class HirthJoint: ( result .polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True) - .tag("directrix") + .tag("dir") ) return result @@ -132,7 +132,7 @@ class HirthJoint: .add(obj2, name="obj2", color=Role.CHILD.color) .constrain("obj1", "Fixed") .constrain("obj1?mate", "obj2?mate", "Plane") - .constrain("obj1?directrix", "obj2?directrix", "Axis", param=angle) + .constrain("obj1?dir", "obj2?dir", "Axis", param=angle) .solve() ) return result @@ -249,7 +249,8 @@ class TorsionJoint: """ # Radius limit for rotating components - radius: float = 40 + radius_track: float = 40 + radius_rider: float = 38 track_disk_height: float = 10 rider_disk_height: float = 8 @@ -281,7 +282,8 @@ class TorsionJoint: def __post_init__(self): - assert self.radius > self.groove_radius_outer + assert self.radius_track > self.groove_radius_outer + assert self.radius_rider > self.groove_radius_outer assert self.groove_radius_outer > self.groove_radius_inner assert self.groove_radius_inner > self.radius_spring assert self.spring_height > self.groove_depth, "Groove is too deep" @@ -289,8 +291,18 @@ class TorsionJoint: @property def total_height(self): + """ + Total height counting from bottom to top + """ return self.track_disk_height + self.rider_disk_height + self.spring_height + @property + def radius(self): + """ + Maximum radius of this joint + """ + return max(self.radius_rider, self.radius_track) + @property def _radius_spring_internal(self): return self.radius_spring - self.spring_thickness @@ -335,14 +347,14 @@ class TorsionJoint: # TODO: Cover outer part of track only. Can we do this? groove_profile = ( Cq.Sketch() - .circle(self.radius) + .circle(self.radius_track) .circle(self.groove_radius_outer, mode='s') .circle(self.groove_radius_inner, mode='a') .circle(self.radius_spring, mode='s') ) spring_hole_profile = ( Cq.Sketch() - .circle(self.radius) + .circle(self.radius_track) .circle(self.radius_spring, mode='s') ) slot_height = self.spring_thickness @@ -359,7 +371,7 @@ class TorsionJoint: result = ( Cq.Workplane('XY') .cylinder( - radius=self.radius, + radius=self.radius_track, height=self.track_disk_height, centered=(True, True, False)) .faces('>Z') @@ -375,12 +387,12 @@ class TorsionJoint: .hole(self.radius_axle * 2) .cut(slot.moved(Cq.Location((0, 0, self.track_disk_height)))) ) - # Insert directrix` + # Insert directrix result.polyline(self._directrix(self.track_disk_height), - forConstruction=True).tag("directrix") + forConstruction=True).tag("dir") return result - def rider(self, rider_slot_begin=None): + def rider(self, rider_slot_begin=None, reverse_directrix_label=False): if not rider_slot_begin: rider_slot_begin = self.rider_slot_begin def slot(loc): @@ -389,11 +401,11 @@ class TorsionJoint: return face.located(loc) wall_profile = ( Cq.Sketch() - .circle(self.radius, mode='a') + .circle(self.radius_rider, mode='a') .circle(self.radius_spring, mode='s') .parray( r=0, - a1=self.rider_slot_begin, + a1=rider_slot_begin, da=self.rider_slot_span, n=self.rider_n_slots) .each(slot, mode='s') @@ -409,7 +421,7 @@ class TorsionJoint: contact_profile .parray( r=0, - a1=self.rider_slot_begin, + a1=rider_slot_begin, da=self.rider_slot_span, n=self.rider_n_slots) .each(slot, mode='s') @@ -420,7 +432,7 @@ class TorsionJoint: result = ( Cq.Workplane('XY') .cylinder( - radius=self.radius, + radius=self.radius_rider, height=self.rider_disk_height, centered=(True, True, False)) .faces('>Z') @@ -442,16 +454,17 @@ class TorsionJoint: #.workplane() .hole(self.radius_axle * 2) ) - theta_begin = math.radians(self.rider_slot_begin) + math.pi + theta_begin = math.radians(rider_slot_begin) theta_span = math.radians(self.rider_slot_span) if abs(math.remainder(self.rider_slot_span, 360)) < TOL: theta_step = theta_span / self.rider_n_slots else: theta_step = theta_span / (self.rider_n_slots - 1) for i in range(self.rider_n_slots): - theta = theta_begin - i * theta_step + theta = theta_begin + i * theta_step + j = self.rider_n_slots - i - 1 if reverse_directrix_label else i result.polyline(self._directrix(self.rider_disk_height, theta), - forConstruction=True).tag(f"directrix{i}") + forConstruction=True).tag(f"dir{j}") return result def rider_track_assembly(self, directrix=0): @@ -462,11 +475,26 @@ class TorsionJoint: Cq.Assembly() .add(spring, name="spring", color=Role.DAMPING.color) .add(track, name="track", color=Role.PARENT.color) - .constrain("track?spring", "spring?top", "Plane") - .constrain("track?directrix", "spring?directrix_bot", "Axis") - .add(rider, name="rider", color=Role.CHILD.color) - .constrain("rider?spring", "spring?bot", "Plane") - .constrain(f"rider?directrix{directrix}", "spring?directrix_top", "Axis") - .solve() + .add(rider, name="rider", color=Role.PARENT.color) + ) + TorsionJoint.add_constraints(result, + rider="rider", track="track", spring="spring", + directrix=directrix) + return result.solve() + + @staticmethod + def add_constraints(assembly: Cq.Assembly, + rider: str, track: str, spring: str, + directrix: int = 0): + """ + Add the necessary constraints to a RT assembly + """ + ( + assembly + .constrain(f"{track}?spring", f"{spring}?top", "Plane") + .constrain(f"{track}?dir", f"{spring}?dir_top", + "Axis", param=0) + .constrain(f"{rider}?spring", f"{spring}?bot", "Plane") + .constrain(f"{rider}?dir{directrix}", f"{spring}?dir_bot", + "Axis", param=0) ) - return result diff --git a/nhf/parts/springs.py b/nhf/parts/springs.py index 7ce0530..c695d23 100644 --- a/nhf/parts/springs.py +++ b/nhf/parts/springs.py @@ -46,11 +46,14 @@ def torsion_spring(radius=12, centered=False) ) r = -radius if right_handed else radius - result.polyline([(0, r, 0), (tail_length, r, 0)], - forConstruction=True).tag("directrix_bot") - c, s = math.cos(omega * math.pi / 180), math.sin(omega * math.pi / 180) - result.polyline([ - (s * tail_length, c * r - s * tail_length, height), - (c * tail_length + s * r, c * r - s * tail_length, height)], - forConstruction=True).tag("directrix_top") + plane = result.copyWorkplane(Cq.Workplane('XY')) + plane.polyline([(0, r, 0), (tail_length, r, 0)], + forConstruction=True).tag("dir_bot") + omega = math.radians(omega) + c, s = math.cos(omega), math.sin(omega) + l = -tail_length + plane.polyline([ + (-s * r, c * r, height), + (c * l - s * r, c * r + s * l, height)], + forConstruction=True).tag("dir_top") return result diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 4445d57..3b40ce4 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -35,11 +35,13 @@ class TestJoints(unittest.TestCase): def test_torsion_joint(self): j = joints.TorsionJoint() for slot in range(j.rider_n_slots): - with self.subTest(slot=slot): + with self.subTest(slot=slot, right_handed=False): self.torsion_joint_case(j, slot) def test_torsion_joint_right_handed(self): j = joints.TorsionJoint(right_handed=True) - self.torsion_joint_case(j, 1) + for slot in range(j.rider_n_slots): + with self.subTest(slot=slot, right_handed=True): + self.torsion_joint_case(j, slot) def test_torsion_joint_covered(self): j = joints.TorsionJoint( spring_hole_cover_track=True, diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 564a7fc..1c210b8 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -30,7 +30,6 @@ s1, s2, s3. The joints are named (from root to tip) shoulder, elbow, wrist in analogy with human anatomy. """ from dataclasses import dataclass, field -import unittest import cadquery as Cq from nhf import Material, Role from nhf.build import Model, TargetKind, target, assembly @@ -93,6 +92,9 @@ class Parameters(Model): wing_root_wall_thickness: float = 8 shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( + radius_track=35, + radius_rider=35, + groove_radius_outer=32, track_disk_height=5.0, rider_disk_height=7.0, radius_axle=8.0, @@ -114,6 +116,9 @@ class Parameters(Model): 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, @@ -132,6 +137,8 @@ class Parameters(Model): super().__init__(name="houjuu-nue") 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" @target(name="trident/handle-connector") def handle_connector(self): @@ -304,14 +311,32 @@ class Parameters(Model): conn_thickness=self.wing_s0_thickness, ) - @target(name="shoulder_parent") - def shoulder_parent_joint(self) -> Cq.Workplane: + @target(name="shoulder_joint_parent") + def shoulder_joint_parent(self) -> Cq.Workplane: + joint = self.shoulder_torsion_joint + # Thickness of the lip connecting this joint to the wing root + lip_thickness = 10 + lip_width = 25 + lip_guard_ext = 40 + lip_guard_height = self.wing_root_wall_thickness + lip_thickness + assert lip_guard_ext > joint.radius_track + + lip_guard = ( + Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height) + .located(Cq.Location((0, -lip_width/2 , 0))) + .cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height)) + ) result = ( - self.shoulder_torsion_joint.rider() + joint.track() + .union(lip_guard, tol=1e-6) + + # Extrude the handle .copyWorkplane(Cq.Workplane( 'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness)))) - .rect(25, 7, centered=(True, False)) + .rect(lip_width, lip_thickness, centered=(True, False)) .extrude("next") + + # Connector holes on the lip .copyWorkplane(Cq.Workplane( 'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness)))) .hole(self.shoulder_attach_diam) @@ -322,27 +347,117 @@ class Parameters(Model): result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1') return result - @target(name="shoulder_child") - def shoulder_child_joint(self) -> Cq.Assembly: - # FIXME: half of conn_height - h = 100 / 2 - dh = h - self.shoulder_torsion_joint.total_height + @target(name="shoulder_joint_child") + def shoulder_joint_child(self) -> Cq.Assembly: + """ + Creates the top/bottom shoulder child joint + """ + + joint = self.shoulder_torsion_joint + # Half of the height of the bridging cylinder + dh = self.wing_s0_height / 2 - joint.total_height + core_start_angle = 30 + core_end_angle1 = 90 + core_end_angle2 = 180 + core_thickness = 2 + + core_profile1 = ( + Cq.Sketch() + .arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) + .segment((0, 0)) + .close() + .assemble() + .circle(joint.radius_rider - core_thickness, mode='s') + ) + core_profile2 = ( + Cq.Sketch() + .arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle)) + .segment((0, 0)) + .close() + .assemble() + .circle(joint.radius_rider - core_thickness, mode='s') + ) core = ( Cq.Workplane('XY') - .moveTo(0, 15) - .box(50, 40, 2 * dh, centered=(True, False, True)) + .placeSketch(core_profile1) + .toPending() + .extrude(dh * 2) + .copyWorkplane(Cq.Workplane('XY')) + .placeSketch(core_profile2) + .toPending() + .extrude(dh * 2) + .translate(Cq.Vector(0, 0, -dh)) ) + # Create the upper and lower lips + lip_height = self.wing_s1_thickness + lip_thickness = joint.rider_disk_height + lip_ext = 40 + joint.radius_rider + hole_dx = self.wing_s1_shoulder_spacer_hole_dist + assert lip_height / 2 <= joint.radius_rider + lip = ( + Cq.Workplane('XY') + .box(lip_ext, lip_height, lip_thickness, + centered=(False, True, False)) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder(radius=joint.radius_rider, height=lip_thickness, + centered=(True, True, False), + combine='cut') + .faces(">Z") + .workplane() + ) + hole_x = lip_ext - hole_dx / 2 + for i in range(2): + plane = ( + lip + .moveTo(hole_x - i * hole_dx, 0) + ) + lip = plane.hole(self.wing_s1_spacer_hole_diam) + plane.tagPlane(f"hole{i}") + loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) result = ( Cq.Assembly() .add(core, name="core", loc=Cq.Location()) - .add(self.shoulder_torsion_joint.track(), name="track_top", + .add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top", loc=Cq.Location((0, 0, dh), (0, 0, 1), -90)) - .add(self.shoulder_torsion_joint.track(), name="track_bot", + .add(joint.rider(rider_slot_begin=180), name="rider_bot", loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) + .add(lip, name="lip_top", + loc=Cq.Location((0, 0, dh))) + .add(lip, name="lip_bot", + loc=Cq.Location((0, 0, -dh)) * loc_rotate) ) return result + @assembly() + def shoulder_assembly(self) -> Cq.Assembly: + directrix = 0 + result = ( + Cq.Assembly() + .add(self.shoulder_joint_child(), name="child", + color=Role.CHILD.color) + .constrain("child/core", "Fixed") + .add(self.shoulder_torsion_joint.spring(), name="spring_top", + color=Role.DAMPING.color) + .add(self.shoulder_joint_parent(), name="parent_top", + color=Role.PARENT.color) + .add(self.shoulder_torsion_joint.spring(), name="spring_bot", + color=Role.DAMPING.color) + .add(self.shoulder_joint_parent(), name="parent_bot", + color=Role.PARENT.color) + ) + TorsionJoint.add_constraints(result, + rider="child/rider_top", + track="parent_top", + spring="spring_top", + directrix=directrix) + TorsionJoint.add_constraints(result, + rider="child/rider_bot", + track="parent_bot", + spring="spring_bot", + directrix=directrix) + return result.solve() + @target(name="wing/s1-spacer", kind=TargetKind.DXF) def wing_s1_spacer(self) -> Cq.Workplane: result = ( @@ -357,6 +472,36 @@ class Parameters(Model): result.faces(">Y").tag("dir") return result + @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) + def wing_s1_shoulder_spacer(self) -> Cq.Workplane: + dx = self.wing_s1_shoulder_spacer_hole_dist + result = ( + Cq.Workplane('XZ') + .sketch() + .rect(self.wing_s1_shoulder_spacer_width, + self.wing_s1_thickness) + .push([ + (0, 0), + (dx, 0), + ]) + .circle(self.wing_s1_spacer_hole_diam / 2, mode='s') + .finalize() + .extrude(self.wing_s1_spacer_thickness) + ) + # Tag the mating surfaces to be glued + result.faces("Z").tag("mate2") + + # Tag the directrix + result.faces(">Y").tag("dir") + + # Tag the holes + plane = result.faces(">Y").workplane() + # Side closer to the parent is 0 + plane.moveTo(dx, 0).tagPlane("hole0") + plane.tagPlane("hole1") + return result + @target(name="wing/r1s1", kind=TargetKind.DXF) def wing_r1s1_profile(self) -> Cq.Sketch: return MW.wing_r1s1_profile() @@ -379,35 +524,6 @@ class Parameters(Model): plane.moveTo(px, sign * py).tagPlane(name) return result - @assembly() - def shoulder_assembly(self) -> Cq.Assembly: - result = ( - Cq.Assembly() - .add(self.shoulder_child_joint(), name="child", - color=Role.CHILD.color) - .constrain("child/core", "Fixed") - # Top parent joint - .add(self.shoulder_torsion_joint.spring(), name="spring_top", - color=Role.DAMPING.color) - .constrain("child/track_top?spring", "spring_top?top", "Plane") - .constrain("child/track_top?directrix", "spring_top?directrix_bot", "Axis") - .add(self.shoulder_parent_joint(), name="parent_top", - color=Role.PARENT.color) - .constrain("parent_top?spring", "spring_top?bot", "Plane") - .constrain("parent_top?directrix0", "spring_top?directrix_top", "Axis") - # Bottom parent joint - .add(self.shoulder_torsion_joint.spring(), name="spring_bot", - color=Role.DAMPING.color) - .constrain("child/track_bot?spring", "spring_bot?top", "Plane") - .constrain("child/track_bot?directrix", "spring_bot?directrix_bot", "Axis") - .add(self.shoulder_parent_joint(), name="parent_bot", - color=Role.PARENT.color) - .constrain("parent_bot?spring", "spring_bot?bot", "Plane") - .constrain("parent_bot?directrix0", "spring_bot?directrix_top", "Axis") - .solve() - ) - return result - @assembly() def wing_r1s1_assembly(self) -> Cq.Assembly: result = ( @@ -427,6 +543,13 @@ class Parameters(Model): .constrain(f"panel_front?{tag}", f"{tag}_spacer?mate1", "Plane") .constrain(f"panel_back?{tag}", f"{tag}_spacer?mate2", "Plane") ) + ( + result + .add(self.shoulder_assembly(), name="shoulder") + .constrain("shoulder_bot_spacer?dir", + "shoulder/child/core?mate_bot", + "Plane") + ) result.solve() return result -- 2.44.1 From 234e1b7adc836bf2bcc741699e629ac1c219a0fa Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 8 Jul 2024 22:32:49 -0700 Subject: [PATCH 046/187] feat: Add shell to wing joint, wing assembly --- nhf/touhou/houjuu_nue/__init__.py | 107 +++++++++++++++++++++--------- nhf/touhou/houjuu_nue/test.py | 3 + 2 files changed, 77 insertions(+), 33 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 1c210b8..774be88 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -407,12 +407,15 @@ class Parameters(Model): ) hole_x = lip_ext - hole_dx / 2 for i in range(2): - plane = ( + x = hole_x - i * hole_dx + lip = lip.moveTo(x, 0).hole(self.wing_s1_spacer_hole_diam) + for i in range(2): + x = hole_x - i * hole_dx + ( lip - .moveTo(hole_x - i * hole_dx, 0) + .moveTo(x, 0) + .tagPlane(f"conn{1 - i}") ) - lip = plane.hole(self.wing_s1_spacer_hole_diam) - plane.tagPlane(f"hole{i}") loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) result = ( @@ -473,7 +476,11 @@ class Parameters(Model): return result @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) - def wing_s1_shoulder_spacer(self) -> Cq.Workplane: + def wing_s1_shoulder_spacer(self, flipped=False) -> Cq.Workplane: + """ + if `flipped = True`, tag on the bottom face. This does not change the + geometry. + """ dx = self.wing_s1_shoulder_spacer_hole_dist result = ( Cq.Workplane('XZ') @@ -493,13 +500,13 @@ class Parameters(Model): result.faces(">Z").tag("mate2") # Tag the directrix - result.faces(">Y").tag("dir") + result.faces("Y").workplane() + plane = result.faces("Y").workplane() # Side closer to the parent is 0 - plane.moveTo(dx, 0).tagPlane("hole0") - plane.tagPlane("hole1") + plane.moveTo(dx if flipped else -dx, 0).tagPlane("conn0") + plane.tagPlane("conn1") return result @target(name="wing/r1s1", kind=TargetKind.DXF) @@ -509,7 +516,8 @@ class Parameters(Model): def wing_r1s1_panel(self, front=True) -> Cq.Workplane: profile = self.wing_r1s1_profile() anchors = [ - ("shoulder_bot", 10, 10), + ("shoulder_top", 10, 55), + ("shoulder_bot", 10, 5), ("middle", 50, -20), ("tip", 390, -150), ] @@ -530,45 +538,78 @@ class Parameters(Model): Cq.Assembly() .add(self.wing_r1s1_panel(front=True), name="panel_front", color=self.material_panel.color) + .constrain("panel_front", "Fixed") .add(self.wing_r1s1_panel(front=False), name="panel_back", color=self.material_panel.color) .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: + def wing_r1_assembly(self, parts=["root", "shoulder", "s1"]) -> Cq.Assembly: result = ( Cq.Assembly() - .add(self.wing_root(), name="r1") - .add(self.shoulder_assembly(), name="shoulder") - .constrain("r1/scaffold", "Fixed") - .constrain("r1/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") - .constrain("r1/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") - .constrain("r1/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") - .constrain("r1/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") - .solve() ) - return result + if "root" in parts: + ( + result + .add(self.wing_root(), name="root") + .constrain("root/scaffold", "Fixed") + ) + if "shoulder" in parts: + result.add(self.shoulder_assembly(), name="shoulder") + + if "root" in parts and "shoulder" in parts: + ( + result + .constrain("root/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") + .constrain("root/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") + .constrain("root/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") + .constrain("root/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") + ) + + if "s1" in parts: + result.add(self.wing_r1s1_assembly(), name="s1") + + if "s1" in parts and "shoulder" in parts: + ( + result + .constrain("shoulder/child/lip_bot?conn0", + "s1/shoulder_bot_spacer?conn0", + "Plane") + .constrain("shoulder/child/lip_bot?conn1", + "s1/shoulder_bot_spacer?conn1", + "Plane") + .constrain("shoulder/child/lip_top?conn0", + "s1/shoulder_top_spacer?conn0", + "Plane") + .constrain("shoulder/child/lip_top?conn1", + "s1/shoulder_top_spacer?conn1", + "Plane") + ) + return result.solve() @assembly() def wings_assembly(self) -> Cq.Assembly: @@ -584,10 +625,10 @@ class Parameters(Model): .add(self.wing_root(), name="w0_l1") .constrain("harness/base", "Fixed") .constrain("w0_r1/joint?mate", "harness/r1?mate", "Plane") - .constrain("w0_r1/joint?directrix", "harness/r1?directrix", + .constrain("w0_r1/joint?dir", "harness/r1?dir", "Axis", param=7 * a_tooth) .constrain("w0_l1/joint?mate", "harness/l1?mate", "Plane") - .constrain("w0_l1/joint?directrix", "harness/l1?directrix", + .constrain("w0_l1/joint?dir", "harness/l1?dir", "Axis", param=-1 * a_tooth) .solve() ) diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 42bd607..2f610d5 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -21,6 +21,9 @@ class Test(unittest.TestCase): self.assertLess(bbox.xlen, 255, msg=msg) self.assertLess(bbox.ylen, 255, msg=msg) self.assertLess(bbox.zlen, 255, msg=msg) + def test_wings_assembly(self): + p = M.Parameters() + p.wings_assembly() def test_trident_assembly(self): p = M.Parameters() assembly = p.trident_assembly() -- 2.44.1 From 48cfd52455ed9da1b1206b73beb040ba478f5092 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 9 Jul 2024 19:57:54 -0700 Subject: [PATCH 047/187] refactor: Wing profile class --- nhf/touhou/houjuu_nue/__init__.py | 77 ++++++++++++++++++++----------- nhf/touhou/houjuu_nue/wing.py | 77 +++++++++++++++++++------------ 2 files changed, 97 insertions(+), 57 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 774be88..64c248b 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -87,6 +87,11 @@ class Parameters(Model): hs_joint_axis_cbore_diam: float = 20 hs_joint_axis_cbore_depth: float = 3 + wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( + shoulder_height = 100, + elbow_height = 120, + )) + # Exterior radius of the wing root assembly wing_root_radius: float = 40 wing_root_wall_thickness: float = 8 @@ -347,6 +352,15 @@ class Parameters(Model): result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1') return result + @property + def shoulder_joint_child_height(self) -> float: + """ + Calculates the y distance between two joint surfaces on the child side + of the shoulder joint. + """ + joint = self.shoulder_torsion_joint + return self.wing_s0_height - 2 * joint.total_height + 2 * joint.rider_disk_height + @target(name="shoulder_joint_child") def shoulder_joint_child(self) -> Cq.Assembly: """ @@ -470,18 +484,18 @@ class Parameters(Model): .finalize() .extrude(self.wing_s1_spacer_thickness) ) - result.faces("Z").tag("mate2") + result.faces("Z").tag("weld2") result.faces(">Y").tag("dir") return result @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) - def wing_s1_shoulder_spacer(self, flipped=False) -> Cq.Workplane: + def wing_s1_shoulder_spacer(self) -> Cq.Workplane: """ - if `flipped = True`, tag on the bottom face. This does not change the - geometry. + The mate tags are on the side closer to the holes. """ dx = self.wing_s1_shoulder_spacer_hole_dist + h = self.wing_s1_spacer_thickness result = ( Cq.Workplane('XZ') .sketch() @@ -493,33 +507,35 @@ class Parameters(Model): ]) .circle(self.wing_s1_spacer_hole_diam / 2, mode='s') .finalize() - .extrude(self.wing_s1_spacer_thickness) + .extrude(h) ) # Tag the mating surfaces to be glued - result.faces("Z").tag("mate2") + result.faces("Z").workplane().moveTo(0, -h).tagPlane("weld2") # Tag the directrix result.faces("Y").workplane() + plane = result.faces(">Y").workplane() # Side closer to the parent is 0 - plane.moveTo(dx if flipped else -dx, 0).tagPlane("conn0") + plane.moveTo(-dx, 0).tagPlane("conn0") plane.tagPlane("conn1") return result @target(name="wing/r1s1", kind=TargetKind.DXF) def wing_r1s1_profile(self) -> Cq.Sketch: - return MW.wing_r1s1_profile() + return self.wing_profile.wing_r1s1_profile() def wing_r1s1_panel(self, front=True) -> Cq.Workplane: profile = self.wing_r1s1_profile() + w = self.wing_s1_shoulder_spacer_width / 2 + h = (self.wing_profile.shoulder_height - self.shoulder_joint_child_height) / 2 anchors = [ - ("shoulder_top", 10, 55), - ("shoulder_bot", 10, 5), + ("shoulder_top", w, h + self.shoulder_joint_child_height), + ("shoulder_bot", w, h), ("middle", 50, -20), - ("tip", 390, -150), + ("tip", 270, 50), ] result = ( Cq.Workplane("XY") @@ -543,25 +559,30 @@ class Parameters(Model): color=self.material_panel.color) .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Sketch: - """ - Generates the first wing segment profile, with the wing root pointing in - the positive x axis. - """ - # Depression of the wing middle - h = 100 - w = 400 - bend = 200 - factor = 0.7 - result = ( - Cq.Sketch() - .segment((0, 0), (0, h)) - .spline([ - (0, h), - (0.5 * w, h - factor * bend), - (w, h - bend), - ]) - .segment( - (w, h - bend), - (w, -bend), + +@dataclass +class WingProfile: + + shoulder_height: float = 100 + elbow_height: float = 120 + + def wing_r1s1_profile(self) -> Cq.Sketch: + """ + Generates the first wing segment profile, with the wing root pointing in + the positive x axis. + """ + + + w = 270 + # Depression of the wing middle, measured + h = 0 + # spline curve easing extension + theta = math.radians(30) + c_th, s_th = math.cos(theta), math.sin(theta) + bend = 30 + ext = 40 + ext_dh = -5 + assert ext * 2 < w + + factor = 0.7 + + result = ( + Cq.Sketch() + .segment((0, 0), (0, self.shoulder_height)) + .spline([ + (0, self.shoulder_height), + ((w - s_th * self.elbow_height) / 2, self.shoulder_height / 2 + (self.elbow_height * c_th - h) / 2 - bend), + (w - s_th * self.elbow_height, self.elbow_height * c_th - h), + ]) + .segment( + (w - s_th * self.elbow_height, self.elbow_height * c_th -h), + (w, -h), + ) + .spline([ + (0, 0), + (w / 2, -h / 2 - bend), + (w, -h), + ]) + .assemble() ) - .spline([ - (w, - bend), - (0.5 * w, - factor * bend), - (0, 0), - ]) - .assemble() - ) - return result + return result -- 2.44.1 From 27ce94124f09173993225b6c5beaea14e1a15c1c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 9 Jul 2024 21:13:16 -0700 Subject: [PATCH 048/187] feat: Right side wing profile --- nhf/touhou/houjuu_nue/__init__.py | 13 ++--- nhf/touhou/houjuu_nue/wing.py | 94 +++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 64c248b..779cdb5 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -88,8 +88,8 @@ class Parameters(Model): hs_joint_axis_cbore_depth: float = 3 wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( - shoulder_height = 100, - elbow_height = 120, + shoulder_height = 80, + elbow_height = 100, )) # Exterior radius of the wing root assembly @@ -115,7 +115,6 @@ class Parameters(Model): joint. """ wing_s0_thickness: float = 40 - wing_s0_height: float = 100 # Length of the spacer wing_s1_thickness: float = 20 @@ -312,7 +311,7 @@ class Parameters(Model): shoulder_attach_dist=self.shoulder_attach_dist, shoulder_attach_diam=self.shoulder_attach_diam, wall_thickness=self.wing_root_wall_thickness, - conn_height=self.wing_s0_height, + conn_height=self.wing_profile.shoulder_height, conn_thickness=self.wing_s0_thickness, ) @@ -359,7 +358,7 @@ class Parameters(Model): of the shoulder joint. """ joint = self.shoulder_torsion_joint - return self.wing_s0_height - 2 * joint.total_height + 2 * joint.rider_disk_height + return self.wing_profile.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height @target(name="shoulder_joint_child") def shoulder_joint_child(self) -> Cq.Assembly: @@ -369,7 +368,7 @@ class Parameters(Model): joint = self.shoulder_torsion_joint # Half of the height of the bridging cylinder - dh = self.wing_s0_height / 2 - joint.total_height + dh = self.wing_profile.shoulder_height / 2 - joint.total_height core_start_angle = 30 core_end_angle1 = 90 core_end_angle2 = 180 @@ -525,7 +524,7 @@ class Parameters(Model): @target(name="wing/r1s1", kind=TargetKind.DXF) def wing_r1s1_profile(self) -> Cq.Sketch: - return self.wing_profile.wing_r1s1_profile() + return self.wing_profile.wing_r1_profile() def wing_r1s1_panel(self, front=True) -> Cq.Workplane: profile = self.wing_r1s1_profile() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 57e1198..b6551b8 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -234,7 +234,101 @@ def wing_root(joint: HirthJoint, class WingProfile: shoulder_height: float = 100 + elbow_height: float = 120 + elbow_x: float = 270 + elbow_y: float = 10 + # Angle of elbow w.r.t. y axis + elbow_angle: float = -20 + + wrist_height: float = 70 + # Bottom point of the wrist + wrist_x: float = 400 + wrist_y: float = 200 + + # Angle of wrist w.r.t. y axis. should be negative + wrist_angle: float = -40 + + # Extends from the wrist to the tip of the arrow + arrow_height: float = 300 + arrow_angle: float = 7 + + # Relative (in wrist coordinate) centre of the ring + ring_x: float = 40 + ring_y: float = 20 + ring_radius_inner: float = 22 + + def __post_init__(self): + assert self.ring_radius > self.ring_radius_inner + + @property + def ring_radius(self) -> float: + dx = self.ring_x + dy = self.ring_y + return (dx * dx + dy * dy) ** 0.5 + + def wing_r1_profile(self) -> Cq.Sketch: + wrist_theta = math.radians(self.wrist_angle) + wrist_s = math.sin(wrist_theta) + wrist_c = math.cos(wrist_theta) + wrist_top_x = self.wrist_x + self.wrist_height * wrist_s + wrist_top_y = self.wrist_y + self.wrist_height * wrist_c + elbow_theta = math.radians(self.elbow_angle) + elbow_top_x = self.elbow_x + self.elbow_height * math.sin(elbow_theta) + elbow_top_y = self.elbow_y + self.elbow_height * math.cos(elbow_theta) + result = ( + Cq.Sketch() + .segment( + (0, 0), + (0, self.shoulder_height), + tag="shoulder") + .arc( + (0, self.shoulder_height), + (elbow_top_x, elbow_top_y), + (wrist_top_x, wrist_top_y), + tag="s1_top") + #.segment( + # (self.wrist_x, self.wrist_y), + # (wrist_top_x, wrist_top_y), + # tag="wrist") + .arc( + (0, 0), + (self.elbow_x, self.elbow_y), + (self.wrist_x, self.wrist_y), + tag="s1_bot") + ) + arrow_theta = math.radians(self.arrow_angle) + arrow_x = self.wrist_x - self.arrow_height * wrist_s + arrow_y = self.wrist_y - self.arrow_height * wrist_c + arrow_tip_x = arrow_x + (self.arrow_height + self.wrist_height) * math.sin(arrow_theta + wrist_theta) + arrow_tip_y = arrow_y + (self.arrow_height + self.wrist_height) * math.cos(arrow_theta + wrist_theta) + result = ( + result + .segment( + (self.wrist_x, self.wrist_y), + (arrow_x, arrow_y) + ) + .segment( + (arrow_x, arrow_y), + (arrow_tip_x, arrow_tip_y) + ) + .segment( + (arrow_tip_x, arrow_tip_y), + (wrist_top_x, wrist_top_y) + ) + ) + # Carve out the ring + result = result.assemble() + ring_x = wrist_top_x + wrist_c * self.ring_x + wrist_s * self.ring_y + ring_y = wrist_top_y - wrist_s * self.ring_x + wrist_c * self.ring_y + result = ( + result + .push([(ring_x, ring_y)]) + .circle(self.ring_radius, mode='a') + .circle(self.ring_radius_inner, mode='s') + .clean() + ) + return result def wing_r1s1_profile(self) -> Cq.Sketch: """ -- 2.44.1 From 8b0c9a000df0346d73fac506de4fe85196302080 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 9 Jul 2024 21:31:00 -0700 Subject: [PATCH 049/187] refactor: Allow different types of handle joints --- nhf/parts/handle.py | 124 ++++++++++++++++++------------ nhf/touhou/houjuu_nue/__init__.py | 3 - 2 files changed, 76 insertions(+), 51 deletions(-) diff --git a/nhf/parts/handle.py b/nhf/parts/handle.py index bdeb459..4956618 100644 --- a/nhf/parts/handle.py +++ b/nhf/parts/handle.py @@ -1,10 +1,70 @@ """ This schematics file contains all designs related to tool handles """ -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Union, Optional import cadquery as Cq import nhf.parts.metric_threads as metric_threads +class TubeJoint: + + def diam_insertion_internal(self): + """ + Maximum permitted diameter of the internal cavity + """ + def diam_connector_external(self): + """ + Maximum permitted diameter of the external size of the insertion + """ + def external_thread(self, length: float) -> Cq.Shape: + """ + Generates the external connector + """ + def internal_thread(self, length: float) -> Cq.Shape: + """ + Generates the internal connector + """ + +@dataclass +class ThreadedJoint(TubeJoint): + + pitch: float = 3 + + # Major diameter of the internal threads, following ISO metric screw thread + # standard. This determines the wall thickness of the insertion. + diam_threading: float = 27 + + def diam_insertion_internal(self): + r = metric_threads.metric_thread_major_radius( + self.diam_threading, + self.pitch, + internal=True) + return r * 2 + def diam_connector_external(self): + r = metric_threads.metric_thread_minor_radius( + self.diam_threading, + self.pitch) + return r * 2 + + def external_thread(self, length: float): + return metric_threads.external_metric_thread( + self.diam_threading, + self.pitch, + length, + top_lead_in=True) + def internal_thread(self, length: float): + return metric_threads.internal_metric_thread( + self.diam_threading, + self.pitch, + length) + +@dataclass +class BayonetJoint(TubeJoint): + """ + Bayonet type joint + """ + pass + @dataclass class Handle: """ @@ -17,20 +77,16 @@ class Handle: Note that all the radial sizes are diameters (in mm). """ - # Outer and inner radius for the handle usually come in standard sizes + # Outer and inner radii for the handle usually come in standard sizes diam: float = 38 diam_inner: float = 33 - # Major diameter of the internal threads, following ISO metric screw thread - # standard. This determines the wall thickness of the insertion. - diam_threading: float = 27.0 - - thread_pitch: float = 3.0 + joint: Optional[TubeJoint] = field(default_factory=lambda: ThreadedJoint()) # Internal cavity diameter. This determines the wall thickness of the connector diam_connector_internal: float = 18.0 - # If set to true, do not generate threads + # If set to true, do not generate the connections simplify_geometry: bool = True # Length for the rim on the female connector @@ -43,26 +99,12 @@ class Handle: def __post_init__(self): assert self.diam > self.diam_inner, "Material thickness cannot be <= 0" - assert self.diam_inner > self.diam_insertion_internal, "Threading radius is too big" - assert self.diam_insertion_internal > self.diam_connector_external - assert self.diam_connector_external > self.diam_connector_internal, "Internal diameter is too large" + if self.joint: + assert self.diam_inner > self.joint.diam_insertion_internal(), "Threading radius is too big" + assert self.joint.diam_insertion_internal() > self.joint.diam_connector_external() + assert self.joint.diam_connector_external() > self.diam_connector_internal, "Internal diameter is too large" assert self.insertion_length > self.rim_length - @property - def diam_insertion_internal(self): - r = metric_threads.metric_thread_major_radius( - self.diam_threading, - self.thread_pitch, - internal=True) - return r * 2 - - @property - def diam_connector_external(self): - r = metric_threads.metric_thread_minor_radius( - self.diam_threading, - self.thread_pitch) - return r * 2 - def segment(self, length: float): result = ( Cq.Workplane() @@ -76,20 +118,6 @@ class Handle: result.faces(">Z").tag("mate2") return result - def _external_thread(self, length=None): - if length is None: - length = self.insertion_length - return metric_threads.external_metric_thread( - self.diam_threading, - self.thread_pitch, - length, - top_lead_in=True) - def _internal_thread(self): - return metric_threads.internal_metric_thread( - self.diam_threading, - self.thread_pitch, - self.insertion_length) - def insertion(self, holes=[]): """ This type of joint is used to connect two handlebar pieces. Each handlebar @@ -121,11 +149,11 @@ class Handle: .circle(self.diam / 2) .extrude(self.rim_length) .faces(">Z") - .hole(self.diam_insertion_internal) + .hole(self.joint.diam_insertion_internal()) ) result.faces(">Z").tag("mate") if not self.simplify_geometry: - thread = self._internal_thread().val() + thread = self.joint.internal_thread(self.connector_length).val() result = result.union(thread) for h in holes: cyl = Cq.Solid.makeCylinder( @@ -157,13 +185,13 @@ class Handle: result .faces(selector) .workplane() - .circle(self.diam_connector_external / 2) + .circle(self.joint.diam_connector_external() / 2) .extrude(self.insertion_length) ) if not solid: result = result.faces(">Z").hole(self.diam_connector_internal) if not self.simplify_geometry: - thread = self._external_thread().val() + thread = self.joint.external_thread(self.insertion_length).val() result = ( result .union( @@ -193,11 +221,11 @@ class Handle: result .faces(">Z") .workplane() - .circle(self.diam_connector_external / 2) + .circle(self.joint.diam_connector_external() / 2) .extrude(self.insertion_length) ) if not self.simplify_geometry: - thread = self._external_thread().val() + thread = self.joint.external_thread(self.insertion_length).val() result = ( result .union( @@ -215,7 +243,7 @@ class Handle: result = ( Cq.Workplane('XY') .cylinder( - radius=self.diam_connector_external / 2, + radius=self.joint.diam_connector_external / 2, height=length, centered=(True, True, False), ) @@ -223,7 +251,7 @@ class Handle: result.faces(">Z").tag("mate") result.faces(" Date: Tue, 9 Jul 2024 21:32:29 -0700 Subject: [PATCH 050/187] fix: Use insertion length for threads --- nhf/parts/handle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/parts/handle.py b/nhf/parts/handle.py index 4956618..5a61bed 100644 --- a/nhf/parts/handle.py +++ b/nhf/parts/handle.py @@ -153,7 +153,7 @@ class Handle: ) result.faces(">Z").tag("mate") if not self.simplify_geometry: - thread = self.joint.internal_thread(self.connector_length).val() + thread = self.joint.internal_thread(self.insertion_length).val() result = result.union(thread) for h in holes: cyl = Cq.Solid.makeCylinder( -- 2.44.1 From b441789c9f9231491888adccc68a01b8daaca323 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 9 Jul 2024 21:34:06 -0700 Subject: [PATCH 051/187] refactor: Use proper "mount" terminology --- nhf/parts/handle.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nhf/parts/handle.py b/nhf/parts/handle.py index 5a61bed..19fe15a 100644 --- a/nhf/parts/handle.py +++ b/nhf/parts/handle.py @@ -6,7 +6,10 @@ from typing import Union, Optional import cadquery as Cq import nhf.parts.metric_threads as metric_threads -class TubeJoint: +class Mount: + """ + Describes the internal connection between two cylinders + """ def diam_insertion_internal(self): """ @@ -26,7 +29,7 @@ class TubeJoint: """ @dataclass -class ThreadedJoint(TubeJoint): +class ThreadedJoint(Mount): pitch: float = 3 @@ -59,7 +62,7 @@ class ThreadedJoint(TubeJoint): length) @dataclass -class BayonetJoint(TubeJoint): +class BayonetMount(Mount): """ Bayonet type joint """ @@ -81,7 +84,7 @@ class Handle: diam: float = 38 diam_inner: float = 33 - joint: Optional[TubeJoint] = field(default_factory=lambda: ThreadedJoint()) + joint: Optional[Mount] = field(default_factory=lambda: ThreadedJoint()) # Internal cavity diameter. This determines the wall thickness of the connector diam_connector_internal: float = 18.0 -- 2.44.1 From 539a5d1229b5178a1bd191a4a4f7be7a3f25921d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 9 Jul 2024 22:09:16 -0700 Subject: [PATCH 052/187] feat: Bayonet mount --- nhf/parts/handle.py | 148 ++++++++++++++++++++++++------ nhf/parts/test.py | 30 ++++-- nhf/touhou/houjuu_nue/__init__.py | 10 +- 3 files changed, 152 insertions(+), 36 deletions(-) diff --git a/nhf/parts/handle.py b/nhf/parts/handle.py index 19fe15a..dc6a01f 100644 --- a/nhf/parts/handle.py +++ b/nhf/parts/handle.py @@ -5,19 +5,20 @@ from dataclasses import dataclass, field from typing import Union, Optional import cadquery as Cq import nhf.parts.metric_threads as metric_threads +import nhf.utils class Mount: """ Describes the internal connection between two cylinders """ - def diam_insertion_internal(self): + def diam_insertion_internal(self) -> float: """ - Maximum permitted diameter of the internal cavity + Diameter of the internal cavity in the insertion """ - def diam_connector_external(self): + def diam_connector_external(self) -> float: """ - Maximum permitted diameter of the external size of the insertion + Diameter of the external size of the connector """ def external_thread(self, length: float) -> Cq.Shape: """ @@ -29,7 +30,7 @@ class Mount: """ @dataclass -class ThreadedJoint(Mount): +class ThreadedMount(Mount): pitch: float = 3 @@ -37,13 +38,13 @@ class ThreadedJoint(Mount): # standard. This determines the wall thickness of the insertion. diam_threading: float = 27 - def diam_insertion_internal(self): + def diam_insertion_internal(self) -> float: r = metric_threads.metric_thread_major_radius( self.diam_threading, self.pitch, internal=True) return r * 2 - def diam_connector_external(self): + def diam_connector_external(self) -> float: r = metric_threads.metric_thread_minor_radius( self.diam_threading, self.pitch) @@ -64,9 +65,94 @@ class ThreadedJoint(Mount): @dataclass class BayonetMount(Mount): """ - Bayonet type joint + Bayonet type connection """ - pass + diam_outer: float = 30 + diam_inner: float = 27 + + # Angular span (in degrees) of the slider + pin_span: float = 15 + pin_height: float = 5 + + # Angular span (in degrees) of the slot + slot_span: float = 90 + + # Number of pins equally distributed along a circle + n_pin: int = 2 + + def __post_init__(self): + assert self.diam_outer > self.diam_inner + assert self.n_pin * self.slot_span < 360 + assert self.slot_span > self.pin_span + + def diam_insertion_internal(self) -> float: + return self.diam_outer + def diam_connector_external(self) -> float: + return self.diam_inner + + def external_thread(self, length: float): + assert length > self.pin_height + pin = ( + Cq.Workplane('XY') + .cylinder( + height=self.pin_height, + radius=self.diam_outer / 2, + angle=self.pin_span, + centered=(True, True, False)) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=self.pin_height, + radius=self.diam_inner / 2, + centered=(True, True, False), + combine="cut") + .val() + ) + result = ( + Cq.Workplane('XY') + .polarArray(radius=0, startAngle=0, angle=360, count=self.n_pin) + .eachpoint(lambda loc: pin.located(loc), combine='a') + .clean() + ) + return result + def internal_thread(self, length: float): + assert length > self.pin_height + slot = ( + Cq.Workplane('XY') + .cylinder( + height=length, + radius=self.diam_outer / 2, + angle=self.pin_span, + centered=(True, True, False) + ) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=self.pin_height, + radius=self.diam_outer / 2, + angle=self.slot_span, + centered=(True, True, False) + ) + .val() + ) + result = ( + Cq.Workplane('XY') + .cylinder( + height=length, + radius=self.diam_outer / 2, + centered=(True, True, False), + ) + .polarArray(radius=0, startAngle=-self.slot_span+self.pin_span, angle=360, count=self.n_pin) + .cutEach(lambda loc: slot.located(loc)) + .clean() + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=length, + radius=self.diam_inner / 2, + centered=(True, True, False), + combine="cut" + ) + ) + return result + @dataclass class Handle: @@ -75,7 +161,7 @@ class Handle: This assumes the handle segment material does not have threads. Each segment attaches to two insertions, which have threads on the inside. A connector - has threads on the outside and joints two insertions. + has threads on the outside and mounts two insertions. Note that all the radial sizes are diameters (in mm). """ @@ -84,7 +170,7 @@ class Handle: diam: float = 38 diam_inner: float = 33 - joint: Optional[Mount] = field(default_factory=lambda: ThreadedJoint()) + mount: Optional[Mount] = field(default_factory=lambda: ThreadedMount()) # Internal cavity diameter. This determines the wall thickness of the connector diam_connector_internal: float = 18.0 @@ -102,10 +188,10 @@ class Handle: def __post_init__(self): assert self.diam > self.diam_inner, "Material thickness cannot be <= 0" - if self.joint: - assert self.diam_inner > self.joint.diam_insertion_internal(), "Threading radius is too big" - assert self.joint.diam_insertion_internal() > self.joint.diam_connector_external() - assert self.joint.diam_connector_external() > self.diam_connector_internal, "Internal diameter is too large" + if self.mount: + assert self.diam_inner > self.mount.diam_insertion_internal(), "Threading radius is too big" + assert self.mount.diam_insertion_internal() >= self.mount.diam_connector_external() + assert self.mount.diam_connector_external() > self.diam_connector_internal, "Internal diameter is too large" assert self.insertion_length > self.rim_length def segment(self, length: float): @@ -123,8 +209,8 @@ class Handle: def insertion(self, holes=[]): """ - This type of joint is used to connect two handlebar pieces. Each handlebar - piece is a tube which cannot be machined, so the joint connects to the + This type of mount is used to connect two handlebar pieces. Each handlebar + piece is a tube which cannot be machined, so the mount connects to the handle by glue. Tags: @@ -152,11 +238,12 @@ class Handle: .circle(self.diam / 2) .extrude(self.rim_length) .faces(">Z") - .hole(self.joint.diam_insertion_internal()) + .hole(self.mount.diam_insertion_internal()) ) result.faces(">Z").tag("mate") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X") if not self.simplify_geometry: - thread = self.joint.internal_thread(self.insertion_length).val() + thread = self.mount.internal_thread(self.insertion_length).val() result = result.union(thread) for h in holes: cyl = Cq.Solid.makeCylinder( @@ -188,23 +275,24 @@ class Handle: result .faces(selector) .workplane() - .circle(self.joint.diam_connector_external() / 2) + .circle(self.mount.diam_connector_external() / 2) .extrude(self.insertion_length) ) if not solid: result = result.faces(">Z").hole(self.diam_connector_internal) if not self.simplify_geometry: - thread = self.joint.external_thread(self.insertion_length).val() + thread = self.mount.external_thread(self.insertion_length).val() result = ( result .union( thread - .located(Cq.Location((0, 0, self.connector_length / 2)))) + .located(Cq.Location((0, 0, -self.connector_length)))) .union( thread .rotate((0,0,0), (1,0,0), angleDegrees=180) - .located(Cq.Location((0, 0, -self.connector_length / 2)))) + .located(Cq.Location((0, 0, self.connector_length)))) ) + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X") return result def one_side_connector(self, height=None): @@ -224,18 +312,19 @@ class Handle: result .faces(">Z") .workplane() - .circle(self.joint.diam_connector_external() / 2) + .circle(self.mount.diam_connector_external() / 2) .extrude(self.insertion_length) ) + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", "+X") if not self.simplify_geometry: - thread = self.joint.external_thread(self.insertion_length).val() + thread = self.mount.external_thread(self.insertion_length).val() result = ( result .union( thread # Avoids collision in some mating cases .rotate((0,0,0), (1,0,0), angleDegrees=180) - .located(Cq.Location((0, 0, height)))) + .located(Cq.Location((0, 0, height + self.insertion_length)))) ) return result @@ -246,7 +335,7 @@ class Handle: result = ( Cq.Workplane('XY') .cylinder( - radius=self.joint.diam_connector_external / 2, + radius=self.mount.diam_connector_external / 2, height=length, centered=(True, True, False), ) @@ -254,7 +343,7 @@ class Handle: result.faces(">Z").tag("mate") result.faces(" Cq.Sketch: -- 2.44.1 From dcb3c31c1d751300194d43f93740944d3a7df2f8 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 9 Jul 2024 22:22:48 -0700 Subject: [PATCH 053/187] feat: Prototype flag, spring re-parameter --- nhf/build.py | 2 ++ nhf/touhou/houjuu_nue/__init__.py | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index 25e957f..dcfbba7 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -39,10 +39,12 @@ class Target: def __init__(self, method, name: str, + prototype: bool = False, kind: TargetKind = TargetKind.STL, **kwargs): self._method = method self.name = name + self.prototype = prototype self.kind = kind self.kwargs = kwargs def __str__(self): diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index e7c0337..6e3254d 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -103,6 +103,8 @@ class Parameters(Model): track_disk_height=5.0, rider_disk_height=7.0, radius_axle=8.0, + radius_spring=9 + 1.2 * 2, + spring_thickness=1.3, )) # Two holes on each side (top and bottom) are used to attach the shoulder @@ -149,8 +151,8 @@ class Parameters(Model): @target(name="trident/handle-insertion") def handle_insertion(self): return self.trident_handle.insertion() - @target(name="trident/proto-handle-terminal-connector") - def handle_experimental_connector(self): + @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): @@ -305,7 +307,7 @@ class Parameters(Model): #def joining_plate(self) -> Cq.Workplane: # return self.wing_joining_plate.plate() - @target(name="wing_root") + @target(name="wing/root") def wing_root(self) -> Cq.Assembly: """ Generate the wing root which contains a Hirth joint at its base and a @@ -320,7 +322,14 @@ class Parameters(Model): conn_thickness=self.wing_s0_thickness, ) - @target(name="shoulder_joint_parent") + @target(name="wing/proto-shoulder-joint-parent", prototype=True) + def proto_shoulder_joint_parent(self): + return self.shoulder_torsion_joint.track() + @target(name="wing/proto-shoulder-joint-child", prototype=True) + def proto_shoulder_joint_child(self): + return self.shoulder_torsion_joint.rider() + + @target(name="wing/shoulder_joint_parent") def shoulder_joint_parent(self) -> Cq.Workplane: joint = self.shoulder_torsion_joint # Thickness of the lip connecting this joint to the wing root @@ -365,7 +374,7 @@ class Parameters(Model): joint = self.shoulder_torsion_joint return self.wing_profile.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height - @target(name="shoulder_joint_child") + @target(name="wing/shoulder_joint_child") def shoulder_joint_child(self) -> Cq.Assembly: """ Creates the top/bottom shoulder child joint -- 2.44.1 From 056f6bb085e6ae6521c4e63e24b200d7a1e7a942 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 9 Jul 2024 22:30:29 -0700 Subject: [PATCH 054/187] feat: Gap in bayonet mount --- nhf/parts/handle.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/nhf/parts/handle.py b/nhf/parts/handle.py index dc6a01f..d6b926c 100644 --- a/nhf/parts/handle.py +++ b/nhf/parts/handle.py @@ -73,6 +73,8 @@ class BayonetMount(Mount): # Angular span (in degrees) of the slider pin_span: float = 15 pin_height: float = 5 + # Wall at the bottom of the slot + gap: float = 3 # Angular span (in degrees) of the slot slot_span: float = 90 @@ -80,6 +82,7 @@ class BayonetMount(Mount): # Number of pins equally distributed along a circle n_pin: int = 2 + def __post_init__(self): assert self.diam_outer > self.diam_inner assert self.n_pin * self.slot_span < 360 @@ -91,7 +94,7 @@ class BayonetMount(Mount): return self.diam_inner def external_thread(self, length: float): - assert length > self.pin_height + assert length > self.pin_height + self.gap pin = ( Cq.Workplane('XY') .cylinder( @@ -109,17 +112,18 @@ class BayonetMount(Mount): ) result = ( Cq.Workplane('XY') + .workplane(offset=self.gap) .polarArray(radius=0, startAngle=0, angle=360, count=self.n_pin) .eachpoint(lambda loc: pin.located(loc), combine='a') .clean() ) return result def internal_thread(self, length: float): - assert length > self.pin_height + assert length > self.pin_height + self.gap slot = ( Cq.Workplane('XY') .cylinder( - height=length, + height=length - self.gap, radius=self.diam_outer / 2, angle=self.pin_span, centered=(True, True, False) @@ -140,7 +144,9 @@ class BayonetMount(Mount): radius=self.diam_outer / 2, centered=(True, True, False), ) - .polarArray(radius=0, startAngle=-self.slot_span+self.pin_span, angle=360, count=self.n_pin) + .copyWorkplane(Cq.Workplane('XY')) + .workplane(offset=self.gap) + .polarArray(radius=0, startAngle=self.slot_span, angle=360, count=self.n_pin) .cutEach(lambda loc: slot.located(loc)) .clean() .copyWorkplane(Cq.Workplane('XY')) -- 2.44.1 From 2395c46839803dbfaf4a78ce12a68c421d2e880f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 10 Jul 2024 10:34:18 -0700 Subject: [PATCH 055/187] fix: Torsion joint rider must have through hole --- nhf/parts/joints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index c37e0a9..3761882 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -450,8 +450,8 @@ class TorsionJoint: .workplane() .circle(self._radius_spring_internal) .extrude(self.spring_height) - #.faces(tag="spring") - #.workplane() + .faces(" Date: Wed, 10 Jul 2024 10:52:48 -0700 Subject: [PATCH 056/187] fix: Torsion joint slot labeling --- nhf/parts/joints.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 3761882..2f5b62b 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Optional import math import cadquery as Cq import nhf.parts.springs as springs @@ -454,14 +455,14 @@ class TorsionJoint: .workplane() .hole(self.radius_axle * 2) ) - theta_begin = math.radians(rider_slot_begin) + theta_begin = -math.radians(rider_slot_begin) theta_span = math.radians(self.rider_slot_span) if abs(math.remainder(self.rider_slot_span, 360)) < TOL: theta_step = theta_span / self.rider_n_slots else: theta_step = theta_span / (self.rider_n_slots - 1) for i in range(self.rider_n_slots): - theta = theta_begin + i * theta_step + theta = theta_begin - i * theta_step j = self.rider_n_slots - i - 1 if reverse_directrix_label else i result.polyline(self._directrix(self.rider_disk_height, theta), forConstruction=True).tag(f"dir{j}") @@ -475,26 +476,34 @@ class TorsionJoint: Cq.Assembly() .add(spring, name="spring", color=Role.DAMPING.color) .add(track, name="track", color=Role.PARENT.color) - .add(rider, name="rider", color=Role.PARENT.color) + .add(rider, name="rider", color=Role.CHILD.color) ) - TorsionJoint.add_constraints(result, - rider="rider", track="track", spring="spring", - directrix=directrix) + TorsionJoint.add_constraints( + result, + rider="rider", track="track", spring="spring", + directrix=directrix) return result.solve() @staticmethod def add_constraints(assembly: Cq.Assembly, - rider: str, track: str, spring: str, + spring: str, + rider: Optional[str] = None, + track: Optional[str] = None, directrix: int = 0): """ Add the necessary constraints to a RT assembly """ - ( - assembly - .constrain(f"{track}?spring", f"{spring}?top", "Plane") - .constrain(f"{track}?dir", f"{spring}?dir_top", - "Axis", param=0) - .constrain(f"{rider}?spring", f"{spring}?bot", "Plane") - .constrain(f"{rider}?dir{directrix}", f"{spring}?dir_bot", - "Axis", param=0) - ) + if track: + ( + assembly + .constrain(f"{track}?spring", f"{spring}?top", "Plane") + .constrain(f"{track}?dir", f"{spring}?dir_top", + "Axis", param=0) + ) + if rider: + ( + assembly + .constrain(f"{rider}?spring", f"{spring}?bot", "Plane") + .constrain(f"{rider}?dir{directrix}", f"{spring}?dir_bot", + "Axis", param=0) + ) -- 2.44.1 From 86a5d6e6bf3d32c6c68453b337a7a45b949b6a9b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 10 Jul 2024 11:58:31 -0700 Subject: [PATCH 057/187] fix: Size of torsion joint cf. spring --- nhf/touhou/houjuu_nue/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 6e3254d..1f1ce0d 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -97,14 +97,18 @@ class Parameters(Model): wing_root_wall_thickness: float = 8 shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( - radius_track=35, - radius_rider=35, - groove_radius_outer=32, + radius_track=18, + radius_rider=18, + groove_radius_outer=16, + groove_radius_inner=13, track_disk_height=5.0, - rider_disk_height=7.0, - radius_axle=8.0, - radius_spring=9 + 1.2 * 2, + rider_disk_height=5.0, + # M8 Axle + radius_axle=3.0, + # inner diameter = 9 + radius_spring=9/2 + 1.2, spring_thickness=1.3, + spring_height=7.5, )) # Two holes on each side (top and bottom) are used to attach the shoulder -- 2.44.1 From 6de1c3bc3911e5cd9b80ef2321a7fadfaaccd122 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 10 Jul 2024 16:20:33 -0700 Subject: [PATCH 058/187] feat: Finalize handle properties --- nhf/touhou/houjuu_nue/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 1f1ce0d..dfccb5a 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -137,7 +137,9 @@ class Parameters(Model): simplify_geometry=False, mount=BayonetMount(n_pin=3), )) - trident_terminal_height: float = 60 + trident_terminal_height: float = 80 + trident_terminal_hole_diam: float = 24 + trident_terminal_bottom_thickness: float = 10 material_panel: Material = Material.ACRYLIC_TRANSPARENT material_bracket: Material = Material.ACRYLIC_TRANSPARENT @@ -160,7 +162,11 @@ class Parameters(Model): return self.trident_handle.one_side_connector(height=15) @target(name="trident/handle-terminal-connector") def handle_terminal_connector(self): - return self.trident_handle.one_side_connector(height=self.trident_terminal_height) + result = self.trident_handle.one_side_connector(height=self.trident_terminal_height) + #result.faces("Z").hole(self.trident_terminal_hole_diam, depth=h) + return result def harness_profile(self) -> Cq.Sketch: -- 2.44.1 From 2cf03eae097f88b7af107235e3392ffdb8b770a5 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 10 Jul 2024 16:20:52 -0700 Subject: [PATCH 059/187] feat: Add inner gap to torsion joint This is for easing movement --- nhf/parts/joints.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 2f5b62b..bb1c449 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -271,6 +271,8 @@ class TorsionJoint: groove_radius_outer: float = 35 groove_radius_inner: float = 20 + # Gap on inner groove to ease movement + groove_inner_gap: float = 0.2 groove_depth: float = 5 rider_gap: float = 1 rider_n_slots: float = 4 @@ -285,7 +287,7 @@ class TorsionJoint: def __post_init__(self): assert self.radius_track > self.groove_radius_outer assert self.radius_rider > self.groove_radius_outer - assert self.groove_radius_outer > self.groove_radius_inner + assert self.groove_radius_outer > self.groove_radius_inner + self.groove_inner_gap assert self.groove_radius_inner > self.radius_spring assert self.spring_height > self.groove_depth, "Groove is too deep" assert self.radius_spring > self.radius_axle @@ -415,7 +417,7 @@ class TorsionJoint: contact_profile = ( Cq.Sketch() .circle(self.groove_radius_outer, mode='a') - .circle(self.groove_radius_inner, mode='s') + .circle(self.groove_radius_inner + self.groove_inner_gap, mode='s') ) if not self.spring_hole_cover_rider: contact_profile = ( -- 2.44.1 From d8a62d33529aec92710acb457230ae06a6968c5a Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 10 Jul 2024 16:21:11 -0700 Subject: [PATCH 060/187] feat: Disk joint for wrist and elbow --- nhf/touhou/houjuu_nue/parts.py | 220 +++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 nhf/touhou/houjuu_nue/parts.py diff --git a/nhf/touhou/houjuu_nue/parts.py b/nhf/touhou/houjuu_nue/parts.py new file mode 100644 index 0000000..a3a5bfb --- /dev/null +++ b/nhf/touhou/houjuu_nue/parts.py @@ -0,0 +1,220 @@ +from dataclasses import dataclass +import cadquery as Cq +from nhf import Role +from nhf.build import Model, target +import nhf.parts.springs as springs +import nhf.utils + +TOL = 1e-6 + +@dataclass +class DiskJoint(Model): + """ + Sandwiched disk joint for the wrist and elbow + """ + + radius_housing: float = 22.0 + radius_disk: float = 20.0 + radius_spring: float = 9 / 2 + radius_axle: float = 3.0 + + housing_thickness: float = 5.0 + disk_thickness: float = 5.0 + # Gap between disk and the housing + #disk_thickness_gap: float = 0.1 + spring_thickness: float = 1.3 + spring_height: float = 6.5 + spring_tail_length: float = 45.0 + spring_angle: float = 90.0 + spring_angle_shift: float = 0 + wall_inset: float = 2.0 + + # Angular span of movement + movement_angle: float = 120.0 + # Angular span of tongue on disk + tongue_span: float = 30.0 + tongue_length: float = 10.0 + + generate_inner_wall: bool = False + + + def __post_init__(self): + super().__init__(name="disk-joint") + assert self.housing_thickness > self.wall_inset + assert self.radius_housing > self.radius_disk + assert self.radius_disk > self.radius_axle + assert self.housing_upper_carve_offset > 0 + + def spring(self): + return springs.torsion_spring( + radius=self.radius_spring, + height=self.spring_height, + thickness=self.spring_thickness, + tail_length=self.spring_tail_length, + right_handed=False, + ) + + @property + def total_thickness(self): + return self.housing_thickness * 2 + self.disk_thickness + + @property + def opening_span(self): + return self.movement_angle + self.tongue_span + + @property + def housing_upper_carve_offset(self): + return self.housing_thickness + self.disk_thickness - self.spring_height + + @property + def radius_spring_internal(self): + return self.radius_spring - self.spring_thickness + + @target(name="disk") + def disk(self) -> Cq.Workplane: + cut = ( + Cq.Solid.makeBox( + length=self.spring_tail_length, + width=self.spring_thickness, + height=self.disk_thickness, + ) + .located(Cq.Location((0, self.radius_spring_internal, 0))) + .rotate((0, 0, 0), (0, 0, 1), self.spring_angle_shift) + ) + result = ( + Cq.Workplane('XY') + .cylinder( + height=self.disk_thickness, + radius=self.radius_disk, + centered=(True, True, False) + ) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=self.disk_thickness, + radius=self.radius_spring, + centered=(True, True, False), + combine='cut', + ) + .cut(cut) + ) + plane = result.copyWorkplane(Cq.Workplane('XY')) + plane.tagPlane("dir", direction="+X") + plane.workplane(offset=self.disk_thickness).tagPlane("mate_top") + result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot") + + radius_tongue = self.radius_disk + self.tongue_length + tongue = ( + Cq.Solid.makeCylinder( + height=self.disk_thickness, + radius=radius_tongue, + angleDegrees=self.tongue_span, + ).cut(Cq.Solid.makeCylinder( + height=self.disk_thickness, + radius=self.radius_disk, + )) + ) + result = result.union(tongue, tol=TOL) + return result + + def wall(self) -> Cq.Compound: + height = self.disk_thickness + self.wall_inset + wall = Cq.Solid.makeCylinder( + radius=self.radius_housing, + height=height, + angleDegrees=360 - self.opening_span, + ).cut(Cq.Solid.makeCylinder( + radius=self.radius_disk, + height=height, + )).rotate((0, 0, 0), (0, 0, 1), self.opening_span) + return wall + + @target(name="housing-lower") + def housing_lower(self) -> Cq.Workplane: + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.radius_housing, + height=self.housing_thickness, + centered=(True, True, False), + ) + .cut(Cq.Solid.makeCylinder( + radius=self.radius_axle, + height=self.housing_thickness, + )) + ) + result.faces(">Z").tag("mate") + result.faces(">Z").workplane().tagPlane("dir", direction="+X") + result = result.cut( + self + .wall() + .mirror("XY") + .located(Cq.Location((0, 0, self.housing_thickness + self.disk_thickness))) + #.rotate((0, 0, 0), (1, 0, 0), 180) + #.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness))) + ) + return result + + @target(name="housing-upper") + def housing_upper(self) -> Cq.Workplane: + carve = ( + Cq.Solid.makeCylinder( + radius=self.radius_spring, + height=self.housing_thickness + ).fuse(Cq.Solid.makeBox( + length=self.spring_tail_length, + width=self.spring_thickness, + height=self.housing_thickness + ).located(Cq.Location((0, self.radius_spring_internal, 0)))) + ).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift) + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.radius_housing, + height=self.housing_thickness, + centered=(True, True, False), + ) + ) + result.faces(">Z").tag("mate") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") + result = result.faces(">Z").hole(self.radius_axle * 2) + + # tube which holds the spring interior + if self.generate_inner_wall: + tube = ( + Cq.Solid.makeCylinder( + radius=self.radius_spring_internal, + height=self.disk_thickness + self.housing_thickness, + ).cut(Cq.Solid.makeCylinder( + radius=self.radius_axle, + height=self.disk_thickness + self.housing_thickness, + )) + ) + result = result.union(tube) + wall = ( + self.wall() + .rotate((0, 0, 0), (1, 0, 0), 180) + .located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset))) + ) + result = ( + result + .union(wall, tol=TOL) + .cut(carve.located(Cq.Location((0, 0, self.housing_upper_carve_offset)))) + ) + return result + + + def assembly(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.disk(), name="disk", color=Role.CHILD.color) + .add(self.housing_lower(), name="housing_lower", color=Role.PARENT.color) + .add(self.housing_upper(), name="housing_upper", color=Role.PARENT.color) + .constrain("disk?mate_bot", "housing_lower?mate", "Plane") + .constrain("disk?mate_top", "housing_upper?mate", "Plane") + .solve() + ) + return result + +if __name__ == '__main__': + p = DiskJoint() + p.build_all() -- 2.44.1 From 2aeeaae0619bb1010aec9aa7045659012de9753b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 11 Jul 2024 08:42:13 -0700 Subject: [PATCH 061/187] feat: Movement span on disk joint --- nhf/materials.py | 1 + nhf/touhou/houjuu_nue/parts.py | 71 +++++++++++++++++++++++++++------- nhf/touhou/houjuu_nue/test.py | 16 ++++++++ 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index 863ee3e..6108dc1 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -15,6 +15,7 @@ class Role(Enum): # Parent and child components in a load bearing joint PARENT = _color('blue4', 0.6) + CASING = _color('dodgerblue3', 0.6) CHILD = _color('darkorange2', 0.6) DAMPING = _color('springgreen', 0.5) STRUCTURE = _color('gray', 0.4) diff --git a/nhf/touhou/houjuu_nue/parts.py b/nhf/touhou/houjuu_nue/parts.py index a3a5bfb..bdf458e 100644 --- a/nhf/touhou/houjuu_nue/parts.py +++ b/nhf/touhou/houjuu_nue/parts.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Optional, Tuple import cadquery as Cq from nhf import Role from nhf.build import Model, target @@ -25,8 +26,12 @@ class DiskJoint(Model): spring_thickness: float = 1.3 spring_height: float = 6.5 spring_tail_length: float = 45.0 - spring_angle: float = 90.0 - spring_angle_shift: float = 0 + + # Spring angle at 0 degrees of movement + spring_angle: float = 30.0 + # Angle at which the spring exerts no torque + spring_angle_neutral: float = 90.0 + spring_angle_shift: float = 30 wall_inset: float = 2.0 # Angular span of movement @@ -55,15 +60,22 @@ class DiskJoint(Model): ) @property - def total_thickness(self): + def neutral_movement_angle(self) -> Optional[float]: + a = self.spring_angle_neutral - self.spring_angle + if 0 <= a and a <= self.movement_angle: + return a + return None + + @property + def total_thickness(self) -> float: return self.housing_thickness * 2 + self.disk_thickness @property - def opening_span(self): + def opening_span(self) -> float: return self.movement_angle + self.tongue_span @property - def housing_upper_carve_offset(self): + def housing_upper_carve_offset(self) -> float: return self.housing_thickness + self.disk_thickness - self.spring_height @property @@ -147,8 +159,7 @@ class DiskJoint(Model): result = result.cut( self .wall() - .mirror("XY") - .located(Cq.Location((0, 0, self.housing_thickness + self.disk_thickness))) + .located(Cq.Location((0, 0, self.disk_thickness - self.wall_inset))) #.rotate((0, 0, 0), (1, 0, 0), 180) #.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness))) ) @@ -165,7 +176,7 @@ class DiskJoint(Model): width=self.spring_thickness, height=self.housing_thickness ).located(Cq.Location((0, self.radius_spring_internal, 0)))) - ).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift) + ).rotate((0, 0, 0), (0, 0, 1), 180 + self.spring_angle - self.spring_angle_shift) result = ( Cq.Workplane('XY') .cylinder( @@ -192,7 +203,8 @@ class DiskJoint(Model): result = result.union(tube) wall = ( self.wall() - .rotate((0, 0, 0), (1, 0, 0), 180) + .rotate((0, 0, 0), (0, 0, 1), self.tongue_span) + .mirror("XY") .located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset))) ) result = ( @@ -202,18 +214,47 @@ class DiskJoint(Model): ) return result + def add_constraints(self, + assembly: Cq.Assembly, + housing_lower: str, + housing_upper: str, + disk: str, + angle: Tuple[float, float, float] = (0, 0, 0), + ) -> Cq.Assembly: + """ + The angle supplied must be perpendicular to the disk normal. + """ + ( + assembly + .constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane") + .constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane") + .constrain(f"{housing_lower}?dir", f"{housing_upper}?dir", "Axis") + .constrain(f"{disk}?dir", "FixedRotation", angle) + ) - def assembly(self) -> Cq.Assembly: + + def assembly(self, angle: Optional[float] = 0) -> Cq.Assembly: + if angle is None: + angle = self.movement_angle + if angle is None: + angle = 0 + else: + assert 0 <= angle <= self.movement_angle result = ( Cq.Assembly() .add(self.disk(), name="disk", color=Role.CHILD.color) .add(self.housing_lower(), name="housing_lower", color=Role.PARENT.color) - .add(self.housing_upper(), name="housing_upper", color=Role.PARENT.color) - .constrain("disk?mate_bot", "housing_lower?mate", "Plane") - .constrain("disk?mate_top", "housing_upper?mate", "Plane") - .solve() + .add(self.housing_upper(), name="housing_upper", color=Role.CASING.color) + .constrain("housing_lower", "Fixed") ) - return result + self.add_constraints( + result, + housing_lower="housing_lower", + housing_upper="housing_upper", + disk="disk", + angle=(0, 0, angle), + ) + return result.solve() if __name__ == '__main__': p = DiskJoint() diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 2f610d5..c5a9d27 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -1,8 +1,24 @@ import unittest import cadquery as Cq import nhf.touhou.houjuu_nue as M +import nhf.touhou.houjuu_nue.parts as MP from nhf.checks import pairwise_intersection +class TestDiskJoint(unittest.TestCase): + + def test_collision_0(self): + j = MP.DiskJoint() + assembly = j.assembly(angle=0) + self.assertEqual(pairwise_intersection(assembly), []) + def test_collision_mid(self): + j = MP.DiskJoint() + assembly = j.assembly(angle=j.movement_angle / 2) + self.assertEqual(pairwise_intersection(assembly), []) + def test_collision_max(self): + j = MP.DiskJoint() + assembly = j.assembly(angle=j.movement_angle) + self.assertEqual(pairwise_intersection(assembly), []) + class Test(unittest.TestCase): def test_hs_joint_parent(self): -- 2.44.1 From d43c1944a7ccc7b3fcc11301ca4061d2de6ecc68 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 11 Jul 2024 16:02:54 -0700 Subject: [PATCH 062/187] feat: Splined wing profile --- nhf/touhou/houjuu_nue/__init__.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 157 ++++++++++++++++++++++++------ nhf/utils.py | 29 ++++++ 3 files changed, 156 insertions(+), 32 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index dfccb5a..1eeb1e8 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -548,7 +548,7 @@ class Parameters(Model): @target(name="wing/r1s1", kind=TargetKind.DXF) def wing_r1s1_profile(self) -> Cq.Sketch: - return self.wing_profile.wing_r1_profile() + return self.wing_profile.profile() def wing_r1s1_panel(self, front=True) -> Cq.Workplane: profile = self.wing_r1s1_profile() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b6551b8..b7dcd7c 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -3,7 +3,9 @@ This file describes the shapes of the wing shells. The joints are defined in `__init__.py`. """ import math +from enum import Enum from dataclasses import dataclass +from typing import Mapping, Tuple import cadquery as Cq from nhf import Material, Role from nhf.parts.joints import HirthJoint @@ -235,19 +237,19 @@ class WingProfile: shoulder_height: float = 100 - elbow_height: float = 120 - elbow_x: float = 270 - elbow_y: float = 10 - # Angle of elbow w.r.t. y axis - elbow_angle: float = -20 + elbow_height: float = 100 + elbow_x: float = 240 + elbow_y: float = 30 + # Tilt of elbow w.r.t. shoulder + elbow_angle: float = 20 wrist_height: float = 70 # Bottom point of the wrist wrist_x: float = 400 wrist_y: float = 200 - # Angle of wrist w.r.t. y axis. should be negative - wrist_angle: float = -40 + # Tile of wrist w.r.t. shoulder + wrist_angle: float = 40 # Extends from the wrist to the tip of the arrow arrow_height: float = 300 @@ -261,21 +263,45 @@ class WingProfile: def __post_init__(self): assert self.ring_radius > self.ring_radius_inner + self.elbow_theta = math.radians(self.elbow_angle) + self.elbow_c = math.cos(self.elbow_theta) + self.elbow_s = math.sin(self.elbow_theta) + self.elbow_top_x, self.elbow_top_y = self.elbow_to_abs(0, self.elbow_height) + self.wrist_theta = math.radians(self.wrist_angle) + self.wrist_c = math.cos(self.wrist_theta) + self.wrist_s = math.sin(self.wrist_theta) + self.wrist_top_x, self.wrist_top_y = self.wrist_to_abs(0, self.wrist_height) + self.arrow_theta = math.radians(self.arrow_angle) + self.arrow_x, self.arrow_y = self.wrist_to_abs(0, -self.arrow_height) + self.arrow_tip_x = self.arrow_x + (self.arrow_height + self.wrist_height) \ + * math.sin(self.arrow_theta - self.wrist_theta) + self.arrow_tip_y = self.arrow_y + (self.arrow_height + self.wrist_height) \ + * math.cos(self.arrow_theta - self.wrist_theta) + # [[c, s], [-s, c]] * [ring_x, ring_y] + self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y + self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y + @property def ring_radius(self) -> float: dx = self.ring_x dy = self.ring_y return (dx * dx + dy * dy) ** 0.5 - def wing_r1_profile(self) -> Cq.Sketch: - wrist_theta = math.radians(self.wrist_angle) - wrist_s = math.sin(wrist_theta) - wrist_c = math.cos(wrist_theta) - wrist_top_x = self.wrist_x + self.wrist_height * wrist_s - wrist_top_y = self.wrist_y + self.wrist_height * wrist_c - elbow_theta = math.radians(self.elbow_angle) - elbow_top_x = self.elbow_x + self.elbow_height * math.sin(elbow_theta) - elbow_top_y = self.elbow_y + self.elbow_height * math.cos(elbow_theta) + def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]: + elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s + elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c + print(f"c={self.elbow_c}, s={self.elbow_s}, x={elbow_x}, y={elbow_y}") + return elbow_x, elbow_y + def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]: + wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s + wrist_y = self.wrist_y + x * self.wrist_s + y * self.wrist_c + return wrist_x, wrist_y + + + def profile(self) -> Cq.Sketch: + """ + Net profile of the wing starting from the wing root with no divisions + """ result = ( Cq.Sketch() .segment( @@ -284,8 +310,8 @@ class WingProfile: tag="shoulder") .arc( (0, self.shoulder_height), - (elbow_top_x, elbow_top_y), - (wrist_top_x, wrist_top_y), + (self.elbow_top_x, self.elbow_top_y), + (self.wrist_top_x, self.wrist_top_y), tag="s1_top") #.segment( # (self.wrist_x, self.wrist_y), @@ -297,39 +323,108 @@ class WingProfile: (self.wrist_x, self.wrist_y), tag="s1_bot") ) - arrow_theta = math.radians(self.arrow_angle) - arrow_x = self.wrist_x - self.arrow_height * wrist_s - arrow_y = self.wrist_y - self.arrow_height * wrist_c - arrow_tip_x = arrow_x + (self.arrow_height + self.wrist_height) * math.sin(arrow_theta + wrist_theta) - arrow_tip_y = arrow_y + (self.arrow_height + self.wrist_height) * math.cos(arrow_theta + wrist_theta) result = ( result .segment( (self.wrist_x, self.wrist_y), - (arrow_x, arrow_y) + (self.arrow_x, self.arrow_y) ) .segment( - (arrow_x, arrow_y), - (arrow_tip_x, arrow_tip_y) + (self.arrow_x, self.arrow_y), + (self.arrow_tip_x, self.arrow_tip_y) ) .segment( - (arrow_tip_x, arrow_tip_y), - (wrist_top_x, wrist_top_y) + (self.arrow_tip_x, self.arrow_tip_y), + (self.wrist_top_x, self.wrist_top_y) ) ) # Carve out the ring result = result.assemble() - ring_x = wrist_top_x + wrist_c * self.ring_x + wrist_s * self.ring_y - ring_y = wrist_top_y - wrist_s * self.ring_x + wrist_c * self.ring_y result = ( result - .push([(ring_x, ring_y)]) + .push([(self.ring_abs_x, self.ring_abs_y)]) .circle(self.ring_radius, mode='a') .circle(self.ring_radius_inner, mode='s') .clean() ) return result + def _mask_elbow(self) -> list[Tuple[float, float]]: + """ + Polygon shape to mask out parts above the elbow + """ + abscissa = 200 + return [ + (0, -abscissa), + (self.elbow_x, self.elbow_y), + (self.elbow_top_x, self.elbow_top_y), + (0, abscissa) + ] + + def _mask_wrist(self) -> list[Tuple[float, float]]: + abscissa = 200 + return [ + (0, -abscissa), + (self.wrist_x - self.wrist_s * abscissa, + self.wrist_y - self.wrist_c * abscissa), + (self.wrist_top_x, self.wrist_top_y), + (0, abscissa), + ] + + + def profile_s1(self) -> Cq.Sketch: + profile = ( + self.profile() + .reset() + .polygon(self._mask_elbow(), mode='i') + ) + return profile + def surface_s1(self, + thickness:float = 25.4/16, + shoulder_mount_inset: float=20, + shoulder_joint_child_height: float=80, + elbow_mount_inset: float=20, + elbow_joint_parent_height: float=60, + front: bool=True) -> Cq.Workplane: + assert shoulder_joint_child_height < self.shoulder_height + assert elbow_joint_parent_height < self.elbow_height + h = (self.shoulder_height - shoulder_joint_child_height) / 2 + tags_shoulder = [ + ("shoulder_bot", (shoulder_mount_inset, h), 90), + ("shoulder_top", (shoulder_mount_inset, h + shoulder_joint_child_height), 270), + ] + h = (self.elbow_height - elbow_joint_parent_height) / 2 + tags_elbow = [ + ("elbow_bot", + self.elbow_to_abs(-elbow_mount_inset, h), + self.elbow_angle + 90), + ("elbow_top", + self.elbow_to_abs(-elbow_mount_inset, h + elbow_joint_parent_height), + self.elbow_angle + 270), + ] + profile = self.profile_s1() + tags = tags_shoulder + tags_elbow + return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) + + + def profile_s2(self) -> Cq.Sketch: + profile = ( + self.profile() + .reset() + .polygon(self._mask_wrist(), mode='i') + .reset() + .polygon(self._mask_elbow(), mode='s') + ) + return profile + def profile_s3(self) -> Cq.Sketch: + profile = ( + self.profile() + .reset() + .polygon(self._mask_wrist(), mode='s') + ) + return profile + + def wing_r1s1_profile(self) -> Cq.Sketch: """ Generates the first wing segment profile, with the wing root pointing in diff --git a/nhf/utils.py b/nhf/utils.py index f375c63..6d1dc8b 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -5,6 +5,7 @@ Adds the functions to `Cq.Workplane`: 1. `tagPoint` 2. `tagPlane` """ +import math import cadquery as Cq from typing import Union, Tuple @@ -50,3 +51,31 @@ def tagPlane(self, tag: str, self.eachpoint(edge.moved, useLocalCoordinates=True).tag(tag) Cq.Workplane.tagPlane = tagPlane + +def extrude_with_markers(sketch: Cq.Sketch, + thickness: float, + tags: list[Tuple[str, Tuple[float, float], float]], + reverse: bool = False): + """ + Extrudes a sketch and place tags on the sketch for mating. + + Each tag is of the format `(name, (x, y), angle)`, where the angle is + specifies in degrees counterclockwise from +X. Two marks are generated for + each `name`, "{name}" for the location (with normal) and "{name}_dir" for + the directrix specified by the angle. + + This simulates a process of laser cutting and bonding (for wood and acrylic) + """ + result = ( + Cq.Workplane('XY') + .placeSketch(sketch) + .extrude(thickness) + ) + plane = result.faces("Z").workplane() + sign = -1 if reverse else 1 + for tag, (px, py), angle in tag: + theta = sign * math.radians(angle) + direction = (math.cos(theta), math.sin(theta), 0) + plane.moveTo(px, sign * py).tagPlane(tag) + plane.moveTo(px, sign * py).tagPlane(f"{tag}_dir", direction) + return result -- 2.44.1 From 9f9946569d50b72cb55c4e27e9e2147a3ed93b74 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 11 Jul 2024 22:29:05 -0700 Subject: [PATCH 063/187] feat: Elbow joint --- nhf/build.py | 4 +- nhf/touhou/houjuu_nue/parts.py | 185 +++++++++++++++++++++++++++++++-- nhf/touhou/houjuu_nue/wing.py | 1 - 3 files changed, 179 insertions(+), 11 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index dcfbba7..9a16a1d 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -23,6 +23,8 @@ from colorama import Fore, Style import cadquery as Cq import nhf.checks as NC +TOL=1e-6 + class TargetKind(Enum): STL = "stl", @@ -70,7 +72,7 @@ class Target: if isinstance(x, Cq.Workplane): x = x.val() if isinstance(x, Cq.Assembly): - x = x.toCompound() + x = x.toCompound().fuse(tol=TOL) x.exportStl(path, **self.kwargs) elif self.kind == TargetKind.DXF: assert isinstance(x, Cq.Workplane) diff --git a/nhf/touhou/houjuu_nue/parts.py b/nhf/touhou/houjuu_nue/parts.py index bdf458e..600350a 100644 --- a/nhf/touhou/houjuu_nue/parts.py +++ b/nhf/touhou/houjuu_nue/parts.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional, Tuple import cadquery as Cq from nhf import Role @@ -8,6 +8,67 @@ import nhf.utils TOL = 1e-6 +@dataclass +class Beam: + """ + A I-shaped spine with two feet + """ + + foot_length: float = 40.0 + foot_width: float = 20.0 + foot_height: float = 5.0 + spine_thickness: float = 4.0 + spine_length: float = 10.0 + total_height: float = 50.0 + + hole_diam: float = 8.0 + # distance between the centres of the two holes + hole_dist: float = 24.0 + + def __post_init__(self): + assert self.spine_height > 0 + assert self.hole_diam + self.hole_dist < self.foot_length + assert self.hole_dist - self.hole_diam >= self.spine_length + + @property + def spine_height(self): + return self.total_height - self.foot_height * 2 + + def foot(self) -> Cq.Workplane: + """ + just one foot + """ + dx = self.hole_dist / 2 + result = ( + Cq.Workplane('XZ') + .box(self.foot_length, self.foot_width, self.foot_height, + centered=(True, True, False)) + .faces(">Y") + .workplane() + .pushPoints([(dx, 0), (-dx, 0)]) + .hole(self.hole_diam) + ) + plane = result.faces(">Y").workplane() + plane.moveTo(dx, 0).tagPlane("conn1") + plane.moveTo(-dx, 0).tagPlane("conn0") + return result + + def beam(self) -> Cq.Assembly: + beam = ( + Cq.Workplane('XZ') + .box(self.spine_length, self.spine_thickness, self.spine_height) + ) + h = self.spine_height / 2 + self.foot_height + result = ( + Cq.Assembly() + .add(beam, name="beam") + .add(self.foot(), name="top", + loc=Cq.Location((0, h, 0))) + .add(self.foot(), name="bot", + loc=Cq.Location((0, -h, 0), (0, 0, 1), 180)) + ) + return result + @dataclass class DiskJoint(Model): """ @@ -76,8 +137,18 @@ class DiskJoint(Model): @property def housing_upper_carve_offset(self) -> float: + """ + Distance between the spring track and the outside of the upper housing + """ return self.housing_thickness + self.disk_thickness - self.spring_height + @property + def housing_upper_dz(self) -> float: + """ + Distance between the default upper housing location and the median line + """ + return self.total_thickness / 2 - self.housing_thickness + @property def radius_spring_internal(self): return self.radius_spring - self.spring_thickness @@ -175,8 +246,8 @@ class DiskJoint(Model): length=self.spring_tail_length, width=self.spring_thickness, height=self.housing_thickness - ).located(Cq.Location((0, self.radius_spring_internal, 0)))) - ).rotate((0, 0, 0), (0, 0, 1), 180 + self.spring_angle - self.spring_angle_shift) + ).located(Cq.Location((0, -self.radius_spring, 0)))) + ).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift) result = ( Cq.Workplane('XY') .cylinder( @@ -185,8 +256,8 @@ class DiskJoint(Model): centered=(True, True, False), ) ) - result.faces(">Z").tag("mate") - result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") + result.faces("Z").hole(self.radius_axle * 2) # tube which holds the spring interior @@ -203,14 +274,12 @@ class DiskJoint(Model): result = result.union(tube) wall = ( self.wall() - .rotate((0, 0, 0), (0, 0, 1), self.tongue_span) - .mirror("XY") - .located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness + self.wall_inset))) + .located(Cq.Location((0, 0, -self.disk_thickness-self.wall_inset))) ) result = ( result + .cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset)))) .union(wall, tol=TOL) - .cut(carve.located(Cq.Location((0, 0, self.housing_upper_carve_offset)))) ) return result @@ -256,6 +325,104 @@ class DiskJoint(Model): ) return result.solve() +@dataclass +class ElbowJoint: + """ + Creates the elbow and wrist joints. + + This consists of a disk joint, where each side of the joint has mounting + holes for connection to the exoskeleton. Each side 2 mounting feet on the + top and bottom, and each foot has 2 holes. + + On the parent side, additional bolts are needed to clamp the two sides of + the housing together. + """ + + disk_joint: DiskJoint = field(default_factory=lambda: DiskJoint( + movement_angle=60, + )) + + # Distance between the child/parent arm to the centre + child_arm_radius: float = 40.0 + parent_arm_radius: float = 40.0 + + child_beam: Beam = field(default_factory=lambda: Beam()) + parent_beam: Beam = field(default_factory=lambda: Beam( + spine_thickness=8.0, + )) + parent_arm_span: float = 40.0 + # Angle of the beginning of the parent arm + parent_arm_angle: float = 180.0 + parent_binding_hole_radius: float = 30.0 + + # Size of the mounting holes + hole_diam: float = 8.0 + + def __post_init__(self): + assert self.child_arm_radius > self.disk_joint.radius_housing + assert self.parent_arm_radius > self.disk_joint.radius_housing + self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk + assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span + assert self.parent_binding_hole_radius - self.hole_diam / 2 > self.disk_joint.radius_housing + + def child_joint(self) -> Cq.Assembly: + angle = -self.disk_joint.tongue_span / 2 + dz = self.disk_joint.disk_thickness / 2 + result = ( + self.child_beam.beam() + .add(self.disk_joint.disk(), name="disk", + loc=Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) + ) + return result + + def parent_joint_bot(self) -> Cq.Workplane: + return self.disk_joint.housing_lower() + + def parent_joint_top(self): + axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) + housing_dz = self.disk_joint.housing_upper_dz + conn_h = self.parent_beam.spine_thickness + connector = ( + Cq.Solid.makeCylinder( + height=conn_h, + radius=self.parent_arm_radius, + angleDegrees=self.parent_arm_span) + .cut(Cq.Solid.makeCylinder( + height=conn_h, + radius=self.disk_joint.radius_housing, + )) + .located(Cq.Location((0, 0, -conn_h / 2))) + .rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2) + ) + housing = self.disk_joint.housing_upper() + result = ( + Cq.Assembly() + .add(housing, name="housing", + loc=axial_offset * Cq.Location((0, 0, housing_dz))) + .add(connector, name="connector", + loc=axial_offset) + .add(self.parent_beam.beam(), name="beam") + ) + return result + + def assembly(self, angle: float = 0) -> Cq.Assembly: + da = self.disk_joint.tongue_span / 2 + result = ( + Cq.Assembly() + .add(self.child_joint(), name="child", color=Role.CHILD.color) + .add(self.parent_joint_bot(), name="parent_bot", color=Role.CASING.color) + .add(self.parent_joint_top(), name="parent_top", color=Role.PARENT.color) + .constrain("parent_bot", "Fixed") + ) + self.disk_joint.add_constraints( + result, + housing_lower="parent_bot", + housing_upper="parent_top/housing", + disk="child/disk", + angle=(0, 0, angle + da), + ) + return result.solve() + if __name__ == '__main__': p = DiskJoint() p.build_all() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b7dcd7c..feb57a8 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -290,7 +290,6 @@ class WingProfile: def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]: elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c - print(f"c={self.elbow_c}, s={self.elbow_s}, x={elbow_x}, y={elbow_y}") return elbow_x, elbow_y def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]: wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s -- 2.44.1 From 9f41f2ea3c7f719ac80e52d2a8ad2d911b79552d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 12 Jul 2024 11:04:28 -0700 Subject: [PATCH 064/187] feat: Wing anchors for right side --- nhf/touhou/houjuu_nue/wing.py | 59 +++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index feb57a8..d39ca76 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -379,12 +379,12 @@ class WingProfile: ) return profile def surface_s1(self, - thickness:float = 25.4/16, - shoulder_mount_inset: float=20, - shoulder_joint_child_height: float=80, - elbow_mount_inset: float=20, - elbow_joint_parent_height: float=60, - front: bool=True) -> Cq.Workplane: + thickness: float = 25.4/16, + shoulder_mount_inset: float = 20, + shoulder_joint_child_height: float = 80, + elbow_mount_inset: float = 20, + elbow_joint_parent_height: float = 60, + front: bool = True) -> Cq.Workplane: assert shoulder_joint_child_height < self.shoulder_height assert elbow_joint_parent_height < self.elbow_height h = (self.shoulder_height - shoulder_joint_child_height) / 2 @@ -415,6 +415,36 @@ class WingProfile: .polygon(self._mask_elbow(), mode='s') ) return profile + def surface_s2(self, + thickness: float = 25.4/16, + elbow_mount_inset: float = 20, + elbow_joint_child_height: float = 80, + wrist_mount_inset: float = 20, + wrist_joint_parent_height: float = 60, + front: bool = True) -> Cq.Workplane: + assert elbow_joint_child_height < self.elbow_height + h = (self.elbow_height - elbow_joint_child_height) / 2 + tags_elbow = [ + ("elbow_bot", + self.elbow_to_abs(elbow_mount_inset, h), + self.elbow_angle + 90), + ("elbow_top", + self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height), + self.elbow_angle + 270), + ] + h = (self.wrist_height - wrist_joint_parent_height) / 2 + tags_wrist = [ + ("wrist_bot", + self.wrist_to_abs(-wrist_mount_inset, h), + self.wrist_angle + 90), + ("wrist_top", + self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height), + self.wrist_angle + 270), + ] + profile = self.profile_s2() + tags = tags_elbow + tags_wrist + return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) + def profile_s3(self) -> Cq.Sketch: profile = ( self.profile() @@ -422,6 +452,23 @@ class WingProfile: .polygon(self._mask_wrist(), mode='s') ) return profile + def surface_s3(self, + thickness: float = 25.4/16, + wrist_mount_inset: float = 20, + wrist_joint_child_height: float = 80, + front: bool = True) -> Cq.Workplane: + assert wrist_joint_child_height < self.wrist_height + h = (self.wrist_height - wrist_joint_child_height) / 2 + tags = [ + ("wrist_bot", + self.elbow_to_abs(wrist_mount_inset, h), + self.elbow_angle + 90), + ("wrist_top", + self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height), + self.elbow_angle + 270), + ] + profile = self.profile_s3() + return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) def wing_r1s1_profile(self) -> Cq.Sketch: -- 2.44.1 From 641755314eb8cef17caf56c96ddb767279a15ebf Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 12 Jul 2024 23:16:04 -0700 Subject: [PATCH 065/187] refactor: Factor out parts of the wing assembly --- nhf/materials.py | 1 + nhf/touhou/houjuu_nue/__init__.py | 430 ++++++++---------- nhf/touhou/houjuu_nue/{parts.py => joints.py} | 220 ++++++++- nhf/touhou/houjuu_nue/test.py | 20 +- nhf/touhou/houjuu_nue/wing.py | 10 +- nhf/utils.py | 55 ++- 6 files changed, 469 insertions(+), 267 deletions(-) rename nhf/touhou/houjuu_nue/{parts.py => joints.py} (64%) diff --git a/nhf/materials.py b/nhf/materials.py index 6108dc1..a95a7eb 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -21,6 +21,7 @@ class Role(Enum): STRUCTURE = _color('gray', 0.4) DECORATION = _color('lightseagreen', 0.4) ELECTRONIC = _color('mediumorchid', 0.5) + MARKER = _color('white', 1.0) def __init__(self, color: Cq.Color): self.color = color diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 1eeb1e8..7cdba60 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -37,6 +37,7 @@ 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 import nhf.utils @dataclass @@ -88,33 +89,21 @@ class Parameters(Model): hs_joint_axis_cbore_depth: float = 3 wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( - shoulder_height = 80, - elbow_height = 100, + shoulder_height=100.0, + elbow_height=110.0, )) # Exterior radius of the wing root assembly wing_root_radius: float = 40 wing_root_wall_thickness: float = 8 - shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( - radius_track=18, - radius_rider=18, - groove_radius_outer=16, - groove_radius_inner=13, - track_disk_height=5.0, - rider_disk_height=5.0, - # M8 Axle - radius_axle=3.0, - # inner diameter = 9 - radius_spring=9/2 + 1.2, - spring_thickness=1.3, - spring_height=7.5, + shoulder_joint: MJ.ShoulderJoint = field(default_factory=lambda: MJ.ShoulderJoint( + shoulder_height=100.0, + )) + elbow_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint( + )) + wrist_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint( )) - - # Two holes on each side (top and bottom) are used to attach the shoulder - # joint. This governs the distance between these two holes - shoulder_attach_dist: float = 25 - shoulder_attach_diam: float = 8 """ Heights for various wing joints, where the numbers start from the first @@ -325,8 +314,8 @@ class Parameters(Model): """ return MW.wing_root( joint=self.hs_hirth_joint, - shoulder_attach_dist=self.shoulder_attach_dist, - shoulder_attach_diam=self.shoulder_attach_diam, + shoulder_attach_dist=self.shoulder_joint.attach_dist, + shoulder_attach_diam=self.shoulder_joint.attach_diam, wall_thickness=self.wing_root_wall_thickness, conn_height=self.wing_profile.shoulder_height, conn_thickness=self.wing_s0_thickness, @@ -334,169 +323,25 @@ class Parameters(Model): @target(name="wing/proto-shoulder-joint-parent", prototype=True) def proto_shoulder_joint_parent(self): - return self.shoulder_torsion_joint.track() + return self.shoulder_joint.torsion_joint.track() @target(name="wing/proto-shoulder-joint-child", prototype=True) def proto_shoulder_joint_child(self): - return self.shoulder_torsion_joint.rider() - - @target(name="wing/shoulder_joint_parent") - def shoulder_joint_parent(self) -> Cq.Workplane: - joint = self.shoulder_torsion_joint - # Thickness of the lip connecting this joint to the wing root - lip_thickness = 10 - lip_width = 25 - lip_guard_ext = 40 - lip_guard_height = self.wing_root_wall_thickness + lip_thickness - assert lip_guard_ext > joint.radius_track - - lip_guard = ( - Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height) - .located(Cq.Location((0, -lip_width/2 , 0))) - .cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height)) - ) - result = ( - joint.track() - .union(lip_guard, tol=1e-6) - - # Extrude the handle - .copyWorkplane(Cq.Workplane( - 'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness)))) - .rect(lip_width, lip_thickness, centered=(True, False)) - .extrude("next") - - # Connector holes on the lip - .copyWorkplane(Cq.Workplane( - 'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness)))) - .hole(self.shoulder_attach_diam) - .moveTo(0, self.shoulder_attach_dist) - .hole(self.shoulder_attach_diam) - ) - result.moveTo(0, 0).tagPlane('conn0') - result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1') - return result - - @property - def shoulder_joint_child_height(self) -> float: - """ - Calculates the y distance between two joint surfaces on the child side - of the shoulder joint. - """ - joint = self.shoulder_torsion_joint - return self.wing_profile.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height - - @target(name="wing/shoulder_joint_child") - def shoulder_joint_child(self) -> Cq.Assembly: - """ - Creates the top/bottom shoulder child joint - """ - - joint = self.shoulder_torsion_joint - # Half of the height of the bridging cylinder - dh = self.wing_profile.shoulder_height / 2 - joint.total_height - core_start_angle = 30 - core_end_angle1 = 90 - core_end_angle2 = 180 - core_thickness = 2 - - core_profile1 = ( - Cq.Sketch() - .arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) - .segment((0, 0)) - .close() - .assemble() - .circle(joint.radius_rider - core_thickness, mode='s') - ) - core_profile2 = ( - Cq.Sketch() - .arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle)) - .segment((0, 0)) - .close() - .assemble() - .circle(joint.radius_rider - core_thickness, mode='s') - ) - core = ( - Cq.Workplane('XY') - .placeSketch(core_profile1) - .toPending() - .extrude(dh * 2) - .copyWorkplane(Cq.Workplane('XY')) - .placeSketch(core_profile2) - .toPending() - .extrude(dh * 2) - .translate(Cq.Vector(0, 0, -dh)) - ) - # Create the upper and lower lips - lip_height = self.wing_s1_thickness - lip_thickness = joint.rider_disk_height - lip_ext = 40 + joint.radius_rider - hole_dx = self.wing_s1_shoulder_spacer_hole_dist - assert lip_height / 2 <= joint.radius_rider - lip = ( - Cq.Workplane('XY') - .box(lip_ext, lip_height, lip_thickness, - centered=(False, True, False)) - .copyWorkplane(Cq.Workplane('XY')) - .cylinder(radius=joint.radius_rider, height=lip_thickness, - centered=(True, True, False), - combine='cut') - .faces(">Z") - .workplane() - ) - hole_x = lip_ext - hole_dx / 2 - for i in range(2): - x = hole_x - i * hole_dx - lip = lip.moveTo(x, 0).hole(self.wing_s1_spacer_hole_diam) - for i in range(2): - x = hole_x - i * hole_dx - ( - lip - .moveTo(x, 0) - .tagPlane(f"conn{1 - i}") - ) - - loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) - result = ( - Cq.Assembly() - .add(core, name="core", loc=Cq.Location()) - .add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top", - loc=Cq.Location((0, 0, dh), (0, 0, 1), -90)) - .add(joint.rider(rider_slot_begin=180), name="rider_bot", - loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) - .add(lip, name="lip_top", - loc=Cq.Location((0, 0, dh))) - .add(lip, name="lip_bot", - loc=Cq.Location((0, 0, -dh)) * loc_rotate) - ) - return result - + return self.shoulder_joint.torsion_joint.rider() @assembly() - def shoulder_assembly(self) -> Cq.Assembly: - directrix = 0 - result = ( - Cq.Assembly() - .add(self.shoulder_joint_child(), name="child", - color=Role.CHILD.color) - .constrain("child/core", "Fixed") - .add(self.shoulder_torsion_joint.spring(), name="spring_top", - color=Role.DAMPING.color) - .add(self.shoulder_joint_parent(), name="parent_top", - color=Role.PARENT.color) - .add(self.shoulder_torsion_joint.spring(), name="spring_bot", - color=Role.DAMPING.color) - .add(self.shoulder_joint_parent(), name="parent_bot", - color=Role.PARENT.color) + def shoulder_assembly(self): + return self.shoulder_joint.assembly( + wing_root_wall_thickness=self.wing_root_wall_thickness, + lip_height=self.wing_s1_thickness, + hole_dist=self.wing_s1_shoulder_spacer_hole_dist, + spacer_hole_diam=self.wing_s1_spacer_hole_diam, ) - TorsionJoint.add_constraints(result, - rider="child/rider_top", - track="parent_top", - spring="spring_top", - directrix=directrix) - TorsionJoint.add_constraints(result, - rider="child/rider_bot", - track="parent_bot", - spring="spring_bot", - directrix=directrix) - return result.solve() + @assembly() + def elbow_assembly(self): + return self.elbow_joint.assembly() + @assembly() + def wrist_assembly(self): + return self.wrist_joint.assembly() + @target(name="wing/s1-spacer", kind=TargetKind.DXF) def wing_s1_spacer(self) -> Cq.Workplane: @@ -515,61 +360,93 @@ class Parameters(Model): @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) def wing_s1_shoulder_spacer(self) -> Cq.Workplane: """ - The mate tags are on the side closer to the holes. + Creates a rectangular spacer. This could be cut from acrylic. + + There are two holes on the top of the spacer. With the holes """ dx = self.wing_s1_shoulder_spacer_hole_dist h = self.wing_s1_spacer_thickness + length = self.wing_s1_shoulder_spacer_width + hole_diam = self.wing_s1_spacer_hole_diam + assert dx + hole_diam < length / 2 result = ( - Cq.Workplane('XZ') + Cq.Workplane('XY') .sketch() - .rect(self.wing_s1_shoulder_spacer_width, - self.wing_s1_thickness) + .rect(length, self.wing_s1_thickness) .push([ (0, 0), (dx, 0), ]) - .circle(self.wing_s1_spacer_hole_diam / 2, mode='s') + .circle(hole_diam / 2, mode='s') .finalize() .extrude(h) ) # Tag the mating surfaces to be glued - result.faces("Z").workplane().moveTo(0, -h).tagPlane("weld2") + result.faces("Y").workplane().moveTo(-length / 2, h).tagPlane("right") # Tag the directrix - result.faces("Z").tag("dir") # Tag the holes - plane = result.faces(">Y").workplane() + plane = result.faces(">Z").workplane() # Side closer to the parent is 0 - plane.moveTo(-dx, 0).tagPlane("conn0") + plane.moveTo(dx, 0).tagPlane("conn0") plane.tagPlane("conn1") return result + def assembly_insert_shoulder_spacer( + self, + assembly, + spacer, + point_tag: str, + front_tag: str = "panel_front", + back_tag: str = "panel_back", + flipped: bool = False, + ): + """ + For a child joint facing up, front panel should be on the right, back + panel on the left + """ + site_front, site_back = "right", "left" + if flipped: + site_front, site_back = site_back, site_front + angle = 0 + ( + assembly + .add(spacer, + name=f"{point_tag}_spacer", + color=self.material_bracket.color) + .constrain(f"{front_tag}?{point_tag}", + f"{point_tag}_spacer?{site_front}", "Plane") + .constrain(f"{back_tag}?{point_tag}", + f"{point_tag}_spacer?{site_back}", "Plane") + .constrain(f"{point_tag}_spacer?dir", f"{front_tag}?{point_tag}_dir", + "Axis", param=angle) + ) @target(name="wing/r1s1", kind=TargetKind.DXF) def wing_r1s1_profile(self) -> Cq.Sketch: + """ + FIXME: Output individual segment profiles + """ return self.wing_profile.profile() def wing_r1s1_panel(self, front=True) -> Cq.Workplane: - profile = self.wing_r1s1_profile() - w = self.wing_s1_shoulder_spacer_width / 2 - h = (self.wing_profile.shoulder_height - self.shoulder_joint_child_height) / 2 - anchors = [ - ("shoulder_top", w, h + self.shoulder_joint_child_height), - ("shoulder_bot", w, h), - ("middle", 50, -20), - ("tip", 270, 50), - ] - result = ( - Cq.Workplane("XY") - .placeSketch(profile) - .extrude(self.panel_thickness) + return self.wing_profile.surface_s1( + thickness=self.panel_thickness, + shoulder_joint_child_height=self.shoulder_joint.child_height, + front=front, + ) + def wing_r1s2_panel(self, front=True) -> Cq.Workplane: + return self.wing_profile.surface_s2( + thickness=self.panel_thickness, + front=front, + ) + def wing_r1s3_panel(self, front=True) -> Cq.Workplane: + return self.wing_profile.surface_s3( + thickness=self.panel_thickness, + front=front, ) - plane = result.faces(">Z" if front else " Cq.Assembly: @@ -582,56 +459,86 @@ class Parameters(Model): color=self.material_panel.color) .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.wing_r1s2_panel(front=True), name="panel_front", + color=self.material_panel.color) + .constrain("panel_front", "Fixed") + .add(self.wing_r1s2_panel(front=False), name="panel_back", + color=self.material_panel.color) + # FIXME: Use s2 thickness + .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.wing_r1s3_panel(front=True), name="panel_front", + color=self.material_panel.color) + .constrain("panel_front", "Fixed") + .add(self.wing_r1s3_panel(front=False), name="panel_back", + color=self.material_panel.color) + # FIXME: Use s2 thickness + .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: + def wing_r1_assembly( + self, + parts=["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"], + ) -> Cq.Assembly: result = ( Cq.Assembly() ) - if "root" in parts: + if "s0" in parts: ( result - .add(self.wing_root(), name="root") - .constrain("root/scaffold", "Fixed") + .add(self.wing_root(), name="s0") + .constrain("s0/scaffold", "Fixed") ) if "shoulder" in parts: result.add(self.shoulder_assembly(), name="shoulder") - if "root" in parts and "shoulder" in parts: + if "s0" in parts and "shoulder" in parts: ( result - .constrain("root/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") - .constrain("root/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") - .constrain("root/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") - .constrain("root/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") + .constrain("s0/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") + .constrain("s0/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") + .constrain("s0/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") + .constrain("s0/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") ) if "s1" in parts: @@ -653,6 +560,45 @@ class Parameters(Model): "s1/shoulder_top_spacer?conn1", "Plane") ) + if "elbow" in parts: + result.add(self.elbow_assembly(), name="elbow") + + if "s2" in parts: + result.add(self.wing_r1s2_assembly(), name="s2") + + if "s1" in parts and "elbow" in parts: + ( + result + .constrain("elbow/parent_upper/top?conn1", + "s1/elbow_top_spacer?conn1", + "Plane") + .constrain("elbow/parent_upper/top?conn0", + "s1/elbow_top_spacer?conn0", + "Plane") + .constrain("elbow/parent_upper/bot?conn1", + "s1/elbow_bot_spacer?conn1", + "Plane") + .constrain("elbow/parent_upper/bot?conn0", + "s1/elbow_bot_spacer?conn0", + "Plane") + ) + + if "s2" in parts and "elbow" in parts: + ( + result + .constrain("elbow/child/bot?conn0", + "s2/elbow_bot_spacer?conn0", + "Plane") + .constrain("elbow/child/bot?conn1", + "s2/elbow_bot_spacer?conn1", + "Plane") + .constrain("elbow/child/top?conn0", + "s2/elbow_top_spacer?conn0", + "Plane") + .constrain("elbow/child/top?conn1", + "s2/elbow_top_spacer?conn1", + "Plane") + ) return result.solve() @assembly() diff --git a/nhf/touhou/houjuu_nue/parts.py b/nhf/touhou/houjuu_nue/joints.py similarity index 64% rename from nhf/touhou/houjuu_nue/parts.py rename to nhf/touhou/houjuu_nue/joints.py index 600350a..6e653af 100644 --- a/nhf/touhou/houjuu_nue/parts.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -2,12 +2,209 @@ from dataclasses import dataclass, field from typing import Optional, Tuple import cadquery as Cq from nhf import Role -from nhf.build import Model, target +from nhf.build import Model, target, assembly import nhf.parts.springs as springs +from nhf.parts.joints import TorsionJoint import nhf.utils TOL = 1e-6 +@dataclass +class ShoulderJoint(Model): + + shoulder_height: float = 100.0 + torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( + radius_track=18, + radius_rider=18, + groove_radius_outer=16, + groove_radius_inner=13, + track_disk_height=5.0, + rider_disk_height=5.0, + # M8 Axle + radius_axle=3.0, + # inner diameter = 9 + radius_spring=9/2 + 1.2, + spring_thickness=1.3, + spring_height=7.5, + )) + # Two holes on each side (top and bottom) are used to attach the shoulder + # joint. This governs the distance between these two holes + attach_dist: float = 25 + attach_diam: float = 8 + + + @target(name="shoulder-joint/parent") + def parent(self, + wing_root_wall_thickness: float = 5.0) -> Cq.Workplane: + joint = self.torsion_joint + # Thickness of the lip connecting this joint to the wing root + lip_thickness = 10 + lip_width = 25 + lip_guard_ext = 40 + lip_guard_height = wing_root_wall_thickness + lip_thickness + assert lip_guard_ext > joint.radius_track + + lip_guard = ( + Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height) + .located(Cq.Location((0, -lip_width/2 , 0))) + .cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height)) + ) + result = ( + joint.track() + .union(lip_guard, tol=1e-6) + + # Extrude the handle + .copyWorkplane(Cq.Workplane( + 'YZ', origin=Cq.Vector((88, 0, wing_root_wall_thickness)))) + .rect(lip_width, lip_thickness, centered=(True, False)) + .extrude("next") + + # Connector holes on the lip + .copyWorkplane(Cq.Workplane( + 'YX', origin=Cq.Vector((57, 0, wing_root_wall_thickness)))) + .hole(self.attach_diam) + .moveTo(0, self.attach_dist) + .hole(self.attach_diam) + ) + result.moveTo(0, 0).tagPlane('conn0') + result.moveTo(0, self.attach_dist).tagPlane('conn1') + return result + + @property + def child_height(self) -> float: + """ + Calculates the y distance between two joint surfaces on the child side + of the shoulder joint. + """ + joint = self.torsion_joint + return self.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height + + @target(name="shoulder-joint/child") + def child(self, + lip_height: float = 20.0, + hole_dist: float = 10.0, + spacer_hole_diam: float = 8.0) -> Cq.Assembly: + """ + Creates the top/bottom shoulder child joint + """ + + joint = self.torsion_joint + # Half of the height of the bridging cylinder + dh = self.shoulder_height / 2 - joint.total_height + core_start_angle = 30 + core_end_angle1 = 90 + core_end_angle2 = 180 + core_thickness = 2 + + core_profile1 = ( + Cq.Sketch() + .arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) + .segment((0, 0)) + .close() + .assemble() + .circle(joint.radius_rider - core_thickness, mode='s') + ) + core_profile2 = ( + Cq.Sketch() + .arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle)) + .segment((0, 0)) + .close() + .assemble() + .circle(joint.radius_rider - core_thickness, mode='s') + ) + core = ( + Cq.Workplane('XY') + .placeSketch(core_profile1) + .toPending() + .extrude(dh * 2) + .copyWorkplane(Cq.Workplane('XY')) + .placeSketch(core_profile2) + .toPending() + .extrude(dh * 2) + .translate(Cq.Vector(0, 0, -dh)) + ) + # Create the upper and lower lips + lip_thickness = joint.rider_disk_height + lip_ext = 40 + joint.radius_rider + assert lip_height / 2 <= joint.radius_rider + lip = ( + Cq.Workplane('XY') + .box(lip_ext, lip_height, lip_thickness, + centered=(False, True, False)) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder(radius=joint.radius_rider, height=lip_thickness, + centered=(True, True, False), + combine='cut') + .faces(">Z") + .workplane() + ) + hole_x = lip_ext - hole_dist / 2 + for i in range(2): + x = hole_x - i * hole_dist + lip = lip.moveTo(x, 0).hole(spacer_hole_diam) + for i in range(2): + x = hole_x - i * hole_dist + ( + lip + .moveTo(x, 0) + .tagPlane(f"conn{1 - i}") + ) + + loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) + result = ( + Cq.Assembly() + .add(core, name="core", loc=Cq.Location()) + .add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top", + loc=Cq.Location((0, 0, dh), (0, 0, 1), -90)) + .add(joint.rider(rider_slot_begin=180), name="rider_bot", + loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) + .add(lip, name="lip_top", + loc=Cq.Location((0, 0, dh))) + .add(lip, name="lip_bot", + loc=Cq.Location((0, 0, -dh)) * loc_rotate) + ) + return result + + @assembly() + def assembly(self, + wing_root_wall_thickness: float = 5.0, + lip_height: float = 5.0, + hole_dist: float = 10.0, + spacer_hole_diam: float = 8.0 + ) -> Cq.Assembly: + directrix = 0 + result = ( + Cq.Assembly() + .add(self.child(lip_height=lip_height, + hole_dist=hole_dist, + spacer_hole_diam=spacer_hole_diam), + name="child", + color=Role.CHILD.color) + .constrain("child/core", "Fixed") + .add(self.torsion_joint.spring(), name="spring_top", + color=Role.DAMPING.color) + .add(self.parent(wing_root_wall_thickness), + name="parent_top", + color=Role.PARENT.color) + .add(self.torsion_joint.spring(), name="spring_bot", + color=Role.DAMPING.color) + .add(self.parent(wing_root_wall_thickness), + name="parent_bot", + color=Role.PARENT.color) + ) + TorsionJoint.add_constraints(result, + rider="child/rider_top", + track="parent_top", + spring="spring_top", + directrix=directrix) + TorsionJoint.add_constraints(result, + rider="child/rider_bot", + track="parent_bot", + spring="spring_bot", + directrix=directrix) + return result.solve() + + @dataclass class Beam: """ @@ -69,6 +266,7 @@ class Beam: ) return result + @dataclass class DiskJoint(Model): """ @@ -325,6 +523,7 @@ class DiskJoint(Model): ) return result.solve() + @dataclass class ElbowJoint: """ @@ -375,10 +574,10 @@ class ElbowJoint: ) return result - def parent_joint_bot(self) -> Cq.Workplane: + def parent_joint_lower(self) -> Cq.Workplane: return self.disk_joint.housing_lower() - def parent_joint_top(self): + def parent_joint_upper(self): axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) housing_dz = self.disk_joint.housing_upper_dz conn_h = self.parent_beam.spine_thickness @@ -396,12 +595,11 @@ class ElbowJoint: ) housing = self.disk_joint.housing_upper() result = ( - Cq.Assembly() + self.parent_beam.beam() .add(housing, name="housing", loc=axial_offset * Cq.Location((0, 0, housing_dz))) .add(connector, name="connector", loc=axial_offset) - .add(self.parent_beam.beam(), name="beam") ) return result @@ -410,19 +608,21 @@ class ElbowJoint: result = ( Cq.Assembly() .add(self.child_joint(), name="child", color=Role.CHILD.color) - .add(self.parent_joint_bot(), name="parent_bot", color=Role.CASING.color) - .add(self.parent_joint_top(), name="parent_top", color=Role.PARENT.color) - .constrain("parent_bot", "Fixed") + .add(self.parent_joint_lower(), name="parent_lower", color=Role.CASING.color) + .add(self.parent_joint_upper(), name="parent_upper", color=Role.PARENT.color) + .constrain("parent_lower", "Fixed") ) self.disk_joint.add_constraints( result, - housing_lower="parent_bot", - housing_upper="parent_top/housing", + housing_lower="parent_lower", + housing_upper="parent_upper/housing", disk="child/disk", angle=(0, 0, angle + da), ) return result.solve() if __name__ == '__main__': + p = ShoulderJoint() + p.build_all() p = DiskJoint() p.build_all() diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index c5a9d27..cf839a7 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -1,21 +1,25 @@ import unittest import cadquery as Cq import nhf.touhou.houjuu_nue as M -import nhf.touhou.houjuu_nue.parts as MP +import nhf.touhou.houjuu_nue.joints as MJ from nhf.checks import pairwise_intersection -class TestDiskJoint(unittest.TestCase): +class TestJoints(unittest.TestCase): - def test_collision_0(self): - j = MP.DiskJoint() + def test_shoulder_collision_0(self): + j = MJ.ShoulderJoint() + assembly = j.assembly() + self.assertEqual(pairwise_intersection(assembly), []) + def test_disk_collision_0(self): + j = MJ.DiskJoint() assembly = j.assembly(angle=0) self.assertEqual(pairwise_intersection(assembly), []) - def test_collision_mid(self): - j = MP.DiskJoint() + def test_disk_collision_mid(self): + j = MJ.DiskJoint() assembly = j.assembly(angle=j.movement_angle / 2) self.assertEqual(pairwise_intersection(assembly), []) - def test_collision_max(self): - j = MP.DiskJoint() + def test_disk_collision_max(self): + j = MJ.DiskJoint() assembly = j.assembly(angle=j.movement_angle) self.assertEqual(pairwise_intersection(assembly), []) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index d39ca76..b3bc434 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -198,7 +198,7 @@ def wing_root(joint: HirthJoint, for sign in [False, True]: y = conn_height / 2 - wall_thickness side = "bottom" if sign else "top" - y = y if sign else -y + y = -y if sign else y plane = ( result # Create connector holes @@ -211,7 +211,7 @@ def wing_root(joint: HirthJoint, side = "bot" for i, (px, py) in enumerate(attach_points): tag = f"conn_{side}{i}" - plane.moveTo(px, -py if side == "top" else py).tagPlane(tag) + plane.moveTo(px, -py if side == "top" else py).tagPlane(tag, "-Z") result.faces("X").tag("conn") @@ -430,7 +430,7 @@ class WingProfile: self.elbow_angle + 90), ("elbow_top", self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height), - self.elbow_angle + 270), + self.elbow_angle - 90), ] h = (self.wrist_height - wrist_joint_parent_height) / 2 tags_wrist = [ @@ -439,7 +439,7 @@ class WingProfile: self.wrist_angle + 90), ("wrist_top", self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height), - self.wrist_angle + 270), + self.wrist_angle - 90), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist @@ -465,7 +465,7 @@ class WingProfile: self.elbow_angle + 90), ("wrist_top", self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height), - self.elbow_angle + 270), + self.elbow_angle - 90), ] profile = self.profile_s3() return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) diff --git a/nhf/utils.py b/nhf/utils.py index 6d1dc8b..f75c8ec 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -7,6 +7,7 @@ Adds the functions to `Cq.Workplane`: """ import math import cadquery as Cq +from nhf import Role from typing import Union, Tuple @@ -19,7 +20,6 @@ def tagPoint(self, tag: str): Cq.Workplane.tagPoint = tagPoint - def tagPlane(self, tag: str, direction: Union[str, Cq.Vector, Tuple[float, float, float]] = '+Z'): """ @@ -52,6 +52,57 @@ def tagPlane(self, tag: str, Cq.Workplane.tagPlane = tagPlane +def make_sphere(r: float = 2) -> Cq.Solid: + """ + Makes a full sphere. The default function makes a hemisphere + """ + return Cq.Solid.makeSphere(r, angleDegrees1=-90) +def make_arrow(size: float = 2) -> Cq.Workplane: + cone = Cq.Solid.makeCone( + radius1 = size, + radius2 = 0, + height=size) + result = ( + Cq.Workplane("XY") + .cylinder(radius=size / 2, height=size, centered=(True, True, False)) + .union(cone.located(Cq.Location((0, 0, size)))) + ) + result.faces(" Cq.Assembly: + """ + Adds a marker to make a point visible + """ + name = f"{tag}_marker" + return ( + self + .add(make_sphere(size), name=name, color=color) + .constrain(tag, name, "Point") + ) + +Cq.Assembly.markPoint = mark_point + +def mark_plane(self: Cq.Assembly, + tag: str, + size: float = 2, + color: Cq.Color = Role.MARKER.color) -> Cq.Assembly: + """ + Adds a marker to make a plane visible + """ + name = tag.replace("?", "__") + "_marker" + return ( + self + .add(make_arrow(size), name=name, color=color) + .constrain(tag, f"{name}?dir_rev", "Plane", param=180) + ) + +Cq.Assembly.markPlane = mark_plane + + def extrude_with_markers(sketch: Cq.Sketch, thickness: float, tags: list[Tuple[str, Tuple[float, float], float]], @@ -73,7 +124,7 @@ def extrude_with_markers(sketch: Cq.Sketch, ) plane = result.faces("Z").workplane() sign = -1 if reverse else 1 - for tag, (px, py), angle in tag: + for tag, (px, py), angle in tags: theta = sign * math.radians(angle) direction = (math.cos(theta), math.sin(theta), 0) plane.moveTo(px, sign * py).tagPlane(tag) -- 2.44.1 From e744250c6ce0d98437ace037bf577b33454e1b20 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 13 Jul 2024 12:57:17 -0700 Subject: [PATCH 066/187] fix: Use non-fixed constraints for disk joint --- nhf/materials.py | 2 +- nhf/touhou/houjuu_nue/__init__.py | 4 +++- nhf/touhou/houjuu_nue/joints.py | 36 ++++++++++++++++++++----------- nhf/utils.py | 7 ++++-- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index a95a7eb..2f96bda 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -21,7 +21,7 @@ class Role(Enum): STRUCTURE = _color('gray', 0.4) DECORATION = _color('lightseagreen', 0.4) ELECTRONIC = _color('mediumorchid', 0.5) - MARKER = _color('white', 1.0) + MARKER = _color('cyan', 1.0) def __init__(self, color: Cq.Color): self.color = color diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 7cdba60..a514bd8 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -599,7 +599,9 @@ class Parameters(Model): "s2/elbow_top_spacer?conn1", "Plane") ) - return result.solve() + if len(parts) > 1: + result.solve() + return result @assembly() def wings_assembly(self) -> Cq.Assembly: diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 6e653af..a2f85a3 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -262,7 +262,7 @@ class Beam: .add(self.foot(), name="top", loc=Cq.Location((0, h, 0))) .add(self.foot(), name="bot", - loc=Cq.Location((0, -h, 0), (0, 0, 1), 180)) + loc=Cq.Location((0, -h, 0), (1, 0, 0), 180)) ) return result @@ -424,7 +424,10 @@ class DiskJoint(Model): )) ) result.faces(">Z").tag("mate") - result.faces(">Z").workplane().tagPlane("dir", direction="+X") + result.faces(">Z").workplane().tagPlane("dirX", direction="+X") + # two directional vectors are required to make the angle constrain + # unambiguous + result.faces(">Z").workplane().tagPlane("dirY", direction="+Y") result = result.cut( self .wall() @@ -486,17 +489,15 @@ class DiskJoint(Model): housing_lower: str, housing_upper: str, disk: str, - angle: Tuple[float, float, float] = (0, 0, 0), + angle: float, ) -> Cq.Assembly: - """ - The angle supplied must be perpendicular to the disk normal. - """ ( assembly .constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane") .constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane") - .constrain(f"{housing_lower}?dir", f"{housing_upper}?dir", "Axis") - .constrain(f"{disk}?dir", "FixedRotation", angle) + .constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis") + .constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle) + .constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90) ) @@ -519,7 +520,7 @@ class DiskJoint(Model): housing_lower="housing_lower", housing_upper="housing_upper", disk="disk", - angle=(0, 0, angle), + angle=angle, ) return result.solve() @@ -567,10 +568,16 @@ class ElbowJoint: def child_joint(self) -> Cq.Assembly: angle = -self.disk_joint.tongue_span / 2 dz = self.disk_joint.disk_thickness / 2 + # We need to ensure the disk is on the "other" side so + flip = Cq.Location((0, 0, 0), (0, 0, 1), 180) result = ( self.child_beam.beam() .add(self.disk_joint.disk(), name="disk", - loc=Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) + loc=flip * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) + .constrain("disk", "Fixed") + .constrain("top", "Fixed") + .constrain("bot", "Fixed") + .solve() ) return result @@ -600,24 +607,27 @@ class ElbowJoint: loc=axial_offset * Cq.Location((0, 0, housing_dz))) .add(connector, name="connector", loc=axial_offset) + .constrain("housing", "Fixed") + .constrain("connector", "Fixed") + .solve() ) return result def assembly(self, angle: float = 0) -> Cq.Assembly: - da = self.disk_joint.tongue_span / 2 + da = self.disk_joint.tongue_span / 2 + 180 result = ( Cq.Assembly() .add(self.child_joint(), name="child", color=Role.CHILD.color) .add(self.parent_joint_lower(), name="parent_lower", color=Role.CASING.color) .add(self.parent_joint_upper(), name="parent_upper", color=Role.PARENT.color) - .constrain("parent_lower", "Fixed") + #.constrain("parent_lower", "Fixed") ) self.disk_joint.add_constraints( result, housing_lower="parent_lower", housing_upper="parent_upper/housing", disk="child/disk", - angle=(0, 0, angle + da), + angle=angle, ) return result.solve() diff --git a/nhf/utils.py b/nhf/utils.py index f75c8ec..a834022 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -70,6 +70,9 @@ def make_arrow(size: float = 2) -> Cq.Workplane: result.faces(" str: + return tag.replace("?", "__T").replace("/", "__Z") + "_marker" + def mark_point(self: Cq.Assembly, tag: str, size: float = 2, @@ -77,7 +80,7 @@ def mark_point(self: Cq.Assembly, """ Adds a marker to make a point visible """ - name = f"{tag}_marker" + name = to_marker_name(tag) return ( self .add(make_sphere(size), name=name, color=color) @@ -93,7 +96,7 @@ def mark_plane(self: Cq.Assembly, """ Adds a marker to make a plane visible """ - name = tag.replace("?", "__") + "_marker" + name = to_marker_name(tag) return ( self .add(make_arrow(size), name=name, color=color) -- 2.44.1 From a0ae8c91eb8ac86bbca075a4fae00d47c482cd62 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 13 Jul 2024 16:19:17 -0700 Subject: [PATCH 067/187] feat: Remove fixed rotation constraints There is currently a bug when it comes to solving deeply nested assemblies. We need to come up with a solution. --- nhf/touhou/houjuu_nue/joints.py | 29 ++++++++++++++--------------- nhf/touhou/houjuu_nue/wing.py | 8 ++++---- nhf/utils.py | 2 +- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index a2f85a3..dfd5207 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -458,7 +458,7 @@ class DiskJoint(Model): ) ) result.faces("Z").hole(self.radius_axle * 2) # tube which holds the spring interior @@ -491,11 +491,11 @@ class DiskJoint(Model): disk: str, angle: float, ) -> Cq.Assembly: - ( + return ( assembly .constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane") .constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane") - .constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis") + .constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0) .constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle) .constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90) ) @@ -513,9 +513,9 @@ class DiskJoint(Model): .add(self.disk(), name="disk", color=Role.CHILD.color) .add(self.housing_lower(), name="housing_lower", color=Role.PARENT.color) .add(self.housing_upper(), name="housing_upper", color=Role.CASING.color) - .constrain("housing_lower", "Fixed") + #.constrain("housing_lower", "Fixed") ) - self.add_constraints( + result = self.add_constraints( result, housing_lower="housing_lower", housing_upper="housing_upper", @@ -574,10 +574,10 @@ class ElbowJoint: self.child_beam.beam() .add(self.disk_joint.disk(), name="disk", loc=flip * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) - .constrain("disk", "Fixed") - .constrain("top", "Fixed") - .constrain("bot", "Fixed") - .solve() + #.constrain("disk", "Fixed") + #.constrain("top", "Fixed") + #.constrain("bot", "Fixed") + #.solve() ) return result @@ -607,22 +607,21 @@ class ElbowJoint: loc=axial_offset * Cq.Location((0, 0, housing_dz))) .add(connector, name="connector", loc=axial_offset) - .constrain("housing", "Fixed") - .constrain("connector", "Fixed") - .solve() + #.constrain("housing", "Fixed") + #.constrain("connector", "Fixed") + #.solve() ) return result def assembly(self, angle: float = 0) -> Cq.Assembly: - da = self.disk_joint.tongue_span / 2 + 180 result = ( Cq.Assembly() .add(self.child_joint(), name="child", color=Role.CHILD.color) .add(self.parent_joint_lower(), name="parent_lower", color=Role.CASING.color) .add(self.parent_joint_upper(), name="parent_upper", color=Role.PARENT.color) - #.constrain("parent_lower", "Fixed") + #.constrain("child/disk?mate_bot", "Fixed") ) - self.disk_joint.add_constraints( + result = self.disk_joint.add_constraints( result, housing_lower="parent_lower", housing_upper="parent_upper/housing", diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b3bc434..da83b90 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -364,8 +364,8 @@ class WingProfile: abscissa = 200 return [ (0, -abscissa), - (self.wrist_x - self.wrist_s * abscissa, - self.wrist_y - self.wrist_c * abscissa), + (self.wrist_x, -abscissa), + (self.wrist_x, self.wrist_y), (self.wrist_top_x, self.wrist_top_y), (0, abscissa), ] @@ -410,9 +410,9 @@ class WingProfile: profile = ( self.profile() .reset() - .polygon(self._mask_wrist(), mode='i') - .reset() .polygon(self._mask_elbow(), mode='s') + .reset() + .polygon(self._mask_wrist(), mode='i') ) return profile def surface_s2(self, diff --git a/nhf/utils.py b/nhf/utils.py index a834022..b1b92d8 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -48,7 +48,7 @@ def tagPlane(self, tag: str, else: v = Cq.Vector(direction) edge = Cq.Edge.makeLine(v * (-1), v) - self.eachpoint(edge.moved, useLocalCoordinates=True).tag(tag) + return self.eachpoint(edge.located, useLocalCoordinates=True).tag(tag) Cq.Workplane.tagPlane = tagPlane -- 2.44.1 From 1bcb27c711585c6615f5f46f0d00585b9c9e3823 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 14 Jul 2024 00:47:44 -0700 Subject: [PATCH 068/187] feat: Wing root class --- nhf/touhou/houjuu_nue/wing.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index da83b90..d05eaf5 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -231,6 +231,51 @@ def wing_root(joint: HirthJoint, ) return result +@dataclass +class WingRoot: + """ + Generator for the wing root profile and model + """ + + panel_thickness: float = 25.4 / 16 + height: float = 100.0 + shoulder_width: float = 20.0 + root_width: float = 60.0 + + def outer_spline(self) -> list[Tuple[float, float]]: + """ + Generate outer wing shape spline + """ + + def profile(self) -> Cq.Sketch: + tip_x, tip_y = -100.0, 70.0 + sketch = ( + Cq.Sketch() + .segment((-self.root_width, 0), (0, 0)) + .spline([ + (0, 0), + (-30.0, 50.0), + (tip_x, tip_y) + ]) + .segment( + (tip_x, tip_y), + (tip_x, tip_y - self.shoulder_width) + ) + .segment( + (tip_x, tip_y - self.shoulder_width), + (-self.root_width, 0) + ) + .assemble() + ) + return sketch + + def xy_surface(self) -> Cq.Workplane: + + return ( + Cq.Workplane() + .placeSketch(self.profile()) + .extrude(self.panel_thickness) + ) @dataclass class WingProfile: -- 2.44.1 From 1f5a65c43fb90c83f28b96f171ef8b3ee77d3c9b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 14 Jul 2024 17:56:02 -0700 Subject: [PATCH 069/187] fix: _subloc patch, wing root strut --- nhf/touhou/houjuu_nue/wing.py | 77 +++++++++++++++++++++++++++++------ nhf/utils.py | 27 +++++++++++- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index d05eaf5..46fd676 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -238,9 +238,13 @@ class WingRoot: """ panel_thickness: float = 25.4 / 16 + spacer_thickness: float = 25.4 / 8 height: float = 100.0 - shoulder_width: float = 20.0 - root_width: float = 60.0 + shoulder_width: float = 30.0 + root_width: float = 80.0 + + tip_x: float = -200.0 + tip_y: float = 160.0 def outer_spline(self) -> list[Tuple[float, float]]: """ @@ -248,34 +252,81 @@ class WingRoot: """ def profile(self) -> Cq.Sketch: - tip_x, tip_y = -100.0, 70.0 sketch = ( Cq.Sketch() .segment((-self.root_width, 0), (0, 0)) .spline([ (0, 0), - (-30.0, 50.0), - (tip_x, tip_y) + (-30.0, 80.0), + (self.tip_x, self.tip_y) ]) .segment( - (tip_x, tip_y), - (tip_x, tip_y - self.shoulder_width) + (self.tip_x, self.tip_y), + (self.tip_x, self.tip_y - self.shoulder_width) ) .segment( - (tip_x, tip_y - self.shoulder_width), + (self.tip_x, self.tip_y - self.shoulder_width), (-self.root_width, 0) ) .assemble() ) return sketch - def xy_surface(self) -> Cq.Workplane: + def spacer(self) -> Cq.Workplane: + """ + Creates a rectangular spacer. This could be cut from acrylic. - return ( - Cq.Workplane() - .placeSketch(self.profile()) - .extrude(self.panel_thickness) + There are two holes on the top of the spacer. With the holes + """ + length = self.height + width = 10.0 + h = self.spacer_thickness + result = ( + Cq.Workplane('XY') + .sketch() + .rect(length, width) + .finalize() + .extrude(h) ) + # Tag the mating surfaces to be glued + result.faces("X").workplane().tagPlane("right") + + # Tag the directrix + result.faces(">Z").tag("dir") + return result + + def surface(self, top: bool = False) -> Cq.Workplane: + tags = [ + ("shoulder", (self.tip_x, self.tip_y + 30), 0), + ("base", (-self.root_width, 0), 90), + ] + return nhf.utils.extrude_with_markers( + self.profile(), + self.panel_thickness, + tags, + reverse=not top, + ) + + def assembly(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface(top=True), name="bot") + .add(self.surface(top=False), name="top") + .constrain("bot@faces@>Z", "top@faces@ Tuple[Cq.Location, str]: + """ + Calculate relative location of an object in a subassembly. + + Returns the relative positions as well as the name of the top assembly. + """ + + rv = Cq.Location() + obj = self.objects[name] + name_out = name + + if obj not in self.children and obj is not self: + locs = [] + while not obj.parent is self: + locs.append(obj.loc) + obj = cast(Cq.Assembly, obj.parent) + name_out = obj.name + + rv = functools.reduce(lambda l1, l2: l2 * l1, locs) + + return (rv, name_out) +Cq.Assembly._subloc = _subloc def tagPoint(self, tag: str): -- 2.44.1 From bc8cda2eeca66acfe12a2590eaeebf870cb73314 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 14 Jul 2024 23:58:42 -0700 Subject: [PATCH 070/187] refactor: Move harness to its own class --- nhf/touhou/houjuu_nue/__init__.py | 183 +++--------------------------- nhf/touhou/houjuu_nue/harness.py | 170 +++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 170 deletions(-) create mode 100644 nhf/touhou/houjuu_nue/harness.py diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index a514bd8..844ce48 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -38,6 +38,7 @@ 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 +import nhf.touhou.houjuu_nue.harness as MH import nhf.utils @dataclass @@ -50,44 +51,16 @@ class Parameters(Model): panel_thickness: float = 25.4 / 16 # Harness - harness_thickness: float = 25.4 / 8 - harness_width: float = 300 - harness_height: float = 400 - harness_fillet: float = 10 - - harness_wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [ - ("r1", 70, 150), - ("l1", -70, 150), - ("r2", 100, 0), - ("l2", -100, 0), - ("r3", 70, -150), - ("l3", -70, -150), - ]) - - # Holes drilled onto harness for attachment with HS joint - harness_to_root_conn_diam: float = 6 + harness: MH.Harness = field(default_factory=lambda: MH.Harness()) hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=30, radius_inner=20, tooth_height=10, - base_height=5 + base_height=5, + n_tooth=24 )) - # Wing root properties - # - # The Houjuu-Scarlett joint mechanism at the base of the wing - hs_joint_base_width: float = 85 - hs_joint_base_thickness: float = 10 - hs_joint_corner_fillet: float = 5 - hs_joint_corner_cbore_diam: float = 12 - hs_joint_corner_cbore_depth: float = 2 - hs_joint_corner_inset: float = 12 - - hs_joint_axis_diam: float = 12 - hs_joint_axis_cbore_diam: float = 20 - hs_joint_axis_cbore_depth: float = 3 - wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( shoulder_height=100.0, elbow_height=110.0, @@ -135,7 +108,8 @@ class Parameters(Model): def __post_init__(self): super().__init__(name="houjuu-nue") - assert self.wing_root_radius > self.hs_hirth_joint.radius,\ + self.harness.hs_hirth_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" @@ -157,150 +131,19 @@ class Parameters(Model): result = result.faces(">Z").hole(self.trident_terminal_hole_diam, depth=h) return result - - def harness_profile(self) -> Cq.Sketch: - """ - Creates the harness shape - """ - w, h = self.harness_width / 2, self.harness_height / 2 - sketch = ( - Cq.Sketch() - .polygon([ - (0.7 * w, h), - (w, 0), - (0.7 * w, -h), - (0.7 * -w, -h), - (-w, 0), - (0.7 * -w, h), - ]) - #.rect(self.harness_width, self.harness_height) - .vertices() - .fillet(self.harness_fillet) - ) - for tag, x, y in self.harness_wing_base_pos: - conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] - sketch = ( - sketch - .push(conn) - .tag(tag) - .circle(self.harness_to_root_conn_diam / 2, mode='s') - .reset() - ) - return sketch - @target(name="harness", kind=TargetKind.DXF) - def harness(self) -> Cq.Shape: - """ - Creates the harness shape - """ - result = ( - Cq.Workplane('XZ') - .placeSketch(self.harness_profile()) - .extrude(self.harness_thickness) - ) - result.faces(">Y").tag("mount") - plane = result.faces(">Y").workplane() - for tag, x, y in self.harness_wing_base_pos: - conn = [(px + x, py + y) for px, py - in self.hs_joint_harness_conn()] - for i, (px, py) in enumerate(conn): - ( - plane - .moveTo(px, py) - .circle(1, forConstruction='True') - .edges() - .tag(f"{tag}_{i}") - ) - return result + def harness_profile(self) -> Cq.Sketch: + return self.harness.profile() - def hs_joint_harness_conn(self) -> list[tuple[int, int]]: - """ - Generates a set of points corresponding to the connectorss - """ - dx = self.hs_joint_base_width / 2 - self.hs_joint_corner_inset - return [ - (dx, dx), - (dx, -dx), - (-dx, -dx), - (-dx, dx), - ] + def harness_surface(self) -> Cq.Workplane: + return self.harness.surface() - @target(name="hs_joint_parent") - def hs_joint_parent(self): - """ - Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth - coupling, a cylindrical base, and a mounting base. - """ - hirth = self.hs_hirth_joint.generate() - conn = self.hs_joint_harness_conn() - result = ( - Cq.Workplane('XY') - .box( - self.hs_joint_base_width, - self.hs_joint_base_width, - self.hs_joint_base_thickness, - centered=(True, True, False)) - .translate((0, 0, -self.hs_joint_base_thickness)) - .edges("|Z") - .fillet(self.hs_joint_corner_fillet) - .faces(">Z") - .workplane() - .pushPoints(conn) - .cboreHole( - diameter=self.harness_to_root_conn_diam, - cboreDiameter=self.hs_joint_corner_cbore_diam, - cboreDepth=self.hs_joint_corner_cbore_depth) - ) - # Creates a plane parallel to the holes but shifted to the base - plane = result.faces(">Z").workplane(offset=-self.hs_joint_base_thickness) - - for i, (px, py) in enumerate(conn): - ( - plane - .pushPoints([(px, py)]) - .circle(1, forConstruction='True') - .edges() - .tag(f"h{i}") - ) - result = ( - result - .faces(">Z") - .workplane() - .union(hirth, tol=0.1) - .clean() - ) - result = ( - result.faces(" Cq.Workplane: + return self.harness.hs_joint_parent() @assembly() def harness_assembly(self) -> Cq.Assembly: - harness = self.harness() - result = ( - Cq.Assembly() - .add(harness, name="base", color=Material.WOOD_BIRCH.color) - .constrain("base", "Fixed") - ) - for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: - j = self.hs_joint_parent() - ( - result - .add(j, name=name, color=Role.PARENT.color) - .constrain("base?mount", f"{name}?base", "Axis") - ) - for i in range(4): - result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") - result.solve() - return result + return self.harness.assembly() #@target(name="wing/joining-plate", kind=TargetKind.DXF) #def joining_plate(self) -> Cq.Workplane: diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py new file mode 100644 index 0000000..3df4cf8 --- /dev/null +++ b/nhf/touhou/houjuu_nue/harness.py @@ -0,0 +1,170 @@ +from dataclasses import dataclass, field +import cadquery as Cq +from nhf.parts.joints import HirthJoint +from nhf import Material, Role +import nhf.utils + +@dataclass +class Harness: + thickness: float = 25.4 / 8 + width: float = 300.0 + height: float = 400.0 + fillet: float = 10.0 + + wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [ + ("r1", 70, 150), + ("l1", -70, 150), + ("r2", 100, 0), + ("l2", -100, 0), + ("r3", 70, -150), + ("l3", -70, -150), + ]) + # Holes drilled onto harness for attachment with HS joint + harness_to_root_conn_diam: float = 6 + + hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( + radius=30, + radius_inner=20, + tooth_height=10, + base_height=5 + )) + + hs_joint_base_width: float = 85 + hs_joint_base_thickness: float = 10 + hs_joint_corner_fillet: float = 5 + hs_joint_corner_cbore_diam: float = 12 + hs_joint_corner_cbore_depth: float = 2 + hs_joint_corner_inset: float = 12 + + hs_joint_axis_diam: float = 12 + hs_joint_axis_cbore_diam: float = 20 + hs_joint_axis_cbore_depth: float = 3 + + def profile(self) -> Cq.Sketch: + """ + Creates the harness shape + """ + w, h = self.width / 2, self.height / 2 + sketch = ( + Cq.Sketch() + .polygon([ + (0.7 * w, h), + (w, 0), + (0.7 * w, -h), + (0.7 * -w, -h), + (-w, 0), + (0.7 * -w, h), + ]) + #.rect(self.harness_width, self.harness_height) + .vertices() + .fillet(self.fillet) + ) + for tag, x, y in self.wing_base_pos: + conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] + sketch = ( + sketch + .push(conn) + .tag(tag) + .circle(self.harness_to_root_conn_diam / 2, mode='s') + .reset() + ) + return sketch + + def surface(self) -> Cq.Workplane: + """ + Creates the harness shape + """ + result = ( + Cq.Workplane('XZ') + .placeSketch(self.profile()) + .extrude(self.thickness) + ) + result.faces(">Y").tag("mount") + plane = result.faces(">Y").workplane() + for tag, x, y in self.wing_base_pos: + conn = [(px + x, py + y) for px, py + in self.hs_joint_harness_conn()] + for i, (px, py) in enumerate(conn): + plane.moveTo(px, py).tagPoint(f"{tag}_{i}") + return result + + def hs_joint_harness_conn(self) -> list[tuple[int, int]]: + """ + Generates a set of points corresponding to the connectorss + """ + dx = self.hs_joint_base_width / 2 - self.hs_joint_corner_inset + return [ + (dx, dx), + (dx, -dx), + (-dx, -dx), + (-dx, dx), + ] + + def hs_joint_parent(self): + """ + Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth + coupling, a cylindrical base, and a mounting base. + """ + hirth = self.hs_hirth_joint.generate() + conn = self.hs_joint_harness_conn() + result = ( + Cq.Workplane('XY') + .box( + self.hs_joint_base_width, + self.hs_joint_base_width, + self.hs_joint_base_thickness, + centered=(True, True, False)) + .translate((0, 0, -self.hs_joint_base_thickness)) + .edges("|Z") + .fillet(self.hs_joint_corner_fillet) + .faces(">Z") + .workplane() + .pushPoints(conn) + .cboreHole( + diameter=self.harness_to_root_conn_diam, + cboreDiameter=self.hs_joint_corner_cbore_diam, + cboreDepth=self.hs_joint_corner_cbore_depth) + ) + # Creates a plane parallel to the holes but shifted to the base + plane = result.faces(">Z").workplane(offset=-self.hs_joint_base_thickness) + + for i, (px, py) in enumerate(conn): + plane.moveTo(px, py).tagPoint(f"h{i}") + result = ( + result + .faces(">Z") + .workplane() + .union(hirth, tol=0.1) + .clean() + ) + result = ( + result.faces(" Cq.Assembly: + harness = self.surface() + result = ( + Cq.Assembly() + .add(harness, name="base", color=Material.WOOD_BIRCH.color) + .constrain("base", "Fixed") + ) + for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: + j = self.hs_joint_parent() + ( + result + .add(j, name=name, color=Role.PARENT.color) + #.constrain("base?mount", f"{name}?base", "Axis") + ) + for i in range(4): + result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") + result.solve() + return result -- 2.44.1 From 027eec7264d6e35df231ca6c509c4e3b8712a34f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 15 Jul 2024 22:57:38 -0700 Subject: [PATCH 071/187] refactor: Move wings to its own class with joints --- nhf/parts/box.py | 110 +++++ nhf/parts/joints.py | 12 + nhf/touhou/houjuu_nue/__init__.py | 332 +------------ nhf/touhou/houjuu_nue/joints.py | 176 ++++--- nhf/touhou/houjuu_nue/wing.py | 786 +++++++++++++++--------------- 5 files changed, 634 insertions(+), 782 deletions(-) create mode 100644 nhf/parts/box.py diff --git a/nhf/parts/box.py b/nhf/parts/box.py new file mode 100644 index 0000000..a95f512 --- /dev/null +++ b/nhf/parts/box.py @@ -0,0 +1,110 @@ +import cadquery as Cq +from dataclasses import dataclass, field +from typing import Tuple, Optional, Union +import nhf.utils + +def box_with_centre_holes( + length: float, + width: float, + height: float, + hole_loc: list[float], + hole_diam: float = 6.0, + ) -> Cq.Workplane: + """ + Creates a box with holes along the X axis, marked `conn0, conn1, ...`. The + box's y axis is centred + """ + result = ( + Cq.Workplane('XY') + .box(length, width, height, centered=(False, True, False)) + .faces(">Z") + .workplane() + ) + plane = result + for i, x in enumerate(hole_loc): + result = result.moveTo(x, 0).hole(hole_diam) + plane.moveTo(x, 0).tagPlane(f"conn{i}") + return result + +@dataclass +class Hole: + x: float + y: float = 0.0 + diam: Optional[float] = None + tag: Optional[str] = None + +@dataclass +class MountingBox: + """ + Create a box with marked holes + """ + length: float = 100.0 + width: float = 60.0 + thickness: float = 1.0 + + # List of (x, y), diam + holes: list[Hole] = field(default_factory=lambda: [ + Hole(x=5, y=5, diam=3), + Hole(x=20, y=10, diam=5), + ]) + hole_diam: Optional[float] = None + + centred: Tuple[bool, bool] = (False, True) + + generate_side_tags: bool = True + + def profile(self) -> Cq.Sketch: + bx, by = 0, 0 + if not self.centred[0]: + bx = self.length / 2 + if not self.centred[1]: + by = self.width / 2 + result = ( + Cq.Sketch() + .push([(bx, by)]) + .rect(self.length, self.width) + ) + for hole in self.holes: + diam = hole.diam if hole.diam else self.hole_diam + result.push([(hole.x, hole.y)]).circle(diam / 2, mode='s') + return result + + def generate(self) -> Cq.Workplane: + """ + Creates box shape with markers + """ + result = ( + Cq.Workplane('XY') + .placeSketch(self.profile()) + .extrude(self.thickness) + ) + plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness) + for i, hole in enumerate(self.holes): + tag = hole.tag if hole.tag else f"conn{i}" + plane.moveTo(hole.x, hole.y).tagPlane(tag) + + if self.generate_side_tags: + result.faces("Z").val().Center()).tagPlane("left") + result.faces(">Y").workplane(origin=result.vertices("Y and >Z").val().Center()).tagPlane("right") + result.faces("Z").val().Center()).tagPlane("bot") + result.faces(">X").workplane(origin=result.vertices(">X and Z").val().Center()).tagPlane("top") + result.faces(">Z").tag("dir") + return result + + def marked_assembly(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.generate(), name="box") + ) + for i in range(len(self.holes)): + result.markPlane(f"box?conn{i}") + if self.generate_side_tags: + ( + result + .markPlane("box?left") + .markPlane("box?right") + .markPlane("box?dir") + .markPlane("box?top") + .markPlane("box?bot") + ) + return result.solve() diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index bb1c449..9867b4f 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -105,6 +105,18 @@ class HirthJoint: ) return result + def add_constraints(self, + assembly: Cq.Assembly, + parent: str, + child: str, + angle: int = 0): + ( + assembly + .constrain(f"{parent}?mate", f"{child}?mate", "Plane") + .constrain(f"{parent}?dir", f"{child}?dir", + "Axis", param=angle * self.tooth_angle) + ) + def assembly(self, offset: int = 1): """ Generate an example assembly diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 844ce48..4e57fae 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -62,7 +62,11 @@ class Parameters(Model): )) wing_profile: MW.WingProfile = field(default_factory=lambda: MW.WingProfile( - shoulder_height=100.0, + shoulder_joint=MJ.ShoulderJoint( + height=100.0, + ), + elbow_joint=MJ.ElbowJoint(), + wrist_joint=MJ.ElbowJoint(), elbow_height=110.0, )) @@ -70,14 +74,6 @@ class Parameters(Model): wing_root_radius: float = 40 wing_root_wall_thickness: float = 8 - shoulder_joint: MJ.ShoulderJoint = field(default_factory=lambda: MJ.ShoulderJoint( - shoulder_height=100.0, - )) - elbow_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint( - )) - wrist_joint: MJ.ElbowJoint = field(default_factory=lambda: MJ.ElbowJoint( - )) - """ Heights for various wing joints, where the numbers start from the first joint. @@ -109,6 +105,7 @@ class Parameters(Model): 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, \ @@ -145,309 +142,20 @@ class Parameters(Model): def harness_assembly(self) -> Cq.Assembly: return self.harness.assembly() - #@target(name="wing/joining-plate", kind=TargetKind.DXF) - #def joining_plate(self) -> Cq.Workplane: - # return self.wing_joining_plate.plate() - - @target(name="wing/root") - def wing_root(self) -> Cq.Assembly: - """ - Generate the wing root which contains a Hirth joint at its base and a - rectangular opening on its side, with the necessary interfaces. - """ - return MW.wing_root( - joint=self.hs_hirth_joint, - shoulder_attach_dist=self.shoulder_joint.attach_dist, - shoulder_attach_diam=self.shoulder_joint.attach_diam, - wall_thickness=self.wing_root_wall_thickness, - conn_height=self.wing_profile.shoulder_height, - conn_thickness=self.wing_s0_thickness, - ) - @target(name="wing/proto-shoulder-joint-parent", prototype=True) def proto_shoulder_joint_parent(self): - return self.shoulder_joint.torsion_joint.track() + 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.shoulder_joint.torsion_joint.rider() - @assembly() - def shoulder_assembly(self): - return self.shoulder_joint.assembly( - wing_root_wall_thickness=self.wing_root_wall_thickness, - lip_height=self.wing_s1_thickness, - hole_dist=self.wing_s1_shoulder_spacer_hole_dist, - spacer_hole_diam=self.wing_s1_spacer_hole_diam, - ) - @assembly() - def elbow_assembly(self): - return self.elbow_joint.assembly() - @assembly() - def wrist_assembly(self): - return self.wrist_joint.assembly() - - - @target(name="wing/s1-spacer", kind=TargetKind.DXF) - def wing_s1_spacer(self) -> Cq.Workplane: - result = ( - Cq.Workplane('XZ') - .sketch() - .rect(self.wing_s1_spacer_width, self.wing_s1_thickness) - .finalize() - .extrude(self.wing_s1_spacer_thickness) - ) - result.faces("Z").tag("weld2") - result.faces(">Y").tag("dir") - return result - - @target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF) - def wing_s1_shoulder_spacer(self) -> Cq.Workplane: - """ - Creates a rectangular spacer. This could be cut from acrylic. - - There are two holes on the top of the spacer. With the holes - """ - dx = self.wing_s1_shoulder_spacer_hole_dist - h = self.wing_s1_spacer_thickness - length = self.wing_s1_shoulder_spacer_width - hole_diam = self.wing_s1_spacer_hole_diam - assert dx + hole_diam < length / 2 - result = ( - Cq.Workplane('XY') - .sketch() - .rect(length, self.wing_s1_thickness) - .push([ - (0, 0), - (dx, 0), - ]) - .circle(hole_diam / 2, mode='s') - .finalize() - .extrude(h) - ) - # Tag the mating surfaces to be glued - result.faces("Y").workplane().moveTo(-length / 2, h).tagPlane("right") - - # Tag the directrix - result.faces(">Z").tag("dir") - - # Tag the holes - plane = result.faces(">Z").workplane() - # Side closer to the parent is 0 - plane.moveTo(dx, 0).tagPlane("conn0") - plane.tagPlane("conn1") - return result - def assembly_insert_shoulder_spacer( - self, - assembly, - spacer, - point_tag: str, - front_tag: str = "panel_front", - back_tag: str = "panel_back", - flipped: bool = False, - ): - """ - For a child joint facing up, front panel should be on the right, back - panel on the left - """ - site_front, site_back = "right", "left" - if flipped: - site_front, site_back = site_back, site_front - angle = 0 - ( - assembly - .add(spacer, - name=f"{point_tag}_spacer", - color=self.material_bracket.color) - .constrain(f"{front_tag}?{point_tag}", - f"{point_tag}_spacer?{site_front}", "Plane") - .constrain(f"{back_tag}?{point_tag}", - f"{point_tag}_spacer?{site_back}", "Plane") - .constrain(f"{point_tag}_spacer?dir", f"{front_tag}?{point_tag}_dir", - "Axis", param=angle) - ) - - @target(name="wing/r1s1", kind=TargetKind.DXF) - def wing_r1s1_profile(self) -> Cq.Sketch: - """ - FIXME: Output individual segment profiles - """ - return self.wing_profile.profile() - - def wing_r1s1_panel(self, front=True) -> Cq.Workplane: - return self.wing_profile.surface_s1( - thickness=self.panel_thickness, - shoulder_joint_child_height=self.shoulder_joint.child_height, - front=front, - ) - def wing_r1s2_panel(self, front=True) -> Cq.Workplane: - return self.wing_profile.surface_s2( - thickness=self.panel_thickness, - front=front, - ) - def wing_r1s3_panel(self, front=True) -> Cq.Workplane: - return self.wing_profile.surface_s3( - thickness=self.panel_thickness, - front=front, - ) - - @assembly() - def wing_r1s1_assembly(self) -> Cq.Assembly: - result = ( - Cq.Assembly() - .add(self.wing_r1s1_panel(front=True), name="panel_front", - color=self.material_panel.color) - .constrain("panel_front", "Fixed") - .add(self.wing_r1s1_panel(front=False), name="panel_back", - color=self.material_panel.color) - .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: - result = ( - Cq.Assembly() - .add(self.wing_r1s2_panel(front=True), name="panel_front", - color=self.material_panel.color) - .constrain("panel_front", "Fixed") - .add(self.wing_r1s2_panel(front=False), name="panel_back", - color=self.material_panel.color) - # FIXME: Use s2 thickness - .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: - result = ( - Cq.Assembly() - .add(self.wing_r1s3_panel(front=True), name="panel_front", - color=self.material_panel.color) - .constrain("panel_front", "Fixed") - .add(self.wing_r1s3_panel(front=False), name="panel_back", - color=self.material_panel.color) - # FIXME: Use s2 thickness - .constrain("panel_front@faces@>Z", "panel_back@faces@ Cq.Assembly: - result = ( - Cq.Assembly() - ) - if "s0" in parts: - ( - result - .add(self.wing_root(), name="s0") - .constrain("s0/scaffold", "Fixed") - ) - if "shoulder" in parts: - result.add(self.shoulder_assembly(), name="shoulder") - - if "s0" in parts and "shoulder" in parts: - ( - result - .constrain("s0/scaffold?conn_top0", "shoulder/parent_top?conn0", "Plane") - .constrain("s0/scaffold?conn_top1", "shoulder/parent_top?conn1", "Plane") - .constrain("s0/scaffold?conn_bot0", "shoulder/parent_bot?conn0", "Plane") - .constrain("s0/scaffold?conn_bot1", "shoulder/parent_bot?conn1", "Plane") - ) - - if "s1" in parts: - result.add(self.wing_r1s1_assembly(), name="s1") - - if "s1" in parts and "shoulder" in parts: - ( - result - .constrain("shoulder/child/lip_bot?conn0", - "s1/shoulder_bot_spacer?conn0", - "Plane") - .constrain("shoulder/child/lip_bot?conn1", - "s1/shoulder_bot_spacer?conn1", - "Plane") - .constrain("shoulder/child/lip_top?conn0", - "s1/shoulder_top_spacer?conn0", - "Plane") - .constrain("shoulder/child/lip_top?conn1", - "s1/shoulder_top_spacer?conn1", - "Plane") - ) - if "elbow" in parts: - result.add(self.elbow_assembly(), name="elbow") - - if "s2" in parts: - result.add(self.wing_r1s2_assembly(), name="s2") - - if "s1" in parts and "elbow" in parts: - ( - result - .constrain("elbow/parent_upper/top?conn1", - "s1/elbow_top_spacer?conn1", - "Plane") - .constrain("elbow/parent_upper/top?conn0", - "s1/elbow_top_spacer?conn0", - "Plane") - .constrain("elbow/parent_upper/bot?conn1", - "s1/elbow_bot_spacer?conn1", - "Plane") - .constrain("elbow/parent_upper/bot?conn0", - "s1/elbow_bot_spacer?conn0", - "Plane") - ) - - if "s2" in parts and "elbow" in parts: - ( - result - .constrain("elbow/child/bot?conn0", - "s2/elbow_bot_spacer?conn0", - "Plane") - .constrain("elbow/child/bot?conn1", - "s2/elbow_bot_spacer?conn1", - "Plane") - .constrain("elbow/child/top?conn0", - "s2/elbow_top_spacer?conn0", - "Plane") - .constrain("elbow/child/top?conn1", - "s2/elbow_top_spacer?conn1", - "Plane") - ) - if len(parts) > 1: - result.solve() - return result + def wing_r1_assembly(self) -> Cq.Assembly: + return self.wing_profile.assembly() @assembly() - def wings_assembly(self) -> Cq.Assembly: + def wings_harness_assembly(self) -> Cq.Assembly: """ Assembly of harness with all the wings """ @@ -456,18 +164,14 @@ class Parameters(Model): result = ( Cq.Assembly() .add(self.harness_assembly(), name="harness", loc=Cq.Location((0, 0, 0))) - .add(self.wing_root(), name="w0_r1") - .add(self.wing_root(), name="w0_l1") - .constrain("harness/base", "Fixed") - .constrain("w0_r1/joint?mate", "harness/r1?mate", "Plane") - .constrain("w0_r1/joint?dir", "harness/r1?dir", - "Axis", param=7 * a_tooth) - .constrain("w0_l1/joint?mate", "harness/l1?mate", "Plane") - .constrain("w0_l1/joint?dir", "harness/l1?dir", - "Axis", param=-1 * a_tooth) - .solve() + .add(self.wing_r1_assembly(), name="wing_r1") + .add(self.wing_r1_assembly(), name="wing_r2") + .add(self.wing_r1_assembly(), name="wing_r3") ) - return result + self.hs_hirth_joint.add_constraints(result, "harness/r1", "wing_r1/s0/hs", angle=9) + self.hs_hirth_joint.add_constraints(result, "harness/r2", "wing_r2/s0/hs", angle=8) + self.hs_hirth_joint.add_constraints(result, "harness/r3", "wing_r3/s0/hs", angle=7) + return result.solve() @assembly(collision_check=False) def trident_assembly(self) -> Cq.Assembly: diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index dfd5207..d25c22c 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -5,6 +5,7 @@ from nhf import Role from nhf.build import Model, target, assembly import nhf.parts.springs as springs from nhf.parts.joints import TorsionJoint +from nhf.parts.box import box_with_centre_holes import nhf.utils TOL = 1e-6 @@ -12,7 +13,7 @@ TOL = 1e-6 @dataclass class ShoulderJoint(Model): - shoulder_height: float = 100.0 + height: float = 100.0 torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( radius_track=18, radius_rider=18, @@ -27,47 +28,67 @@ class ShoulderJoint(Model): spring_thickness=1.3, spring_height=7.5, )) - # Two holes on each side (top and bottom) are used to attach the shoulder - # joint. This governs the distance between these two holes - attach_dist: float = 25 - attach_diam: float = 8 + + # On the parent side, drill vertical holes + + parent_conn_hole_diam: float = 6.0 + # Position of the holes relative + parent_conn_hole_pos: list[float] = field(default_factory=lambda: [20, 30]) + + parent_lip_length: float = 40.0 + parent_lip_width: float = 20.0 + parent_lip_thickness: float = 8.0 + parent_lip_ext: float = 40.0 + parent_lip_guard_height: float = 10.0 + + # Measured from centre of axle + child_lip_length: float = 45.0 + child_lip_width: float = 20.0 + child_conn_hole_diam: float = 6.0 + # Measured from centre of axle + child_conn_hole_pos: list[float] = field(default_factory=lambda: [25, 35]) + child_core_thickness: float = 3.0 @target(name="shoulder-joint/parent") def parent(self, - wing_root_wall_thickness: float = 5.0) -> Cq.Workplane: + root_wall_thickness: float = 25.4 / 16) -> Cq.Assembly: joint = self.torsion_joint # Thickness of the lip connecting this joint to the wing root - lip_thickness = 10 - lip_width = 25 - lip_guard_ext = 40 - lip_guard_height = wing_root_wall_thickness + lip_thickness - assert lip_guard_ext > joint.radius_track + dz = root_wall_thickness + assert self.parent_lip_width <= joint.radius_track * 2 + assert self.parent_lip_ext > joint.radius_track lip_guard = ( - Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height) - .located(Cq.Location((0, -lip_width/2 , 0))) - .cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height)) + Cq.Solid.makeBox( + self.parent_lip_ext, + self.parent_lip_width, + self.parent_lip_guard_height) + .located(Cq.Location((0, -self.parent_lip_width/2 , dz))) + .cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_lip_guard_height + dz)) ) + lip = box_with_centre_holes( + length=self.parent_lip_length - dz, + width=self.parent_lip_width, + height=self.parent_lip_thickness, + hole_loc=[ + self.height / 2 - dz - x + for x in self.parent_conn_hole_pos + ], + hole_diam=self.parent_conn_hole_diam, + ) + # Flip so the lip's holes point to -X + loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90) + # so they point to +X + loc_dir = Cq.Location((0,0,0), (0, 0, 1), 180) + loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, dz)) + result = ( - joint.track() - .union(lip_guard, tol=1e-6) - - # Extrude the handle - .copyWorkplane(Cq.Workplane( - 'YZ', origin=Cq.Vector((88, 0, wing_root_wall_thickness)))) - .rect(lip_width, lip_thickness, centered=(True, False)) - .extrude("next") - - # Connector holes on the lip - .copyWorkplane(Cq.Workplane( - 'YX', origin=Cq.Vector((57, 0, wing_root_wall_thickness)))) - .hole(self.attach_diam) - .moveTo(0, self.attach_dist) - .hole(self.attach_diam) + Cq.Assembly() + .add(joint.track(), name="track") + .add(lip_guard, name="lip_guard") + .add(lip, name="lip", loc=loc_pos * loc_dir * loc_axis) ) - result.moveTo(0, 0).tagPlane('conn0') - result.moveTo(0, self.attach_dist).tagPlane('conn1') return result @property @@ -77,32 +98,32 @@ class ShoulderJoint(Model): of the shoulder joint. """ joint = self.torsion_joint - return self.shoulder_height - 2 * joint.total_height + 2 * joint.rider_disk_height + return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height @target(name="shoulder-joint/child") - def child(self, - lip_height: float = 20.0, - hole_dist: float = 10.0, - spacer_hole_diam: float = 8.0) -> Cq.Assembly: + def child(self) -> Cq.Assembly: """ Creates the top/bottom shoulder child joint """ joint = self.torsion_joint + assert all(r > joint.radius_rider for r in self.child_conn_hole_pos) + assert all(r < self.child_lip_length for r in self.child_conn_hole_pos) + # Half of the height of the bridging cylinder - dh = self.shoulder_height / 2 - joint.total_height + dh = self.height / 2 - joint.total_height core_start_angle = 30 core_end_angle1 = 90 core_end_angle2 = 180 - core_thickness = 2 + radius_core_inner = joint.radius_rider - self.child_core_thickness core_profile1 = ( Cq.Sketch() .arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) .segment((0, 0)) .close() .assemble() - .circle(joint.radius_rider - core_thickness, mode='s') + .circle(radius_core_inner, mode='s') ) core_profile2 = ( Cq.Sketch() @@ -110,7 +131,7 @@ class ShoulderJoint(Model): .segment((0, 0)) .close() .assemble() - .circle(joint.radius_rider - core_thickness, mode='s') + .circle(radius_core_inner, mode='s') ) core = ( Cq.Workplane('XY') @@ -123,33 +144,24 @@ class ShoulderJoint(Model): .extrude(dh * 2) .translate(Cq.Vector(0, 0, -dh)) ) - # Create the upper and lower lips + assert self.child_lip_width / 2 <= joint.radius_rider lip_thickness = joint.rider_disk_height - lip_ext = 40 + joint.radius_rider - assert lip_height / 2 <= joint.radius_rider - lip = ( - Cq.Workplane('XY') - .box(lip_ext, lip_height, lip_thickness, - centered=(False, True, False)) - .copyWorkplane(Cq.Workplane('XY')) - .cylinder(radius=joint.radius_rider, height=lip_thickness, - centered=(True, True, False), - combine='cut') - .faces(">Z") - .workplane() + lip = box_with_centre_holes( + length=self.child_lip_length, + width=self.child_lip_width, + height=lip_thickness, + hole_loc=self.child_conn_hole_pos, + hole_diam=self.child_conn_hole_diam, + ) + lip = ( + lip + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + radius=joint.radius_rider, + height=lip_thickness, + centered=(True, True, False), + combine='cut') ) - hole_x = lip_ext - hole_dist / 2 - for i in range(2): - x = hole_x - i * hole_dist - lip = lip.moveTo(x, 0).hole(spacer_hole_diam) - for i in range(2): - x = hole_x - i * hole_dist - ( - lip - .moveTo(x, 0) - .tagPlane(f"conn{1 - i}") - ) - loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) result = ( Cq.Assembly() @@ -167,18 +179,12 @@ class ShoulderJoint(Model): @assembly() def assembly(self, - wing_root_wall_thickness: float = 5.0, - lip_height: float = 5.0, - hole_dist: float = 10.0, - spacer_hole_diam: float = 8.0 + wing_root_wall_thickness: float = 25.4/16, ) -> Cq.Assembly: directrix = 0 result = ( Cq.Assembly() - .add(self.child(lip_height=lip_height, - hole_dist=hole_dist, - spacer_hole_diam=spacer_hole_diam), - name="child", + .add(self.child(), name="child", color=Role.CHILD.color) .constrain("child/core", "Fixed") .add(self.torsion_joint.spring(), name="spring_top", @@ -194,12 +200,12 @@ class ShoulderJoint(Model): ) TorsionJoint.add_constraints(result, rider="child/rider_top", - track="parent_top", + track="parent_top/track", spring="spring_top", directrix=directrix) TorsionJoint.add_constraints(result, rider="child/rider_bot", - track="parent_bot", + track="parent_bot/track", spring="spring_bot", directrix=directrix) return result.solve() @@ -565,15 +571,31 @@ class ElbowJoint: assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span assert self.parent_binding_hole_radius - self.hole_diam / 2 > self.disk_joint.radius_housing + def child_hole_pos(self) -> list[float]: + """ + List of hole positions measured from axle + """ + dx = self.child_beam.hole_dist / 2 + r = self.child_arm_radius + return [r - dx, r + dx] + def parent_hole_pos(self) -> list[float]: + """ + List of hole positions measured from axle + """ + dx = self.parent_beam.hole_dist / 2 + r = self.parent_arm_radius + return [r - dx, r + dx] + def child_joint(self) -> Cq.Assembly: angle = -self.disk_joint.tongue_span / 2 dz = self.disk_joint.disk_thickness / 2 # We need to ensure the disk is on the "other" side so - flip = Cq.Location((0, 0, 0), (0, 0, 1), 180) + flip_x = Cq.Location((0, 0, 0), (1, 0, 0), 180) + flip_z = Cq.Location((0, 0, 0), (0, 0, 1), 180) result = ( self.child_beam.beam() .add(self.disk_joint.disk(), name="disk", - loc=flip * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) + loc=flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) #.constrain("disk", "Fixed") #.constrain("top", "Fixed") #.constrain("bot", "Fixed") diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 46fd676..b832dff 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -4,341 +4,46 @@ This file describes the shapes of the wing shells. The joints are defined in """ import math from enum import Enum -from dataclasses import dataclass -from typing import Mapping, Tuple +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.parts.box import box_with_centre_holes, MountingBox, Hole from nhf.parts.joints import HirthJoint +from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint import nhf.utils - -def wing_root_profiles( - base_sweep=150, - wall_thickness=8, - base_radius=40, - middle_offset=30, - middle_height=80, - conn_thickness=40, - conn_height=100) -> tuple[Cq.Wire, Cq.Wire]: - assert base_sweep < 180 - assert middle_offset > 0 - theta = math.pi * base_sweep / 180 - c, s = math.cos(theta), math.sin(theta) - c_1, s_1 = math.cos(theta * 0.75), math.sin(theta * 0.75) - c_2, s_2 = math.cos(theta / 2), math.sin(theta / 2) - r1 = base_radius - r2 = base_radius - wall_thickness - base = ( - Cq.Sketch() - .arc( - (c * r1, s * r1), - (c_1 * r1, s_1 * r1), - (c_2 * r1, s_2 * r1), - ) - .arc( - (c_2 * r1, s_2 * r1), - (r1, 0), - (c_2 * r1, -s_2 * r1), - ) - .arc( - (c_2 * r1, -s_2 * r1), - (c_1 * r1, -s_1 * r1), - (c * r1, -s * r1), - ) - .segment( - (c * r1, -s * r1), - (c * r2, -s * r2), - ) - .arc( - (c * r2, -s * r2), - (c_1 * r2, -s_1 * r2), - (c_2 * r2, -s_2 * r2), - ) - .arc( - (c_2 * r2, -s_2 * r2), - (r2, 0), - (c_2 * r2, s_2 * r2), - ) - .arc( - (c_2 * r2, s_2 * r2), - (c_1 * r2, s_1 * r2), - (c * r2, s * r2), - ) - .segment( - (c * r2, s * r2), - (c * r1, s * r1), - ) - .assemble(tag="wire") - .wires().val() - ) - assert isinstance(base, Cq.Wire) - - # The interior sweep is given by theta, but the exterior sweep exceeds the - # interior sweep so the wall does not become thinner towards the edges. - # If the exterior sweep is theta', it has to satisfy - # - # sin(theta) * r2 + wall_thickness = sin(theta') * r1 - x, y = conn_thickness / 2, middle_height / 2 - t = wall_thickness - dx = middle_offset - middle = ( - Cq.Sketch() - # Interior arc, top point - .arc( - (x - t, y - t), - (x - t + dx, 0), - (x - t, -y + t), - ) - .segment( - (x - t, -y + t), - (-x, -y+t) - ) - .segment((-x, -y)) - .segment((x, -y)) - # Outer arc, bottom point - .arc( - (x, -y), - (x + dx, 0), - (x, y), - ) - .segment( - (x, y), - (-x, y) - ) - .segment((-x, y-t)) - #.segment((x2, a)) - .close() - .assemble(tag="wire") - .wires().val() - ) - assert isinstance(middle, Cq.Wire) - - x, y = conn_thickness / 2, conn_height / 2 - t = wall_thickness - tip = ( - Cq.Sketch() - .segment((-x, y), (x, y)) - .segment((x, -y)) - .segment((-x, -y)) - .segment((-x, -y+t)) - .segment((x-t, -y+t)) - .segment((x-t, y-t)) - .segment((-x, y-t)) - .close() - .assemble(tag="wire") - .wires().val() - ) - return base, middle, tip - - -def wing_root(joint: HirthJoint, - bolt_diam: int = 12, - union_tol=1e-4, - shoulder_attach_diam=8, - shoulder_attach_dist=25, - conn_thickness=40, - conn_height=100, - wall_thickness=8) -> Cq.Assembly: - """ - Generate the contiguous components of the root wing segment - """ - tip_centre = Cq.Vector((-150, 0, -80)) - attach_theta = math.radians(5) - c, s = math.cos(attach_theta), math.sin(attach_theta) - attach_points = [ - (15, 4), - (15 + shoulder_attach_dist * c, 4 + shoulder_attach_dist * s), - ] - root_profile, middle_profile, tip_profile = wing_root_profiles( - conn_thickness=conn_thickness, - conn_height=conn_height, - wall_thickness=8, - ) - middle_profile = middle_profile.located(Cq.Location( - (-40, 0, -40), (0, 1, 0), 30 - )) - antetip_profile = tip_profile.located(Cq.Location( - (-95, 0, -75), (0, 1, 0), 60 - )) - tip_profile = tip_profile.located(Cq.Location( - tip_centre, (0, 1, 0), 90 - )) - profiles = [ - root_profile, - middle_profile, - antetip_profile, - tip_profile, - ] - result = None - for p1, p2 in zip(profiles[:-1], profiles[1:]): - seg = ( - Cq.Workplane('XY') - .add(p1) - .toPending() - .workplane() # This call is necessary - .add(p2) - .toPending() - .loft() - ) - if result: - result = result.union(seg, tol=union_tol) - else: - result = seg - result = ( - result - # Create connector holes - .copyWorkplane( - Cq.Workplane('bottom', origin=tip_centre + Cq.Vector((0, -50, 0))) - ) - .pushPoints(attach_points) - .hole(shoulder_attach_diam) - ) - # Generate attach point tags - - for sign in [False, True]: - y = conn_height / 2 - wall_thickness - side = "bottom" if sign else "top" - y = -y if sign else y - plane = ( - result - # Create connector holes - .copyWorkplane( - Cq.Workplane(side, origin=tip_centre + - Cq.Vector((0, y, 0))) - ) - ) - if side == "bottom": - side = "bot" - for i, (px, py) in enumerate(attach_points): - tag = f"conn_{side}{i}" - plane.moveTo(px, -py if side == "top" else py).tagPlane(tag, "-Z") - - result.faces("X").tag("conn") - - j = ( - joint.generate(is_mated=True) - .faces(" list[Tuple[float, float]]: - """ - Generate outer wing shape spline - """ - - def profile(self) -> Cq.Sketch: - sketch = ( - Cq.Sketch() - .segment((-self.root_width, 0), (0, 0)) - .spline([ - (0, 0), - (-30.0, 80.0), - (self.tip_x, self.tip_y) - ]) - .segment( - (self.tip_x, self.tip_y), - (self.tip_x, self.tip_y - self.shoulder_width) - ) - .segment( - (self.tip_x, self.tip_y - self.shoulder_width), - (-self.root_width, 0) - ) - .assemble() - ) - return sketch - - def spacer(self) -> Cq.Workplane: - """ - Creates a rectangular spacer. This could be cut from acrylic. - - There are two holes on the top of the spacer. With the holes - """ - length = self.height - width = 10.0 - h = self.spacer_thickness - result = ( - Cq.Workplane('XY') - .sketch() - .rect(length, width) - .finalize() - .extrude(h) - ) - # Tag the mating surfaces to be glued - result.faces("X").workplane().tagPlane("right") - - # Tag the directrix - result.faces(">Z").tag("dir") - return result - - def surface(self, top: bool = False) -> Cq.Workplane: - tags = [ - ("shoulder", (self.tip_x, self.tip_y + 30), 0), - ("base", (-self.root_width, 0), 90), - ] - return nhf.utils.extrude_with_markers( - self.profile(), - self.panel_thickness, - tags, - reverse=not top, - ) - - def assembly(self) -> Cq.Assembly: - result = ( - Cq.Assembly() - .add(self.surface(top=True), name="bot") - .add(self.surface(top=False), name="top") - .constrain("bot@faces@>Z", "top@faces@ self.ring_radius_inner self.elbow_theta = math.radians(self.elbow_angle) @@ -377,6 +88,145 @@ class WingProfile: self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y + @property + def root_height(self) -> float: + return self.shoulder_joint.height + + def profile_s0(self) -> Cq.Sketch: + tip_x = self.shoulder_tip_x + tip_y = self.shoulder_tip_y + sketch = ( + Cq.Sketch() + .segment((-self.root_width, 0), (0, 0)) + .spline([ + (0, 0), + (-30.0, 80.0), + (tip_x, tip_y) + ]) + .segment( + (tip_x, tip_y), + (tip_x, tip_y - self.shoulder_width) + ) + .segment( + (tip_x, tip_y - self.shoulder_width), + (-self.root_width, 0) + ) + .assemble() + ) + return sketch + + def spacer_s0_shoulder(self) -> MountingBox: + """ + Should be cut + """ + holes = [ + hole + for i, x in enumerate(self.shoulder_joint.parent_conn_hole_pos) + for hole in [ + Hole(x=x, tag=f"conn_top{i}"), + Hole(x=-x, tag=f"conn_bot{i}"), + ] + ] + return MountingBox( + length=self.shoulder_joint.height, + width=self.shoulder_width, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.shoulder_joint.parent_conn_hole_diam, + centred=(True, True), + ) + def spacer_s0_base(self) -> MountingBox: + """ + Should be cut + """ + dx = self.hs_joint_corner_dx + holes = [ + Hole(x=-dx, y=-dx), + Hole(x=dx, y=-dx), + Hole(x=dx, y=dx), + Hole(x=-dx, y=dx), + ] + return MountingBox( + length=self.root_height, + width=self.root_width, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.hs_joint_corner_hole_diam, + centred=(True, True), + ) + + def surface_s0(self, top: bool = False) -> Cq.Workplane: + tags = [ + ("shoulder", (self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width), 0), + ("base", (0, 0), 90), + ] + return nhf.utils.extrude_with_markers( + self.profile_s0(), + self.panel_thickness, + tags, + reverse=not top, + ) + + def assembly_s0(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface_s0(top=True), name="bot", color=self.material_panel.color) + .add(self.surface_s0(top=False), name="top", color=self.material_panel.color) + .constrain("bot@faces@>Z", "top@faces@ float: dx = self.ring_x @@ -401,10 +251,10 @@ class WingProfile: Cq.Sketch() .segment( (0, 0), - (0, self.shoulder_height), + (0, self.shoulder_joint.height), tag="shoulder") .arc( - (0, self.shoulder_height), + (0, self.shoulder_joint.height), (self.elbow_top_x, self.elbow_top_y), (self.wrist_top_x, self.wrist_top_y), tag="s1_top") @@ -475,31 +325,74 @@ class WingProfile: ) return profile def surface_s1(self, - thickness: float = 25.4/16, - shoulder_mount_inset: float = 20, - shoulder_joint_child_height: float = 80, - elbow_mount_inset: float = 20, - elbow_joint_parent_height: float = 60, + shoulder_mount_inset: float = 0, + elbow_mount_inset: float = 0, front: bool = True) -> Cq.Workplane: - assert shoulder_joint_child_height < self.shoulder_height - assert elbow_joint_parent_height < self.elbow_height - h = (self.shoulder_height - shoulder_joint_child_height) / 2 + shoulder_h = self.shoulder_joint.child_height + h = (self.shoulder_joint.height - shoulder_h) / 2 tags_shoulder = [ ("shoulder_bot", (shoulder_mount_inset, h), 90), - ("shoulder_top", (shoulder_mount_inset, h + shoulder_joint_child_height), 270), + ("shoulder_top", (shoulder_mount_inset, h + shoulder_h), 270), ] - h = (self.elbow_height - elbow_joint_parent_height) / 2 + elbow_h = self.elbow_joint.parent_beam.total_height + h = (self.elbow_height - elbow_h) / 2 tags_elbow = [ ("elbow_bot", self.elbow_to_abs(-elbow_mount_inset, h), self.elbow_angle + 90), ("elbow_top", - self.elbow_to_abs(-elbow_mount_inset, h + elbow_joint_parent_height), + self.elbow_to_abs(-elbow_mount_inset, h + elbow_h), self.elbow_angle + 270), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow - return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) + return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + def spacer_s1_shoulder(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.shoulder_joint.child_conn_hole_pos + ] + return MountingBox( + length=50.0, # FIXME: magic + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.shoulder_joint.child_conn_hole_diam, + ) + def spacer_s1_elbow(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.elbow_joint.parent_hole_pos() + ] + return MountingBox( + length=70.0, # FIXME: magic + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.elbow_joint.hole_diam, + ) + def assembly_s1(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface_s1(front=True), name="front", + color=self.material_panel.color) + .constrain("front", "Fixed") + .add(self.surface_s1(front=False), name="back", + color=self.material_panel.color) + .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: @@ -513,33 +406,78 @@ class WingProfile: return profile def surface_s2(self, thickness: float = 25.4/16, - elbow_mount_inset: float = 20, - elbow_joint_child_height: float = 80, - wrist_mount_inset: float = 20, - wrist_joint_parent_height: float = 60, + elbow_mount_inset: float = 0, + wrist_mount_inset: float = 0, front: bool = True) -> Cq.Workplane: - assert elbow_joint_child_height < self.elbow_height - h = (self.elbow_height - elbow_joint_child_height) / 2 + elbow_h = self.elbow_joint.child_beam.total_height + h = (self.elbow_height - elbow_h) / 2 tags_elbow = [ ("elbow_bot", self.elbow_to_abs(elbow_mount_inset, h), self.elbow_angle + 90), ("elbow_top", - self.elbow_to_abs(elbow_mount_inset, h + elbow_joint_child_height), + self.elbow_to_abs(elbow_mount_inset, h + elbow_h), self.elbow_angle - 90), ] - h = (self.wrist_height - wrist_joint_parent_height) / 2 + wrist_h = self.wrist_joint.parent_beam.total_height + h = (self.wrist_height - wrist_h) / 2 tags_wrist = [ ("wrist_bot", self.wrist_to_abs(-wrist_mount_inset, h), self.wrist_angle + 90), ("wrist_top", - self.wrist_to_abs(-wrist_mount_inset, h + wrist_joint_parent_height), + self.wrist_to_abs(-wrist_mount_inset, h + wrist_h), self.wrist_angle - 90), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) + def spacer_s2_elbow(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.elbow_joint.child_hole_pos() + ] + return MountingBox( + length=50.0, # FIXME: magic + width=self.s2_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.elbow_joint.hole_diam, + ) + def spacer_s2_wrist(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.wrist_joint.parent_hole_pos() + ] + return MountingBox( + length=70.0, # FIXME: magic + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.wrist_joint.hole_diam, + ) + def assembly_s2(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface_s2(front=True), name="front", + color=self.material_panel.color) + .constrain("front", "Fixed") + .add(self.surface_s2(front=False), name="back", + color=self.material_panel.color) + .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: profile = ( @@ -549,61 +487,127 @@ class WingProfile: ) return profile def surface_s3(self, - thickness: float = 25.4/16, - wrist_mount_inset: float = 20, - wrist_joint_child_height: float = 80, front: bool = True) -> Cq.Workplane: - assert wrist_joint_child_height < self.wrist_height - h = (self.wrist_height - wrist_joint_child_height) / 2 + wrist_mount_inset = 0 + wrist_h = self.wrist_joint.child_beam.total_height + h = (self.wrist_height - wrist_h) / 2 tags = [ ("wrist_bot", - self.elbow_to_abs(wrist_mount_inset, h), - self.elbow_angle + 90), + self.wrist_to_abs(wrist_mount_inset, h), + self.wrist_angle + 90), ("wrist_top", - self.elbow_to_abs(wrist_mount_inset, h + wrist_joint_child_height), - self.elbow_angle - 90), + self.wrist_to_abs(wrist_mount_inset, h + wrist_h), + self.wrist_angle - 90), ] profile = self.profile_s3() - return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) - - - def wing_r1s1_profile(self) -> Cq.Sketch: - """ - Generates the first wing segment profile, with the wing root pointing in - the positive x axis. - """ - - - w = 270 - # Depression of the wing middle, measured - h = 0 - # spline curve easing extension - theta = math.radians(30) - c_th, s_th = math.cos(theta), math.sin(theta) - bend = 30 - ext = 40 - ext_dh = -5 - assert ext * 2 < w - - factor = 0.7 - - result = ( - Cq.Sketch() - .segment((0, 0), (0, self.shoulder_height)) - .spline([ - (0, self.shoulder_height), - ((w - s_th * self.elbow_height) / 2, self.shoulder_height / 2 + (self.elbow_height * c_th - h) / 2 - bend), - (w - s_th * self.elbow_height, self.elbow_height * c_th - h), - ]) - .segment( - (w - s_th * self.elbow_height, self.elbow_height * c_th -h), - (w, -h), - ) - .spline([ - (0, 0), - (w / 2, -h / 2 - bend), - (w, -h), - ]) - .assemble() + return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + def spacer_s3_wrist(self) -> MountingBox: + holes = [ + Hole(x) + for x in self.wrist_joint.child_hole_pos() + ] + return MountingBox( + length=70.0, # FIXME: magic + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=self.wrist_joint.hole_diam ) - return result + def assembly_s3(self) -> Cq.Assembly: + result = ( + Cq.Assembly() + .add(self.surface_s3(front=True), name="front", + color=self.material_panel.color) + .constrain("front", "Fixed") + .add(self.surface_s3(front=False), name="back", + color=self.material_panel.color) + .constrain("front@faces@>Z", "back@faces@ Cq.Assembly(): + if parts is None: + parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] + result = ( + Cq.Assembly() + ) + if "s0" in parts: + result.add(self.assembly_s0(), name="s0") + if "shoulder" in parts: + result.add(self.shoulder_joint.assembly(), name="shoulder") + if "s0" in parts and "shoulder" in parts: + ( + result + .constrain("s0/shoulder?conn_top0", "shoulder/parent_top/lip?conn0", "Plane") + .constrain("s0/shoulder?conn_top1", "shoulder/parent_top/lip?conn1", "Plane") + .constrain("s0/shoulder?conn_bot0", "shoulder/parent_bot/lip?conn0", "Plane") + .constrain("s0/shoulder?conn_bot1", "shoulder/parent_bot/lip?conn1", "Plane") + ) + if "s1" in parts: + result.add(self.assembly_s1(), name="s1") + if "s1" in parts and "shoulder" in parts: + ( + result + .constrain("s1/shoulder_top?conn0", "shoulder/child/lip_top?conn0", "Plane") + .constrain("s1/shoulder_top?conn1", "shoulder/child/lip_top?conn1", "Plane") + .constrain("s1/shoulder_bot?conn0", "shoulder/child/lip_bot?conn0", "Plane") + .constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane") + ) + if "elbow" in parts: + result.add(self.elbow_joint.assembly(), name="elbow") + if "s1" in parts and "elbow" in parts: + ( + result + .constrain("s1/elbow_top?conn0", "elbow/parent_upper/top?conn0", "Plane") + .constrain("s1/elbow_top?conn1", "elbow/parent_upper/top?conn1", "Plane") + .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/bot?conn0", "Plane") + .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/bot?conn1", "Plane") + ) + if "s2" in parts: + result.add(self.assembly_s2(), name="s2") + if "s2" in parts and "elbow" in parts: + ( + result + .constrain("s2/elbow_top?conn0", "elbow/child/top?conn0", "Plane") + .constrain("s2/elbow_top?conn1", "elbow/child/top?conn1", "Plane") + .constrain("s2/elbow_bot?conn0", "elbow/child/bot?conn0", "Plane") + .constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane") + ) + if "wrist" in parts: + result.add(self.wrist_joint.assembly(), name="wrist") + if "s2" in parts and "wrist" in parts: + ( + result + .constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane") + .constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane") + .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane") + .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane") + ) + if "s3" in parts: + result.add(self.assembly_s3(), name="s3") + if "s3" in parts and "wrist" in parts: + ( + result + .constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane") + .constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane") + .constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane") + .constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane") + ) + + return result.solve() + + + -- 2.44.1 From c73675bbe36fe22940f78b025b6006291c488930 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 16 Jul 2024 11:55:38 -0700 Subject: [PATCH 072/187] feat: Colouring assembly by role and material --- nhf/materials.py | 102 +++++++++++++++++++++++++----- nhf/parts/joints.py | 10 +-- nhf/touhou/houjuu_nue/__init__.py | 4 -- nhf/touhou/houjuu_nue/harness.py | 8 ++- nhf/touhou/houjuu_nue/joints.py | 45 +++++++------ nhf/touhou/houjuu_nue/trident.py | 35 ++++++---- nhf/touhou/houjuu_nue/wing.py | 50 +++++++++------ nhf/utils.py | 6 +- 8 files changed, 180 insertions(+), 80 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index 2f96bda..9408164 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -1,30 +1,55 @@ """ A catalog of material properties """ -from enum import Enum +from enum import Enum, Flag, auto +from typing import Union, Optional import cadquery as Cq def _color(name: str, alpha: float) -> Cq.Color: r, g, b, _ = Cq.Color(name).toTuple() return Cq.Color(r, g, b, alpha) -class Role(Enum): +class Role(Flag): """ Describes the role of a part """ - # Parent and child components in a load bearing joint - PARENT = _color('blue4', 0.6) - CASING = _color('dodgerblue3', 0.6) - CHILD = _color('darkorange2', 0.6) - DAMPING = _color('springgreen', 0.5) - STRUCTURE = _color('gray', 0.4) - DECORATION = _color('lightseagreen', 0.4) - ELECTRONIC = _color('mediumorchid', 0.5) - MARKER = _color('cyan', 1.0) + PARENT = auto() + CHILD = auto() + CASING = auto() + DAMPING = auto() + STRUCTURE = auto() + DECORATION = auto() + ELECTRONIC = auto() + CONNECTION = auto() + + # Parent and child components in a load bearing joint + + def color_avg(self) -> Cq.Color: + r, g, b, a = zip(*[ROLE_COLOR_MAP[component].toTuple() for component in self]) + + def avg(li): + assert li + return sum(li) / len(li) + r, g, b, a = avg(r), avg(g), avg(b), avg(a) + return Cq.Color(r, g, b, a) + + def color_head(self) -> Cq.Color: + head = next(iter(self)) + return ROLE_COLOR_MAP[head] + +# Maps roles to their colours +ROLE_COLOR_MAP = { + Role.PARENT: _color('blue4', 0.6), + Role.CASING: _color('dodgerblue3', 0.6), + Role.CHILD: _color('darkorange2', 0.6), + Role.DAMPING: _color('springgreen', 0.8), + Role.STRUCTURE: _color('gray', 0.4), + Role.DECORATION: _color('lightseagreen', 0.4), + Role.ELECTRONIC: _color('mediumorchid', 0.5), + Role.CONNECTION: _color('steelblue3', 0.8) +} - def __init__(self, color: Cq.Color): - self.color = color class Material(Enum): """ @@ -32,11 +57,56 @@ class Material(Enum): """ WOOD_BIRCH = 0.8, _color('bisque', 0.9) - PLASTIC_PLA = 0.5, _color('azure3', 0.6) - RESIN_TRANSPARENT = 1.1, _color('cadetblue2', 0.6) - ACRYLIC_BLACK = 0.5, _color('gray50', 0.6) + PLASTIC_PLA = 0.5, _color('mistyrose', 0.8) + RESIN_TRANSPERENT = 1.1, _color('cadetblue2', 0.6) + RESIN_TOUGH_1500 = 1.1, _color('seashell3', 0.7) + ACRYLIC_BLACK = 0.5, _color('gray5', 0.8) + ACRYLIC_TRANSLUSCENT = 0.5, _color('ivory2', 0.8) ACRYLIC_TRANSPARENT = 0.5, _color('ghostwhite', 0.5) + STEEL_SPRING = 1.0, _color('gray', 0.8) def __init__(self, density: float, color: Cq.Color): self.density = density self.color = color + +def add_with_material_role( + self: Cq.Assembly, + obj: Union[Cq.Shape, Cq.Workplane, None], + loc: Optional[Cq.Location] = None, + name: Optional[str] = None, + material: Optional[Material] = None, + role: Optional[Role] = None) -> Cq.Assembly: + """ + Structural add function which allows specifying material and role + """ + metadata = {} + color = None + if material: + metadata["material"] = material + color = material.color + if role: + metadata["role"] = role + color = role.color_avg() + if len(metadata) == 0: + metadata = None + + self.add(obj, loc=loc, name=name, color=color, metadata=metadata) + return self + +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: + continue + a.color = a.metadata["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: + continue + role = a.metadata["role"] + a.color = role.color_avg() if avg else role.color_head() + return self +Cq.Assembly.color_by_role = color_by_role diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 9867b4f..77dcc94 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -141,8 +141,8 @@ class HirthJoint: angle = offset * self.tooth_angle result = ( Cq.Assembly() - .add(obj1, name="obj1", color=Role.PARENT.color) - .add(obj2, name="obj2", color=Role.CHILD.color) + .addS(obj1, name="obj1", role=Role.PARENT) + .addS(obj2, name="obj2", role=Role.CHILD) .constrain("obj1", "Fixed") .constrain("obj1?mate", "obj2?mate", "Plane") .constrain("obj1?dir", "obj2?dir", "Axis", param=angle) @@ -488,9 +488,9 @@ class TorsionJoint: spring = self.spring() result = ( Cq.Assembly() - .add(spring, name="spring", color=Role.DAMPING.color) - .add(track, name="track", color=Role.PARENT.color) - .add(rider, name="rider", color=Role.CHILD.color) + .addS(spring, name="spring", role=Role.DAMPING) + .addS(track, name="track", role=Role.PARENT) + .addS(rider, name="rider", role=Role.CHILD) ) TorsionJoint.add_constraints( result, diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 4e57fae..ea07fad 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -31,7 +31,6 @@ shoulder, elbow, wrist in analogy with human anatomy. """ from dataclasses import dataclass, field import cadquery as Cq -from nhf import Material, Role from nhf.build import Model, TargetKind, target, assembly from nhf.parts.joints import HirthJoint, TorsionJoint from nhf.parts.handle import Handle, BayonetMount @@ -99,9 +98,6 @@ class Parameters(Model): trident_terminal_hole_diam: float = 24 trident_terminal_bottom_thickness: float = 10 - material_panel: Material = Material.ACRYLIC_TRANSPARENT - material_bracket: Material = Material.ACRYLIC_TRANSPARENT - def __post_init__(self): super().__init__(name="houjuu-nue") self.harness.hs_hirth_joint = self.hs_hirth_joint diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index 3df4cf8..2bcba19 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -154,14 +154,18 @@ class Harness: harness = self.surface() result = ( Cq.Assembly() - .add(harness, name="base", color=Material.WOOD_BIRCH.color) + .addS(harness, name="base", + material=Material.WOOD_BIRCH, + role=Role.STRUCTURE) .constrain("base", "Fixed") ) for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: j = self.hs_joint_parent() ( result - .add(j, name=name, color=Role.PARENT.color) + .addS(j, name=name, + role=Role.PARENT, + material=Material.PLASTIC_PLA) #.constrain("base?mount", f"{name}?base", "Axis") ) for i in range(4): diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index d25c22c..05acc97 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from typing import Optional, Tuple import cadquery as Cq -from nhf import Role +from nhf import Material, Role from nhf.build import Model, target, assembly import nhf.parts.springs as springs from nhf.parts.joints import TorsionJoint @@ -182,21 +182,23 @@ class ShoulderJoint(Model): wing_root_wall_thickness: float = 25.4/16, ) -> Cq.Assembly: directrix = 0 + mat = Material.RESIN_TRANSPERENT + mat_spring = Material.STEEL_SPRING result = ( Cq.Assembly() - .add(self.child(), name="child", - color=Role.CHILD.color) + .addS(self.child(), name="child", + role=Role.CHILD, material=mat) .constrain("child/core", "Fixed") - .add(self.torsion_joint.spring(), name="spring_top", - color=Role.DAMPING.color) - .add(self.parent(wing_root_wall_thickness), - name="parent_top", - color=Role.PARENT.color) - .add(self.torsion_joint.spring(), name="spring_bot", - color=Role.DAMPING.color) - .add(self.parent(wing_root_wall_thickness), - name="parent_bot", - color=Role.PARENT.color) + .addS(self.torsion_joint.spring(), name="spring_top", + role=Role.DAMPING, material=mat_spring) + .addS(self.parent(wing_root_wall_thickness), + name="parent_top", + role=Role.PARENT, material=mat) + .addS(self.torsion_joint.spring(), name="spring_bot", + role=Role.DAMPING, material=mat_spring) + .addS(self.parent(wing_root_wall_thickness), + name="parent_bot", + role=Role.PARENT, material=mat) ) TorsionJoint.add_constraints(result, rider="child/rider_top", @@ -516,9 +518,9 @@ class DiskJoint(Model): assert 0 <= angle <= self.movement_angle result = ( Cq.Assembly() - .add(self.disk(), name="disk", color=Role.CHILD.color) - .add(self.housing_lower(), name="housing_lower", color=Role.PARENT.color) - .add(self.housing_upper(), name="housing_upper", color=Role.CASING.color) + .addS(self.disk(), name="disk", role=Role.CHILD) + .addS(self.housing_lower(), name="housing_lower", role=Role.PARENT) + .addS(self.housing_upper(), name="housing_upper", role=Role.CASING) #.constrain("housing_lower", "Fixed") ) result = self.add_constraints( @@ -564,6 +566,8 @@ class ElbowJoint: # Size of the mounting holes hole_diam: float = 8.0 + material: Material = Material.RESIN_TRANSPERENT + def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing @@ -638,9 +642,12 @@ class ElbowJoint: def assembly(self, angle: float = 0) -> Cq.Assembly: result = ( Cq.Assembly() - .add(self.child_joint(), name="child", color=Role.CHILD.color) - .add(self.parent_joint_lower(), name="parent_lower", color=Role.CASING.color) - .add(self.parent_joint_upper(), name="parent_upper", color=Role.PARENT.color) + .addS(self.child_joint(), name="child", + role=Role.CHILD, material=self.material) + .addS(self.parent_joint_lower(), name="parent_lower", + role=Role.CASING, material=self.material) + .addS(self.parent_joint_upper(), name="parent_upper", + role=Role.PARENT, material=self.material) #.constrain("child/disk?mate_bot", "Fixed") ) result = self.disk_joint.add_constraints( diff --git a/nhf/touhou/houjuu_nue/trident.py b/nhf/touhou/houjuu_nue/trident.py index 312ff85..bd5c5ea 100644 --- a/nhf/touhou/houjuu_nue/trident.py +++ b/nhf/touhou/houjuu_nue/trident.py @@ -1,6 +1,6 @@ import math import cadquery as Cq -from nhf import Material +from nhf import Material, Role from nhf.parts.handle import Handle def trident_assembly( @@ -16,27 +16,40 @@ def trident_assembly( .faces(">Z") .hole(15, terminal_height + handle.insertion_length - 10) ) - mat_i = Material.PLASTIC_PLA + mat_c = Material.PLASTIC_PLA + mat_i = Material.RESIN_TOUGH_1500 mat_s = Material.ACRYLIC_BLACK + role_i = Role.CONNECTION + role_c = Role.CONNECTION + role_s = Role.STRUCTURE assembly = ( Cq.Assembly() - .add(handle.insertion(), name="i0", color=mat_i.color) + .addS(handle.insertion(), name="i0", + material=mat_i, role=role_i) .constrain("i0", "Fixed") - .add(segment(), name="s1", color=mat_s.color) + .addS(segment(), name="s1", + material=mat_s, role=role_s) .constrain("i0?rim", "s1?mate1", "Plane", param=0) - .add(handle.insertion(), name="i1", color=mat_i.color) - .add(handle.connector(), name="c1", color=mat_i.color) - .add(handle.insertion(), name="i2", color=mat_i.color) + .addS(handle.insertion(), name="i1", + material=mat_i, role=role_i) + .addS(handle.connector(), name="c1", + material=mat_c, role=role_c) + .addS(handle.insertion(), name="i2", + material=mat_i, role=role_i) .constrain("s1?mate2", "i1?rim", "Plane", param=0) .constrain("i1?mate", "c1?mate1", "Plane") .constrain("i2?mate", "c1?mate2", "Plane") - .add(segment(), name="s2", color=mat_s.color) + .addS(segment(), name="s2", + material=mat_s, role=role_s) .constrain("i2?rim", "s2?mate1", "Plane", param=0) - .add(handle.insertion(), name="i3", color=mat_i.color) + .addS(handle.insertion(), name="i3", + material=mat_i, role=role_i) .constrain("s2?mate2", "i3?rim", "Plane", param=0) - .add(handle.one_side_connector(), name="head", color=mat_i.color) + .addS(handle.one_side_connector(), name="head", + material=mat_c, role=role_c) .constrain("i3?mate", "head?mate", "Plane") - .add(terminal, name="terminal", color=mat_i.color) + .addS(terminal, name="terminal", + material=mat_c, role=role_c) .constrain("i0?mate", "terminal?mate", "Plane") ) return assembly.solve() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b832dff..9d84552 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -63,8 +63,10 @@ class WingProfile(Model): ring_y: float = 20 ring_radius_inner: float = 22 - material_panel: Material = Material.ACRYLIC_TRANSPARENT - material_bracket: Material = Material.ACRYLIC_TRANSPARENT + mat_panel: Material = Material.ACRYLIC_TRANSLUSCENT + mat_bracket: Material = Material.ACRYLIC_TRANSPARENT + mat_hs_joint: Material = Material.PLASTIC_PLA + role_panel: Role = Role.STRUCTURE def __post_init__(self): super().__init__(name=self.name) @@ -170,8 +172,10 @@ class WingProfile(Model): def assembly_s0(self) -> Cq.Assembly: result = ( Cq.Assembly() - .add(self.surface_s0(top=True), name="bot", color=self.material_panel.color) - .add(self.surface_s0(top=False), name="top", color=self.material_panel.color) + .addS(self.surface_s0(top=True), name="bot", + material=self.mat_panel, role=self.role_panel) + .addS(self.surface_s0(top=False), name="top", + material=self.mat_panel, role=self.role_panel) .constrain("bot@faces@>Z", "top@faces@ Cq.Assembly: result = ( Cq.Assembly() - .add(self.surface_s1(front=True), name="front", - color=self.material_panel.color) + .addS(self.surface_s1(front=True), name="front", + material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") - .add(self.surface_s1(front=False), name="back", - color=self.material_panel.color) + .addS(self.surface_s1(front=False), name="back", + material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Assembly: result = ( Cq.Assembly() - .add(self.surface_s2(front=True), name="front", - color=self.material_panel.color) + .addS(self.surface_s2(front=True), name="front", + material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") - .add(self.surface_s2(front=False), name="back", - color=self.material_panel.color) + .addS(self.surface_s2(front=False), name="back", + material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Assembly: result = ( Cq.Assembly() - .add(self.surface_s3(front=True), name="front", - color=self.material_panel.color) + .addS(self.surface_s3(front=True), name="front", + material=self.mat_panel, role=self.role_panel) .constrain("front", "Fixed") - .add(self.surface_s3(front=False), name="back", - color=self.material_panel.color) + .addS(self.surface_s3(front=False), name="back", + material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Tuple[Cq.Location, str]: """ @@ -101,7 +103,7 @@ def to_marker_name(tag: str) -> str: def mark_point(self: Cq.Assembly, tag: str, size: float = 2, - color: Cq.Color = Role.MARKER.color) -> Cq.Assembly: + color: Cq.Color = COLOR_MARKER) -> Cq.Assembly: """ Adds a marker to make a point visible """ @@ -117,7 +119,7 @@ Cq.Assembly.markPoint = mark_point def mark_plane(self: Cq.Assembly, tag: str, size: float = 2, - color: Cq.Color = Role.MARKER.color) -> Cq.Assembly: + color: Cq.Color = COLOR_MARKER) -> Cq.Assembly: """ Adds a marker to make a plane visible """ -- 2.44.1 From cdb46263f8386761e1e840b0d615cda9143ab338 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 16 Jul 2024 12:03:51 -0700 Subject: [PATCH 073/187] fix: Ambiguous rotation in Hirth Joint --- nhf/parts/joints.py | 28 ++++++++++++++++++---------- nhf/touhou/houjuu_nue/__init__.py | 19 ++++++++++--------- nhf/touhou/houjuu_nue/wing.py | 9 ++++----- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 77dcc94..3ff76e9 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -101,7 +101,12 @@ class HirthJoint: ( result .polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True) - .tag("dir") + .tag("dirX") + ) + ( + result + .polyline([(0, 0, 0), (0, 1, 0)], forConstruction=True) + .tag("dirY") ) return result @@ -109,12 +114,15 @@ class HirthJoint: assembly: Cq.Assembly, parent: str, child: str, - angle: int = 0): + offset: int = 0): + angle = offset * self.tooth_angle ( assembly .constrain(f"{parent}?mate", f"{child}?mate", "Plane") - .constrain(f"{parent}?dir", f"{child}?dir", - "Axis", param=angle * self.tooth_angle) + .constrain(f"{parent}?dirX", f"{child}?dirX", + "Axis", param=angle) + .constrain(f"{parent}?dirY", f"{child}?dirX", + "Axis", param=90 - angle) ) def assembly(self, offset: int = 1): @@ -138,17 +146,17 @@ class HirthJoint: self.generate(is_mated=True) .union(tab) ) - angle = offset * self.tooth_angle result = ( Cq.Assembly() .addS(obj1, name="obj1", role=Role.PARENT) .addS(obj2, name="obj2", role=Role.CHILD) - .constrain("obj1", "Fixed") - .constrain("obj1?mate", "obj2?mate", "Plane") - .constrain("obj1?dir", "obj2?dir", "Axis", param=angle) - .solve() ) - return result + self.add_constraints( + result, + parent="obj1", + child="obj2", + offset=offset) + return result.solve() def comma_joint(radius=30, shaft_radius=10, diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index ea07fad..0a05a35 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -30,6 +30,7 @@ s1, s2, s3. The joints are named (from root to tip) 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.parts.joints import HirthJoint, TorsionJoint @@ -147,11 +148,11 @@ class Parameters(Model): @assembly() - def wing_r1_assembly(self) -> Cq.Assembly: - return self.wing_profile.assembly() + def wing_r1_assembly(self, parts: Optional[list[str]] = None) -> Cq.Assembly: + return self.wing_profile.assembly(parts) @assembly() - def wings_harness_assembly(self) -> Cq.Assembly: + def wings_harness_assembly(self, parts: Optional[list[str]] = None) -> Cq.Assembly: """ Assembly of harness with all the wings """ @@ -160,13 +161,13 @@ class Parameters(Model): result = ( Cq.Assembly() .add(self.harness_assembly(), name="harness", loc=Cq.Location((0, 0, 0))) - .add(self.wing_r1_assembly(), name="wing_r1") - .add(self.wing_r1_assembly(), name="wing_r2") - .add(self.wing_r1_assembly(), name="wing_r3") + .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") ) - self.hs_hirth_joint.add_constraints(result, "harness/r1", "wing_r1/s0/hs", angle=9) - self.hs_hirth_joint.add_constraints(result, "harness/r2", "wing_r2/s0/hs", angle=8) - self.hs_hirth_joint.add_constraints(result, "harness/r3", "wing_r3/s0/hs", angle=7) + 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) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 9d84552..b520384 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -192,7 +192,7 @@ class WingProfile(Model): .constrain(f"{tag}?top", f"top?{tag}", "Plane") .constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis") ) - hirth = self.base_joint.generate() + hirth = self.base_joint.generate(is_mated=True) ( result .addS(hirth, name="hs", role=Role.CHILD, material=self.mat_hs_joint) @@ -614,8 +614,7 @@ class WingProfile(Model): .constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane") .constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane") ) + if len(parts) > 1: + result.solve() - return result.solve() - - - + return result -- 2.44.1 From ef6b2a86633283b29f0b613f4968b4d6ac136a0d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 16 Jul 2024 13:28:53 -0700 Subject: [PATCH 074/187] refactor: Create class for torsion spring --- nhf/materials.py | 2 +- nhf/parts/joints.py | 173 ++++++-------------------------- nhf/parts/springs.py | 122 ++++++++++++---------- nhf/parts/test.py | 2 - nhf/touhou/houjuu_nue/joints.py | 161 +++++++++++++++-------------- nhf/touhou/houjuu_nue/wing.py | 24 +++-- 6 files changed, 201 insertions(+), 283 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index 9408164..f87b796 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -43,7 +43,7 @@ ROLE_COLOR_MAP = { Role.PARENT: _color('blue4', 0.6), Role.CASING: _color('dodgerblue3', 0.6), Role.CHILD: _color('darkorange2', 0.6), - Role.DAMPING: _color('springgreen', 0.8), + Role.DAMPING: _color('springgreen', 1.0), Role.STRUCTURE: _color('gray', 0.4), Role.DECORATION: _color('lightseagreen', 0.4), Role.ELECTRONIC: _color('mediumorchid', 0.5), diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 3ff76e9..594c2cb 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -1,8 +1,8 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional import math import cadquery as Cq -import nhf.parts.springs as springs +from nhf.parts.springs import TorsionSpring from nhf import Role import nhf.utils @@ -158,99 +158,6 @@ class HirthJoint: offset=offset) return result.solve() -def comma_joint(radius=30, - shaft_radius=10, - height=10, - flange=10, - flange_thickness=25, - n_serration=16, - serration_angle_offset=0, - serration_height=5, - serration_inner_radius=20, - serration_theta=2 * math.pi / 48, - serration_tilt=-30, - right_handed=False): - """ - Produces a "o_" shaped joint, with serrations to accomodate a torsion spring - """ - assert flange_thickness <= radius - flange_poly = [ - (0, radius - flange_thickness), - (0, radius), - (flange + radius, radius), - (flange + radius, radius - flange_thickness) - ] - if right_handed: - flange_poly = [(x, -y) for x,y in flange_poly] - sketch = ( - Cq.Sketch() - .circle(radius) - .polygon(flange_poly, mode='a') - .circle(shaft_radius, mode='s') - ) - serration_poly = [ - (0, 0), (radius, 0), - (radius, radius * math.tan(serration_theta)) - ] - serration = ( - Cq.Workplane('XY') - .sketch() - .polygon(serration_poly) - .circle(radius, mode='i') - .circle(serration_inner_radius, mode='s') - .finalize() - .extrude(serration_height) - .translate(Cq.Vector((-serration_inner_radius, 0, height))) - .rotate( - axisStartPoint=(0, 0, 0), - axisEndPoint=(0, 0, height), - angleDegrees=serration_tilt) - .val() - ) - serrations = ( - Cq.Workplane('XY') - .polarArray(radius=serration_inner_radius, - startAngle=0+serration_angle_offset, - angle=360+serration_angle_offset, - count=n_serration) - .eachpoint(lambda loc: serration.located(loc)) - ) - result = ( - Cq.Workplane() - .add(sketch) - .extrude(height) - .union(serrations) - .clean() - ) - - result.polyline([ - (0, 0, height - serration_height), - (0, 0, height + serration_height)], - forConstruction=True).tag("serrated") - result.polyline([ - (0, radius, 0), - (flange + radius, radius, 0)], - forConstruction=True).tag("tail") - result.faces('>X').tag("tail_end") - return result - -def comma_assembly(): - joint1 = comma_joint() - joint2 = comma_joint() - spring = springs.torsion_spring() - result = ( - Cq.Assembly() - .add(joint1, name="joint1", color=Cq.Color(0.8,0.8,0.5,0.3)) - .add(joint2, name="joint2", color=Cq.Color(0.8,0.8,0.5,0.3)) - .add(spring, name="spring", color=Cq.Color(0.5,0.5,0.5,1)) - .constrain("joint1?serrated", "spring?bot", "Plane") - .constrain("joint2?serrated", "spring?top", "Plane") - .constrain("joint1?tail", "FixedAxis", (1, 0, 0)) - .constrain("joint2?tail", "FixedAxis", (-1, 0, 0)) - .solve() - ) - return result - @dataclass class TorsionJoint: """ @@ -268,6 +175,13 @@ class TorsionJoint: 2. A slotted annular extrusion where the slot allows the spring to rest 3. An outer and an inner annuli which forms a track the rider can move on """ + spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( + radius=10.0, + thickness=2.0, + height=15.0, + tail_length=35.0, + right_handed=False, + )) # Radius limit for rotating components radius_track: float = 40 @@ -275,7 +189,6 @@ class TorsionJoint: track_disk_height: float = 10 rider_disk_height: float = 8 - radius_spring: float = 15 radius_axle: float = 6 # If true, cover the spring hole. May make it difficult to insert the spring @@ -283,12 +196,6 @@ class TorsionJoint: spring_hole_cover_track: bool = False spring_hole_cover_rider: bool = False - # Also used for the height of the hole for the spring - spring_thickness: float = 2 - spring_height: float = 15 - - spring_tail_length: float = 35 - groove_radius_outer: float = 35 groove_radius_inner: float = 20 # Gap on inner groove to ease movement @@ -301,23 +208,19 @@ class TorsionJoint: rider_slot_begin: float = 0 rider_slot_span: float = 90 - right_handed: bool = False - def __post_init__(self): assert self.radius_track > self.groove_radius_outer - assert self.radius_rider > self.groove_radius_outer - assert self.groove_radius_outer > self.groove_radius_inner + self.groove_inner_gap - assert self.groove_radius_inner > self.radius_spring - assert self.spring_height > self.groove_depth, "Groove is too deep" - assert self.radius_spring > self.radius_axle + assert self.radius_rider > self.groove_radius_outer > self.groove_radius_inner + self.groove_inner_gap + assert self.groove_radius_inner > self.spring.radius > self.radius_axle + assert self.spring.height > self.groove_depth, "Groove is too deep" @property def total_height(self): """ Total height counting from bottom to top """ - return self.track_disk_height + self.rider_disk_height + self.spring_height + return self.track_disk_height + self.rider_disk_height + self.spring.height @property def radius(self): @@ -326,28 +229,24 @@ class TorsionJoint: """ return max(self.radius_rider, self.radius_track) - @property - def _radius_spring_internal(self): - return self.radius_spring - self.spring_thickness - def _slot_polygon(self, flip: bool=False): - r1 = self.radius_spring - self.spring_thickness - r2 = self.radius_spring - flip = flip != self.right_handed + r1 = self.spring.radius_inner + r2 = self.spring.radius + flip = flip != self.spring.right_handed if flip: r1 = -r1 r2 = -r2 return [ (0, r2), - (self.spring_tail_length, r2), - (self.spring_tail_length, r1), + (self.spring.tail_length, r2), + (self.spring.tail_length, r1), (0, r1), ] def _directrix(self, height, theta=0): c, s = math.cos(theta), math.sin(theta) - r2 = self.radius_spring - l = self.spring_tail_length - if self.right_handed: + r2 = self.spring.radius + l = self.spring.tail_length + if self.spring.right_handed: r2 = -r2 # This is (0, r2) and (l, r2) transformed by right handed rotation # matrix `[[c, -s], [s, c]]` @@ -356,16 +255,6 @@ class TorsionJoint: (c * l - s * r2, s * l + c * r2, height), ] - - def spring(self): - return springs.torsion_spring( - radius=self.radius_spring, - height=self.spring_height, - thickness=self.spring_thickness, - tail_length=self.spring_tail_length, - right_handed=self.right_handed, - ) - def track(self): # TODO: Cover outer part of track only. Can we do this? groove_profile = ( @@ -373,14 +262,14 @@ class TorsionJoint: .circle(self.radius_track) .circle(self.groove_radius_outer, mode='s') .circle(self.groove_radius_inner, mode='a') - .circle(self.radius_spring, mode='s') + .circle(self.spring.radius, mode='s') ) spring_hole_profile = ( Cq.Sketch() .circle(self.radius_track) - .circle(self.radius_spring, mode='s') + .circle(self.spring.radius, mode='s') ) - slot_height = self.spring_thickness + slot_height = self.spring.thickness if not self.spring_hole_cover_track: slot_height += self.groove_depth slot = ( @@ -400,7 +289,7 @@ class TorsionJoint: .faces('>Z') .tag("spring") .placeSketch(spring_hole_profile) - .extrude(self.spring_thickness) + .extrude(self.spring.thickness) # If the spring hole profile is not simply connected, this workplane # will have to be created from the `spring-mate` face. .faces('>Z') @@ -425,7 +314,7 @@ class TorsionJoint: wall_profile = ( Cq.Sketch() .circle(self.radius_rider, mode='a') - .circle(self.radius_spring, mode='s') + .circle(self.spring.radius, mode='s') .parray( r=0, a1=rider_slot_begin, @@ -451,7 +340,7 @@ class TorsionJoint: .reset() ) #.circle(self._radius_wall, mode='a') - middle_height = self.spring_height - self.groove_depth - self.rider_gap - self.spring_thickness + middle_height = self.spring.height - self.groove_depth - self.rider_gap - self.spring.thickness result = ( Cq.Workplane('XY') .cylinder( @@ -471,8 +360,8 @@ class TorsionJoint: .extrude(self.groove_depth + self.rider_gap) .faces(tag="spring") .workplane() - .circle(self._radius_spring_internal) - .extrude(self.spring_height) + .circle(self.spring.radius_inner) + .extrude(self.spring.height) .faces("Z").tag("top") - base.faces(" float: + return self.radius - self.thickness + + def torque_at(self, theta: float) -> float: + return self.torsion_rate * theta + + def generate(self, deflection: float = 0): + omega = self.angle_neutral + deflection + omega = -omega if self.right_handed else omega + base = ( + Cq.Workplane('XY') + .cylinder(height=self.height, radius=self.radius, + centered=(True, True, False)) + ) + base.faces(">Z").tag("top") + base.faces(" self.radius_axle assert self.housing_upper_carve_offset > 0 - def spring(self): - return springs.torsion_spring( - radius=self.radius_spring, - height=self.spring_height, - thickness=self.spring_thickness, - tail_length=self.spring_tail_length, - right_handed=False, - ) - @property def neutral_movement_angle(self) -> Optional[float]: - a = self.spring_angle_neutral - self.spring_angle + a = self.spring.angle_neutral - self.spring_angle_at_0 if 0 <= a and a <= self.movement_angle: return a return None @@ -346,7 +342,7 @@ class DiskJoint(Model): """ Distance between the spring track and the outside of the upper housing """ - return self.housing_thickness + self.disk_thickness - self.spring_height + return self.housing_thickness + self.disk_thickness - self.spring.height @property def housing_upper_dz(self) -> float: @@ -355,42 +351,17 @@ class DiskJoint(Model): """ return self.total_thickness / 2 - self.housing_thickness - @property - def radius_spring_internal(self): - return self.radius_spring - self.spring_thickness - @target(name="disk") def disk(self) -> Cq.Workplane: cut = ( Cq.Solid.makeBox( - length=self.spring_tail_length, - width=self.spring_thickness, + length=self.spring.tail_length, + width=self.spring.thickness, height=self.disk_thickness, ) - .located(Cq.Location((0, self.radius_spring_internal, 0))) - .rotate((0, 0, 0), (0, 0, 1), self.spring_angle_shift) + .located(Cq.Location((0, self.spring.radius_inner, 0))) + .rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset) ) - result = ( - Cq.Workplane('XY') - .cylinder( - height=self.disk_thickness, - radius=self.radius_disk, - centered=(True, True, False) - ) - .copyWorkplane(Cq.Workplane('XY')) - .cylinder( - height=self.disk_thickness, - radius=self.radius_spring, - centered=(True, True, False), - combine='cut', - ) - .cut(cut) - ) - plane = result.copyWorkplane(Cq.Workplane('XY')) - plane.tagPlane("dir", direction="+X") - plane.workplane(offset=self.disk_thickness).tagPlane("mate_top") - result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot") - radius_tongue = self.radius_disk + self.tongue_length tongue = ( Cq.Solid.makeCylinder( @@ -402,7 +373,29 @@ class DiskJoint(Model): radius=self.radius_disk, )) ) - result = result.union(tongue, tol=TOL) + result = ( + Cq.Workplane('XY') + .cylinder( + height=self.disk_thickness, + radius=self.radius_disk, + centered=(True, True, False) + ) + .union(tongue, tol=TOL) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=self.disk_thickness, + radius=self.spring.radius, + centered=(True, True, False), + combine='cut', + ) + .cut(cut) + ) + plane = result.copyWorkplane(Cq.Workplane('XY')) + theta = math.radians(self.spring_slot_offset) + plane.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0)) + plane.workplane(offset=self.disk_thickness).tagPlane("mate_top") + result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot") + return result def wall(self) -> Cq.Compound: @@ -433,9 +426,6 @@ class DiskJoint(Model): ) result.faces(">Z").tag("mate") result.faces(">Z").workplane().tagPlane("dirX", direction="+X") - # two directional vectors are required to make the angle constrain - # unambiguous - result.faces(">Z").workplane().tagPlane("dirY", direction="+Y") result = result.cut( self .wall() @@ -447,16 +437,17 @@ class DiskJoint(Model): @target(name="housing-upper") def housing_upper(self) -> Cq.Workplane: + carve_angle = -(self.spring_angle_at_0 - self.spring_slot_offset) carve = ( Cq.Solid.makeCylinder( - radius=self.radius_spring, + radius=self.spring.radius, height=self.housing_thickness ).fuse(Cq.Solid.makeBox( - length=self.spring_tail_length, - width=self.spring_thickness, + length=self.spring.tail_length, + width=self.spring.thickness, height=self.housing_thickness - ).located(Cq.Location((0, -self.radius_spring, 0)))) - ).rotate((0, 0, 0), (0, 0, 1), self.spring_angle - self.spring_angle_shift) + ).located(Cq.Location((0, -self.spring.radius, 0)))) + ).rotate((0, 0, 0), (0, 0, 1), carve_angle) result = ( Cq.Workplane('XY') .cylinder( @@ -465,8 +456,11 @@ class DiskJoint(Model): centered=(True, True, False), ) ) + theta = math.radians(carve_angle) result.faces("Z").hole(self.radius_axle * 2) # tube which holds the spring interior @@ -490,28 +484,42 @@ class DiskJoint(Model): .cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset)))) .union(wall, tol=TOL) ) - return result + return result.clean() def add_constraints(self, assembly: Cq.Assembly, housing_lower: str, housing_upper: str, disk: str, - angle: float, + angle: float = 0.0, ) -> Cq.Assembly: - return ( + deflection = angle - self.neutral_movement_angle + spring_name = disk.replace("/", "__Z") + "_spring" + ( assembly + .addS( + self.spring.generate(deflection=-deflection), + name=spring_name, + role=Role.DAMPING, + material=Material.STEEL_SPRING) .constrain(f"{disk}?mate_bot", f"{housing_lower}?mate", "Plane") .constrain(f"{disk}?mate_top", f"{housing_upper}?mate", "Plane") - .constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0) - .constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle) - .constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90) + .constrain(f"{housing_lower}?dirX", f"{housing_upper}?dirX", "Axis", param=0) + .constrain(f"{housing_upper}?dir", f"{spring_name}?dir_top", "Axis", param=0) + .constrain(f"{spring_name}?dir_bot", f"{disk}?dir", "Axis", param=0) + .constrain(f"{disk}?mate_bot", f"{spring_name}?bot", "Plane", param=0) + #.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0) + #.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle) + #.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90) + ) + return ( + assembly ) def assembly(self, angle: Optional[float] = 0) -> Cq.Assembly: if angle is None: - angle = self.movement_angle + angle = self.neutral_movement_angle if angle is None: angle = 0 else: @@ -521,7 +529,7 @@ class DiskJoint(Model): .addS(self.disk(), name="disk", role=Role.CHILD) .addS(self.housing_lower(), name="housing_lower", role=Role.PARENT) .addS(self.housing_upper(), name="housing_upper", role=Role.CASING) - #.constrain("housing_lower", "Fixed") + .constrain("housing_lower", "Fixed") ) result = self.add_constraints( result, @@ -627,10 +635,15 @@ class ElbowJoint: .rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2) ) housing = self.disk_joint.housing_upper() + housing_loc = Cq.Location( + (0, 0, housing_dz), + (0, 0, 1), + -self.disk_joint.tongue_span / 2 + ) result = ( self.parent_beam.beam() .add(housing, name="housing", - loc=axial_offset * Cq.Location((0, 0, housing_dz))) + loc=axial_offset * housing_loc) .add(connector, name="connector", loc=axial_offset) #.constrain("housing", "Fixed") diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b520384..99fcde2 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -545,7 +545,8 @@ class WingProfile(Model): return result.solve() def assembly(self, - parts: Optional[list[str]] = None + parts: Optional[list[str]] = None, + angle_elbow_wrist: float = 0.0, ) -> Cq.Assembly(): if parts is None: parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] @@ -575,7 +576,7 @@ class WingProfile(Model): .constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane") ) if "elbow" in parts: - result.add(self.elbow_joint.assembly(), name="elbow") + result.add(self.elbow_joint.assembly(angle=angle_elbow_wrist), name="elbow") if "s1" in parts and "elbow" in parts: ( result @@ -595,24 +596,25 @@ class WingProfile(Model): .constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane") ) if "wrist" in parts: - result.add(self.wrist_joint.assembly(), name="wrist") + result.add(self.wrist_joint.assembly(angle=angle_elbow_wrist), name="wrist") if "s2" in parts and "wrist" in parts: + # Mounted backwards to bend in other direction ( result - .constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane") - .constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane") - .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane") - .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane") + .constrain("s2/wrist_top?conn0", "wrist/parent_upper/bot?conn0", "Plane") + .constrain("s2/wrist_top?conn1", "wrist/parent_upper/bot?conn1", "Plane") + .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/top?conn0", "Plane") + .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/top?conn1", "Plane") ) if "s3" in parts: result.add(self.assembly_s3(), name="s3") if "s3" in parts and "wrist" in parts: ( result - .constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane") - .constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane") - .constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane") - .constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane") + .constrain("s3/wrist_top?conn0", "wrist/child/bot?conn0", "Plane") + .constrain("s3/wrist_top?conn1", "wrist/child/bot?conn1", "Plane") + .constrain("s3/wrist_bot?conn0", "wrist/child/top?conn0", "Plane") + .constrain("s3/wrist_bot?conn1", "wrist/child/top?conn1", "Plane") ) if len(parts) > 1: result.solve() -- 2.44.1 From 0cc6100d0ed89b578d9de969074913fbe0d44132 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 16 Jul 2024 14:25:17 -0700 Subject: [PATCH 075/187] refactor: Move flip to ElbowJoint --- nhf/touhou/houjuu_nue/__init__.py | 2 -- nhf/touhou/houjuu_nue/joints.py | 22 +++++++++++++------ nhf/touhou/houjuu_nue/wing.py | 36 ++++++++++++++++++++----------- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 0a05a35..d9b3f26 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -65,8 +65,6 @@ class Parameters(Model): shoulder_joint=MJ.ShoulderJoint( height=100.0, ), - elbow_joint=MJ.ElbowJoint(), - wrist_joint=MJ.ElbowJoint(), elbow_height=110.0, )) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 5d2b895..cff350e 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -261,18 +261,22 @@ class Beam: plane.moveTo(-dx, 0).tagPlane("conn0") return result - def beam(self) -> Cq.Assembly: + def generate(self, flip: bool = False) -> Cq.Assembly: beam = ( Cq.Workplane('XZ') .box(self.spine_length, self.spine_thickness, self.spine_height) ) h = self.spine_height / 2 + self.foot_height + + tag_p, tag_n = "top", "bot" + if flip: + tag_p, tag_n = tag_n, tag_p result = ( Cq.Assembly() .add(beam, name="beam") - .add(self.foot(), name="top", + .add(self.foot(), name=tag_p, loc=Cq.Location((0, h, 0))) - .add(self.foot(), name="bot", + .add(self.foot(), name=tag_n, loc=Cq.Location((0, -h, 0), (1, 0, 0), 180)) ) return result @@ -301,7 +305,7 @@ class DiskJoint(Model): #disk_thickness_gap: float = 0.1 # Spring angle at 0 degrees of movement - spring_angle_at_0: float = 30.0 + spring_angle_at_0: float = 60.0 spring_slot_offset: float = 15.0 wall_inset: float = 2.0 @@ -493,6 +497,7 @@ class DiskJoint(Model): disk: str, angle: float = 0.0, ) -> Cq.Assembly: + assert 0 <= angle <= self.movement_angle deflection = angle - self.neutral_movement_angle spring_name = disk.replace("/", "__Z") + "_spring" ( @@ -541,7 +546,7 @@ class DiskJoint(Model): return result.solve() -@dataclass +@dataclass(kw_only=True) class ElbowJoint: """ Creates the elbow and wrist joints. @@ -576,6 +581,9 @@ class ElbowJoint: material: Material = Material.RESIN_TRANSPERENT + # If true, flip the top and bottom tags + flip: bool = False + def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing @@ -605,7 +613,7 @@ class ElbowJoint: flip_x = Cq.Location((0, 0, 0), (1, 0, 0), 180) flip_z = Cq.Location((0, 0, 0), (0, 0, 1), 180) result = ( - self.child_beam.beam() + self.child_beam.generate(flip=self.flip) .add(self.disk_joint.disk(), name="disk", loc=flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) #.constrain("disk", "Fixed") @@ -641,7 +649,7 @@ class ElbowJoint: -self.disk_joint.tongue_span / 2 ) result = ( - self.parent_beam.beam() + self.parent_beam.generate(flip=self.flip) .add(housing, name="housing", loc=axial_offset * housing_loc) .add(connector, name="connector", diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 99fcde2..c1e038f 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -11,10 +11,10 @@ from nhf import Material, Role from nhf.build import Model, target, assembly 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 +from nhf.touhou.houjuu_nue.joints import ShoulderJoint, ElbowJoint, DiskJoint import nhf.utils -@dataclass +@dataclass(kw_only=True) class WingProfile(Model): name: str = "wing" @@ -34,7 +34,12 @@ class WingProfile(Model): s1_thickness: float = 25.0 - elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint()) + elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + disk_joint=DiskJoint( + movement_angle=55, + ), + flip=False, + )) elbow_height: float = 100 elbow_x: float = 240 elbow_y: float = 30 @@ -43,7 +48,12 @@ class WingProfile(Model): s2_thickness: float = 25.0 - wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint()) + wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + disk_joint=DiskJoint( + movement_angle=45, + ), + flip=True, + )) wrist_height: float = 70 # Bottom point of the wrist wrist_x: float = 400 @@ -548,6 +558,8 @@ class WingProfile(Model): parts: Optional[list[str]] = None, angle_elbow_wrist: float = 0.0, ) -> Cq.Assembly(): + assert not self.elbow_joint.flip + assert self.wrist_joint.flip if parts is None: parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] result = ( @@ -601,20 +613,20 @@ class WingProfile(Model): # Mounted backwards to bend in other direction ( result - .constrain("s2/wrist_top?conn0", "wrist/parent_upper/bot?conn0", "Plane") - .constrain("s2/wrist_top?conn1", "wrist/parent_upper/bot?conn1", "Plane") - .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/top?conn0", "Plane") - .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/top?conn1", "Plane") + .constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane") + .constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane") + .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane") + .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane") ) if "s3" in parts: result.add(self.assembly_s3(), name="s3") if "s3" in parts and "wrist" in parts: ( result - .constrain("s3/wrist_top?conn0", "wrist/child/bot?conn0", "Plane") - .constrain("s3/wrist_top?conn1", "wrist/child/bot?conn1", "Plane") - .constrain("s3/wrist_bot?conn0", "wrist/child/top?conn0", "Plane") - .constrain("s3/wrist_bot?conn1", "wrist/child/top?conn1", "Plane") + .constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane") + .constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane") + .constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane") + .constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane") ) if len(parts) > 1: result.solve() -- 2.44.1 From 66b26fa056cc2d7839ab82b4bb2549d4215c55cc Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 16 Jul 2024 15:42:39 -0700 Subject: [PATCH 076/187] feat: Submodel in build system --- nhf/build.py | 106 ++++++++++++++++++++++-- nhf/parts/box.py | 4 +- nhf/touhou/houjuu_nue/__init__.py | 125 ++++++---------------------- nhf/touhou/houjuu_nue/harness.py | 9 +- nhf/touhou/houjuu_nue/trident.py | 133 +++++++++++++++++++----------- nhf/touhou/houjuu_nue/wing.py | 25 +++++- 6 files changed, 239 insertions(+), 163 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index 9a16a1d..3db9c36 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -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 """ - return f"{self.name}.{self.kind.ext}" + 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"" + 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)) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index a95f512..842cf8b 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -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]: diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index d9b3f26..f628ede 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -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").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__': diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index 2bcba19..d5a35cb 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -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(" Cq.Assembly: harness = self.surface() result = ( diff --git a/nhf/touhou/houjuu_nue/trident.py b/nhf/touhou/houjuu_nue/trident.py index bd5c5ea..bfc435f 100644 --- a/nhf/touhou/houjuu_nue/trident.py +++ b/nhf/touhou/houjuu_nue/trident.py @@ -1,55 +1,88 @@ 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): - def segment(): - return handle.segment(handle_segment_length) +@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 - terminal = ( - handle - .one_side_connector(height=terminal_height) - .faces(">Z") - .hole(15, terminal_height + handle.insertion_length - 10) - ) - mat_c = Material.PLASTIC_PLA - mat_i = Material.RESIN_TOUGH_1500 - mat_s = Material.ACRYLIC_BLACK - role_i = Role.CONNECTION - role_c = Role.CONNECTION - role_s = Role.STRUCTURE - assembly = ( - Cq.Assembly() - .addS(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", - material=mat_i, role=role_i) - .addS(handle.connector(), name="c1", - material=mat_c, role=role_c) - .addS(handle.insertion(), name="i2", - material=mat_i, role=role_i) - .constrain("s1?mate2", "i1?rim", "Plane", param=0) - .constrain("i1?mate", "c1?mate1", "Plane") - .constrain("i2?mate", "c1?mate2", "Plane") - .addS(segment(), name="s2", - material=mat_s, role=role_s) - .constrain("i2?rim", "s2?mate1", "Plane", param=0) - .addS(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", - 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() + @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").hole(self.terminal_hole_diam, depth=h) + return result + + @assembly() + def assembly(self): + def segment(): + return self.handle.segment(self.segment_length) + + terminal = ( + self.handle + .one_side_connector(height=self.terminal_height) + .faces(">Z") + .hole(15, self.terminal_height + self.handle.insertion_length - 10) + ) + mat_c = Material.PLASTIC_PLA + mat_i = Material.RESIN_TOUGH_1500 + mat_s = Material.ACRYLIC_BLACK + role_i = Role.CONNECTION + role_c = Role.CONNECTION + role_s = Role.STRUCTURE + a = ( + Cq.Assembly() + .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(self.handle.insertion(), name="i1", + material=mat_i, role=role_i) + .addS(self.handle.connector(), name="c1", + material=mat_c, role=role_c) + .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") + .constrain("i2?mate", "c1?mate2", "Plane") + .addS(segment(), name="s2", + material=mat_s, role=role_s) + .constrain("i2?rim", "s2?mate1", "Plane", param=0) + .addS(self.handle.insertion(), name="i3", + material=mat_i, role=role_i) + .constrain("s2?mate2", "i3?rim", "Plane", param=0) + .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 a.solve() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index c1e038f..fbb6c72 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -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, -- 2.44.1 From bc5a7df30fa3ddbcd515b0c25ee2f56667add483 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 16 Jul 2024 17:18:28 -0700 Subject: [PATCH 077/187] feat: Left side wing --- nhf/parts/box.py | 8 +- nhf/touhou/houjuu_nue/__init__.py | 48 +++- nhf/touhou/houjuu_nue/test.py | 17 +- nhf/touhou/houjuu_nue/wing.py | 351 +++++++++++++++++++++--------- poetry.lock | 281 +++++++++++------------- pyproject.toml | 5 +- 6 files changed, 441 insertions(+), 269 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 842cf8b..109c667 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -54,6 +54,8 @@ class MountingBox(Model): generate_side_tags: bool = True + flip_y: bool = False + @target(kind=TargetKind.DXF) def profile(self) -> Cq.Sketch: bx, by = 0, 0 @@ -88,8 +90,10 @@ class MountingBox(Model): if self.generate_side_tags: result.faces("Z").val().Center()).tagPlane("left") result.faces(">Y").workplane(origin=result.vertices("Y and >Z").val().Center()).tagPlane("right") - result.faces("Z").val().Center()).tagPlane("bot") - result.faces(">X").workplane(origin=result.vertices(">X and Z").val().Center()).tagPlane("top") + + c_y = ">Y" if self.flip_y else "Z").val().Center()).tagPlane("bot") + result.faces(">X").workplane(origin=result.vertices(f">X and {c_y} and >Z").val().Center()).tagPlane("top") result.faces(">Z").tag("dir") return result diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index f628ede..1dabd04 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -57,9 +57,21 @@ class Parameters(Model): n_tooth=24 )) - 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")) + wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR(name="r1")) + wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR(name="r2")) + wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR(name="r3")) + wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL( + name="l1", + wrist_angle=-45.0, + )) + wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( + name="l2", + wrist_angle=-30.0, + )) + wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL( + name="l3", + wrist_angle=0.0, + )) trident: MT.Trident = field(default_factory=lambda: MT.Trident()) @@ -83,6 +95,15 @@ class Parameters(Model): @submodel(name="wing-r3") def submodel_wing_r3(self) -> Model: return self.wing_r3 + @submodel(name="wing-r1") + def submodel_wing_l1(self) -> Model: + return self.wing_l1 + @submodel(name="wing-l2") + def submodel_wing_l2(self) -> Model: + return self.wing_l2 + @submodel(name="wing-l3") + def submodel_wing_l3(self) -> Model: + return self.wing_l3 @assembly() def wings_harness_assembly(self, parts: Optional[list[str]] = None) -> Cq.Assembly: @@ -95,16 +116,31 @@ class Parameters(Model): .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") + .add(self.wing_l1.assembly(parts), name="wing_l1") + .add(self.wing_l2.assembly(parts), name="wing_l2") + .add(self.wing_l3.assembly(parts), name="wing_l3") ) - 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) + self.hs_hirth_joint.add_constraints(result, "harness/r1", "wing_r1/s0/hs", offset=11) + self.hs_hirth_joint.add_constraints(result, "harness/r2", "wing_r2/s0/hs", offset=10) + self.hs_hirth_joint.add_constraints(result, "harness/r3", "wing_r3/s0/hs", offset=9) + self.hs_hirth_joint.add_constraints(result, "harness/l1", "wing_l1/s0/hs", offset=6) + self.hs_hirth_joint.add_constraints(result, "harness/l2", "wing_l2/s0/hs", offset=7) + self.hs_hirth_joint.add_constraints(result, "harness/l3", "wing_l3/s0/hs", offset=8) return result.solve() @submodel(name="trident") def submodel_trident(self) -> Model: return self.trident + def stat(self) -> dict[str, float]: + a = self.wings_harness_assembly() + bbox = a.toCompound().BoundingBox() + return { + "wing-span": bbox.xlen, + "wing-thickness": bbox.ylen, + "wing-height": bbox.zlen, + } + if __name__ == '__main__': p = Parameters() diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index cf839a7..ea10f8a 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -27,26 +27,15 @@ class Test(unittest.TestCase): def test_hs_joint_parent(self): p = M.Parameters() - obj = p.hs_joint_parent() + obj = p.harness.hs_joint_parent() self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece") - def test_wing_root(self): - p = M.Parameters() - obj = p.wing_root() - assert isinstance(obj, Cq.Assembly) - #self.assertIsInstance(obj.solids(), Cq.Solid, msg="Wing root must be in one piece") - bbox = obj.toCompound().BoundingBox() - - msg = "Must fix 256^3 bbox" - self.assertLess(bbox.xlen, 255, msg=msg) - self.assertLess(bbox.ylen, 255, msg=msg) - self.assertLess(bbox.zlen, 255, msg=msg) def test_wings_assembly(self): p = M.Parameters() - p.wings_assembly() + p.wings_harness_assembly() def test_trident_assembly(self): p = M.Parameters() - assembly = p.trident_assembly() + assembly = p.trident.assembly() bbox = assembly.toCompound().BoundingBox() length = bbox.zlen self.assertGreater(length, 1300) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index fbb6c72..26f07a6 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -19,7 +19,10 @@ class WingProfile(Model): name: str = "wing" - base_joint: HirthJoint = field(default_factory=lambda: HirthJoint()) + base_joint: HirthJoint = field(default_factory=lambda: HirthJoint( + radius=30.0, + radius_inner=20.0, + )) root_width: float = 80.0 hs_joint_corner_dx: float = 30.0 hs_joint_corner_hole_diam: float = 6.0 @@ -28,7 +31,7 @@ class WingProfile(Model): spacer_thickness: float = 25.4 / 8 shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( - height=100.0, + height=60.0, )) shoulder_width: float = 30.0 shoulder_tip_x: float = -200.0 @@ -42,11 +45,6 @@ class WingProfile(Model): ), flip=False, )) - elbow_height: float = 110 - elbow_x: float = 240 - elbow_y: float = 30 - # Tilt of elbow w.r.t. shoulder - elbow_angle: float = 20 s2_thickness: float = 25.0 @@ -56,33 +54,28 @@ class WingProfile(Model): ), flip=True, )) - wrist_height: float = 70 - # Bottom point of the wrist - wrist_x: float = 400 - wrist_y: float = 200 - - # Tile of wrist w.r.t. shoulder - wrist_angle: float = 40 s3_thickness: float = 25.0 - # Extends from the wrist to the tip of the arrow - arrow_height: float = 300 - arrow_angle: float = 7 - - # Relative (in wrist coordinate) centre of the ring - ring_x: float = 40 - ring_y: float = 20 - ring_radius_inner: float = 22 - mat_panel: Material = Material.ACRYLIC_TRANSLUSCENT mat_bracket: Material = Material.ACRYLIC_TRANSPARENT mat_hs_joint: Material = Material.PLASTIC_PLA role_panel: Role = Role.STRUCTURE + # Subclass must populate + elbow_x: float + elbow_y: float + elbow_angle: float + elbow_height: float + wrist_x: float + wrist_y: float + wrist_angle: float + wrist_height: float + + flip: bool = False + def __post_init__(self): super().__init__(name=self.name) - assert self.ring_radius > self.ring_radius_inner self.elbow_theta = math.radians(self.elbow_angle) self.elbow_c = math.cos(self.elbow_theta) @@ -92,20 +85,15 @@ class WingProfile(Model): self.wrist_c = math.cos(self.wrist_theta) self.wrist_s = math.sin(self.wrist_theta) self.wrist_top_x, self.wrist_top_y = self.wrist_to_abs(0, self.wrist_height) - self.arrow_theta = math.radians(self.arrow_angle) - self.arrow_x, self.arrow_y = self.wrist_to_abs(0, -self.arrow_height) - self.arrow_tip_x = self.arrow_x + (self.arrow_height + self.wrist_height) \ - * math.sin(self.arrow_theta - self.wrist_theta) - self.arrow_tip_y = self.arrow_y + (self.arrow_height + self.wrist_height) \ - * math.cos(self.arrow_theta - self.wrist_theta) - # [[c, s], [-s, c]] * [ring_x, ring_y] - self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y - self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y @property def root_height(self) -> float: return self.shoulder_joint.height + @property + def shoulder_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 @@ -150,6 +138,7 @@ class WingProfile(Model): holes=holes, hole_diam=self.shoulder_joint.parent_conn_hole_diam, centred=(True, True), + flip_y=self.flip, ) @submodel(name="spacer-s0-shoulder") def spacer_s0_base(self) -> MountingBox: @@ -170,6 +159,7 @@ class WingProfile(Model): holes=holes, hole_diam=self.hs_joint_corner_hole_diam, centred=(True, True), + flip_y=self.flip, ) def surface_s0(self, top: bool = False) -> Cq.Workplane: @@ -199,13 +189,16 @@ class WingProfile(Model): (self.spacer_s0_shoulder().generate(), "shoulder"), (self.spacer_s0_base().generate(), "base") ]: + top_tag, bot_tag = "top", "bot" + if self.flip: + top_tag, bot_tag = bot_tag, top_tag ( result .addS(o, name=tag, role=Role.STRUCTURE | Role.CONNECTION, material=self.mat_bracket) - .constrain(f"{tag}?bot", f"bot?{tag}", "Plane") - .constrain(f"{tag}?top", f"top?{tag}", "Plane") + .constrain(f"{tag}?{bot_tag}", f"bot?{tag}", "Plane") + .constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane") .constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis") ) hirth = self.base_joint.generate(is_mated=True) @@ -218,6 +211,10 @@ class WingProfile(Model): ### s1, s2, s3 ### + def profile(self) -> Cq.Sketch: + """ + Generates profile from shoulder and above + """ def _assembly_insert_spacer( self, @@ -251,12 +248,6 @@ class WingProfile(Model): "Axis", param=angle) ) - @property - def ring_radius(self) -> float: - dx = self.ring_x - dy = self.ring_y - return (dx * dx + dy * dy) ** 0.5 - def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]: elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c @@ -267,77 +258,30 @@ class WingProfile(Model): return wrist_x, wrist_y - def profile(self) -> Cq.Sketch: - """ - Net profile of the wing starting from the wing root with no divisions - """ - result = ( - Cq.Sketch() - .segment( - (0, 0), - (0, self.shoulder_joint.height), - tag="shoulder") - .arc( - (0, self.shoulder_joint.height), - (self.elbow_top_x, self.elbow_top_y), - (self.wrist_top_x, self.wrist_top_y), - tag="s1_top") - #.segment( - # (self.wrist_x, self.wrist_y), - # (wrist_top_x, wrist_top_y), - # tag="wrist") - .arc( - (0, 0), - (self.elbow_x, self.elbow_y), - (self.wrist_x, self.wrist_y), - tag="s1_bot") - ) - result = ( - result - .segment( - (self.wrist_x, self.wrist_y), - (self.arrow_x, self.arrow_y) - ) - .segment( - (self.arrow_x, self.arrow_y), - (self.arrow_tip_x, self.arrow_tip_y) - ) - .segment( - (self.arrow_tip_x, self.arrow_tip_y), - (self.wrist_top_x, self.wrist_top_y) - ) - ) - # Carve out the ring - result = result.assemble() - result = ( - result - .push([(self.ring_abs_x, self.ring_abs_y)]) - .circle(self.ring_radius, mode='a') - .circle(self.ring_radius_inner, mode='s') - .clean() - ) - return result def _mask_elbow(self) -> list[Tuple[float, float]]: """ Polygon shape to mask out parts above the elbow """ - abscissa = 200 + l = 200 return [ - (0, -abscissa), + (0, -l), + (self.elbow_x, -l), (self.elbow_x, self.elbow_y), (self.elbow_top_x, self.elbow_top_y), - (0, abscissa) + (self.elbow_top_x, l), + (0, l) ] def _mask_wrist(self) -> list[Tuple[float, float]]: - abscissa = 200 + l = 200 return [ - (0, -abscissa), - (self.wrist_x, -abscissa), + (0, -l), + (self.wrist_x, -l), (self.wrist_x, self.wrist_y), (self.wrist_top_x, self.wrist_top_y), - (0, abscissa), + (self.wrist_top_x, l), + (0, l), ] @@ -649,3 +593,214 @@ class WingProfile(Model): result.solve() return result + + + +@dataclass(kw_only=True) +class WingR(WingProfile): + """ + Right side wings + """ + + elbow_height: float = 111.0 + elbow_x: float = 363.0 + elbow_y: float = 44.0 + # Tilt of elbow w.r.t. shoulder + elbow_angle: float = 30.0 + + wrist_height: float = 60.0 + # Bottom point of the wrist + wrist_x: float = 403.0 + wrist_y: float = 253.0 + + # Tile of wrist w.r.t. shoulder + wrist_angle: float = 40 + # Extends from the wrist to the tip of the arrow + arrow_height: float = 300 + arrow_angle: float = 8 + + # Relative (in wrist coordinate) centre of the ring + ring_x: float = 45 + ring_y: float = 25 + ring_radius_inner: float = 22 + + def __post_init__(self): + super().__post_init__() + self.arrow_theta = math.radians(self.arrow_angle) + self.arrow_x, self.arrow_y = self.wrist_to_abs(0, -self.arrow_height) + self.arrow_tip_x = self.arrow_x + (self.arrow_height + self.wrist_height) \ + * math.sin(self.arrow_theta - self.wrist_theta) + self.arrow_tip_y = self.arrow_y + (self.arrow_height + self.wrist_height) \ + * math.cos(self.arrow_theta - self.wrist_theta) + # [[c, s], [-s, c]] * [ring_x, ring_y] + self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y + self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y + assert self.ring_radius > self.ring_radius_inner + + @property + def ring_radius(self) -> float: + dx = self.ring_x + dy = self.ring_y + return (dx * dx + dy * dy) ** 0.5 + + def profile(self) -> Cq.Sketch: + """ + Net profile of the wing starting from the wing root with no divisions + """ + result = ( + Cq.Sketch() + .segment( + (0, 0), + (0, self.shoulder_joint.height), + tag="shoulder") + .spline([ + (0, self.shoulder_joint.height), + (self.elbow_top_x, self.elbow_top_y), + (self.wrist_top_x, self.wrist_top_y), + ], + tag="s1_top") + #.segment( + # (self.wrist_x, self.wrist_y), + # (wrist_top_x, wrist_top_y), + # tag="wrist") + .spline([ + (0, 0), + (self.elbow_x, self.elbow_y), + (self.wrist_x, self.wrist_y), + ], + tag="s1_bot") + ) + result = ( + result + .segment( + (self.wrist_x, self.wrist_y), + (self.arrow_x, self.arrow_y) + ) + .segment( + (self.arrow_x, self.arrow_y), + (self.arrow_tip_x, self.arrow_tip_y) + ) + .segment( + (self.arrow_tip_x, self.arrow_tip_y), + (self.wrist_top_x, self.wrist_top_y) + ) + ) + # Carve out the ring + result = result.assemble() + result = ( + result + .push([(self.ring_abs_x, self.ring_abs_y)]) + .circle(self.ring_radius, mode='a') + .circle(self.ring_radius_inner, mode='s') + .clean() + ) + return result + +@dataclass(kw_only=True) +class WingL(WingProfile): + + elbow_x: float = 230.0 + elbow_y: float = 110.0 + elbow_angle: float = -10.0 + elbow_height: float = 80.0 + + wrist_x: float = 480.0 + wrist_y: float = 0.0 + wrist_angle: float = -45 + wrist_height: float = 43.0 + + shoulder_bezier_ext: float = 80.0 + elbow_bezier_ext: float = 100.0 + wrist_bezier_ext: float = 30.0 + + arrow_length: float = 135.0 + arrow_height: float = 120.0 + + flip: bool = True + + def __post_init__(self): + super().__post_init__() + assert self.wrist_height <= self.shoulder_joint.height + + def arrow_to_abs(self, x, y) -> Tuple[float, float]: + return self.wrist_to_abs(x * self.arrow_length, y * self.arrow_height / 2 + self.wrist_height / 2) + + def profile(self) -> Cq.Sketch: + result = ( + Cq.Sketch() + .segment( + (0,0), + (0, self.shoulder_height) + ) + #.spline([ + # (0, 0), + # self.elbow_to_abs(0, 0), + # self.wrist_to_abs(0, 0), + #]) + #.spline([ + # (0, self.shoulder_height), + # self.elbow_to_abs(0, self.elbow_height), + # self.wrist_to_abs(0, self.wrist_height), + #]) + .bezier([ + (0, 0), + (self.shoulder_bezier_ext, 0), + self.elbow_to_abs(-self.elbow_bezier_ext, 0), + self.elbow_to_abs(0, 0), + ]) + .bezier([ + (0, self.shoulder_joint.height), + (self.shoulder_bezier_ext, self.shoulder_joint.height), + self.elbow_to_abs(-self.elbow_bezier_ext, self.elbow_height), + self.elbow_to_abs(0, self.elbow_height), + ]) + .bezier([ + self.elbow_to_abs(0, 0), + self.elbow_to_abs(self.elbow_bezier_ext, 0), + self.wrist_to_abs(-self.wrist_bezier_ext, 0), + self.wrist_to_abs(0, 0), + ]) + .bezier([ + self.elbow_to_abs(0, self.elbow_height), + self.elbow_to_abs(self.elbow_bezier_ext, self.elbow_height), + self.wrist_to_abs(-self.wrist_bezier_ext, self.wrist_height), + self.wrist_to_abs(0, self.wrist_height), + ]) + ) + # arrow base positions + base_u, base_v = 0.3, 0.3 + result = ( + result + .bezier([ + self.wrist_to_abs(0, self.wrist_height), + self.wrist_to_abs(self.wrist_bezier_ext, self.wrist_height), + self.arrow_to_abs(base_u, base_v), + ]) + .bezier([ + self.wrist_to_abs(0, 0), + self.wrist_to_abs(self.wrist_bezier_ext, 0), + self.arrow_to_abs(base_u, -base_v), + ]) + ) + # Create the arrow + arrow_beziers = [ + [ + (0, 1), + (0.3, 1), + (0.8, .2), + (1, 0), + ], + [ + (0, 1), + (0.1, 0.8), + (base_u, base_v), + ] + ] + arrow_beziers = [ + l2 + for l in arrow_beziers + for l2 in [l, [(x, -y) for x,y in l]] + ] + for line in arrow_beziers: + result = result.bezier([self.arrow_to_abs(x, y) for x,y in line]) + return result.assemble() diff --git a/poetry.lock b/poetry.lock index 756343c..88d30ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -57,29 +57,32 @@ typing-extensions = ">=4.6.0,<5" [[package]] name = "cadquery" -version = "2.4.0" +version = "2.5.0.dev0" description = "CadQuery is a parametric scripting language for creating and traversing CAD models" optional = false -python-versions = ">=3.8" -files = [ - {file = "cadquery-2.4.0-py3-none-any.whl", hash = "sha256:66c865b1e5db205b81a5ddc8533d4741577291292cf2dc80b104ae9e3085b195"}, - {file = "cadquery-2.4.0.tar.gz", hash = "sha256:38e8e302060f2e50943ab0f8acab985c37a73009e972c7b02767c90bef7fb3e7"}, -] +python-versions = ">=3.9" +files = [] +develop = false [package.dependencies] cadquery-ocp = ">=7.7.0a0,<7.8" casadi = "*" ezdxf = "*" -multimethod = "1.9.1" +multimethod = ">=1.11,<2.0" nlopt = "*" -nptyping = "2.0.1" path = "*" typish = "*" [package.extras] -dev = ["black (==19.10b0)", "click (==8.0.4)", "docutils", "ipython", "pytest"] +dev = ["black @ git+https://github.com/cadquery/black.git@cq", "docutils", "ipython", "pytest"] ipython = ["ipython"] +[package.source] +type = "git" +url = "https://github.com/CadQuery/cadquery.git" +reference = "HEAD" +resolved_reference = "8ea37a71d40d383b55b8009c68987526f47a7613" + [[package]] name = "cadquery-ocp" version = "7.7.2" @@ -188,13 +191,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -216,45 +219,45 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "ezdxf" -version = "1.3.1" +version = "1.3.2" description = "A Python package to create/manipulate DXF drawings." optional = false python-versions = ">=3.9" files = [ - {file = "ezdxf-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25b0523d0fb15c830689c1d9e313b4e8c278fbd6df4670394f427cece8c5f8e3"}, - {file = "ezdxf-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5b613573cf8b5373e5e5fb12127b17c70138a6307ac520d001d36f1ba8f1bf0d"}, - {file = "ezdxf-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a79e0908d789fe4a92630b991248aca01907c069c4269c34a8f0d2a2facb95"}, - {file = "ezdxf-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2db0790c414151cc0343bac13d81fc02b2b9c4974a17d2c0e0d40fd8d5f4a735"}, - {file = "ezdxf-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7095d899c7fdf884dce2b8a629000d594894ef014b5e00d529860f0a46eed76"}, - {file = "ezdxf-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:92b08ac7adac6d91768b9dd6179dd35a23c7eef9382aebc14125e055eb85de28"}, - {file = "ezdxf-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eebb9bb3caa7b150bf828f431807762174e06c497dc4c2d9251e8dabe84e660d"}, - {file = "ezdxf-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:44e08030a365550ab31bcf3839c948cb9074e1b477174f44aa089de5ec9adc1b"}, - {file = "ezdxf-1.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bcaaf578a6651497bb759260ecffa221f7637b42771439b28a6af7bc1fe4c1ec"}, - {file = "ezdxf-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:45981c25860a21ae3329c7978517ff3b0be0a2f65ea456df24798970c10545e4"}, - {file = "ezdxf-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:965096be7d02fe7edc66086b3903801a7f0673c96a1704710259ee5ec02f0512"}, - {file = "ezdxf-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4a3b7c80507d4ca1a1aa5f14b02fdeced58c8b5638850b045b01b3bcb47c3b1"}, - {file = "ezdxf-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99d0479c6a02085d7b539dd0d89d08549b2d665bb7f5fa9f3a97d6deadfc0829"}, - {file = "ezdxf-1.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37b36b49f1f8663d72f4ccff58fc1fa50ca87748c3b3df6a181b6cbb0a947e5a"}, - {file = "ezdxf-1.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef8bc194cc3892cbb02d08cfa9e50c9143bad5653db34018f3dde9c263559652"}, - {file = "ezdxf-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c03852a0b18cae7d4573e6ed84ca157d89373076af4f4539f740396d635c25c4"}, - {file = "ezdxf-1.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:29a11a8fe5d4552ca8bb1338c7b6fed513fc4f4857b30d8817eaa1309a67af7f"}, - {file = "ezdxf-1.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c5c016a35295e558cd9d78345b6a5bb1ab2491ff42deb52490320c25b7ea13a4"}, - {file = "ezdxf-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c000f95503851490150ad8a9b60f4a0efae6f566472543ea8d59bbd17971ad9"}, - {file = "ezdxf-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:317b73a19407c0705d1f3430148154f719e65cf68e958218c80dd7bd59d1cddc"}, - {file = "ezdxf-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e00ea19fed35b0e22d8256fa67fdffe99f33738133ea5974d360a7e3d8411bf"}, - {file = "ezdxf-1.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:99bb5a3810a8657a601e075ad5827a6519a61d91dc651f1e3776dd0bc7cb223f"}, - {file = "ezdxf-1.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:de3b08f85c79cc8bb9a579fd1b7a4951cc6a9f497f5ade2d9b3d92ca9d057288"}, - {file = "ezdxf-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4d53bf7e069e7775c342378f328af8167238429824e49869b794d8688dd85575"}, - {file = "ezdxf-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:37f49f6d0006a5969a736183889d38aab608d35ec2a13d9b7798271f785cde3e"}, - {file = "ezdxf-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:aac05ab36290aac4a442d3d7bf3294a1e61a4a2ab1f275a740817d7908407cc7"}, - {file = "ezdxf-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:83cc14ec51278791f37c7369f6db961535810e3391f24e355517f11c014a85f3"}, - {file = "ezdxf-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22b62a82d1031200e2217eaf0caee01d150e94f6cc9f3aaeaf621ba520d6848"}, - {file = "ezdxf-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c344bb0e09ccc90b50b4f4833126ca6ace6b23be1fe32dd276875ee5e6bbae9"}, - {file = "ezdxf-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:819877f43dcf623c18f0fc00b9bf34180976f0078dbd6fb022ad402759379489"}, - {file = "ezdxf-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ffad1495c0f0a44d1ff53cd5e766ec631026227d2dde3e5940d5acd38784e13"}, - {file = "ezdxf-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:3f8910132479ebd829d8c47e123db6ba0f5a8e9f7dfe79f104407a25517972c2"}, - {file = "ezdxf-1.3.1-py3-none-any.whl", hash = "sha256:68ba8a6f87b04bcdf43808adb7fed0c32a9ad158126415481b19bf4a9a8178a4"}, - {file = "ezdxf-1.3.1.zip", hash = "sha256:160c8e0bbc8bc0d199a2299a6b489df5fa88ab724550243783c81c4ad3e409dd"}, + {file = "ezdxf-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6f4eacaa8d55ddcbd64795409ff4f5e452c4b066f4e33b210bc4c6189c71ec6f"}, + {file = "ezdxf-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35d1fa27f175d2b648f3aa5f31448b81ae8fe3627b0e908862a15983bdeb191b"}, + {file = "ezdxf-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:240f7e894fe0364585d28e8f697c12e93db6fbb426c37d6a3f43a027c57d6dbf"}, + {file = "ezdxf-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c20adceb7c78e1370f117615c245a293bc7fe65525457eeb287d24fa4cd96c8"}, + {file = "ezdxf-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a311c455a84e7c2f03cefa0922fa4919d6950e9207e8e7175893507889852012"}, + {file = "ezdxf-1.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8c2955db7f41596b7245441090d02b083cae060110fd595abc2f3347bfd3cb09"}, + {file = "ezdxf-1.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:120273751ca4818d87a216cfd0f74d0fc73518b5ec052aa8c17bad9711463e48"}, + {file = "ezdxf-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:90274032eb4b047af2b38f71bca749dc6bff2110bb2f4c818f5f48e6864e6a97"}, + {file = "ezdxf-1.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:464689421c55e1c9d193da46ea461bfc82a1c0ab0007a37cbaefb44189385b04"}, + {file = "ezdxf-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7a39234e9ccb072e2362b086f511706ce76ac5774ddb618fe7ca6710b5418f72"}, + {file = "ezdxf-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:193f5146e6c8b93e6293248467d8b0c38fa12fc41b85507300f15e85b73ce219"}, + {file = "ezdxf-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e37534530c9734c927f6afafe1f3f5a6fdbde2bbf438a661173ff0ba86de8937"}, + {file = "ezdxf-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6118e375852f6db04b66c0111ded47c0e0acd42869a43aaa302815b947c5e8de"}, + {file = "ezdxf-1.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:420b6d7f80fa1bff374c7fb611ba8aef071d5523dbab9ad3a64465f7b2ac82cc"}, + {file = "ezdxf-1.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f60ada8f7b0d232a6d45cbfec4b205dc7a1beb94bb90a2518893e7a9b43681c6"}, + {file = "ezdxf-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:06550cf39bf60f62a1db3ee43426a8c66485fc946a44324d921a901f7d35bfe7"}, + {file = "ezdxf-1.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1a1bcda7d2d97f3aa3fb0db14006c91000ad51cd5aa16d51b73d42b3e88a794e"}, + {file = "ezdxf-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cd36e1430b6150e071466f1bd712aad8552c986a165fcabd1c74b47cf72684d6"}, + {file = "ezdxf-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e6a645036c3874c1693e6e2411647645ab67882e5c0c762f700e55ac9a0dc56"}, + {file = "ezdxf-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c12e9602abc8444dc5606e0c39cb6826df17e7c1a01d576d586f0a39696d539d"}, + {file = "ezdxf-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77aed29c3d14067c2e7986057b6fe6842167b89d6a35df5d1636b6627e1ea117"}, + {file = "ezdxf-1.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e0881f8fb4fa6386ef963a657bc7291f5ec3029844ba6e7a905c9f9b713ccae"}, + {file = "ezdxf-1.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fddf6cfd0bf7fe78273918986f917b4f515d9a6371ee1b8cf310d4cd879d33e9"}, + {file = "ezdxf-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:d504f843c20e9b7c2d331352ac91710bd6ebd14cf56c576a3432dacdfdde7106"}, + {file = "ezdxf-1.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bcd10a9ac39728d949d0edfd7eb460707d4b4620d37db41b4790c3c871dbab"}, + {file = "ezdxf-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4795843993061f9a3127e41328c5c02483ba619fda53b91bbe1e764b4294ad31"}, + {file = "ezdxf-1.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cfcb2bee332917b1f7353f30d8cfe1e24774034e86d1f1360eaa0675b2c402bf"}, + {file = "ezdxf-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13acf2a25854d735b23ba569500aa9222ae34862a5dc39a3bb867089b884274"}, + {file = "ezdxf-1.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f3fd73b9f654491864e37153d86ceb14cfae6cc78d0693259cea49bdcd935882"}, + {file = "ezdxf-1.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5629cb3a21ccc3895b57a507f046951a76836b9aaafff7dd5c1cda67ef258271"}, + {file = "ezdxf-1.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6615464a6b2a6af282716f0ab3f218e0a8abf27604e2cc638ee27285b29c8034"}, + {file = "ezdxf-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:e4f3dd9c93623c25488f7cddbd2914a9a18b29fc32c7ae5a95a3915b149836dc"}, + {file = "ezdxf-1.3.2-py3-none-any.whl", hash = "sha256:4451a04765323e93df943a0584db50f3851be0ca4aa8b8a4ee809faf492b3a5d"}, + {file = "ezdxf-1.3.2.zip", hash = "sha256:ecaa9e69f20fb66245164f235e616dd0789a11ac8a72a0302780b77621e1c354"}, ] [package.dependencies] @@ -271,53 +274,53 @@ draw5 = ["Pillow", "PyMuPDF (>=1.20.0)", "PyQt5", "matplotlib"] [[package]] name = "fonttools" -version = "4.53.0" +version = "4.53.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.53.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20"}, - {file = "fonttools-4.53.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d"}, - {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6"}, - {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5"}, - {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9"}, - {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca"}, - {file = "fonttools-4.53.0-cp310-cp310-win32.whl", hash = "sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068"}, - {file = "fonttools-4.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68"}, - {file = "fonttools-4.53.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec"}, - {file = "fonttools-4.53.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749"}, - {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161"}, - {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee"}, - {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6"}, - {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e"}, - {file = "fonttools-4.53.0-cp311-cp311-win32.whl", hash = "sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005"}, - {file = "fonttools-4.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796"}, - {file = "fonttools-4.53.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a"}, - {file = "fonttools-4.53.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4"}, - {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06"}, - {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d"}, - {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109"}, - {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2"}, - {file = "fonttools-4.53.0-cp312-cp312-win32.whl", hash = "sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea"}, - {file = "fonttools-4.53.0-cp312-cp312-win_amd64.whl", hash = "sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380"}, - {file = "fonttools-4.53.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12"}, - {file = "fonttools-4.53.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4"}, - {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce"}, - {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f"}, - {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206"}, - {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd"}, - {file = "fonttools-4.53.0-cp38-cp38-win32.whl", hash = "sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af"}, - {file = "fonttools-4.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b"}, - {file = "fonttools-4.53.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac"}, - {file = "fonttools-4.53.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d"}, - {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1"}, - {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f"}, - {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64"}, - {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0"}, - {file = "fonttools-4.53.0-cp39-cp39-win32.whl", hash = "sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9"}, - {file = "fonttools-4.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2"}, - {file = "fonttools-4.53.0-py3-none-any.whl", hash = "sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4"}, - {file = "fonttools-4.53.0.tar.gz", hash = "sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, + {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, + {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, + {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, + {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, + {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, + {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, + {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, + {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, + {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, + {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, + {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, + {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, ] [package.extras] @@ -336,13 +339,13 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] [[package]] name = "ipython" -version = "8.25.0" +version = "8.26.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.25.0-py3-none-any.whl", hash = "sha256:53eee7ad44df903a06655871cbab66d156a051fd86f3ec6750470ac9604ac1ab"}, - {file = "ipython-8.25.0.tar.gz", hash = "sha256:c6ed726a140b6e725b911528f80439c534fac915246af3efc39440a6b0f9d716"}, + {file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"}, + {file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"}, ] [package.dependencies] @@ -369,7 +372,7 @@ nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] [[package]] @@ -407,13 +410,13 @@ traitlets = "*" [[package]] name = "multimethod" -version = "1.9.1" +version = "1.12" description = "Multiple argument dispatching." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "multimethod-1.9.1-py3-none-any.whl", hash = "sha256:52f8f1f2b9d5a4c7adfdcc114dbeeebe3245a4420801e8807e26522a79fb6bc2"}, - {file = "multimethod-1.9.1.tar.gz", hash = "sha256:1589bf52ca294667fd15527ea830127c763f5bfc38562e3642591ffd0fd9d56f"}, + {file = "multimethod-1.12-py3-none-any.whl", hash = "sha256:fd0c473c43558908d97cc06e4d68e8f69202f167db46f7b4e4058893e7dbdf60"}, + {file = "multimethod-1.12.tar.gz", hash = "sha256:8db8ef2a8d2a247e3570cc23317680892fdf903d84c8c1053667c8e8f7671a67"}, ] [[package]] @@ -446,24 +449,6 @@ files = [ [package.dependencies] numpy = ">=1.14" -[[package]] -name = "nptyping" -version = "2.0.1" -description = "Type hints for NumPy." -optional = false -python-versions = ">=3.7" -files = [ - {file = "nptyping-2.0.1-py3-none-any.whl", hash = "sha256:0fc5c4d76c65e12a77e750b9e2701dab6468d00926c8c4f383867bd70598a532"}, -] - -[package.dependencies] -numpy = ">=1.20.0" - -[package.extras] -build = ["codecov (>=2.1.0)", "invoke (>=1.6.0)", "pip-tools (>=6.5.0)"] -dev = ["autoflake", "beartype (<0.10.0)", "beartype (>=0.10.0)", "black", "codecov (>=2.1.0)", "coverage", "invoke (>=1.6.0)", "isort", "mypy", "pip-tools (>=6.5.0)", "pylint", "setuptools", "typeguard", "wheel"] -qa = ["autoflake", "beartype (<0.10.0)", "beartype (>=0.10.0)", "black", "coverage", "isort", "mypy", "pylint", "setuptools", "typeguard", "wheel"] - [[package]] name = "numpy" version = "1.26.4" @@ -685,45 +670,45 @@ tests = ["flake8", "loguru", "pytest", "pytest-asyncio", "pytest-cov", "pytest-m [[package]] name = "scipy" -version = "1.13.1" +version = "1.14.0" description = "Fundamental algorithms for scientific computing in Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, - {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, - {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, - {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, - {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, - {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, - {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, - {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, - {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, - {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, - {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0"}, + {file = "scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d"}, + {file = "scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d056a8709ccda6cf36cdd2eac597d13bc03dba38360f418560a93050c76a16e"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f0a50da861a7ec4573b7c716b2ebdcdf142b66b756a0d392c236ae568b3a93fb"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94c164a9e2498e68308e6e148646e486d979f7fcdb8b4cf34b5441894bdb9caf"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a7d46c3e0aea5c064e734c3eac5cf9eb1f8c4ceee756262f2c7327c4c2691c86"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eee2989868e274aae26125345584254d97c56194c072ed96cb433f32f692ed8"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3154691b9f7ed73778d746da2df67a19d046a6c8087c8b385bc4cdb2cfca74"}, + {file = "scipy-1.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c40003d880f39c11c1edbae8144e3813904b10514cd3d3d00c277ae996488cdb"}, + {file = "scipy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b083c8940028bb7e0b4172acafda6df762da1927b9091f9611b0bcd8676f2bc"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff2438ea1330e06e53c424893ec0072640dac00f29c6a43a575cbae4c99b2b9"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bbc0471b5f22c11c389075d091d3885693fd3f5e9a54ce051b46308bc787e5d4"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:64b2ff514a98cf2bb734a9f90d32dc89dc6ad4a4a36a312cd0d6327170339eb0"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:7d3da42fbbbb860211a811782504f38ae7aaec9de8764a9bef6b262de7a2b50f"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d91db2c41dd6c20646af280355d41dfa1ec7eead235642178bd57635a3f82209"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a01cc03bcdc777c9da3cfdcc74b5a75caffb48a6c39c8450a9a05f82c4250a14"}, + {file = "scipy-1.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65df4da3c12a2bb9ad52b86b4dcf46813e869afb006e58be0f516bc370165159"}, + {file = "scipy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c4161597c75043f7154238ef419c29a64ac4a7c889d588ea77690ac4d0d9b20"}, + {file = "scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b"}, ] [package.dependencies] -numpy = ">=1.22.4,<2.3" +numpy = ">=1.23.5,<2.3" [package.extras] -dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] -test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "six" @@ -857,4 +842,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "ec47ccffd60fbda610a5c3725fc064a08b1b794f23084672bd62beb20b1b19f7" +content-hash = "3403086281e26faefd12217e6dec4c0696e3468c5a9d8c952f8d988857aafba0" diff --git a/pyproject.toml b/pyproject.toml index 8ca0117..c24f3d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,14 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" -cadquery = "^2.4.0" +cadquery = {git = "https://github.com/CadQuery/cadquery.git"} build123d = "^0.5.0" numpy = "^1.26.4" colorama = "^0.4.6" +# cadquery dependency +multimethod = "^1.12" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" -- 2.44.1 From 2a968f9446ee5e2b4c4951b9f0fdf5d9ee49b4d2 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 16 Jul 2024 21:20:45 -0700 Subject: [PATCH 078/187] feat: Improved H-S joint and harness geometry --- nhf/parts/joints.py | 4 + nhf/touhou/houjuu_nue/__init__.py | 37 +++++---- nhf/touhou/houjuu_nue/harness.py | 43 +++++----- nhf/touhou/houjuu_nue/wing.py | 134 ++++++++++++++++++++++++++---- 4 files changed, 164 insertions(+), 54 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 594c2cb..f80b1b6 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -36,6 +36,10 @@ class HirthJoint: def total_height(self): return self.base_height + self.tooth_height + @property + def joint_height(self): + return 2 * self.base_height + self.tooth_height + def generate(self, is_mated=False, tol=0.01): """ diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 1dabd04..4602be3 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -49,14 +49,6 @@ class Parameters(Model): # Harness harness: MH.Harness = field(default_factory=lambda: MH.Harness()) - hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( - radius=30, - radius_inner=20, - tooth_height=10, - base_height=5, - n_tooth=24 - )) - wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR(name="r1")) wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR(name="r2")) wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR(name="r3")) @@ -77,10 +69,19 @@ class Parameters(Model): def __post_init__(self): super().__init__(name="houjuu-nue") - self.harness.hs_hirth_joint = self.hs_hirth_joint - 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 + self.wing_r1.base_joint = self.harness.hs_hirth_joint + self.wing_r2.base_joint = self.harness.hs_hirth_joint + self.wing_r3.base_joint = self.harness.hs_hirth_joint + self.wing_l1.base_joint = self.harness.hs_hirth_joint + self.wing_l2.base_joint = self.harness.hs_hirth_joint + self.wing_l3.base_joint = self.harness.hs_hirth_joint + + assert self.wing_r1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam + assert self.wing_r2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam + assert self.wing_r3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam + assert self.wing_l1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam + assert self.wing_l2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam + assert self.wing_l3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam @submodel(name="harness") def submodel_harness(self) -> Model: @@ -120,12 +121,12 @@ class Parameters(Model): .add(self.wing_l2.assembly(parts), name="wing_l2") .add(self.wing_l3.assembly(parts), name="wing_l3") ) - self.hs_hirth_joint.add_constraints(result, "harness/r1", "wing_r1/s0/hs", offset=11) - self.hs_hirth_joint.add_constraints(result, "harness/r2", "wing_r2/s0/hs", offset=10) - self.hs_hirth_joint.add_constraints(result, "harness/r3", "wing_r3/s0/hs", offset=9) - self.hs_hirth_joint.add_constraints(result, "harness/l1", "wing_l1/s0/hs", offset=6) - self.hs_hirth_joint.add_constraints(result, "harness/l2", "wing_l2/s0/hs", offset=7) - self.hs_hirth_joint.add_constraints(result, "harness/l3", "wing_l3/s0/hs", offset=8) + for tag, offset in [("r1", 10), ("r2", 8), ("r3", 6), ("l1", 6), ("l2", 7), ("l3", 8)]: + self.harness.hs_hirth_joint.add_constraints( + result, + f"harness/{tag}", + f"wing_{tag}/s0/hs", + offset=offset) return result.solve() @submodel(name="trident") diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index d5a35cb..a6f4243 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -8,26 +8,27 @@ import nhf.utils @dataclass class Harness(Model): thickness: float = 25.4 / 8 - width: float = 300.0 - height: float = 400.0 + width: float = 200.0 + height: float = 300.0 fillet: float = 10.0 wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [ - ("r1", 70, 150), - ("l1", -70, 150), - ("r2", 100, 0), - ("l2", -100, 0), - ("r3", 70, -150), - ("l3", -70, -150), + ("r1", 55, 90), + ("l1", -55, 90), + ("r2", 60, 0), + ("l2", -60, 0), + ("r3", 55, -90), + ("l3", -55, -90), ]) # Holes drilled onto harness for attachment with HS joint harness_to_root_conn_diam: float = 6 hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( - radius=30, - radius_inner=20, - tooth_height=10, - base_height=5 + radius=25.0, + radius_inner=20.0, + tooth_height=7.0, + base_height=5.0, + n_tooth=24, )) hs_joint_base_width: float = 85 @@ -37,7 +38,7 @@ class Harness(Model): hs_joint_corner_cbore_depth: float = 2 hs_joint_corner_inset: float = 12 - hs_joint_axis_diam: float = 12 + hs_joint_axis_diam: float = 12.0 hs_joint_axis_cbore_diam: float = 20 hs_joint_axis_cbore_depth: float = 3 @@ -53,12 +54,16 @@ class Harness(Model): sketch = ( Cq.Sketch() .polygon([ - (0.7 * w, h), - (w, 0), - (0.7 * w, -h), - (0.7 * -w, -h), - (-w, 0), - (0.7 * -w, h), + (w, h), + (w, -h), + (-w, -h), + (-w, h), + #(0.7 * w, h), + #(w, 0), + #(0.7 * w, -h), + #(0.7 * -w, -h), + #(-w, 0), + #(0.7 * -w, h), ]) #.rect(self.harness_width, self.harness_height) .vertices() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 26f07a6..16a23da 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -20,12 +20,18 @@ class WingProfile(Model): name: str = "wing" base_joint: HirthJoint = field(default_factory=lambda: HirthJoint( - radius=30.0, + radius=25.0, radius_inner=20.0, + tooth_height=7.0, + base_height=5, + n_tooth=24, )) - root_width: float = 80.0 - hs_joint_corner_dx: float = 30.0 + base_width: float = 80.0 + hs_joint_corner_dx: float = 17.0 + hs_joint_corner_dz: float = 24.0 hs_joint_corner_hole_diam: float = 6.0 + hs_joint_axis_diam: float = 12.0 + base_plate_width: float = 50.0 panel_thickness: float = 25.4 / 16 spacer_thickness: float = 25.4 / 8 @@ -36,6 +42,8 @@ class WingProfile(Model): shoulder_width: float = 30.0 shoulder_tip_x: float = -200.0 shoulder_tip_y: float = 160.0 + shoulder_mid_x: float = -105.0 + shoulder_mid_y: float = 75.0 s1_thickness: float = 25.0 @@ -94,13 +102,64 @@ class WingProfile(Model): def shoulder_height(self) -> float: return self.shoulder_joint.height + @target(name="base-hs-joint") + def base_hs_joint(self) -> Cq.Workplane: + """ + Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth + coupling, a cylindrical base, and a mounting base. + """ + hirth = self.base_joint.generate(is_mated=True) + dy = self.hs_joint_corner_dx + dx = self.hs_joint_corner_dz + conn = [ + (-dx, -dy), + (dx, -dy), + (dx, dy), + (-dx, dy), + ] + result = ( + Cq.Workplane('XY') + .box( + self.root_height, + self.base_plate_width, + self.base_joint.base_height, + centered=(True, True, False)) + #.translate((0, 0, -self.base_joint.base_height)) + #.edges("|Z") + #.fillet(self.hs_joint_corner_fillet) + .faces(">Z") + .workplane() + .pushPoints(conn) + .hole(self.hs_joint_corner_hole_diam) + ) + # Creates a plane parallel to the holes but shifted to the base + plane = result.faces(">Z").workplane(offset=-self.base_joint.base_height) + + for i, (px, py) in enumerate(conn): + plane.moveTo(px, py).tagPlane(f"conn{i}") + result = ( + result + .faces(">Z") + .workplane() + .union(hirth, tol=0.1) + .clean() + ) + result = ( + result.faces(" Cq.Sketch: tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y + mid_x = self.shoulder_mid_x + mid_y = self.shoulder_mid_y sketch = ( Cq.Sketch() - .segment((-self.root_width, 0), (0, 0)) + .segment((-self.base_width, 0), (0, 0)) .spline([ (0, 0), (-30.0, 80.0), @@ -108,16 +167,42 @@ class WingProfile(Model): ]) .segment( (tip_x, tip_y), - (tip_x, tip_y - self.shoulder_width) + (tip_x, tip_y - self.shoulder_width), ) .segment( (tip_x, tip_y - self.shoulder_width), - (-self.root_width, 0) + (mid_x, mid_y), + ) + .segment( + (mid_x, mid_y), + (-self.base_width, 0), ) .assemble() ) return sketch + def outer_shell_s0(self) -> Cq.Workplane: + tip_x = self.shoulder_tip_x + tip_y = self.shoulder_tip_y + t = self.panel_thickness + edge = Cq.Edge.makeSpline([ + Cq.Vector(x, y, 0) + for x,y in [ + (0, 0), + (-30.0, 80.0), + (tip_x, tip_y) + ] + ]) + result = ( + Cq.Workplane('XZ') + .rect(t, self.root_height + t*2, centered=(False, False)) + .sweep(edge) + ) + plane = result.copyWorkplane(Cq.Workplane('XZ')) + plane.moveTo(0, 0).tagPlane("bot") + plane.moveTo(0, self.root_height + t*2).tagPlane("top") + return result + @submodel(name="spacer-s0-shoulder") def spacer_s0_shoulder(self) -> MountingBox: """ @@ -145,16 +230,20 @@ class WingProfile(Model): """ Should be cut """ - dx = self.hs_joint_corner_dx + assert self.base_plate_width < self.base_width + assert self.hs_joint_corner_dx * 2 < self.base_width + assert self.hs_joint_corner_dz * 2 < self.root_height + dy = self.hs_joint_corner_dx + dx = self.hs_joint_corner_dz holes = [ - Hole(x=-dx, y=-dx), - Hole(x=dx, y=-dx), - Hole(x=dx, y=dx), - Hole(x=-dx, y=dx), + Hole(x=-dx, y=-dy), + Hole(x=dx, y=-dy), + Hole(x=dx, y=dy), + Hole(x=-dx, y=dy), ] return MountingBox( length=self.root_height, - width=self.root_width, + width=self.base_plate_width, thickness=self.spacer_thickness, holes=holes, hole_diam=self.hs_joint_corner_hole_diam, @@ -163,16 +252,21 @@ class WingProfile(Model): ) def surface_s0(self, top: bool = False) -> Cq.Workplane: + base_dx = -(self.base_width - self.base_plate_width) / 2 + base_dy = self.base_joint.joint_height tags = [ ("shoulder", (self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width), 0), - ("base", (0, 0), 90), + ("base", (base_dx, base_dy), 90), ] - return nhf.utils.extrude_with_markers( + result = nhf.utils.extrude_with_markers( self.profile_s0(), self.panel_thickness, tags, reverse=not top, ) + h = self.panel_thickness if top else 0 + result.copyWorkplane(Cq.Workplane('XZ')).moveTo(0, h).tagPlane("corner") + return result @assembly() def assembly_s0(self) -> Cq.Assembly: @@ -184,6 +278,10 @@ class WingProfile(Model): material=self.mat_panel, role=self.role_panel) .constrain("bot@faces@>Z", "top@faces@ Date: Tue, 16 Jul 2024 22:26:06 -0700 Subject: [PATCH 079/187] feat: Staggered shoulder joint --- nhf/touhou/houjuu_nue/__init__.py | 17 ++++-- nhf/touhou/houjuu_nue/joints.py | 89 +++++++++++++++++++------------ nhf/touhou/houjuu_nue/wing.py | 15 ++++-- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 4602be3..af76e05 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -46,15 +46,23 @@ class Parameters(Model): Defines dimensions for the Houjuu Nue cosplay """ - # Harness harness: MH.Harness = field(default_factory=lambda: MH.Harness()) - wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR(name="r1")) - wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR(name="r2")) - wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR(name="r3")) + wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR( + name="r1", + shoulder_joint=MJ.ShoulderJoint(directrix_id=1), + )) + wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR( + name="r2", + )) + wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR( + name="r3", + shoulder_joint=MJ.ShoulderJoint(directrix_id=1), + )) wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL( name="l1", wrist_angle=-45.0, + shoulder_joint=MJ.ShoulderJoint(directrix_id=1), )) wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( name="l2", @@ -63,6 +71,7 @@ class Parameters(Model): wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL( name="l3", wrist_angle=0.0, + shoulder_joint=MJ.ShoulderJoint(directrix_id=1), )) trident: MT.Trident = field(default_factory=lambda: MT.Trident()) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index cff350e..7840715 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -14,7 +14,7 @@ TOL = 1e-6 @dataclass class ShoulderJoint(Model): - height: float = 100.0 + height: float = 60.0 torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( radius_track=18, radius_rider=18, @@ -30,19 +30,23 @@ class ShoulderJoint(Model): thickness=1.3, height=7.5, ), + rider_slot_begin=0, + rider_n_slots=2, + rider_slot_span=15, )) # On the parent side, drill vertical holes - parent_conn_hole_diam: float = 6.0 + parent_conn_hole_diam: float = 4.0 # Position of the holes relative - parent_conn_hole_pos: list[float] = field(default_factory=lambda: [20, 30]) + parent_conn_hole_pos: list[float] = field(default_factory=lambda: [15]) - parent_lip_length: float = 40.0 - parent_lip_width: float = 20.0 - parent_lip_thickness: float = 8.0 + parent_lip_length: float = 25.0 + parent_lip_width: float = 30.0 + parent_lip_thickness: float = 5.0 parent_lip_ext: float = 40.0 - parent_lip_guard_height: float = 10.0 + parent_lip_guard_height: float = 8.0 + parent_root_wall_thickness: float = 25.4 / 16 # Measured from centre of axle child_lip_length: float = 45.0 @@ -52,13 +56,19 @@ class ShoulderJoint(Model): child_conn_hole_pos: list[float] = field(default_factory=lambda: [25, 35]) child_core_thickness: float = 3.0 + # Rotates the torsion joint to avoid collisions or for some other purpose + axis_rotate_bot: float = 225.0 + axis_rotate_top: float = -225.0 - @target(name="shoulder-joint/parent") - def parent(self, - root_wall_thickness: float = 25.4 / 16) -> Cq.Assembly: + directrix_id: int = 0 + + def __post_init__(self): + assert self.parent_lip_length * 2 < self.height + + def parent(self, top: bool = False) -> Cq.Assembly: joint = self.torsion_joint # Thickness of the lip connecting this joint to the wing root - dz = root_wall_thickness + dz = self.parent_root_wall_thickness assert self.parent_lip_width <= joint.radius_track * 2 assert self.parent_lip_ext > joint.radius_track @@ -86,14 +96,24 @@ class ShoulderJoint(Model): loc_dir = Cq.Location((0,0,0), (0, 0, 1), 180) loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, dz)) + rot = -self.axis_rotate_top if top else self.axis_rotate_bot + result = ( Cq.Assembly() - .add(joint.track(), name="track") + .add(joint.track(), name="track", + loc=Cq.Location((0, 0, 0), (0, 0, 1), rot)) .add(lip_guard, name="lip_guard") .add(lip, name="lip", loc=loc_pos * loc_dir * loc_axis) ) return result + @target(name="parent-bot") + def parent_bot(self) -> Cq.Assembly: + return self.parent(top=False) + @target(name="parent-top") + def parent_top(self) -> Cq.Assembly: + return self.parent(top=True) + @property def child_height(self) -> float: """ @@ -103,7 +123,7 @@ class ShoulderJoint(Model): joint = self.torsion_joint return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height - @target(name="shoulder-joint/child") + @target(name="child") def child(self) -> Cq.Assembly: """ Creates the top/bottom shoulder child joint @@ -165,14 +185,17 @@ class ShoulderJoint(Model): centered=(True, True, False), combine='cut') ) + theta = self.torsion_joint.spring.angle_neutral - self.torsion_joint.rider_slot_span loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) + loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot) + loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top) result = ( Cq.Assembly() .add(core, name="core", loc=Cq.Location()) .add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top", - loc=Cq.Location((0, 0, dh), (0, 0, 1), -90)) + loc=loc_axis_rotate_top * Cq.Location((0, 0, dh), (0, 0, 1), -90) * Cq.Location((0, 0, 0), (0, 0, 1), theta)) .add(joint.rider(rider_slot_begin=180), name="rider_bot", - loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) + loc=loc_axis_rotate_bot * Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) .add(lip, name="lip_top", loc=Cq.Location((0, 0, dh))) .add(lip, name="lip_bot", @@ -181,10 +204,8 @@ class ShoulderJoint(Model): return result @assembly() - def assembly(self, - wing_root_wall_thickness: float = 25.4/16, - ) -> Cq.Assembly: - directrix = 0 + def assembly(self, deflection: float = 0) -> Cq.Assembly: + directrix = self.directrix_id mat = Material.RESIN_TRANSPERENT mat_spring = Material.STEEL_SPRING result = ( @@ -192,27 +213,29 @@ class ShoulderJoint(Model): .addS(self.child(), name="child", role=Role.CHILD, material=mat) .constrain("child/core", "Fixed") - .addS(self.torsion_joint.spring.generate(), name="spring_top", + .addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top", role=Role.DAMPING, material=mat_spring) - .addS(self.parent(wing_root_wall_thickness), + .addS(self.parent_top(), name="parent_top", role=Role.PARENT, material=mat) - .addS(self.torsion_joint.spring.generate(), name="spring_bot", + .addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot", role=Role.DAMPING, material=mat_spring) - .addS(self.parent(wing_root_wall_thickness), + .addS(self.parent_bot(), name="parent_bot", role=Role.PARENT, material=mat) ) - TorsionJoint.add_constraints(result, - rider="child/rider_top", - track="parent_top/track", - spring="spring_top", - directrix=directrix) - TorsionJoint.add_constraints(result, - rider="child/rider_bot", - track="parent_bot/track", - spring="spring_bot", - directrix=directrix) + TorsionJoint.add_constraints( + result, + rider="child/rider_top", + track="parent_top/track", + spring="spring_top", + directrix=directrix) + TorsionJoint.add_constraints( + result, + rider="child/rider_bot", + track="parent_bot/track", + spring="spring_bot", + directrix=directrix) return result.solve() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 16a23da..e7e352e 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -37,7 +37,6 @@ class WingProfile(Model): spacer_thickness: float = 25.4 / 8 shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( - height=60.0, )) shoulder_width: float = 30.0 shoulder_tip_x: float = -200.0 @@ -94,6 +93,16 @@ class WingProfile(Model): self.wrist_s = math.sin(self.wrist_theta) self.wrist_top_x, self.wrist_top_y = self.wrist_to_abs(0, self.wrist_height) + @submodel(name="shoulder-joint") + def submodel_shoulder_joint(self) -> Model: + return self.shoulder_joint + @submodel(name="elbow-joint") + def submodel_elbow_joint(self) -> Model: + return self.elbow_joint + @submodel(name="wrist-joint") + def submodel_wrist_joint(self) -> Model: + return self.wrist_joint + @property def root_height(self) -> float: return self.shoulder_joint.height @@ -634,9 +643,9 @@ class WingProfile(Model): ( result .constrain("s0/shoulder?conn_top0", "shoulder/parent_top/lip?conn0", "Plane") - .constrain("s0/shoulder?conn_top1", "shoulder/parent_top/lip?conn1", "Plane") + #.constrain("s0/shoulder?conn_top1", "shoulder/parent_top/lip?conn1", "Plane") .constrain("s0/shoulder?conn_bot0", "shoulder/parent_bot/lip?conn0", "Plane") - .constrain("s0/shoulder?conn_bot1", "shoulder/parent_bot/lip?conn1", "Plane") + #.constrain("s0/shoulder?conn_bot1", "shoulder/parent_bot/lip?conn1", "Plane") ) if "s1" in parts: result.add(self.assembly_s1(), name="s1") -- 2.44.1 From 3adb887ef598d69583912584d14629a285f1a369 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 16 Jul 2024 23:32:23 -0700 Subject: [PATCH 080/187] fix: Incorrect staggering of left wings --- nhf/parts/box.py | 1 + nhf/touhou/houjuu_nue/__init__.py | 3 +-- nhf/touhou/houjuu_nue/joints.py | 6 +++++- nhf/touhou/houjuu_nue/wing.py | 18 ++++++++++-------- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 109c667..7d2cbff 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -54,6 +54,7 @@ class MountingBox(Model): generate_side_tags: bool = True + # Determines the position of side tags flip_y: bool = False @target(kind=TargetKind.DXF) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index af76e05..6f8347c 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -62,16 +62,15 @@ class Parameters(Model): wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL( name="l1", wrist_angle=-45.0, - shoulder_joint=MJ.ShoulderJoint(directrix_id=1), )) wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( name="l2", wrist_angle=-30.0, + shoulder_joint=MJ.ShoulderJoint(directrix_id=1), )) wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL( name="l3", wrist_angle=0.0, - shoulder_joint=MJ.ShoulderJoint(directrix_id=1), )) trident: MT.Trident = field(default_factory=lambda: MT.Trident()) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 7840715..246dfa2 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -570,7 +570,7 @@ class DiskJoint(Model): @dataclass(kw_only=True) -class ElbowJoint: +class ElbowJoint(Model): """ Creates the elbow and wrist joints. @@ -629,6 +629,7 @@ class ElbowJoint: r = self.parent_arm_radius return [r - dx, r + dx] + @target(name="child") def child_joint(self) -> Cq.Assembly: angle = -self.disk_joint.tongue_span / 2 dz = self.disk_joint.disk_thickness / 2 @@ -646,9 +647,11 @@ class ElbowJoint: ) return result + @target(name="parent-lower") def parent_joint_lower(self) -> Cq.Workplane: return self.disk_joint.housing_lower() + @target(name="parent-upper") def parent_joint_upper(self): axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) housing_dz = self.disk_joint.housing_upper_dz @@ -683,6 +686,7 @@ class ElbowJoint: ) return result + @assembly() def assembly(self, angle: float = 0) -> Cq.Assembly: result = ( Cq.Assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index e7e352e..86ec226 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -635,6 +635,10 @@ class WingProfile(Model): result = ( Cq.Assembly() ) + tag_top, tag_bot = "top", "bot" + if self.flip: + tag_top, tag_bot = tag_bot, tag_top + if "s0" in parts: result.add(self.assembly_s0(), name="s0") if "shoulder" in parts: @@ -642,20 +646,18 @@ class WingProfile(Model): if "s0" in parts and "shoulder" in parts: ( result - .constrain("s0/shoulder?conn_top0", "shoulder/parent_top/lip?conn0", "Plane") - #.constrain("s0/shoulder?conn_top1", "shoulder/parent_top/lip?conn1", "Plane") - .constrain("s0/shoulder?conn_bot0", "shoulder/parent_bot/lip?conn0", "Plane") - #.constrain("s0/shoulder?conn_bot1", "shoulder/parent_bot/lip?conn1", "Plane") + .constrain(f"s0/shoulder?conn_top0", f"shoulder/parent_{tag_top}/lip?conn0", "Plane") + .constrain(f"s0/shoulder?conn_bot0", f"shoulder/parent_{tag_bot}/lip?conn0", "Plane") ) if "s1" in parts: result.add(self.assembly_s1(), name="s1") if "s1" in parts and "shoulder" in parts: ( result - .constrain("s1/shoulder_top?conn0", "shoulder/child/lip_top?conn0", "Plane") - .constrain("s1/shoulder_top?conn1", "shoulder/child/lip_top?conn1", "Plane") - .constrain("s1/shoulder_bot?conn0", "shoulder/child/lip_bot?conn0", "Plane") - .constrain("s1/shoulder_bot?conn1", "shoulder/child/lip_bot?conn1", "Plane") + .constrain("s1/shoulder_top?conn0", f"shoulder/child/lip_{tag_top}?conn0", "Plane") + .constrain("s1/shoulder_top?conn1", f"shoulder/child/lip_{tag_top}?conn1", "Plane") + .constrain("s1/shoulder_bot?conn0", f"shoulder/child/lip_{tag_bot}?conn0", "Plane") + .constrain("s1/shoulder_bot?conn1", f"shoulder/child/lip_{tag_bot}?conn1", "Plane") ) if "elbow" in parts: result.add(self.elbow_joint.assembly(angle=angle_elbow_wrist), name="elbow") -- 2.44.1 From 572c39d31f0951598922107c4c3493c87401fb1a Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 00:30:41 -0700 Subject: [PATCH 081/187] fix: H-S and shoulder joint locations --- nhf/touhou/houjuu_nue/harness.py | 2 +- nhf/touhou/houjuu_nue/joints.py | 32 +++++++------- nhf/touhou/houjuu_nue/wing.py | 72 +++++++++++++++++++------------- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index a6f4243..3a5ea90 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -25,7 +25,7 @@ class Harness(Model): hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=25.0, - radius_inner=20.0, + radius_inner=15.0, tooth_height=7.0, base_height=5.0, n_tooth=24, diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 246dfa2..8411313 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -6,7 +6,7 @@ from nhf import Material, Role from nhf.build import Model, target, assembly from nhf.parts.springs import TorsionSpring from nhf.parts.joints import TorsionJoint -from nhf.parts.box import box_with_centre_holes +from nhf.parts.box import Hole, MountingBox, box_with_centre_holes import nhf.utils TOL = 1e-6 @@ -37,16 +37,18 @@ class ShoulderJoint(Model): # On the parent side, drill vertical holes - parent_conn_hole_diam: float = 4.0 + parent_conn_hole_diam: float = 6.0 # Position of the holes relative - parent_conn_hole_pos: list[float] = field(default_factory=lambda: [15]) + parent_conn_hole_pos: list[Tuple[float, float]] = field(default_factory=lambda: [ + (15, 8), + (15, -8), + ]) parent_lip_length: float = 25.0 parent_lip_width: float = 30.0 parent_lip_thickness: float = 5.0 parent_lip_ext: float = 40.0 parent_lip_guard_height: float = 8.0 - parent_root_wall_thickness: float = 25.4 / 16 # Measured from centre of axle child_lip_length: float = 45.0 @@ -68,7 +70,6 @@ class ShoulderJoint(Model): def parent(self, top: bool = False) -> Cq.Assembly: joint = self.torsion_joint # Thickness of the lip connecting this joint to the wing root - dz = self.parent_root_wall_thickness assert self.parent_lip_width <= joint.radius_track * 2 assert self.parent_lip_ext > joint.radius_track @@ -77,24 +78,25 @@ class ShoulderJoint(Model): self.parent_lip_ext, self.parent_lip_width, self.parent_lip_guard_height) - .located(Cq.Location((0, -self.parent_lip_width/2 , dz))) - .cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_lip_guard_height + dz)) + .located(Cq.Location((0, -self.parent_lip_width/2 , 0))) + .cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_lip_guard_height)) ) - lip = box_with_centre_holes( - length=self.parent_lip_length - dz, + lip = MountingBox( + length=self.parent_lip_length, width=self.parent_lip_width, - height=self.parent_lip_thickness, - hole_loc=[ - self.height / 2 - dz - x - for x in self.parent_conn_hole_pos + thickness=self.parent_lip_thickness, + holes=[ + Hole(x=self.height / 2 - x, y=y) + for x, y in self.parent_conn_hole_pos ], hole_diam=self.parent_conn_hole_diam, + generate_side_tags=False, ) # Flip so the lip's holes point to -X loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90) # so they point to +X loc_dir = Cq.Location((0,0,0), (0, 0, 1), 180) - loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, dz)) + loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, 0)) rot = -self.axis_rotate_top if top else self.axis_rotate_bot @@ -103,7 +105,7 @@ class ShoulderJoint(Model): .add(joint.track(), name="track", loc=Cq.Location((0, 0, 0), (0, 0, 1), rot)) .add(lip_guard, name="lip_guard") - .add(lip, name="lip", loc=loc_pos * loc_dir * loc_axis) + .add(lip.generate(), name="lip", loc=loc_pos * loc_dir * loc_axis) ) return result diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 86ec226..b029bec 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -21,7 +21,7 @@ class WingProfile(Model): base_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=25.0, - radius_inner=20.0, + radius_inner=15.0, tooth_height=7.0, base_height=5, n_tooth=24, @@ -38,7 +38,7 @@ class WingProfile(Model): shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( )) - shoulder_width: float = 30.0 + shoulder_width: float = 36.0 shoulder_tip_x: float = -200.0 shoulder_tip_y: float = 160.0 shoulder_mid_x: float = -105.0 @@ -160,26 +160,42 @@ class WingProfile(Model): ) return result + def outer_profile_s0(self) -> Cq.Sketch: + """ + The outer boundary of s0, used to produce the curved panel and the + top/bottom slots + """ + tip_x = self.shoulder_tip_x + tip_y = self.shoulder_tip_y + return ( + Cq.Sketch() + .spline([ + (0, 0), + (-30.0, 80.0), + (tip_x, tip_y) + ]) + #.segment( + # (tip_x, tip_y), + # (tip_x - 10, tip_y), + #) + ) + @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 mid_x = self.shoulder_mid_x mid_y = self.shoulder_mid_y + sw = self.shoulder_width sketch = ( - Cq.Sketch() + self.outer_profile_s0() .segment((-self.base_width, 0), (0, 0)) - .spline([ - (0, 0), - (-30.0, 80.0), - (tip_x, tip_y) - ]) .segment( (tip_x, tip_y), - (tip_x, tip_y - self.shoulder_width), + (tip_x, tip_y - sw), ) .segment( - (tip_x, tip_y - self.shoulder_width), + (tip_x, tip_y - sw), (mid_x, mid_y), ) .segment( @@ -191,21 +207,12 @@ class WingProfile(Model): return sketch def outer_shell_s0(self) -> Cq.Workplane: - tip_x = self.shoulder_tip_x - tip_y = self.shoulder_tip_y t = self.panel_thickness - edge = Cq.Edge.makeSpline([ - Cq.Vector(x, y, 0) - for x,y in [ - (0, 0), - (-30.0, 80.0), - (tip_x, tip_y) - ] - ]) + profile = Cq.Wire.assembleEdges(self.outer_profile_s0().edges().vals()) result = ( Cq.Workplane('XZ') .rect(t, self.root_height + t*2, centered=(False, False)) - .sweep(edge) + .sweep(profile) ) plane = result.copyWorkplane(Cq.Workplane('XZ')) plane.moveTo(0, 0).tagPlane("bot") @@ -219,15 +226,15 @@ class WingProfile(Model): """ holes = [ hole - for i, x in enumerate(self.shoulder_joint.parent_conn_hole_pos) + for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos) for hole in [ - Hole(x=x, tag=f"conn_top{i}"), - Hole(x=-x, tag=f"conn_bot{i}"), + Hole(x=x, y=y, tag=f"conn_top{i}"), + Hole(x=-x, y=y, tag=f"conn_bot{i}"), ] ] return MountingBox( length=self.shoulder_joint.height, - width=self.shoulder_width, + width=self.shoulder_joint.parent_lip_width, thickness=self.spacer_thickness, holes=holes, hole_diam=self.shoulder_joint.parent_conn_hole_diam, @@ -263,8 +270,10 @@ class WingProfile(Model): def surface_s0(self, top: bool = False) -> Cq.Workplane: base_dx = -(self.base_width - self.base_plate_width) / 2 base_dy = self.base_joint.joint_height + + axle_dist = self.shoulder_joint.parent_lip_ext tags = [ - ("shoulder", (self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width), 0), + ("shoulder", (self.shoulder_tip_x + axle_dist, self.shoulder_tip_y - self.shoulder_width), 0), ("base", (base_dx, base_dy), 90), ] result = nhf.utils.extrude_with_markers( @@ -284,9 +293,12 @@ class WingProfile(Model): .addS(self.surface_s0(top=True), name="bot", material=self.mat_panel, role=self.role_panel) .addS(self.surface_s0(top=False), name="top", - material=self.mat_panel, role=self.role_panel) - .constrain("bot@faces@>Z", "top@faces@Z", "top@faces@ Date: Wed, 17 Jul 2024 01:06:52 -0700 Subject: [PATCH 082/187] feat: Shoulder joint follow wing direction --- nhf/touhou/houjuu_nue/joints.py | 5 +++-- nhf/touhou/houjuu_nue/wing.py | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 8411313..e9951ec 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -63,6 +63,7 @@ class ShoulderJoint(Model): axis_rotate_top: float = -225.0 directrix_id: int = 0 + angle_neutral: float = 10.0 def __post_init__(self): assert self.parent_lip_length * 2 < self.height @@ -189,8 +190,8 @@ class ShoulderJoint(Model): ) theta = self.torsion_joint.spring.angle_neutral - self.torsion_joint.rider_slot_span loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) - loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot) - loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top) + loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot + self.angle_neutral) + loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral) result = ( Cq.Assembly() .add(core, name="core", loc=Cq.Location()) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b029bec..e21edf2 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -93,6 +93,8 @@ class WingProfile(Model): self.wrist_s = math.sin(self.wrist_theta) self.wrist_top_x, self.wrist_top_y = self.wrist_to_abs(0, self.wrist_height) + self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral + @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: return self.shoulder_joint @@ -180,6 +182,17 @@ class WingProfile(Model): #) ) + @property + def shoulder_angle_neutral(self) -> float: + """ + Returns the neutral angle of the shoulder + """ + dx = self.shoulder_mid_x - self.shoulder_tip_x + dy = -(self.shoulder_mid_y - (self.shoulder_tip_y - self.shoulder_width)) + result = math.degrees(math.atan2(dy, dx)) + assert result >= 0 + return result + @target(name="profile-s0", kind=TargetKind.DXF) def profile_s0(self) -> Cq.Sketch: tip_x = self.shoulder_tip_x @@ -270,10 +283,17 @@ class WingProfile(Model): def surface_s0(self, top: bool = False) -> Cq.Workplane: base_dx = -(self.base_width - self.base_plate_width) / 2 base_dy = self.base_joint.joint_height + sw = self.shoulder_width axle_dist = self.shoulder_joint.parent_lip_ext + theta = math.radians(self.shoulder_joint.angle_neutral) + c, s = math.cos(-theta), math.sin(-theta) tags = [ - ("shoulder", (self.shoulder_tip_x + axle_dist, self.shoulder_tip_y - self.shoulder_width), 0), + # transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2) + ("shoulder", ( + self.shoulder_tip_x + axle_dist * c + (-sw/2) * s, + self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c), + self.shoulder_joint.angle_neutral), ("base", (base_dx, base_dy), 90), ] result = nhf.utils.extrude_with_markers( -- 2.44.1 From d668fb1966ff3976d30da770b29706c730d9c259 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 01:19:17 -0700 Subject: [PATCH 083/187] fix: Splitting line for each wing --- nhf/touhou/houjuu_nue/wing.py | 74 ++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index e21edf2..4834080 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -399,31 +399,14 @@ class WingProfile(Model): return wrist_x, wrist_y - def _mask_elbow(self) -> list[Tuple[float, float]]: """ Polygon shape to mask out parts above the elbow """ - l = 200 - return [ - (0, -l), - (self.elbow_x, -l), - (self.elbow_x, self.elbow_y), - (self.elbow_top_x, self.elbow_top_y), - (self.elbow_top_x, l), - (0, l) - ] - def _mask_wrist(self) -> list[Tuple[float, float]]: - l = 200 - return [ - (0, -l), - (self.wrist_x, -l), - (self.wrist_x, self.wrist_y), - (self.wrist_top_x, self.wrist_top_y), - (self.wrist_top_x, l), - (0, l), - ] + """ + Polygon shape to mask wrist + """ @target(name="profile-s1", kind=TargetKind.DXF) @@ -748,10 +731,10 @@ class WingR(WingProfile): """ elbow_height: float = 111.0 - elbow_x: float = 363.0 - elbow_y: float = 44.0 + elbow_x: float = 285.0 + elbow_y: float = 5.0 # Tilt of elbow w.r.t. shoulder - elbow_angle: float = 30.0 + elbow_angle: float = 25.0 wrist_height: float = 60.0 # Bottom point of the wrist @@ -841,6 +824,29 @@ class WingR(WingProfile): ) return result + def _mask_elbow(self) -> list[Tuple[float, float]]: + l = 200 + return [ + (0, -l), + (self.elbow_x, -l), + (self.elbow_x, self.elbow_y), + (self.elbow_top_x, self.elbow_top_y), + (self.elbow_top_x, l), + (0, l) + ] + + def _mask_wrist(self) -> list[Tuple[float, float]]: + l = 200 + return [ + (0, -l), + (self.wrist_x, -l), + (self.wrist_x, self.wrist_y), + (self.wrist_top_x, self.wrist_top_y), + #(self.wrist_top_x, self.wrist_top_y), + (0, self.wrist_top_y), + ] + + @dataclass(kw_only=True) class WingL(WingProfile): @@ -949,3 +955,25 @@ class WingL(WingProfile): for line in arrow_beziers: result = result.bezier([self.arrow_to_abs(x, y) for x,y in line]) return result.assemble() + + def _mask_elbow(self) -> list[Tuple[float, float]]: + l = 200 + return [ + (0, -l), + (self.elbow_x, -l), + (self.elbow_x, self.elbow_y), + (self.elbow_top_x, self.elbow_top_y), + (self.elbow_top_x, l), + (0, l) + ] + + def _mask_wrist(self) -> list[Tuple[float, float]]: + l = 200 + return [ + (0, -l), + (self.elbow_x, self.wrist_y), + (self.wrist_x, self.wrist_y), + (self.wrist_top_x, self.wrist_top_y), + (self.elbow_x, self.elbow_top_y + 50), + (0, l), + ] -- 2.44.1 From b86904bd967b440c166b2cf35b3fd3b9a92c9e4c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 01:22:05 -0700 Subject: [PATCH 084/187] feat: Smaller disk for wrist joint --- nhf/touhou/houjuu_nue/joints.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index e9951ec..fb0a2b7 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -255,7 +255,7 @@ class Beam: spine_length: float = 10.0 total_height: float = 50.0 - hole_diam: float = 8.0 + hole_diam: float = 6.0 # distance between the centres of the two holes hole_dist: float = 24.0 diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 4834080..3550a6b 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -58,6 +58,8 @@ class WingProfile(Model): wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( movement_angle=45, + radius_disk=13.0, + radius_housing=15.0, ), flip=True, )) -- 2.44.1 From 21e5ad0b82ac1af24d62462e99d1a4b1095f1e43 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 10:22:59 -0700 Subject: [PATCH 085/187] feat: Simplify elbow joint --- nhf/touhou/houjuu_nue/joints.py | 71 +++++++------ nhf/touhou/houjuu_nue/wing.py | 172 +++++++++++++++----------------- 2 files changed, 122 insertions(+), 121 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index fb0a2b7..020ebb1 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -593,44 +593,45 @@ class ElbowJoint(Model): child_arm_radius: float = 40.0 parent_arm_radius: float = 40.0 - child_beam: Beam = field(default_factory=lambda: Beam()) - parent_beam: Beam = field(default_factory=lambda: Beam( - spine_thickness=8.0, - )) - parent_arm_span: float = 40.0 + lip_thickness: float = 5.0 + lip_length: float = 60.0 + hole_pos: list[float] = field(default_factory=lambda: [15, 25]) + parent_arm_span: float = 30.0 # Angle of the beginning of the parent arm parent_arm_angle: float = 180.0 - parent_binding_hole_radius: float = 30.0 # Size of the mounting holes - hole_diam: float = 8.0 + hole_diam: float = 6.0 material: Material = Material.RESIN_TRANSPERENT - # If true, flip the top and bottom tags - flip: bool = False + angle_neutral: float = 20.0 def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span - assert self.parent_binding_hole_radius - self.hole_diam / 2 > self.disk_joint.radius_housing - def child_hole_pos(self) -> list[float]: - """ - List of hole positions measured from axle - """ - dx = self.child_beam.hole_dist / 2 - r = self.child_arm_radius - return [r - dx, r + dx] - def parent_hole_pos(self) -> list[float]: - """ - List of hole positions measured from axle - """ - dx = self.parent_beam.hole_dist / 2 - r = self.parent_arm_radius - return [r - dx, r + dx] + def lip(self) -> Cq.Workplane: + holes = [ + h + for i, x in enumerate(self.hole_pos) + for h in [ + Hole(x=x, tag=f"conn_top{i}"), + Hole(x=-x, tag=f"conn_bot{i}") + ] + ] + mbox = MountingBox( + length=self.lip_length, + width=self.disk_joint.total_thickness, + thickness=self.lip_thickness, + holes=holes, + hole_diam=self.hole_diam, + centred=(True, True), + generate_side_tags=False, + ) + return mbox.generate() @target(name="child") def child_joint(self) -> Cq.Assembly: @@ -639,14 +640,15 @@ class ElbowJoint(Model): # We need to ensure the disk is on the "other" side so flip_x = Cq.Location((0, 0, 0), (1, 0, 0), 180) flip_z = Cq.Location((0, 0, 0), (0, 0, 1), 180) + lip_dz = self.lip_thickness / 2 result = ( - self.child_beam.generate(flip=self.flip) + Cq.Assembly() + .add(self.lip(), name="lip", loc= + Cq.Location((0, 0, 0), (0, 1, 0), 180) * + Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * + Cq.Location((0, 0, 0), (0, 1, 0), 90)) .add(self.disk_joint.disk(), name="disk", loc=flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) - #.constrain("disk", "Fixed") - #.constrain("top", "Fixed") - #.constrain("bot", "Fixed") - #.solve() ) return result @@ -658,7 +660,7 @@ class ElbowJoint(Model): def parent_joint_upper(self): axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) housing_dz = self.disk_joint.housing_upper_dz - conn_h = self.parent_beam.spine_thickness + conn_h = self.lip_thickness connector = ( Cq.Solid.makeCylinder( height=conn_h, @@ -675,10 +677,15 @@ class ElbowJoint(Model): housing_loc = Cq.Location( (0, 0, housing_dz), (0, 0, 1), - -self.disk_joint.tongue_span / 2 + -self.disk_joint.tongue_span / 2 + self.angle_neutral ) + lip_dz = self.lip_thickness / 2 result = ( - self.parent_beam.generate(flip=self.flip) + Cq.Assembly() + .add(self.lip(), name="lip", loc= + Cq.Location((0, 0, 0), (0, 1, 0), 180) * + Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * + Cq.Location((0, 0, 0), (0, 1, 0), 90)) .add(housing, name="housing", loc=axial_offset * housing_loc) .add(connector, name="connector", diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 3550a6b..d74b2ba 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -50,8 +50,9 @@ class WingProfile(Model): disk_joint=DiskJoint( movement_angle=55, ), - flip=False, )) + # Distance between the two spacers on the elbow, halved + elbow_h2: float = 5.0 s2_thickness: float = 25.0 @@ -61,8 +62,9 @@ class WingProfile(Model): radius_disk=13.0, radius_housing=15.0, ), - flip=True, )) + # Distance between the two spacers on the elbow, halved + wrist_h2: float = 5.0 s3_thickness: float = 25.0 @@ -367,6 +369,7 @@ class WingProfile(Model): front_tag: str = "front", back_tag: str = "back", flipped: bool = False, + rotate: bool = False, ): """ For a child joint facing up, front panel should be on the right, back @@ -375,7 +378,7 @@ class WingProfile(Model): site_front, site_back = "right", "left" if flipped: site_front, site_back = site_back, site_front - angle = 0 + angle = 180 if rotate else 0 ( a .addS( @@ -410,6 +413,27 @@ class WingProfile(Model): Polygon shape to mask wrist """ + def spacer_of_joint( + self, + joint: ElbowJoint, + segment_thickness: float, + dx: float, + bot=False) -> MountingBox: + length = joint.lip_length / 2 - dx + holes = [ + Hole(x - dx) + for x in joint.hole_pos + ] + mbox = MountingBox( + length=length, + width=segment_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=joint.hole_diam, + centred=(False, True), + ) + return mbox + @target(name="profile-s1", kind=TargetKind.DXF) def profile_s1(self) -> Cq.Sketch: @@ -429,15 +453,14 @@ class WingProfile(Model): ("shoulder_bot", (shoulder_mount_inset, h), 90), ("shoulder_top", (shoulder_mount_inset, h + shoulder_h), 270), ] - elbow_h = self.elbow_joint.parent_beam.total_height - h = (self.elbow_height - elbow_h) / 2 + h = self.elbow_height / 2 tags_elbow = [ ("elbow_bot", - self.elbow_to_abs(-elbow_mount_inset, h), - self.elbow_angle + 90), + self.elbow_to_abs(-elbow_mount_inset, h - self.elbow_h2), + self.elbow_angle + 0), ("elbow_top", - self.elbow_to_abs(-elbow_mount_inset, h + elbow_h), - self.elbow_angle + 270), + self.elbow_to_abs(-elbow_mount_inset, h + self.elbow_h2), + self.elbow_angle + 0), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow @@ -457,16 +480,10 @@ class WingProfile(Model): ) @submodel(name="spacer-s1-elbow") def spacer_s1_elbow(self) -> MountingBox: - holes = [ - Hole(x) - for x in self.elbow_joint.parent_hole_pos() - ] - return MountingBox( - length=70.0, # FIXME: magic - width=self.s1_thickness, - thickness=self.spacer_thickness, - holes=holes, - hole_diam=self.elbow_joint.hole_diam, + return self.spacer_of_joint( + joint=self.elbow_joint, + segment_thickness=self.s1_thickness, + dx=self.elbow_h2, ) @assembly() def assembly_s1(self) -> Cq.Assembly: @@ -488,7 +505,7 @@ class WingProfile(Model): result, o, point_tag=t, - flipped=is_top != is_parent, + flipped=is_top != True #is_parent, ) return result.solve() @@ -503,58 +520,43 @@ class WingProfile(Model): ) return profile def surface_s2(self, - thickness: float = 25.4/16, elbow_mount_inset: float = 0, wrist_mount_inset: float = 0, front: bool = True) -> Cq.Workplane: - elbow_h = self.elbow_joint.child_beam.total_height - h = (self.elbow_height - elbow_h) / 2 + h = self.elbow_height / 2 tags_elbow = [ ("elbow_bot", - self.elbow_to_abs(elbow_mount_inset, h), - self.elbow_angle + 90), + self.elbow_to_abs(elbow_mount_inset, h - self.elbow_h2), + self.elbow_angle), ("elbow_top", - self.elbow_to_abs(elbow_mount_inset, h + elbow_h), - self.elbow_angle - 90), + self.elbow_to_abs(elbow_mount_inset, h + self.elbow_h2), + self.elbow_angle), ] - wrist_h = self.wrist_joint.parent_beam.total_height - h = (self.wrist_height - wrist_h) / 2 + h = self.wrist_height / 2 tags_wrist = [ ("wrist_bot", - self.wrist_to_abs(-wrist_mount_inset, h), - self.wrist_angle + 90), + self.wrist_to_abs(-wrist_mount_inset, h - self.wrist_h2), + self.wrist_angle), ("wrist_top", - self.wrist_to_abs(-wrist_mount_inset, h + wrist_h), - self.wrist_angle - 90), + self.wrist_to_abs(-wrist_mount_inset, h + self.wrist_h2), + self.wrist_angle), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist - return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front) + return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s2-elbow") def spacer_s2_elbow(self) -> MountingBox: - holes = [ - Hole(x) - for x in self.elbow_joint.child_hole_pos() - ] - return MountingBox( - length=50.0, # FIXME: magic - width=self.s2_thickness, - thickness=self.spacer_thickness, - holes=holes, - hole_diam=self.elbow_joint.hole_diam, + return self.spacer_of_joint( + joint=self.elbow_joint, + segment_thickness=self.s2_thickness, + dx=self.elbow_h2, ) @submodel(name="spacer-s2-wrist") def spacer_s2_wrist(self) -> MountingBox: - holes = [ - Hole(x) - for x in self.wrist_joint.parent_hole_pos() - ] - return MountingBox( - length=70.0, # FIXME: magic - width=self.s1_thickness, - thickness=self.spacer_thickness, - holes=holes, - hole_diam=self.wrist_joint.hole_diam, + return self.spacer_of_joint( + joint=self.wrist_joint, + segment_thickness=self.s2_thickness, + dx=self.wrist_h2, ) @assembly() def assembly_s2(self) -> Cq.Assembly: @@ -576,7 +578,8 @@ class WingProfile(Model): result, o.generate(), point_tag=t, - flipped=is_top != is_parent, + flipped=is_top,# != is_parent, + rotate=is_parent, ) return result.solve() @@ -591,30 +594,23 @@ class WingProfile(Model): def surface_s3(self, front: bool = True) -> Cq.Workplane: wrist_mount_inset = 0 - wrist_h = self.wrist_joint.child_beam.total_height - h = (self.wrist_height - wrist_h) / 2 + h = self.wrist_height / 2 tags = [ ("wrist_bot", - self.wrist_to_abs(wrist_mount_inset, h), - self.wrist_angle + 90), + self.wrist_to_abs(wrist_mount_inset, h - self.wrist_h2), + self.wrist_angle), ("wrist_top", - self.wrist_to_abs(wrist_mount_inset, h + wrist_h), - self.wrist_angle - 90), + self.wrist_to_abs(wrist_mount_inset, h + self.wrist_h2), + self.wrist_angle), ] 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) - for x in self.wrist_joint.child_hole_pos() - ] - return MountingBox( - length=70.0, # FIXME: magic - width=self.s1_thickness, - thickness=self.spacer_thickness, - holes=holes, - hole_diam=self.wrist_joint.hole_diam + return self.spacer_of_joint( + joint=self.wrist_joint, + segment_thickness=self.s3_thickness, + dx=self.wrist_h2, ) @assembly() def assembly_s3(self) -> Cq.Assembly: @@ -645,8 +641,6 @@ class WingProfile(Model): parts: Optional[list[str]] = None, angle_elbow_wrist: float = 0.0, ) -> Cq.Assembly(): - assert not self.elbow_joint.flip - assert self.wrist_joint.flip if parts is None: parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] result = ( @@ -683,20 +677,20 @@ class WingProfile(Model): if "s1" in parts and "elbow" in parts: ( result - .constrain("s1/elbow_top?conn0", "elbow/parent_upper/top?conn0", "Plane") - .constrain("s1/elbow_top?conn1", "elbow/parent_upper/top?conn1", "Plane") - .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/bot?conn0", "Plane") - .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/bot?conn1", "Plane") + .constrain("s1/elbow_top?conn0", "elbow/parent_upper/lip?conn_top0", "Plane") + .constrain("s1/elbow_top?conn1", "elbow/parent_upper/lip?conn_top1", "Plane") + .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/lip?conn_bot0", "Plane") + .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/lip?conn_bot1", "Plane") ) if "s2" in parts: result.add(self.assembly_s2(), name="s2") if "s2" in parts and "elbow" in parts: ( result - .constrain("s2/elbow_top?conn0", "elbow/child/top?conn0", "Plane") - .constrain("s2/elbow_top?conn1", "elbow/child/top?conn1", "Plane") - .constrain("s2/elbow_bot?conn0", "elbow/child/bot?conn0", "Plane") - .constrain("s2/elbow_bot?conn1", "elbow/child/bot?conn1", "Plane") + .constrain("s2/elbow_top?conn0", "elbow/child/lip?conn_top0", "Plane") + .constrain("s2/elbow_top?conn1", "elbow/child/lip?conn_top1", "Plane") + .constrain("s2/elbow_bot?conn0", "elbow/child/lip?conn_bot0", "Plane") + .constrain("s2/elbow_bot?conn1", "elbow/child/lip?conn_bot1", "Plane") ) if "wrist" in parts: result.add(self.wrist_joint.assembly(angle=angle_elbow_wrist), name="wrist") @@ -704,20 +698,20 @@ class WingProfile(Model): # Mounted backwards to bend in other direction ( result - .constrain("s2/wrist_top?conn0", "wrist/parent_upper/top?conn0", "Plane") - .constrain("s2/wrist_top?conn1", "wrist/parent_upper/top?conn1", "Plane") - .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/bot?conn0", "Plane") - .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/bot?conn1", "Plane") + .constrain("s2/wrist_top?conn0", "wrist/parent_upper/bot?conn0", "Plane") + .constrain("s2/wrist_top?conn1", "wrist/parent_upper/bot?conn1", "Plane") + .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/top?conn0", "Plane") + .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/top?conn1", "Plane") ) if "s3" in parts: result.add(self.assembly_s3(), name="s3") if "s3" in parts and "wrist" in parts: ( result - .constrain("s3/wrist_top?conn0", "wrist/child/top?conn0", "Plane") - .constrain("s3/wrist_top?conn1", "wrist/child/top?conn1", "Plane") - .constrain("s3/wrist_bot?conn0", "wrist/child/bot?conn0", "Plane") - .constrain("s3/wrist_bot?conn1", "wrist/child/bot?conn1", "Plane") + .constrain("s3/wrist_top?conn0", "wrist/child/bot?conn0", "Plane") + .constrain("s3/wrist_top?conn1", "wrist/child/bot?conn1", "Plane") + .constrain("s3/wrist_bot?conn0", "wrist/child/top?conn0", "Plane") + .constrain("s3/wrist_bot?conn1", "wrist/child/top?conn1", "Plane") ) if len(parts) > 1: result.solve() -- 2.44.1 From 348799c46ee56370b63b650c72be762bc71be502 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 12:11:08 -0700 Subject: [PATCH 086/187] fix: Tag points on wing --- nhf/touhou/houjuu_nue/wing.py | 40 +++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index d74b2ba..2ba73e9 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -62,6 +62,8 @@ class WingProfile(Model): radius_disk=13.0, radius_housing=15.0, ), + child_arm_radius=20.0, + parent_arm_radius=30.0, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -464,7 +466,8 @@ 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) + 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 = [ @@ -505,7 +508,7 @@ class WingProfile(Model): result, o, point_tag=t, - flipped=is_top != True #is_parent, + flipped=not is_top, ) return result.solve() @@ -527,10 +530,10 @@ class WingProfile(Model): tags_elbow = [ ("elbow_bot", self.elbow_to_abs(elbow_mount_inset, h - self.elbow_h2), - self.elbow_angle), + self.elbow_angle + 180), ("elbow_top", self.elbow_to_abs(elbow_mount_inset, h + self.elbow_h2), - self.elbow_angle), + self.elbow_angle + 180), ] h = self.wrist_height / 2 tags_wrist = [ @@ -578,8 +581,8 @@ class WingProfile(Model): result, o.generate(), point_tag=t, - flipped=is_top,# != is_parent, - rotate=is_parent, + flipped=is_top == is_parent, + #rotate=True, ) return result.solve() @@ -598,10 +601,10 @@ class WingProfile(Model): tags = [ ("wrist_bot", self.wrist_to_abs(wrist_mount_inset, h - self.wrist_h2), - self.wrist_angle), + self.wrist_angle + 180), ("wrist_top", self.wrist_to_abs(wrist_mount_inset, h + self.wrist_h2), - self.wrist_angle), + self.wrist_angle + 180), ] profile = self.profile_s3() return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @@ -632,7 +635,8 @@ class WingProfile(Model): result, o.generate(), point_tag=t, - flipped=is_top != is_parent, + flipped=is_top,# != is_parent, + #rotate=True, ) return result.solve() @@ -698,20 +702,20 @@ class WingProfile(Model): # Mounted backwards to bend in other direction ( result - .constrain("s2/wrist_top?conn0", "wrist/parent_upper/bot?conn0", "Plane") - .constrain("s2/wrist_top?conn1", "wrist/parent_upper/bot?conn1", "Plane") - .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/top?conn0", "Plane") - .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/top?conn1", "Plane") + .constrain("s2/wrist_top?conn0", "wrist/parent_upper/lip?conn_bot0", "Plane") + .constrain("s2/wrist_top?conn1", "wrist/parent_upper/lip?conn_bot1", "Plane") + .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/lip?conn_top0", "Plane") + .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/lip?conn_top1", "Plane") ) if "s3" in parts: result.add(self.assembly_s3(), name="s3") if "s3" in parts and "wrist" in parts: ( result - .constrain("s3/wrist_top?conn0", "wrist/child/bot?conn0", "Plane") - .constrain("s3/wrist_top?conn1", "wrist/child/bot?conn1", "Plane") - .constrain("s3/wrist_bot?conn0", "wrist/child/top?conn0", "Plane") - .constrain("s3/wrist_bot?conn1", "wrist/child/top?conn1", "Plane") + .constrain("s3/wrist_top?conn0", "wrist/child/lip?conn_bot0", "Plane") + .constrain("s3/wrist_top?conn1", "wrist/child/lip?conn_bot1", "Plane") + .constrain("s3/wrist_bot?conn0", "wrist/child/lip?conn_top0", "Plane") + .constrain("s3/wrist_bot?conn1", "wrist/child/lip?conn_top1", "Plane") ) if len(parts) > 1: result.solve() @@ -970,6 +974,6 @@ class WingL(WingProfile): (self.elbow_x, self.wrist_y), (self.wrist_x, self.wrist_y), (self.wrist_top_x, self.wrist_top_y), - (self.elbow_x, self.elbow_top_y + 50), + (self.elbow_x, self.elbow_top_y + l), (0, l), ] -- 2.44.1 From 77cc69acfb8809ed3e453a14c0a28a74f7c1fcbd Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 13:09:46 -0700 Subject: [PATCH 087/187] fix: Arm radius in elbow and shoulder joints --- nhf/touhou/houjuu_nue/joints.py | 10 ++++----- nhf/touhou/houjuu_nue/test.py | 39 +++++++++++++++++++++++++++++++++ nhf/touhou/houjuu_nue/wing.py | 35 +++++++++++------------------ nhf/utils.py | 23 +++++++++++++++++-- 4 files changed, 78 insertions(+), 29 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 020ebb1..2dcd131 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -605,12 +605,12 @@ class ElbowJoint(Model): material: Material = Material.RESIN_TRANSPERENT - angle_neutral: float = 20.0 + angle_neutral: float = 0.0 def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing - self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk + self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span def lip(self) -> Cq.Workplane: @@ -640,7 +640,7 @@ class ElbowJoint(Model): # We need to ensure the disk is on the "other" side so flip_x = Cq.Location((0, 0, 0), (1, 0, 0), 180) flip_z = Cq.Location((0, 0, 0), (0, 0, 1), 180) - lip_dz = self.lip_thickness / 2 + lip_dz = self.lip_thickness result = ( Cq.Assembly() .add(self.lip(), name="lip", loc= @@ -664,7 +664,7 @@ class ElbowJoint(Model): connector = ( Cq.Solid.makeCylinder( height=conn_h, - radius=self.parent_arm_radius, + radius=self.parent_arm_radius - self.lip_thickness / 2, angleDegrees=self.parent_arm_span) .cut(Cq.Solid.makeCylinder( height=conn_h, @@ -679,7 +679,7 @@ class ElbowJoint(Model): (0, 0, 1), -self.disk_joint.tongue_span / 2 + self.angle_neutral ) - lip_dz = self.lip_thickness / 2 + lip_dz = self.lip_thickness result = ( Cq.Assembly() .add(self.lip(), name="lip", loc= diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index ea10f8a..0fcda84 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -10,6 +10,23 @@ class TestJoints(unittest.TestCase): j = MJ.ShoulderJoint() assembly = j.assembly() self.assertEqual(pairwise_intersection(assembly), []) + + def test_shoulder_joint_dist(self): + """ + Tests the arm radius + """ + j = MJ.ShoulderJoint() + for deflection in [0, 40, 95, 120]: + with self.subTest(deflection=deflection): + a = j.assembly(deflection=deflection) + # Axle + o = a.get_abs_location("parent_top/track?spring") + l_c1 = a.get_abs_location("parent_top/lip?conn0") + l_c2= a.get_abs_location("parent_top/lip?conn1") + v_c = 0.5 * ((l_c1 - o) + (l_c2 - o)) + v_c.z = 0 + self.assertAlmostEqual(v_c.Length, j.parent_lip_ext) + def test_disk_collision_0(self): j = MJ.DiskJoint() assembly = j.assembly(angle=0) @@ -23,6 +40,28 @@ class TestJoints(unittest.TestCase): assembly = j.assembly(angle=j.movement_angle) self.assertEqual(pairwise_intersection(assembly), []) + def test_elbow_joint_dist(self): + """ + Tests the arm radius + """ + j = MJ.ElbowJoint() + for angle in [0, 10, 20, j.disk_joint.movement_angle]: + with self.subTest(angle=angle): + a = j.assembly(angle=angle) + o = a.get_abs_location("child/disk?mate_bot") + l_c1 = a.get_abs_location("child/lip?conn_top0") + l_c2 = a.get_abs_location("child/lip?conn_bot0") + v_c = 0.5 * ((l_c1 - o) + (l_c2 - o)) + v_c.z = 0 + self.assertAlmostEqual(v_c.Length, j.child_arm_radius) + + l_p1 = a.get_abs_location("parent_upper/lip?conn_top0") + l_p2 = a.get_abs_location("parent_upper/lip?conn_bot0") + v_p = 0.5 * ((l_p1 - o) + (l_p2 - o)) + v_p.z = 0 + self.assertAlmostEqual(v_p.Length, j.parent_arm_radius) + + class Test(unittest.TestCase): def test_hs_joint_parent(self): diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 2ba73e9..1962157 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -445,23 +445,20 @@ class WingProfile(Model): .polygon(self._mask_elbow(), mode='i') ) return profile - def surface_s1(self, - shoulder_mount_inset: float = 0, - elbow_mount_inset: float = 0, - front: bool = True) -> Cq.Workplane: + def surface_s1(self, front: bool = True) -> Cq.Workplane: shoulder_h = self.shoulder_joint.child_height h = (self.shoulder_joint.height - shoulder_h) / 2 tags_shoulder = [ - ("shoulder_bot", (shoulder_mount_inset, h), 90), - ("shoulder_top", (shoulder_mount_inset, h + shoulder_h), 270), + ("shoulder_bot", (0, h), 90), + ("shoulder_top", (0, h + shoulder_h), 270), ] h = self.elbow_height / 2 tags_elbow = [ ("elbow_bot", - self.elbow_to_abs(-elbow_mount_inset, h - self.elbow_h2), + self.elbow_to_abs(-self.elbow_joint.parent_arm_radius, h - self.elbow_h2), self.elbow_angle + 0), ("elbow_top", - self.elbow_to_abs(-elbow_mount_inset, h + self.elbow_h2), + self.elbow_to_abs(-self.elbow_joint.parent_arm_radius, h + self.elbow_h2), self.elbow_angle + 0), ] profile = self.profile_s1() @@ -522,26 +519,23 @@ class WingProfile(Model): .polygon(self._mask_wrist(), mode='i') ) return profile - def surface_s2(self, - elbow_mount_inset: float = 0, - wrist_mount_inset: float = 0, - front: bool = True) -> Cq.Workplane: + def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 tags_elbow = [ ("elbow_bot", - self.elbow_to_abs(elbow_mount_inset, h - self.elbow_h2), + self.elbow_to_abs(self.elbow_joint.child_arm_radius, h - self.elbow_h2), self.elbow_angle + 180), ("elbow_top", - self.elbow_to_abs(elbow_mount_inset, h + self.elbow_h2), + self.elbow_to_abs(self.elbow_joint.child_arm_radius, h + self.elbow_h2), self.elbow_angle + 180), ] h = self.wrist_height / 2 tags_wrist = [ ("wrist_bot", - self.wrist_to_abs(-wrist_mount_inset, h - self.wrist_h2), + self.wrist_to_abs(-self.wrist_joint.parent_arm_radius, h - self.wrist_h2), self.wrist_angle), ("wrist_top", - self.wrist_to_abs(-wrist_mount_inset, h + self.wrist_h2), + self.wrist_to_abs(-self.wrist_joint.parent_arm_radius, h + self.wrist_h2), self.wrist_angle), ] profile = self.profile_s2() @@ -596,14 +590,13 @@ class WingProfile(Model): return profile def surface_s3(self, front: bool = True) -> Cq.Workplane: - wrist_mount_inset = 0 h = self.wrist_height / 2 tags = [ ("wrist_bot", - self.wrist_to_abs(wrist_mount_inset, h - self.wrist_h2), + self.wrist_to_abs(self.wrist_joint.child_arm_radius, h - self.wrist_h2), self.wrist_angle + 180), ("wrist_top", - self.wrist_to_abs(wrist_mount_inset, h + self.wrist_h2), + self.wrist_to_abs(self.wrist_joint.child_arm_radius, h + self.wrist_h2), self.wrist_angle + 180), ] profile = self.profile_s3() @@ -629,14 +622,12 @@ class WingProfile(Model): ) for t in ["wrist_bot", "wrist_top"]: is_top = t.endswith("_top") - is_parent = True o = self.spacer_s3_wrist() self._assembly_insert_spacer( result, o.generate(), point_tag=t, - flipped=is_top,# != is_parent, - #rotate=True, + flipped=is_top, ) return result.solve() diff --git a/nhf/utils.py b/nhf/utils.py index 1dc4d3c..7878472 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -11,8 +11,6 @@ import cadquery as Cq from nhf import Role from typing import Union, Tuple, cast -COLOR_MARKER = Cq.Color(0, 1, 1, 1) - # Bug fixes def _subloc(self, name: str) -> Tuple[Cq.Location, str]: """ @@ -37,6 +35,15 @@ def _subloc(self, name: str) -> Tuple[Cq.Location, str]: return (rv, name_out) Cq.Assembly._subloc = _subloc +### Vector arithmetic + +def location_sub(self: Cq.Location, rhs: Cq.Location) -> Cq.Vector: + (x1, y1, z1), _ = self.toTuple() + (x2, y2, z2), _ = rhs.toTuple() + return Cq.Vector(x1 - x2, y1 - y2, z1 - z2) +Cq.Location.__sub__ = location_sub + +### Tags def tagPoint(self, tag: str): """ @@ -100,6 +107,8 @@ def make_arrow(size: float = 2) -> Cq.Workplane: def to_marker_name(tag: str) -> str: return tag.replace("?", "__T").replace("/", "__Z") + "_marker" +COLOR_MARKER = Cq.Color(0, 1, 1, 1) + def mark_point(self: Cq.Assembly, tag: str, size: float = 2, @@ -132,6 +141,16 @@ def mark_plane(self: Cq.Assembly, Cq.Assembly.markPlane = mark_plane +def get_abs_location(self: Cq.Assembly, + tag: str) -> Cq.Location: + name, shape = self._query(tag) + loc_self = shape.location() + loc_parent, _ = self._subloc(name) + loc = loc_parent * loc_self + return loc + +Cq.Assembly.get_abs_location = get_abs_location + def extrude_with_markers(sketch: Cq.Sketch, thickness: float, -- 2.44.1 From 3aa4a592f0a9bd281a1a2b5ba44ce6482850fb9f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 13:20:06 -0700 Subject: [PATCH 088/187] fix: Collision between spring and track --- nhf/checks.py | 27 +++++++++++++++++++++++++++ nhf/parts/joints.py | 1 + nhf/parts/test.py | 4 ++-- nhf/touhou/houjuu_nue/joints.py | 1 + nhf/touhou/houjuu_nue/test.py | 4 ++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/nhf/checks.py b/nhf/checks.py index a5a413d..1f1f4c3 100644 --- a/nhf/checks.py +++ b/nhf/checks.py @@ -6,6 +6,33 @@ def binary_intersection(a: Cq.Assembly) -> Cq.Shape: obj1, obj2 = objs[:2] return obj1.intersect(obj2) +def visualize_intersection(assembly: Cq.Assembly, tol: float=1e-6) -> Cq.Shape: + """ + Given an assembly, test the pairwise intersection volume of its components. + Return the pairs whose intersection volume exceeds `tol`. + """ + m = {name: (i, shape.moved(loc)) + for i, (shape, name, loc, _) + in enumerate(assembly)} + for name, (i1, sh1) in m.items(): + for name2, (i2, sh2) in m.items(): + if name == name2: + assert i1 == i2 + continue + if i2 <= i1: + # Remove the upper diagonal + continue + head = name.split('/', 2)[1] + head2 = name2.split('/', 2)[1] + if head == head2: + # Do not test into subassemblies + continue + + isect = sh1.intersect(sh2) + vol = isect.Volume() + if vol > tol: + return isect + return None def pairwise_intersection(assembly: Cq.Assembly, tol: float=1e-6) -> list[(str, str, float)]: """ diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index f80b1b6..20fd507 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -218,6 +218,7 @@ class TorsionJoint: assert self.radius_rider > self.groove_radius_outer > self.groove_radius_inner + self.groove_inner_gap assert self.groove_radius_inner > self.spring.radius > self.radius_axle assert self.spring.height > self.groove_depth, "Groove is too deep" + assert self.groove_depth < self.spring.height - self.spring.thickness * 2 @property def total_height(self): diff --git a/nhf/parts/test.py b/nhf/parts/test.py index b711efa..6e01420 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -1,7 +1,7 @@ import unittest import cadquery as Cq from nhf.checks import binary_intersection, pairwise_intersection -from nhf.parts import joints, handle, metric_threads +from nhf.parts import joints, handle, metric_threads, springs class TestJoints(unittest.TestCase): @@ -36,7 +36,7 @@ class TestJoints(unittest.TestCase): with self.subTest(slot=slot, right_handed=False): self.torsion_joint_case(j, slot) def test_torsion_joint_right_handed(self): - j = joints.TorsionJoint(right_handed=True) + j = joints.TorsionJoint(springs.TorsionSpring(right_handed=True)) for slot in range(j.rider_n_slots): with self.subTest(slot=slot, right_handed=True): self.torsion_joint_case(j, slot) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 2dcd131..a01bb14 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -18,6 +18,7 @@ class ShoulderJoint(Model): torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( radius_track=18, radius_rider=18, + groove_depth=4.8, groove_radius_outer=16, groove_radius_inner=13, track_disk_height=5.0, diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 0fcda84..01fcc74 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -6,6 +6,10 @@ from nhf.checks import pairwise_intersection class TestJoints(unittest.TestCase): + def test_shoulder_collision_of_torsion_joint(self): + j = MJ.ShoulderJoint() + assembly = j.torsion_joint.rider_track_assembly() + self.assertEqual(pairwise_intersection(assembly), []) def test_shoulder_collision_0(self): j = MJ.ShoulderJoint() assembly = j.assembly() -- 2.44.1 From bbe24091da1248b4b3393393ef0f8355fd49f262 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 13:27:48 -0700 Subject: [PATCH 089/187] fix: Target name --- nhf/touhou/houjuu_nue/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 6f8347c..4c24b10 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -104,7 +104,7 @@ class Parameters(Model): @submodel(name="wing-r3") def submodel_wing_r3(self) -> Model: return self.wing_r3 - @submodel(name="wing-r1") + @submodel(name="wing-l1") def submodel_wing_l1(self) -> Model: return self.wing_l1 @submodel(name="wing-l2") -- 2.44.1 From eb445b3d8b81911287498cb2d05b2d3bf3952021 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 14:47:22 -0700 Subject: [PATCH 090/187] fix: Housing wall location --- nhf/touhou/houjuu_nue/joints.py | 54 ++++++++++++++++++++------------- nhf/touhou/houjuu_nue/wing.py | 6 +++- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index a01bb14..45bce76 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -326,8 +326,8 @@ class DiskJoint(Model): radius_disk: float = 20.0 radius_axle: float = 3.0 - housing_thickness: float = 5.0 - disk_thickness: float = 5.0 + housing_thickness: float = 4.0 + disk_thickness: float = 7.0 # Gap between disk and the housing #disk_thickness_gap: float = 0.1 @@ -382,9 +382,8 @@ class DiskJoint(Model): """ return self.total_thickness / 2 - self.housing_thickness - @target(name="disk") - def disk(self) -> Cq.Workplane: - cut = ( + def _disk_cut(self) -> Cq.Workplane: + return ( Cq.Solid.makeBox( length=self.spring.tail_length, width=self.spring.thickness, @@ -393,6 +392,9 @@ class DiskJoint(Model): .located(Cq.Location((0, self.spring.radius_inner, 0))) .rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset) ) + + @target(name="disk") + def disk(self) -> Cq.Workplane: radius_tongue = self.radius_disk + self.tongue_length tongue = ( Cq.Solid.makeCylinder( @@ -419,7 +421,7 @@ class DiskJoint(Model): centered=(True, True, False), combine='cut', ) - .cut(cut) + .cut(self._disk_cut()) ) plane = result.copyWorkplane(Cq.Workplane('XY')) theta = math.radians(self.spring_slot_offset) @@ -460,7 +462,7 @@ class DiskJoint(Model): result = result.cut( self .wall() - .located(Cq.Location((0, 0, self.disk_thickness - self.wall_inset))) + .located(Cq.Location((0, 0, self.housing_thickness - self.wall_inset))) #.rotate((0, 0, 0), (1, 0, 0), 180) #.located(Cq.Location((0, 0, self.disk_thickness + self.housing_thickness))) ) @@ -512,8 +514,8 @@ class DiskJoint(Model): ) result = ( result - .cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset)))) .union(wall, tol=TOL) + .cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset)))) ) return result.clean() @@ -597,7 +599,7 @@ class ElbowJoint(Model): lip_thickness: float = 5.0 lip_length: float = 60.0 hole_pos: list[float] = field(default_factory=lambda: [15, 25]) - parent_arm_span: float = 30.0 + parent_arm_width: float = 10.0 # Angle of the beginning of the parent arm parent_arm_angle: float = 180.0 @@ -612,7 +614,6 @@ class ElbowJoint(Model): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 - assert self.disk_joint.movement_angle < self.parent_arm_angle < 360 - self.parent_arm_span def lip(self) -> Cq.Workplane: holes = [ @@ -642,14 +643,18 @@ class ElbowJoint(Model): flip_x = Cq.Location((0, 0, 0), (1, 0, 0), 180) flip_z = Cq.Location((0, 0, 0), (0, 0, 1), 180) lip_dz = self.lip_thickness + loc_lip = ( + Cq.Location((0, 0, 0), (0, 1, 0), 180) * + Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * + Cq.Location((0, 0, 0), (0, 1, 0), 90) + ) + loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle) + disk_cut = self.disk_joint._disk_cut().located( + loc_lip.inverse * Cq.Location((0, self.disk_joint.spring.radius_inner, 0)) * loc_disk) result = ( Cq.Assembly() - .add(self.lip(), name="lip", loc= - Cq.Location((0, 0, 0), (0, 1, 0), 180) * - Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * - Cq.Location((0, 0, 0), (0, 1, 0), 90)) - .add(self.disk_joint.disk(), name="disk", - loc=flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle)) + .add(self.lip().cut(disk_cut), name="lip", loc=loc_lip) + .add(self.disk_joint.disk(), name="disk", loc=loc_disk) ) return result @@ -661,18 +666,25 @@ class ElbowJoint(Model): def parent_joint_upper(self): axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) housing_dz = self.disk_joint.housing_upper_dz - conn_h = self.lip_thickness + conn_h = self.disk_joint.total_thickness + conn_w = self.parent_arm_width connector = ( - Cq.Solid.makeCylinder( + Cq.Solid.makeBox( + length=self.parent_arm_radius, + width=conn_w, height=conn_h, - radius=self.parent_arm_radius - self.lip_thickness / 2, - angleDegrees=self.parent_arm_span) + ).located(Cq.Location((0, -conn_w/2, 0))) + #Cq.Solid.makeCylinder( + # height=conn_h, + # radius=self.parent_arm_radius - self.lip_thickness / 2, + # angleDegrees=self.parent_arm_span) .cut(Cq.Solid.makeCylinder( height=conn_h, radius=self.disk_joint.radius_housing, )) .located(Cq.Location((0, 0, -conn_h / 2))) - .rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2) + .rotate((0,0,0), (0,0,1), 180) + #.rotate((0,0,0), (0,0,1), 180-self.parent_arm_span / 2) ) housing = self.disk_joint.housing_upper() housing_loc = Cq.Location( diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 1962157..697f2e1 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -50,6 +50,7 @@ class WingProfile(Model): disk_joint=DiskJoint( movement_angle=55, ), + hole_diam=6.0, )) # Distance between the two spacers on the elbow, halved elbow_h2: float = 5.0 @@ -62,8 +63,11 @@ class WingProfile(Model): radius_disk=13.0, radius_housing=15.0, ), - child_arm_radius=20.0, + hole_pos=[10, 20], + lip_length=50, + child_arm_radius=23.0, parent_arm_radius=30.0, + hole_diam=4.0, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 -- 2.44.1 From 9de4159166786c784c8ed64827be3676fcda1cbd Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 14:47:34 -0700 Subject: [PATCH 091/187] feat: 2d location --- nhf/test.py | 16 ++++++++++++++++ nhf/utils.py | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/nhf/test.py b/nhf/test.py index fc9d15b..ebef77c 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -62,6 +62,22 @@ class TestChecks(unittest.TestCase): class TestUtils(unittest.TestCase): + def test_2d_orientation(self): + l1 = Cq.Location.from2d(1.2, 0) + l2 = Cq.Location.from2d(0, 0, 90) + l3 = l2 * l1 + (x, y), r = l3.to2d() + self.assertAlmostEqual(x, 0) + self.assertAlmostEqual(y, 1.2) + self.assertAlmostEqual(r, 90) + + def test_2d_planar(self): + l1 = Cq.Location.from2d(1.2, 4.5, 67) + l2 = Cq.Location.from2d(98, 5.4, 36) + l3 = Cq.Location.from2d(10, 10, 0) + l = l3 * l2 * l1 + self.assertTrue(l.is2d()) + def test_tag_point(self): """ A board with 3 holes of unequal sizes. Each hole is marked diff --git a/nhf/utils.py b/nhf/utils.py index 7878472..7b7a16a 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -43,6 +43,26 @@ def location_sub(self: Cq.Location, rhs: Cq.Location) -> Cq.Vector: return Cq.Vector(x1 - x2, y1 - y2, z1 - z2) Cq.Location.__sub__ = location_sub +def from2d(x: float, y: float, rotate: float=0.0) -> Cq.Location: + return Cq.Location((x, y, 0), (0, 0, 1), rotate) +Cq.Location.from2d = from2d + +def is2d(self: Cq.Location) -> bool: + (_, _, z), (rx, ry, _) = self.toTuple() + return z == 0 and rx == 0 and ry == 0 +Cq.Location.is2d = is2d + +def to2d(self: Cq.Location) -> Tuple[Tuple[float, float], float]: + """ + Returns position and angle + """ + (x, y, z), (rx, ry, rz) = self.toTuple() + assert z == 0 + assert rx == 0 + assert ry == 0 + return (x, y), rz +Cq.Location.to2d = to2d + ### Tags def tagPoint(self, tag: str): -- 2.44.1 From 014784be34d7722776b8fe6e5da6725b7f78801f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 19:13:06 -0700 Subject: [PATCH 092/187] feat: Calculation of total mass --- nhf/materials.py | 31 +++++++++++++++++++++++-------- nhf/touhou/houjuu_nue/__init__.py | 12 ++++++++++-- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index f87b796..714995c 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -54,16 +54,18 @@ ROLE_COLOR_MAP = { class Material(Enum): """ A catalog of common material properties + + Density listed is in g/cm^3 or mg/mm^3 """ - WOOD_BIRCH = 0.8, _color('bisque', 0.9) - PLASTIC_PLA = 0.5, _color('mistyrose', 0.8) - RESIN_TRANSPERENT = 1.1, _color('cadetblue2', 0.6) - RESIN_TOUGH_1500 = 1.1, _color('seashell3', 0.7) - ACRYLIC_BLACK = 0.5, _color('gray5', 0.8) - ACRYLIC_TRANSLUSCENT = 0.5, _color('ivory2', 0.8) - ACRYLIC_TRANSPARENT = 0.5, _color('ghostwhite', 0.5) - STEEL_SPRING = 1.0, _color('gray', 0.8) + WOOD_BIRCH = 0.71, _color('bisque', 0.9) + PLASTIC_PLA = 1.2, _color('mistyrose', 0.8) + RESIN_TRANSPERENT = 1.17, _color('cadetblue2', 0.6) + RESIN_TOUGH_1500 = 1.17, _color('seashell3', 0.7) + ACRYLIC_BLACK = 1.18, _color('gray5', 0.8) + ACRYLIC_TRANSLUSCENT = 1.18, _color('ivory2', 0.8) + ACRYLIC_TRANSPARENT = 1.18, _color('ghostwhite', 0.5) + STEEL_SPRING = 7.8, _color('gray', 0.8) def __init__(self, density: float, color: Cq.Color): self.density = density @@ -110,3 +112,16 @@ def color_by_role(self: Cq.Assembly, avg: bool = True) -> Cq.Assembly: 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 diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 4c24b10..a9cb819 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -146,11 +146,19 @@ class Parameters(Model): bbox = a.toCompound().BoundingBox() return { "wing-span": bbox.xlen, - "wing-thickness": bbox.ylen, + "wing-depth": bbox.ylen, "wing-height": bbox.zlen, + "wing-mass": a.total_mass(), } if __name__ == '__main__': + import sys + p = Parameters() - p.build_all() + if len(sys.argv) == 1: + p.build_all() + sys.exit(0) + + if sys.argv[1] == 'stat': + print(p.stat()) -- 2.44.1 From 6d72749c9b639689f8fcfba0698d990e2cfa2efc Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 19:28:56 -0700 Subject: [PATCH 093/187] refactor: Use 2d location in extrusion argument --- nhf/parts/planar.py | 36 ++++++++++++++++++ nhf/touhou/houjuu_nue/wing.py | 69 ++++++++++++++++++----------------- nhf/utils.py | 35 +----------------- 3 files changed, 72 insertions(+), 68 deletions(-) create mode 100644 nhf/parts/planar.py diff --git a/nhf/parts/planar.py b/nhf/parts/planar.py new file mode 100644 index 0000000..9405943 --- /dev/null +++ b/nhf/parts/planar.py @@ -0,0 +1,36 @@ +""" +Operations on planar geometry (usually used for laser cutting parts) +""" +import math +from typing import Tuple +import cadquery as Cq + +def extrude_with_markers( + sketch: Cq.Sketch, + thickness: float, + tags: list[Tuple[str, Cq.Location]], + reverse: bool = False): + """ + Extrudes a sketch and place tags on the sketch for mating. + + Each tag is of the format `(name, loc)`, where the (must be 2d) location's + angle is specifies in degrees counterclockwise from +X. Two marks are + generated for each `name`, "{name}" for the location (with normal) and + "{name}_dir" for the directrix specified by the angle. + + This simulates a process of laser cutting and bonding (for wood and acrylic) + """ + result = ( + Cq.Workplane('XY') + .placeSketch(sketch) + .extrude(thickness) + ) + plane = result.faces("Z").workplane() + sign = -1 if reverse else 1 + for tag, p in tags: + (x, y), angle = p.to2d() + theta = sign * math.radians(angle) + direction = (math.cos(theta), math.sin(theta), 0) + plane.moveTo(x, sign * y).tagPlane(tag) + plane.moveTo(x, sign * y).tagPlane(f"{tag}_dir", direction) + return result diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 697f2e1..2501043 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -12,6 +12,7 @@ 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 +from nhf.parts.planar import extrude_with_markers import nhf.utils @dataclass(kw_only=True) @@ -300,13 +301,13 @@ class WingProfile(Model): c, s = math.cos(-theta), math.sin(-theta) tags = [ # transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2) - ("shoulder", ( + ("shoulder", Cq.Location.from2d( self.shoulder_tip_x + axle_dist * c + (-sw/2) * s, - self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c), - self.shoulder_joint.angle_neutral), - ("base", (base_dx, base_dy), 90), + self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c, + self.shoulder_joint.angle_neutral)), + ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ] - result = nhf.utils.extrude_with_markers( + result = extrude_with_markers( self.profile_s0(), self.panel_thickness, tags, @@ -453,21 +454,21 @@ class WingProfile(Model): shoulder_h = self.shoulder_joint.child_height h = (self.shoulder_joint.height - shoulder_h) / 2 tags_shoulder = [ - ("shoulder_bot", (0, h), 90), - ("shoulder_top", (0, h + shoulder_h), 270), + ("shoulder_bot", Cq.Location.from2d(0, h, 90)), + ("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)), ] h = self.elbow_height / 2 tags_elbow = [ - ("elbow_bot", - self.elbow_to_abs(-self.elbow_joint.parent_arm_radius, h - self.elbow_h2), - self.elbow_angle + 0), - ("elbow_top", - self.elbow_to_abs(-self.elbow_joint.parent_arm_radius, h + self.elbow_h2), - self.elbow_angle + 0), + ("elbow_bot", Cq.Location.from2d( + *self.elbow_to_abs(-self.elbow_joint.parent_arm_radius, h - self.elbow_h2), + self.elbow_angle + 0)), + ("elbow_top", Cq.Location.from2d( + *self.elbow_to_abs(-self.elbow_joint.parent_arm_radius, h + self.elbow_h2), + self.elbow_angle + 0)), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow - return nhf.utils.extrude_with_markers( + return extrude_with_markers( profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s1-shoulder") def spacer_s1_shoulder(self) -> MountingBox: @@ -526,25 +527,25 @@ class WingProfile(Model): def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 tags_elbow = [ - ("elbow_bot", - self.elbow_to_abs(self.elbow_joint.child_arm_radius, h - self.elbow_h2), - self.elbow_angle + 180), - ("elbow_top", - self.elbow_to_abs(self.elbow_joint.child_arm_radius, h + self.elbow_h2), - self.elbow_angle + 180), + ("elbow_bot", Cq.Location.from2d( + *self.elbow_to_abs(self.elbow_joint.child_arm_radius, h - self.elbow_h2), + self.elbow_angle + 180)), + ("elbow_top", Cq.Location.from2d( + *self.elbow_to_abs(self.elbow_joint.child_arm_radius, h + self.elbow_h2), + self.elbow_angle + 180)), ] h = self.wrist_height / 2 tags_wrist = [ - ("wrist_bot", - self.wrist_to_abs(-self.wrist_joint.parent_arm_radius, h - self.wrist_h2), - self.wrist_angle), - ("wrist_top", - self.wrist_to_abs(-self.wrist_joint.parent_arm_radius, h + self.wrist_h2), - self.wrist_angle), + ("wrist_bot", Cq.Location.from2d( + *self.wrist_to_abs(-self.wrist_joint.parent_arm_radius, h - self.wrist_h2), + self.wrist_angle)), + ("wrist_top", Cq.Location.from2d( + *self.wrist_to_abs(-self.wrist_joint.parent_arm_radius, h + self.wrist_h2), + self.wrist_angle)), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist - return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s2-elbow") def spacer_s2_elbow(self) -> MountingBox: return self.spacer_of_joint( @@ -596,15 +597,15 @@ class WingProfile(Model): front: bool = True) -> Cq.Workplane: h = self.wrist_height / 2 tags = [ - ("wrist_bot", - self.wrist_to_abs(self.wrist_joint.child_arm_radius, h - self.wrist_h2), - self.wrist_angle + 180), - ("wrist_top", - self.wrist_to_abs(self.wrist_joint.child_arm_radius, h + self.wrist_h2), - self.wrist_angle + 180), + ("wrist_bot", Cq.Location.from2d( + *self.wrist_to_abs(self.wrist_joint.child_arm_radius, h - self.wrist_h2), + self.wrist_angle + 180)), + ("wrist_top", Cq.Location.from2d( + *self.wrist_to_abs(self.wrist_joint.child_arm_radius, h + self.wrist_h2), + self.wrist_angle + 180)), ] profile = self.profile_s3() - return nhf.utils.extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s3-wrist") def spacer_s3_wrist(self) -> MountingBox: return self.spacer_of_joint( diff --git a/nhf/utils.py b/nhf/utils.py index 7b7a16a..db8c73d 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -1,9 +1,5 @@ """ -Marking utilities for `Cq.Workplane` - -Adds the functions to `Cq.Workplane`: -1. `tagPoint` -2. `tagPlane` +Utility functions for cadquery objects """ import math import functools @@ -170,32 +166,3 @@ def get_abs_location(self: Cq.Assembly, return loc Cq.Assembly.get_abs_location = get_abs_location - - -def extrude_with_markers(sketch: Cq.Sketch, - thickness: float, - tags: list[Tuple[str, Tuple[float, float], float]], - reverse: bool = False): - """ - Extrudes a sketch and place tags on the sketch for mating. - - Each tag is of the format `(name, (x, y), angle)`, where the angle is - specifies in degrees counterclockwise from +X. Two marks are generated for - each `name`, "{name}" for the location (with normal) and "{name}_dir" for - the directrix specified by the angle. - - This simulates a process of laser cutting and bonding (for wood and acrylic) - """ - result = ( - Cq.Workplane('XY') - .placeSketch(sketch) - .extrude(thickness) - ) - plane = result.faces("Z").workplane() - sign = -1 if reverse else 1 - for tag, (px, py), angle in tags: - theta = sign * math.radians(angle) - direction = (math.cos(theta), math.sin(theta), 0) - plane.moveTo(px, sign * py).tagPlane(tag) - plane.moveTo(px, sign * py).tagPlane(f"{tag}_dir", direction) - return result -- 2.44.1 From 6c6c17ea07cd98570d0bc2300c21fc5d54d5e579 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 21:17:50 -0700 Subject: [PATCH 094/187] refactor: Use 2d locations for wing tags --- nhf/touhou/houjuu_nue/__init__.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 238 +++++++++++++----------------- nhf/utils.py | 23 +++ 3 files changed, 129 insertions(+), 134 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index a9cb819..13df365 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -70,7 +70,7 @@ class Parameters(Model): )) wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL( name="l3", - wrist_angle=0.0, + wrist_angle=-0.0, )) trident: MT.Trident = field(default_factory=lambda: MT.Trident()) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 2501043..b43b802 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -81,13 +81,9 @@ class WingProfile(Model): role_panel: Role = Role.STRUCTURE # Subclass must populate - elbow_x: float - elbow_y: float - elbow_angle: float + elbow_bot_loc: Cq.Location elbow_height: float - wrist_x: float - wrist_y: float - wrist_angle: float + wrist_bot_loc: Cq.Location wrist_height: float flip: bool = False @@ -95,14 +91,8 @@ class WingProfile(Model): def __post_init__(self): super().__init__(name=self.name) - self.elbow_theta = math.radians(self.elbow_angle) - self.elbow_c = math.cos(self.elbow_theta) - self.elbow_s = math.sin(self.elbow_theta) - self.elbow_top_x, self.elbow_top_y = self.elbow_to_abs(0, self.elbow_height) - self.wrist_theta = math.radians(self.wrist_angle) - self.wrist_c = math.cos(self.wrist_theta) - self.wrist_s = math.sin(self.wrist_theta) - self.wrist_top_x, self.wrist_top_y = self.wrist_to_abs(0, self.wrist_height) + self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) + self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral @@ -401,16 +391,6 @@ class WingProfile(Model): "Axis", param=angle) ) - def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]: - elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s - elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c - return elbow_x, elbow_y - def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]: - wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s - wrist_y = self.wrist_y + x * self.wrist_s + y * self.wrist_c - return wrist_x, wrist_y - - def _mask_elbow(self) -> list[Tuple[float, float]]: """ Polygon shape to mask out parts above the elbow @@ -459,12 +439,12 @@ class WingProfile(Model): ] h = self.elbow_height / 2 tags_elbow = [ - ("elbow_bot", Cq.Location.from2d( - *self.elbow_to_abs(-self.elbow_joint.parent_arm_radius, h - self.elbow_h2), - self.elbow_angle + 0)), - ("elbow_top", Cq.Location.from2d( - *self.elbow_to_abs(-self.elbow_joint.parent_arm_radius, h + self.elbow_h2), - self.elbow_angle + 0)), + ("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d( + -self.elbow_joint.parent_arm_radius, + h - self.elbow_h2)), + ("elbow_top", self.elbow_bot_loc * Cq.Location.from2d( + -self.elbow_joint.parent_arm_radius, + h + self.elbow_h2)), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow @@ -527,21 +507,23 @@ class WingProfile(Model): def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 tags_elbow = [ - ("elbow_bot", Cq.Location.from2d( - *self.elbow_to_abs(self.elbow_joint.child_arm_radius, h - self.elbow_h2), - self.elbow_angle + 180)), - ("elbow_top", Cq.Location.from2d( - *self.elbow_to_abs(self.elbow_joint.child_arm_radius, h + self.elbow_h2), - self.elbow_angle + 180)), + ("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d( + self.elbow_joint.child_arm_radius, + h - self.elbow_h2, + 180)), + ("elbow_top", self.elbow_bot_loc * Cq.Location.from2d( + self.elbow_joint.child_arm_radius, + h + self.elbow_h2, + 180)), ] h = self.wrist_height / 2 tags_wrist = [ - ("wrist_bot", Cq.Location.from2d( - *self.wrist_to_abs(-self.wrist_joint.parent_arm_radius, h - self.wrist_h2), - self.wrist_angle)), - ("wrist_top", Cq.Location.from2d( - *self.wrist_to_abs(-self.wrist_joint.parent_arm_radius, h + self.wrist_h2), - self.wrist_angle)), + ("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d( + -self.wrist_joint.parent_arm_radius, + h - self.wrist_h2)), + ("wrist_top", self.wrist_bot_loc * Cq.Location.from2d( + -self.wrist_joint.parent_arm_radius, + h + self.wrist_h2)), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist @@ -597,12 +579,14 @@ class WingProfile(Model): front: bool = True) -> Cq.Workplane: h = self.wrist_height / 2 tags = [ - ("wrist_bot", Cq.Location.from2d( - *self.wrist_to_abs(self.wrist_joint.child_arm_radius, h - self.wrist_h2), - self.wrist_angle + 180)), - ("wrist_top", Cq.Location.from2d( - *self.wrist_to_abs(self.wrist_joint.child_arm_radius, h + self.wrist_h2), - self.wrist_angle + 180)), + ("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d( + self.wrist_joint.child_arm_radius, + h - self.wrist_h2, + 180)), + ("wrist_top", self.wrist_bot_loc * Cq.Location.from2d( + self.wrist_joint.child_arm_radius, + h + self.wrist_h2, + 180)), ] profile = self.profile_s3() return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @@ -726,45 +710,34 @@ class WingR(WingProfile): Right side wings """ + elbow_bot_loc: Cq.Location = Cq.Location.from2d(285.0, 5.0, 25.0) elbow_height: float = 111.0 - elbow_x: float = 285.0 - elbow_y: float = 5.0 - # Tilt of elbow w.r.t. shoulder - elbow_angle: float = 25.0 + wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 253.0, 40.0) wrist_height: float = 60.0 - # Bottom point of the wrist - wrist_x: float = 403.0 - wrist_y: float = 253.0 - # Tile of wrist w.r.t. shoulder - wrist_angle: float = 40 # Extends from the wrist to the tip of the arrow arrow_height: float = 300 - arrow_angle: float = 8 + arrow_angle: float = -8 # Relative (in wrist coordinate) centre of the ring - ring_x: float = 45 - ring_y: float = 25 - ring_radius_inner: float = 22 + ring_rel_loc: Cq.Location = Cq.Location.from2d(45.0, 25.0) + ring_radius_inner: float = 22.0 def __post_init__(self): super().__post_init__() - self.arrow_theta = math.radians(self.arrow_angle) - self.arrow_x, self.arrow_y = self.wrist_to_abs(0, -self.arrow_height) - self.arrow_tip_x = self.arrow_x + (self.arrow_height + self.wrist_height) \ - * math.sin(self.arrow_theta - self.wrist_theta) - self.arrow_tip_y = self.arrow_y + (self.arrow_height + self.wrist_height) \ - * math.cos(self.arrow_theta - self.wrist_theta) - # [[c, s], [-s, c]] * [ring_x, ring_y] - self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y - self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y + assert self.arrow_angle < 0, "Arrow angle cannot be positive" + self.arrow_bot_loc = self.wrist_bot_loc \ + * Cq.Location.from2d(0, -self.arrow_height) + self.arrow_other_loc = self.arrow_bot_loc \ + * Cq.Location.rot2d(self.arrow_angle) \ + * Cq.Location.from2d(0, self.arrow_height + self.wrist_height) + self.ring_loc = self.wrist_top_loc * self.ring_rel_loc assert self.ring_radius > self.ring_radius_inner @property def ring_radius(self) -> float: - dx = self.ring_x - dy = self.ring_y + (dx, dy), _ = self.ring_rel_loc.to2d() return (dx * dx + dy * dy) ** 0.5 def profile(self) -> Cq.Sketch: @@ -779,8 +752,8 @@ class WingR(WingProfile): tag="shoulder") .spline([ (0, self.shoulder_joint.height), - (self.elbow_top_x, self.elbow_top_y), - (self.wrist_top_x, self.wrist_top_y), + self.elbow_top_loc.to2d_pos(), + self.wrist_top_loc.to2d_pos(), ], tag="s1_top") #.segment( @@ -789,31 +762,31 @@ class WingR(WingProfile): # tag="wrist") .spline([ (0, 0), - (self.elbow_x, self.elbow_y), - (self.wrist_x, self.wrist_y), + self.elbow_bot_loc.to2d_pos(), + self.wrist_bot_loc.to2d_pos(), ], tag="s1_bot") ) result = ( result .segment( - (self.wrist_x, self.wrist_y), - (self.arrow_x, self.arrow_y) + self.wrist_bot_loc.to2d_pos(), + self.arrow_bot_loc.to2d_pos(), ) .segment( - (self.arrow_x, self.arrow_y), - (self.arrow_tip_x, self.arrow_tip_y) + self.arrow_bot_loc.to2d_pos(), + self.arrow_other_loc.to2d_pos(), ) .segment( - (self.arrow_tip_x, self.arrow_tip_y), - (self.wrist_top_x, self.wrist_top_y) + self.arrow_other_loc.to2d_pos(), + self.wrist_top_loc.to2d_pos(), ) ) # Carve out the ring result = result.assemble() result = ( result - .push([(self.ring_abs_x, self.ring_abs_y)]) + .push([self.ring_loc.to2d_pos()]) .circle(self.ring_radius, mode='a') .circle(self.ring_radius_inner, mode='s') .clean() @@ -822,38 +795,39 @@ class WingR(WingProfile): def _mask_elbow(self) -> list[Tuple[float, float]]: l = 200 + elbow_x, _ = self.elbow_bot_loc.to2d_pos() + elbow_top_x, _ = self.elbow_top_loc.to2d_pos() return [ (0, -l), - (self.elbow_x, -l), - (self.elbow_x, self.elbow_y), - (self.elbow_top_x, self.elbow_top_y), - (self.elbow_top_x, l), + (elbow_x, -l), + self.elbow_bot_loc.to2d_pos(), + self.elbow_top_loc.to2d_pos(), + (elbow_top_x, l), (0, l) ] def _mask_wrist(self) -> list[Tuple[float, float]]: l = 200 + wrist_x, _ = self.wrist_bot_loc.to2d_pos() + _, wrist_top_y = self.wrist_top_loc.to2d_pos() return [ (0, -l), - (self.wrist_x, -l), - (self.wrist_x, self.wrist_y), - (self.wrist_top_x, self.wrist_top_y), + (wrist_x, -l), + self.wrist_bot_loc.to2d_pos(), + self.wrist_top_loc.to2d_pos(), #(self.wrist_top_x, self.wrist_top_y), - (0, self.wrist_top_y), + (0, wrist_top_y), ] @dataclass(kw_only=True) class WingL(WingProfile): - elbow_x: float = 230.0 - elbow_y: float = 110.0 - elbow_angle: float = -10.0 + elbow_bot_loc: Cq.Location = Cq.Location.from2d(230.0, 110.0, -10.0) elbow_height: float = 80.0 - wrist_x: float = 480.0 - wrist_y: float = 0.0 - wrist_angle: float = -45 + wrist_angle: float = -45.0 + wrist_bot_loc: Cq.Location = Cq.Location.from2d(480.0, 0.0, -45.0) wrist_height: float = 43.0 shoulder_bezier_ext: float = 80.0 @@ -866,11 +840,14 @@ class WingL(WingProfile): flip: bool = True def __post_init__(self): - super().__post_init__() assert self.wrist_height <= self.shoulder_joint.height + self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) + + super().__post_init__() def arrow_to_abs(self, x, y) -> Tuple[float, float]: - return self.wrist_to_abs(x * self.arrow_length, y * self.arrow_height / 2 + self.wrist_height / 2) + rel = Cq.Location.from2d(x * self.arrow_length, y * self.arrow_height / 2 + self.wrist_height / 2) + return (self.wrist_bot_loc * rel).to2d_pos() def profile(self) -> Cq.Sketch: result = ( @@ -879,39 +856,29 @@ class WingL(WingProfile): (0,0), (0, self.shoulder_height) ) - #.spline([ - # (0, 0), - # self.elbow_to_abs(0, 0), - # self.wrist_to_abs(0, 0), - #]) - #.spline([ - # (0, self.shoulder_height), - # self.elbow_to_abs(0, self.elbow_height), - # self.wrist_to_abs(0, self.wrist_height), - #]) .bezier([ (0, 0), (self.shoulder_bezier_ext, 0), - self.elbow_to_abs(-self.elbow_bezier_ext, 0), - self.elbow_to_abs(0, 0), + (self.elbow_bot_loc * Cq.Location.from2d(-self.elbow_bezier_ext, 0)).to2d_pos(), + self.elbow_bot_loc.to2d_pos(), ]) .bezier([ (0, self.shoulder_joint.height), (self.shoulder_bezier_ext, self.shoulder_joint.height), - self.elbow_to_abs(-self.elbow_bezier_ext, self.elbow_height), - self.elbow_to_abs(0, self.elbow_height), + (self.elbow_top_loc * Cq.Location.from2d(-self.elbow_bezier_ext, 0)).to2d_pos(), + self.elbow_top_loc.to2d_pos(), ]) .bezier([ - self.elbow_to_abs(0, 0), - self.elbow_to_abs(self.elbow_bezier_ext, 0), - self.wrist_to_abs(-self.wrist_bezier_ext, 0), - self.wrist_to_abs(0, 0), + self.elbow_bot_loc.to2d_pos(), + (self.elbow_bot_loc * Cq.Location.from2d(self.elbow_bezier_ext, 0)).to2d_pos(), + (self.wrist_bot_loc * Cq.Location.from2d(-self.wrist_bezier_ext, 0)).to2d_pos(), + self.wrist_bot_loc.to2d_pos(), ]) .bezier([ - self.elbow_to_abs(0, self.elbow_height), - self.elbow_to_abs(self.elbow_bezier_ext, self.elbow_height), - self.wrist_to_abs(-self.wrist_bezier_ext, self.wrist_height), - self.wrist_to_abs(0, self.wrist_height), + self.elbow_top_loc.to2d_pos(), + (self.elbow_top_loc * Cq.Location.from2d(self.elbow_bezier_ext, 0)).to2d_pos(), + (self.wrist_top_loc * Cq.Location.from2d(-self.wrist_bezier_ext, 0)).to2d_pos(), + self.wrist_top_loc.to2d_pos(), ]) ) # arrow base positions @@ -919,13 +886,13 @@ class WingL(WingProfile): result = ( result .bezier([ - self.wrist_to_abs(0, self.wrist_height), - self.wrist_to_abs(self.wrist_bezier_ext, self.wrist_height), + self.wrist_top_loc.to2d_pos(), + (self.wrist_top_loc * Cq.Location.from2d(self.wrist_bezier_ext, 0)).to2d_pos(), self.arrow_to_abs(base_u, base_v), ]) .bezier([ - self.wrist_to_abs(0, 0), - self.wrist_to_abs(self.wrist_bezier_ext, 0), + self.wrist_bot_loc.to2d_pos(), + (self.wrist_bot_loc * Cq.Location.from2d(self.wrist_bezier_ext, 0)).to2d_pos(), self.arrow_to_abs(base_u, -base_v), ]) ) @@ -954,22 +921,27 @@ class WingL(WingProfile): def _mask_elbow(self) -> list[Tuple[float, float]]: l = 200 + elbow_bot_x, _ = self.elbow_bot_loc.to2d_pos() + elbow_top_x, _ = self.elbow_top_loc.to2d_pos() return [ (0, -l), - (self.elbow_x, -l), - (self.elbow_x, self.elbow_y), - (self.elbow_top_x, self.elbow_top_y), - (self.elbow_top_x, l), + (elbow_bot_x, -l), + self.elbow_bot_loc.to2d_pos(), + self.elbow_top_loc.to2d_pos(), + (elbow_top_x, l), (0, l) ] def _mask_wrist(self) -> list[Tuple[float, float]]: l = 200 + elbow_bot_x, _ = self.elbow_bot_loc.to2d_pos() + _, elbow_top_y = self.elbow_top_loc.to2d_pos() + _, wrist_bot_y = self.wrist_bot_loc.to2d_pos() return [ (0, -l), - (self.elbow_x, self.wrist_y), - (self.wrist_x, self.wrist_y), - (self.wrist_top_x, self.wrist_top_y), - (self.elbow_x, self.elbow_top_y + l), + (elbow_bot_x, wrist_bot_y), + self.wrist_bot_loc.to2d_pos(), + self.wrist_top_loc.to2d_pos(), + (elbow_bot_x, elbow_top_y + l), (0, l), ] diff --git a/nhf/utils.py b/nhf/utils.py index db8c73d..be3912c 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -43,6 +43,10 @@ def from2d(x: float, y: float, rotate: float=0.0) -> Cq.Location: return Cq.Location((x, y, 0), (0, 0, 1), rotate) Cq.Location.from2d = from2d +def rot2d(angle: float) -> Cq.Location: + return Cq.Location((0, 0, 0), (0, 0, 1), angle) +Cq.Location.rot2d = rot2d + def is2d(self: Cq.Location) -> bool: (_, _, z), (rx, ry, _) = self.toTuple() return z == 0 and rx == 0 and ry == 0 @@ -59,6 +63,25 @@ def to2d(self: Cq.Location) -> Tuple[Tuple[float, float], float]: return (x, y), rz Cq.Location.to2d = to2d +def to2d_pos(self: Cq.Location) -> Tuple[float, float]: + """ + Returns position and angle + """ + (x, y, z), (rx, ry, _) = self.toTuple() + assert z == 0 + assert rx == 0 + assert ry == 0 + return (x, y) +Cq.Location.to2d_pos = to2d_pos + +def with_angle_2d(self: Cq.Location, angle: float) -> Tuple[float, float]: + """ + Returns position and angle + """ + x, y = self.to2d_pos() + return Cq.Location.from2d(x, y, angle) +Cq.Location.with_angle_2d = with_angle_2d + ### Tags def tagPoint(self, tag: str): -- 2.44.1 From e73c6c0fedc2912bbfcad31422bef3c377c7626f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 17 Jul 2024 21:37:08 -0700 Subject: [PATCH 095/187] feat: Reduce the number of slots on shoulder Previously every shoulder joint was the same with two slots that specify the neutral position. Experiment reveals this to be too fragile. --- nhf/parts/joints.py | 6 +++++- nhf/touhou/houjuu_nue/__init__.py | 8 +++++--- nhf/touhou/houjuu_nue/joints.py | 4 ++-- nhf/touhou/houjuu_nue/test.py | 10 ++++++++++ nhf/touhou/houjuu_nue/wing.py | 9 +++++---- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 20fd507..8602147 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -219,6 +219,8 @@ class TorsionJoint: assert self.groove_radius_inner > self.spring.radius > self.radius_axle assert self.spring.height > self.groove_depth, "Groove is too deep" assert self.groove_depth < self.spring.height - self.spring.thickness * 2 + if self.rider_n_slots == 1: + assert self.rider_slot_span == 0.0, "Non-zero span is impossible with multiple riders" @property def total_height(self): @@ -373,7 +375,9 @@ class TorsionJoint: ) theta_begin = -math.radians(rider_slot_begin) theta_span = math.radians(self.rider_slot_span) - if abs(math.remainder(self.rider_slot_span, 360)) < TOL: + if self.rider_n_slots <= 1: + theta_step = 0 + elif abs(math.remainder(self.rider_slot_span, 360)) < TOL: theta_step = theta_span / self.rider_n_slots else: theta_step = theta_span / (self.rider_n_slots - 1) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 13df365..f0c996a 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -50,14 +50,14 @@ class Parameters(Model): wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR( name="r1", - shoulder_joint=MJ.ShoulderJoint(directrix_id=1), + shoulder_angle_bias = 15.0, )) wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR( name="r2", )) wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR( name="r3", - shoulder_joint=MJ.ShoulderJoint(directrix_id=1), + shoulder_angle_bias = 15.0, )) wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL( name="l1", @@ -66,7 +66,7 @@ class Parameters(Model): wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( name="l2", wrist_angle=-30.0, - shoulder_joint=MJ.ShoulderJoint(directrix_id=1), + shoulder_angle_bias = 15.0, )) wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL( name="l3", @@ -84,6 +84,8 @@ class Parameters(Model): self.wing_l2.base_joint = self.harness.hs_hirth_joint self.wing_l3.base_joint = self.harness.hs_hirth_joint + self.wing_r1.shoulder_joint.torsion_joint + assert self.wing_r1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam assert self.wing_r2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam assert self.wing_r3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 45bce76..5fa9a6d 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -32,8 +32,8 @@ class ShoulderJoint(Model): height=7.5, ), rider_slot_begin=0, - rider_n_slots=2, - rider_slot_span=15, + rider_n_slots=1, + rider_slot_span=0, )) # On the parent side, drill vertical holes diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 01fcc74..9dbb41b 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -10,11 +10,21 @@ class TestJoints(unittest.TestCase): j = MJ.ShoulderJoint() assembly = j.torsion_joint.rider_track_assembly() self.assertEqual(pairwise_intersection(assembly), []) + def test_shoulder_collision_0(self): j = MJ.ShoulderJoint() assembly = j.assembly() self.assertEqual(pairwise_intersection(assembly), []) + def test_shoulder_align(self): + j = MJ.ShoulderJoint() + a = j.assembly() + l_t_c0 = a.get_abs_location("parent_top/lip?conn0") + l_b_c0 = a.get_abs_location("parent_bot/lip?conn0") + v = l_t_c0 - l_b_c0 + self.assertAlmostEqual(v.x, 0) + self.assertAlmostEqual(v.y, 0) + def test_shoulder_joint_dist(self): """ Tests the arm radius diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b43b802..e122c58 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -39,6 +39,7 @@ class WingProfile(Model): shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( )) + shoulder_angle_bias: float = 0.0 shoulder_width: float = 36.0 shoulder_tip_x: float = -200.0 shoulder_tip_y: float = 160.0 @@ -94,7 +95,7 @@ class WingProfile(Model): self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) - self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral + self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: @@ -287,14 +288,14 @@ class WingProfile(Model): sw = self.shoulder_width axle_dist = self.shoulder_joint.parent_lip_ext - theta = math.radians(self.shoulder_joint.angle_neutral) - c, s = math.cos(-theta), math.sin(-theta) + theta = math.radians(self.shoulder_angle_neutral) + c, s = math.cos(theta), math.sin(theta) tags = [ # transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2) ("shoulder", Cq.Location.from2d( self.shoulder_tip_x + axle_dist * c + (-sw/2) * s, self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c, - self.shoulder_joint.angle_neutral)), + -self.shoulder_angle_neutral)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ] result = extrude_with_markers( -- 2.44.1 From 9795f7b7144337a556d61b131809ba547e3d60fc Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 18 Jul 2024 11:08:34 -0700 Subject: [PATCH 096/187] fix: Wing s1 tangent to shoulder --- nhf/touhou/houjuu_nue/__init__.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index f0c996a..aa327fb 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -131,7 +131,7 @@ class Parameters(Model): .add(self.wing_l2.assembly(parts), name="wing_l2") .add(self.wing_l3.assembly(parts), name="wing_l3") ) - for tag, offset in [("r1", 10), ("r2", 8), ("r3", 6), ("l1", 6), ("l2", 7), ("l3", 8)]: + for tag, offset in [("r1", 9), ("r2", 7), ("r3", 6), ("l1", 7), ("l2", 8), ("l3", 9)]: self.harness.hs_hirth_joint.add_constraints( result, f"harness/{tag}", diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index e122c58..587fd51 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -711,10 +711,10 @@ class WingR(WingProfile): Right side wings """ - elbow_bot_loc: Cq.Location = Cq.Location.from2d(285.0, 5.0, 25.0) + elbow_bot_loc: Cq.Location = Cq.Location.from2d(290.0, 30.0, 27.0) elbow_height: float = 111.0 - wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 253.0, 40.0) + wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 289.0, 45.0) wrist_height: float = 60.0 # Extends from the wrist to the tip of the arrow @@ -824,15 +824,16 @@ class WingR(WingProfile): @dataclass(kw_only=True) class WingL(WingProfile): - elbow_bot_loc: Cq.Location = Cq.Location.from2d(230.0, 110.0, -10.0) + elbow_bot_loc: Cq.Location = Cq.Location.from2d(250.0, 110.0, 10.0) elbow_height: float = 80.0 wrist_angle: float = -45.0 wrist_bot_loc: Cq.Location = Cq.Location.from2d(480.0, 0.0, -45.0) wrist_height: float = 43.0 - shoulder_bezier_ext: float = 80.0 - elbow_bezier_ext: float = 100.0 + shoulder_bezier_ext: float = 120.0 + shoulder_bezier_drop: float = 15.0 + elbow_bezier_ext: float = 80.0 wrist_bezier_ext: float = 30.0 arrow_length: float = 135.0 @@ -859,7 +860,7 @@ class WingL(WingProfile): ) .bezier([ (0, 0), - (self.shoulder_bezier_ext, 0), + (self.shoulder_bezier_ext, -self.shoulder_bezier_drop), (self.elbow_bot_loc * Cq.Location.from2d(-self.elbow_bezier_ext, 0)).to2d_pos(), self.elbow_bot_loc.to2d_pos(), ]) -- 2.44.1 From 4c5985fa08a6b18da62bab37952af9182402ab74 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 18 Jul 2024 14:03:01 -0700 Subject: [PATCH 097/187] feat: Bent elbow joint --- nhf/touhou/houjuu_nue/joints.py | 61 ++++++++++++++++++++++++--------- nhf/touhou/houjuu_nue/wing.py | 56 +++++++++++++++--------------- nhf/utils.py | 9 +++++ 3 files changed, 80 insertions(+), 46 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 5fa9a6d..78cd819 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -313,6 +313,9 @@ class Beam: class DiskJoint(Model): """ Sandwiched disk joint for the wrist and elbow + + We embed a spring inside the joint, with one leg in the disk and one leg in + the housing. This provides torsion resistance. """ spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( radius=9 / 2, @@ -328,14 +331,15 @@ class DiskJoint(Model): housing_thickness: float = 4.0 disk_thickness: float = 7.0 - # Gap between disk and the housing - #disk_thickness_gap: float = 0.1 + + # Amount by which the wall carves in + wall_inset: float = 2.0 + # Height of the spring hole; if you make it too short the spring can't enter + spring_tail_hole_height: float = 2.0 # Spring angle at 0 degrees of movement - spring_angle_at_0: float = 60.0 - spring_slot_offset: float = 15.0 - - wall_inset: float = 2.0 + spring_angle_at_0: float = 90.0 + spring_slot_offset: float = 5.0 # Angular span of movement movement_angle: float = 120.0 @@ -348,10 +352,11 @@ class DiskJoint(Model): def __post_init__(self): super().__init__(name="disk-joint") - assert self.housing_thickness > self.wall_inset - assert self.radius_housing > self.radius_disk - assert self.radius_disk > self.radius_axle + assert self.radius_housing > self.radius_disk > self.radius_axle + assert self.spring.height < self.housing_thickness + self.disk_thickness + assert self.housing_upper_carve_offset > 0 + assert self.spring_tail_hole_height > self.spring.thickness @property def neutral_movement_angle(self) -> Optional[float]: @@ -363,6 +368,12 @@ class DiskJoint(Model): @property def total_thickness(self) -> float: return self.housing_thickness * 2 + self.disk_thickness + @property + def disk_bot_thickness(self) -> float: + """ + Pads the bottom of the disk up to spring height + """ + return max(0, self.disk_thickness + self.spring.thickness - self.spring.height) @property def opening_span(self) -> float: @@ -373,7 +384,7 @@ class DiskJoint(Model): """ Distance between the spring track and the outside of the upper housing """ - return self.housing_thickness + self.disk_thickness - self.spring.height + return self.spring_tail_hole_height + (self.disk_thickness - self.disk_bot_thickness) - self.spring.height @property def housing_upper_dz(self) -> float: @@ -387,9 +398,9 @@ class DiskJoint(Model): Cq.Solid.makeBox( length=self.spring.tail_length, width=self.spring.thickness, - height=self.disk_thickness, + height=self.spring.height-self.disk_bot_thickness, ) - .located(Cq.Location((0, self.spring.radius_inner, 0))) + .located(Cq.Location((0, self.spring.radius_inner, self.disk_bot_thickness))) .rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset) ) @@ -427,6 +438,7 @@ class DiskJoint(Model): theta = math.radians(self.spring_slot_offset) plane.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0)) plane.workplane(offset=self.disk_thickness).tagPlane("mate_top") + plane.workplane(offset=self.disk_bot_thickness).tagPlane("mate_spring") result.copyWorkplane(Cq.Workplane('YX')).tagPlane("mate_bot") return result @@ -474,11 +486,11 @@ class DiskJoint(Model): carve = ( Cq.Solid.makeCylinder( radius=self.spring.radius, - height=self.housing_thickness + height=self.spring_tail_hole_height, ).fuse(Cq.Solid.makeBox( length=self.spring.tail_length, width=self.spring.thickness, - height=self.housing_thickness + height=self.spring_tail_hole_height, ).located(Cq.Location((0, -self.spring.radius, 0)))) ).rotate((0, 0, 0), (0, 0, 1), carve_angle) result = ( @@ -515,6 +527,7 @@ class DiskJoint(Model): result = ( result .union(wall, tol=TOL) + #.cut(carve) .cut(carve.located(Cq.Location((0, 0, -self.housing_upper_carve_offset)))) ) return result.clean() @@ -541,7 +554,7 @@ class DiskJoint(Model): .constrain(f"{housing_lower}?dirX", f"{housing_upper}?dirX", "Axis", param=0) .constrain(f"{housing_upper}?dir", f"{spring_name}?dir_top", "Axis", param=0) .constrain(f"{spring_name}?dir_bot", f"{disk}?dir", "Axis", param=0) - .constrain(f"{disk}?mate_bot", f"{spring_name}?bot", "Plane", param=0) + .constrain(f"{disk}?mate_spring", f"{spring_name}?bot", "Plane") #.constrain(f"{housing_lower}?dirX", f"{housing_upper}?dir", "Axis", param=0) #.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle) #.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90) @@ -608,13 +621,26 @@ class ElbowJoint(Model): material: Material = Material.RESIN_TRANSPERENT - angle_neutral: float = 0.0 + angle_neutral: float = 30.0 def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 + def parent_arm_loc(self) -> Cq.Location: + """ + 2d Location of the centre of the arm surface on the parent side, assuming + axle is at position 0, and parent direction is -X + """ + return Cq.Location.from2d(-self.parent_arm_radius, 0, 0) + def child_arm_loc(self) -> Cq.Location: + """ + 2d Location of the centre of the arm surface on the child side, assuming + axle is at position 0, and parent direction is -X + """ + return Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.child_arm_radius, 0, 180) + def lip(self) -> Cq.Workplane: holes = [ h @@ -649,8 +675,9 @@ class ElbowJoint(Model): Cq.Location((0, 0, 0), (0, 1, 0), 90) ) loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle) + loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness)) disk_cut = self.disk_joint._disk_cut().located( - loc_lip.inverse * Cq.Location((0, self.disk_joint.spring.radius_inner, 0)) * loc_disk) + loc_lip.inverse * loc_cut_rel * loc_disk) result = ( Cq.Assembly() .add(self.lip().cut(disk_cut), name="lip", loc=loc_lip) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 587fd51..9df38e6 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -53,6 +53,7 @@ class WingProfile(Model): movement_angle=55, ), hole_diam=6.0, + angle_neutral=15.0, )) # Distance between the two spacers on the elbow, halved elbow_h2: float = 5.0 @@ -70,6 +71,7 @@ class WingProfile(Model): child_arm_radius=23.0, parent_arm_radius=30.0, hole_diam=4.0, + angle_neutral=30.0, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -86,6 +88,8 @@ class WingProfile(Model): elbow_height: float wrist_bot_loc: Cq.Location wrist_height: float + elbow_rotate: float = -5 + wrist_rotate: float = 30.0 flip: bool = False @@ -94,6 +98,8 @@ class WingProfile(Model): self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) + self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2) + self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias @@ -439,13 +445,12 @@ class WingProfile(Model): ("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)), ] h = self.elbow_height / 2 + loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.parent_arm_loc() tags_elbow = [ - ("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d( - -self.elbow_joint.parent_arm_radius, - h - self.elbow_h2)), - ("elbow_top", self.elbow_bot_loc * Cq.Location.from2d( - -self.elbow_joint.parent_arm_radius, - h + self.elbow_h2)), + ("elbow_bot", self.elbow_axle_loc * loc_elbow *\ + Cq.Location.from2d(0, -self.elbow_h2)), + ("elbow_top", self.elbow_axle_loc * loc_elbow *\ + Cq.Location.from2d(0, self.elbow_h2)), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow @@ -507,24 +512,20 @@ class WingProfile(Model): return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 + loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc() tags_elbow = [ - ("elbow_bot", self.elbow_bot_loc * Cq.Location.from2d( - self.elbow_joint.child_arm_radius, - h - self.elbow_h2, - 180)), - ("elbow_top", self.elbow_bot_loc * Cq.Location.from2d( - self.elbow_joint.child_arm_radius, - h + self.elbow_h2, - 180)), + ("elbow_bot", self.elbow_axle_loc * loc_elbow *\ + Cq.Location.from2d(0, self.elbow_h2)), + ("elbow_top", self.elbow_axle_loc * loc_elbow *\ + Cq.Location.from2d(0, -self.elbow_h2)), ] h = self.wrist_height / 2 + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc().flip_y() tags_wrist = [ - ("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d( - -self.wrist_joint.parent_arm_radius, - h - self.wrist_h2)), - ("wrist_top", self.wrist_bot_loc * Cq.Location.from2d( - -self.wrist_joint.parent_arm_radius, - h + self.wrist_h2)), + ("wrist_bot", self.wrist_axle_loc * loc_wrist *\ + Cq.Location.from2d(0, -self.wrist_h2)), + ("wrist_top", self.wrist_axle_loc * loc_wrist *\ + Cq.Location.from2d(0, self.wrist_h2)), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist @@ -564,7 +565,7 @@ class WingProfile(Model): o.generate(), point_tag=t, flipped=is_top == is_parent, - #rotate=True, + #rotate=not is_parent, ) return result.solve() @@ -579,15 +580,12 @@ class WingProfile(Model): def surface_s3(self, front: bool = True) -> Cq.Workplane: h = self.wrist_height / 2 + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc().flip_y() tags = [ - ("wrist_bot", self.wrist_bot_loc * Cq.Location.from2d( - self.wrist_joint.child_arm_radius, - h - self.wrist_h2, - 180)), - ("wrist_top", self.wrist_bot_loc * Cq.Location.from2d( - self.wrist_joint.child_arm_radius, - h + self.wrist_h2, - 180)), + ("wrist_bot", self.wrist_axle_loc * loc_wrist *\ + Cq.Location.from2d(0, self.wrist_h2)), + ("wrist_top", self.wrist_axle_loc * loc_wrist *\ + Cq.Location.from2d(0, -self.wrist_h2)), ] profile = self.profile_s3() return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) diff --git a/nhf/utils.py b/nhf/utils.py index be3912c..7cd0d42 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -82,6 +82,15 @@ def with_angle_2d(self: Cq.Location, angle: float) -> Tuple[float, float]: return Cq.Location.from2d(x, y, angle) Cq.Location.with_angle_2d = with_angle_2d +def flip_x(self: Cq.Location) -> Cq.Location: + (x, y), a = self.to2d() + return Cq.Location.from2d(-x, y, 90 - a) +Cq.Location.flip_x = flip_x +def flip_y(self: Cq.Location) -> Cq.Location: + (x, y), a = self.to2d() + return Cq.Location.from2d(x, -y, -a) +Cq.Location.flip_y = flip_y + ### Tags def tagPoint(self, tag: str): -- 2.44.1 From 052575017a6cdc7b0a65df993af5e17398d5fbfd Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 18 Jul 2024 14:09:53 -0700 Subject: [PATCH 098/187] feat: Rotated wrist joint on left side --- nhf/touhou/houjuu_nue/wing.py | 6 +++++- nhf/utils.py | 16 +++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 9df38e6..a1d5d37 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -88,7 +88,7 @@ class WingProfile(Model): elbow_height: float wrist_bot_loc: Cq.Location wrist_height: float - elbow_rotate: float = -5 + elbow_rotate: float = -5.0 wrist_rotate: float = 30.0 flip: bool = False @@ -842,6 +842,10 @@ class WingL(WingProfile): def __post_init__(self): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) + self.elbow_joint.angle_neutral = -15.0 + self.elbow_rotate = 5.0 + self.wrist_joint.angle_neutral = -self.wrist_bot_loc.to2d_rot() - 30.0 + self.wrist_rotate = self.wrist_joint.angle_neutral super().__post_init__() diff --git a/nhf/utils.py b/nhf/utils.py index 7cd0d42..f542632 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -67,13 +67,19 @@ def to2d_pos(self: Cq.Location) -> Tuple[float, float]: """ Returns position and angle """ - (x, y, z), (rx, ry, _) = self.toTuple() - assert z == 0 - assert rx == 0 - assert ry == 0 - return (x, y) + (x, y), _ = self.to2d() + return x, y Cq.Location.to2d_pos = to2d_pos +def to2d_rot(self: Cq.Location) -> Tuple[float, float]: + """ + Returns position and angle + """ + _, r = self.to2d() + return r +Cq.Location.to2d_rot = to2d_rot + + def with_angle_2d(self: Cq.Location, angle: float) -> Tuple[float, float]: """ Returns position and angle -- 2.44.1 From 0ed1a1a5a40f22ce3bf1ad603e3ac0b517c2d7c9 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 18 Jul 2024 14:41:29 -0700 Subject: [PATCH 099/187] feat: Add deflection parameter to assembly --- nhf/touhou/houjuu_nue/__init__.py | 16 +++++++++------- nhf/touhou/houjuu_nue/wing.py | 11 +++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index aa327fb..f2e779b 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -117,19 +117,21 @@ class Parameters(Model): return self.wing_l3 @assembly() - def wings_harness_assembly(self, parts: Optional[list[str]] = None) -> Cq.Assembly: + def wings_harness_assembly(self, + parts: Optional[list[str]] = None, + **kwargs) -> Cq.Assembly: """ Assembly of harness with all the wings """ 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_r2.assembly(parts), name="wing_r2") - .add(self.wing_r3.assembly(parts), name="wing_r3") - .add(self.wing_l1.assembly(parts), name="wing_l1") - .add(self.wing_l2.assembly(parts), name="wing_l2") - .add(self.wing_l3.assembly(parts), name="wing_l3") + .add(self.wing_r1.assembly(parts, **kwargs), name="wing_r1") + .add(self.wing_r2.assembly(parts, **kwargs), name="wing_r2") + .add(self.wing_r3.assembly(parts, **kwargs), name="wing_r3") + .add(self.wing_l1.assembly(parts, **kwargs), name="wing_l1") + .add(self.wing_l2.assembly(parts, **kwargs), name="wing_l2") + .add(self.wing_l3.assembly(parts, **kwargs), name="wing_l3") ) for tag, offset in [("r1", 9), ("r2", 7), ("r3", 6), ("l1", 7), ("l2", 8), ("l3", 9)]: self.harness.hs_hirth_joint.add_constraints( diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index a1d5d37..2c70b57 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -622,7 +622,8 @@ class WingProfile(Model): @assembly() def assembly(self, parts: Optional[list[str]] = None, - angle_elbow_wrist: float = 0.0, + shoulder_deflection: float = 0.0, + elbow_wrist_deflection: float = 0.0, ) -> Cq.Assembly(): if parts is None: parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] @@ -636,7 +637,7 @@ class WingProfile(Model): if "s0" in parts: result.add(self.assembly_s0(), name="s0") if "shoulder" in parts: - result.add(self.shoulder_joint.assembly(), name="shoulder") + result.add(self.shoulder_joint.assembly(deflection=shoulder_deflection * 80), name="shoulder") if "s0" in parts and "shoulder" in parts: ( result @@ -656,7 +657,8 @@ class WingProfile(Model): .constrain("s1/shoulder_bot?conn1", f"shoulder/child/lip_{tag_bot}?conn1", "Plane") ) if "elbow" in parts: - result.add(self.elbow_joint.assembly(angle=angle_elbow_wrist), name="elbow") + angle = self.elbow_joint.disk_joint.movement_angle * elbow_wrist_deflection + result.add(self.elbow_joint.assembly(angle=angle), name="elbow") if "s1" in parts and "elbow" in parts: ( result @@ -676,7 +678,8 @@ class WingProfile(Model): .constrain("s2/elbow_bot?conn1", "elbow/child/lip?conn_bot1", "Plane") ) if "wrist" in parts: - result.add(self.wrist_joint.assembly(angle=angle_elbow_wrist), name="wrist") + angle = self.wrist_joint.disk_joint.movement_angle * elbow_wrist_deflection + result.add(self.wrist_joint.assembly(angle=elbow_wrist_deflection), name="wrist") if "s2" in parts and "wrist" in parts: # Mounted backwards to bend in other direction ( -- 2.44.1 From 7e7b9e1f64a0481596719d7a84406d0794c58b7d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 18 Jul 2024 21:07:08 -0700 Subject: [PATCH 100/187] fix: Incorrect folding on left side and on wrist --- nhf/touhou/houjuu_nue/joints.py | 7 +++- nhf/touhou/houjuu_nue/wing.py | 65 +++++++++++++++++---------------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 78cd819..cb53ae1 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -634,12 +634,15 @@ class ElbowJoint(Model): axle is at position 0, and parent direction is -X """ return Cq.Location.from2d(-self.parent_arm_radius, 0, 0) - def child_arm_loc(self) -> Cq.Location: + def child_arm_loc(self, flip: bool = False) -> Cq.Location: """ 2d Location of the centre of the arm surface on the child side, assuming axle is at position 0, and parent direction is -X + + Set `flip=True` to indicate that the joint is supposed to be installed upside down """ - return Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.child_arm_radius, 0, 180) + result = Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.child_arm_radius, 0, 180) + return result.flip_y() if flip else result def lip(self) -> Cq.Workplane: holes = [ diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 2c70b57..b9e2014 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -62,7 +62,7 @@ class WingProfile(Model): wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( - movement_angle=45, + movement_angle=30, radius_disk=13.0, radius_housing=15.0, ), @@ -91,6 +91,7 @@ class WingProfile(Model): elbow_rotate: float = -5.0 wrist_rotate: float = 30.0 + # False for the right side, True for the left side flip: bool = False def __post_init__(self): @@ -447,9 +448,9 @@ class WingProfile(Model): h = self.elbow_height / 2 loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.parent_arm_loc() tags_elbow = [ - ("elbow_bot", self.elbow_axle_loc * loc_elbow *\ + ("elbow_bot", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, -self.elbow_h2)), - ("elbow_top", self.elbow_axle_loc * loc_elbow *\ + ("elbow_top", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, self.elbow_h2)), ] profile = self.profile_s1() @@ -512,19 +513,19 @@ class WingProfile(Model): return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 - loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc() + loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc(flip=self.flip) tags_elbow = [ - ("elbow_bot", self.elbow_axle_loc * loc_elbow *\ + ("elbow_bot", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, self.elbow_h2)), - ("elbow_top", self.elbow_axle_loc * loc_elbow *\ + ("elbow_top", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, -self.elbow_h2)), ] h = self.wrist_height / 2 - loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc().flip_y() + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc() tags_wrist = [ - ("wrist_bot", self.wrist_axle_loc * loc_wrist *\ + ("wrist_bot", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, -self.wrist_h2)), - ("wrist_top", self.wrist_axle_loc * loc_wrist *\ + ("wrist_top", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, self.wrist_h2)), ] profile = self.profile_s2() @@ -580,11 +581,11 @@ class WingProfile(Model): def surface_s3(self, front: bool = True) -> Cq.Workplane: h = self.wrist_height / 2 - loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc().flip_y() + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip) tags = [ - ("wrist_bot", self.wrist_axle_loc * loc_wrist *\ + ("wrist_bot", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, self.wrist_h2)), - ("wrist_top", self.wrist_axle_loc * loc_wrist *\ + ("wrist_top", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, -self.wrist_h2)), ] profile = self.profile_s3() @@ -662,42 +663,42 @@ class WingProfile(Model): if "s1" in parts and "elbow" in parts: ( result - .constrain("s1/elbow_top?conn0", "elbow/parent_upper/lip?conn_top0", "Plane") - .constrain("s1/elbow_top?conn1", "elbow/parent_upper/lip?conn_top1", "Plane") - .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/lip?conn_bot0", "Plane") - .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/lip?conn_bot1", "Plane") + .constrain("s1/elbow_top?conn0", f"elbow/parent_upper/lip?conn_{tag_top}0", "Plane") + .constrain("s1/elbow_top?conn1", f"elbow/parent_upper/lip?conn_{tag_top}1", "Plane") + .constrain("s1/elbow_bot?conn0", f"elbow/parent_upper/lip?conn_{tag_bot}0", "Plane") + .constrain("s1/elbow_bot?conn1", f"elbow/parent_upper/lip?conn_{tag_bot}1", "Plane") ) if "s2" in parts: result.add(self.assembly_s2(), name="s2") if "s2" in parts and "elbow" in parts: ( result - .constrain("s2/elbow_top?conn0", "elbow/child/lip?conn_top0", "Plane") - .constrain("s2/elbow_top?conn1", "elbow/child/lip?conn_top1", "Plane") - .constrain("s2/elbow_bot?conn0", "elbow/child/lip?conn_bot0", "Plane") - .constrain("s2/elbow_bot?conn1", "elbow/child/lip?conn_bot1", "Plane") + .constrain("s2/elbow_top?conn0", f"elbow/child/lip?conn_{tag_top}0", "Plane") + .constrain("s2/elbow_top?conn1", f"elbow/child/lip?conn_{tag_top}1", "Plane") + .constrain("s2/elbow_bot?conn0", f"elbow/child/lip?conn_{tag_bot}0", "Plane") + .constrain("s2/elbow_bot?conn1", f"elbow/child/lip?conn_{tag_bot}1", "Plane") ) if "wrist" in parts: angle = self.wrist_joint.disk_joint.movement_angle * elbow_wrist_deflection - result.add(self.wrist_joint.assembly(angle=elbow_wrist_deflection), name="wrist") + result.add(self.wrist_joint.assembly(angle=angle), name="wrist") if "s2" in parts and "wrist" in parts: # Mounted backwards to bend in other direction ( result - .constrain("s2/wrist_top?conn0", "wrist/parent_upper/lip?conn_bot0", "Plane") - .constrain("s2/wrist_top?conn1", "wrist/parent_upper/lip?conn_bot1", "Plane") - .constrain("s2/wrist_bot?conn0", "wrist/parent_upper/lip?conn_top0", "Plane") - .constrain("s2/wrist_bot?conn1", "wrist/parent_upper/lip?conn_top1", "Plane") + .constrain("s2/wrist_top?conn0", f"wrist/parent_upper/lip?conn_{tag_bot}0", "Plane") + .constrain("s2/wrist_top?conn1", f"wrist/parent_upper/lip?conn_{tag_bot}1", "Plane") + .constrain("s2/wrist_bot?conn0", f"wrist/parent_upper/lip?conn_{tag_top}0", "Plane") + .constrain("s2/wrist_bot?conn1", f"wrist/parent_upper/lip?conn_{tag_top}1", "Plane") ) if "s3" in parts: result.add(self.assembly_s3(), name="s3") if "s3" in parts and "wrist" in parts: ( result - .constrain("s3/wrist_top?conn0", "wrist/child/lip?conn_bot0", "Plane") - .constrain("s3/wrist_top?conn1", "wrist/child/lip?conn_bot1", "Plane") - .constrain("s3/wrist_bot?conn0", "wrist/child/lip?conn_top0", "Plane") - .constrain("s3/wrist_bot?conn1", "wrist/child/lip?conn_top1", "Plane") + .constrain("s3/wrist_top?conn0", f"wrist/child/lip?conn_{tag_bot}0", "Plane") + .constrain("s3/wrist_top?conn1", f"wrist/child/lip?conn_{tag_bot}1", "Plane") + .constrain("s3/wrist_bot?conn0", f"wrist/child/lip?conn_{tag_top}0", "Plane") + .constrain("s3/wrist_bot?conn1", f"wrist/child/lip?conn_{tag_top}1", "Plane") ) if len(parts) > 1: result.solve() @@ -845,10 +846,10 @@ class WingL(WingProfile): def __post_init__(self): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) - self.elbow_joint.angle_neutral = -15.0 + self.elbow_joint.angle_neutral = 15.0 self.elbow_rotate = 5.0 - self.wrist_joint.angle_neutral = -self.wrist_bot_loc.to2d_rot() - 30.0 - self.wrist_rotate = self.wrist_joint.angle_neutral + self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0 + self.wrist_rotate = -self.wrist_joint.angle_neutral super().__post_init__() -- 2.44.1 From 7cfc6f46fe1570f3760485417e86bfb395d868b5 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 18 Jul 2024 21:33:17 -0700 Subject: [PATCH 101/187] feat: Add mannequin to show perspective --- nhf/materials.py | 7 ++++ nhf/touhou/houjuu_nue/harness.py | 68 ++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/nhf/materials.py b/nhf/materials.py index 714995c..c4c4e49 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -14,10 +14,15 @@ class Role(Flag): Describes the role of a part """ + # Externally supplied object + FIXTURE = auto() + # Parent and child sides of joints PARENT = auto() CHILD = auto() CASING = auto() + # Springs, cushions DAMPING = auto() + # Main structural support STRUCTURE = auto() DECORATION = auto() ELECTRONIC = auto() @@ -38,8 +43,10 @@ class Role(Flag): head = next(iter(self)) return ROLE_COLOR_MAP[head] + # Maps roles to their colours ROLE_COLOR_MAP = { + Role.FIXTURE: _color('black', 0.2), Role.PARENT: _color('blue4', 0.6), Role.CASING: _color('dodgerblue3', 0.6), Role.CHILD: _color('darkorange2', 0.6), diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index 3a5ea90..4a036fd 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -5,7 +5,46 @@ from nhf import Material, Role from nhf.build import Model, TargetKind, target, assembly import nhf.utils -@dataclass +@dataclass(frozen=True, kw_only=True) +class Mannequin: + """ + A mannequin for calibration + """ + + shoulder_width: float = 400 + shoulder_to_waist: float = 440 + waist_width: float = 250 + head_height: float = 220.0 + neck_height: float = 105.0 + neck_diam: float = 140 + head_diam: float = 210 + torso_thickness: float = 150 + + def generate(self) -> Cq.Workplane: + head_neck = ( + Cq.Workplane("XY") + .cylinder( + radius=self.neck_diam/2, + height=self.neck_height, + centered=(True, True, False)) + .faces(">Z") + .workplane() + .cylinder( + radius=self.head_diam/2, + height=self.head_height, + combine=True, centered=(True, True, False)) + ) + result = ( + Cq.Workplane("XY") + .rect(self.waist_width, self.torso_thickness) + .workplane(offset=self.shoulder_to_waist) + .rect(self.shoulder_width, self.torso_thickness) + .loft(combine=True) + .union(head_neck.translate((0, 0, self.shoulder_to_waist))) + ) + return result.translate((0, self.torso_thickness / 2, 0)) + +@dataclass(kw_only=True) class Harness(Model): thickness: float = 25.4 / 8 width: float = 200.0 @@ -42,6 +81,8 @@ class Harness(Model): hs_joint_axis_cbore_diam: float = 20 hs_joint_axis_cbore_depth: float = 3 + mannequin: Mannequin = Mannequin() + def __post_init__(self): super().__init__(name="harness") @@ -164,22 +205,27 @@ class Harness(Model): @assembly() def assembly(self) -> Cq.Assembly: harness = self.surface() + mannequin_z = self.mannequin.shoulder_to_waist * 0.6 result = ( Cq.Assembly() - .addS(harness, name="base", - material=Material.WOOD_BIRCH, - role=Role.STRUCTURE) + .addS( + harness, name="base", + material=Material.WOOD_BIRCH, + role=Role.STRUCTURE) .constrain("base", "Fixed") + .addS( + self.mannequin.generate(), + name="mannequin", + role=Role.FIXTURE, + loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180)) + .constrain("mannequin", "Fixed") ) for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: j = self.hs_joint_parent() - ( - result - .addS(j, name=name, - role=Role.PARENT, - material=Material.PLASTIC_PLA) - #.constrain("base?mount", f"{name}?base", "Axis") - ) + result.addS( + j, name=name, + role=Role.PARENT, + material=Material.PLASTIC_PLA) for i in range(4): result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") result.solve() -- 2.44.1 From dccae49b9d374d9a52334572a3862606113c6b96 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 18 Jul 2024 21:40:47 -0700 Subject: [PATCH 102/187] feat: Spread the wing roots apart to make space --- nhf/touhou/houjuu_nue/harness.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index 4a036fd..f5921e1 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -44,20 +44,24 @@ class Mannequin: ) return result.translate((0, self.torso_thickness / 2, 0)) + +BASE_POS_X = 60.0 +BASE_POS_Y = 100.0 + @dataclass(kw_only=True) class Harness(Model): thickness: float = 25.4 / 8 - width: float = 200.0 - height: float = 300.0 + width: float = 220.0 + height: float = 310.0 fillet: float = 10.0 wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [ - ("r1", 55, 90), - ("l1", -55, 90), - ("r2", 60, 0), - ("l2", -60, 0), - ("r3", 55, -90), - ("l3", -55, -90), + ("r1", BASE_POS_X + 10, BASE_POS_Y), + ("l1", -BASE_POS_X - 10, BASE_POS_Y), + ("r2", BASE_POS_X + 10, 0), + ("l2", -BASE_POS_X - 10, 0), + ("r3", BASE_POS_X, -BASE_POS_Y), + ("l3", -BASE_POS_X, -BASE_POS_Y), ]) # Holes drilled onto harness for attachment with HS joint harness_to_root_conn_diam: float = 6 -- 2.44.1 From 7cb00c07940053cb0e28e18fa5f2920820b194f7 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 00:58:10 -0700 Subject: [PATCH 103/187] feat: Item baseclass, and fasteners --- nhf/__init__.py | 1 + nhf/materials.py | 36 ++++++++--------- nhf/parts/fasteners.py | 91 ++++++++++++++++++++++++++++++++++++++++++ nhf/parts/item.py | 67 +++++++++++++++++++++++++++++++ nhf/utils.py | 18 +++++++++ 5 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 nhf/parts/fasteners.py create mode 100644 nhf/parts/item.py diff --git a/nhf/__init__.py b/nhf/__init__.py index b582e6a..70b975b 100644 --- a/nhf/__init__.py +++ b/nhf/__init__.py @@ -1 +1,2 @@ from nhf.materials import Material, Role +from nhf.parts.item import Item diff --git a/nhf/materials.py b/nhf/materials.py index c4c4e49..af336da 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -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 diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py new file mode 100644 index 0000000..c6a0afe --- /dev/null +++ b/nhf/parts/fasteners.py @@ -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("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("top") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") + return result diff --git a/nhf/parts/item.py b/nhf/parts/item.py new file mode 100644 index 0000000..16fbef7 --- /dev/null +++ b/nhf/parts/item.py @@ -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 diff --git a/nhf/utils.py b/nhf/utils.py index f542632..e999e85 100644 --- a/nhf/utils.py +++ b/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 -- 2.44.1 From 3e5fe7bc5ebe8c5e8bf101c640ec3cedead543ed Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 14:06:13 -0700 Subject: [PATCH 104/187] fix: Shoulder joint axle --- nhf/parts/fasteners.py | 75 ++++++++++++++++++++++++--------- nhf/parts/item.py | 6 +-- nhf/parts/joints.py | 2 + nhf/parts/springs.py | 12 ++++-- nhf/parts/test.py | 19 +++++++++ nhf/touhou/houjuu_nue/joints.py | 33 ++++++++++++++- nhf/touhou/houjuu_nue/wing.py | 26 ++++++------ 7 files changed, 132 insertions(+), 41 deletions(-) diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index c6a0afe..78d7cfc 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -4,6 +4,42 @@ import cadquery as Cq from nhf import Item, Role import nhf.utils +@dataclass(frozen=True) +class FlatHeadBolt(Item): + diam_head: float + height_head: float + diam_thread: float + height_thread: float + + @property + def name(self) -> str: + return f"Bolt M{int(self.diam_thread)} h{int(self.height_thread)}mm" + + + def generate(self) -> Cq.Assembly: + print(self.name) + head = Cq.Solid.makeCylinder( + radius=self.diam_head / 2, + height=self.height_head, + ) + rod = ( + Cq.Workplane('XY') + .cylinder( + radius=self.diam_thread/ 2, + height=self.height_thread, + centered=(True, True, False)) + ) + rod.faces("Z").tag("root") + + return ( + Cq.Assembly() + .addS(rod, name="thread", role=Role.CONNECTION) + .addS(head, name="head", role=Role.CONNECTION, + loc=Cq.Location((0, 0, self.height_thread))) + ) + + @dataclass(frozen=True) class ThreaddedKnob(Item): """ @@ -12,8 +48,8 @@ class ThreaddedKnob(Item): > Othmro Black 12mm(M12) x 50mm Thread Replacement Star Hand Knob Tightening > Screws """ - diam_rod: float - height_rod: float + diam_thread: float + height_thread: float diam_knob: float diam_neck: float @@ -22,7 +58,7 @@ class ThreaddedKnob(Item): @property def name(self) -> str: - return f"Knob-M{int(self.diam_rod)}-{int(self.height_rod)}mm" + return f"Knob M{int(self.diam_thread)} h{int(self.height_thread)}mm" def generate(self) -> Cq.Assembly: print(self.name) @@ -34,41 +70,40 @@ class ThreaddedKnob(Item): radius=self.diam_neck / 2, height=self.height_neck, ) - rod = ( + thread = ( Cq.Workplane('XY') .cylinder( - radius=self.diam_rod / 2, - height=self.height_rod, + radius=self.diam_thread / 2, + height=self.height_thread, centered=(True, True, False)) ) - rod.faces("Z").tag("root") + thread.faces("Z").tag("root") return ( Cq.Assembly() - .addS(rod, name="rod", role=Role.CONNECTION) + .addS(thread, name="thread", role=Role.CONNECTION) .addS(neck, name="neck", role=Role.HANDLE, - loc=Cq.Location((0, 0, self.height_rod))) + loc=Cq.Location((0, 0, self.height_thread))) .addS(knob, name="knob", role=Role.HANDLE, - loc=Cq.Location((0, 0, self.height_rod + self.height_neck))) + loc=Cq.Location((0, 0, self.height_thread + self.height_neck))) ) @dataclass(frozen=True) class HexNut(Item): - diam: float + diam_thread: float pitch: float - # FIXME: Measure these - m: float - s: float + thickness: float + width: float def __post_init__(self): - assert self.s > self.diam + assert self.width > self.diam_thread @property def name(self): - return f"HexNut-M{int(self.diam)}-{self.pitch}" + return f"HexNut M{int(self.diam_thread)}-{self.pitch}" @property def role(self): @@ -76,14 +111,14 @@ class HexNut(Item): def generate(self) -> Cq.Workplane: print(self.name) - r = self.s / math.sqrt(3) + r = self.width / math.sqrt(3) result = ( Cq.Workplane("XY") .sketch() .regularPolygon(r=r, n=6) - .circle(r=self.diam/2, mode='s') + .circle(r=self.diam_thread/2, mode='s') .finalize() - .extrude(self.m) + .extrude(self.thickness) ) result.faces("Z").tag("top") diff --git a/nhf/parts/item.py b/nhf/parts/item.py index 16fbef7..1e321fc 100644 --- a/nhf/parts/item.py +++ b/nhf/parts/item.py @@ -31,17 +31,17 @@ class Item: def role(self) -> Optional[Role]: return None - def generate(self) -> Union[Cq.Assembly, Cq.Workplane]: + def generate(self, **kwargs) -> Union[Cq.Assembly, Cq.Workplane]: """ Creates an assembly for this item. Subclass should implement this """ return Cq.Assembly() - def assembly(self) -> Cq.Assembly: + def assembly(self, **kwargs) -> Cq.Assembly: """ Interface for creating assembly with the necessary metadata """ - a = self.generate() + a = self.generate(**kwargs) if isinstance(a, Cq.Workplane): a = Cq.Assembly(a) if role := self.role: diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 8602147..31bb686 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -180,6 +180,7 @@ class TorsionJoint: 3. An outer and an inner annuli which forms a track the rider can move on """ spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( + mass=float('nan'), radius=10.0, thickness=2.0, height=15.0, @@ -306,6 +307,7 @@ class TorsionJoint: .hole(self.radius_axle * 2) .cut(slot.moved(Cq.Location((0, 0, self.track_disk_height)))) ) + result.faces(" str: + return f"TorsionSpring-{int(self.radius)}-{int(self.height)}" + @property def radius_inner(self) -> float: return self.radius - self.thickness @@ -28,7 +33,7 @@ class TorsionSpring: def torque_at(self, theta: float) -> float: return self.torsion_rate * theta - def generate(self, deflection: float = 0): + def generate(self, deflection: float = 0) -> Cq.Workplane: omega = self.angle_neutral + deflection omega = -omega if self.right_handed else omega base = ( @@ -39,7 +44,6 @@ class TorsionSpring: base.faces(">Z").tag("top") base.faces(" Cq.Location: + """ + 2d location of the arm surface on the parent side, relative to axle + """ + return Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.parent_lip_ext, 0, 0) + def parent(self, top: bool = False) -> Cq.Assembly: joint = self.torsion_joint # Thickness of the lip connecting this joint to the wing root @@ -219,14 +239,18 @@ class ShoulderJoint(Model): .constrain("child/core", "Fixed") .addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top", role=Role.DAMPING, material=mat_spring) + .addS(self.bolt.assembly(), name="bolt_top") .addS(self.parent_top(), name="parent_top", role=Role.PARENT, material=mat) .addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot", role=Role.DAMPING, material=mat_spring) + .addS(self.bolt.assembly(), name="bolt_bot") .addS(self.parent_bot(), name="parent_bot", role=Role.PARENT, material=mat) + .constrain("bolt_top/thread?root", "parent_top/track?bot", "Plane", param=0) + .constrain("bolt_bot/thread?root", "parent_bot/track?bot", "Plane", param=0) ) TorsionJoint.add_constraints( result, @@ -318,6 +342,7 @@ class DiskJoint(Model): the housing. This provides torsion resistance. """ spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( + mass=float('nan'), radius=9 / 2, thickness=1.3, height=6.5, @@ -617,7 +642,7 @@ class ElbowJoint(Model): parent_arm_angle: float = 180.0 # Size of the mounting holes - hole_diam: float = 6.0 + hole_diam: float = 4.0 material: Material = Material.RESIN_TRANSPERENT @@ -628,6 +653,10 @@ class ElbowJoint(Model): assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 + @property + def total_thickness(self): + return self.disk_joint.total_thickness + def parent_arm_loc(self) -> Cq.Location: """ 2d Location of the centre of the arm surface on the parent side, assuming diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b9e2014..77f51f8 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -41,8 +41,8 @@ class WingProfile(Model): )) shoulder_angle_bias: float = 0.0 shoulder_width: float = 36.0 - shoulder_tip_x: float = -200.0 - shoulder_tip_y: float = 160.0 + shoulder_tip_x: float = -260.0 + shoulder_tip_y: float = 165.0 shoulder_mid_x: float = -105.0 shoulder_mid_y: float = 75.0 @@ -102,7 +102,11 @@ class WingProfile(Model): self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2) self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) + assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness) + assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) + self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias + self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, -self.shoulder_angle_bias) @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: @@ -225,6 +229,9 @@ class WingProfile(Model): (-self.base_width, 0), ) .assemble() + .push([self.shoulder_axle_loc.to2d_pos()]) + .circle(self.shoulder_joint.radius, mode='a') + .circle(self.shoulder_joint.bolt.diam_head / 2, mode='s') ) return sketch @@ -292,17 +299,12 @@ class WingProfile(Model): def surface_s0(self, top: bool = False) -> Cq.Workplane: base_dx = -(self.base_width - self.base_plate_width) / 2 base_dy = self.base_joint.joint_height - sw = self.shoulder_width - - axle_dist = self.shoulder_joint.parent_lip_ext - theta = math.radians(self.shoulder_angle_neutral) - c, s = math.cos(theta), math.sin(theta) + loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2) tags = [ - # transforms [axle_dist, -sw/2] about the centre (tip_x, tip_y - sw/2) - ("shoulder", Cq.Location.from2d( - self.shoulder_tip_x + axle_dist * c + (-sw/2) * s, - self.shoulder_tip_y - sw / 2 - axle_dist * s + (-sw/2) * c, - -self.shoulder_angle_neutral)), + ("shoulder", + self.shoulder_axle_loc * + self.shoulder_joint.parent_arm_loc() * + loc_tip), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ] result = extrude_with_markers( -- 2.44.1 From 4b6b05853edf5595cf82bb72991fd1ca66841396 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 15:06:57 -0700 Subject: [PATCH 105/187] refactor: HS Joint into its own class --- nhf/parts/fasteners.py | 2 +- nhf/touhou/houjuu_nue/__init__.py | 42 +++--- nhf/touhou/houjuu_nue/harness.py | 122 ++++------------- nhf/touhou/houjuu_nue/joints.py | 209 +++++++++++++++++++++++++++++- nhf/touhou/houjuu_nue/wing.py | 113 +++++----------- 5 files changed, 281 insertions(+), 207 deletions(-) diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index 78d7cfc..eb251f7 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -122,5 +122,5 @@ class HexNut(Item): ) result.faces("Z").tag("top") - result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dir", direction="+X") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dirX", direction="+X") return result diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index f2e779b..3a414cf 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -77,22 +77,15 @@ class Parameters(Model): def __post_init__(self): super().__init__(name="houjuu-nue") - self.wing_r1.base_joint = self.harness.hs_hirth_joint - self.wing_r2.base_joint = self.harness.hs_hirth_joint - self.wing_r3.base_joint = self.harness.hs_hirth_joint - self.wing_l1.base_joint = self.harness.hs_hirth_joint - self.wing_l2.base_joint = self.harness.hs_hirth_joint - self.wing_l3.base_joint = self.harness.hs_hirth_joint + self.wing_r1.root_joint = self.harness.root_joint + self.wing_r2.root_joint = self.harness.root_joint + self.wing_r3.root_joint = self.harness.root_joint + self.wing_l1.root_joint = self.harness.root_joint + self.wing_l2.root_joint = self.harness.root_joint + self.wing_l3.root_joint = self.harness.root_joint self.wing_r1.shoulder_joint.torsion_joint - assert self.wing_r1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_r2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_r3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_l1.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_l2.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - assert self.wing_l3.hs_joint_axis_diam == self.harness.hs_joint_axis_diam - @submodel(name="harness") def submodel_harness(self) -> Model: return self.harness @@ -126,19 +119,20 @@ class Parameters(Model): result = ( Cq.Assembly() .add(self.harness.assembly(), name="harness", loc=Cq.Location((0, 0, 0))) - .add(self.wing_r1.assembly(parts, **kwargs), name="wing_r1") - .add(self.wing_r2.assembly(parts, **kwargs), name="wing_r2") - .add(self.wing_r3.assembly(parts, **kwargs), name="wing_r3") - .add(self.wing_l1.assembly(parts, **kwargs), name="wing_l1") - .add(self.wing_l2.assembly(parts, **kwargs), name="wing_l2") - .add(self.wing_l3.assembly(parts, **kwargs), name="wing_l3") + .add(self.wing_r1.assembly(parts, root_offset=9, **kwargs), name="wing_r1") + .add(self.wing_r2.assembly(parts, root_offset=7, **kwargs), name="wing_r2") + .add(self.wing_r3.assembly(parts, root_offset=6, **kwargs), name="wing_r3") + .add(self.wing_l1.assembly(parts, root_offset=7, **kwargs), name="wing_l1") + .add(self.wing_l2.assembly(parts, root_offset=8, **kwargs), name="wing_l2") + .add(self.wing_l3.assembly(parts, root_offset=9, **kwargs), name="wing_l3") ) - for tag, offset in [("r1", 9), ("r2", 7), ("r3", 6), ("l1", 7), ("l2", 8), ("l3", 9)]: - self.harness.hs_hirth_joint.add_constraints( + for tag in ["r1", "r2", "r3", "l1", "l2", "l3"]: + self.harness.add_root_joint_constraint( result, - f"harness/{tag}", - f"wing_{tag}/s0/hs", - offset=offset) + "harness/base", + f"wing_{tag}/root", + tag + ) return result.solve() @submodel(name="trident") diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index f5921e1..8507873 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -2,7 +2,8 @@ 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 +from nhf.build import Model, TargetKind, target, assembly, submodel +from nhf.touhou.houjuu_nue.joints import RootJoint import nhf.utils @dataclass(frozen=True, kw_only=True) @@ -63,33 +64,18 @@ class Harness(Model): ("r3", BASE_POS_X, -BASE_POS_Y), ("l3", -BASE_POS_X, -BASE_POS_Y), ]) - # Holes drilled onto harness for attachment with HS joint - harness_to_root_conn_diam: float = 6 - hs_hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( - radius=25.0, - radius_inner=15.0, - tooth_height=7.0, - base_height=5.0, - n_tooth=24, - )) - - hs_joint_base_width: float = 85 - hs_joint_base_thickness: float = 10 - hs_joint_corner_fillet: float = 5 - hs_joint_corner_cbore_diam: float = 12 - hs_joint_corner_cbore_depth: float = 2 - hs_joint_corner_inset: float = 12 - - hs_joint_axis_diam: float = 12.0 - hs_joint_axis_cbore_diam: float = 20 - hs_joint_axis_cbore_depth: float = 3 + root_joint: RootJoint = field(default_factory=lambda: RootJoint()) mannequin: Mannequin = Mannequin() def __post_init__(self): super().__init__(name="harness") + @submodel(name="root-joint") + def submodel_root_joint(self) -> Model: + return self.root_joint + @target(name="profile", kind=TargetKind.DXF) def profile(self) -> Cq.Sketch: """ @@ -115,12 +101,12 @@ class Harness(Model): .fillet(self.fillet) ) for tag, x, y in self.wing_base_pos: - conn = [(px + x, py + y) for px, py in self.hs_joint_harness_conn()] + conn = [(px + x, py + y) for px, py in self.root_joint.corner_pos()] sketch = ( sketch .push(conn) .tag(tag) - .circle(self.harness_to_root_conn_diam / 2, mode='s') + .circle(self.root_joint.corner_hole_diam / 2, mode='s') .reset() ) return sketch @@ -138,78 +124,27 @@ class Harness(Model): plane = result.faces(">Y").workplane() for tag, x, y in self.wing_base_pos: conn = [(px + x, py + y) for px, py - in self.hs_joint_harness_conn()] + in self.root_joint.corner_pos()] for i, (px, py) in enumerate(conn): plane.moveTo(px, py).tagPoint(f"{tag}_{i}") return result - def hs_joint_harness_conn(self) -> list[tuple[int, int]]: - """ - Generates a set of points corresponding to the connectorss - """ - dx = self.hs_joint_base_width / 2 - self.hs_joint_corner_inset - return [ - (dx, dx), - (dx, -dx), - (-dx, -dx), - (-dx, dx), - ] + def add_root_joint_constraint( + self, + a: Cq.Assembly, + harness_tag: str, + joint_tag: str, + mount_tag: str): + for i in range(4): + a.constrain(f"{harness_tag}?{mount_tag}_{i}", f"{joint_tag}/parent?h{i}", "Point") - @target(name="hs-joint-parent") - def hs_joint_parent(self): - """ - Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth - coupling, a cylindrical base, and a mounting base. - """ - hirth = self.hs_hirth_joint.generate() - conn = self.hs_joint_harness_conn() - result = ( - Cq.Workplane('XY') - .box( - self.hs_joint_base_width, - self.hs_joint_base_width, - self.hs_joint_base_thickness, - centered=(True, True, False)) - .translate((0, 0, -self.hs_joint_base_thickness)) - .edges("|Z") - .fillet(self.hs_joint_corner_fillet) - .faces(">Z") - .workplane() - .pushPoints(conn) - .cboreHole( - diameter=self.harness_to_root_conn_diam, - cboreDiameter=self.hs_joint_corner_cbore_diam, - cboreDepth=self.hs_joint_corner_cbore_depth) - ) - # Creates a plane parallel to the holes but shifted to the base - plane = result.faces(">Z").workplane(offset=-self.hs_joint_base_thickness) - - for i, (px, py) in enumerate(conn): - plane.moveTo(px, py).tagPoint(f"h{i}") - result = ( - result - .faces(">Z") - .workplane() - .union(hirth, tol=0.1) - .clean() - ) - result = ( - result.faces(" Cq.Assembly: + def assembly(self, with_root_joint: bool = False) -> Cq.Assembly: harness = self.surface() mannequin_z = self.mannequin.shoulder_to_waist * 0.6 + + result = ( Cq.Assembly() .addS( @@ -224,13 +159,12 @@ class Harness(Model): loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180)) .constrain("mannequin", "Fixed") ) - for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: - j = self.hs_joint_parent() - result.addS( - j, name=name, - role=Role.PARENT, - material=Material.PLASTIC_PLA) - for i in range(4): - result.constrain(f"base?{name}_{i}", f"{name}?h{i}", "Point") + if with_root_joint: + for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: + result.addS( + self.root_joint.assembly(), name=name, + role=Role.PARENT, + material=Material.PLASTIC_PLA) + self.add_root_joint_constraint(result, "base", name, name) result.solve() return result diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 04291aa..0dd446a 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -5,13 +5,204 @@ import cadquery as Cq from nhf import Material, Role from nhf.build import Model, target, assembly from nhf.parts.springs import TorsionSpring -from nhf.parts.fasteners import FlatHeadBolt -from nhf.parts.joints import TorsionJoint +from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob +from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes import nhf.utils TOL = 1e-6 +@dataclass +class RootJoint(Model): + """ + The Houjuu-Scarlett Mechanism + """ + knob: ThreaddedKnob = ThreaddedKnob( + mass=float('nan'), + diam_thread=12.0, + height_thread=30.0, + diam_knob=50.0, + # FIXME: Undetermined + diam_neck=30.0, + height_neck=10.0, + height_knob=10.0, + ) + hex_nut: HexNut = HexNut( + # FIXME: Undetermined + mass=float('nan'), + + diam_thread=12.0, + pitch=1.75, + thickness=9.8, + width=18.9, + ) + hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( + radius=25.0, + radius_inner=15.0, + tooth_height=7.0, + base_height=5.0, + n_tooth=24, + )) + parent_width: float = 85 + parent_thickness: float = 10 + parent_corner_fillet: float = 5 + parent_corner_cbore_diam: float = 12 + parent_corner_cbore_depth: float = 2 + parent_corner_inset: float = 12 + parent_mount_thickness: float = 25.4 / 16 + + child_corner_dx: float = 17.0 + child_corner_dz: float = 24.0 + + axis_diam: float = 12.0 + axis_cbore_diam: float = 20 + axis_cbore_depth: float = 3 + corner_hole_diam: float = 6.0 + + child_height: float = 60.0 + child_width: float = 50.0 + child_mount_thickness: float = 25.4 / 8 + + def corner_pos(self) -> list[tuple[int, int]]: + """ + Generates a set of points corresponding to the connectorss + """ + dx = self.parent_width / 2 - self.parent_corner_inset + return [ + (dx, dx), + (dx, -dx), + (-dx, -dx), + (-dx, dx), + ] + + @property + def total_height(self) -> float: + return self.parent_thickness + self.hirth_joint.total_height + + @target(name="parent") + def parent(self): + """ + Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth + coupling, a cylindrical base, and a mounting base. + """ + hirth = self.hirth_joint.generate() + conn = self.corner_pos() + result = ( + Cq.Workplane('XY') + .box( + self.parent_width, + self.parent_width, + self.parent_thickness, + centered=(True, True, False)) + .translate((0, 0, -self.parent_thickness)) + .edges("|Z") + .fillet(self.parent_corner_fillet) + .faces(">Z") + .workplane() + .pushPoints(conn) + .cboreHole( + diameter=self.corner_hole_diam, + cboreDiameter=self.parent_corner_cbore_diam, + cboreDepth=self.parent_corner_cbore_depth) + ) + # Creates a plane parallel to the holes but shifted to the base + plane = result.faces(">Z").workplane(offset=-self.parent_thickness) + + for i, (px, py) in enumerate(conn): + plane.moveTo(px, py).tagPoint(f"h{i}") + result = ( + result + .faces(">Z") + .workplane() + .union(hirth, tol=0.1) + .clean() + ) + result = ( + result.faces(" Cq.Workplane: + hirth = self.hirth_joint.generate(is_mated=True) + dy = self.child_corner_dx + dx = self.child_corner_dz + conn = [ + (-dx, -dy), + (dx, -dy), + (dx, dy), + (-dx, dy), + ] + result = ( + Cq.Workplane('XY') + .box( + self.child_height, + self.child_width, + self.hirth_joint.base_height, + centered=(True, True, False)) + #.translate((0, 0, -self.base_joint.base_height)) + #.edges("|Z") + #.fillet(self.hs_joint_corner_fillet) + .faces(">Z") + .workplane() + .pushPoints(conn) + .hole(self.corner_hole_diam) + ) + # Creates a plane parallel to the holes but shifted to the base + plane = result.faces(">Z").workplane(offset=-self.hirth_joint.base_height) + + for i, (px, py) in enumerate(conn): + plane.moveTo(px, py).tagPlane(f"conn{i}") + result = ( + result + .faces(">Z") + .workplane() + .union(hirth, tol=0.1) + .clean() + ) + result = ( + result.faces(" Cq.Assembly: + """ + Specify knob position to determine the position of the knob from fully + inserted (0) or fully uninserted (1) + """ + knob_h = self.total_height + self.child_mount_thickness + result = ( + Cq.Assembly() + .addS(self.parent(), name="parent", + material=Material.PLASTIC_PLA, + role=Role.PARENT) + .constrain("parent", "Fixed") + .addS(self.child(), name="child", + material=Material.PLASTIC_PLA, + role=Role.CHILD) + .addS(self.hex_nut.assembly(), name="hex_nut") + .addS(self.knob.assembly(), name="knob", + loc=Cq.Location((0, 0, knob_h * fastener_pos))) + .constrain("knob/thread", "Fixed") + .constrain("hex_nut?bot", "parent?base", "Plane", param=0) + .constrain("hex_nut?dirX", "parent@faces@>X", "Axis", param=0) + ) + self.hirth_joint.add_constraints( + result, + "parent", + "child", + offset=offset + ) + return result.solve() + @dataclass class ShoulderJoint(Model): @@ -228,10 +419,11 @@ class ShoulderJoint(Model): return result @assembly() - def assembly(self, deflection: float = 0) -> Cq.Assembly: + def assembly(self, fastener_pos: float = 0.0, deflection: float = 0) -> Cq.Assembly: directrix = self.directrix_id mat = Material.RESIN_TRANSPERENT mat_spring = Material.STEEL_SPRING + bolt_z = self.height / 2 + self.bolt.height_thread * (fastener_pos - 1) result = ( Cq.Assembly() .addS(self.child(), name="child", @@ -239,18 +431,21 @@ class ShoulderJoint(Model): .constrain("child/core", "Fixed") .addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top", role=Role.DAMPING, material=mat_spring) - .addS(self.bolt.assembly(), name="bolt_top") .addS(self.parent_top(), name="parent_top", role=Role.PARENT, material=mat) .addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot", role=Role.DAMPING, material=mat_spring) - .addS(self.bolt.assembly(), name="bolt_bot") .addS(self.parent_bot(), name="parent_bot", role=Role.PARENT, material=mat) - .constrain("bolt_top/thread?root", "parent_top/track?bot", "Plane", param=0) - .constrain("bolt_bot/thread?root", "parent_bot/track?bot", "Plane", param=0) + # Fasteners + .addS(self.bolt.assembly(), name="bolt_top", + loc=Cq.Location((0, 0, bolt_z))) + .constrain("bolt_top/thread?root", 'Fixed') + .addS(self.bolt.assembly(), name="bolt_bot", + loc=Cq.Location((0, 0, -bolt_z), (1,0,0), 180)) + .constrain("bolt_bot/thread?root", 'Fixed') ) TorsionJoint.add_constraints( result, diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 77f51f8..515245f 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -11,7 +11,7 @@ from nhf import Material, Role 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 +from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint from nhf.parts.planar import extrude_with_markers import nhf.utils @@ -20,19 +20,8 @@ class WingProfile(Model): name: str = "wing" - base_joint: HirthJoint = field(default_factory=lambda: HirthJoint( - radius=25.0, - radius_inner=15.0, - tooth_height=7.0, - base_height=5, - n_tooth=24, - )) + root_joint: RootJoint = field(default_factory=lambda: RootJoint()) base_width: float = 80.0 - hs_joint_corner_dx: float = 17.0 - hs_joint_corner_dz: float = 24.0 - hs_joint_corner_hole_diam: float = 6.0 - hs_joint_axis_diam: float = 12.0 - base_plate_width: float = 50.0 panel_thickness: float = 25.4 / 16 spacer_thickness: float = 25.4 / 8 @@ -108,6 +97,8 @@ class WingProfile(Model): self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, -self.shoulder_angle_bias) + assert self.spacer_thickness == self.root_joint.child_mount_thickness + @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: return self.shoulder_joint @@ -126,55 +117,6 @@ class WingProfile(Model): def shoulder_height(self) -> float: return self.shoulder_joint.height - @target(name="base-hs-joint") - def base_hs_joint(self) -> Cq.Workplane: - """ - Parent part of the Houjuu-Scarlett joint, which is composed of a Hirth - coupling, a cylindrical base, and a mounting base. - """ - hirth = self.base_joint.generate(is_mated=True) - dy = self.hs_joint_corner_dx - dx = self.hs_joint_corner_dz - conn = [ - (-dx, -dy), - (dx, -dy), - (dx, dy), - (-dx, dy), - ] - result = ( - Cq.Workplane('XY') - .box( - self.root_height, - self.base_plate_width, - self.base_joint.base_height, - centered=(True, True, False)) - #.translate((0, 0, -self.base_joint.base_height)) - #.edges("|Z") - #.fillet(self.hs_joint_corner_fillet) - .faces(">Z") - .workplane() - .pushPoints(conn) - .hole(self.hs_joint_corner_hole_diam) - ) - # Creates a plane parallel to the holes but shifted to the base - plane = result.faces(">Z").workplane(offset=-self.base_joint.base_height) - - for i, (px, py) in enumerate(conn): - plane.moveTo(px, py).tagPlane(f"conn{i}") - result = ( - result - .faces(">Z") - .workplane() - .union(hirth, tol=0.1) - .clean() - ) - result = ( - result.faces(" Cq.Sketch: """ The outer boundary of s0, used to produce the curved panel and the @@ -275,11 +217,11 @@ class WingProfile(Model): """ Should be cut """ - assert self.base_plate_width < self.base_width - assert self.hs_joint_corner_dx * 2 < self.base_width - assert self.hs_joint_corner_dz * 2 < self.root_height - dy = self.hs_joint_corner_dx - dx = self.hs_joint_corner_dz + assert self.root_joint.child_width < self.base_width + assert self.root_joint.child_corner_dx * 2 < self.base_width + assert self.root_joint.child_corner_dz * 2 < self.root_height + dy = self.root_joint.child_corner_dx + dx = self.root_joint.child_corner_dz holes = [ Hole(x=-dx, y=-dy), Hole(x=dx, y=-dy), @@ -288,17 +230,17 @@ class WingProfile(Model): ] return MountingBox( length=self.root_height, - width=self.base_plate_width, + width=self.root_joint.child_width, thickness=self.spacer_thickness, holes=holes, - hole_diam=self.hs_joint_corner_hole_diam, + hole_diam=self.root_joint.corner_hole_diam, centred=(True, True), flip_y=self.flip, ) def surface_s0(self, top: bool = False) -> Cq.Workplane: - base_dx = -(self.base_width - self.base_plate_width) / 2 - base_dy = self.base_joint.joint_height + base_dx = -(self.base_width - self.root_joint.child_width) / 2 + base_dy = self.root_joint.hirth_joint.joint_height loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2) tags = [ ("shoulder", @@ -351,14 +293,6 @@ class WingProfile(Model): .constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane") .constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis") ) - hs_joint = self.base_hs_joint() - ( - result - .addS(hs_joint, name="hs", role=Role.CHILD, material=self.mat_hs_joint) - .constrain("hs?conn0", "base?conn0", "Plane", param=0) - .constrain("hs?conn1", "base?conn1", "Plane", param=0) - .constrain("hs?conn2", "base?conn2", "Plane", param=0) - ) return result.solve() @@ -627,9 +561,11 @@ class WingProfile(Model): parts: Optional[list[str]] = None, shoulder_deflection: float = 0.0, elbow_wrist_deflection: float = 0.0, + root_offset: int = 5, + fastener_pos: float = 0.0, ) -> Cq.Assembly(): if parts is None: - parts = ["s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] + parts = ["root", "s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] result = ( Cq.Assembly() ) @@ -639,8 +575,23 @@ class WingProfile(Model): if "s0" in parts: result.add(self.assembly_s0(), name="s0") + if "root" in parts: + result.addS(self.root_joint.assembly( + offset=root_offset, + fastener_pos=fastener_pos, + ), name="root") + result.constrain("root/parent", "Fixed") + if "s0" in parts and "root" in parts: + ( + result + .constrain("s0/base?conn0", "root/child?conn0", "Plane", param=0) + .constrain("s0/base?conn1", "root/child?conn1", "Plane", param=0) + .constrain("s0/base?conn2", "root/child?conn2", "Plane", param=0) + ) if "shoulder" in parts: - result.add(self.shoulder_joint.assembly(deflection=shoulder_deflection * 80), name="shoulder") + result.add(self.shoulder_joint.assembly( + fastener_pos=fastener_pos, + deflection=shoulder_deflection * 80), name="shoulder") if "s0" in parts and "shoulder" in parts: ( result -- 2.44.1 From 34f6b400930870adddfcf0c20c19fb684a6b7f65 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 16:13:33 -0700 Subject: [PATCH 106/187] feat: s0 support in the middle --- nhf/parts/box.py | 5 +---- nhf/touhou/houjuu_nue/joints.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 40 +++++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 7d2cbff..603eb2d 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -44,10 +44,7 @@ class MountingBox(Model): thickness: float = 1.0 # List of (x, y), diam - holes: list[Hole] = field(default_factory=lambda: [ - Hole(x=5, y=5, diam=3), - Hole(x=20, y=10, diam=5), - ]) + holes: list[Hole] = field(default_factory=lambda: []) hole_diam: Optional[float] = None centred: Tuple[bool, bool] = (False, True) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 0dd446a..dc52908 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -178,7 +178,7 @@ class RootJoint(Model): Specify knob position to determine the position of the knob from fully inserted (0) or fully uninserted (1) """ - knob_h = self.total_height + self.child_mount_thickness + knob_h = self.hex_nut.thickness result = ( Cq.Assembly() .addS(self.parent(), name="parent", diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 515245f..03f0fd9 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -20,8 +20,8 @@ class WingProfile(Model): name: str = "wing" - root_joint: RootJoint = field(default_factory=lambda: RootJoint()) base_width: float = 80.0 + root_joint: RootJoint = field(default_factory=lambda: RootJoint()) panel_thickness: float = 25.4 / 16 spacer_thickness: float = 25.4 / 8 @@ -32,7 +32,7 @@ class WingProfile(Model): shoulder_width: float = 36.0 shoulder_tip_x: float = -260.0 shoulder_tip_y: float = 165.0 - shoulder_mid_x: float = -105.0 + shoulder_mid_x: float = -125.0 shoulder_mid_y: float = 75.0 s1_thickness: float = 25.0 @@ -95,7 +95,7 @@ class WingProfile(Model): assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias - self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, -self.shoulder_angle_bias) + self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, self.shoulder_angle_bias) assert self.spacer_thickness == self.root_joint.child_mount_thickness @@ -193,7 +193,8 @@ class WingProfile(Model): @submodel(name="spacer-s0-shoulder") def spacer_s0_shoulder(self) -> MountingBox: """ - Should be cut + Shoulder side serves double purpose for mounting shoulder joint and + structural support """ holes = [ hole @@ -215,7 +216,7 @@ class WingProfile(Model): @submodel(name="spacer-s0-shoulder") def spacer_s0_base(self) -> MountingBox: """ - Should be cut + Base side connects to H-S joint """ assert self.root_joint.child_width < self.base_width assert self.root_joint.child_corner_dx * 2 < self.base_width @@ -237,17 +238,40 @@ class WingProfile(Model): centred=(True, True), flip_y=self.flip, ) + @submodel(name="spacer-s0-mid3") + def spacer_s0_mid3(self) -> MountingBox: + return MountingBox( + length=self.root_height, + width=40, + thickness=self.spacer_thickness, + flip_y=self.flip + ) + @submodel(name="spacer-s0-mid2") + def spacer_s0_mid2(self) -> MountingBox: + return MountingBox( + length=self.root_height, + width=60, + thickness=self.spacer_thickness, + flip_y=self.flip + ) def surface_s0(self, top: bool = False) -> Cq.Workplane: - base_dx = -(self.base_width - self.root_joint.child_width) / 2 + base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10 base_dy = self.root_joint.hirth_joint.joint_height loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2) + mid_spacer_loc = ( + Cq.Location.from2d(0, -self.shoulder_width/2) * + self.shoulder_axle_loc * + Cq.Location.rot2d(self.shoulder_joint.angle_neutral) + ) tags = [ ("shoulder", self.shoulder_axle_loc * self.shoulder_joint.parent_arm_loc() * loc_tip), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), + ("mid3", mid_spacer_loc * Cq.Location.from2d(90, 0)), + ("mid2", mid_spacer_loc * Cq.Location.from2d(150, 0)), ] result = extrude_with_markers( self.profile_s0(), @@ -279,7 +303,9 @@ class WingProfile(Model): ) for o, tag in [ (self.spacer_s0_shoulder().generate(), "shoulder"), - (self.spacer_s0_base().generate(), "base") + (self.spacer_s0_base().generate(), "base"), + (self.spacer_s0_mid3().generate(), "mid3"), + (self.spacer_s0_mid2().generate(), "mid2"), ]: top_tag, bot_tag = "top", "bot" if self.flip: -- 2.44.1 From 433a553957c9c1c94a6b85b85efa5f8a6506e9e1 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 16:15:49 -0700 Subject: [PATCH 107/187] fix: Missing mass argument, extranous print --- nhf/parts/fasteners.py | 3 --- nhf/parts/test.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index eb251f7..c9c9e61 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -17,7 +17,6 @@ class FlatHeadBolt(Item): def generate(self) -> Cq.Assembly: - print(self.name) head = Cq.Solid.makeCylinder( radius=self.diam_head / 2, height=self.height_head, @@ -61,7 +60,6 @@ class ThreaddedKnob(Item): return f"Knob M{int(self.diam_thread)} h{int(self.height_thread)}mm" def generate(self) -> Cq.Assembly: - print(self.name) knob = Cq.Solid.makeCylinder( radius=self.diam_knob / 2, height=self.height_knob, @@ -110,7 +108,6 @@ class HexNut(Item): return Role.CONNECTION def generate(self) -> Cq.Workplane: - print(self.name) r = self.width / math.sqrt(3) result = ( Cq.Workplane("XY") diff --git a/nhf/parts/test.py b/nhf/parts/test.py index 27ae9a5..3af5eca 100644 --- a/nhf/parts/test.py +++ b/nhf/parts/test.py @@ -55,7 +55,7 @@ class TestJoints(unittest.TestCase): with self.subTest(slot=slot, right_handed=False): self.torsion_joint_case(j, slot) def test_torsion_joint_right_handed(self): - j = joints.TorsionJoint(springs.TorsionSpring(right_handed=True)) + j = joints.TorsionJoint(springs.TorsionSpring(mass=float('nan'), right_handed=True)) for slot in range(j.rider_n_slots): with self.subTest(slot=slot, right_handed=True): self.torsion_joint_case(j, slot) -- 2.44.1 From 560e9b54ddb748c7835b8bc6791feb1fffb187a1 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 16:37:47 -0700 Subject: [PATCH 108/187] feat: Child guard to prevent collision in shoulder --- nhf/touhou/houjuu_nue/joints.py | 50 +++++++++++++++++++++++---------- nhf/touhou/houjuu_nue/wing.py | 2 +- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index dc52908..57e80c9 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -252,14 +252,19 @@ class ShoulderJoint(Model): parent_lip_ext: float = 40.0 parent_lip_guard_height: float = 8.0 - # Measured from centre of axle - child_lip_length: float = 45.0 + # Generates a child guard which covers up the internals. The lip length is + # relative to the +X surface of the guard. + child_guard_ext: float = 30.0 + child_guard_width: float = 25.0 + # guard length measured from axle + child_lip_length: float = 40.0 child_lip_width: float = 20.0 child_conn_hole_diam: float = 6.0 # Measured from centre of axle - child_conn_hole_pos: list[float] = field(default_factory=lambda: [25, 35]) + child_conn_hole_pos: list[float] = field(default_factory=lambda: [15, 25]) child_core_thickness: float = 3.0 + # Rotates the torsion joint to avoid collisions or for some other purpose axis_rotate_bot: float = 225.0 axis_rotate_top: float = -225.0 @@ -345,7 +350,6 @@ class ShoulderJoint(Model): """ joint = self.torsion_joint - assert all(r > joint.radius_rider for r in self.child_conn_hole_pos) assert all(r < self.child_lip_length for r in self.child_conn_hole_pos) # Half of the height of the bridging cylinder @@ -371,6 +375,30 @@ class ShoulderJoint(Model): .assemble() .circle(radius_core_inner, mode='s') ) + core_guard = ( + Cq.Workplane('XY') + .box( + length=self.child_guard_ext, + width=self.child_guard_width, + height=self.height, + centered=(False, True, True), + ) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + radius=self.radius, + height=self.height, + combine='cut', + centered=True, + ) + .copyWorkplane(Cq.Workplane('XY')) + .box( + length=self.child_guard_ext, + width=self.child_lip_width, + height=self.height - self.torsion_joint.total_height * 2, + combine='cut', + centered=(False, True, True), + ) + ) core = ( Cq.Workplane('XY') .placeSketch(core_profile1) @@ -381,6 +409,7 @@ class ShoulderJoint(Model): .toPending() .extrude(dh * 2) .translate(Cq.Vector(0, 0, -dh)) + .union(core_guard) ) assert self.child_lip_width / 2 <= joint.radius_rider lip_thickness = joint.rider_disk_height @@ -391,15 +420,6 @@ class ShoulderJoint(Model): hole_loc=self.child_conn_hole_pos, hole_diam=self.child_conn_hole_diam, ) - lip = ( - lip - .copyWorkplane(Cq.Workplane('XY')) - .cylinder( - radius=joint.radius_rider, - height=lip_thickness, - centered=(True, True, False), - combine='cut') - ) theta = self.torsion_joint.spring.angle_neutral - self.torsion_joint.rider_slot_span loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot + self.angle_neutral) @@ -412,9 +432,9 @@ class ShoulderJoint(Model): .add(joint.rider(rider_slot_begin=180), name="rider_bot", loc=loc_axis_rotate_bot * Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) .add(lip, name="lip_top", - loc=Cq.Location((0, 0, dh))) + loc=Cq.Location((self.child_guard_ext, 0, dh))) .add(lip, name="lip_bot", - loc=Cq.Location((0, 0, -dh)) * loc_rotate) + loc=Cq.Location((self.child_guard_ext, 0, -dh)) * loc_rotate) ) return result diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 03f0fd9..c5bd232 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -96,7 +96,7 @@ class WingProfile(Model): self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, self.shoulder_angle_bias) - + self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2 assert self.spacer_thickness == self.root_joint.child_mount_thickness @submodel(name="shoulder-joint") -- 2.44.1 From 39110d0785b7bc654ae18cb6665cfcc8148fb012 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 18:59:58 -0700 Subject: [PATCH 109/187] feat: Adjust shape to be closer to Nue left --- nhf/touhou/houjuu_nue/__init__.py | 4 +++- nhf/touhou/houjuu_nue/joints.py | 4 +++- nhf/touhou/houjuu_nue/wing.py | 7 ++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 3a414cf..f14ac2e 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -38,6 +38,7 @@ import nhf.touhou.houjuu_nue.wing as MW import nhf.touhou.houjuu_nue.trident as MT import nhf.touhou.houjuu_nue.joints as MJ import nhf.touhou.houjuu_nue.harness as MH +from nhf.parts.item import Item import nhf.utils @dataclass @@ -61,7 +62,7 @@ class Parameters(Model): )) wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL( name="l1", - wrist_angle=-45.0, + wrist_angle=-60.0, )) wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( name="l2", @@ -147,6 +148,7 @@ class Parameters(Model): "wing-depth": bbox.ylen, "wing-height": bbox.zlen, "wing-mass": a.total_mass(), + "items": Item.count(a), } diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 57e80c9..e5aa154 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -215,7 +215,7 @@ class ShoulderJoint(Model): mass=float('nan'), ) - height: float = 60.0 + height: float = 70.0 torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( radius_track=18, radius_rider=18, @@ -271,6 +271,7 @@ class ShoulderJoint(Model): directrix_id: int = 0 angle_neutral: float = 10.0 + angle_max_deflection: float = 80.0 def __post_init__(self): assert self.parent_lip_length * 2 < self.height @@ -440,6 +441,7 @@ class ShoulderJoint(Model): @assembly() def assembly(self, fastener_pos: float = 0.0, deflection: float = 0) -> Cq.Assembly: + assert deflection <= self.angle_max_deflection directrix = self.directrix_id mat = Material.RESIN_TRANSPERENT mat_spring = Material.STEEL_SPRING diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index c5bd232..6d9780b 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -615,9 +615,10 @@ class WingProfile(Model): .constrain("s0/base?conn2", "root/child?conn2", "Plane", param=0) ) if "shoulder" in parts: + angle = shoulder_deflection * self.shoulder_joint.angle_max_deflection result.add(self.shoulder_joint.assembly( fastener_pos=fastener_pos, - deflection=shoulder_deflection * 80), name="shoulder") + deflection=angle), name="shoulder") if "s0" in parts and "shoulder" in parts: ( result @@ -805,11 +806,11 @@ class WingR(WingProfile): @dataclass(kw_only=True) class WingL(WingProfile): - elbow_bot_loc: Cq.Location = Cq.Location.from2d(250.0, 110.0, 10.0) + elbow_bot_loc: Cq.Location = Cq.Location.from2d(260.0, 110.0, 0.0) elbow_height: float = 80.0 wrist_angle: float = -45.0 - wrist_bot_loc: Cq.Location = Cq.Location.from2d(480.0, 0.0, -45.0) + wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0) wrist_height: float = 43.0 shoulder_bezier_ext: float = 120.0 -- 2.44.1 From f5b048d0b98efa06c43ca675d677b73d0da5e776 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 21:00:10 -0700 Subject: [PATCH 110/187] feat: Add linear actuator component --- nhf/materials.py | 4 +- nhf/parts/fasteners.py | 7 +- nhf/parts/item.py | 6 +- nhf/parts/joints.py | 2 +- nhf/touhou/houjuu_nue/electronics.py | 155 +++++++++++++++++++++++++++ nhf/touhou/houjuu_nue/joints.py | 104 ++++++++++-------- 6 files changed, 223 insertions(+), 55 deletions(-) create mode 100644 nhf/touhou/houjuu_nue/electronics.py diff --git a/nhf/materials.py b/nhf/materials.py index af336da..bc172c3 100644 --- a/nhf/materials.py +++ b/nhf/materials.py @@ -31,6 +31,7 @@ class Role(Flag): STRUCTURE = auto() DECORATION = auto() ELECTRONIC = auto() + MOTION = auto() # Fasteners, etc. CONNECTION = auto() @@ -61,7 +62,8 @@ ROLE_COLOR_MAP = { Role.DAMPING: _color('springgreen', 1.0), Role.STRUCTURE: _color('gray', 0.4), Role.DECORATION: _color('lightseagreen', 0.4), - Role.ELECTRONIC: _color('mediumorchid', 0.5), + Role.ELECTRONIC: _color('mediumorchid', 0.7), + Role.MOTION: _color('thistle3', 0.7), Role.CONNECTION: _color('steelblue3', 0.8), Role.HANDLE: _color('tomato4', 0.8), } diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index c9c9e61..38c8f84 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -42,10 +42,7 @@ class FlatHeadBolt(Item): @dataclass(frozen=True) class ThreaddedKnob(Item): """ - Sourced from: - - > Othmro Black 12mm(M12) x 50mm Thread Replacement Star Hand Knob Tightening - > Screws + A threaded rod with knob on one side """ diam_thread: float height_thread: float @@ -104,7 +101,7 @@ class HexNut(Item): return f"HexNut M{int(self.diam_thread)}-{self.pitch}" @property - def role(self): + def role(self) -> Role: return Role.CONNECTION def generate(self) -> Cq.Workplane: diff --git a/nhf/parts/item.py b/nhf/parts/item.py index 1e321fc..e7a153e 100644 --- a/nhf/parts/item.py +++ b/nhf/parts/item.py @@ -44,9 +44,9 @@ class Item: a = self.generate(**kwargs) if isinstance(a, Cq.Workplane): a = Cq.Assembly(a) - if role := self.role: - a.metadata[KEY_ROLE] = role - a.color = role.color_avg() + 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 diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index 31bb686..e0e0357 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -393,7 +393,7 @@ class TorsionJoint: def rider_track_assembly(self, directrix: int = 0, deflection: float = 0): rider = self.rider() track = self.track() - spring = self.spring.generate(deflection=deflection) + spring = self.spring.assembly(deflection=deflection) result = ( Cq.Assembly() .addS(spring, name="spring", role=Role.DAMPING) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py new file mode 100644 index 0000000..5e4f690 --- /dev/null +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -0,0 +1,155 @@ +""" +Electronic components +""" +from dataclasses import dataclass +import cadquery as Cq +from nhf.materials import Role +from nhf.parts.item import Item +from nhf.parts.fasteners import FlatHeadBolt, HexNut + +@dataclass(frozen=True) +class LinearActuator(Item): + stroke_length: float + shaft_diam: float = 9.04 + + front_hole_ext: float = 4.41 + front_hole_diam: float = 4.41 + front_length: float = 9.55 + front_width: float = 9.24 + front_height: float = 5.98 + + segment1_length: float = 37.55 + segment1_width: float = 15.95 + segment1_height: float = 11.94 + + segment2_length: float = 37.47 + segment2_width: float = 20.03 + segment2_height: float = 15.03 + + back_hole_ext: float = 4.58 + back_hole_diam: float = 4.18 + back_length: float = 9.27 + back_width: float = 10.16 + back_height: float = 8.12 + + @property + def name(self) -> str: + return f"LinearActuator {self.stroke_length}mm" + + @property + def role(self) -> Role: + return Role.MOTION + + @property + def conn_length(self): + return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext + + def generate(self, pos: float=0) -> Cq.Assembly: + stroke_x = pos * self.stroke_length + front = ( + Cq.Workplane('XZ') + .cylinder( + radius=self.front_width / 2, + height=self.front_height, + centered=True, + ) + .box( + length=self.front_hole_ext, + width=self.front_width, + height=self.front_height, + combine=True, + centered=(False, True, True) + ) + .copyWorkplane(Cq.Workplane('XZ')) + .cylinder( + radius=self.front_hole_diam / 2, + height=self.front_height, + centered=True, + combine='cut', + ) + ) + if stroke_x > 0: + shaft = ( + Cq.Workplane('YZ') + .cylinder( + radius=self.shaft_diam / 2, + height=stroke_x, + centered=(True, True, False) + ) + ) + else: + shaft = None + segment1 = ( + Cq.Workplane() + .box( + length=self.segment1_length, + height=self.segment1_width, + width=self.segment1_height, + centered=(False, True, True), + ) + ) + segment2 = ( + Cq.Workplane() + .box( + length=self.segment2_length, + height=self.segment2_width, + width=self.segment2_height, + centered=(False, True, True), + ) + ) + back = ( + Cq.Workplane('XZ') + .cylinder( + radius=self.back_width / 2, + height=self.back_height, + centered=True, + ) + .box( + length=self.back_hole_ext, + width=self.back_width, + height=self.back_height, + combine=True, + centered=(False, True, True) + ) + .copyWorkplane(Cq.Workplane('XZ')) + .cylinder( + radius=self.back_hole_diam / 2, + height=self.back_height, + centered=True, + combine='cut', + ) + ) + result = ( + Cq.Assembly() + .add(front, name="front", + loc=Cq.Location((-self.front_hole_ext, 0, 0))) + .add(segment1, name="segment1", + loc=Cq.Location((stroke_x, 0, 0))) + .add(segment2, name="segment2", + loc=Cq.Location((stroke_x + self.segment1_length, 0, 0))) + .add(back, name="back", + loc=Cq.Location((stroke_x + self.segment1_length + self.segment2_length + self.back_hole_ext, 0, 0), (0, 1, 0), 180)) + ) + if shaft: + result.add(shaft, name="shaft") + return result + + +LINEAR_ACTUATOR_SHOULDER = LinearActuator( + mass=34.0, + stroke_length=30, +) +LINEAR_ACTUATOR_HEX_NUT = HexNut( + mass=0.8, + diam_thread=4, + pitch=0.7, + thickness=4.16, + width=6.79, +) +LINEAR_ACTUATOR_BOLT = FlatHeadBolt( + mass=1.7, + diam_head=16.68, + height_head=2.98, + diam_thread=4.0, + height_thread=15.83, +) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index e5aa154..04df3f9 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -12,30 +12,64 @@ import nhf.utils TOL = 1e-6 +# Parts used + +# uxcell 2 Pcs Star Knobs Grips M12 x 30mm Male Thread Steel Zinc Stud Replacement PP +HS_JOINT_KNOB = ThreaddedKnob( + mass=float('nan'), + diam_thread=12.0, + height_thread=30.0, + diam_knob=50.0, + # FIXME: Undetermined + diam_neck=30.0, + height_neck=10.0, + height_knob=10.0, +) +# Tom's world 8Pcs M12-1.75 Hex Nut Assortment Set Stainless Steel 304(18-8) +# Metric Hexagon Nut for Bolts, Bright Finish, Full Thread (M12) +HS_JOINT_HEX_NUT = HexNut( + mass=14.9, + diam_thread=12.0, + pitch=1.75, + thickness=9.7, + width=18.9, +) +SHOULDER_AXIS_BOLT = FlatHeadBolt( + # FIXME: measure + diam_head=10.0, + height_head=3.0, + diam_thread=6.0, + height_thread=20.0, + mass=float('nan'), +) +# Hoypeyfiy 10 Pieces Torsion Spring Woodworking DIY 90 Degrees Torsional +# Springs Repair Maintenance Spring +SHOULDER_TORSION_SPRING = TorsionSpring( + mass=2.2, + # inner diameter = 9 + radius=9/2 + 1.2, + thickness=1.3, + height=7.5, +) +# KALIONE 10 Pieces Torsion Spring, Stainless Steel Small Torsion Springs, Tiny +# Torsional Spring, 90° Deflection Compression Spring Kit for Repair Tools +# Woodworking DIY, 50mm +ELBOW_TORSION_SPRING = TorsionSpring( + mass=1.7, + radius=9 / 2, + thickness=1.3, + height=6.5, + tail_length=45.0, + right_handed=False, +) + @dataclass class RootJoint(Model): """ The Houjuu-Scarlett Mechanism """ - knob: ThreaddedKnob = ThreaddedKnob( - mass=float('nan'), - diam_thread=12.0, - height_thread=30.0, - diam_knob=50.0, - # FIXME: Undetermined - diam_neck=30.0, - height_neck=10.0, - height_knob=10.0, - ) - hex_nut: HexNut = HexNut( - # FIXME: Undetermined - mass=float('nan'), - - diam_thread=12.0, - pitch=1.75, - thickness=9.8, - width=18.9, - ) + knob: ThreaddedKnob = HS_JOINT_KNOB + hex_nut: HexNut = HS_JOINT_HEX_NUT hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=25.0, radius_inner=15.0, @@ -206,14 +240,7 @@ class RootJoint(Model): @dataclass class ShoulderJoint(Model): - bolt: FlatHeadBolt = FlatHeadBolt( - # FIXME: measure - diam_head=10.0, - height_head=3.0, - diam_thread=6.0, - height_thread=20.0, - mass=float('nan'), - ) + bolt: FlatHeadBolt = SHOULDER_AXIS_BOLT height: float = 70.0 torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint( @@ -225,13 +252,7 @@ class ShoulderJoint(Model): track_disk_height=5.0, rider_disk_height=5.0, radius_axle=3.0, - spring=TorsionSpring( - mass=float('nan'), - # inner diameter = 9 - radius=9/2 + 1.2, - thickness=1.3, - height=7.5, - ), + spring=SHOULDER_TORSION_SPRING, rider_slot_begin=0, rider_n_slots=1, rider_slot_span=0, @@ -451,12 +472,12 @@ class ShoulderJoint(Model): .addS(self.child(), name="child", role=Role.CHILD, material=mat) .constrain("child/core", "Fixed") - .addS(self.torsion_joint.spring.generate(deflection=-deflection), name="spring_top", + .addS(self.torsion_joint.spring.assembly(deflection=-deflection), name="spring_top", role=Role.DAMPING, material=mat_spring) .addS(self.parent_top(), name="parent_top", role=Role.PARENT, material=mat) - .addS(self.torsion_joint.spring.generate(deflection=deflection), name="spring_bot", + .addS(self.torsion_joint.spring.assembly(deflection=deflection), name="spring_bot", role=Role.DAMPING, material=mat_spring) .addS(self.parent_bot(), name="parent_bot", @@ -558,14 +579,7 @@ class DiskJoint(Model): We embed a spring inside the joint, with one leg in the disk and one leg in the housing. This provides torsion resistance. """ - spring: TorsionSpring = field(default_factory=lambda: TorsionSpring( - mass=float('nan'), - radius=9 / 2, - thickness=1.3, - height=6.5, - tail_length=45.0, - right_handed=False, - )) + spring: TorsionSpring = ELBOW_TORSION_SPRING radius_housing: float = 22.0 radius_disk: float = 20.0 @@ -787,7 +801,7 @@ class DiskJoint(Model): ( assembly .addS( - self.spring.generate(deflection=-deflection), + self.spring.assembly(deflection=-deflection), name=spring_name, role=Role.DAMPING, material=Material.STEEL_SPRING) -- 2.44.1 From d3a6f1e1c534dff16a9f70eb6ebd1a26e949305d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 22:29:57 -0700 Subject: [PATCH 111/187] feat: Cut polygons to remove joint conflict --- nhf/touhou/houjuu_nue/joints.py | 6 ++- nhf/touhou/houjuu_nue/wing.py | 65 ++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 04df3f9..c368d60 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -95,7 +95,7 @@ class RootJoint(Model): child_height: float = 60.0 child_width: float = 50.0 - child_mount_thickness: float = 25.4 / 8 + child_mount_thickness: float = 25.4 / 4 def corner_pos(self) -> list[tuple[int, int]]: """ @@ -888,6 +888,10 @@ class ElbowJoint(Model): def total_thickness(self): return self.disk_joint.total_thickness + @property + def motion_span(self) -> float: + return self.disk_joint.movement_angle + def parent_arm_loc(self) -> Cq.Location: """ 2d Location of the centre of the arm surface on the parent side, assuming diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 6d9780b..27c4c6e 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -24,7 +24,9 @@ class WingProfile(Model): root_joint: RootJoint = field(default_factory=lambda: RootJoint()) panel_thickness: float = 25.4 / 16 - spacer_thickness: float = 25.4 / 8 + # 1/4" acrylic for the spacer. Anything thinner would threathen structural + # strength + spacer_thickness: float = 25.4 / 4 shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( )) @@ -325,9 +327,52 @@ class WingProfile(Model): ### s1, s2, s3 ### def profile(self) -> Cq.Sketch: """ - Generates profile from shoulder and above + Generates profile from shoulder and above. Subclass should implement """ + def _elbow_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch: + """ + Creates a cutting polygon for removing the contraction part of a joint + """ + theta = math.radians(self.elbow_joint.motion_span) + h = self.elbow_height + dx = h * math.tan(theta / 2) + dy = h + sign = -1 if self.flip else 1 + points = [ + (0, 0), + (dx, sign * dy), + (-dx, sign * dy), + ] + return ( + Cq.Sketch() + .polygon([ + (loc * Cq.Location.from2d(*p)).to2d_pos() + for p in points + ]) + ) + def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch: + """ + Creates a cutting polygon for removing the contraction part of a joint + """ + theta = math.radians(self.wrist_joint.motion_span) + dx = self.wrist_height * math.tan(theta) + dy = self.wrist_height + sign = -1 if self.flip else 1 + points = [ + (0, 0), + (0, -sign * dy), + (-dx, -sign * dy), + ] + return ( + Cq.Sketch() + .polygon([ + (loc * Cq.Location.from2d(*p)).to2d_pos() + for p in points + ]) + ) + + def _assembly_insert_spacer( self, a: Cq.Assembly, @@ -374,8 +419,7 @@ class WingProfile(Model): self, joint: ElbowJoint, segment_thickness: float, - dx: float, - bot=False) -> MountingBox: + dx: float) -> MountingBox: length = joint.lip_length / 2 - dx holes = [ Hole(x - dx) @@ -398,6 +442,9 @@ class WingProfile(Model): self.profile() .reset() .polygon(self._mask_elbow(), mode='i') + .reset() + .push([self.elbow_axle_loc]) + .each(self._elbow_joint_retract_cut_polygon, mode='s') ) return profile def surface_s1(self, front: bool = True) -> Cq.Workplane: @@ -471,6 +518,12 @@ class WingProfile(Model): .polygon(self._mask_elbow(), mode='s') .reset() .polygon(self._mask_wrist(), mode='i') + .reset() + .push([self.elbow_axle_loc]) + .each(self._elbow_joint_retract_cut_polygon, mode='s') + .reset() + .push([self.wrist_axle_loc]) + .each(self._wrist_joint_retract_cut_polygon, mode='s') ) return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: @@ -638,7 +691,7 @@ class WingProfile(Model): .constrain("s1/shoulder_bot?conn1", f"shoulder/child/lip_{tag_bot}?conn1", "Plane") ) if "elbow" in parts: - angle = self.elbow_joint.disk_joint.movement_angle * elbow_wrist_deflection + angle = self.elbow_joint.motion_span * elbow_wrist_deflection result.add(self.elbow_joint.assembly(angle=angle), name="elbow") if "s1" in parts and "elbow" in parts: ( @@ -659,7 +712,7 @@ class WingProfile(Model): .constrain("s2/elbow_bot?conn1", f"elbow/child/lip?conn_{tag_bot}1", "Plane") ) if "wrist" in parts: - angle = self.wrist_joint.disk_joint.movement_angle * elbow_wrist_deflection + angle = self.wrist_joint.motion_span * elbow_wrist_deflection result.add(self.wrist_joint.assembly(angle=angle), name="wrist") if "s2" in parts and "wrist" in parts: # Mounted backwards to bend in other direction -- 2.44.1 From f75375e38455448872ef0f771e785828319868f7 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 19 Jul 2024 23:49:38 -0700 Subject: [PATCH 112/187] feat: Nue right side blade fix: `Cq.Location.to2d_rot()` signature --- nhf/build.py | 17 +++-- nhf/touhou/houjuu_nue/joints.py | 4 +- nhf/touhou/houjuu_nue/wing.py | 113 +++++++++++++++++++++++++++++--- nhf/utils.py | 2 +- 4 files changed, 119 insertions(+), 17 deletions(-) diff --git a/nhf/build.py b/nhf/build.py index 3db9c36..106577c 100644 --- a/nhf/build.py +++ b/nhf/build.py @@ -69,8 +69,13 @@ class Target: else: return None - def write_to(self, obj, path: str): + def write_to(self, obj, path: str) -> bool: + """ + Returns false if target is `None` + """ x = self._method(obj) + if x is None: + return False if self.kind == TargetKind.STL: assert isinstance(x, Union[ Cq.Workplane, Cq.Shape, Cq.Compound, Cq.Assembly]) @@ -92,6 +97,7 @@ class Target: Cq.exporters.exportDXF(x, path, **self.kwargs) else: assert False, f"Invalid kind: {self.kind}" + return True @classmethod def methods(cls, subject): @@ -257,8 +263,9 @@ class Model: """ total = 0 for t in Target.methods(self).values(): - t(self) - total += 1 + result = t(self) + if result: + total += 1 for t in Assembly.methods(self).values(): t.check(self) total += 1 @@ -288,8 +295,8 @@ class Model: print(f"{Fore.BLUE}Building{Style.RESET_ALL} {output_file}") try: - t.write_to(self, str(output_file)) - if verbose >= 1: + flag = t.write_to(self, str(output_file)) + if flag and verbose >= 1: 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}") diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index c368d60..5a79461 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -16,7 +16,7 @@ TOL = 1e-6 # uxcell 2 Pcs Star Knobs Grips M12 x 30mm Male Thread Steel Zinc Stud Replacement PP HS_JOINT_KNOB = ThreaddedKnob( - mass=float('nan'), + mass=0.0, # FIXME: Measure diam_thread=12.0, height_thread=30.0, diam_knob=50.0, @@ -36,11 +36,11 @@ HS_JOINT_HEX_NUT = HexNut( ) SHOULDER_AXIS_BOLT = FlatHeadBolt( # FIXME: measure + mass=0.0, diam_head=10.0, height_head=3.0, diam_thread=6.0, height_thread=20.0, - mass=float('nan'), ) # Hoypeyfiy 10 Pieces Torsion Spring Woodworking DIY 90 Degrees Torsional # Springs Repair Maintenance Spring diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 27c4c6e..82ed569 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -62,7 +62,7 @@ class WingProfile(Model): child_arm_radius=23.0, parent_arm_radius=30.0, hole_diam=4.0, - angle_neutral=30.0, + angle_neutral=-20.0, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -80,10 +80,10 @@ class WingProfile(Model): wrist_bot_loc: Cq.Location wrist_height: float elbow_rotate: float = -5.0 - wrist_rotate: float = 30.0 + wrist_rotate: float = -20.0 # False for the right side, True for the left side - flip: bool = False + flip: bool def __post_init__(self): super().__init__(name=self.name) @@ -91,7 +91,10 @@ class WingProfile(Model): self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2) - self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) + if self.flip: + self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) + else: + self.wrist_axle_loc = self.wrist_bot_loc assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness) assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) @@ -329,6 +332,12 @@ class WingProfile(Model): """ Generates profile from shoulder and above. Subclass should implement """ + @target(name="profile-s3-extra", kind=TargetKind.DXF) + def profile_s3_extra(self) -> Optional[Cq.Sketch]: + """ + Extra element to be glued on s3. Not needed for left side + """ + return None def _elbow_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch: """ @@ -351,10 +360,15 @@ class WingProfile(Model): for p in points ]) ) - def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch: + def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Optional[Cq.Sketch]: """ Creates a cutting polygon for removing the contraction part of a joint """ + if not self.flip: + """ + No cutting needed on RHS + """ + return None theta = math.radians(self.wrist_joint.motion_span) dx = self.wrist_height * math.tan(theta) dy = self.wrist_height @@ -521,10 +535,14 @@ class WingProfile(Model): .reset() .push([self.elbow_axle_loc]) .each(self._elbow_joint_retract_cut_polygon, mode='s') - .reset() - .push([self.wrist_axle_loc]) - .each(self._wrist_joint_retract_cut_polygon, mode='s') ) + if self.flip: + profile = ( + profile + .reset() + .push([self.wrist_axle_loc]) + .each(self._wrist_joint_retract_cut_polygon, mode='s') + ) return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: h = self.elbow_height / 2 @@ -595,7 +613,6 @@ class WingProfile(Model): return profile def surface_s3(self, front: bool = True) -> Cq.Workplane: - h = self.wrist_height / 2 loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip) tags = [ ("wrist_bot", self.wrist_axle_loc * loc_wrist * @@ -605,6 +622,19 @@ class WingProfile(Model): ] profile = self.profile_s3() return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + def surface_s3_extra(self, + front: bool = True) -> Optional[Cq.Workplane]: + profile = self.profile_s3_extra() + if profile is None: + return None + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip) + tags = [ + ("wrist_bot", self.wrist_axle_loc * loc_wrist * + Cq.Location.from2d(0, self.wrist_h2)), + ("wrist_top", self.wrist_axle_loc * loc_wrist * + Cq.Location.from2d(0, -self.wrist_h2)), + ] + return extrude_with_markers(profile, self.panel_thickness, tags, reverse=not front) @submodel(name="spacer-s3-wrist") def spacer_s3_wrist(self) -> MountingBox: return self.spacer_of_joint( @@ -624,6 +654,18 @@ class WingProfile(Model): .constrain("front@faces@>Z", "back@faces@ self.ring_radius_inner + assert 0 > self.blade_overlap_angle > self.arrow_angle + assert 0 < self.blade_hole_angle < self.blade_angle + assert self.blade_wrist_approx_tangent_angle <= self.wrist_bot_loc.to2d_rot() + @property def ring_radius(self) -> float: (dx, dy), _ = self.ring_rel_loc.to2d() @@ -829,6 +887,43 @@ class WingR(WingProfile): ) return result + def profile_s3_extra(self) -> Cq.Sketch: + """ + Implements the blade part on Nue's wing + """ + left_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(-1) + hole_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_hole_angle) + right_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_angle) + h_loc = Cq.Location.from2d(0, self.arrow_height + self.blade_overlap_arrow_height) + + # Law of sines, uses the triangle of (wrist_bot_loc, arrow_bot_loc, ?) + theta_wp = math.radians(90 - self.blade_wrist_approx_tangent_angle) + theta_b = math.radians(self.blade_angle) + h_blade = math.sin(theta_wp) / math.sin(math.pi - theta_b - theta_wp) * self.arrow_height + h_blade_loc = Cq.Location.from2d(0, h_blade) + return ( + Cq.Sketch() + .segment( + self.arrow_bot_loc.to2d_pos(), + (left_bot_loc * h_loc).to2d_pos(), + ) + .segment( + (self.arrow_bot_loc * h_loc).to2d_pos(), + ) + .segment( + (right_bot_loc * h_blade_loc).to2d_pos(), + ) + .close() + .assemble() + .reset() + .push([ + (hole_bot_loc * Cq.Location.from2d(0, h)).to2d_pos() + for h in self.blade_hole_heights + ]) + .circle(self.blade_hole_diam / 2, mode='s') + ) + + def _mask_elbow(self) -> list[Tuple[float, float]]: l = 200 elbow_x, _ = self.elbow_bot_loc.to2d_pos() diff --git a/nhf/utils.py b/nhf/utils.py index e999e85..3399fce 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -72,7 +72,7 @@ def to2d_pos(self: Cq.Location) -> Tuple[float, float]: return x, y Cq.Location.to2d_pos = to2d_pos -def to2d_rot(self: Cq.Location) -> Tuple[float, float]: +def to2d_rot(self: Cq.Location) -> float: """ Returns position and angle """ -- 2.44.1 From 82d8cf9599a4dc578cb69f09b204521c693aee9c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 20 Jul 2024 22:55:43 -0700 Subject: [PATCH 113/187] feat: Extension profile on both sides --- nhf/touhou/houjuu_nue/wing.py | 115 ++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 82ed569..548e1d7 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -62,7 +62,7 @@ class WingProfile(Model): child_arm_radius=23.0, parent_arm_radius=30.0, hole_diam=4.0, - angle_neutral=-20.0, + angle_neutral=-30.0, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -80,7 +80,7 @@ class WingProfile(Model): wrist_bot_loc: Cq.Location wrist_height: float elbow_rotate: float = -5.0 - wrist_rotate: float = -20.0 + wrist_rotate: float = -30.0 # False for the right side, True for the left side flip: bool @@ -385,6 +385,36 @@ class WingProfile(Model): for p in points ]) ) + def _joint_extension_profile( + self, + axle_loc: Cq.Location, + radius: float, + angle_span: float, + bot: bool = False) -> Cq.Sketch: + sign = -1 if bot else 1 + #sign = -1 + axle_loc = axle_loc * Cq.Location.rot2d(180 if bot else 0) + loc_h = Cq.Location.from2d(0, radius) + start = axle_loc * loc_h + mid = axle_loc * Cq.Location.rot2d(-sign * angle_span/2) * loc_h + end = axle_loc * Cq.Location.rot2d(-sign * angle_span) * loc_h + return ( + Cq.Sketch() + .segment( + axle_loc.to2d_pos(), + start.to2d_pos(), + ) + .arc( + start.to2d_pos(), + mid.to2d_pos(), + end.to2d_pos(), + ) + .segment( + end.to2d_pos(), + axle_loc.to2d_pos(), + ) + .assemble() + ) def _assembly_insert_spacer( @@ -480,6 +510,35 @@ class WingProfile(Model): tags = tags_shoulder + tags_elbow return extrude_with_markers( profile, self.panel_thickness, tags, reverse=front) + @target(name="profile-s1-bridge", kind=TargetKind.DXF) + def profile_s1_bridge(self) -> Cq.Workplane: + return ( + self.profile() + #.reset() + #.polygon(self._mask_elbow(), mode='i') + .reset() + .push([self.elbow_axle_loc]) + .each(self._elbow_joint_retract_cut_polygon, mode='i') + .reset() + .push([self.elbow_axle_loc]) + .each(lambda loc: self._joint_extension_profile( + axle_loc=self.elbow_axle_loc, + radius=self.elbow_height / 2, + angle_span=self.elbow_joint.motion_span, + bot=True, + ), mode='a') + ) + def surface_s1_bridge(self, front: bool = True) -> Cq.Workplane: + profile = self.profile_s1_bridge() + loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.parent_arm_loc() + tags = [ + ("elbow_bot", self.elbow_axle_loc * loc_elbow * + Cq.Location.from2d(0, -self.elbow_h2)), + ("elbow_top", self.elbow_axle_loc * loc_elbow * + Cq.Location.from2d(0, self.elbow_h2)), + ] + return extrude_with_markers( + profile, self.panel_thickness, tags, reverse=not front) @submodel(name="spacer-s1-shoulder") def spacer_s1_shoulder(self) -> MountingBox: holes = [ @@ -511,6 +570,14 @@ class WingProfile(Model): material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Workplane: - h = self.elbow_height / 2 loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc(flip=self.flip) tags_elbow = [ ("elbow_bot", self.elbow_axle_loc * loc_elbow * @@ -553,7 +619,6 @@ class WingProfile(Model): ("elbow_top", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, -self.elbow_h2)), ] - h = self.wrist_height / 2 loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc() tags_wrist = [ ("wrist_bot", self.wrist_axle_loc * loc_wrist * @@ -564,6 +629,40 @@ class WingProfile(Model): profile = self.profile_s2() tags = tags_elbow + tags_wrist return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) + @target(name="profile-s2-bridge", kind=TargetKind.DXF) + def profile_s2_bridge(self) -> Cq.Workplane: + # Generates the extension profile, which is required on both sides + profile = self._joint_extension_profile( + axle_loc=self.wrist_axle_loc, + radius=self.wrist_height * (0.5 if self.flip else 1), + angle_span=self.wrist_joint.motion_span, + bot=False, + ) + # Generates the contraction (cut) profile. only required on the left + if self.flip: + extra = ( + self.profile() + .reset() + .push([self.wrist_axle_loc]) + .each(self._wrist_joint_retract_cut_polygon, mode='i') + ) + profile = ( + profile + .push([self.wrist_axle_loc]) + .each(lambda _: extra, mode='a') + ) + return profile + def surface_s2_bridge(self, front: bool = True) -> Cq.Workplane: + profile = self.profile_s2_bridge() + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc() + tags = [ + ("wrist_bot", self.wrist_axle_loc * loc_wrist * + Cq.Location.from2d(0, -self.wrist_h2)), + ("wrist_top", self.wrist_axle_loc * loc_wrist * + Cq.Location.from2d(0, self.wrist_h2)), + ] + return extrude_with_markers( + profile, self.panel_thickness, tags, reverse=not front) @submodel(name="spacer-s2-elbow") def spacer_s2_elbow(self) -> MountingBox: return self.spacer_of_joint( @@ -589,6 +688,14 @@ class WingProfile(Model): material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Date: Sat, 20 Jul 2024 23:08:32 -0700 Subject: [PATCH 114/187] fix: Extension profiles for the left side --- nhf/touhou/houjuu_nue/wing.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 548e1d7..41b3300 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -391,10 +391,12 @@ class WingProfile(Model): radius: float, angle_span: float, bot: bool = False) -> Cq.Sketch: + """ + Creates a sector profile which accomodates extension + """ sign = -1 if bot else 1 - #sign = -1 - axle_loc = axle_loc * Cq.Location.rot2d(180 if bot else 0) - loc_h = Cq.Location.from2d(0, radius) + axle_loc = axle_loc * Cq.Location.rot2d(-90 if bot else 90) + loc_h = Cq.Location.from2d(radius, 0) start = axle_loc * loc_h mid = axle_loc * Cq.Location.rot2d(-sign * angle_span/2) * loc_h end = axle_loc * Cq.Location.rot2d(-sign * angle_span) * loc_h @@ -525,7 +527,7 @@ class WingProfile(Model): axle_loc=self.elbow_axle_loc, radius=self.elbow_height / 2, angle_span=self.elbow_joint.motion_span, - bot=True, + bot=not self.flip, ), mode='a') ) def surface_s1_bridge(self, front: bool = True) -> Cq.Workplane: @@ -636,7 +638,7 @@ class WingProfile(Model): axle_loc=self.wrist_axle_loc, radius=self.wrist_height * (0.5 if self.flip else 1), angle_span=self.wrist_joint.motion_span, - bot=False, + bot=self.flip, ) # Generates the contraction (cut) profile. only required on the left if self.flip: -- 2.44.1 From a47f56d41e20b204c4a3cd427e3393080db6730c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 20 Jul 2024 23:11:42 -0700 Subject: [PATCH 115/187] feat: Measurements for knob --- nhf/touhou/houjuu_nue/joints.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 5a79461..df67ece 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -16,14 +16,13 @@ TOL = 1e-6 # uxcell 2 Pcs Star Knobs Grips M12 x 30mm Male Thread Steel Zinc Stud Replacement PP HS_JOINT_KNOB = ThreaddedKnob( - mass=0.0, # FIXME: Measure + mass=77.3, diam_thread=12.0, height_thread=30.0, diam_knob=50.0, - # FIXME: Undetermined - diam_neck=30.0, - height_neck=10.0, - height_knob=10.0, + diam_neck=25.0, + height_neck=12.5, + height_knob=15.0, ) # Tom's world 8Pcs M12-1.75 Hex Nut Assortment Set Stainless Steel 304(18-8) # Metric Hexagon Nut for Bolts, Bright Finish, Full Thread (M12) -- 2.44.1 From 3ad17f0c3e5522cbd8917b35d571e7c4018c4d31 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 20 Jul 2024 23:52:11 -0700 Subject: [PATCH 116/187] fix: get_abs_location partial --- nhf/test.py | 17 +++++++++++++++++ nhf/touhou/houjuu_nue/test.py | 2 +- nhf/utils.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/nhf/test.py b/nhf/test.py index ebef77c..7066a9a 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -163,5 +163,22 @@ class TestUtils(unittest.TestCase): self.assertAlmostEqual(bbox.ylen, 15) self.assertAlmostEqual(bbox.zlen, 5) + def test_abs_location(self): + box = Cq.Solid.makeBox(1, 1, 1) + assembly = ( + Cq.Assembly() + .add(box, name="b1") + .add(box, name="b2", loc=Cq.Location((0,0,1))) + .add(box, name="b3", loc=Cq.Location((0,0,2))) + ) + (x, y, z), _ = assembly.get_abs_location("b2@faces@>Y").toTuple() + self.assertAlmostEqual(x, 0.5) + self.assertAlmostEqual(y, 1) + self.assertAlmostEqual(z, 1.5) + (rx, ry, rz), _ = assembly.get_abs_direction("b2@faces@>Y").toTuple() + self.assertAlmostEqual(rx, 0) + self.assertAlmostEqual(ry, 1) + self.assertAlmostEqual(rz, 0) + if __name__ == '__main__': unittest.main() diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 9dbb41b..f67c0a2 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -30,7 +30,7 @@ class TestJoints(unittest.TestCase): Tests the arm radius """ j = MJ.ShoulderJoint() - for deflection in [0, 40, 95, 120]: + for deflection in [0, 40, j.angle_max_deflection]: with self.subTest(deflection=deflection): a = j.assembly(deflection=deflection) # Axle diff --git a/nhf/utils.py b/nhf/utils.py index 3399fce..876f891 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -1,9 +1,10 @@ """ Utility functions for cadquery objects """ -import math import functools +import math import cadquery as Cq +from cadquery.occ_impl.solver import ConstraintSpec from nhf import Role from typing import Union, Tuple, cast from nhf.materials import KEY_ITEM, KEY_MATERIAL @@ -198,14 +199,40 @@ Cq.Assembly.markPlane = mark_plane def get_abs_location(self: Cq.Assembly, tag: str) -> Cq.Location: + """ + Gets the location of a tag + + BUG: Currently bugged. See `nhf/test.py` for example + """ name, shape = self._query(tag) - loc_self = shape.location() + loc_self = Cq.Location(shape.Center()) loc_parent, _ = self._subloc(name) loc = loc_parent * loc_self return loc Cq.Assembly.get_abs_location = get_abs_location +def get_abs_direction(self: Cq.Assembly, + tag: str) -> Cq.Location: + """ + Gets the location of a tag + """ + name, shape = self._query(tag) + # Must match `cadquery.occ_impl.solver.ConstraintSpec._getAxis` + if isinstance(shape, Cq.Face): + vec_dir = shape.normalAt() + elif isinstance(shape, Cq.Edge) and shape.geomType() != "CIRCLE": + vec_dir = shape.tangentAt() + elif isinstance(shape, Cq.Edge) and shape.geomType() == "CIRCLE": + vec_dir = shape.normal() + else: + raise ValueError(f"Cannot construct Axis for {shape}") + loc_self = Cq.Location(vec_dir) + loc_parent, _ = self._subloc(name) + loc = loc_parent * loc_self + return loc +Cq.Assembly.get_abs_direction = get_abs_direction + # Tallying functions -- 2.44.1 From e23cb5cc47a7da1fa522ad7b8401c9306661e7c6 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 00:04:59 -0700 Subject: [PATCH 117/187] feat: Centre of mass --- nhf/parts/item.py | 4 ++-- nhf/test.py | 27 +++++++++++++++++++++++++++ nhf/utils.py | 32 +++++++++++++++++++++++++++----- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/nhf/parts/item.py b/nhf/parts/item.py index e7a153e..3fd5450 100644 --- a/nhf/parts/item.py +++ b/nhf/parts/item.py @@ -31,7 +31,7 @@ class Item: def role(self) -> Optional[Role]: return None - def generate(self, **kwargs) -> Union[Cq.Assembly, Cq.Workplane]: + def generate(self, **kwargs) -> Union[Cq.Solid, Cq.Assembly, Cq.Workplane]: """ Creates an assembly for this item. Subclass should implement this """ @@ -42,7 +42,7 @@ class Item: Interface for creating assembly with the necessary metadata """ a = self.generate(**kwargs) - if isinstance(a, Cq.Workplane): + if isinstance(a, Cq.Workplane) or isinstance(a, Cq.Solid): a = Cq.Assembly(a) if role := self.role: a.metadata[KEY_ROLE] = role diff --git a/nhf/test.py b/nhf/test.py index 7066a9a..b2dbb45 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -1,9 +1,11 @@ """ Unit tests for tooling """ +from dataclasses import dataclass import unittest import cadquery as Cq from nhf.build import Model, target +from nhf.parts.item import Item import nhf.checks import nhf.utils @@ -17,6 +19,20 @@ def makeSphere(r: float) -> Cq.Solid: """ return Cq.Solid.makeSphere(r, angleDegrees1=-90) +@dataclass(frozen=True) +class MassBall(Item): + """ + A ball with fixed mass + """ + radius: float = 0.2 + + @property + def name(self) -> str: + return f"MassBall {self.mass}" + def generate(self) -> Cq.Solid: + return makeSphere(self.radius) + + class BuildScaffold(Model): def __init__(self): @@ -180,5 +196,16 @@ class TestUtils(unittest.TestCase): self.assertAlmostEqual(ry, 1) self.assertAlmostEqual(rz, 0) + def test_centre_of_mass(self): + assembly = ( + Cq.Assembly() + .add(MassBall(mass=3).assembly(), name="s1", loc=Cq.Location((0, 0, 0))) + .add(MassBall(mass=7).assembly(), name="s2", loc=Cq.Location((0, 0, 10))) + ) + com = assembly.centre_of_mass() + self.assertAlmostEqual(com.x, 0) + self.assertAlmostEqual(com.y, 0) + self.assertAlmostEqual(com.z, 7) + if __name__ == '__main__': unittest.main() diff --git a/nhf/utils.py b/nhf/utils.py index 876f891..1d2c796 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -3,6 +3,7 @@ Utility functions for cadquery objects """ import functools import math +from typing import Optional import cadquery as Cq from cadquery.occ_impl.solver import ConstraintSpec from nhf import Role @@ -236,16 +237,37 @@ Cq.Assembly.get_abs_direction = get_abs_direction # Tallying functions +def assembly_this_mass(self: Cq.Assembly) -> Optional[float]: + """ + Gets the mass of an assembly, without considering its components. + """ + if item := self.metadata.get(KEY_ITEM): + return item.mass + elif material := self.metadata.get(KEY_MATERIAL): + vol = self.toCompound().Volume() + return (vol / 1000) * material.density + else: + return None + 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 + if m := assembly_this_mass(a): + total += m return total Cq.Assembly.total_mass = total_mass + +def centre_of_mass(self: Cq.Assembly) -> Optional[float]: + moment = Cq.Vector() + total = 0.0 + for n, a in self.traverse(): + if m := assembly_this_mass(a): + moment += m * a.toCompound().Center() + total += m + if total == 0.0: + return None + return moment / total +Cq.Assembly.centre_of_mass = centre_of_mass -- 2.44.1 From aba1ce0f3e244f03c3252c1473fb35c7bce2e741 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 00:08:14 -0700 Subject: [PATCH 118/187] feat: Compute centre of mass on wings --- nhf/touhou/houjuu_nue/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index f14ac2e..18a0758 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -148,6 +148,7 @@ class Parameters(Model): "wing-depth": bbox.ylen, "wing-height": bbox.zlen, "wing-mass": a.total_mass(), + "wing-centre-of-mass": a.centre_of_mass().toTuple(), "items": Item.count(a), } -- 2.44.1 From 579c10e37345352db2c9d7d692f41875ae7cb270 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 00:17:43 -0700 Subject: [PATCH 119/187] fix: Polygon sliver on left s3 --- nhf/touhou/houjuu_nue/wing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 41b3300..6f510e0 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -1180,13 +1180,15 @@ class WingL(WingProfile): def _mask_wrist(self) -> list[Tuple[float, float]]: l = 200 elbow_bot_x, _ = self.elbow_bot_loc.to2d_pos() - _, elbow_top_y = self.elbow_top_loc.to2d_pos() + elbow_top_x, elbow_top_y = self.elbow_top_loc.to2d_pos() _, wrist_bot_y = self.wrist_bot_loc.to2d_pos() + wrist_top_x, wrist_top_y = self.wrist_top_loc.to2d_pos() return [ (0, -l), (elbow_bot_x, wrist_bot_y), self.wrist_bot_loc.to2d_pos(), self.wrist_top_loc.to2d_pos(), - (elbow_bot_x, elbow_top_y + l), + (wrist_top_x, wrist_top_y + l), + (elbow_top_x, elbow_top_y + l), (0, l), ] -- 2.44.1 From b3a472add41308e73c7629722079269f59c2ee29 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 05:46:18 -0700 Subject: [PATCH 120/187] feat: Linear actuator assembly --- nhf/parts/fasteners.py | 13 ++- nhf/touhou/houjuu_nue/electronics.py | 130 ++++++++++++++++++++++++++- nhf/touhou/houjuu_nue/joints.py | 4 +- 3 files changed, 137 insertions(+), 10 deletions(-) diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index 38c8f84..9dfd20f 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -15,6 +15,10 @@ class FlatHeadBolt(Item): def name(self) -> str: return f"Bolt M{int(self.diam_thread)} h{int(self.height_thread)}mm" + @property + def role(self) -> Role: + return Role.CONNECTION + def generate(self) -> Cq.Assembly: head = Cq.Solid.makeCylinder( @@ -30,13 +34,8 @@ class FlatHeadBolt(Item): ) rod.faces("Z").tag("root") - - return ( - Cq.Assembly() - .addS(rod, name="thread", role=Role.CONNECTION) - .addS(head, name="head", role=Role.CONNECTION, - loc=Cq.Location((0, 0, self.height_thread))) - ) + rod = rod.union(head.located(Cq.Location((0, 0, self.height_thread)))) + return rod @dataclass(frozen=True) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 5e4f690..90d4574 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -2,10 +2,12 @@ Electronic components """ from dataclasses import dataclass +from typing import Optional import cadquery as Cq from nhf.materials import Role from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut +import nhf.utils @dataclass(frozen=True) class LinearActuator(Item): @@ -68,6 +70,7 @@ class LinearActuator(Item): combine='cut', ) ) + front.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn') if stroke_x > 0: shaft = ( Cq.Workplane('YZ') @@ -119,6 +122,7 @@ class LinearActuator(Item): combine='cut', ) ) + back.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn') result = ( Cq.Assembly() .add(front, name="front", @@ -134,6 +138,71 @@ class LinearActuator(Item): result.add(shaft, name="shaft") return result +@dataclass(frozen=True) +class MountingBracket(Item): + """ + Mounting bracket for a linear actuator + """ + mass: float = 1.6 + hole_diam: float = 4.0 + width: float = 8.0 + height: float = 12.20 + thickness: float = 0.98 + length: float = 13.00 + hole_to_side_ext: float = 8.10 + + def __post_init__(self): + assert self.hole_to_side_ext - self.hole_diam / 2 > 0 + + @property + def name(self) -> str: + return f"MountingBracket M{int(self.hole_diam)}" + + @property + def role(self) -> Role: + return Role.MOTION + + def generate(self) -> Cq.Workplane: + result = ( + Cq.Workplane('XY') + .box( + length=self.hole_to_side_ext, + width=self.width, + height=self.height, + centered=(False, True, True) + ) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=self.height, + radius=self.width / 2, + combine=True, + ) + .copyWorkplane(Cq.Workplane('XY')) + .box( + length=2 * (self.hole_to_side_ext - self.thickness), + width=self.width, + height=self.height - self.thickness * 2, + combine='cut', + ) + .copyWorkplane(Cq.Workplane('XY')) + .cylinder( + height=self.height, + radius=self.hole_diam / 2, + combine='cut' + ) + .copyWorkplane(Cq.Workplane('YZ')) + .cylinder( + height=self.hole_to_side_ext * 2, + radius=self.hole_diam / 2, + combine='cut' + ) + ) + result.copyWorkplane(Cq.Workplane('YZ', origin=(self.hole_to_side_ext, 0, 0))).tagPlane("conn_side") + result.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.height/2))).tagPlane("conn_top") + result.copyWorkplane(Cq.Workplane('YX', origin=(0, 0, -self.height/2))).tagPlane("conn_bot") + result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid") + return result + LINEAR_ACTUATOR_SHOULDER = LinearActuator( mass=34.0, @@ -148,8 +217,67 @@ LINEAR_ACTUATOR_HEX_NUT = HexNut( ) LINEAR_ACTUATOR_BOLT = FlatHeadBolt( mass=1.7, - diam_head=16.68, + diam_head=6.68, height_head=2.98, diam_thread=4.0, height_thread=15.83, ) +LINEAR_ACTUATOR_BRACKET = MountingBracket() + +@dataclass(frozen=True) +class LinearActuatorAssembly: + + # FIXME: Measure + actuator: LinearActuator = LINEAR_ACTUATOR_SHOULDER + nut: HexNut = LINEAR_ACTUATOR_HEX_NUT + bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT + bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET + + def add_to( + self, + a: Cq.Assembly, + tag_prefix: Optional[str] = None, + tag_hole_front: Optional[str] = None, + tag_hole_back: Optional[str] = None, + tag_dir: Optional[str] = None): + """ + Adds the necessary mechanical components to this assembly. Does not + invoke `a.solve()`. + """ + if tag_prefix: + tag_prefix = tag_prefix + "_" + name_actuator = f"{tag_prefix}actuator" + name_bracket_front = f"{tag_prefix}bracket_front" + name_bracket_back = f"{tag_prefix}bracket_back" + name_bolt_front = f"{tag_prefix}front_bolt" + name_bolt_back = f"{tag_prefix}back_bolt" + name_nut_front = f"{tag_prefix}front_nut" + name_nut_back = f"{tag_prefix}back_nut" + ( + a + .add(self.actuator.assembly(), name=name_actuator) + .add(self.bracket.assembly(), name=name_bracket_front) + .add(self.bolt.assembly(), name=name_bolt_front) + .add(self.nut.assembly(), name=name_nut_front) + .constrain(f"{name_actuator}/front?conn", f"{name_bracket_front}?conn_mid", + "Plane", param=0) + .constrain(f"{name_bolt_front}?root", f"{name_bracket_front}?conn_top", + "Plane", param=0) + .constrain(f"{name_nut_front}?bot", f"{name_bracket_front}?conn_bot", + "Plane") + .add(self.bracket.assembly(), name=name_bracket_back) + .add(self.bolt.assembly(), name=name_bolt_back) + .add(self.nut.assembly(), name=name_nut_back) + .constrain(f"{name_actuator}/back?conn", f"{name_bracket_back}?conn_mid", + "Plane", param=0) + .constrain(f"{name_bolt_back}?root", f"{name_bracket_back}?conn_top", + "Plane", param=0) + .constrain(f"{name_nut_back}?bot", f"{name_bracket_back}?conn_bot", + "Plane") + ) + if tag_hole_front: + a.constrain(tag_hole_front, f"{name_bracket_front}?conn_side", "Plane") + if tag_hole_back: + a.constrain(tag_hole_back, f"{name_bracket_back}?conn_side", "Plane") + if tag_dir: + a.constrain(tag_dir, f"{name_bracket_front}?conn_mid", "Axis", param=0) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index df67ece..a545d48 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -484,10 +484,10 @@ class ShoulderJoint(Model): # Fasteners .addS(self.bolt.assembly(), name="bolt_top", loc=Cq.Location((0, 0, bolt_z))) - .constrain("bolt_top/thread?root", 'Fixed') + .constrain("bolt_top?root", 'Fixed') .addS(self.bolt.assembly(), name="bolt_bot", loc=Cq.Location((0, 0, -bolt_z), (1,0,0), 180)) - .constrain("bolt_bot/thread?root", 'Fixed') + .constrain("bolt_bot?root", 'Fixed') ) TorsionJoint.add_constraints( result, -- 2.44.1 From 2bdae6df01a6ff5a7bbcd9b56ec886a674d228bf Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 18:45:13 -0700 Subject: [PATCH 121/187] feat: Linear actuator in joint (preliminary) --- nhf/parts/box.py | 8 +++ nhf/touhou/houjuu_nue/electronics.py | 88 +++++++++++++++++++++++++--- nhf/touhou/houjuu_nue/joints.py | 61 ++++++++++++++++--- nhf/touhou/houjuu_nue/wing.py | 11 +++- poetry.lock | 2 +- pyproject.toml | 1 + 6 files changed, 155 insertions(+), 16 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 603eb2d..3498f6d 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -50,6 +50,8 @@ class MountingBox(Model): centred: Tuple[bool, bool] = (False, True) generate_side_tags: bool = True + # Generate tags on the opposite side + generate_reverse_tags: bool = False # Determines the position of side tags flip_y: bool = False @@ -81,9 +83,13 @@ class MountingBox(Model): .extrude(self.thickness) ) plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness) + reverse_plane = result.copyWorkplane(Cq.Workplane('XY')) for i, hole in enumerate(self.holes): tag = hole.tag if hole.tag else f"conn{i}" plane.moveTo(hole.x, hole.y).tagPlane(tag) + if self.generate_reverse_tags: + rev_tag = hole.tag + "_rev" if hole.tag else f"conn{i}_rev" + reverse_plane.moveTo(hole.x, hole.y).tagPlane(rev_tag, '-Z') if self.generate_side_tags: result.faces("Z").val().Center()).tagPlane("left") @@ -102,6 +108,8 @@ class MountingBox(Model): ) for i in range(len(self.holes)): result.markPlane(f"box?conn{i}") + if self.generate_reverse_tags: + result.markPlane(f"box?conn{i}_rev") if self.generate_side_tags: ( result diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 90d4574..26e9386 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -2,12 +2,14 @@ Electronic components """ from dataclasses import dataclass -from typing import Optional +from typing import Optional, Tuple +import math import cadquery as Cq from nhf.materials import Role from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut import nhf.utils +import scipy.optimize as SO @dataclass(frozen=True) class LinearActuator(Item): @@ -47,6 +49,7 @@ class LinearActuator(Item): return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext def generate(self, pos: float=0) -> Cq.Assembly: + assert -1e-6 <= pos <= 1 + 1e-6 stroke_x = pos * self.stroke_length front = ( Cq.Workplane('XZ') @@ -204,10 +207,37 @@ class MountingBracket(Item): return result -LINEAR_ACTUATOR_SHOULDER = LinearActuator( +LINEAR_ACTUATOR_50 = LinearActuator( + mass=34.0, + stroke_length=50, + # FIXME: Measure + front_hole_ext=6, + back_hole_ext=6, + segment1_length=50, + segment2_length=50, +) +LINEAR_ACTUATOR_30 = LinearActuator( mass=34.0, stroke_length=30, ) +LINEAR_ACTUATOR_21 = LinearActuator( + # FIXME: Measure + mass=0.0, + stroke_length=21, + front_hole_ext=4, + back_hole_ext=4, + segment1_length=75/2, + segment2_length=75/2, +) +LINEAR_ACTUATOR_10 = LinearActuator( + # FIXME: Measure + mass=0.0, + stroke_length=10, + front_hole_ext=4.5/2, + back_hole_ext=4.5/2, + segment1_length=30.0, + segment2_length=30.0, +) LINEAR_ACTUATOR_HEX_NUT = HexNut( mass=0.8, diam_thread=4, @@ -224,18 +254,61 @@ LINEAR_ACTUATOR_BOLT = FlatHeadBolt( ) LINEAR_ACTUATOR_BRACKET = MountingBracket() -@dataclass(frozen=True) -class LinearActuatorAssembly: +@dataclass +class Flexor: + """ + Actuator assembly which flexes, similar to biceps + """ + motion_span: float - # FIXME: Measure - actuator: LinearActuator = LINEAR_ACTUATOR_SHOULDER + actuator: LinearActuator = LINEAR_ACTUATOR_30 nut: HexNut = LINEAR_ACTUATOR_HEX_NUT bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET + # FIXME: Add a compression spring so the serviceable distances are not as fixed + mount_loc_r: float = float('nan') + mount_loc_angle: float = float('nan') + + def __post_init__(self): + d_open = self.actuator.conn_length + self.actuator.stroke_length + d_closed = self.actuator.conn_length + theta = math.radians(self.motion_span) + + def target(args): + r, phi = args + e1 = d_open * d_open - 2 * r * r * (1 - math.cos(theta + phi)) + e2 = d_closed * d_closed - 2 * r * r * (1 - math.cos(phi)) + return [e1, e2] + + self.mount_loc_r, phi = SO.fsolve(target, [self.actuator.conn_length, theta]) + self.mount_loc_angle = math.degrees(theta + phi) + + @property + def mount_height(self): + return self.bracket.hole_to_side_ext + @property + def min_serviceable_distance(self): + return self.bracket.hole_to_side_ext * 2 + self.actuator.conn_length + @property + def max_serviceable_distance(self): + return self.min_serviceable_distance + self.actuator.stroke_length + + def target_length_at_angle( + self, + angle: float = 0.0 + ) -> float: + # law of cosines + r = self.mount_loc_r + th = math.radians(self.mount_loc_angle - angle) + d2 = 2 * r * r * (1 - math.cos(th)) + return math.sqrt(d2) + + def add_to( self, a: Cq.Assembly, + target_length: float, tag_prefix: Optional[str] = None, tag_hole_front: Optional[str] = None, tag_hole_back: Optional[str] = None, @@ -244,6 +317,7 @@ class LinearActuatorAssembly: Adds the necessary mechanical components to this assembly. Does not invoke `a.solve()`. """ + pos = (target_length - self.actuator.conn_length) / self.actuator.stroke_length if tag_prefix: tag_prefix = tag_prefix + "_" name_actuator = f"{tag_prefix}actuator" @@ -255,7 +329,7 @@ class LinearActuatorAssembly: name_nut_back = f"{tag_prefix}back_nut" ( a - .add(self.actuator.assembly(), name=name_actuator) + .add(self.actuator.assembly(pos=pos), name=name_actuator) .add(self.bracket.assembly(), name=name_bracket_front) .add(self.bolt.assembly(), name=name_bolt_front) .add(self.nut.assembly(), name=name_nut_front) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index a545d48..cbf600b 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -8,6 +8,7 @@ from nhf.parts.springs import TorsionSpring from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes +from nhf.touhou.houjuu_nue.electronics import Flexor import nhf.utils TOL = 1e-6 @@ -878,10 +879,15 @@ class ElbowJoint(Model): angle_neutral: float = 30.0 + flexor: Flexor = None + flexor_offset_angle: float = 30.0 + flexor_mount_rot: float = 95.0 + def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 + self.flexor = Flexor(motion_span=self.motion_span) @property def total_thickness(self): @@ -897,15 +903,40 @@ class ElbowJoint(Model): axle is at position 0, and parent direction is -X """ return Cq.Location.from2d(-self.parent_arm_radius, 0, 0) - def child_arm_loc(self, flip: bool = False) -> Cq.Location: + def child_arm_loc(self, flip: bool = False, angle: float = 0.0) -> Cq.Location: """ 2d Location of the centre of the arm surface on the child side, assuming axle is at position 0, and parent direction is -X Set `flip=True` to indicate that the joint is supposed to be installed upside down """ - result = Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.child_arm_radius, 0, 180) + result = Cq.Location.rot2d(self.angle_neutral + angle) * Cq.Location.from2d(self.child_arm_radius, 0, 180) return result.flip_y() if flip else result + def actuator_mount(self) -> Cq.Workplane: + holes = [ + Hole(x=0, y=0, tag="mount"), + ] + mbox = MountingBox( + length=self.disk_joint.total_thickness, + width=self.disk_joint.total_thickness, + thickness=self.lip_thickness, + holes=holes, + hole_diam=self.hole_diam, + centred=(True, True), + generate_side_tags=False, + ) + return mbox.generate() + def actuator_mount_loc(self, child: bool) -> Cq.Location: + # Orientes the hole surface so it faces +X + loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) + # Moves the hole so the axle of the mount is perpendicular to it + loc_mount = Cq.Location.from2d(self.flexor.mount_height, 0) * Cq.Location.rot2d(180) + loc_mount_orient = Cq.Location.rot2d(self.flexor_mount_rot * (-1 if child else 1)) + # Moves the hole to be some distance apart from 0 + loc_span = Cq.Location.from2d(self.flexor.mount_loc_r, 0) + r = 0 if child else self.flexor.mount_loc_angle + loc_rot = Cq.Location.rot2d(r + self.flexor_offset_angle) + return loc_rot * loc_span * loc_mount_orient * loc_mount * loc_thickness def lip(self) -> Cq.Workplane: holes = [ @@ -924,6 +955,7 @@ class ElbowJoint(Model): hole_diam=self.hole_diam, centred=(True, True), generate_side_tags=False, + generate_reverse_tags=True, ) return mbox.generate() @@ -946,8 +978,9 @@ class ElbowJoint(Model): loc_lip.inverse * loc_cut_rel * loc_disk) result = ( Cq.Assembly() - .add(self.lip().cut(disk_cut), name="lip", loc=loc_lip) - .add(self.disk_joint.disk(), name="disk", loc=loc_disk) + .add(self.disk_joint.disk(), name="disk") + .add(self.lip().cut(disk_cut), name="lip", loc=loc_disk.inverse * loc_lip) + .add(self.actuator_mount(), name="act", loc=self.actuator_mount_loc(child=True)) ) return result @@ -986,16 +1019,19 @@ class ElbowJoint(Model): -self.disk_joint.tongue_span / 2 + self.angle_neutral ) lip_dz = self.lip_thickness + loc_net_housing = axial_offset * housing_loc result = ( Cq.Assembly() + .add(housing, name="housing") .add(self.lip(), name="lip", loc= + loc_net_housing.inverse * Cq.Location((0, 0, 0), (0, 1, 0), 180) * Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * Cq.Location((0, 0, 0), (0, 1, 0), 90)) - .add(housing, name="housing", - loc=axial_offset * housing_loc) .add(connector, name="connector", - loc=axial_offset) + loc=loc_net_housing.inverse * axial_offset) + .add(self.actuator_mount(), + name="act", loc=self.actuator_mount_loc(child=False)) #.constrain("housing", "Fixed") #.constrain("connector", "Fixed") #.solve() @@ -1021,6 +1057,17 @@ class ElbowJoint(Model): disk="child/disk", angle=angle, ) + if self.flexor: + target_length = self.flexor.target_length_at_angle( + angle=angle, + ) + self.flexor.add_to( + result, + target_length=target_length, + tag_hole_back="parent_upper/act?mount", + tag_hole_front="child/act?mount", + tag_dir="parent_lower?mate", + ) return result.solve() if __name__ == '__main__': diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 6f510e0..6941bee 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -795,7 +795,16 @@ class WingProfile(Model): fastener_pos: float = 0.0, ) -> Cq.Assembly(): if parts is None: - parts = ["root", "s0", "shoulder", "s1", "elbow", "s2", "wrist", "s3"] + parts = [ + "root", + "s0", + "shoulder", + "s1", + "elbow", + "s2", + "wrist", + "s3", + ] result = ( Cq.Assembly() ) diff --git a/poetry.lock b/poetry.lock index 88d30ec..0ea1413 100644 --- a/poetry.lock +++ b/poetry.lock @@ -842,4 +842,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3403086281e26faefd12217e6dec4c0696e3468c5a9d8c952f8d988857aafba0" +content-hash = "6fc2644e7778ba22f8f5f2bcb2ca54f03b325f62c8a3fcd1c265a17561d874b8" diff --git a/pyproject.toml b/pyproject.toml index c24f3d8..7bc47b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ colorama = "^0.4.6" # cadquery dependency multimethod = "^1.12" +scipy = "^1.14.0" [build-system] requires = ["poetry-core"] -- 2.44.1 From 71da0c10a74415a609ea46e8a2fccf8cd2d86bd6 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 18:49:07 -0700 Subject: [PATCH 122/187] fix: Elbow joint z offset problem --- nhf/touhou/houjuu_nue/joints.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index cbf600b..6bc40e4 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -972,13 +972,13 @@ class ElbowJoint(Model): Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * Cq.Location((0, 0, 0), (0, 1, 0), 90) ) - loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, -dz), (0, 0, 1), angle) + loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, 0), (0, 0, 1), angle) loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness)) disk_cut = self.disk_joint._disk_cut().located( loc_lip.inverse * loc_cut_rel * loc_disk) result = ( Cq.Assembly() - .add(self.disk_joint.disk(), name="disk") + .add(self.disk_joint.disk(), name="disk", loc=Cq.Location((0, 0, -dz))) .add(self.lip().cut(disk_cut), name="lip", loc=loc_disk.inverse * loc_lip) .add(self.actuator_mount(), name="act", loc=self.actuator_mount_loc(child=True)) ) @@ -1014,7 +1014,7 @@ class ElbowJoint(Model): ) housing = self.disk_joint.housing_upper() housing_loc = Cq.Location( - (0, 0, housing_dz), + (0, 0, 0), (0, 0, 1), -self.disk_joint.tongue_span / 2 + self.angle_neutral ) @@ -1022,7 +1022,7 @@ class ElbowJoint(Model): loc_net_housing = axial_offset * housing_loc result = ( Cq.Assembly() - .add(housing, name="housing") + .add(housing, name="housing", loc=Cq.Location((0, 0, housing_dz))) .add(self.lip(), name="lip", loc= loc_net_housing.inverse * Cq.Location((0, 0, 0), (0, 1, 0), 180) * -- 2.44.1 From 9ab6a1aa69ce213bbb57f0860497500ad065d0ee Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 21:49:28 -0700 Subject: [PATCH 123/187] feat: Actuator mount position rel. to parent --- nhf/touhou/houjuu_nue/electronics.py | 2 +- nhf/touhou/houjuu_nue/joints.py | 16 +++++++++++----- nhf/touhou/houjuu_nue/wing.py | 6 +++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 26e9386..37c31ac 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -261,7 +261,7 @@ class Flexor: """ motion_span: float - actuator: LinearActuator = LINEAR_ACTUATOR_30 + actuator: LinearActuator = LINEAR_ACTUATOR_50 nut: HexNut = LINEAR_ACTUATOR_HEX_NUT bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 6bc40e4..e3d5595 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -8,7 +8,7 @@ from nhf.parts.springs import TorsionSpring from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes -from nhf.touhou.houjuu_nue.electronics import Flexor +from nhf.touhou.houjuu_nue.electronics import Flexor, LinearActuator import nhf.utils TOL = 1e-6 @@ -879,15 +879,21 @@ class ElbowJoint(Model): angle_neutral: float = 30.0 + actuator: LinearActuator flexor: Flexor = None - flexor_offset_angle: float = 30.0 - flexor_mount_rot: float = 95.0 + # Rotates the entire flexor + flexor_offset_angle: float = 0 + # Rotates the surface of the mount + flexor_mount_rot: float = 90.0 def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 - self.flexor = Flexor(motion_span=self.motion_span) + self.flexor = Flexor( + actuator=self.actuator, + motion_span=self.motion_span + ) @property def total_thickness(self): @@ -934,7 +940,7 @@ class ElbowJoint(Model): loc_mount_orient = Cq.Location.rot2d(self.flexor_mount_rot * (-1 if child else 1)) # Moves the hole to be some distance apart from 0 loc_span = Cq.Location.from2d(self.flexor.mount_loc_r, 0) - r = 0 if child else self.flexor.mount_loc_angle + r = (-self.flexor.mount_loc_angle if child else 0) + 180 loc_rot = Cq.Location.rot2d(r + self.flexor_offset_angle) return loc_rot * loc_span * loc_mount_orient * loc_mount * loc_thickness diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 6941bee..f34869a 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -11,8 +11,9 @@ from nhf import Material, Role 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 RootJoint, ShoulderJoint, ElbowJoint, DiskJoint from nhf.parts.planar import extrude_with_markers +from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint +from nhf.touhou.houjuu_nue.electronics import LINEAR_ACTUATOR_10, LINEAR_ACTUATOR_50 import nhf.utils @dataclass(kw_only=True) @@ -45,6 +46,8 @@ class WingProfile(Model): ), hole_diam=6.0, angle_neutral=15.0, + actuator=LINEAR_ACTUATOR_50, + flexor_offset_angle=-15, )) # Distance between the two spacers on the elbow, halved elbow_h2: float = 5.0 @@ -63,6 +66,7 @@ class WingProfile(Model): parent_arm_radius=30.0, hole_diam=4.0, angle_neutral=-30.0, + actuator=LINEAR_ACTUATOR_10, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 -- 2.44.1 From 340aa7c6daecadad350926ab12cb3053e8a62180 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 22:13:56 -0700 Subject: [PATCH 124/187] feat: Subduct s2 into s1. Off-centre elbow --- nhf/touhou/houjuu_nue/wing.py | 137 ++++++++++++++++------------------ 1 file changed, 63 insertions(+), 74 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index f34869a..fe99ad4 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -52,8 +52,6 @@ class WingProfile(Model): # Distance between the two spacers on the elbow, halved elbow_h2: float = 5.0 - s2_thickness: float = 25.0 - wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( movement_angle=30, @@ -71,8 +69,6 @@ class WingProfile(Model): # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 - s3_thickness: float = 25.0 - mat_panel: Material = Material.ACRYLIC_TRANSLUSCENT mat_bracket: Material = Material.ACRYLIC_TRANSPARENT mat_hs_joint: Material = Material.PLASTIC_PLA @@ -85,6 +81,8 @@ class WingProfile(Model): wrist_height: float elbow_rotate: float = -5.0 wrist_rotate: float = -30.0 + # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) + elbow_axle_pos: float = 0.3 # False for the right side, True for the left side flip: bool @@ -94,7 +92,9 @@ class WingProfile(Model): self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) - self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height / 2) + if self.flip: + self.elbow_axle_pos = 1 - self.elbow_axle_pos + self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height * self.elbow_axle_pos) if self.flip: self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) else: @@ -108,6 +108,19 @@ class WingProfile(Model): self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2 assert self.spacer_thickness == self.root_joint.child_mount_thickness + @property + def s2_thickness(self) -> float: + """ + s2 needs to duck under s1, so its thinner + """ + return self.s1_thickness - 2 * self.panel_thickness + @property + def s3_thickness(self) -> float: + """ + s3 does not need to duck under s2 + """ + return self.s1_thickness - 2 * self.panel_thickness + @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: return self.shoulder_joint @@ -343,27 +356,6 @@ class WingProfile(Model): """ return None - def _elbow_joint_retract_cut_polygon(self, loc: Cq.Location) -> Cq.Sketch: - """ - Creates a cutting polygon for removing the contraction part of a joint - """ - theta = math.radians(self.elbow_joint.motion_span) - h = self.elbow_height - dx = h * math.tan(theta / 2) - dy = h - sign = -1 if self.flip else 1 - points = [ - (0, 0), - (dx, sign * dy), - (-dx, sign * dy), - ] - return ( - Cq.Sketch() - .polygon([ - (loc * Cq.Location.from2d(*p)).to2d_pos() - for p in points - ]) - ) def _wrist_joint_retract_cut_polygon(self, loc: Cq.Location) -> Optional[Cq.Sketch]: """ Creates a cutting polygon for removing the contraction part of a joint @@ -422,6 +414,44 @@ class WingProfile(Model): .assemble() ) + def _parent_joint_extension_profile( + self, + loc_axle: Cq.Location, + loc_bot: Cq.Location, + loc_top: Cq.Location, + angle_span: float, + bot: bool = True + ) -> Cq.Sketch: + """ + Generates a sector-like profile on the child side of a panel to + accomodate for joint rotation + """ + sign = -1 if bot else 1 + + loc_tip = loc_top if bot else loc_bot + loc_arc_right = loc_bot if bot else loc_top + loc_rel_arc_right = loc_axle.inverse * loc_arc_right + loc_arc_left = loc_axle * Cq.Location.rot2d(sign * angle_span) * loc_rel_arc_right + loc_arc_middle = loc_axle * Cq.Location.rot2d(sign * angle_span / 2) * loc_rel_arc_right + + return ( + Cq.Sketch() + .segment( + loc_tip.to2d_pos(), + loc_arc_right.to2d_pos(), + ) + .arc( + loc_arc_right.to2d_pos(), + loc_arc_middle.to2d_pos(), + loc_arc_left.to2d_pos(), + ) + .segment( + loc_tip.to2d_pos(), + loc_arc_left.to2d_pos(), + ) + .assemble() + ) + def _assembly_insert_spacer( self, @@ -492,9 +522,6 @@ class WingProfile(Model): self.profile() .reset() .polygon(self._mask_elbow(), mode='i') - .reset() - .push([self.elbow_axle_loc]) - .each(self._elbow_joint_retract_cut_polygon, mode='s') ) return profile def surface_s1(self, front: bool = True) -> Cq.Workplane: @@ -516,35 +543,6 @@ class WingProfile(Model): tags = tags_shoulder + tags_elbow return extrude_with_markers( profile, self.panel_thickness, tags, reverse=front) - @target(name="profile-s1-bridge", kind=TargetKind.DXF) - def profile_s1_bridge(self) -> Cq.Workplane: - return ( - self.profile() - #.reset() - #.polygon(self._mask_elbow(), mode='i') - .reset() - .push([self.elbow_axle_loc]) - .each(self._elbow_joint_retract_cut_polygon, mode='i') - .reset() - .push([self.elbow_axle_loc]) - .each(lambda loc: self._joint_extension_profile( - axle_loc=self.elbow_axle_loc, - radius=self.elbow_height / 2, - angle_span=self.elbow_joint.motion_span, - bot=not self.flip, - ), mode='a') - ) - def surface_s1_bridge(self, front: bool = True) -> Cq.Workplane: - profile = self.profile_s1_bridge() - loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.parent_arm_loc() - tags = [ - ("elbow_bot", self.elbow_axle_loc * loc_elbow * - Cq.Location.from2d(0, -self.elbow_h2)), - ("elbow_top", self.elbow_axle_loc * loc_elbow * - Cq.Location.from2d(0, self.elbow_h2)), - ] - return extrude_with_markers( - profile, self.panel_thickness, tags, reverse=not front) @submodel(name="spacer-s1-shoulder") def spacer_s1_shoulder(self) -> MountingBox: holes = [ @@ -576,14 +574,6 @@ class WingProfile(Model): material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Workplane: loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc(flip=self.flip) -- 2.44.1 From c878f65b4752612c3adf68137a42f74c8abc431b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 22:16:18 -0700 Subject: [PATCH 125/187] feat: Leave movement gap for cushion --- nhf/touhou/houjuu_nue/joints.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index e3d5595..76d6ccb 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -599,6 +599,8 @@ class DiskJoint(Model): # Angular span of movement movement_angle: float = 120.0 + # leave some gap for cushion + movement_gap: float = 5.0 # Angular span of tongue on disk tongue_span: float = 30.0 tongue_length: float = 10.0 @@ -704,11 +706,11 @@ class DiskJoint(Model): wall = Cq.Solid.makeCylinder( radius=self.radius_housing, height=height, - angleDegrees=360 - self.opening_span, + angleDegrees=360 - self.opening_span - self.movement_gap*2, ).cut(Cq.Solid.makeCylinder( radius=self.radius_disk, height=height, - )).rotate((0, 0, 0), (0, 0, 1), self.opening_span) + )).rotate((0, 0, 0), (0, 0, 1), self.opening_span+self.movement_gap) return wall @target(name="housing-lower") -- 2.44.1 From d898df6165af916c2ebc282da4f8a2bded07ad03 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 22:34:19 -0700 Subject: [PATCH 126/187] feat: Add battery box item --- nhf/touhou/houjuu_nue/electronics.py | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 37c31ac..2cf1c4e 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -206,6 +206,70 @@ class MountingBracket(Item): result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid") return result +@dataclass(frozen=True) +class BatteryBox18650(Item): + """ + A number of 18650 batteries in series + """ + mass: float = 17.4 + 68.80 * 3 + length: float = 75.70 + width_base: float = 61.46 - 18.48 - 20.18 * 2 + battery_dist: float = 20.18 + height: float = 19.66 + # space from bottom to battery begin + thickness: float = 1.66 + battery_diam: float = 18.48 + battery_height: float = 68.80 + n_batteries: int = 3 + + def __post_init__(self): + assert 2 * self.thickness < min(self.length, self.height) + + @property + def name(self) -> str: + return f"BatteryBox 18650*{self.n_batteries}" + + @property + def role(self) -> Role: + return Role.ELECTRONIC + + def generate(self) -> Cq.Workplane: + width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam + return ( + Cq.Workplane('XY') + .box( + length=self.length, + width=width, + height=self.height, + centered=(True, True, False), + ) + .copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.thickness))) + .box( + length=self.length - self.thickness*2, + width=width - self.thickness*2, + height=self.height - self.thickness, + centered=(True, True, False), + combine='cut', + ) + .copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2))) + .rarray( + xSpacing=1, + ySpacing=self.battery_dist, + xCount=1, + yCount=self.n_batteries, + center=True, + ) + .cylinder( + radius=self.battery_diam/2, + height=self.battery_height, + direct=(1, 0, 0), + centered=(True, True, False), + combine=True, + ) + ) + + + LINEAR_ACTUATOR_50 = LinearActuator( mass=34.0, @@ -254,6 +318,8 @@ LINEAR_ACTUATOR_BOLT = FlatHeadBolt( ) LINEAR_ACTUATOR_BRACKET = MountingBracket() +BATTERY_BOX = BatteryBox18650() + @dataclass class Flexor: """ -- 2.44.1 From f665d0d53ea0a3d18e3d8ff00fc9725ebf88a17a Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 21 Jul 2024 23:34:02 -0700 Subject: [PATCH 127/187] feat: Add mount for onboard electronics --- nhf/touhou/houjuu_nue/__init__.py | 12 ++++++ nhf/touhou/houjuu_nue/electronics.py | 6 +++ nhf/touhou/houjuu_nue/wing.py | 60 +++++++++++++++++----------- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 18a0758..da2d1c1 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -52,26 +52,38 @@ class Parameters(Model): wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR( name="r1", shoulder_angle_bias = 15.0, + s0_top_hole=False, + s0_bot_hole=True, )) wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR( name="r2", + s0_top_hole=True, + s0_bot_hole=True, )) wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR( name="r3", shoulder_angle_bias = 15.0, + s0_top_hole=True, + s0_bot_hole=False, )) wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL( name="l1", wrist_angle=-60.0, + s0_top_hole=False, + s0_bot_hole=True, )) wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( name="l2", wrist_angle=-30.0, shoulder_angle_bias = 15.0, + s0_top_hole=True, + s0_bot_hole=True, )) wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL( name="l3", wrist_angle=-0.0, + s0_top_hole=True, + s0_bot_hole=False, )) trident: MT.Trident = field(default_factory=lambda: MT.Trident()) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 2cf1c4e..b07bdd7 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -421,3 +421,9 @@ class Flexor: a.constrain(tag_hole_back, f"{name_bracket_back}?conn_side", "Plane") if tag_dir: a.constrain(tag_dir, f"{name_bracket_front}?conn_mid", "Axis", param=0) + + +@dataclass +class ElectronicBoard: + + hole_diam: float = 4.0 diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index fe99ad4..0f7419f 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -13,7 +13,7 @@ from nhf.parts.box import box_with_centre_holes, MountingBox, Hole from nhf.parts.joints import HirthJoint from nhf.parts.planar import extrude_with_markers from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint -from nhf.touhou.houjuu_nue.electronics import LINEAR_ACTUATOR_10, LINEAR_ACTUATOR_50 +from nhf.touhou.houjuu_nue.electronics import LINEAR_ACTUATOR_10, LINEAR_ACTUATOR_50, ElectronicBoard import nhf.utils @dataclass(kw_only=True) @@ -38,6 +38,13 @@ class WingProfile(Model): shoulder_mid_x: float = -125.0 shoulder_mid_y: float = 75.0 + s0_hole_loc: Cq.Location = Cq.Location.from2d(-25, 33) + s0_hole_diam: float = 15.0 + s0_top_hole: bool = False + s0_bot_hole: bool = True + + electronic_board: ElectronicBoard = field(default_factory=lambda: ElectronicBoard()) + s1_thickness: float = 25.0 elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( @@ -171,7 +178,7 @@ class WingProfile(Model): return result @target(name="profile-s0", kind=TargetKind.DXF) - def profile_s0(self) -> Cq.Sketch: + def profile_s0(self, top: bool = True) -> Cq.Sketch: tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y mid_x = self.shoulder_mid_x @@ -197,6 +204,14 @@ class WingProfile(Model): .circle(self.shoulder_joint.radius, mode='a') .circle(self.shoulder_joint.bolt.diam_head / 2, mode='s') ) + top = top == self.flip + if (self.s0_top_hole and top) or (self.s0_bot_hole and not top): + sketch = ( + sketch + .reset() + .push([self.s0_hole_loc.to2d_pos()]) + .circle(self.s0_hole_diam / 2, mode='s') + ) return sketch def outer_shell_s0(self) -> Cq.Workplane: @@ -260,19 +275,20 @@ class WingProfile(Model): centred=(True, True), flip_y=self.flip, ) - @submodel(name="spacer-s0-mid3") - def spacer_s0_mid3(self) -> MountingBox: + @submodel(name="spacer-s0-electronic") + def spacer_s0_electronic(self) -> MountingBox: + holes = [ + Hole(x=30, y=80), + Hole(x=30, y=-80), + Hole(x=-30, y=80), + Hole(x=-30, y=-80), + ] return MountingBox( + holes=holes, + hole_diam=self.electronic_board.hole_diam, length=self.root_height, - width=40, - thickness=self.spacer_thickness, - flip_y=self.flip - ) - @submodel(name="spacer-s0-mid2") - def spacer_s0_mid2(self) -> MountingBox: - return MountingBox( - length=self.root_height, - width=60, + width=170, + centred=(True, True), thickness=self.spacer_thickness, flip_y=self.flip ) @@ -281,22 +297,21 @@ class WingProfile(Model): base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10 base_dy = self.root_joint.hirth_joint.joint_height loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2) - mid_spacer_loc = ( - Cq.Location.from2d(0, -self.shoulder_width/2) * - self.shoulder_axle_loc * - Cq.Location.rot2d(self.shoulder_joint.angle_neutral) - ) + #mid_spacer_loc = ( + # Cq.Location.from2d(0, -self.shoulder_width/2) * + # self.shoulder_axle_loc * + # Cq.Location.rot2d(self.shoulder_joint.angle_neutral) + #) tags = [ ("shoulder", self.shoulder_axle_loc * self.shoulder_joint.parent_arm_loc() * loc_tip), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), - ("mid3", mid_spacer_loc * Cq.Location.from2d(90, 0)), - ("mid2", mid_spacer_loc * Cq.Location.from2d(150, 0)), + ("electronic", Cq.Location.from2d(-55, 75, 64)), ] result = extrude_with_markers( - self.profile_s0(), + self.profile_s0(top=top), self.panel_thickness, tags, reverse=not top, @@ -326,8 +341,7 @@ class WingProfile(Model): for o, tag in [ (self.spacer_s0_shoulder().generate(), "shoulder"), (self.spacer_s0_base().generate(), "base"), - (self.spacer_s0_mid3().generate(), "mid3"), - (self.spacer_s0_mid2().generate(), "mid2"), + (self.spacer_s0_electronic().generate(), "electronic"), ]: top_tag, bot_tag = "top", "bot" if self.flip: -- 2.44.1 From 7371333a84cd964adc2274f0a878c4e67e8576cb Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 22 Jul 2024 01:28:58 -0700 Subject: [PATCH 128/187] refactor: Actuator arm position into its own class --- nhf/geometry.py | 32 ++++++++++++++++++++++++++++ nhf/test.py | 21 ++++++++++++++++++ nhf/touhou/houjuu_nue/electronics.py | 29 +++++++++---------------- nhf/touhou/houjuu_nue/joints.py | 6 ++++-- 4 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 nhf/geometry.py diff --git a/nhf/geometry.py b/nhf/geometry.py new file mode 100644 index 0000000..b4d14d1 --- /dev/null +++ b/nhf/geometry.py @@ -0,0 +1,32 @@ +""" +Geometry functions +""" +from typing import Tuple +import math + +def contraction_actuator_span_pos( + d_open: float, + d_closed: float, + theta: float, + ) -> Tuple[float, float]: + """ + Calculates the position of the two ends of an actuator, whose fully opened + length is `d_open`, closed length is `d_closed`, and whose motion spans a + range `theta` (in radians). Returns (r, phi): If one end of the actuator is + held at `(r, 0)`, then the other end will trace an arc `r` away from the + origin with span `theta` + + Let `P` (resp. `P'`) be the position of the front of the actuator when its + fully open (resp. closed), `Q` be the position of the back of the actuator, + we note that `OP = OP' = OQ`. + """ + pq2 = d_open * d_open + p_q2 = d_closed * d_closed + # angle of PQP' + psi = 0.5 * theta + # |P-P'|, via the triangle PQP' + pp_2 = pq2 + p_q2 - 2 * d_open * d_closed * math.cos(psi) + r2 = pp_2 / (2 - 2 * math.cos(theta)) + # Law of cosines on POQ: + phi = math.acos(1 - pq2 / 2 / r2) + return math.sqrt(r2), phi diff --git a/nhf/test.py b/nhf/test.py index b2dbb45..cd23c56 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -2,11 +2,13 @@ Unit tests for tooling """ from dataclasses import dataclass +import math import unittest import cadquery as Cq from nhf.build import Model, target from nhf.parts.item import Item import nhf.checks +import nhf.geometry import nhf.utils # Color presets for testing purposes @@ -76,6 +78,25 @@ class TestChecks(unittest.TestCase): with self.subTest(offset=offset): self.intersect_test_case(offset) +class TestGeometry(unittest.TestCase): + + def test_actuator_arm_pos(self): + do = 70.0 + dc = 40.0 + theta = math.radians(35.0) + r, phi = nhf.geometry.contraction_actuator_span_pos(do, dc, theta) + with self.subTest(state='open'): + x = r * math.cos(phi) + y = r * math.sin(phi) + d = math.sqrt((x - r) ** 2 + y ** 2) + self.assertAlmostEqual(d, do) + with self.subTest(state='closed'): + x = r * math.cos(phi - theta) + y = r * math.sin(phi - theta) + d = math.sqrt((x - r) ** 2 + y ** 2) + self.assertAlmostEqual(d, dc) + + class TestUtils(unittest.TestCase): def test_2d_orientation(self): diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index b07bdd7..75d8ea5 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -9,7 +9,6 @@ from nhf.materials import Role from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut import nhf.utils -import scipy.optimize as SO @dataclass(frozen=True) class LinearActuator(Item): @@ -333,22 +332,6 @@ class Flexor: bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET # FIXME: Add a compression spring so the serviceable distances are not as fixed - mount_loc_r: float = float('nan') - mount_loc_angle: float = float('nan') - - def __post_init__(self): - d_open = self.actuator.conn_length + self.actuator.stroke_length - d_closed = self.actuator.conn_length - theta = math.radians(self.motion_span) - - def target(args): - r, phi = args - e1 = d_open * d_open - 2 * r * r * (1 - math.cos(theta + phi)) - e2 = d_closed * d_closed - 2 * r * r * (1 - math.cos(phi)) - return [e1, e2] - - self.mount_loc_r, phi = SO.fsolve(target, [self.actuator.conn_length, theta]) - self.mount_loc_angle = math.degrees(theta + phi) @property def mount_height(self): @@ -360,13 +343,21 @@ class Flexor: def max_serviceable_distance(self): return self.min_serviceable_distance + self.actuator.stroke_length + def open_pos(self) -> Tuple[float, float]: + r, phi = nhf.geometry.contraction_actuator_span_pos( + d_open=self.actuator.conn_length + self.actuator.stroke_length, + d_closed=self.actuator.conn_length, + theta=math.radians(self.motion_span) + ) + return r, math.degrees(phi) + def target_length_at_angle( self, angle: float = 0.0 ) -> float: # law of cosines - r = self.mount_loc_r - th = math.radians(self.mount_loc_angle - angle) + r, phi = self.open_pos() + th = math.radians(phi - angle) d2 = 2 * r * r * (1 - math.cos(th)) return math.sqrt(d2) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 76d6ccb..1920ed0 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -9,6 +9,7 @@ from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes from nhf.touhou.houjuu_nue.electronics import Flexor, LinearActuator +import nhf.geometry import nhf.utils TOL = 1e-6 @@ -941,8 +942,9 @@ class ElbowJoint(Model): loc_mount = Cq.Location.from2d(self.flexor.mount_height, 0) * Cq.Location.rot2d(180) loc_mount_orient = Cq.Location.rot2d(self.flexor_mount_rot * (-1 if child else 1)) # Moves the hole to be some distance apart from 0 - loc_span = Cq.Location.from2d(self.flexor.mount_loc_r, 0) - r = (-self.flexor.mount_loc_angle if child else 0) + 180 + mount_r, mount_loc_angle = self.flexor.open_pos() + loc_span = Cq.Location.from2d(mount_r, 0) + r = (-mount_loc_angle if child else 0) + 180 loc_rot = Cq.Location.rot2d(r + self.flexor_offset_angle) return loc_rot * loc_span * loc_mount_orient * loc_mount * loc_thickness -- 2.44.1 From ddeaf1194f68804f01bc11aa240ccc453fecf421 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 22 Jul 2024 09:49:16 -0700 Subject: [PATCH 129/187] feat: Optional actuator on wrist --- nhf/touhou/houjuu_nue/electronics.py | 2 ++ nhf/touhou/houjuu_nue/joints.py | 23 +++++++----- nhf/touhou/houjuu_nue/wing.py | 53 +++++++++++++++------------- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 75d8ea5..8469d52 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -300,6 +300,8 @@ LINEAR_ACTUATOR_10 = LinearActuator( back_hole_ext=4.5/2, segment1_length=30.0, segment2_length=30.0, + segment1_width=15.0, + segment2_width=21.0, ) LINEAR_ACTUATOR_HEX_NUT = HexNut( mass=0.8, diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 1920ed0..36d1914 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -882,8 +882,8 @@ class ElbowJoint(Model): angle_neutral: float = 30.0 - actuator: LinearActuator - flexor: Flexor = None + actuator: Optional[LinearActuator] + flexor: Optional[Flexor] = None # Rotates the entire flexor flexor_offset_angle: float = 0 # Rotates the surface of the mount @@ -893,10 +893,11 @@ class ElbowJoint(Model): assert self.child_arm_radius > self.disk_joint.radius_housing assert self.parent_arm_radius > self.disk_joint.radius_housing self.disk_joint.tongue_length = self.child_arm_radius - self.disk_joint.radius_disk - self.lip_thickness / 2 - self.flexor = Flexor( - actuator=self.actuator, - motion_span=self.motion_span - ) + if self.actuator: + self.flexor = Flexor( + actuator=self.actuator, + motion_span=self.motion_span + ) @property def total_thickness(self): @@ -990,8 +991,9 @@ class ElbowJoint(Model): Cq.Assembly() .add(self.disk_joint.disk(), name="disk", loc=Cq.Location((0, 0, -dz))) .add(self.lip().cut(disk_cut), name="lip", loc=loc_disk.inverse * loc_lip) - .add(self.actuator_mount(), name="act", loc=self.actuator_mount_loc(child=True)) ) + if self.flexor: + result.add(self.actuator_mount(), name="act", loc=self.actuator_mount_loc(child=True)) return result @target(name="parent-lower") @@ -1040,12 +1042,15 @@ class ElbowJoint(Model): Cq.Location((0, 0, 0), (0, 1, 0), 90)) .add(connector, name="connector", loc=loc_net_housing.inverse * axial_offset) - .add(self.actuator_mount(), - name="act", loc=self.actuator_mount_loc(child=False)) #.constrain("housing", "Fixed") #.constrain("connector", "Fixed") #.solve() ) + if self.flexor: + result.add( + self.actuator_mount(), + name="act", + loc=self.actuator_mount_loc(child=False)) return result @assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 0f7419f..3610d4c 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -51,7 +51,7 @@ class WingProfile(Model): disk_joint=DiskJoint( movement_angle=55, ), - hole_diam=6.0, + hole_diam=4.0, angle_neutral=15.0, actuator=LINEAR_ACTUATOR_50, flexor_offset_angle=-15, @@ -65,13 +65,14 @@ class WingProfile(Model): radius_disk=13.0, radius_housing=15.0, ), - hole_pos=[10, 20], - lip_length=50, + hole_pos=[10], + lip_length=30, child_arm_radius=23.0, parent_arm_radius=30.0, hole_diam=4.0, angle_neutral=-30.0, - actuator=LINEAR_ACTUATOR_10, + # The left side wrist is too small for an actuator to work + actuator=None, #LINEAR_ACTUATOR_10, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -90,6 +91,7 @@ class WingProfile(Model): wrist_rotate: float = -30.0 # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) elbow_axle_pos: float = 0.3 + wrist_axle_pos: float = 0.0 # False for the right side, True for the left side flip: bool @@ -103,9 +105,8 @@ class WingProfile(Model): self.elbow_axle_pos = 1 - self.elbow_axle_pos self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height * self.elbow_axle_pos) if self.flip: - self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height / 2) - else: - self.wrist_axle_loc = self.wrist_bot_loc + self.wrist_axle_pos = 1 - self.wrist_axle_pos + self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height * self.wrist_axle_pos) assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness) assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) @@ -126,7 +127,8 @@ class WingProfile(Model): """ s3 does not need to duck under s2 """ - return self.s1_thickness - 2 * self.panel_thickness + extra = 2 * self.panel_thickness if self.flip else 0 + return self.s1_thickness - 2 * self.panel_thickness - extra @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: @@ -395,7 +397,7 @@ class WingProfile(Model): for p in points ]) ) - def _joint_extension_profile( + def _child_joint_extension_profile( self, axle_loc: Cq.Location, radius: float, @@ -640,8 +642,10 @@ class WingProfile(Model): return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) @target(name="profile-s2-bridge", kind=TargetKind.DXF) def profile_s2_bridge(self) -> Cq.Workplane: + # FIXME: Leave some margin here so we can glue the panels + # Generates the extension profile, which is required on both sides - profile = self._joint_extension_profile( + profile = self._child_joint_extension_profile( axle_loc=self.wrist_axle_loc, radius=self.wrist_height * (0.5 if self.flip else 1), angle_span=self.wrist_joint.motion_span, @@ -881,25 +885,24 @@ class WingProfile(Model): if "wrist" in parts: angle = self.wrist_joint.motion_span * elbow_wrist_deflection result.add(self.wrist_joint.assembly(angle=angle), name="wrist") + wrist_n_holes = len(self.wrist_joint.hole_pos) if "s2" in parts and "wrist" in parts: # Mounted backwards to bend in other direction - ( - result - .constrain("s2/wrist_top?conn0", f"wrist/parent_upper/lip?conn_{tag_bot}0", "Plane") - .constrain("s2/wrist_top?conn1", f"wrist/parent_upper/lip?conn_{tag_bot}1", "Plane") - .constrain("s2/wrist_bot?conn0", f"wrist/parent_upper/lip?conn_{tag_top}0", "Plane") - .constrain("s2/wrist_bot?conn1", f"wrist/parent_upper/lip?conn_{tag_top}1", "Plane") - ) + for i in range(wrist_n_holes): + ( + result + .constrain(f"s2/wrist_top?conn{i}", f"wrist/parent_upper/lip?conn_{tag_bot}{i}", "Plane") + .constrain(f"s2/wrist_bot?conn{i}", f"wrist/parent_upper/lip?conn_{tag_top}{i}", "Plane") + ) if "s3" in parts: result.add(self.assembly_s3(), name="s3") if "s3" in parts and "wrist" in parts: - ( - result - .constrain("s3/wrist_top?conn0", f"wrist/child/lip?conn_{tag_bot}0", "Plane") - .constrain("s3/wrist_top?conn1", f"wrist/child/lip?conn_{tag_bot}1", "Plane") - .constrain("s3/wrist_bot?conn0", f"wrist/child/lip?conn_{tag_top}0", "Plane") - .constrain("s3/wrist_bot?conn1", f"wrist/child/lip?conn_{tag_top}1", "Plane") - ) + for i in range(wrist_n_holes): + ( + result + .constrain(f"s3/wrist_top?conn{i}", f"wrist/child/lip?conn_{tag_bot}{i}", "Plane") + .constrain(f"s3/wrist_bot?conn{i}", f"wrist/child/lip?conn_{tag_top}{i}", "Plane") + ) if len(parts) > 1: result.solve() @@ -1095,6 +1098,8 @@ class WingL(WingProfile): arrow_height: float = 120.0 flip: bool = True + elbow_axle_pos: float = 0.4 + wrist_axle_pos: float = 0.5 def __post_init__(self): assert self.wrist_height <= self.shoulder_joint.height -- 2.44.1 From 07fb43cd0171a9301c72e419e7393ae524b716ad Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 22 Jul 2024 13:26:37 -0700 Subject: [PATCH 130/187] feat: Actuator position to minimize tangential --- nhf/geometry.py | 34 +++++++++++++++++++++++++- nhf/test.py | 27 +++++++++++++++++---- nhf/touhou/houjuu_nue/electronics.py | 36 ++++++++++++++++++---------- nhf/touhou/houjuu_nue/joints.py | 5 ++-- nhf/touhou/houjuu_nue/test.py | 15 ++++++++++++ nhf/touhou/houjuu_nue/wing.py | 4 ++-- 6 files changed, 98 insertions(+), 23 deletions(-) diff --git a/nhf/geometry.py b/nhf/geometry.py index b4d14d1..5ae48c5 100644 --- a/nhf/geometry.py +++ b/nhf/geometry.py @@ -4,7 +4,7 @@ Geometry functions from typing import Tuple import math -def contraction_actuator_span_pos( +def min_radius_contraction_span_pos( d_open: float, d_closed: float, theta: float, @@ -20,6 +20,9 @@ def contraction_actuator_span_pos( fully open (resp. closed), `Q` be the position of the back of the actuator, we note that `OP = OP' = OQ`. """ + assert d_open > d_closed + assert 0 < theta < math.pi + pq2 = d_open * d_open p_q2 = d_closed * d_closed # angle of PQP' @@ -30,3 +33,32 @@ def contraction_actuator_span_pos( # Law of cosines on POQ: phi = math.acos(1 - pq2 / 2 / r2) return math.sqrt(r2), phi + +def min_tangent_contraction_span_pos( + d_open: float, + d_closed: float, + theta: float, + ) -> Tuple[float, float, float]: + """ + Returns `(r, phi, r')` where `r` is the distance of the arm to origin, `r'` + is the distance of the base to origin, and `phi` the angle in the open + state. + """ + assert d_open > d_closed + assert 0 < theta < math.pi + # Angle of OPQ = OPP' + pp_ = d_open - d_closed + pq = d_open + p_q = d_closed + + a = (math.pi - theta) / 2 + # Law of sines on POP' + r = math.sin(a) / math.sin(theta) * pp_ + # Law of cosine on OPQ + oq = math.sqrt(r * r + pq * pq - 2 * r * pq * math.cos(a)) + # Law of sines on OP'Q. Not using OPQ for numerical reasons since the angle + # `phi` could be very close to `pi/2` + phi_ = math.asin(math.sin(a) / oq * p_q) + phi = phi_ + theta + assert theta <= phi < math.pi + return r, phi, oq diff --git a/nhf/test.py b/nhf/test.py index cd23c56..6ca9da5 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -80,11 +80,12 @@ class TestChecks(unittest.TestCase): class TestGeometry(unittest.TestCase): - def test_actuator_arm_pos(self): - do = 70.0 - dc = 40.0 - theta = math.radians(35.0) - r, phi = nhf.geometry.contraction_actuator_span_pos(do, dc, theta) + def test_min_radius_contraction_span_pos(self): + sl = 50.0 + dc = 112.0 + do = dc + sl + theta = math.radians(60.0) + r, phi = nhf.geometry.min_radius_contraction_span_pos(do, dc, theta) with self.subTest(state='open'): x = r * math.cos(phi) y = r * math.sin(phi) @@ -95,6 +96,22 @@ class TestGeometry(unittest.TestCase): y = r * math.sin(phi - theta) d = math.sqrt((x - r) ** 2 + y ** 2) self.assertAlmostEqual(d, dc) + def test_min_tangent_contraction_span_pos(self): + sl = 50.0 + dc = 112.0 + do = dc + sl + theta = math.radians(60.0) + r, phi, rp = nhf.geometry.min_tangent_contraction_span_pos(do, dc, theta) + with self.subTest(state='open'): + x = r * math.cos(phi) + y = r * math.sin(phi) + d = math.sqrt((x - rp) ** 2 + y ** 2) + self.assertAlmostEqual(d, do) + with self.subTest(state='closed'): + x = r * math.cos(phi - theta) + y = r * math.sin(phi - theta) + d = math.sqrt((x - rp) ** 2 + y ** 2) + self.assertAlmostEqual(d, dc) class TestUtils(unittest.TestCase): diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 8469d52..02468ec 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -338,29 +338,39 @@ class Flexor: @property def mount_height(self): return self.bracket.hole_to_side_ext - @property - def min_serviceable_distance(self): - return self.bracket.hole_to_side_ext * 2 + self.actuator.conn_length - @property - def max_serviceable_distance(self): - return self.min_serviceable_distance + self.actuator.stroke_length - def open_pos(self) -> Tuple[float, float]: - r, phi = nhf.geometry.contraction_actuator_span_pos( + def open_pos(self) -> Tuple[float, float, float]: + r, phi, r_ = nhf.geometry.min_tangent_contraction_span_pos( d_open=self.actuator.conn_length + self.actuator.stroke_length, d_closed=self.actuator.conn_length, - theta=math.radians(self.motion_span) + theta=math.radians(self.motion_span), ) - return r, math.degrees(phi) + return r, math.degrees(phi), r_ + #r, phi = nhf.geometry.min_radius_contraction_span_pos( + # d_open=self.actuator.conn_length + self.actuator.stroke_length, + # d_closed=self.actuator.conn_length, + # theta=math.radians(self.motion_span), + #) + #return r, math.degrees(phi), r + r, phi, r_ = nhf.geometry.min_tangent_contraction_span_pos( + d_open=self.actuator.conn_length + self.actuator.stroke_length, + d_closed=self.actuator.conn_length, + theta=math.radians(self.motion_span), + ) + return r, math.degrees(phi), r_ def target_length_at_angle( self, angle: float = 0.0 ) -> float: - # law of cosines - r, phi = self.open_pos() + """ + Length of the actuator at some angle + """ + r, phi, rp = self.open_pos() th = math.radians(phi - angle) - d2 = 2 * r * r * (1 - math.cos(th)) + return math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2) + # Law of cosines + d2 = r * r + rp * rp - 2 * r * rp * math.cos(th) return math.sqrt(d2) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 36d1914..56433fb 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -943,8 +943,8 @@ class ElbowJoint(Model): loc_mount = Cq.Location.from2d(self.flexor.mount_height, 0) * Cq.Location.rot2d(180) loc_mount_orient = Cq.Location.rot2d(self.flexor_mount_rot * (-1 if child else 1)) # Moves the hole to be some distance apart from 0 - mount_r, mount_loc_angle = self.flexor.open_pos() - loc_span = Cq.Location.from2d(mount_r, 0) + mount_r, mount_loc_angle, mount_parent_r = self.flexor.open_pos() + loc_span = Cq.Location.from2d(mount_r if child else mount_parent_r, 0) r = (-mount_loc_angle if child else 0) + 180 loc_rot = Cq.Location.rot2d(r + self.flexor_offset_angle) return loc_rot * loc_span * loc_mount_orient * loc_mount * loc_thickness @@ -1055,6 +1055,7 @@ class ElbowJoint(Model): @assembly() def assembly(self, angle: float = 0) -> Cq.Assembly: + assert 0 <= angle <= self.motion_span result = ( Cq.Assembly() .addS(self.child_joint(), name="child", diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index f67c0a2..91d81ad 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -2,8 +2,23 @@ import unittest import cadquery as Cq import nhf.touhou.houjuu_nue as M import nhf.touhou.houjuu_nue.joints as MJ +import nhf.touhou.houjuu_nue.electronics as ME from nhf.checks import pairwise_intersection +class TestElectronics(unittest.TestCase): + + def test_flexor(self): + flexor = ME.Flexor( + motion_span=60, + ) + self.assertAlmostEqual( + flexor.target_length_at_angle(0), + flexor.actuator.stroke_length + flexor.actuator.conn_length) + self.assertAlmostEqual( + flexor.target_length_at_angle(flexor.motion_span), + flexor.actuator.conn_length) + + class TestJoints(unittest.TestCase): def test_shoulder_collision_of_torsion_joint(self): diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 3610d4c..ce3fe7b 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -6,6 +6,7 @@ import math from enum import Enum 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, TargetKind, target, assembly, submodel @@ -71,8 +72,7 @@ class WingProfile(Model): parent_arm_radius=30.0, hole_diam=4.0, angle_neutral=-30.0, - # The left side wrist is too small for an actuator to work - actuator=None, #LINEAR_ACTUATOR_10, + actuator=LINEAR_ACTUATOR_10, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 -- 2.44.1 From ddbf904f5842bc6955e9548237e74f2f9d13af15 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 22 Jul 2024 15:02:26 -0700 Subject: [PATCH 131/187] feat: Electronic board assembly --- nhf/parts/box.py | 25 ++++++---- nhf/touhou/houjuu_nue/common.py | 18 +++++++ nhf/touhou/houjuu_nue/electronics.py | 72 +++++++++++++++++++++++++--- nhf/touhou/houjuu_nue/wing.py | 52 +++++++++++++------- 4 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 nhf/touhou/houjuu_nue/common.py diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 3498f6d..36828fe 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -34,6 +34,11 @@ class Hole: diam: Optional[float] = None tag: Optional[str] = None + @property + def rev_tag(self) -> str: + assert self.tag is not None + return self.tag + "_rev" + @dataclass class MountingBox(Model): """ @@ -56,6 +61,11 @@ class MountingBox(Model): # Determines the position of side tags flip_y: bool = False + def __post_init__(self): + for i, hole in enumerate(self.holes): + if hole.tag is None: + hole.tag = f"conn{i}" + @target(kind=TargetKind.DXF) def profile(self) -> Cq.Sketch: bx, by = 0, 0 @@ -84,12 +94,11 @@ class MountingBox(Model): ) plane = result.copyWorkplane(Cq.Workplane('XY')).workplane(offset=self.thickness) reverse_plane = result.copyWorkplane(Cq.Workplane('XY')) - for i, hole in enumerate(self.holes): - tag = hole.tag if hole.tag else f"conn{i}" - plane.moveTo(hole.x, hole.y).tagPlane(tag) + for hole in self.holes: + assert hole.tag + plane.moveTo(hole.x, hole.y).tagPlane(hole.tag) if self.generate_reverse_tags: - rev_tag = hole.tag + "_rev" if hole.tag else f"conn{i}_rev" - reverse_plane.moveTo(hole.x, hole.y).tagPlane(rev_tag, '-Z') + reverse_plane.moveTo(hole.x, hole.y).tagPlane(hole.rev_tag, '-Z') if self.generate_side_tags: result.faces("Z").val().Center()).tagPlane("left") @@ -106,10 +115,10 @@ class MountingBox(Model): Cq.Assembly() .add(self.generate(), name="box") ) - for i in range(len(self.holes)): - result.markPlane(f"box?conn{i}") + for hole in self.holes: + result.markPlane(f"box?{hole.tag}") if self.generate_reverse_tags: - result.markPlane(f"box?conn{i}_rev") + result.markPlane(f"box?{hole.rev_tag}") if self.generate_side_tags: ( result diff --git a/nhf/touhou/houjuu_nue/common.py b/nhf/touhou/houjuu_nue/common.py new file mode 100644 index 0000000..5d3d0a0 --- /dev/null +++ b/nhf/touhou/houjuu_nue/common.py @@ -0,0 +1,18 @@ +from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob + +NUT_COMMON = HexNut( + # FIXME: measure + mass=0.0, + diam_thread=4.0, + pitch=0.7, + thickness=3.2, + width=7.0, +) +BOLT_COMMON = FlatHeadBolt( + # FIXME: measure + mass=0.0, + diam_head=8.0, + height_head=2.0, + diam_thread=4.0, + height_thread=20.0, +) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 02468ec..ae114e1 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -1,13 +1,16 @@ """ Electronic components """ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional, Tuple import math import cadquery as Cq -from nhf.materials import Role +from nhf.build import Model, TargetKind, target, assembly, submodel +from nhf.materials import Role, Material +from nhf.parts.box import MountingBox, Hole from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut +from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON import nhf.utils @dataclass(frozen=True) @@ -268,8 +271,6 @@ class BatteryBox18650(Item): ) - - LINEAR_ACTUATOR_50 = LinearActuator( mass=34.0, stroke_length=50, @@ -427,6 +428,65 @@ class Flexor: @dataclass -class ElectronicBoard: +class ElectronicBoard(Model): - hole_diam: float = 4.0 + name: str = "electronic-board" + nut: HexNut = NUT_COMMON + bolt: FlatHeadBolt = BOLT_COMMON + length: float = 70.0 + width: float = 170.0 + mount_holes: list[Hole] = field(default_factory=lambda: [ + Hole(x=30, y=80), + Hole(x=30, y=-80), + Hole(x=-30, y=80), + Hole(x=-30, y=-80), + ]) + panel_thickness: float = 25.4 / 16 + mount_panel_thickness: float = 25.4 / 4 + material: Material = Material.WOOD_BIRCH + + @property + def mount_hole_diam(self) -> float: + return self.bolt.diam_thread + + def __post_init__(self): + super().__init__(name=self.name) + + @submodel(name="panel") + def panel(self) -> MountingBox: + return MountingBox( + holes=self.mount_holes, + hole_diam=self.mount_hole_diam, + length=self.length, + width=self.width, + centred=(True, True), + thickness=self.panel_thickness, + generate_reverse_tags=True, + ) + + def assembly(self) -> Cq.Assembly: + panel = self.panel() + result = ( + Cq.Assembly() + .addS(panel.generate(), name="panel", + role=Role.STRUCTURE, material=self.material) + ) + for hole in panel.holes: + spacer_name = f"{hole.tag}_spacer" + bolt_name = f"{hole.tag}_bolt" + ( + result + .add(self.nut.assembly(), name=spacer_name) + .add(self.bolt.assembly(), name=bolt_name) + .constrain( + f"{spacer_name}?top", + f"panel?{hole.rev_tag}", + "Plane" + ) + .constrain( + f"{bolt_name}?root", + f"panel?{hole.tag}", + "Plane", param=0 + ) + ) + return result.solve() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index ce3fe7b..bc1c690 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -90,7 +90,7 @@ class WingProfile(Model): elbow_rotate: float = -5.0 wrist_rotate: float = -30.0 # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) - elbow_axle_pos: float = 0.3 + elbow_axle_pos: float = 0.5 wrist_axle_pos: float = 0.0 # False for the right side, True for the left side @@ -99,6 +99,8 @@ class WingProfile(Model): def __post_init__(self): super().__init__(name=self.name) + assert self.electronic_board.length == self.shoulder_height + self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) if self.flip: @@ -114,6 +116,7 @@ class WingProfile(Model): self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, self.shoulder_angle_bias) self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2 + assert self.spacer_thickness == self.root_joint.child_mount_thickness @property @@ -278,21 +281,16 @@ class WingProfile(Model): flip_y=self.flip, ) @submodel(name="spacer-s0-electronic") - def spacer_s0_electronic(self) -> MountingBox: - holes = [ - Hole(x=30, y=80), - Hole(x=30, y=-80), - Hole(x=-30, y=80), - Hole(x=-30, y=-80), - ] + def spacer_s0_electronic_mount(self) -> MountingBox: return MountingBox( - holes=holes, - hole_diam=self.electronic_board.hole_diam, + holes=self.electronic_board.mount_holes, + hole_diam=self.electronic_board.mount_hole_diam, length=self.root_height, - width=170, + width=self.electronic_board.width, centred=(True, True), thickness=self.spacer_thickness, - flip_y=self.flip + flip_y=self.flip, + generate_reverse_tags=True, ) def surface_s0(self, top: bool = False) -> Cq.Workplane: @@ -310,7 +308,7 @@ class WingProfile(Model): self.shoulder_joint.parent_arm_loc() * loc_tip), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), - ("electronic", Cq.Location.from2d(-55, 75, 64)), + ("electronic_mount", Cq.Location.from2d(-55, 75, 64)), ] result = extrude_with_markers( self.profile_s0(top=top), @@ -323,7 +321,7 @@ class WingProfile(Model): return result @assembly() - def assembly_s0(self) -> Cq.Assembly: + def assembly_s0(self, ignore_detail: bool=False) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s0(top=True), name="bot", @@ -343,7 +341,7 @@ class WingProfile(Model): for o, tag in [ (self.spacer_s0_shoulder().generate(), "shoulder"), (self.spacer_s0_base().generate(), "base"), - (self.spacer_s0_electronic().generate(), "electronic"), + (self.spacer_s0_electronic_mount().generate(), "electronic_mount"), ]: top_tag, bot_tag = "top", "bot" if self.flip: @@ -357,6 +355,27 @@ class WingProfile(Model): .constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane") .constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis") ) + if not ignore_detail: + result.add(self.electronic_board.assembly(), name="electronic_board") + for hole in self.electronic_board.mount_holes: + assert hole.tag + nut_name = f"electronic_board_{hole.tag}_nut" + ( + result + .addS( + self.electronic_board.nut.assembly(), + name=nut_name) + .constrain( + f"electronic_mount?{hole.rev_tag}", + f'{nut_name}?top', + "Plane" + ) + .constrain( + f"electronic_mount?{hole.tag}", + f'electronic_board/{hole.tag}_spacer?bot', + "Plane" + ) + ) return result.solve() @@ -804,6 +823,7 @@ class WingProfile(Model): elbow_wrist_deflection: float = 0.0, root_offset: int = 5, fastener_pos: float = 0.0, + ignore_detail: bool = False, ) -> Cq.Assembly(): if parts is None: parts = [ @@ -824,7 +844,7 @@ class WingProfile(Model): tag_top, tag_bot = tag_bot, tag_top if "s0" in parts: - result.add(self.assembly_s0(), name="s0") + result.add(self.assembly_s0(ignore_detail=ignore_detail), name="s0") if "root" in parts: result.addS(self.root_joint.assembly( offset=root_offset, -- 2.44.1 From be118be6cca240f16d15d8db212a023433a5e316 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 22 Jul 2024 15:20:09 -0700 Subject: [PATCH 132/187] feat: Use bezier curve for inner s0 --- nhf/touhou/houjuu_nue/wing.py | 72 ++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index bc1c690..69cadaa 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -36,8 +36,10 @@ class WingProfile(Model): shoulder_width: float = 36.0 shoulder_tip_x: float = -260.0 shoulder_tip_y: float = 165.0 - shoulder_mid_x: float = -125.0 - shoulder_mid_y: float = 75.0 + shoulder_tip_bezier_x: float = 100.0 + shoulder_tip_bezier_y: float = -50.0 + shoulder_base_bezier_x: float = -30.0 + shoulder_base_bezier_y: float = 30.0 s0_hole_loc: Cq.Location = Cq.Location.from2d(-25, 33) s0_hole_diam: float = 15.0 @@ -153,8 +155,7 @@ class WingProfile(Model): def outer_profile_s0(self) -> Cq.Sketch: """ - The outer boundary of s0, used to produce the curved panel and the - top/bottom slots + The outer boundary of s0 top/bottom slots """ tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y @@ -170,15 +171,35 @@ class WingProfile(Model): # (tip_x - 10, tip_y), #) ) + def inner_profile_s0(self) -> Cq.Edge: + """ + The inner boundary of s0 + """ + tip_x = self.shoulder_tip_x + tip_y = self.shoulder_tip_y + dx2 = self.shoulder_tip_bezier_x + dy2 = self.shoulder_tip_bezier_y + dx1 = self.shoulder_base_bezier_x + dy1 = self.shoulder_base_bezier_y + sw = self.shoulder_width + return Cq.Edge.makeBezier( + [ + Cq.Vector(*p) + for p in [ + (tip_x, tip_y - sw), + (tip_x + dx2, tip_y - sw + dy2), + (-self.base_width + dx1, dy1), + (-self.base_width, 0), + ] + ] + ) @property def shoulder_angle_neutral(self) -> float: """ Returns the neutral angle of the shoulder """ - dx = self.shoulder_mid_x - self.shoulder_tip_x - dy = -(self.shoulder_mid_y - (self.shoulder_tip_y - self.shoulder_width)) - result = math.degrees(math.atan2(dy, dx)) + result = math.degrees(math.atan2(-self.shoulder_tip_bezier_y, self.shoulder_tip_bezier_x)) assert result >= 0 return result @@ -186,8 +207,10 @@ class WingProfile(Model): def profile_s0(self, top: bool = True) -> Cq.Sketch: tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y - mid_x = self.shoulder_mid_x - mid_y = self.shoulder_mid_y + dx2 = self.shoulder_tip_bezier_x + dy2 = self.shoulder_tip_bezier_y + dx1 = self.shoulder_base_bezier_x + dy1 = self.shoulder_base_bezier_y sw = self.shoulder_width sketch = ( self.outer_profile_s0() @@ -196,14 +219,12 @@ class WingProfile(Model): (tip_x, tip_y), (tip_x, tip_y - sw), ) - .segment( + .bezier([ (tip_x, tip_y - sw), - (mid_x, mid_y), - ) - .segment( - (mid_x, mid_y), + (tip_x + dx2, tip_y - sw + dy2), + (-self.base_width + dx1, dy1), (-self.base_width, 0), - ) + ]) .assemble() .push([self.shoulder_axle_loc.to2d_pos()]) .circle(self.shoulder_joint.radius, mode='a') @@ -231,6 +252,18 @@ class WingProfile(Model): plane.moveTo(0, 0).tagPlane("bot") plane.moveTo(0, self.root_height + t*2).tagPlane("top") return result + def inner_shell_s0(self) -> Cq.Workplane: + t = self.panel_thickness + #profile = Cq.Wire.assembleEdges(self.inner_profile_s0()) + result = ( + Cq.Workplane('XZ') + .rect(t, self.root_height + t*2, centered=(False, False)) + .sweep(self.inner_profile_s0()) + ) + plane = result.copyWorkplane(Cq.Workplane('XZ')) + plane.moveTo(t, 0).tagPlane("bot") + plane.moveTo(t, self.root_height + t*2).tagPlane("top") + return result @submodel(name="spacer-s0-shoulder") def spacer_s0_shoulder(self) -> MountingBox: @@ -318,6 +351,7 @@ class WingProfile(Model): ) h = self.panel_thickness if top else 0 result.copyWorkplane(Cq.Workplane('XZ')).moveTo(0, h).tagPlane("corner") + result.copyWorkplane(Cq.Workplane('XZ')).moveTo(-self.base_width, h).tagPlane("corner_left") return result @assembly() @@ -331,12 +365,16 @@ class WingProfile(Model): loc=Cq.Location((0, 0, self.root_height + self.panel_thickness))) .constrain("bot", "Fixed") .constrain("top", "Fixed") - #.constrain("bot@faces@>Z", "top@faces@Z", "top@faces@ Date: Tue, 23 Jul 2024 16:49:25 -0700 Subject: [PATCH 133/187] feat: Anti-collision shoulder joint --- nhf/parts/box.py | 10 ++- nhf/touhou/houjuu_nue/__init__.py | 7 +- nhf/touhou/houjuu_nue/joints.py | 124 ++++++++++++++++++++++-------- nhf/touhou/houjuu_nue/wing.py | 42 +++++----- 4 files changed, 129 insertions(+), 54 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 36828fe..57a5d63 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -58,6 +58,8 @@ class MountingBox(Model): # Generate tags on the opposite side generate_reverse_tags: bool = False + centre_bot_top_tags: bool = False + # Determines the position of side tags flip_y: bool = False @@ -105,8 +107,12 @@ class MountingBox(Model): result.faces(">Y").workplane(origin=result.vertices("Y and >Z").val().Center()).tagPlane("right") c_y = ">Y" if self.flip_y else "Z").val().Center()).tagPlane("bot") - result.faces(">X").workplane(origin=result.vertices(f">X and {c_y} and >Z").val().Center()).tagPlane("top") + if self.centre_bot_top_tags: + result.faces("Z").val().Center()).tagPlane("bot") + result.faces(">X").workplane(origin=result.edges(f">X and >Z").val().Center()).tagPlane("top") + else: + result.faces("Z").val().Center()).tagPlane("bot") + result.faces(">X").workplane(origin=result.vertices(f">X and {c_y} and >Z").val().Center()).tagPlane("top") result.faces(">Z").tag("dir") return result diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index da2d1c1..c6b4d4c 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -41,6 +41,7 @@ import nhf.touhou.houjuu_nue.harness as MH from nhf.parts.item import Item import nhf.utils +WING_DEFLECT = 10.0 @dataclass class Parameters(Model): """ @@ -51,7 +52,7 @@ class Parameters(Model): wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR( name="r1", - shoulder_angle_bias = 15.0, + shoulder_angle_bias = WING_DEFLECT, s0_top_hole=False, s0_bot_hole=True, )) @@ -62,7 +63,7 @@ class Parameters(Model): )) wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR( name="r3", - shoulder_angle_bias = 15.0, + shoulder_angle_bias = WING_DEFLECT, s0_top_hole=True, s0_bot_hole=False, )) @@ -75,7 +76,7 @@ class Parameters(Model): wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( name="l2", wrist_angle=-30.0, - shoulder_angle_bias = 15.0, + shoulder_angle_bias = WING_DEFLECT, s0_top_hole=True, s0_bot_hole=True, )) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 56433fb..9cd530a 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -261,18 +261,23 @@ class ShoulderJoint(Model): # On the parent side, drill vertical holes - parent_conn_hole_diam: float = 6.0 - # Position of the holes relative + parent_conn_hole_diam: float = 4.0 + # Position of the holes relative centre line parent_conn_hole_pos: list[Tuple[float, float]] = field(default_factory=lambda: [ - (15, 8), - (15, -8), + (20, 8), + (20, -8), ]) + # Distance from centre of lips to the axis + parent_lip_ext: float = 40.0 + parent_lip_length: float = 25.0 parent_lip_width: float = 30.0 parent_lip_thickness: float = 5.0 - parent_lip_ext: float = 40.0 - parent_lip_guard_height: float = 8.0 + + # The parent side has arms which connect to the lips + parent_arm_width: float = 25.0 + parent_arm_height: float = 12.0 # Generates a child guard which covers up the internals. The lip length is # relative to the +X surface of the guard. @@ -281,19 +286,19 @@ class ShoulderJoint(Model): # guard length measured from axle child_lip_length: float = 40.0 child_lip_width: float = 20.0 - child_conn_hole_diam: float = 6.0 + child_conn_hole_diam: float = 4.0 # Measured from centre of axle - child_conn_hole_pos: list[float] = field(default_factory=lambda: [15, 25]) + child_conn_hole_pos: list[float] = field(default_factory=lambda: [8, 19, 30]) child_core_thickness: float = 3.0 # Rotates the torsion joint to avoid collisions or for some other purpose - axis_rotate_bot: float = 225.0 - axis_rotate_top: float = -225.0 + axis_rotate_bot: float = 90 + axis_rotate_top: float = 0 directrix_id: int = 0 - angle_neutral: float = 10.0 - angle_max_deflection: float = 80.0 + angle_neutral: float = -15.0 + angle_max_deflection: float = 90.0 def __post_init__(self): assert self.parent_lip_length * 2 < self.height @@ -302,51 +307,105 @@ class ShoulderJoint(Model): def radius(self): return self.torsion_joint.radius - def parent_arm_loc(self) -> Cq.Location: + def parent_lip_loc(self, left: bool=True) -> Cq.Location: """ 2d location of the arm surface on the parent side, relative to axle """ - return Cq.Location.rot2d(self.angle_neutral) * Cq.Location.from2d(self.parent_lip_ext, 0, 0) + dy = self.parent_arm_width / 2 + sign = 1 if left else -1 + loc_dir = Cq.Location((0,sign * dy,0), (0, 0, 1), sign * 90) + return Cq.Location.from2d(self.parent_lip_ext, 0, 0) * loc_dir + + @property + def _max_contraction_angle(self) -> float: + return self.angle_max_deflection + self.angle_neutral + + def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid: + """ + Generates a cylindrical sector which cuts away overlapping regions of the child and parent + """ + aspect = self.child_guard_width / self.parent_arm_width + theta = math.radians(self._max_contraction_angle) + theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect)) + angle = math.degrees(theta_p) + assert 0 <= angle <= 90 + # outer radius of the cut, overestimated + cut_radius = math.sqrt(self.child_guard_width ** 2 + self.parent_arm_width ** 2) + span = 180 + result = ( + Cq.Solid.makeCylinder( + height=self.height, + radius=cut_radius, + angleDegrees=span, + ).cut(Cq.Solid.makeCylinder( + height=self.height, + radius=self.torsion_joint.radius, + )) + ) + if parent: + angle = - span - angle + else: + angle = self._max_contraction_angle - angle + result = result.located(Cq.Location((0,0,-self.height/2), (0,0,1), angle)) + if mirror: + result = result.mirror('XZ') + return result def parent(self, top: bool = False) -> Cq.Assembly: joint = self.torsion_joint # Thickness of the lip connecting this joint to the wing root - assert self.parent_lip_width <= joint.radius_track * 2 + assert self.parent_arm_width <= joint.radius_track * 2 assert self.parent_lip_ext > joint.radius_track - lip_guard = ( + arm = ( Cq.Solid.makeBox( - self.parent_lip_ext, - self.parent_lip_width, - self.parent_lip_guard_height) - .located(Cq.Location((0, -self.parent_lip_width/2 , 0))) - .cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_lip_guard_height)) + self.parent_lip_ext + self.parent_lip_width / 2, + self.parent_arm_width, + self.parent_arm_height) + .located(Cq.Location((0, -self.parent_arm_width/2 , 0))) + .cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_arm_height)) + .cut(self._contraction_cut_geometry(parent=True, mirror=top)) ) - lip = MountingBox( + lip_args = dict( length=self.parent_lip_length, width=self.parent_lip_width, thickness=self.parent_lip_thickness, + hole_diam=self.parent_conn_hole_diam, + generate_side_tags=False, + ) + lip1 = MountingBox( + **lip_args, + holes=[ + Hole(x=self.height / 2 - x, y=-y) + for x, y in self.parent_conn_hole_pos + ], + ) + lip2 = MountingBox( + **lip_args, holes=[ Hole(x=self.height / 2 - x, y=y) for x, y in self.parent_conn_hole_pos ], - hole_diam=self.parent_conn_hole_diam, - generate_side_tags=False, ) + lip_dy = self.parent_arm_width / 2 - self.parent_lip_thickness # Flip so the lip's holes point to -X loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90) - # so they point to +X - loc_dir = Cq.Location((0,0,0), (0, 0, 1), 180) - loc_pos = Cq.Location((self.parent_lip_ext - self.parent_lip_thickness, 0, 0)) + loc_dir1 = Cq.Location((0,lip_dy,0), (0, 0, 1), -90) + loc_dir2 = Cq.Location((0,-lip_dy,0), (0, 0, 1), 90) + loc_pos = Cq.Location((self.parent_lip_ext, 0, 0)) rot = -self.axis_rotate_top if top else self.axis_rotate_bot + lip_p_tag, lip_n_tag = "lip_right", "lip_left" + if not top: + lip_p_tag, lip_n_tag = lip_n_tag, lip_p_tag result = ( Cq.Assembly() .add(joint.track(), name="track", loc=Cq.Location((0, 0, 0), (0, 0, 1), rot)) - .add(lip_guard, name="lip_guard") - .add(lip.generate(), name="lip", loc=loc_pos * loc_dir * loc_axis) + .add(arm, name="arm") + .add(lip1.generate(), name=lip_p_tag, loc=loc_pos * loc_dir1 * loc_axis) + .add(lip2.generate(), name=lip_n_tag, loc=loc_pos * loc_dir2 * loc_axis) ) return result @@ -421,6 +480,7 @@ class ShoulderJoint(Model): combine='cut', centered=(False, True, True), ) + .cut(self._contraction_cut_geometry(parent=False)) ) core = ( Cq.Workplane('XY') @@ -462,7 +522,11 @@ class ShoulderJoint(Model): return result @assembly() - def assembly(self, fastener_pos: float = 0.0, deflection: float = 0) -> Cq.Assembly: + def assembly( + self, + fastener_pos: float = 0.0, + deflection: float = 0.0, + ) -> Cq.Assembly: assert deflection <= self.angle_max_deflection directrix = self.directrix_id mat = Material.RESIN_TRANSPERENT diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 69cadaa..a548af8 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -116,7 +116,7 @@ class WingProfile(Model): assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias - self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, self.shoulder_angle_bias) + self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, 0) self.shoulder_joint.child_guard_width = self.s1_thickness + self.panel_thickness * 2 assert self.spacer_thickness == self.root_joint.child_mount_thickness @@ -201,7 +201,7 @@ class WingProfile(Model): """ result = math.degrees(math.atan2(-self.shoulder_tip_bezier_y, self.shoulder_tip_bezier_x)) assert result >= 0 - return result + return result / 2 @target(name="profile-s0", kind=TargetKind.DXF) def profile_s0(self, top: bool = True) -> Cq.Sketch: @@ -266,17 +266,18 @@ class WingProfile(Model): return result @submodel(name="spacer-s0-shoulder") - def spacer_s0_shoulder(self) -> MountingBox: + def spacer_s0_shoulder(self, left: bool=True) -> MountingBox: """ Shoulder side serves double purpose for mounting shoulder joint and structural support """ + sign = 1 if left else -1 holes = [ hole for i, (x, y) in enumerate(self.shoulder_joint.parent_conn_hole_pos) for hole in [ - Hole(x=x, y=y, tag=f"conn_top{i}"), - Hole(x=-x, y=y, tag=f"conn_bot{i}"), + Hole(x=x, y=sign * y, tag=f"conn_top{i}"), + Hole(x=-x, y=sign * y, tag=f"conn_bot{i}"), ] ] return MountingBox( @@ -287,6 +288,7 @@ class WingProfile(Model): hole_diam=self.shoulder_joint.parent_conn_hole_diam, centred=(True, True), flip_y=self.flip, + centre_bot_top_tags=True, ) @submodel(name="spacer-s0-shoulder") def spacer_s0_base(self) -> MountingBox: @@ -329,19 +331,19 @@ class WingProfile(Model): def surface_s0(self, top: bool = False) -> Cq.Workplane: base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10 base_dy = self.root_joint.hirth_joint.joint_height - loc_tip = Cq.Location(0, -self.shoulder_joint.parent_lip_width / 2) #mid_spacer_loc = ( # Cq.Location.from2d(0, -self.shoulder_width/2) * # self.shoulder_axle_loc * # Cq.Location.rot2d(self.shoulder_joint.angle_neutral) #) + axle_rotate = Cq.Location.rot2d(-self.shoulder_angle_neutral) tags = [ - ("shoulder", - self.shoulder_axle_loc * - self.shoulder_joint.parent_arm_loc() * - loc_tip), + ("shoulder_left", + self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=True)), + ("shoulder_right", + self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), - ("electronic_mount", Cq.Location.from2d(-55, 75, 64)), + ("electronic_mount", Cq.Location.from2d(-45, 75, 64)), ] result = extrude_with_markers( self.profile_s0(top=top), @@ -377,7 +379,8 @@ class WingProfile(Model): #.constrain("top?corner_left", "inner_shell?top", "Point") ) for o, tag in [ - (self.spacer_s0_shoulder().generate(), "shoulder"), + (self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"), + (self.spacer_s0_shoulder(left=False).generate(), "shoulder_right"), (self.spacer_s0_base().generate(), "base"), (self.spacer_s0_electronic_mount().generate(), "electronic_mount"), ]: @@ -902,13 +905,14 @@ class WingProfile(Model): fastener_pos=fastener_pos, deflection=angle), name="shoulder") if "s0" in parts and "shoulder" in parts: - ( - result - .constrain(f"s0/shoulder?conn_top0", f"shoulder/parent_{tag_top}/lip?conn0", "Plane") - .constrain(f"s0/shoulder?conn_top1", f"shoulder/parent_{tag_top}/lip?conn1", "Plane") - .constrain(f"s0/shoulder?conn_bot0", f"shoulder/parent_{tag_bot}/lip?conn0", "Plane") - .constrain(f"s0/shoulder?conn_bot1", f"shoulder/parent_{tag_bot}/lip?conn1", "Plane") - ) + for i in range(len(self.shoulder_joint.parent_conn_hole_pos)): + ( + result + .constrain(f"s0/shoulder_left?conn_top{i}", f"shoulder/parent_{tag_top}/lip_left?conn{i}", "Plane") + .constrain(f"s0/shoulder_left?conn_bot{i}", f"shoulder/parent_{tag_bot}/lip_left?conn{i}", "Plane") + .constrain(f"s0/shoulder_right?conn_top{i}", f"shoulder/parent_{tag_top}/lip_right?conn{i}", "Plane") + .constrain(f"s0/shoulder_right?conn_bot{i}", f"shoulder/parent_{tag_bot}/lip_right?conn{i}", "Plane") + ) if "s1" in parts: result.add(self.assembly_s1(), name="s1") if "s1" in parts and "shoulder" in parts: -- 2.44.1 From 656a2ae5bbfee97ded2ebef15776adcf98cc63ae Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 23 Jul 2024 19:13:06 -0700 Subject: [PATCH 134/187] feat: Stable positioning of actuators --- nhf/parts/box.py | 9 +++- nhf/touhou/houjuu_nue/joints.py | 57 ++++++++++++-------- nhf/touhou/houjuu_nue/wing.py | 95 +++++++++++++++++++++++---------- 3 files changed, 109 insertions(+), 52 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 57a5d63..9aac091 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -59,6 +59,7 @@ class MountingBox(Model): generate_reverse_tags: bool = False centre_bot_top_tags: bool = False + centre_left_right_tags: bool = False # Determines the position of side tags flip_y: bool = False @@ -103,8 +104,12 @@ class MountingBox(Model): reverse_plane.moveTo(hole.x, hole.y).tagPlane(hole.rev_tag, '-Z') if self.generate_side_tags: - result.faces("Z").val().Center()).tagPlane("left") - result.faces(">Y").workplane(origin=result.vertices("Y and >Z").val().Center()).tagPlane("right") + if self.centre_left_right_tags: + result.faces("Z").val().Center()).tagPlane("left") + result.faces(">Y").workplane(origin=result.edges(">Y and >Z").val().Center()).tagPlane("right") + else: + result.faces("Z").val().Center()).tagPlane("left") + result.faces(">Y").workplane(origin=result.vertices("Y and >Z").val().Center()).tagPlane("right") c_y = ">Y" if self.flip_y else " self.disk_joint.radius_housing @@ -977,15 +979,13 @@ class ElbowJoint(Model): axle is at position 0, and parent direction is -X """ return Cq.Location.from2d(-self.parent_arm_radius, 0, 0) - def child_arm_loc(self, flip: bool = False, angle: float = 0.0) -> Cq.Location: + def child_arm_loc(self, angle: float = 0.0) -> Cq.Location: """ 2d Location of the centre of the arm surface on the child side, assuming axle is at position 0, and parent direction is -X - - Set `flip=True` to indicate that the joint is supposed to be installed upside down """ result = Cq.Location.rot2d(self.angle_neutral + angle) * Cq.Location.from2d(self.child_arm_radius, 0, 180) - return result.flip_y() if flip else result + return result.flip_y() if self.flip else result def actuator_mount(self) -> Cq.Workplane: holes = [ Hole(x=0, y=0, tag="mount"), @@ -1000,26 +1000,31 @@ class ElbowJoint(Model): generate_side_tags=False, ) return mbox.generate() - def actuator_mount_loc(self, child: bool) -> Cq.Location: - # Orientes the hole surface so it faces +X - loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) + def actuator_mount_loc( + self, + child: bool = False, + # If set to true, use the local coordinate system + unflip: bool = False, + ) -> Cq.Location: # Moves the hole so the axle of the mount is perpendicular to it loc_mount = Cq.Location.from2d(self.flexor.mount_height, 0) * Cq.Location.rot2d(180) loc_mount_orient = Cq.Location.rot2d(self.flexor_mount_rot * (-1 if child else 1)) # Moves the hole to be some distance apart from 0 mount_r, mount_loc_angle, mount_parent_r = self.flexor.open_pos() loc_span = Cq.Location.from2d(mount_r if child else mount_parent_r, 0) - r = (-mount_loc_angle if child else 0) + 180 - loc_rot = Cq.Location.rot2d(r + self.flexor_offset_angle) - return loc_rot * loc_span * loc_mount_orient * loc_mount * loc_thickness + r = (-mount_loc_angle - self.angle_neutral if child else 0) + 180 + self.flexor_offset_angle + loc_rot = Cq.Location.rot2d(r) + loc = loc_rot * loc_span * loc_mount_orient * loc_mount + return loc.flip_y() if self.flip and not child and not unflip else loc def lip(self) -> Cq.Workplane: + sign = -1 if self.flip else 1 holes = [ h for i, x in enumerate(self.hole_pos) for h in [ - Hole(x=x, tag=f"conn_top{i}"), - Hole(x=-x, tag=f"conn_bot{i}") + Hole(x=sign * x, tag=f"conn_top{i}"), + Hole(x=-sign * x, tag=f"conn_bot{i}") ] ] mbox = MountingBox( @@ -1047,17 +1052,22 @@ class ElbowJoint(Model): Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * Cq.Location((0, 0, 0), (0, 1, 0), 90) ) - loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, 0), (0, 0, 1), angle) + loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, 0)) loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness)) disk_cut = self.disk_joint._disk_cut().located( loc_lip.inverse * loc_cut_rel * loc_disk) result = ( Cq.Assembly() - .add(self.disk_joint.disk(), name="disk", loc=Cq.Location((0, 0, -dz))) + .add(self.disk_joint.disk(), name="disk", loc=Cq.Location((0, 0, -dz), (0,0,1), angle)) .add(self.lip().cut(disk_cut), name="lip", loc=loc_disk.inverse * loc_lip) ) + # Orientes the hole surface so it faces +X + loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) if self.flexor: - result.add(self.actuator_mount(), name="act", loc=self.actuator_mount_loc(child=True)) + result.add( + self.actuator_mount(), + name="act", + loc=self.actuator_mount_loc(child=True) * loc_thickness) return result @target(name="parent-lower") @@ -1090,31 +1100,32 @@ class ElbowJoint(Model): ) housing = self.disk_joint.housing_upper() housing_loc = Cq.Location( - (0, 0, 0), + (0, 0, housing_dz), (0, 0, 1), -self.disk_joint.tongue_span / 2 + self.angle_neutral ) lip_dz = self.lip_thickness - loc_net_housing = axial_offset * housing_loc result = ( Cq.Assembly() - .add(housing, name="housing", loc=Cq.Location((0, 0, housing_dz))) + # rotate so 0 degree is at +X + .add(housing, name="housing", loc=housing_loc) .add(self.lip(), name="lip", loc= - loc_net_housing.inverse * + axial_offset.inverse * Cq.Location((0, 0, 0), (0, 1, 0), 180) * Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * Cq.Location((0, 0, 0), (0, 1, 0), 90)) - .add(connector, name="connector", - loc=loc_net_housing.inverse * axial_offset) + .add(connector, name="connector") #.constrain("housing", "Fixed") #.constrain("connector", "Fixed") #.solve() ) if self.flexor: + # Orientes the hole surface so it faces +X + loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) result.add( self.actuator_mount(), name="act", - loc=self.actuator_mount_loc(child=False)) + loc=self.actuator_mount_loc(child=False) * loc_thickness) return result @assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index a548af8..2b0f7a2 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -14,7 +14,7 @@ from nhf.parts.box import box_with_centre_holes, MountingBox, Hole from nhf.parts.joints import HirthJoint from nhf.parts.planar import extrude_with_markers from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint -from nhf.touhou.houjuu_nue.electronics import LINEAR_ACTUATOR_10, LINEAR_ACTUATOR_50, ElectronicBoard +from nhf.touhou.houjuu_nue.electronics import LINEAR_ACTUATOR_21, LINEAR_ACTUATOR_50, ElectronicBoard import nhf.utils @dataclass(kw_only=True) @@ -57,7 +57,8 @@ class WingProfile(Model): hole_diam=4.0, angle_neutral=15.0, actuator=LINEAR_ACTUATOR_50, - flexor_offset_angle=-15, + flexor_offset_angle=0, + flip=False, )) # Distance between the two spacers on the elbow, halved elbow_h2: float = 5.0 @@ -74,7 +75,9 @@ class WingProfile(Model): parent_arm_radius=30.0, hole_diam=4.0, angle_neutral=-30.0, - actuator=LINEAR_ACTUATOR_10, + actuator=LINEAR_ACTUATOR_21, + flexor_offset_angle=0, + flip=True, )) # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -608,12 +611,15 @@ class WingProfile(Model): ("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)), ] h = self.elbow_height / 2 - loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.parent_arm_loc() + rot_elbow = Cq.Location.rot2d(self.elbow_rotate) + loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc() tags_elbow = [ ("elbow_bot", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, -self.elbow_h2)), ("elbow_top", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, self.elbow_h2)), + ("elbow_act", self.elbow_axle_loc * rot_elbow * + self.elbow_joint.actuator_mount_loc()), ] profile = self.profile_s1() tags = tags_shoulder + tags_elbow @@ -639,6 +645,17 @@ class WingProfile(Model): segment_thickness=self.s1_thickness, dx=self.elbow_h2, ) + @submodel(name="spacer-s1-elbow-act") + def spacer_s1_elbow_act(self) -> MountingBox: + return MountingBox( + length=self.s1_thickness, + width=self.s1_thickness, + thickness=self.spacer_thickness, + holes=[Hole(x=0,y=0)], + centred=(True, True), + hole_diam=self.elbow_joint.hole_diam, + centre_left_right_tags=True, + ) @assembly() def assembly_s1(self) -> Cq.Assembly: result = ( @@ -651,13 +668,17 @@ class WingProfile(Model): .constrain("front@faces@>Z", "back@faces@ Cq.Workplane: - loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc(flip=self.flip) + loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc() tags_elbow = [ ("elbow_bot", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, self.elbow_h2)), ("elbow_top", self.elbow_axle_loc * loc_elbow * Cq.Location.from2d(0, -self.elbow_h2)), ] - loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc() + rot_wrist = Cq.Location.rot2d(self.wrist_rotate) + loc_wrist = rot_wrist * self.wrist_joint.parent_arm_loc() tags_wrist = [ ("wrist_bot", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, -self.wrist_h2)), ("wrist_top", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, self.wrist_h2)), + ("wrist_act", self.wrist_axle_loc * rot_wrist * + self.wrist_joint.actuator_mount_loc()), ] profile = self.profile_s2() tags = tags_elbow + tags_wrist @@ -750,6 +774,17 @@ class WingProfile(Model): segment_thickness=self.s2_thickness, dx=self.wrist_h2, ) + @submodel(name="spacer-s2-wrist-act") + def spacer_s2_wrist_act(self) -> MountingBox: + return MountingBox( + length=self.s2_thickness, + width=self.s2_thickness, + thickness=self.spacer_thickness, + holes=[Hole(x=0,y=0)], + centred=(True, True), + hole_diam=self.wrist_joint.hole_diam, + centre_left_right_tags=True, + ) @assembly() def assembly_s2(self) -> Cq.Assembly: result = ( @@ -770,10 +805,15 @@ class WingProfile(Model): .constrain("back?wrist_bot", "bridge_back?wrist_bot", "Plane") .constrain("back?wrist_top", "bridge_back?wrist_top", "Plane") ) - for t in ["elbow_bot", "elbow_top", "wrist_bot", "wrist_top"]: + for o, t in [ + (self.spacer_s2_elbow(), "elbow_bot"), + (self.spacer_s2_elbow(), "elbow_top"), + (self.spacer_s2_wrist(), "wrist_bot"), + (self.spacer_s2_wrist(), "wrist_top"), + (self.spacer_s2_wrist_act(), "wrist_act"), + ]: is_top = t.endswith("_top") is_parent = t.startswith("elbow") - o = self.spacer_s2_elbow() if is_parent else self.spacer_s2_wrist() self._assembly_insert_spacer( result, o.generate(), @@ -793,7 +833,7 @@ class WingProfile(Model): return profile def surface_s3(self, front: bool = True) -> Cq.Workplane: - loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip) + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc() tags = [ ("wrist_bot", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, self.wrist_h2)), @@ -807,7 +847,7 @@ class WingProfile(Model): profile = self.profile_s3_extra() if profile is None: return None - loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc(flip=not self.flip) + loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc() tags = [ ("wrist_bot", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, self.wrist_h2)), @@ -929,32 +969,31 @@ class WingProfile(Model): if "s1" in parts and "elbow" in parts: ( result - .constrain("s1/elbow_top?conn0", f"elbow/parent_upper/lip?conn_{tag_top}0", "Plane") - .constrain("s1/elbow_top?conn1", f"elbow/parent_upper/lip?conn_{tag_top}1", "Plane") - .constrain("s1/elbow_bot?conn0", f"elbow/parent_upper/lip?conn_{tag_bot}0", "Plane") - .constrain("s1/elbow_bot?conn1", f"elbow/parent_upper/lip?conn_{tag_bot}1", "Plane") + .constrain("s1/elbow_top?conn0", "elbow/parent_upper/lip?conn_top0", "Plane") + .constrain("s1/elbow_top?conn1", "elbow/parent_upper/lip?conn_top1", "Plane") + .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/lip?conn_bot0", "Plane") + .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/lip?conn_bot1", "Plane") ) if "s2" in parts: result.add(self.assembly_s2(), name="s2") if "s2" in parts and "elbow" in parts: ( result - .constrain("s2/elbow_top?conn0", f"elbow/child/lip?conn_{tag_top}0", "Plane") - .constrain("s2/elbow_top?conn1", f"elbow/child/lip?conn_{tag_top}1", "Plane") - .constrain("s2/elbow_bot?conn0", f"elbow/child/lip?conn_{tag_bot}0", "Plane") - .constrain("s2/elbow_bot?conn1", f"elbow/child/lip?conn_{tag_bot}1", "Plane") + .constrain("s2/elbow_top?conn0", "elbow/child/lip?conn_top0", "Plane") + .constrain("s2/elbow_top?conn1", "elbow/child/lip?conn_top1", "Plane") + .constrain("s2/elbow_bot?conn0", "elbow/child/lip?conn_bot0", "Plane") + .constrain("s2/elbow_bot?conn1", "elbow/child/lip?conn_bot1", "Plane") ) if "wrist" in parts: angle = self.wrist_joint.motion_span * elbow_wrist_deflection result.add(self.wrist_joint.assembly(angle=angle), name="wrist") wrist_n_holes = len(self.wrist_joint.hole_pos) if "s2" in parts and "wrist" in parts: - # Mounted backwards to bend in other direction for i in range(wrist_n_holes): ( result - .constrain(f"s2/wrist_top?conn{i}", f"wrist/parent_upper/lip?conn_{tag_bot}{i}", "Plane") - .constrain(f"s2/wrist_bot?conn{i}", f"wrist/parent_upper/lip?conn_{tag_top}{i}", "Plane") + .constrain(f"s2/wrist_top?conn{i}", f"wrist/parent_upper/lip?conn_top{i}", "Plane") + .constrain(f"s2/wrist_bot?conn{i}", f"wrist/parent_upper/lip?conn_bot{i}", "Plane") ) if "s3" in parts: result.add(self.assembly_s3(), name="s3") @@ -962,8 +1001,8 @@ class WingProfile(Model): for i in range(wrist_n_holes): ( result - .constrain(f"s3/wrist_top?conn{i}", f"wrist/child/lip?conn_{tag_bot}{i}", "Plane") - .constrain(f"s3/wrist_bot?conn{i}", f"wrist/child/lip?conn_{tag_top}{i}", "Plane") + .constrain(f"s3/wrist_top?conn{i}", f"wrist/child/lip?conn_top{i}", "Plane") + .constrain(f"s3/wrist_bot?conn{i}", f"wrist/child/lip?conn_bot{i}", "Plane") ) if len(parts) > 1: result.solve() @@ -1167,9 +1206,11 @@ class WingL(WingProfile): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) self.elbow_joint.angle_neutral = 15.0 + self.elbow_joint.flip = True self.elbow_rotate = 5.0 self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0 self.wrist_rotate = -self.wrist_joint.angle_neutral + self.wrist_joint.flip = False super().__post_init__() -- 2.44.1 From 4e04d30ee201527f11d16e36ecd210b4e4b6f2ae Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 23 Jul 2024 22:12:46 -0700 Subject: [PATCH 135/187] feat: Use simple joint overlaps, not bridges --- nhf/touhou/houjuu_nue/wing.py | 295 ++++++++++++++++++++++------------ nhf/utils.py | 9 ++ 2 files changed, 201 insertions(+), 103 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 2b0f7a2..c33deda 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -55,7 +55,7 @@ class WingProfile(Model): movement_angle=55, ), hole_diam=4.0, - angle_neutral=15.0, + angle_neutral=30.0, actuator=LINEAR_ACTUATOR_50, flexor_offset_angle=0, flip=False, @@ -92,7 +92,9 @@ class WingProfile(Model): elbow_height: float wrist_bot_loc: Cq.Location wrist_height: float - elbow_rotate: float = -5.0 + elbow_rotate: float = 10.0 + elbow_joint_overlap_median: float = 0.3 + wrist_joint_overlap_median: float = 0.5 wrist_rotate: float = -30.0 # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) elbow_axle_pos: float = 0.5 @@ -428,6 +430,9 @@ class WingProfile(Model): """ Generates profile from shoulder and above. Subclass should implement """ + @target(name="profile-s2-bridge", kind=TargetKind.DXF) + def profile_s2_bridge(self) -> Optional[Cq.Sketch]: + return None @target(name="profile-s3-extra", kind=TargetKind.DXF) def profile_s3_extra(self) -> Optional[Cq.Sketch]: """ @@ -460,73 +465,44 @@ class WingProfile(Model): for p in points ]) ) - def _child_joint_extension_profile( - self, - axle_loc: Cq.Location, - radius: float, - angle_span: float, - bot: bool = False) -> Cq.Sketch: - """ - Creates a sector profile which accomodates extension - """ - sign = -1 if bot else 1 - axle_loc = axle_loc * Cq.Location.rot2d(-90 if bot else 90) - loc_h = Cq.Location.from2d(radius, 0) - start = axle_loc * loc_h - mid = axle_loc * Cq.Location.rot2d(-sign * angle_span/2) * loc_h - end = axle_loc * Cq.Location.rot2d(-sign * angle_span) * loc_h - return ( - Cq.Sketch() - .segment( - axle_loc.to2d_pos(), - start.to2d_pos(), - ) - .arc( - start.to2d_pos(), - mid.to2d_pos(), - end.to2d_pos(), - ) - .segment( - end.to2d_pos(), - axle_loc.to2d_pos(), - ) - .assemble() - ) - def _parent_joint_extension_profile( + def _joint_extension_cut_polygon( self, - loc_axle: Cq.Location, loc_bot: Cq.Location, loc_top: Cq.Location, + height: float, angle_span: float, - bot: bool = True + axle_pos: float, + bot: bool = True, + child: bool = False, + overestimate: float = 1.2, + median: float = 0.5, ) -> Cq.Sketch: """ - Generates a sector-like profile on the child side of a panel to - accomodate for joint rotation + A cut polygon to accomodate for joint extensions """ - sign = -1 if bot else 1 - + loc_ext = loc_bot if bot else loc_top loc_tip = loc_top if bot else loc_bot - loc_arc_right = loc_bot if bot else loc_top - loc_rel_arc_right = loc_axle.inverse * loc_arc_right - loc_arc_left = loc_axle * Cq.Location.rot2d(sign * angle_span) * loc_rel_arc_right - loc_arc_middle = loc_axle * Cq.Location.rot2d(sign * angle_span / 2) * loc_rel_arc_right + theta = math.radians(angle_span * (median if child else 1 - median)) + y_sign = -1 if bot else 1 + sign = -1 if child else 1 + dh = axle_pos * height * (overestimate - 1) + loc_left = loc_ext * Cq.Location.from2d(0, y_sign * dh) + loc_right = loc_left * Cq.Location.from2d(sign * height * overestimate * axle_pos * math.tan(theta), 0) return ( Cq.Sketch() .segment( loc_tip.to2d_pos(), - loc_arc_right.to2d_pos(), - ) - .arc( - loc_arc_right.to2d_pos(), - loc_arc_middle.to2d_pos(), - loc_arc_left.to2d_pos(), + loc_left.to2d_pos(), ) .segment( + loc_left.to2d_pos(), + loc_right.to2d_pos(), + ) + .segment( + loc_right.to2d_pos(), loc_tip.to2d_pos(), - loc_arc_left.to2d_pos(), ) .assemble() ) @@ -597,10 +573,22 @@ class WingProfile(Model): @target(name="profile-s1", kind=TargetKind.DXF) def profile_s1(self) -> Cq.Sketch: + cut_poly = self._joint_extension_cut_polygon( + loc_bot=self.elbow_bot_loc, + loc_top=self.elbow_top_loc, + height=self.elbow_height, + angle_span=self.elbow_joint.motion_span, + axle_pos=self.elbow_axle_pos, + bot=not self.elbow_joint.flip, + median=self.elbow_joint_overlap_median, + child=False, + ).reset().polygon(self._mask_elbow(), mode='a') profile = ( self.profile() .reset() - .polygon(self._mask_elbow(), mode='i') + .push([self.elbow_axle_loc.to2d_pos()]) + .each(lambda _: cut_poly, mode='i') + #.polygon(self._mask_elbow(), mode='i') ) return profile def surface_s1(self, front: bool = True) -> Cq.Workplane: @@ -686,21 +674,47 @@ class WingProfile(Model): @target(name="profile-s2", kind=TargetKind.DXF) def profile_s2(self) -> Cq.Sketch: + # Calculates `(profile - (E - JE)) * (W + JW)` + cut_elbow = ( + Cq.Sketch() + .polygon(self._mask_elbow()) + .reset() + .boolean(self._joint_extension_cut_polygon( + loc_bot=self.elbow_bot_loc, + loc_top=self.elbow_top_loc, + height=self.elbow_height, + angle_span=self.elbow_joint.motion_span, + axle_pos=self.elbow_axle_pos, + bot=not self.elbow_joint.flip, + median=self.elbow_joint_overlap_median, + child=True, + ), mode='s') + ) + cut_wrist = ( + Cq.Sketch() + .polygon(self._mask_wrist()) + ) + if self.flip: + poly = self._joint_extension_cut_polygon( + loc_bot=self.wrist_bot_loc, + loc_top=self.wrist_top_loc, + height=self.wrist_height, + angle_span=self.wrist_joint.motion_span, + axle_pos=self.wrist_axle_pos, + bot=not self.wrist_joint.flip, + median=self.wrist_joint_overlap_median, + child=False, + ) + cut_wrist = ( + cut_wrist + .reset() + .boolean(poly, mode='a') + ) profile = ( self.profile() .reset() - .polygon(self._mask_elbow(), mode='s') - .reset() - .polygon(self._mask_wrist(), mode='i') - .reset() - .push([self.elbow_axle_loc]) - .each(lambda loc: self._parent_joint_extension_profile( - loc, - self.elbow_bot_loc, - self.elbow_top_loc, - self.elbow_joint.motion_span, - bot=not self.flip, - ), mode='a') + .boolean(cut_elbow, mode='s') + .boolean(cut_wrist, mode='i') ) return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: @@ -724,33 +738,10 @@ class WingProfile(Model): profile = self.profile_s2() tags = tags_elbow + tags_wrist return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) - @target(name="profile-s2-bridge", kind=TargetKind.DXF) - def profile_s2_bridge(self) -> Cq.Workplane: - # FIXME: Leave some margin here so we can glue the panels - - # Generates the extension profile, which is required on both sides - profile = self._child_joint_extension_profile( - axle_loc=self.wrist_axle_loc, - radius=self.wrist_height * (0.5 if self.flip else 1), - angle_span=self.wrist_joint.motion_span, - bot=self.flip, - ) - # Generates the contraction (cut) profile. only required on the left - if self.flip: - extra = ( - self.profile() - .reset() - .push([self.wrist_axle_loc]) - .each(self._wrist_joint_retract_cut_polygon, mode='i') - ) - profile = ( - profile - .push([self.wrist_axle_loc]) - .each(lambda _: extra, mode='a') - ) - return profile - def surface_s2_bridge(self, front: bool = True) -> Cq.Workplane: + def surface_s2_bridge(self, front: bool = True) -> Optional[Cq.Workplane]: profile = self.profile_s2_bridge() + if profile is None: + return None loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.parent_arm_loc() tags = [ ("wrist_bot", self.wrist_axle_loc * loc_wrist * @@ -796,15 +787,25 @@ class WingProfile(Model): material=self.mat_panel, role=self.role_panel) .constrain("front@faces@>Z", "back@faces@ Cq.Sketch: + cut_wrist = ( + Cq.Sketch() + .polygon(self._mask_wrist()) + ) + if self.flip: + poly = self._joint_extension_cut_polygon( + loc_bot=self.wrist_bot_loc, + loc_top=self.wrist_top_loc, + height=self.wrist_height, + angle_span=self.wrist_joint.motion_span, + axle_pos=self.wrist_axle_pos, + bot=not self.wrist_joint.flip, + median=self.wrist_joint_overlap_median, + child=True, + ) + cut_wrist = ( + cut_wrist + .boolean(poly, mode='s') + ) profile = ( self.profile() - .reset() - .polygon(self._mask_wrist(), mode='s') + .boolean(cut_wrist, mode='s') ) return profile def surface_s3(self, @@ -1029,7 +1048,6 @@ class WingR(WingProfile): # Underapproximate the wrist tangent angle to leave no gaps on the blade blade_wrist_approx_tangent_angle: float = 40.0 - blade_overlap_arrow_height: float = 5.0 # Some overlap needed to glue the two sides blade_overlap_angle: float = -1 blade_hole_angle: float = 3 @@ -1116,6 +1134,74 @@ class WingR(WingProfile): ) return result + def _child_joint_extension_profile( + self, + axle_loc: Cq.Location, + radius: float, + angle_span: float, + bot: bool = False) -> Cq.Sketch: + """ + Creates a sector profile which accomodates extension + """ + # leave some margin for gluing + margin = 5 + sign = -1 if bot else 1 + axle_loc = axle_loc * Cq.Location.rot2d(-90 if bot else 90) + loc_h = Cq.Location.from2d(radius, 0) + loc_offset = axle_loc * Cq.Location.from2d(0, margin) + start = axle_loc * loc_h + mid = axle_loc * Cq.Location.rot2d(-sign * angle_span/2) * loc_h + end = axle_loc * Cq.Location.rot2d(-sign * angle_span) * loc_h + return ( + Cq.Sketch() + .segment( + loc_offset.to2d_pos(), + start.to2d_pos(), + ) + .arc( + start.to2d_pos(), + mid.to2d_pos(), + end.to2d_pos(), + ) + .segment( + end.to2d_pos(), + axle_loc.to2d_pos(), + ) + .segment( + axle_loc.to2d_pos(), + loc_offset.to2d_pos(), + ) + .assemble() + ) + + @target(name="profile-s2-bridge", kind=TargetKind.DXF) + def profile_s2_bridge(self) -> Cq.Sketch: + """ + This extension profile is required to accomodate the awkward shaped + joint next to the scythe + """ + # Generates the extension profile, which is required on both sides + profile = self._child_joint_extension_profile( + axle_loc=self.wrist_axle_loc, + radius=self.wrist_height, + angle_span=self.wrist_joint.motion_span, + bot=self.flip, + ) + # Generates the contraction (cut) profile. only required on the left + if self.flip: + extra = ( + self.profile() + .reset() + .push([self.wrist_axle_loc]) + .each(self._wrist_joint_retract_cut_polygon, mode='i') + ) + profile = ( + profile + .push([self.wrist_axle_loc]) + .each(lambda _: extra, mode='a') + ) + return profile + def profile_s3_extra(self) -> Cq.Sketch: """ Implements the blade part on Nue's wing @@ -1123,7 +1209,7 @@ class WingR(WingProfile): left_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(-1) hole_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_hole_angle) right_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_angle) - h_loc = Cq.Location.from2d(0, self.arrow_height + self.blade_overlap_arrow_height) + h_loc = Cq.Location.from2d(0, self.arrow_height) # Law of sines, uses the triangle of (wrist_bot_loc, arrow_bot_loc, ?) theta_wp = math.radians(90 - self.blade_wrist_approx_tangent_angle) @@ -1202,6 +1288,9 @@ class WingL(WingProfile): elbow_axle_pos: float = 0.4 wrist_axle_pos: float = 0.5 + elbow_joint_overlap_median: float = 0.5 + wrist_joint_overlap_median: float = 0.5 + def __post_init__(self): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) diff --git a/nhf/utils.py b/nhf/utils.py index 1d2c796..13469fd 100644 --- a/nhf/utils.py +++ b/nhf/utils.py @@ -100,6 +100,15 @@ def flip_y(self: Cq.Location) -> Cq.Location: return Cq.Location.from2d(x, -y, -a) Cq.Location.flip_y = flip_y +def boolean(self: Cq.Sketch, obj, **kwargs) -> Cq.Sketch: + return ( + self + .reset() + .push([(0, 0)]) + .each(lambda _: obj, **kwargs) + ) +Cq.Sketch.boolean = boolean + ### Tags def tagPoint(self, tag: str): -- 2.44.1 From da58eeafe6a409f373bb6fe15bd740944d64534b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 23 Jul 2024 22:40:49 -0700 Subject: [PATCH 136/187] feat: Option to simplify geometry --- nhf/touhou/houjuu_nue/electronics.py | 3 ++ nhf/touhou/houjuu_nue/joints.py | 69 ++++++++++++++++++---------- nhf/touhou/houjuu_nue/wing.py | 36 ++++++++++++--- 3 files changed, 76 insertions(+), 32 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index ae114e1..8f5db83 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -390,6 +390,8 @@ class Flexor: pos = (target_length - self.actuator.conn_length) / self.actuator.stroke_length if tag_prefix: tag_prefix = tag_prefix + "_" + else: + tag_prefix = "" name_actuator = f"{tag_prefix}actuator" name_bracket_front = f"{tag_prefix}bracket_front" name_bracket_back = f"{tag_prefix}bracket_back" @@ -397,6 +399,7 @@ class Flexor: name_bolt_back = f"{tag_prefix}back_bolt" name_nut_front = f"{tag_prefix}front_nut" name_nut_back = f"{tag_prefix}back_nut" + print(name_bracket_back) ( a .add(self.actuator.assembly(pos=pos), name=name_actuator) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 3489746..e5c6f13 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -208,7 +208,8 @@ class RootJoint(Model): def assembly(self, offset: int = 0, - fastener_pos: float = 0) -> Cq.Assembly: + fastener_pos: float = 0, + ignore_fasteners: bool = False) -> Cq.Assembly: """ Specify knob position to determine the position of the knob from fully inserted (0) or fully uninserted (1) @@ -223,13 +224,17 @@ class RootJoint(Model): .addS(self.child(), name="child", material=Material.PLASTIC_PLA, role=Role.CHILD) - .addS(self.hex_nut.assembly(), name="hex_nut") - .addS(self.knob.assembly(), name="knob", - loc=Cq.Location((0, 0, knob_h * fastener_pos))) - .constrain("knob/thread", "Fixed") - .constrain("hex_nut?bot", "parent?base", "Plane", param=0) - .constrain("hex_nut?dirX", "parent@faces@>X", "Axis", param=0) ) + if not ignore_fasteners: + ( + result + .addS(self.hex_nut.assembly(), name="hex_nut") + .addS(self.knob.assembly(), name="knob", + loc=Cq.Location((0, 0, knob_h * fastener_pos))) + .constrain("knob/thread", "Fixed") + .constrain("hex_nut?bot", "parent?base", "Plane", param=0) + .constrain("hex_nut?dirX", "parent@faces@>X", "Axis", param=0) + ) self.hirth_joint.add_constraints( result, "parent", @@ -526,6 +531,7 @@ class ShoulderJoint(Model): self, fastener_pos: float = 0.0, deflection: float = 0.0, + ignore_fasteners: bool = False, ) -> Cq.Assembly: assert deflection <= self.angle_max_deflection directrix = self.directrix_id @@ -547,14 +553,18 @@ class ShoulderJoint(Model): .addS(self.parent_bot(), name="parent_bot", role=Role.PARENT, material=mat) - # Fasteners - .addS(self.bolt.assembly(), name="bolt_top", - loc=Cq.Location((0, 0, bolt_z))) - .constrain("bolt_top?root", 'Fixed') - .addS(self.bolt.assembly(), name="bolt_bot", - loc=Cq.Location((0, 0, -bolt_z), (1,0,0), 180)) - .constrain("bolt_bot?root", 'Fixed') ) + if not ignore_fasteners: + ( + result + # Fasteners + .addS(self.bolt.assembly(), name="bolt_top", + loc=Cq.Location((0, 0, bolt_z))) + .constrain("bolt_top?root", 'Fixed') + .addS(self.bolt.assembly(), name="bolt_bot", + loc=Cq.Location((0, 0, -bolt_z), (1,0,0), 180)) + .constrain("bolt_bot?root", 'Fixed') + ) TorsionJoint.add_constraints( result, rider="child/rider_top", @@ -1075,7 +1085,7 @@ class ElbowJoint(Model): return self.disk_joint.housing_lower() @target(name="parent-upper") - def parent_joint_upper(self): + def parent_joint_upper(self, generate_mount: bool=False): axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) housing_dz = self.disk_joint.housing_upper_dz conn_h = self.disk_joint.total_thickness @@ -1120,16 +1130,25 @@ class ElbowJoint(Model): #.solve() ) if self.flexor: - # Orientes the hole surface so it faces +X - loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) - result.add( - self.actuator_mount(), - name="act", - loc=self.actuator_mount_loc(child=False) * loc_thickness) + if generate_mount: + # Orientes the hole surface so it faces +X + loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) + result.add( + self.actuator_mount(), + name="act", + loc=self.actuator_mount_loc(child=False) * loc_thickness) + else: + result.add( + Cq.Edge.makeLine((-1,0,0), (1,0,0)), + name="act", + loc=self.actuator_mount_loc(child=False)) return result @assembly() - def assembly(self, angle: float = 0) -> Cq.Assembly: + def assembly(self, + angle: float = 0, + generate_mount: bool = False, + ignore_actuators: bool = False) -> Cq.Assembly: assert 0 <= angle <= self.motion_span result = ( Cq.Assembly() @@ -1137,7 +1156,7 @@ class ElbowJoint(Model): role=Role.CHILD, material=self.material) .addS(self.parent_joint_lower(), name="parent_lower", role=Role.CASING, material=self.material) - .addS(self.parent_joint_upper(), name="parent_upper", + .addS(self.parent_joint_upper(generate_mount=generate_mount), name="parent_upper", role=Role.PARENT, material=self.material) #.constrain("child/disk?mate_bot", "Fixed") ) @@ -1148,14 +1167,14 @@ class ElbowJoint(Model): disk="child/disk", angle=angle, ) - if self.flexor: + if not ignore_actuators and self.flexor: target_length = self.flexor.target_length_at_angle( angle=angle, ) self.flexor.add_to( result, target_length=target_length, - tag_hole_back="parent_upper/act?mount", + tag_hole_back="parent_upper/act", tag_hole_front="child/act?mount", tag_dir="parent_lower?mate", ) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index c33deda..b18358a 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -362,7 +362,9 @@ class WingProfile(Model): return result @assembly() - def assembly_s0(self, ignore_detail: bool=False) -> Cq.Assembly: + def assembly_s0( + self, + ignore_electronics: bool=False) -> Cq.Assembly: result = ( Cq.Assembly() .addS(self.surface_s0(top=True), name="bot", @@ -401,7 +403,7 @@ class WingProfile(Model): .constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane") .constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis") ) - if not ignore_detail: + if not ignore_electronics: result.add(self.electronic_board.assembly(), name="electronic_board") for hole in self.electronic_board.mount_holes: assert hole.tag @@ -923,7 +925,9 @@ class WingProfile(Model): elbow_wrist_deflection: float = 0.0, root_offset: int = 5, fastener_pos: float = 0.0, - ignore_detail: bool = False, + ignore_fasteners: bool = False, + ignore_electronics: bool = False, + ignore_actuators: bool = False, ) -> Cq.Assembly(): if parts is None: parts = [ @@ -944,11 +948,14 @@ class WingProfile(Model): tag_top, tag_bot = tag_bot, tag_top if "s0" in parts: - result.add(self.assembly_s0(ignore_detail=ignore_detail), name="s0") + result.add(self.assembly_s0( + ignore_electronics=ignore_electronics + ), name="s0") if "root" in parts: result.addS(self.root_joint.assembly( offset=root_offset, fastener_pos=fastener_pos, + ignore_fasteners=ignore_fasteners, ), name="root") result.constrain("root/parent", "Fixed") if "s0" in parts and "root" in parts: @@ -962,7 +969,8 @@ class WingProfile(Model): angle = shoulder_deflection * self.shoulder_joint.angle_max_deflection result.add(self.shoulder_joint.assembly( fastener_pos=fastener_pos, - deflection=angle), name="shoulder") + deflection=angle, + ignore_fasteners=ignore_fasteners), name="shoulder") if "s0" in parts and "shoulder" in parts: for i in range(len(self.shoulder_joint.parent_conn_hole_pos)): ( @@ -984,7 +992,9 @@ class WingProfile(Model): ) if "elbow" in parts: angle = self.elbow_joint.motion_span * elbow_wrist_deflection - result.add(self.elbow_joint.assembly(angle=angle), name="elbow") + result.add(self.elbow_joint.assembly( + angle=angle, + ignore_actuators=ignore_actuators), name="elbow") if "s1" in parts and "elbow" in parts: ( result @@ -993,6 +1003,11 @@ class WingProfile(Model): .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/lip?conn_bot0", "Plane") .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/lip?conn_bot1", "Plane") ) + if not ignore_actuators: + ( + result + .constrain("elbow/bracket_back?conn_side", "s1/elbow_act?conn0", "Plane") + ) if "s2" in parts: result.add(self.assembly_s2(), name="s2") if "s2" in parts and "elbow" in parts: @@ -1005,7 +1020,9 @@ class WingProfile(Model): ) if "wrist" in parts: angle = self.wrist_joint.motion_span * elbow_wrist_deflection - result.add(self.wrist_joint.assembly(angle=angle), name="wrist") + result.add(self.wrist_joint.assembly( + angle=angle, + ignore_actuators=ignore_actuators), name="wrist") wrist_n_holes = len(self.wrist_joint.hole_pos) if "s2" in parts and "wrist" in parts: for i in range(wrist_n_holes): @@ -1023,6 +1040,11 @@ class WingProfile(Model): .constrain(f"s3/wrist_top?conn{i}", f"wrist/child/lip?conn_top{i}", "Plane") .constrain(f"s3/wrist_bot?conn{i}", f"wrist/child/lip?conn_bot{i}", "Plane") ) + if not ignore_actuators: + ( + result + .constrain("wrist/bracket_back?conn_side", "s2/wrist_act?conn0", "Plane") + ) if len(parts) > 1: result.solve() -- 2.44.1 From a010baa0997992deae49cabe4f1b1b2bd22fb90f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 23 Jul 2024 22:52:06 -0700 Subject: [PATCH 137/187] fix: Remove extraneous print --- nhf/touhou/houjuu_nue/electronics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 8f5db83..0a0b466 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -399,7 +399,6 @@ class Flexor: name_bolt_back = f"{tag_prefix}back_bolt" name_nut_front = f"{tag_prefix}front_nut" name_nut_back = f"{tag_prefix}back_nut" - print(name_bracket_back) ( a .add(self.actuator.assembly(pos=pos), name=name_actuator) -- 2.44.1 From 45213adda77667e7fb384744751a5a45c96a0416 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 00:22:38 -0700 Subject: [PATCH 138/187] fix: Collision of wing geometry --- nhf/touhou/houjuu_nue/__init__.py | 13 +++++++++---- nhf/touhou/houjuu_nue/joints.py | 17 ++++++++++------- nhf/touhou/houjuu_nue/wing.py | 17 +++++++++++------ 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index c6b4d4c..6022342 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -41,7 +41,8 @@ import nhf.touhou.houjuu_nue.harness as MH from nhf.parts.item import Item import nhf.utils -WING_DEFLECT = 10.0 +WING_DEFLECT_ODD = 0.0 +WING_DEFLECT_EVEN = 25.0 @dataclass class Parameters(Model): """ @@ -52,23 +53,26 @@ class Parameters(Model): wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR( name="r1", - shoulder_angle_bias = WING_DEFLECT, + shoulder_angle_bias = WING_DEFLECT_ODD, s0_top_hole=False, s0_bot_hole=True, + arrow_height=350.0 )) wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR( name="r2", + shoulder_angle_bias = WING_DEFLECT_EVEN, s0_top_hole=True, s0_bot_hole=True, )) wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR( name="r3", - shoulder_angle_bias = WING_DEFLECT, + shoulder_angle_bias = WING_DEFLECT_ODD, s0_top_hole=True, s0_bot_hole=False, )) wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL( name="l1", + shoulder_angle_bias = WING_DEFLECT_EVEN, wrist_angle=-60.0, s0_top_hole=False, s0_bot_hole=True, @@ -76,12 +80,13 @@ class Parameters(Model): wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( name="l2", wrist_angle=-30.0, - shoulder_angle_bias = WING_DEFLECT, + shoulder_angle_bias = WING_DEFLECT_ODD, s0_top_hole=True, s0_bot_hole=True, )) wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL( name="l3", + shoulder_angle_bias = WING_DEFLECT_EVEN, wrist_angle=-0.0, s0_top_hole=True, s0_bot_hole=False, diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index e5c6f13..126259d 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -303,7 +303,7 @@ class ShoulderJoint(Model): directrix_id: int = 0 angle_neutral: float = -15.0 - angle_max_deflection: float = 90.0 + angle_max_deflection: float = 65.0 def __post_init__(self): assert self.parent_lip_length * 2 < self.height @@ -1022,8 +1022,8 @@ class ElbowJoint(Model): # Moves the hole to be some distance apart from 0 mount_r, mount_loc_angle, mount_parent_r = self.flexor.open_pos() loc_span = Cq.Location.from2d(mount_r if child else mount_parent_r, 0) - r = (-mount_loc_angle - self.angle_neutral if child else 0) + 180 + self.flexor_offset_angle - loc_rot = Cq.Location.rot2d(r) + alpha = (-mount_loc_angle if child else 0) + 180 - self.flexor_offset_angle + loc_rot = Cq.Location.rot2d(alpha) loc = loc_rot * loc_span * loc_mount_orient * loc_mount return loc.flip_y() if self.flip and not child and not unflip else loc @@ -1062,14 +1062,17 @@ class ElbowJoint(Model): Cq.Location((-lip_dz, 0, 0), (1, 0, 0), 90) * Cq.Location((0, 0, 0), (0, 1, 0), 90) ) + loc_rot_neutral = Cq.Location.rot2d(self.angle_neutral) loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, 0)) loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness)) disk_cut = self.disk_joint._disk_cut().located( loc_lip.inverse * loc_cut_rel * loc_disk) result = ( Cq.Assembly() - .add(self.disk_joint.disk(), name="disk", loc=Cq.Location((0, 0, -dz), (0,0,1), angle)) - .add(self.lip().cut(disk_cut), name="lip", loc=loc_disk.inverse * loc_lip) + .add(self.disk_joint.disk(), name="disk", + loc=loc_rot_neutral * Cq.Location((0, 0, -dz), (0,0,1), angle)) + .add(self.lip().cut(disk_cut), name="lip", + loc=loc_rot_neutral * loc_disk.inverse * loc_lip) ) # Orientes the hole surface so it faces +X loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) @@ -1136,12 +1139,12 @@ class ElbowJoint(Model): result.add( self.actuator_mount(), name="act", - loc=self.actuator_mount_loc(child=False) * loc_thickness) + loc=self.actuator_mount_loc(child=False, unflip=True) * loc_thickness) else: result.add( Cq.Edge.makeLine((-1,0,0), (1,0,0)), name="act", - loc=self.actuator_mount_loc(child=False)) + loc=self.actuator_mount_loc(child=False, unflip=True)) return result @assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b18358a..0176e0e 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -14,7 +14,12 @@ from nhf.parts.box import box_with_centre_holes, MountingBox, Hole from nhf.parts.joints import HirthJoint from nhf.parts.planar import extrude_with_markers from nhf.touhou.houjuu_nue.joints import RootJoint, ShoulderJoint, ElbowJoint, DiskJoint -from nhf.touhou.houjuu_nue.electronics import LINEAR_ACTUATOR_21, LINEAR_ACTUATOR_50, ElectronicBoard +from nhf.touhou.houjuu_nue.electronics import ( + LINEAR_ACTUATOR_10, + LINEAR_ACTUATOR_21, + LINEAR_ACTUATOR_50, + ElectronicBoard +) import nhf.utils @dataclass(kw_only=True) @@ -55,9 +60,9 @@ class WingProfile(Model): movement_angle=55, ), hole_diam=4.0, - angle_neutral=30.0, + angle_neutral=10.0, actuator=LINEAR_ACTUATOR_50, - flexor_offset_angle=0, + flexor_offset_angle=30, flip=False, )) # Distance between the two spacers on the elbow, halved @@ -74,9 +79,9 @@ class WingProfile(Model): child_arm_radius=23.0, parent_arm_radius=30.0, hole_diam=4.0, - angle_neutral=-30.0, - actuator=LINEAR_ACTUATOR_21, - flexor_offset_angle=0, + angle_neutral=0.0, + actuator=LINEAR_ACTUATOR_10, + flexor_offset_angle=30.0, flip=True, )) # Distance between the two spacers on the elbow, halved -- 2.44.1 From a9b3aa8f70c6c3fc58a687ec961963b814c9f948 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 01:35:23 -0700 Subject: [PATCH 139/187] feat: Spool for wires --- nhf/touhou/houjuu_nue/joints.py | 54 ++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 126259d..ea4947f 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -305,13 +305,28 @@ class ShoulderJoint(Model): angle_neutral: float = -15.0 angle_max_deflection: float = 65.0 + spool_radius: float = 14.0 + spool_groove_depth: float = 1.0 + spool_base_height: float = 3.0 + spool_height: float = 5.0 + spool_groove_inset: float = 2.0 + def __post_init__(self): assert self.parent_lip_length * 2 < self.height + assert self.spool_groove_depth < self.spool_radius < self.torsion_joint.radius_rider - self.child_core_thickness + assert self.spool_base_height > self.spool_groove_depth @property def radius(self): return self.torsion_joint.radius + @property + def draft_length(self): + """ + Amount of wires that need to draft on the spool + """ + return (self.spool_radius - self.spool_groove_depth / 2) * math.radians(self.angle_max_deflection) + def parent_lip_loc(self, left: bool=True) -> Cq.Location: """ 2d location of the arm surface on the parent side, relative to axle @@ -416,7 +431,10 @@ class ShoulderJoint(Model): @target(name="parent-bot") def parent_bot(self) -> Cq.Assembly: - return self.parent(top=False) + result = ( + self.parent(top=False) + ) + return result @target(name="parent-top") def parent_top(self) -> Cq.Assembly: return self.parent(top=True) @@ -430,6 +448,36 @@ class ShoulderJoint(Model): joint = self.torsion_joint return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height + def _spool(self) -> Cq.Workplane: + """ + Generates the spool piece which holds the line in tension + """ + t = self.spool_groove_depth + bulk = Cq.Solid.makeCylinder( + radius=self.spool_radius, + height=self.spool_height, + ).located(Cq.Location((0, 0, self.spool_base_height))) + base = Cq.Solid.makeCylinder( + radius=self.spool_radius - t, + height=self.spool_base_height, + ) + hole_x = self.spool_radius - (t + self.spool_groove_inset) + slot = Cq.Solid.makeBox( + length=t + self.spool_groove_inset, + width=t, + height=self.spool_base_height, + ).located(Cq.Location((hole_x, -t/2, 0))) + hole = Cq.Solid.makeBox( + length=t, + width=t, + height=self.spool_height + self.spool_base_height, + ).located(Cq.Location((hole_x, -t/2, 0))) + centre_hole = Cq.Solid.makeCylinder( + radius=self.torsion_joint.radius_axle, + height=self.spool_height + self.spool_base_height, + ) + return bulk.fuse(base).cut(slot, hole, centre_hole) + @target(name="child") def child(self) -> Cq.Assembly: """ @@ -512,6 +560,8 @@ class ShoulderJoint(Model): loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot + self.angle_neutral) loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral) + spool_dz = self.height / 2 - self.torsion_joint.total_height + spool_angle = 180 + self.angle_neutral result = ( Cq.Assembly() .add(core, name="core", loc=Cq.Location()) @@ -523,6 +573,8 @@ class ShoulderJoint(Model): loc=Cq.Location((self.child_guard_ext, 0, dh))) .add(lip, name="lip_bot", loc=Cq.Location((self.child_guard_ext, 0, -dh)) * loc_rotate) + .add(self._spool(), name="spool", + loc=Cq.Location((0, 0, -spool_dz), (0, 0, 1), spool_angle)) ) return result -- 2.44.1 From b8c6fb51fd802e21d2f32dd0f999401708416ac9 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 12:45:38 -0700 Subject: [PATCH 140/187] feat: Torsion resistant shoulder --- nhf/touhou/houjuu_nue/joints.py | 98 ++++++++++++++++++++++++--------- nhf/touhou/houjuu_nue/wing.py | 45 +++++++++------ 2 files changed, 100 insertions(+), 43 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index ea4947f..70c5d03 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -8,7 +8,9 @@ from nhf.parts.springs import TorsionSpring from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes -from nhf.touhou.houjuu_nue.electronics import Flexor, LinearActuator +from nhf.touhou.houjuu_nue.electronics import ( + Flexor, LinearActuator, LINEAR_ACTUATOR_21, +) import nhf.geometry import nhf.utils @@ -286,17 +288,18 @@ class ShoulderJoint(Model): # Generates a child guard which covers up the internals. The lip length is # relative to the +X surface of the guard. - child_guard_ext: float = 30.0 + child_guard_ext: float = 20.0 child_guard_width: float = 25.0 # guard length measured from axle - child_lip_length: float = 40.0 + child_lip_ext: float = 50.0 child_lip_width: float = 20.0 + child_lip_height: float = 40.0 + child_lip_thickness: float = 5.0 child_conn_hole_diam: float = 4.0 # Measured from centre of axle - child_conn_hole_pos: list[float] = field(default_factory=lambda: [8, 19, 30]) + child_conn_hole_pos: list[float] = field(default_factory=lambda: [-15, -5, 5, 15]) child_core_thickness: float = 3.0 - # Rotates the torsion joint to avoid collisions or for some other purpose axis_rotate_bot: float = 90 axis_rotate_top: float = 0 @@ -311,10 +314,16 @@ class ShoulderJoint(Model): spool_height: float = 5.0 spool_groove_inset: float = 2.0 + flip: bool = False + actuator: LinearActuator = LINEAR_ACTUATOR_21 + def __post_init__(self): assert self.parent_lip_length * 2 < self.height + assert self.child_guard_ext > self.torsion_joint.radius_rider assert self.spool_groove_depth < self.spool_radius < self.torsion_joint.radius_rider - self.child_core_thickness assert self.spool_base_height > self.spool_groove_depth + assert self.child_lip_height < self.height + assert self.draft_length <= self.actuator.stroke_length @property def radius(self): @@ -327,6 +336,13 @@ class ShoulderJoint(Model): """ return (self.spool_radius - self.spool_groove_depth / 2) * math.radians(self.angle_max_deflection) + @property + def draft_height(self): + """ + Position of the middle of the spool measured from the middle + """ + return self.height / 2 - self.torsion_joint.total_height - self.spool_base_height / 2 + def parent_lip_loc(self, left: bool=True) -> Cq.Location: """ 2d location of the arm surface on the parent side, relative to axle @@ -335,10 +351,15 @@ class ShoulderJoint(Model): sign = 1 if left else -1 loc_dir = Cq.Location((0,sign * dy,0), (0, 0, 1), sign * 90) return Cq.Location.from2d(self.parent_lip_ext, 0, 0) * loc_dir + def child_lip_loc(self) -> Cq.Location: + """ + 2d location to middle of lip + """ + return Cq.Location.from2d(self.child_lip_ext - self.child_guard_ext, 0, 180) @property def _max_contraction_angle(self) -> float: - return self.angle_max_deflection + self.angle_neutral + return 180 - self.angle_max_deflection + self.angle_neutral def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid: """ @@ -346,7 +367,8 @@ class ShoulderJoint(Model): """ aspect = self.child_guard_width / self.parent_arm_width theta = math.radians(self._max_contraction_angle) - theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect)) + #theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect)) + theta_p = math.atan2(math.sin(theta), math.cos(theta) + aspect) angle = math.degrees(theta_p) assert 0 <= angle <= 90 # outer radius of the cut, overestimated @@ -431,10 +453,7 @@ class ShoulderJoint(Model): @target(name="parent-bot") def parent_bot(self) -> Cq.Assembly: - result = ( - self.parent(top=False) - ) - return result + return self.parent(top=False) @target(name="parent-top") def parent_top(self) -> Cq.Assembly: return self.parent(top=True) @@ -485,18 +504,15 @@ class ShoulderJoint(Model): """ joint = self.torsion_joint - assert all(r < self.child_lip_length for r in self.child_conn_hole_pos) # Half of the height of the bridging cylinder dh = self.height / 2 - joint.total_height core_start_angle = 30 - core_end_angle1 = 90 - core_end_angle2 = 180 radius_core_inner = joint.radius_rider - self.child_core_thickness core_profile1 = ( Cq.Sketch() - .arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle) + .arc((0, 0), joint.radius_rider, core_start_angle, self.angle_max_deflection) .segment((0, 0)) .close() .assemble() @@ -504,12 +520,28 @@ class ShoulderJoint(Model): ) core_profile2 = ( Cq.Sketch() - .arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle)) + .arc((0, 0), joint.radius_rider, -core_start_angle, -(90 - self.angle_neutral)) .segment((0, 0)) .close() .assemble() .circle(radius_core_inner, mode='s') ) + lip_extension = ( + Cq.Solid.makeBox( + length=self.child_lip_ext - self.child_guard_ext, + width=self.child_lip_width, + height=self.child_lip_height, + ).cut(Cq.Solid.makeBox( + length=self.child_lip_ext - self.child_guard_ext, + width=self.child_lip_width - self.child_lip_thickness * 2, + height=self.child_lip_height, + ).located(Cq.Location((0, self.child_lip_thickness, 0)))) + .located(Cq.Location(( + self.child_guard_ext, + -self.child_lip_width / 2, + -self.child_lip_height / 2, + ))) + ) core_guard = ( Cq.Workplane('XY') .box( @@ -518,6 +550,14 @@ class ShoulderJoint(Model): height=self.height, centered=(False, True, True), ) + #.copyWorkplane(Cq.Workplane('XY')) + #.box( + # length=self.child_lip_ext, + # width=self.child_guard_width, + # height=self.child_lip_height, + # combine=True, + # centered=(False, True, True), + #) .copyWorkplane(Cq.Workplane('XY')) .cylinder( radius=self.radius, @@ -527,12 +567,13 @@ class ShoulderJoint(Model): ) .copyWorkplane(Cq.Workplane('XY')) .box( - length=self.child_guard_ext, + length=self.child_lip_ext, width=self.child_lip_width, height=self.height - self.torsion_joint.total_height * 2, combine='cut', centered=(False, True, True), ) + .union(lip_extension) .cut(self._contraction_cut_geometry(parent=False)) ) core = ( @@ -548,13 +589,17 @@ class ShoulderJoint(Model): .union(core_guard) ) assert self.child_lip_width / 2 <= joint.radius_rider - lip_thickness = joint.rider_disk_height - lip = box_with_centre_holes( - length=self.child_lip_length, + sign = 1 if self.flip else -1 + holes = [Hole(x=sign * x) for x in self.child_conn_hole_pos] + lip_obj = MountingBox( + length=self.child_lip_height, width=self.child_lip_width, - height=lip_thickness, - hole_loc=self.child_conn_hole_pos, + thickness=self.child_lip_thickness, + holes=holes, hole_diam=self.child_conn_hole_diam, + centred=(True, True), + generate_side_tags=False, + generate_reverse_tags=False, ) theta = self.torsion_joint.spring.angle_neutral - self.torsion_joint.rider_slot_span loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) @@ -562,6 +607,7 @@ class ShoulderJoint(Model): loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral) spool_dz = self.height / 2 - self.torsion_joint.total_height spool_angle = 180 + self.angle_neutral + loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) if self.flip else Cq.Location() result = ( Cq.Assembly() .add(core, name="core", loc=Cq.Location()) @@ -569,12 +615,10 @@ class ShoulderJoint(Model): loc=loc_axis_rotate_top * Cq.Location((0, 0, dh), (0, 0, 1), -90) * Cq.Location((0, 0, 0), (0, 0, 1), theta)) .add(joint.rider(rider_slot_begin=180), name="rider_bot", loc=loc_axis_rotate_bot * Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate) - .add(lip, name="lip_top", - loc=Cq.Location((self.child_guard_ext, 0, dh))) - .add(lip, name="lip_bot", - loc=Cq.Location((self.child_guard_ext, 0, -dh)) * loc_rotate) + .add(lip_obj.generate(), name="lip", + loc=Cq.Location((self.child_lip_ext - self.child_lip_thickness,0,0), (0,1,0), 90)) .add(self._spool(), name="spool", - loc=Cq.Location((0, 0, -spool_dz), (0, 0, 1), spool_angle)) + loc=loc_spool_flip * Cq.Location((0, 0, -spool_dz), (0, 0, 1), spool_angle)) ) return result diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 0176e0e..22efbbc 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -337,6 +337,19 @@ class WingProfile(Model): flip_y=self.flip, generate_reverse_tags=True, ) + @submodel(name="spacer-s0-shoulder-act") + def spacer_s0_shoulder_act(self) -> MountingBox: + dx = self.shoulder_joint.draft_height + return MountingBox( + holes=[Hole(x=dx), Hole(x=-dx)], + hole_diam=self.shoulder_joint.actuator.back_hole_diam, + length=self.root_height, + width=10.0, + centred=(True, True), + thickness=self.spacer_thickness, + flip_y=self.flip, + generate_reverse_tags=True, + ) def surface_s0(self, top: bool = False) -> Cq.Workplane: base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10 @@ -352,6 +365,8 @@ class WingProfile(Model): self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=True)), ("shoulder_right", self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), + ("shoulder_act", + self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(150, -40)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("electronic_mount", Cq.Location.from2d(-45, 75, 64)), ] @@ -393,6 +408,7 @@ class WingProfile(Model): for o, tag in [ (self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"), (self.spacer_s0_shoulder(left=False).generate(), "shoulder_right"), + (self.spacer_s0_shoulder_act().generate(), "shoulder_act"), (self.spacer_s0_base().generate(), "base"), (self.spacer_s0_electronic_mount().generate(), "electronic_mount"), ]: @@ -599,13 +615,11 @@ class WingProfile(Model): ) return profile def surface_s1(self, front: bool = True) -> Cq.Workplane: - shoulder_h = self.shoulder_joint.child_height - h = (self.shoulder_joint.height - shoulder_h) / 2 tags_shoulder = [ - ("shoulder_bot", Cq.Location.from2d(0, h, 90)), - ("shoulder_top", Cq.Location.from2d(0, h + shoulder_h, 270)), + ("shoulder", + Cq.Location((0, self.shoulder_height / 2, 0)) * + self.shoulder_joint.child_lip_loc()), ] - h = self.elbow_height / 2 rot_elbow = Cq.Location.rot2d(self.elbow_rotate) loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc() tags_elbow = [ @@ -622,16 +636,20 @@ class WingProfile(Model): profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s1-shoulder") def spacer_s1_shoulder(self) -> MountingBox: + sign = -1 if self.flip else 1 holes = [ - Hole(x) + Hole(x=sign * x) for x in self.shoulder_joint.child_conn_hole_pos ] return MountingBox( - length=50.0, # FIXME: magic + length=self.shoulder_joint.child_lip_height, width=self.s1_thickness, thickness=self.spacer_thickness, holes=holes, + centred=(True, True), hole_diam=self.shoulder_joint.child_conn_hole_diam, + centre_left_right_tags=True, + centre_bot_top_tags=True, ) @submodel(name="spacer-s1-elbow") def spacer_s1_elbow(self) -> MountingBox: @@ -664,8 +682,7 @@ class WingProfile(Model): param=self.s1_thickness) ) for o, t in [ - (self.spacer_s1_shoulder(), "shoulder_bot"), - (self.spacer_s1_shoulder(), "shoulder_top"), + (self.spacer_s1_shoulder(), "shoulder"), (self.spacer_s1_elbow(), "elbow_top"), (self.spacer_s1_elbow(), "elbow_bot"), (self.spacer_s1_elbow_act(), "elbow_act"), @@ -988,13 +1005,8 @@ class WingProfile(Model): if "s1" in parts: result.add(self.assembly_s1(), name="s1") if "s1" in parts and "shoulder" in parts: - ( - result - .constrain("s1/shoulder_top?conn0", f"shoulder/child/lip_{tag_top}?conn0", "Plane") - .constrain("s1/shoulder_top?conn1", f"shoulder/child/lip_{tag_top}?conn1", "Plane") - .constrain("s1/shoulder_bot?conn0", f"shoulder/child/lip_{tag_bot}?conn0", "Plane") - .constrain("s1/shoulder_bot?conn1", f"shoulder/child/lip_{tag_bot}?conn1", "Plane") - ) + for i in range(len(self.shoulder_joint.child_conn_hole_pos)): + result.constrain(f"s1/shoulder?conn{i}", f"shoulder/child/lip?conn{i}", "Plane") if "elbow" in parts: angle = self.elbow_joint.motion_span * elbow_wrist_deflection result.add(self.elbow_joint.assembly( @@ -1327,6 +1339,7 @@ class WingL(WingProfile): self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0 self.wrist_rotate = -self.wrist_joint.angle_neutral self.wrist_joint.flip = False + self.shoulder_joint.flip = True super().__post_init__() -- 2.44.1 From b6d429d272325ae051f013738f193b414538fb16 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 15:15:58 -0700 Subject: [PATCH 141/187] fix: Orientation of left wing --- nhf/touhou/houjuu_nue/wing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 22efbbc..86a0e23 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -636,7 +636,7 @@ class WingProfile(Model): profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s1-shoulder") def spacer_s1_shoulder(self) -> MountingBox: - sign = -1 if self.flip else 1 + sign = 1#-1 if self.flip else 1 holes = [ Hole(x=sign * x) for x in self.shoulder_joint.child_conn_hole_pos -- 2.44.1 From 8b5906948ef8cb319c0732035a0ddea50dc04255 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 16:17:07 -0700 Subject: [PATCH 142/187] feat: Simpler wrist and elbow mounts --- nhf/touhou/houjuu_nue/joints.py | 45 +++++--- nhf/touhou/houjuu_nue/wing.py | 177 +++++++++++++++----------------- 2 files changed, 115 insertions(+), 107 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 70c5d03..e9f78ff 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -76,8 +76,8 @@ class RootJoint(Model): hirth_joint: HirthJoint = field(default_factory=lambda: HirthJoint( radius=25.0, radius_inner=15.0, - tooth_height=7.0, - base_height=5.0, + tooth_height=5.60, + base_height=4.0, n_tooth=24, )) parent_width: float = 85 @@ -100,6 +100,10 @@ class RootJoint(Model): child_width: float = 50.0 child_mount_thickness: float = 25.4 / 4 + def __post_init__(self): + assert self.child_extra_thickness > 0.0 + assert self.parent_thickness >= self.hex_nut.thickness + def corner_pos(self) -> list[tuple[int, int]]: """ Generates a set of points corresponding to the connectorss @@ -113,8 +117,15 @@ class RootJoint(Model): ] @property - def total_height(self) -> float: - return self.parent_thickness + self.hirth_joint.total_height + def child_extra_thickness(self) -> float: + """ + Extra thickness allocated to child for padding + """ + return self.knob.height_thread - self.hirth_joint.joint_height - self.child_mount_thickness - self.parent_thickness + + @property + def base_to_surface_thickness(self) -> float: + return self.hirth_joint.joint_height + self.child_extra_thickness @target(name="parent") def parent(self): @@ -127,9 +138,9 @@ class RootJoint(Model): result = ( Cq.Workplane('XY') .box( - self.parent_width, - self.parent_width, - self.parent_thickness, + length=self.parent_width, + width=self.parent_width, + height=self.parent_thickness, centered=(True, True, False)) .translate((0, 0, -self.parent_thickness)) .edges("|Z") @@ -177,11 +188,11 @@ class RootJoint(Model): result = ( Cq.Workplane('XY') .box( - self.child_height, - self.child_width, - self.hirth_joint.base_height, + length=self.child_height, + width=self.child_width, + height=self.child_extra_thickness + self.hirth_joint.base_height, centered=(True, True, False)) - #.translate((0, 0, -self.base_joint.base_height)) + .translate((0, 0, -self.child_extra_thickness)) #.edges("|Z") #.fillet(self.hs_joint_corner_fillet) .faces(">Z") @@ -190,7 +201,7 @@ class RootJoint(Model): .hole(self.corner_hole_diam) ) # Creates a plane parallel to the holes but shifted to the base - plane = result.faces(">Z").workplane(offset=-self.hirth_joint.base_height) + plane = result.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, -self.child_extra_thickness))) for i, (px, py) in enumerate(conn): plane.moveTo(px, py).tagPlane(f"conn{i}") @@ -232,7 +243,7 @@ class RootJoint(Model): result .addS(self.hex_nut.assembly(), name="hex_nut") .addS(self.knob.assembly(), name="knob", - loc=Cq.Location((0, 0, knob_h * fastener_pos))) + loc=Cq.Location((0, 0, knob_h * fastener_pos - self.parent_thickness))) .constrain("knob/thread", "Fixed") .constrain("hex_nut?bot", "parent?base", "Plane", param=0) .constrain("hex_nut?dirX", "parent@faces@>X", "Axis", param=0) @@ -1071,6 +1082,14 @@ class ElbowJoint(Model): motion_span=self.motion_span ) + def hole_loc_tags(self): + """ + An iterator which iterates through positions of the hole and tags + """ + for i, x in enumerate(self.hole_pos): + yield x, f"conn_top{i}" + yield -x, f"conn_bot{i}" + @property def total_thickness(self): return self.disk_joint.total_thickness diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 86a0e23..2be8c1e 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -63,6 +63,7 @@ class WingProfile(Model): angle_neutral=10.0, actuator=LINEAR_ACTUATOR_50, flexor_offset_angle=30, + parent_arm_width=15, flip=False, )) # Distance between the two spacers on the elbow, halved @@ -353,7 +354,7 @@ class WingProfile(Model): def surface_s0(self, top: bool = False) -> Cq.Workplane: base_dx = -(self.base_width - self.root_joint.child_width) / 2 - 10 - base_dy = self.root_joint.hirth_joint.joint_height + base_dy = self.root_joint.base_to_surface_thickness #mid_spacer_loc = ( # Cq.Location.from2d(0, -self.shoulder_width/2) * # self.shoulder_axle_loc * @@ -593,6 +594,25 @@ class WingProfile(Model): ) return mbox + def _spacer_from_disk_joint( + self, + joint: ElbowJoint, + segment_thickness: float, + ) -> MountingBox: + holes = [ + Hole(x, tag=tag) + for x, tag in joint.hole_loc_tags() + ] + mbox = MountingBox( + length=joint.lip_length, + width=segment_thickness, + thickness=self.spacer_thickness, + holes=holes, + hole_diam=joint.hole_diam, + centred=(True, True), + centre_left_right_tags=True, + ) + return mbox @target(name="profile-s1", kind=TargetKind.DXF) def profile_s1(self) -> Cq.Sketch: @@ -615,23 +635,17 @@ class WingProfile(Model): ) return profile def surface_s1(self, front: bool = True) -> Cq.Workplane: - tags_shoulder = [ + rot_elbow = Cq.Location.rot2d(self.elbow_rotate) + loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc() + tags = [ ("shoulder", Cq.Location((0, self.shoulder_height / 2, 0)) * self.shoulder_joint.child_lip_loc()), - ] - rot_elbow = Cq.Location.rot2d(self.elbow_rotate) - loc_elbow = rot_elbow * self.elbow_joint.parent_arm_loc() - tags_elbow = [ - ("elbow_bot", self.elbow_axle_loc * loc_elbow * - Cq.Location.from2d(0, -self.elbow_h2)), - ("elbow_top", self.elbow_axle_loc * loc_elbow * - Cq.Location.from2d(0, self.elbow_h2)), + ("elbow", self.elbow_axle_loc * loc_elbow), ("elbow_act", self.elbow_axle_loc * rot_elbow * self.elbow_joint.actuator_mount_loc()), ] profile = self.profile_s1() - tags = tags_shoulder + tags_elbow return extrude_with_markers( profile, self.panel_thickness, tags, reverse=front) @submodel(name="spacer-s1-shoulder") @@ -653,10 +667,9 @@ class WingProfile(Model): ) @submodel(name="spacer-s1-elbow") def spacer_s1_elbow(self) -> MountingBox: - return self.spacer_of_joint( + return self._spacer_from_disk_joint( joint=self.elbow_joint, segment_thickness=self.s1_thickness, - dx=self.elbow_h2, ) @submodel(name="spacer-s1-elbow-act") def spacer_s1_elbow_act(self) -> MountingBox: @@ -683,16 +696,14 @@ class WingProfile(Model): ) for o, t in [ (self.spacer_s1_shoulder(), "shoulder"), - (self.spacer_s1_elbow(), "elbow_top"), - (self.spacer_s1_elbow(), "elbow_bot"), + (self.spacer_s1_elbow(), "elbow"), (self.spacer_s1_elbow_act(), "elbow_act"), ]: - is_top = t.endswith("_top") self._assembly_insert_spacer( result, o.generate(), point_tag=t, - flipped=not is_top, + flipped=True, ) return result.solve() @@ -743,24 +754,21 @@ class WingProfile(Model): return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc() - tags_elbow = [ - ("elbow_bot", self.elbow_axle_loc * loc_elbow * - Cq.Location.from2d(0, self.elbow_h2)), - ("elbow_top", self.elbow_axle_loc * loc_elbow * - Cq.Location.from2d(0, -self.elbow_h2)), - ] rot_wrist = Cq.Location.rot2d(self.wrist_rotate) loc_wrist = rot_wrist * self.wrist_joint.parent_arm_loc() - tags_wrist = [ + tags = [ + ("elbow", self.elbow_axle_loc * loc_elbow), + ("wrist", self.wrist_axle_loc * loc_wrist), + ("wrist_act", self.wrist_axle_loc * rot_wrist * + self.wrist_joint.actuator_mount_loc()), + + # for mounting the bridge only ("wrist_bot", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, -self.wrist_h2)), ("wrist_top", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, self.wrist_h2)), - ("wrist_act", self.wrist_axle_loc * rot_wrist * - self.wrist_joint.actuator_mount_loc()), ] profile = self.profile_s2() - tags = tags_elbow + tags_wrist return extrude_with_markers(profile, self.panel_thickness, tags, reverse=front) def surface_s2_bridge(self, front: bool = True) -> Optional[Cq.Workplane]: profile = self.profile_s2_bridge() @@ -777,17 +785,15 @@ class WingProfile(Model): profile, self.panel_thickness, tags, reverse=not front) @submodel(name="spacer-s2-elbow") def spacer_s2_elbow(self) -> MountingBox: - return self.spacer_of_joint( + return self._spacer_from_disk_joint( joint=self.elbow_joint, segment_thickness=self.s2_thickness, - dx=self.elbow_h2, ) @submodel(name="spacer-s2-wrist") def spacer_s2_wrist(self) -> MountingBox: - return self.spacer_of_joint( + return self._spacer_from_disk_joint( joint=self.wrist_joint, segment_thickness=self.s2_thickness, - dx=self.wrist_h2, ) @submodel(name="spacer-s2-wrist-act") def spacer_s2_wrist_act(self) -> MountingBox: @@ -831,20 +837,16 @@ class WingProfile(Model): .constrain("back?wrist_top", "bridge_back?wrist_top", "Plane") ) for o, t in [ - (self.spacer_s2_elbow(), "elbow_bot"), - (self.spacer_s2_elbow(), "elbow_top"), - (self.spacer_s2_wrist(), "wrist_bot"), - (self.spacer_s2_wrist(), "wrist_top"), + (self.spacer_s2_elbow(), "elbow"), + (self.spacer_s2_wrist(), "wrist"), (self.spacer_s2_wrist_act(), "wrist_act"), ]: - is_top = t.endswith("_top") is_parent = t.startswith("elbow") self._assembly_insert_spacer( result, o.generate(), point_tag=t, - flipped=is_top == is_parent, - #rotate=not is_parent, + flipped=is_parent, ) return result.solve() @@ -878,6 +880,7 @@ class WingProfile(Model): front: bool = True) -> Cq.Workplane: loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc() tags = [ + ("wrist", self.wrist_axle_loc * loc_wrist), ("wrist_bot", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, self.wrist_h2)), ("wrist_top", self.wrist_axle_loc * loc_wrist * @@ -900,10 +903,9 @@ class WingProfile(Model): return extrude_with_markers(profile, self.panel_thickness, tags, reverse=not front) @submodel(name="spacer-s3-wrist") def spacer_s3_wrist(self) -> MountingBox: - return self.spacer_of_joint( + return self._spacer_from_disk_joint( joint=self.wrist_joint, segment_thickness=self.s3_thickness, - dx=self.wrist_h2, ) @assembly() def assembly_s3(self) -> Cq.Assembly: @@ -929,28 +931,26 @@ class WingProfile(Model): .constrain("back?wrist_bot", "extra_back?wrist_bot", "Plane") .constrain("back?wrist_top", "extra_back?wrist_top", "Plane") ) - for t in ["wrist_bot", "wrist_top"]: - is_top = t.endswith("_top") - o = self.spacer_s3_wrist() - self._assembly_insert_spacer( - result, - o.generate(), - point_tag=t, - flipped=is_top, - ) + self._assembly_insert_spacer( + result, + self.spacer_s3_wrist().generate(), + point_tag="wrist", + flipped=True, + ) return result.solve() @assembly() - def assembly(self, - parts: Optional[list[str]] = None, - shoulder_deflection: float = 0.0, - elbow_wrist_deflection: float = 0.0, - root_offset: int = 5, - fastener_pos: float = 0.0, - ignore_fasteners: bool = False, - ignore_electronics: bool = False, - ignore_actuators: bool = False, - ) -> Cq.Assembly(): + def assembly( + self, + parts: Optional[list[str]] = None, + shoulder_deflection: float = 0.0, + elbow_wrist_deflection: float = 0.0, + root_offset: int = 5, + fastener_pos: float = 0.0, + ignore_fasteners: bool = False, + ignore_electronics: bool = False, + ignore_actuators: bool = False, + ) -> Cq.Assembly(): if parts is None: parts = [ "root", @@ -1013,55 +1013,44 @@ class WingProfile(Model): angle=angle, ignore_actuators=ignore_actuators), name="elbow") if "s1" in parts and "elbow" in parts: - ( - result - .constrain("s1/elbow_top?conn0", "elbow/parent_upper/lip?conn_top0", "Plane") - .constrain("s1/elbow_top?conn1", "elbow/parent_upper/lip?conn_top1", "Plane") - .constrain("s1/elbow_bot?conn0", "elbow/parent_upper/lip?conn_bot0", "Plane") - .constrain("s1/elbow_bot?conn1", "elbow/parent_upper/lip?conn_bot1", "Plane") - ) + for _, tag in self.elbow_joint.hole_loc_tags(): + result.constrain( + f"s1/elbow?{tag}", + f"elbow/parent_upper/lip?{tag}", "Plane") if not ignore_actuators: - ( - result - .constrain("elbow/bracket_back?conn_side", "s1/elbow_act?conn0", "Plane") - ) + result.constrain( + "elbow/bracket_back?conn_side", + "s1/elbow_act?conn0", + "Plane") if "s2" in parts: result.add(self.assembly_s2(), name="s2") if "s2" in parts and "elbow" in parts: - ( - result - .constrain("s2/elbow_top?conn0", "elbow/child/lip?conn_top0", "Plane") - .constrain("s2/elbow_top?conn1", "elbow/child/lip?conn_top1", "Plane") - .constrain("s2/elbow_bot?conn0", "elbow/child/lip?conn_bot0", "Plane") - .constrain("s2/elbow_bot?conn1", "elbow/child/lip?conn_bot1", "Plane") - ) + for _, tag in self.elbow_joint.hole_loc_tags(): + result.constrain( + f"s2/elbow?{tag}", + f"elbow/child/lip?{tag}", "Plane") if "wrist" in parts: angle = self.wrist_joint.motion_span * elbow_wrist_deflection result.add(self.wrist_joint.assembly( angle=angle, ignore_actuators=ignore_actuators), name="wrist") - wrist_n_holes = len(self.wrist_joint.hole_pos) if "s2" in parts and "wrist" in parts: - for i in range(wrist_n_holes): - ( - result - .constrain(f"s2/wrist_top?conn{i}", f"wrist/parent_upper/lip?conn_top{i}", "Plane") - .constrain(f"s2/wrist_bot?conn{i}", f"wrist/parent_upper/lip?conn_bot{i}", "Plane") - ) + for _, tag in self.wrist_joint.hole_loc_tags(): + result.constrain( + f"s2/wrist?{tag}", + f"wrist/parent_upper/lip?{tag}", "Plane") if "s3" in parts: result.add(self.assembly_s3(), name="s3") if "s3" in parts and "wrist" in parts: - for i in range(wrist_n_holes): - ( - result - .constrain(f"s3/wrist_top?conn{i}", f"wrist/child/lip?conn_top{i}", "Plane") - .constrain(f"s3/wrist_bot?conn{i}", f"wrist/child/lip?conn_bot{i}", "Plane") - ) + for _, tag in self.wrist_joint.hole_loc_tags(): + result.constrain( + f"s3/wrist?{tag}", + f"wrist/child/lip?{tag}", "Plane") if not ignore_actuators: - ( - result - .constrain("wrist/bracket_back?conn_side", "s2/wrist_act?conn0", "Plane") - ) + result.constrain( + "wrist/bracket_back?conn_side", + "s2/wrist_act?conn0", + "Plane") if len(parts) > 1: result.solve() -- 2.44.1 From 7508d472658beb317ed919b474ce98e0cc55233e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 16:21:46 -0700 Subject: [PATCH 143/187] fix: Tag direction in HS joint holes --- nhf/touhou/houjuu_nue/joints.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index e9f78ff..05cf0e7 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -204,7 +204,7 @@ class RootJoint(Model): plane = result.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, -self.child_extra_thickness))) for i, (px, py) in enumerate(conn): - plane.moveTo(px, py).tagPlane(f"conn{i}") + plane.moveTo(px, py).tagPlane(f"conn{i}", direction="-Z") result = ( result .faces(">Z") diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 2be8c1e..985080d 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -983,9 +983,10 @@ class WingProfile(Model): if "s0" in parts and "root" in parts: ( result - .constrain("s0/base?conn0", "root/child?conn0", "Plane", param=0) - .constrain("s0/base?conn1", "root/child?conn1", "Plane", param=0) - .constrain("s0/base?conn2", "root/child?conn2", "Plane", param=0) + .constrain("s0/base?conn0", "root/child?conn0", "Point") + .constrain("s0/base?conn1", "root/child?conn1", "Point") + .constrain("s0/base?conn2", "root/child?conn2", "Point") + #.constrain("s0/base?conn3", "root/child?conn3", "Point") ) if "shoulder" in parts: angle = shoulder_deflection * self.shoulder_joint.angle_max_deflection -- 2.44.1 From 98a93cc6514dc861ed7fd80c3be887e58f2e45f0 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 18:03:43 -0700 Subject: [PATCH 144/187] feat: Elbow joint connectedness --- nhf/touhou/houjuu_nue/joints.py | 21 ++++++++++++++++++--- nhf/touhou/houjuu_nue/wing.py | 16 +++++++++++----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 05cf0e7..25b0ebc 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1050,6 +1050,8 @@ class ElbowJoint(Model): parent_arm_radius: float = 40.0 lip_thickness: float = 5.0 + # Extra bit on top of the lip to connect to actuator mount + child_lip_extra_length: float = 1.0 lip_length: float = 60.0 hole_pos: list[float] = field(default_factory=lambda: [15, 25]) parent_arm_width: float = 10.0 @@ -1069,8 +1071,9 @@ class ElbowJoint(Model): flexor: Optional[Flexor] = None # Rotates the entire flexor flexor_offset_angle: float = 0 - # Rotates the surface of the mount - flexor_mount_rot: float = 0 + # Rotates the surface of the mount relative to radially inwards + flexor_mount_angle_parent: float = 0 + flexor_mount_angle_child: float = -90 def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing @@ -1133,7 +1136,8 @@ class ElbowJoint(Model): ) -> Cq.Location: # Moves the hole so the axle of the mount is perpendicular to it loc_mount = Cq.Location.from2d(self.flexor.mount_height, 0) * Cq.Location.rot2d(180) - loc_mount_orient = Cq.Location.rot2d(self.flexor_mount_rot * (-1 if child else 1)) + mount_angle = self.flexor_mount_angle_child if child else self.flexor_mount_angle_parent + loc_mount_orient = Cq.Location.rot2d(mount_angle) # Moves the hole to be some distance apart from 0 mount_r, mount_loc_angle, mount_parent_r = self.flexor.open_pos() loc_span = Cq.Location.from2d(mount_r if child else mount_parent_r, 0) @@ -1182,12 +1186,23 @@ class ElbowJoint(Model): loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness)) disk_cut = self.disk_joint._disk_cut().located( loc_lip.inverse * loc_cut_rel * loc_disk) + lip_extra = Cq.Solid.makeBox( + length=self.child_lip_extra_length, + width=self.total_thickness, + height=self.lip_thickness, + ).located(Cq.Location(( + self.lip_length / 2, + -self.total_thickness / 2, + 0, + ))) result = ( Cq.Assembly() .add(self.disk_joint.disk(), name="disk", loc=loc_rot_neutral * Cq.Location((0, 0, -dz), (0,0,1), angle)) .add(self.lip().cut(disk_cut), name="lip", loc=loc_rot_neutral * loc_disk.inverse * loc_lip) + .add(lip_extra, name="lip_extra", + loc=loc_rot_neutral * loc_disk.inverse * loc_lip) ) # Orientes the hole surface so it faces +X loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 985080d..8d4c241 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -64,6 +64,7 @@ class WingProfile(Model): actuator=LINEAR_ACTUATOR_50, flexor_offset_angle=30, parent_arm_width=15, + child_lip_extra_length=8, flip=False, )) # Distance between the two spacers on the elbow, halved @@ -99,12 +100,12 @@ class WingProfile(Model): wrist_bot_loc: Cq.Location wrist_height: float elbow_rotate: float = 10.0 - elbow_joint_overlap_median: float = 0.3 - wrist_joint_overlap_median: float = 0.5 wrist_rotate: float = -30.0 # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) - elbow_axle_pos: float = 0.5 - wrist_axle_pos: float = 0.0 + elbow_axle_pos: float + wrist_axle_pos: float + elbow_joint_overlap_median: float + wrist_joint_overlap_median: float # False for the right side, True for the left side flip: bool @@ -1089,6 +1090,10 @@ class WingR(WingProfile): ring_radius_inner: float = 22.0 flip: bool = False + elbow_axle_pos: float = 0.4 + wrist_axle_pos: float = 0.0 + elbow_joint_overlap_median: float = 0.35 + wrist_joint_overlap_median: float = 0.5 def __post_init__(self): super().__post_init__() @@ -1099,6 +1104,8 @@ class WingR(WingProfile): * Cq.Location.rot2d(self.arrow_angle) \ * Cq.Location.from2d(0, self.arrow_height + self.wrist_height) self.ring_loc = self.wrist_top_loc * self.ring_rel_loc + self.elbow_joint.flexor_offset_angle = 15 + self.elbow_joint.flexor_mount_angle_child = -75 assert self.ring_radius > self.ring_radius_inner assert 0 > self.blade_overlap_angle > self.arrow_angle @@ -1316,7 +1323,6 @@ class WingL(WingProfile): flip: bool = True elbow_axle_pos: float = 0.4 wrist_axle_pos: float = 0.5 - elbow_joint_overlap_median: float = 0.5 wrist_joint_overlap_median: float = 0.5 -- 2.44.1 From 57262f542fe2218ce9d8613e03eb4151cc2db7e2 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 18:17:39 -0700 Subject: [PATCH 145/187] feat: Measurements for 30mm and 50mm actuators --- nhf/touhou/houjuu_nue/electronics.py | 29 +++++++++++++++++++--------- nhf/touhou/houjuu_nue/test.py | 8 ++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 0a0b466..6d3df18 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -24,11 +24,11 @@ class LinearActuator(Item): front_width: float = 9.24 front_height: float = 5.98 - segment1_length: float = 37.55 + segment1_length: float = 37.54 segment1_width: float = 15.95 segment1_height: float = 11.94 - segment2_length: float = 37.47 + segment2_length: float = 37.37 segment2_width: float = 20.03 segment2_height: float = 15.03 @@ -154,7 +154,7 @@ class MountingBracket(Item): height: float = 12.20 thickness: float = 0.98 length: float = 13.00 - hole_to_side_ext: float = 8.10 + hole_to_side_ext: float = 8.25 def __post_init__(self): assert self.hole_to_side_ext - self.hole_diam / 2 > 0 @@ -272,13 +272,24 @@ class BatteryBox18650(Item): LINEAR_ACTUATOR_50 = LinearActuator( - mass=34.0, + mass=40.8, stroke_length=50, - # FIXME: Measure - front_hole_ext=6, - back_hole_ext=6, - segment1_length=50, - segment2_length=50, + shaft_diam=9.05, + front_hole_ext=4.32, + back_hole_ext=4.54, + segment1_length=57.35, + segment1_width=15.97, + segment1_height=11.95, + segment2_length=37.69, + segment2_width=19.97, + segment2_height=14.96, + + front_length=9.40, + front_width=9.17, + front_height=6.12, + back_length=9.18, + back_width=10.07, + back_height=8.06, ) LINEAR_ACTUATOR_30 = LinearActuator( mass=34.0, diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 91d81ad..465891c 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -7,6 +7,14 @@ from nhf.checks import pairwise_intersection class TestElectronics(unittest.TestCase): + def test_actuator_length(self): + self.assertAlmostEqual( + ME.LINEAR_ACTUATOR_50.conn_length, 103.9 + ) + self.assertAlmostEqual( + ME.LINEAR_ACTUATOR_30.conn_length, 83.9 + ) + def test_flexor(self): flexor = ME.Flexor( motion_span=60, -- 2.44.1 From ac6710eeeb709b7f4dfa5dda710f4b6388b12ace Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 21:49:54 -0700 Subject: [PATCH 146/187] feat: Solve actuator position with variable r --- nhf/geometry.py | 48 +++++++- nhf/test.py | 38 ++++++ nhf/touhou/houjuu_nue/electronics.py | 29 ++--- nhf/touhou/houjuu_nue/joints.py | 24 ++-- nhf/touhou/houjuu_nue/wing.py | 167 +++++++++++++-------------- 5 files changed, 189 insertions(+), 117 deletions(-) diff --git a/nhf/geometry.py b/nhf/geometry.py index 5ae48c5..b4aa68f 100644 --- a/nhf/geometry.py +++ b/nhf/geometry.py @@ -1,7 +1,7 @@ """ Geometry functions """ -from typing import Tuple +from typing import Tuple, Optional import math def min_radius_contraction_span_pos( @@ -62,3 +62,49 @@ def min_tangent_contraction_span_pos( phi = phi_ + theta assert theta <= phi < math.pi return r, phi, oq + +def contraction_span_pos_from_radius( + d_open: float, + d_closed: float, + theta: float, + r: Optional[float] = None, + smaller: bool = True, + ) -> Tuple[float, float, float]: + """ + Returns `(r, phi, r')` + + Set `smaller` to false to use the other solution, which has a larger + profile. + """ + if r is None: + return min_tangent_contraction_span_pos( + d_open=d_open, + d_closed=d_closed, + theta=theta) + assert 0 < theta < math.pi + assert d_open > d_closed + assert r > 0 + # Law of cosines + pp_ = r * math.sqrt(2 * (1 - math.cos(theta))) + d = d_open - d_closed + assert pp_ > d, f"Triangle inequality is violated. This joint is impossible: {pp_}, {d}" + assert d_open + d_closed > pp_, f"The span is too great to cover with this stroke length: {pp_}" + # Angle of PP'Q, via a numerically stable acos + beta = math.acos( + - d / pp_ * (1 + d / (2 * d_closed)) + + pp_ / (2 * d_closed)) + # Two solutions based on angle complementarity + if smaller: + contra_phi = beta - (math.pi - theta) / 2 + else: + # technically there's a 2pi in front + contra_phi = -(math.pi - theta) / 2 - beta + # Law of cosines, calculates `r'` + r_ = math.sqrt( + r * r + d_closed * d_closed - 2 * r * d_closed * math.cos(contra_phi) + ) + # sin phi_ / P'Q = sin contra_phi / r' + phi_ = math.asin(math.sin(contra_phi) / r_ * d_closed) + assert phi_ > 0, f"Actuator would need to traverse pass its minimal point, {math.degrees(phi_)}" + assert 0 <= theta + phi_ <= math.pi + return r, theta + phi_, r_ diff --git a/nhf/test.py b/nhf/test.py index 6ca9da5..40618b7 100644 --- a/nhf/test.py +++ b/nhf/test.py @@ -112,6 +112,44 @@ class TestGeometry(unittest.TestCase): y = r * math.sin(phi - theta) d = math.sqrt((x - rp) ** 2 + y ** 2) self.assertAlmostEqual(d, dc) + def test_contraction_span_pos_from_radius(self): + sl = 50.0 + dc = 112.0 + do = dc + sl + r = 70.0 + theta = math.radians(60.0) + for smaller in [False, True]: + with self.subTest(smaller=smaller): + r, phi, rp = nhf.geometry.contraction_span_pos_from_radius(do, dc, r=r, theta=theta, smaller=smaller) + with self.subTest(state='open'): + x = r * math.cos(phi) + y = r * math.sin(phi) + d = math.sqrt((x - rp) ** 2 + y ** 2) + self.assertAlmostEqual(d, do) + with self.subTest(state='closed'): + x = r * math.cos(phi - theta) + y = r * math.sin(phi - theta) + d = math.sqrt((x - rp) ** 2 + y ** 2) + self.assertAlmostEqual(d, dc) + def test_contraction_span_pos_from_radius_2(self): + sl = 40.0 + dc = 170.0 + do = dc + sl + r = 50.0 + theta = math.radians(120.0) + for smaller in [False, True]: + with self.subTest(smaller=smaller): + r, phi, rp = nhf.geometry.contraction_span_pos_from_radius(do, dc, r=r, theta=theta, smaller=smaller) + with self.subTest(state='open'): + x = r * math.cos(phi) + y = r * math.sin(phi) + d = math.sqrt((x - rp) ** 2 + y ** 2) + self.assertAlmostEqual(d, do) + with self.subTest(state='closed'): + x = r * math.cos(phi - theta) + y = r * math.sin(phi - theta) + d = math.sqrt((x - rp) ** 2 + y ** 2) + self.assertAlmostEqual(d, dc) class TestUtils(unittest.TestCase): diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 6d3df18..b06ec72 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -51,7 +51,7 @@ class LinearActuator(Item): return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext def generate(self, pos: float=0) -> Cq.Assembly: - assert -1e-6 <= pos <= 1 + 1e-6 + assert -1e-6 <= pos <= 1 + 1e-6, f"Illegal position: {pos}" stroke_x = pos * self.stroke_length front = ( Cq.Workplane('XZ') @@ -339,6 +339,7 @@ class Flexor: Actuator assembly which flexes, similar to biceps """ motion_span: float + arm_radius: Optional[float] = None actuator: LinearActuator = LINEAR_ACTUATOR_50 nut: HexNut = LINEAR_ACTUATOR_HEX_NUT @@ -352,22 +353,11 @@ class Flexor: return self.bracket.hole_to_side_ext def open_pos(self) -> Tuple[float, float, float]: - r, phi, r_ = nhf.geometry.min_tangent_contraction_span_pos( - d_open=self.actuator.conn_length + self.actuator.stroke_length, - d_closed=self.actuator.conn_length, - theta=math.radians(self.motion_span), - ) - return r, math.degrees(phi), r_ - #r, phi = nhf.geometry.min_radius_contraction_span_pos( - # d_open=self.actuator.conn_length + self.actuator.stroke_length, - # d_closed=self.actuator.conn_length, - # theta=math.radians(self.motion_span), - #) - #return r, math.degrees(phi), r - r, phi, r_ = nhf.geometry.min_tangent_contraction_span_pos( + r, phi, r_ = nhf.geometry.contraction_span_pos_from_radius( d_open=self.actuator.conn_length + self.actuator.stroke_length, d_closed=self.actuator.conn_length, theta=math.radians(self.motion_span), + r=self.arm_radius, ) return r, math.degrees(phi), r_ @@ -378,12 +368,15 @@ class Flexor: """ Length of the actuator at some angle """ + assert 0 <= angle <= self.motion_span r, phi, rp = self.open_pos() th = math.radians(phi - angle) - return math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2) - # Law of cosines - d2 = r * r + rp * rp - 2 * r * rp * math.cos(th) - return math.sqrt(d2) + + result = math.sqrt(r * r + rp * rp - 2 * r * rp * math.cos(th)) + #result = math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2) + assert self.actuator.conn_length <= result <= self.actuator.conn_length + self.actuator.stroke_length, \ + f"Illegal length: {result} in {self.actuator.conn_length}+{self.actuator.stroke_length}" + return result def add_to( diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 25b0ebc..4e15372 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -768,7 +768,7 @@ class DiskJoint(Model): radius_axle: float = 3.0 housing_thickness: float = 4.0 - disk_thickness: float = 7.0 + disk_thickness: float = 8.0 # Amount by which the wall carves in wall_inset: float = 2.0 @@ -784,7 +784,7 @@ class DiskJoint(Model): # leave some gap for cushion movement_gap: float = 5.0 # Angular span of tongue on disk - tongue_span: float = 30.0 + tongue_span: float = 25.0 tongue_length: float = 10.0 generate_inner_wall: bool = False @@ -1053,7 +1053,7 @@ class ElbowJoint(Model): # Extra bit on top of the lip to connect to actuator mount child_lip_extra_length: float = 1.0 lip_length: float = 60.0 - hole_pos: list[float] = field(default_factory=lambda: [15, 25]) + hole_pos: list[float] = field(default_factory=lambda: [12, 24]) parent_arm_width: float = 10.0 # Angle of the beginning of the parent arm parent_arm_angle: float = 180.0 @@ -1074,6 +1074,7 @@ class ElbowJoint(Model): # Rotates the surface of the mount relative to radially inwards flexor_mount_angle_parent: float = 0 flexor_mount_angle_child: float = -90 + flexor_child_arm_radius: Optional[float] = None def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing @@ -1082,7 +1083,8 @@ class ElbowJoint(Model): if self.actuator: self.flexor = Flexor( actuator=self.actuator, - motion_span=self.motion_span + motion_span=self.motion_span, + arm_radius=self.flexor_child_arm_radius, ) def hole_loc_tags(self): @@ -1263,18 +1265,18 @@ class ElbowJoint(Model): #.solve() ) if self.flexor: + result.add( + Cq.Edge.makeLine((-1,0,0), (1,0,0)), + name="act", + loc=self.actuator_mount_loc(child=False, unflip=True)) if generate_mount: # Orientes the hole surface so it faces +X loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) result.add( self.actuator_mount(), - name="act", - loc=self.actuator_mount_loc(child=False, unflip=True) * loc_thickness) - else: - result.add( - Cq.Edge.makeLine((-1,0,0), (1,0,0)), - name="act", - loc=self.actuator_mount_loc(child=False, unflip=True)) + name="act_mount", + loc=self.actuator_mount_loc(child=False, unflip=True) * loc_thickness + ) return result @assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 8d4c241..4cfcf4c 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -22,6 +22,31 @@ from nhf.touhou.houjuu_nue.electronics import ( ) import nhf.utils +ELBOW_PARAMS = dict( + disk_joint=DiskJoint( + movement_angle=55, + ), + hole_diam=4.0, + actuator=LINEAR_ACTUATOR_50, + parent_arm_width=15, +) +WRIST_PARAMS = dict( + disk_joint=DiskJoint( + movement_angle=30, + radius_disk=13.0, + radius_housing=15.0, + ), + hole_pos=[10], + lip_length=30, + child_arm_radius=23.0, + parent_arm_radius=30.0, + hole_diam=4.0, + angle_neutral=0.0, + actuator=LINEAR_ACTUATOR_10, + flexor_offset_angle=30.0, + flexor_child_arm_radius=None, +) + @dataclass(kw_only=True) class WingProfile(Model): @@ -55,37 +80,11 @@ class WingProfile(Model): s1_thickness: float = 25.0 - elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( - disk_joint=DiskJoint( - movement_angle=55, - ), - hole_diam=4.0, - angle_neutral=10.0, - actuator=LINEAR_ACTUATOR_50, - flexor_offset_angle=30, - parent_arm_width=15, - child_lip_extra_length=8, - flip=False, - )) + elbow_joint: ElbowJoint # Distance between the two spacers on the elbow, halved elbow_h2: float = 5.0 - wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( - disk_joint=DiskJoint( - movement_angle=30, - radius_disk=13.0, - radius_housing=15.0, - ), - hole_pos=[10], - lip_length=30, - child_arm_radius=23.0, - parent_arm_radius=30.0, - hole_diam=4.0, - angle_neutral=0.0, - actuator=LINEAR_ACTUATOR_10, - flexor_offset_angle=30.0, - flip=True, - )) + wrist_joint: ElbowJoint # Distance between the two spacers on the elbow, halved wrist_h2: float = 5.0 @@ -99,7 +98,7 @@ class WingProfile(Model): elbow_height: float wrist_bot_loc: Cq.Location wrist_height: float - elbow_rotate: float = 10.0 + elbow_rotate: float wrist_rotate: float = -30.0 # Position of the elbow axle with 0 being bottom and 1 being top (flipped on the left side) elbow_axle_pos: float @@ -575,35 +574,18 @@ class WingProfile(Model): Polygon shape to mask wrist """ - def spacer_of_joint( - self, - joint: ElbowJoint, - segment_thickness: float, - dx: float) -> MountingBox: - length = joint.lip_length / 2 - dx - holes = [ - Hole(x - dx) - for x in joint.hole_pos - ] - mbox = MountingBox( - length=length, - width=segment_thickness, - thickness=self.spacer_thickness, - holes=holes, - hole_diam=joint.hole_diam, - centred=(False, True), - ) - return mbox - def _spacer_from_disk_joint( self, joint: ElbowJoint, segment_thickness: float, + child: bool=False, ) -> MountingBox: + sign = 1 if child else -1 holes = [ - Hole(x, tag=tag) + Hole(sign * x, tag=tag) for x, tag in joint.hole_loc_tags() ] + # FIXME: Carve out the sides so light can pass through mbox = MountingBox( length=joint.lip_length, width=segment_thickness, @@ -789,6 +771,7 @@ class WingProfile(Model): return self._spacer_from_disk_joint( joint=self.elbow_joint, segment_thickness=self.s2_thickness, + child=True, ) @submodel(name="spacer-s2-wrist") def spacer_s2_wrist(self) -> MountingBox: @@ -847,7 +830,7 @@ class WingProfile(Model): result, o.generate(), point_tag=t, - flipped=is_parent, + flipped=True,#is_parent, ) return result.solve() @@ -907,6 +890,7 @@ class WingProfile(Model): return self._spacer_from_disk_joint( joint=self.wrist_joint, segment_thickness=self.s3_thickness, + child=True, ) @assembly() def assembly_s3(self) -> Cq.Assembly: @@ -952,6 +936,9 @@ class WingProfile(Model): ignore_electronics: bool = False, ignore_actuators: bool = False, ) -> Cq.Assembly(): + assert 0 <= elbow_wrist_deflection <= 1 + assert 0 <= shoulder_deflection <= 1 + assert 0 <= fastener_pos <= 1 if parts is None: parts = [ "root", @@ -963,9 +950,7 @@ class WingProfile(Model): "wrist", "s3", ] - result = ( - Cq.Assembly() - ) + result = Cq.Assembly() tag_top, tag_bot = "top", "bot" if self.flip: tag_top, tag_bot = tag_bot, tag_top @@ -1019,11 +1004,11 @@ class WingProfile(Model): result.constrain( f"s1/elbow?{tag}", f"elbow/parent_upper/lip?{tag}", "Plane") - if not ignore_actuators: - result.constrain( - "elbow/bracket_back?conn_side", - "s1/elbow_act?conn0", - "Plane") + #if not ignore_actuators: + # result.constrain( + # "elbow/bracket_back?conn_side", + # "s1/elbow_act?conn0", + # "Plane") if "s2" in parts: result.add(self.assembly_s2(), name="s2") if "s2" in parts and "elbow" in parts: @@ -1048,11 +1033,11 @@ class WingProfile(Model): result.constrain( f"s3/wrist?{tag}", f"wrist/child/lip?{tag}", "Plane") - if not ignore_actuators: - result.constrain( - "wrist/bracket_back?conn_side", - "s2/wrist_act?conn0", - "Plane") + #if not ignore_actuators: + # result.constrain( + # "wrist/bracket_back?conn_side", + # "s2/wrist_act?conn0", + # "Plane") if len(parts) > 1: result.solve() @@ -1068,9 +1053,23 @@ class WingR(WingProfile): elbow_bot_loc: Cq.Location = Cq.Location.from2d(290.0, 30.0, 27.0) elbow_height: float = 111.0 + elbow_rotate: float = 10.0 + elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + flexor_offset_angle=15, + flexor_mount_angle_child=-75, + flexor_child_arm_radius=None, + angle_neutral=10.0, + child_lip_extra_length=8, + flip=False, + **ELBOW_PARAMS + )) wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 289.0, 45.0) wrist_height: float = 60.0 + wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + flip=True, + **WRIST_PARAMS + )) # Extends from the wrist to the tip of the arrow arrow_height: float = 300 @@ -1104,8 +1103,6 @@ class WingR(WingProfile): * Cq.Location.rot2d(self.arrow_angle) \ * Cq.Location.from2d(0, self.arrow_height + self.wrist_height) self.ring_loc = self.wrist_top_loc * self.ring_rel_loc - self.elbow_joint.flexor_offset_angle = 15 - self.elbow_joint.flexor_mount_angle_child = -75 assert self.ring_radius > self.ring_radius_inner assert 0 > self.blade_overlap_angle > self.arrow_angle @@ -1216,26 +1213,12 @@ class WingR(WingProfile): This extension profile is required to accomodate the awkward shaped joint next to the scythe """ - # Generates the extension profile, which is required on both sides profile = self._child_joint_extension_profile( axle_loc=self.wrist_axle_loc, radius=self.wrist_height, angle_span=self.wrist_joint.motion_span, - bot=self.flip, + bot=False, ) - # Generates the contraction (cut) profile. only required on the left - if self.flip: - extra = ( - self.profile() - .reset() - .push([self.wrist_axle_loc]) - .each(self._wrist_joint_retract_cut_polygon, mode='i') - ) - profile = ( - profile - .push([self.wrist_axle_loc]) - .each(lambda _: extra, mode='a') - ) return profile def profile_s3_extra(self) -> Cq.Sketch: @@ -1274,7 +1257,6 @@ class WingR(WingProfile): .circle(self.blade_hole_diam / 2, mode='s') ) - def _mask_elbow(self) -> list[Tuple[float, float]]: l = 200 elbow_x, _ = self.elbow_bot_loc.to2d_pos() @@ -1306,11 +1288,24 @@ class WingR(WingProfile): class WingL(WingProfile): elbow_bot_loc: Cq.Location = Cq.Location.from2d(260.0, 110.0, 0.0) - elbow_height: float = 80.0 + elbow_height: float = 90.0 + elbow_rotate: float = 15.0 + elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + angle_neutral=30.0, + flexor_mount_angle_child=180, + flexor_offset_angle=15, + flexor_child_arm_radius=60.0, + flip=True, + **ELBOW_PARAMS + )) wrist_angle: float = -45.0 wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0) wrist_height: float = 43.0 + wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + flip=False, + **WRIST_PARAMS + )) shoulder_bezier_ext: float = 120.0 shoulder_bezier_drop: float = 15.0 @@ -1326,15 +1321,13 @@ class WingL(WingProfile): elbow_joint_overlap_median: float = 0.5 wrist_joint_overlap_median: float = 0.5 + def __post_init__(self): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) - self.elbow_joint.angle_neutral = 15.0 - self.elbow_joint.flip = True - self.elbow_rotate = 5.0 + self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0 self.wrist_rotate = -self.wrist_joint.angle_neutral - self.wrist_joint.flip = False self.shoulder_joint.flip = True super().__post_init__() -- 2.44.1 From a6ddfec55210b8ab0fbacbe9f7533d3b65bee5fc Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 24 Jul 2024 22:24:23 -0700 Subject: [PATCH 147/187] refactor: Separate options for Wing{L,R} --- nhf/touhou/houjuu_nue/electronics.py | 2 ++ nhf/touhou/houjuu_nue/joints.py | 2 ++ nhf/touhou/houjuu_nue/wing.py | 24 ++++++++++++------------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index b06ec72..e2ab9d6 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -340,6 +340,7 @@ class Flexor: """ motion_span: float arm_radius: Optional[float] = None + pos_smaller: bool = True actuator: LinearActuator = LINEAR_ACTUATOR_50 nut: HexNut = LINEAR_ACTUATOR_HEX_NUT @@ -358,6 +359,7 @@ class Flexor: d_closed=self.actuator.conn_length, theta=math.radians(self.motion_span), r=self.arm_radius, + smaller=self.pos_smaller, ) return r, math.degrees(phi), r_ diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 4e15372..5f99f19 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1074,6 +1074,7 @@ class ElbowJoint(Model): # Rotates the surface of the mount relative to radially inwards flexor_mount_angle_parent: float = 0 flexor_mount_angle_child: float = -90 + flexor_pos_smaller: bool = True flexor_child_arm_radius: Optional[float] = None def __post_init__(self): @@ -1084,6 +1085,7 @@ class ElbowJoint(Model): self.flexor = Flexor( actuator=self.actuator, motion_span=self.motion_span, + pos_smaller=self.flexor_pos_smaller, arm_radius=self.flexor_child_arm_radius, ) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 4cfcf4c..9a91e89 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -40,8 +40,6 @@ WRIST_PARAMS = dict( lip_length=30, child_arm_radius=23.0, parent_arm_radius=30.0, - hole_diam=4.0, - angle_neutral=0.0, actuator=LINEAR_ACTUATOR_10, flexor_offset_angle=30.0, flexor_child_arm_radius=None, @@ -116,12 +114,10 @@ class WingProfile(Model): self.elbow_top_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height) self.wrist_top_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height) - if self.flip: - self.elbow_axle_pos = 1 - self.elbow_axle_pos - self.elbow_axle_loc = self.elbow_bot_loc * Cq.Location.from2d(0, self.elbow_height * self.elbow_axle_pos) - if self.flip: - self.wrist_axle_pos = 1 - self.wrist_axle_pos - self.wrist_axle_loc = self.wrist_bot_loc * Cq.Location.from2d(0, self.wrist_height * self.wrist_axle_pos) + self.elbow_axle_loc = self.elbow_bot_loc * \ + Cq.Location.from2d(0, self.elbow_height * self.elbow_axle_pos) + self.wrist_axle_loc = self.wrist_bot_loc * \ + Cq.Location.from2d(0, self.wrist_height * self.wrist_axle_pos) assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness) assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) @@ -1068,6 +1064,7 @@ class WingR(WingProfile): wrist_height: float = 60.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( flip=True, + angle_neutral=0.0, **WRIST_PARAMS )) @@ -1292,14 +1289,17 @@ class WingL(WingProfile): elbow_rotate: float = 15.0 elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( angle_neutral=30.0, - flexor_mount_angle_child=180, + flexor_mount_angle_child=170, + flexor_mount_angle_parent=-30, flexor_offset_angle=15, + child_lip_extra_length=5.0, flexor_child_arm_radius=60.0, + flexor_pos_smaller=False, flip=True, **ELBOW_PARAMS )) - wrist_angle: float = -45.0 + wrist_angle: float = 0.0 wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0) wrist_height: float = 43.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( @@ -1316,7 +1316,7 @@ class WingL(WingProfile): arrow_height: float = 120.0 flip: bool = True - elbow_axle_pos: float = 0.4 + elbow_axle_pos: float = 0.5 wrist_axle_pos: float = 0.5 elbow_joint_overlap_median: float = 0.5 wrist_joint_overlap_median: float = 0.5 @@ -1326,7 +1326,7 @@ class WingL(WingProfile): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) - self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 30.0 + self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 50.0 self.wrist_rotate = -self.wrist_joint.angle_neutral self.shoulder_joint.flip = True -- 2.44.1 From bbfeb50f8ee1546e2501c16b5ceeb7e4526ae17e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 00:09:16 -0700 Subject: [PATCH 148/187] feat: Tension fibre --- nhf/parts/fibre.py | 58 ++++++++++++++++++++++++++++ nhf/touhou/houjuu_nue/electronics.py | 56 ++++++++++++++++++++++----- nhf/touhou/houjuu_nue/joints.py | 6 ++- nhf/touhou/houjuu_nue/wing.py | 4 ++ 4 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 nhf/parts/fibre.py diff --git a/nhf/parts/fibre.py b/nhf/parts/fibre.py new file mode 100644 index 0000000..003e64b --- /dev/null +++ b/nhf/parts/fibre.py @@ -0,0 +1,58 @@ +""" +A fibre, for bearing tension +""" +import cadquery as Cq +from dataclasses import dataclass +import nhf.utils + +def tension_fibre( + length: float, + hole_diam: float, + hole_twist: float=0, + thickness: float=0.5) -> Cq.Workplane: + """ + A fibre which holds tension, with an eyes on each end. + + """ + eye_female = Cq.Solid.makeTorus( + radius1=hole_diam/2 + thickness/2, + radius2=thickness/2, + dir=(1,0,0), + ) + hole_length_male = hole_diam * 2.5 + hole_height_male = hole_diam * 1.2 + eye_male = Cq.Solid.makeBox( + length=hole_length_male + thickness * 2, + width=thickness, + height=hole_height_male + thickness * 2, + ).located( + Cq.Location((-hole_length_male/2-thickness, -thickness/2, -hole_height_male/2-thickness)) + ).cut(Cq.Solid.makeBox( + length=hole_length_male, + width=thickness, + height=hole_height_male, + ).located(Cq.Location((-hole_length_male/2, -thickness/2, -hole_height_male/2)))) + height = length - hole_diam - thickness + assert height > 0, "String is too short to support the given hole sizes" + h1 = length/2 - hole_diam/2 - thickness/2 + h2 = length/2 - hole_height_male - thickness/2 + result = ( + Cq.Workplane('XY') + .cylinder( + radius=thickness/2, + height=h1, + centered=(True, True, False), + ) + .copyWorkplane(Cq.Workplane('YX')) + .cylinder( + radius=thickness/2, + height=h2, + centered=(True, True, False), + ) + .union(eye_female.located(Cq.Location((0, 0,length/2)))) + .union(eye_male.located(Cq.Location((0, 0,-length/2+hole_height_male/2+thickness/2), (0,0,1), hole_twist))) + ) + result.copyWorkplane(Cq.Workplane(Cq.Plane(origin=(0,0,length/2), normal=(1,0,0)))).tagPlane("female") + conn1_normal, _ = (Cq.Location((0,0,0),(0,0,1),hole_twist) * Cq.Location((1,0,0))).toTuple() + result.copyWorkplane(Cq.Workplane(Cq.Plane(origin=(0,0,-length/2), normal=conn1_normal))).tagPlane("male") + return result diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index e2ab9d6..84dc49f 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -8,6 +8,7 @@ import cadquery as Cq from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.materials import Role, Material from nhf.parts.box import MountingBox, Hole +from nhf.parts.fibre import tension_fibre from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON @@ -333,7 +334,7 @@ LINEAR_ACTUATOR_BRACKET = MountingBracket() BATTERY_BOX = BatteryBox18650() -@dataclass +@dataclass(kw_only=True) class Flexor: """ Actuator assembly which flexes, similar to biceps @@ -346,17 +347,30 @@ class Flexor: nut: HexNut = LINEAR_ACTUATOR_HEX_NUT bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET + # Length of line attached to the flexor + line_length: float = 0.0 + line_thickness: float = 0.5 + # By how much is the line permitted to slack. This reduces the effective stroke length + line_slack: float = 0.0 - # FIXME: Add a compression spring so the serviceable distances are not as fixed + def __post_init__(self): + assert self.line_slack <= self.line_length < self.actuator.stroke_length @property def mount_height(self): return self.bracket.hole_to_side_ext + @property + def d_open(self): + return self.actuator.conn_length + self.actuator.stroke_length + self.line_length - self.line_slack + @property + def d_closed(self): + return self.actuator.conn_length + self.line_length + def open_pos(self) -> Tuple[float, float, float]: r, phi, r_ = nhf.geometry.contraction_span_pos_from_radius( - d_open=self.actuator.conn_length + self.actuator.stroke_length, - d_closed=self.actuator.conn_length, + d_open=self.d_open, + d_closed=self.d_closed, theta=math.radians(self.motion_span), r=self.arm_radius, smaller=self.pos_smaller, @@ -376,8 +390,8 @@ class Flexor: result = math.sqrt(r * r + rp * rp - 2 * r * rp * math.cos(th)) #result = math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2) - assert self.actuator.conn_length <= result <= self.actuator.conn_length + self.actuator.stroke_length, \ - f"Illegal length: {result} in {self.actuator.conn_length}+{self.actuator.stroke_length}" + assert self.d_closed -1e-6 <= result <= self.d_open + 1e-6,\ + f"Illegal length: {result} not in [{self.d_closed}, {self.d_open}]" return result @@ -393,7 +407,9 @@ class Flexor: Adds the necessary mechanical components to this assembly. Does not invoke `a.solve()`. """ - pos = (target_length - self.actuator.conn_length) / self.actuator.stroke_length + draft = max(0, target_length - self.d_closed - self.line_length) + pos = draft / self.actuator.stroke_length + line_l = target_length - draft - self.actuator.conn_length if tag_prefix: tag_prefix = tag_prefix + "_" else: @@ -411,8 +427,6 @@ class Flexor: .add(self.bracket.assembly(), name=name_bracket_front) .add(self.bolt.assembly(), name=name_bolt_front) .add(self.nut.assembly(), name=name_nut_front) - .constrain(f"{name_actuator}/front?conn", f"{name_bracket_front}?conn_mid", - "Plane", param=0) .constrain(f"{name_bolt_front}?root", f"{name_bracket_front}?conn_top", "Plane", param=0) .constrain(f"{name_nut_front}?bot", f"{name_bracket_front}?conn_bot", @@ -427,6 +441,30 @@ class Flexor: .constrain(f"{name_nut_back}?bot", f"{name_bracket_back}?conn_bot", "Plane") ) + if self.line_length == 0.0: + a.constrain( + f"{name_actuator}/front?conn", + f"{name_bracket_front}?conn_mid", + "Plane", param=0) + else: + ( + a + .addS(tension_fibre( + length=line_l, + hole_diam=self.nut.diam_thread, + thickness=self.line_thickness, + ), name="fibre", role=Role.CONNECTION) + .constrain( + f"{name_actuator}/front?conn", + "fibre?male", + "Plane" + ) + .constrain( + f"{name_bracket_front}?conn_mid", + "fibre?female", + "Plane" + ) + ) if tag_hole_front: a.constrain(tag_hole_front, f"{name_bracket_front}?conn_side", "Plane") if tag_hole_back: diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 5f99f19..d7fd70a 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1067,7 +1067,7 @@ class ElbowJoint(Model): flip: bool = False angle_neutral: float = 30.0 - actuator: Optional[LinearActuator] + actuator: Optional[LinearActuator] = None flexor: Optional[Flexor] = None # Rotates the entire flexor flexor_offset_angle: float = 0 @@ -1076,6 +1076,8 @@ class ElbowJoint(Model): flexor_mount_angle_child: float = -90 flexor_pos_smaller: bool = True flexor_child_arm_radius: Optional[float] = None + flexor_line_length: float = 0.0 + flexor_line_slack: float = 0.0 def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing @@ -1087,6 +1089,8 @@ class ElbowJoint(Model): motion_span=self.motion_span, pos_smaller=self.flexor_pos_smaller, arm_radius=self.flexor_child_arm_radius, + line_length=self.flexor_line_length, + line_slack=self.flexor_line_slack, ) def hole_loc_tags(self): diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 9a91e89..5d2e4a3 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -1291,6 +1291,10 @@ class WingL(WingProfile): angle_neutral=30.0, flexor_mount_angle_child=170, flexor_mount_angle_parent=-30, + flexor_line_length=30.0, + flexor_line_slack=5.0, + #flexor_line_length=0.0, + #flexor_line_slack=0.0, flexor_offset_angle=15, child_lip_extra_length=5.0, flexor_child_arm_radius=60.0, -- 2.44.1 From c1107aed2eca6e23eeadba463276f44a0d1c0bb2 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 09:47:41 -0700 Subject: [PATCH 149/187] feat: Bridging joints for root --- nhf/parts/joints.py | 2 +- nhf/touhou/houjuu_nue/__init__.py | 45 ++++++++++++------ nhf/touhou/houjuu_nue/harness.py | 56 +++++++++++++++++----- nhf/touhou/houjuu_nue/joints.py | 79 +++++++++++++++++++++++++++++-- nhf/touhou/houjuu_nue/wing.py | 4 ++ 5 files changed, 155 insertions(+), 31 deletions(-) diff --git a/nhf/parts/joints.py b/nhf/parts/joints.py index e0e0357..4245eff 100644 --- a/nhf/parts/joints.py +++ b/nhf/parts/joints.py @@ -8,7 +8,7 @@ import nhf.utils TOL = 1e-6 -@dataclass +@dataclass(frozen=True) class HirthJoint: """ A Hirth joint attached to a cylindrical base diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 6022342..9d3c155 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -33,7 +33,6 @@ from dataclasses import dataclass, field from typing import Optional import cadquery as Cq from nhf.build import Model, TargetKind, target, assembly, submodel -from nhf.parts.joints import HirthJoint, TorsionJoint import nhf.touhou.houjuu_nue.wing as MW import nhf.touhou.houjuu_nue.trident as MT import nhf.touhou.houjuu_nue.joints as MJ @@ -53,40 +52,64 @@ class Parameters(Model): wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR( name="r1", - shoulder_angle_bias = WING_DEFLECT_ODD, + root_joint=MJ.RootJoint( + parent_substrate_cull_corners=(0,1,1,1), + parent_substrate_cull_edges=(0,0,1,0), + ), + shoulder_angle_bias=WING_DEFLECT_ODD, s0_top_hole=False, s0_bot_hole=True, arrow_height=350.0 )) wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR( name="r2", - shoulder_angle_bias = WING_DEFLECT_EVEN, + root_joint=MJ.RootJoint( + parent_substrate_cull_corners=(1,1,1,1), + parent_substrate_cull_edges=(0,0,1,0), + ), + shoulder_angle_bias=WING_DEFLECT_EVEN, s0_top_hole=True, s0_bot_hole=True, )) wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR( name="r3", - shoulder_angle_bias = WING_DEFLECT_ODD, + root_joint=MJ.RootJoint( + parent_substrate_cull_corners=(1,1,1,0), + parent_substrate_cull_edges=(0,0,1,0), + ), + shoulder_angle_bias=WING_DEFLECT_ODD, s0_top_hole=True, s0_bot_hole=False, )) wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL( name="l1", - shoulder_angle_bias = WING_DEFLECT_EVEN, + root_joint=MJ.RootJoint( + parent_substrate_cull_corners=(1,0,1,1), + parent_substrate_cull_edges=(1,0,0,0), + ), + shoulder_angle_bias=WING_DEFLECT_EVEN, wrist_angle=-60.0, s0_top_hole=False, s0_bot_hole=True, )) wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL( name="l2", + root_joint=MJ.RootJoint( + parent_substrate_cull_corners=(1,1,1,1), + parent_substrate_cull_edges=(1,0,0,0), + ), wrist_angle=-30.0, - shoulder_angle_bias = WING_DEFLECT_ODD, + shoulder_angle_bias=WING_DEFLECT_ODD, s0_top_hole=True, s0_bot_hole=True, )) wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL( name="l3", - shoulder_angle_bias = WING_DEFLECT_EVEN, + root_joint=MJ.RootJoint( + parent_substrate_cull_corners=(1,1,0,1), + parent_substrate_cull_edges=(1,0,0,0), + ), + shoulder_angle_bias=WING_DEFLECT_EVEN, wrist_angle=-0.0, s0_top_hole=True, s0_bot_hole=False, @@ -96,14 +119,6 @@ class Parameters(Model): def __post_init__(self): super().__init__(name="houjuu-nue") - self.wing_r1.root_joint = self.harness.root_joint - self.wing_r2.root_joint = self.harness.root_joint - self.wing_r3.root_joint = self.harness.root_joint - self.wing_l1.root_joint = self.harness.root_joint - self.wing_l2.root_joint = self.harness.root_joint - self.wing_l3.root_joint = self.harness.root_joint - - self.wing_r1.shoulder_joint.torsion_joint @submodel(name="harness") def submodel_harness(self) -> Model: diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index 8507873..1f31dfc 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -4,6 +4,7 @@ from nhf.parts.joints import HirthJoint from nhf import Material, Role from nhf.build import Model, TargetKind, target, assembly, submodel from nhf.touhou.houjuu_nue.joints import RootJoint +from nhf.parts.box import MountingBox import nhf.utils @dataclass(frozen=True, kw_only=True) @@ -46,21 +47,21 @@ class Mannequin: return result.translate((0, self.torso_thickness / 2, 0)) -BASE_POS_X = 60.0 +BASE_POS_X = 70.0 BASE_POS_Y = 100.0 @dataclass(kw_only=True) class Harness(Model): thickness: float = 25.4 / 8 width: float = 220.0 - height: float = 310.0 + height: float = 304.8 fillet: float = 10.0 wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [ - ("r1", BASE_POS_X + 10, BASE_POS_Y), - ("l1", -BASE_POS_X - 10, BASE_POS_Y), - ("r2", BASE_POS_X + 10, 0), - ("l2", -BASE_POS_X - 10, 0), + ("r1", BASE_POS_X, BASE_POS_Y), + ("l1", -BASE_POS_X, BASE_POS_Y), + ("r2", BASE_POS_X, 0), + ("l2", -BASE_POS_X, 0), ("r3", BASE_POS_X, -BASE_POS_Y), ("l3", -BASE_POS_X, -BASE_POS_Y), ]) @@ -72,9 +73,12 @@ class Harness(Model): def __post_init__(self): super().__init__(name="harness") - @submodel(name="root-joint") - def submodel_root_joint(self) -> Model: - return self.root_joint + @submodel(name="bridge-pair-horizontal") + def bridge_pair_horizontal(self) -> MountingBox: + return self.root_joint.bridge_pair_horizontal(centre_dx=BASE_POS_X * 2) + @submodel(name="bridge-pair-vertical") + def bridge_pair_vertical(self) -> MountingBox: + return self.root_joint.bridge_pair_vertical(centre_dy=BASE_POS_Y) @target(name="profile", kind=TargetKind.DXF) def profile(self) -> Cq.Sketch: @@ -126,7 +130,7 @@ class Harness(Model): conn = [(px + x, py + y) for px, py in self.root_joint.corner_pos()] for i, (px, py) in enumerate(conn): - plane.moveTo(px, py).tagPoint(f"{tag}_{i}") + plane.moveTo(px, py).tagPlane(f"{tag}_{i}") return result def add_root_joint_constraint( @@ -144,7 +148,6 @@ class Harness(Model): harness = self.surface() mannequin_z = self.mannequin.shoulder_to_waist * 0.6 - result = ( Cq.Assembly() .addS( @@ -159,6 +162,37 @@ class Harness(Model): loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180)) .constrain("mannequin", "Fixed") ) + bridge_h = self.bridge_pair_horizontal().generate() + for i in [1,2,3]: + name = f"r{i}l{i}_bridge" + ( + result + .addS( + bridge_h, name=name, + role=Role.FIXTURE, + material=Material.WOOD_BIRCH, + ) + .constrain(f"{name}?conn0_rev", f"base?r{i}_1", "Point") + .constrain(f"{name}?conn1_rev", f"base?l{i}_0", "Point") + .constrain(f"{name}?conn2_rev", f"base?l{i}_3", "Point") + .constrain(f"{name}?conn3_rev", f"base?r{i}_2", "Point") + ) + bridge_v = self.bridge_pair_vertical().generate() + ( + result + .addS(bridge_v, name="r1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH) + .constrain("r1_bridge?conn0_rev", "base?r1_3", 'Plane') + .constrain("r1_bridge?conn1_rev", "base?r2_0", 'Plane') + .addS(bridge_v, name="r2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH) + .constrain("r2_bridge?conn0_rev", "base?r2_3", 'Plane') + .constrain("r2_bridge?conn1_rev", "base?r3_0", 'Plane') + .addS(bridge_v, name="l1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH) + .constrain("l1_bridge?conn0_rev", "base?l1_2", 'Plane') + .constrain("l1_bridge?conn1_rev", "base?l2_1", 'Plane') + .addS(bridge_v, name="l2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH) + .constrain("l2_bridge?conn0_rev", "base?l2_2", 'Plane') + .constrain("l2_bridge?conn1_rev", "base?l3_1", 'Plane') + ) if with_root_joint: for name in ["l1", "l2", "l3", "r1", "r2", "r3"]: result.addS( diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index d7fd70a..4be4bae 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -4,6 +4,7 @@ from typing import Optional, Tuple import cadquery as Cq from nhf import Material, Role from nhf.build import Model, target, assembly +from nhf.parts.box import MountingBox from nhf.parts.springs import TorsionSpring from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob from nhf.parts.joints import TorsionJoint, HirthJoint @@ -88,6 +89,10 @@ class RootJoint(Model): parent_corner_inset: float = 12 parent_mount_thickness: float = 25.4 / 16 + parent_substrate_thickness: float = 25.4 / 16 + parent_substrate_cull_corners: Tuple[bool, bool, bool, bool] = (False, False, True, False) + parent_substrate_cull_edges: Tuple[bool, bool, bool, bool] = (False, False, True, False) + child_corner_dx: float = 17.0 child_corner_dz: float = 24.0 @@ -103,6 +108,8 @@ class RootJoint(Model): def __post_init__(self): assert self.child_extra_thickness > 0.0 assert self.parent_thickness >= self.hex_nut.thickness + # twice the buffer allocated for substratum + assert self.parent_width >= 4 * self.parent_corner_inset def corner_pos(self) -> list[tuple[int, int]]: """ @@ -111,9 +118,9 @@ class RootJoint(Model): dx = self.parent_width / 2 - self.parent_corner_inset return [ (dx, dx), - (dx, -dx), - (-dx, -dx), (-dx, dx), + (-dx, -dx), + (dx, -dx), ] @property @@ -127,6 +134,45 @@ class RootJoint(Model): def base_to_surface_thickness(self) -> float: return self.hirth_joint.joint_height + self.child_extra_thickness + @property + def substrate_inset(self) -> float: + return self.parent_corner_inset * 2 + + def bridge_pair_horizontal(self, centre_dx: float) -> MountingBox: + hole_dx = centre_dx / 2 - self.parent_width / 2 + self.parent_corner_inset + hole_dy = self.parent_width / 2 - self.parent_corner_inset + holes = [ + Hole(x=hole_dx, y=hole_dy), + Hole(x=-hole_dx, y=hole_dy), + Hole(x=-hole_dx, y=-hole_dy), + Hole(x=hole_dx, y=-hole_dy), + ] + return MountingBox( + length=centre_dx - self.parent_width + self.substrate_inset * 2, + width=self.parent_width, + thickness=self.parent_substrate_thickness, + hole_diam=self.corner_hole_diam, + holes=holes, + centred=(True, True), + generate_reverse_tags=True, + ) + def bridge_pair_vertical(self, centre_dy: float) -> MountingBox: + hole_dy = centre_dy / 2 - self.parent_width / 2 + self.parent_corner_inset + holes = [ + Hole(x=0, y=hole_dy), + Hole(x=0, y=-hole_dy), + ] + return MountingBox( + length=self.substrate_inset, + width=centre_dy - self.parent_width + self.substrate_inset * 2, + thickness=self.parent_substrate_thickness, + hole_diam=self.corner_hole_diam, + holes=holes, + centred=(True, True), + generate_reverse_tags=True, + ) + + @target(name="parent") def parent(self): """ @@ -142,7 +188,6 @@ class RootJoint(Model): width=self.parent_width, height=self.parent_thickness, centered=(True, True, False)) - .translate((0, 0, -self.parent_thickness)) .edges("|Z") .fillet(self.parent_corner_fillet) .faces(">Z") @@ -153,6 +198,32 @@ class RootJoint(Model): cboreDiameter=self.parent_corner_cbore_diam, cboreDepth=self.parent_corner_cbore_depth) ) + sso = self.parent_width / 2 - self.substrate_inset + loc_corner = Cq.Location((sso, sso, 0)) + loc_edge= Cq.Location((sso, -sso, 0)) + cut_corner = Cq.Solid.makeBox( + length=self.parent_width, + width=self.parent_width, + height=self.parent_substrate_thickness, + ) + cut_edge = Cq.Solid.makeBox( + length=self.parent_width, + width=self.parent_width - self.substrate_inset * 2, + height=self.parent_substrate_thickness, + ) + step = 90 + for i, flag in enumerate(self.parent_substrate_cull_corners): + if not flag: + continue + loc = Cq.Location((0,0,0),(0,0,1), i * step) * loc_corner + result = result.cut(cut_corner.located(loc)) + for i, flag in enumerate(self.parent_substrate_cull_edges): + if not flag: + continue + loc = Cq.Location((0,0,0),(0,0,1), i * step) * loc_edge + result = result.cut(cut_edge.located(loc)) + + result = result.translate((0, 0, -self.parent_thickness)) # Creates a plane parallel to the holes but shifted to the base plane = result.faces(">Z").workplane(offset=-self.parent_thickness) @@ -171,7 +242,7 @@ class RootJoint(Model): .hole(diameter=self.axis_diam) .cut(self.hex_nut.generate().translate((0, 0, -self.parent_thickness))) ) - result.faces(" Model: + return self.root_joint @submodel(name="shoulder-joint") def submodel_shoulder_joint(self) -> Model: return self.shoulder_joint @@ -312,6 +315,7 @@ class WingProfile(Model): Hole(x=dx, y=-dy), Hole(x=dx, y=dy), Hole(x=-dx, y=dy), + Hole(x=0, y=0, diam=self.root_joint.axis_diam, tag="axle"), ] return MountingBox( length=self.root_height, -- 2.44.1 From 82570528da0f488ee558aefe09355c9ca62b8a57 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 10:41:58 -0700 Subject: [PATCH 150/187] feat: Detach mount point from joint --- nhf/touhou/houjuu_nue/electronics.py | 3 +- nhf/touhou/houjuu_nue/joints.py | 57 ++++++++++++++++++---------- nhf/touhou/houjuu_nue/wing.py | 56 ++++++++++++++++++--------- 3 files changed, 77 insertions(+), 39 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 84dc49f..a17c819 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -354,7 +354,8 @@ class Flexor: line_slack: float = 0.0 def __post_init__(self): - assert self.line_slack <= self.line_length < self.actuator.stroke_length + assert self.line_slack <= self.line_length, f"Insufficient length: {self.line_slack} >= {self.line_length}" + assert self.line_slack < self.actuator.stroke_length @property def mount_height(self): diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 4be4bae..9884a88 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1121,8 +1121,6 @@ class ElbowJoint(Model): parent_arm_radius: float = 40.0 lip_thickness: float = 5.0 - # Extra bit on top of the lip to connect to actuator mount - child_lip_extra_length: float = 1.0 lip_length: float = 60.0 hole_pos: list[float] = field(default_factory=lambda: [12, 24]) parent_arm_width: float = 10.0 @@ -1149,6 +1147,8 @@ class ElbowJoint(Model): flexor_child_arm_radius: Optional[float] = None flexor_line_length: float = 0.0 flexor_line_slack: float = 0.0 + flexor_parent_angle_fix: Optional[float] = 180.0 + flexor_child_angle_fix: Optional[float] = None def __post_init__(self): assert self.child_arm_radius > self.disk_joint.radius_housing @@ -1220,10 +1220,15 @@ class ElbowJoint(Model): # Moves the hole to be some distance apart from 0 mount_r, mount_loc_angle, mount_parent_r = self.flexor.open_pos() loc_span = Cq.Location.from2d(mount_r if child else mount_parent_r, 0) - alpha = (-mount_loc_angle if child else 0) + 180 - self.flexor_offset_angle + if self.flexor_parent_angle_fix is not None: + alpha = (-mount_loc_angle if child else 0) + self.flexor_parent_angle_fix - self.flexor_offset_angle + elif self.flexor_child_angle_fix is not None: + alpha = self.flexor_child_angle_fix + (0 if child else mount_loc_angle) + else: + raise ValueError("One of flexor_{parent,child}_angle_fix must be set") loc_rot = Cq.Location.rot2d(alpha) loc = loc_rot * loc_span * loc_mount_orient * loc_mount - return loc.flip_y() if self.flip and not child and not unflip else loc + return loc.flip_y() if self.flip and not unflip else loc def lip(self) -> Cq.Workplane: sign = -1 if self.flip else 1 @@ -1248,7 +1253,7 @@ class ElbowJoint(Model): return mbox.generate() @target(name="child") - def child_joint(self) -> Cq.Assembly: + def child_joint(self, generate_mount: bool=False) -> Cq.Assembly: angle = -self.disk_joint.tongue_span / 2 dz = self.disk_joint.disk_thickness / 2 # We need to ensure the disk is on the "other" side so @@ -1265,31 +1270,40 @@ class ElbowJoint(Model): loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness)) disk_cut = self.disk_joint._disk_cut().located( loc_lip.inverse * loc_cut_rel * loc_disk) - lip_extra = Cq.Solid.makeBox( - length=self.child_lip_extra_length, - width=self.total_thickness, - height=self.lip_thickness, - ).located(Cq.Location(( - self.lip_length / 2, - -self.total_thickness / 2, - 0, - ))) + #lip_extra = Cq.Solid.makeBox( + # length=self.child_lip_extra_length, + # width=self.total_thickness, + # height=self.lip_thickness, + #).located(Cq.Location(( + # self.lip_length / 2, + # -self.total_thickness / 2, + # 0, + #))) result = ( Cq.Assembly() .add(self.disk_joint.disk(), name="disk", loc=loc_rot_neutral * Cq.Location((0, 0, -dz), (0,0,1), angle)) .add(self.lip().cut(disk_cut), name="lip", loc=loc_rot_neutral * loc_disk.inverse * loc_lip) - .add(lip_extra, name="lip_extra", - loc=loc_rot_neutral * loc_disk.inverse * loc_lip) + #.add(lip_extra, name="lip_extra", + # loc=loc_rot_neutral * loc_disk.inverse * loc_lip) ) # Orientes the hole surface so it faces +X loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) if self.flexor: + loc_mount = self.actuator_mount_loc(child=True, unflip=True) result.add( - self.actuator_mount(), + Cq.Edge.makeLine((-1,0,0), (1,0,0)), name="act", - loc=self.actuator_mount_loc(child=True) * loc_thickness) + loc=loc_mount) + if generate_mount: + # Orientes the hole surface so it faces +X + loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) + result.add( + self.actuator_mount(), + name="act_mount", + loc=loc_mount * loc_thickness, + ) return result @target(name="parent-lower") @@ -1342,17 +1356,18 @@ class ElbowJoint(Model): #.solve() ) if self.flexor: + loc_mount = self.actuator_mount_loc(child=False, unflip=True) result.add( Cq.Edge.makeLine((-1,0,0), (1,0,0)), name="act", - loc=self.actuator_mount_loc(child=False, unflip=True)) + loc=loc_mount) if generate_mount: # Orientes the hole surface so it faces +X loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) result.add( self.actuator_mount(), name="act_mount", - loc=self.actuator_mount_loc(child=False, unflip=True) * loc_thickness + loc=loc_mount * loc_thickness ) return result @@ -1364,7 +1379,7 @@ class ElbowJoint(Model): assert 0 <= angle <= self.motion_span result = ( Cq.Assembly() - .addS(self.child_joint(), name="child", + .addS(self.child_joint(generate_mount=generate_mount), name="child", role=Role.CHILD, material=self.material) .addS(self.parent_joint_lower(), name="parent_lower", role=Role.CASING, material=self.material) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 0e0873b..9d057dd 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -23,9 +23,6 @@ from nhf.touhou.houjuu_nue.electronics import ( import nhf.utils ELBOW_PARAMS = dict( - disk_joint=DiskJoint( - movement_angle=55, - ), hole_diam=4.0, actuator=LINEAR_ACTUATOR_50, parent_arm_width=15, @@ -508,6 +505,8 @@ class WingProfile(Model): loc_ext = loc_bot if bot else loc_top loc_tip = loc_top if bot else loc_bot theta = math.radians(angle_span * (median if child else 1 - median)) + if self.flip: + axle_pos = 1 - axle_pos y_sign = -1 if bot else 1 sign = -1 if child else 1 dh = axle_pos * height * (overestimate - 1) @@ -736,11 +735,14 @@ class WingProfile(Model): ) return profile def surface_s2(self, front: bool = True) -> Cq.Workplane: - loc_elbow = Cq.Location.rot2d(self.elbow_rotate) * self.elbow_joint.child_arm_loc() + rot_elbow = Cq.Location.rot2d(self.elbow_rotate) + loc_elbow = rot_elbow * self.elbow_joint.child_arm_loc() rot_wrist = Cq.Location.rot2d(self.wrist_rotate) loc_wrist = rot_wrist * self.wrist_joint.parent_arm_loc() tags = [ ("elbow", self.elbow_axle_loc * loc_elbow), + ("elbow_act", self.elbow_axle_loc * rot_elbow * + self.elbow_joint.actuator_mount_loc(child=True)), ("wrist", self.wrist_axle_loc * loc_wrist), ("wrist_act", self.wrist_axle_loc * rot_wrist * self.wrist_joint.actuator_mount_loc()), @@ -773,6 +775,17 @@ class WingProfile(Model): segment_thickness=self.s2_thickness, child=True, ) + @submodel(name="spacer-s1-elbow-act") + def spacer_s2_elbow_act(self) -> MountingBox: + return MountingBox( + length=self.s2_thickness, + width=self.s2_thickness, + thickness=self.spacer_thickness, + holes=[Hole(x=0,y=0)], + centred=(True, True), + hole_diam=self.elbow_joint.hole_diam, + centre_left_right_tags=True, + ) @submodel(name="spacer-s2-wrist") def spacer_s2_wrist(self) -> MountingBox: return self._spacer_from_disk_joint( @@ -822,6 +835,7 @@ class WingProfile(Model): ) for o, t in [ (self.spacer_s2_elbow(), "elbow"), + (self.spacer_s2_elbow_act(), "elbow_act"), (self.spacer_s2_wrist(), "wrist"), (self.spacer_s2_wrist_act(), "wrist_act"), ]: @@ -1055,11 +1069,13 @@ class WingR(WingProfile): elbow_height: float = 111.0 elbow_rotate: float = 10.0 elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + disk_joint=DiskJoint( + movement_angle=55, + ), flexor_offset_angle=15, flexor_mount_angle_child=-75, flexor_child_arm_radius=None, angle_neutral=10.0, - child_lip_extra_length=8, flip=False, **ELBOW_PARAMS )) @@ -1288,24 +1304,32 @@ class WingR(WingProfile): @dataclass(kw_only=True) class WingL(WingProfile): - elbow_bot_loc: Cq.Location = Cq.Location.from2d(260.0, 110.0, 0.0) - elbow_height: float = 90.0 + elbow_bot_loc: Cq.Location = Cq.Location.from2d(260.0, 105.0, 0.0) + elbow_height: float = 95.0 elbow_rotate: float = 15.0 elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( - angle_neutral=30.0, - flexor_mount_angle_child=170, - flexor_mount_angle_parent=-30, - flexor_line_length=30.0, - flexor_line_slack=5.0, + disk_joint=DiskJoint( + movement_angle=50, + ), + angle_neutral=25.0, + flexor_mount_angle_child=220, + flexor_mount_angle_parent=0, + flexor_line_length=50.0, + flexor_line_slack=10.0, #flexor_line_length=0.0, #flexor_line_slack=0.0, - flexor_offset_angle=15, - child_lip_extra_length=5.0, - flexor_child_arm_radius=60.0, + flexor_offset_angle=0, + flexor_child_angle_fix=85, + flexor_parent_angle_fix=None, + flexor_child_arm_radius=50.0, + parent_arm_radius=50.0, + child_arm_radius=50.0, flexor_pos_smaller=False, flip=True, **ELBOW_PARAMS )) + elbow_axle_pos: float = 0.53 + elbow_joint_overlap_median: float = 0.5 wrist_angle: float = 0.0 wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0) @@ -1324,9 +1348,7 @@ class WingL(WingProfile): arrow_height: float = 120.0 flip: bool = True - elbow_axle_pos: float = 0.5 wrist_axle_pos: float = 0.5 - elbow_joint_overlap_median: float = 0.5 wrist_joint_overlap_median: float = 0.5 -- 2.44.1 From 720de20b85a51c9f9248f504228391aa69482a3a Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 12:31:25 -0700 Subject: [PATCH 151/187] feat: Right side wrist joint all clear --- nhf/touhou/houjuu_nue/wing.py | 49 ++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 9d057dd..ec9f4f1 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -33,13 +33,7 @@ WRIST_PARAMS = dict( radius_disk=13.0, radius_housing=15.0, ), - hole_pos=[10], - lip_length=30, - child_arm_radius=23.0, - parent_arm_radius=30.0, - actuator=LINEAR_ACTUATOR_10, - flexor_offset_angle=30.0, - flexor_child_arm_radius=None, + actuator=LINEAR_ACTUATOR_21, ) @dataclass(kw_only=True) @@ -775,7 +769,7 @@ class WingProfile(Model): segment_thickness=self.s2_thickness, child=True, ) - @submodel(name="spacer-s1-elbow-act") + @submodel(name="spacer-s2-elbow-act") def spacer_s2_elbow_act(self) -> MountingBox: return MountingBox( length=self.s2_thickness, @@ -876,9 +870,12 @@ class WingProfile(Model): return profile def surface_s3(self, front: bool = True) -> Cq.Workplane: - loc_wrist = Cq.Location.rot2d(self.wrist_rotate) * self.wrist_joint.child_arm_loc() + rot_wrist = Cq.Location.rot2d(self.wrist_rotate) + loc_wrist = rot_wrist * self.wrist_joint.child_arm_loc() tags = [ ("wrist", self.wrist_axle_loc * loc_wrist), + ("wrist_act", self.wrist_axle_loc * rot_wrist * + self.wrist_joint.actuator_mount_loc(child=True)), ("wrist_bot", self.wrist_axle_loc * loc_wrist * Cq.Location.from2d(0, self.wrist_h2)), ("wrist_top", self.wrist_axle_loc * loc_wrist * @@ -906,6 +903,17 @@ class WingProfile(Model): segment_thickness=self.s3_thickness, child=True, ) + @submodel(name="spacer-s3-wrist-act") + def spacer_s3_wrist_act(self) -> MountingBox: + return MountingBox( + length=self.s3_thickness, + width=self.s3_thickness, + thickness=self.spacer_thickness, + holes=[Hole(x=0,y=0)], + centred=(True, True), + hole_diam=self.wrist_joint.hole_diam, + centre_left_right_tags=True, + ) @assembly() def assembly_s3(self) -> Cq.Assembly: result = ( @@ -930,6 +938,12 @@ class WingProfile(Model): .constrain("back?wrist_bot", "extra_back?wrist_bot", "Plane") .constrain("back?wrist_top", "extra_back?wrist_top", "Plane") ) + self._assembly_insert_spacer( + result, + self.spacer_s3_wrist_act().generate(), + point_tag="wrist_act", + flipped=True, + ) self._assembly_insert_spacer( result, self.spacer_s3_wrist().generate(), @@ -1084,7 +1098,16 @@ class WingR(WingProfile): wrist_height: float = 60.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( flip=True, - angle_neutral=0.0, + angle_neutral=-20.0, + child_arm_radius=23.0, + parent_arm_radius=30.0, + flexor_offset_angle=30.0, + flexor_child_arm_radius=50.0, + flexor_mount_angle_parent=20, + flexor_mount_angle_child=-40, + hole_pos=[10, 20], + lip_length=50, + #flexor_pos_smaller=False, **WRIST_PARAMS )) @@ -1336,6 +1359,12 @@ class WingL(WingProfile): wrist_height: float = 43.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( flip=False, + hole_pos=[10], + lip_length=30, + child_arm_radius=23.0, + parent_arm_radius=30.0, + flexor_offset_angle=30.0, + flexor_child_arm_radius=None, **WRIST_PARAMS )) -- 2.44.1 From 33e32aa14b4144533e1b2f3145e9777e5ffbf987 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 12:35:36 -0700 Subject: [PATCH 152/187] fix: Left side wrist joint --- nhf/parts/box.py | 6 +++++- nhf/touhou/houjuu_nue/wing.py | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 9aac091..ebd5d63 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -1,6 +1,6 @@ import cadquery as Cq from dataclasses import dataclass, field -from typing import Tuple, Optional, Union +from typing import Tuple, Optional, Union, Callable from nhf.build import Model, TargetKind, target import nhf.utils @@ -64,6 +64,8 @@ class MountingBox(Model): # Determines the position of side tags flip_y: bool = False + profile_callback: Optional[Callable[[Cq.Sketch], Cq.Sketch]] = None + def __post_init__(self): for i, hole in enumerate(self.holes): if hole.tag is None: @@ -84,6 +86,8 @@ class MountingBox(Model): for hole in self.holes: diam = hole.diam if hole.diam else self.hole_diam result.push([(hole.x, hole.y)]).circle(diam / 2, mode='s') + if self.profile_callback: + profile = self.profile_callback(profile) return result def generate(self) -> Cq.Workplane: diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index ec9f4f1..acd0bd4 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -33,7 +33,6 @@ WRIST_PARAMS = dict( radius_disk=13.0, radius_housing=15.0, ), - actuator=LINEAR_ACTUATOR_21, ) @dataclass(kw_only=True) @@ -1107,6 +1106,7 @@ class WingR(WingProfile): flexor_mount_angle_child=-40, hole_pos=[10, 20], lip_length=50, + actuator=LINEAR_ACTUATOR_21, #flexor_pos_smaller=False, **WRIST_PARAMS )) @@ -1363,8 +1363,11 @@ class WingL(WingProfile): lip_length=30, child_arm_radius=23.0, parent_arm_radius=30.0, - flexor_offset_angle=30.0, + flexor_offset_angle=0.0, flexor_child_arm_radius=None, + flexor_line_length=50.0, + flexor_line_slack=1.0, + actuator=LINEAR_ACTUATOR_10, **WRIST_PARAMS )) -- 2.44.1 From f951f69d62fcb8d0d2fc3219d31f6f9f549f679e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 12:38:05 -0700 Subject: [PATCH 153/187] fix: left elbow joint spaces --- nhf/touhou/houjuu_nue/wing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index acd0bd4..b05ca9d 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -1334,7 +1334,7 @@ class WingL(WingProfile): disk_joint=DiskJoint( movement_angle=50, ), - angle_neutral=25.0, + angle_neutral=30.0, flexor_mount_angle_child=220, flexor_mount_angle_parent=0, flexor_line_length=50.0, -- 2.44.1 From fc154a981009e8685990c4cdefca803a267a697e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 12:42:35 -0700 Subject: [PATCH 154/187] fix: Left side joint alignment --- nhf/touhou/houjuu_nue/wing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index b05ca9d..1032772 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -1388,7 +1388,7 @@ class WingL(WingProfile): assert self.wrist_height <= self.shoulder_joint.height self.wrist_bot_loc = self.wrist_bot_loc.with_angle_2d(self.wrist_angle) - self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() + 50.0 + self.wrist_joint.angle_neutral = self.wrist_bot_loc.to2d_rot() * 0.7 + 30.0 self.wrist_rotate = -self.wrist_joint.angle_neutral self.shoulder_joint.flip = True -- 2.44.1 From 0094e19d3ab288147a4de1c0199df6ff31810813 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 12:57:33 -0700 Subject: [PATCH 155/187] feat: Carve channels on sides of connectors --- nhf/parts/box.py | 30 ++++++++++++++++-------------- nhf/touhou/houjuu_nue/wing.py | 12 ++++++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index ebd5d63..69a40f8 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -87,7 +87,7 @@ class MountingBox(Model): diam = hole.diam if hole.diam else self.hole_diam result.push([(hole.x, hole.y)]).circle(diam / 2, mode='s') if self.profile_callback: - profile = self.profile_callback(profile) + result = self.profile_callback(result) return result def generate(self) -> Cq.Workplane: @@ -108,20 +108,22 @@ class MountingBox(Model): reverse_plane.moveTo(hole.x, hole.y).tagPlane(hole.rev_tag, '-Z') if self.generate_side_tags: - if self.centre_left_right_tags: - result.faces("Z").val().Center()).tagPlane("left") - result.faces(">Y").workplane(origin=result.edges(">Y and >Z").val().Center()).tagPlane("right") - else: - result.faces("Z").val().Center()).tagPlane("left") - result.faces(">Y").workplane(origin=result.vertices("Y and >Z").val().Center()).tagPlane("right") + xn, xp = 0, self.length + if self.centred[0]: + xn -= self.length/2 + xp -= self.length/2 + yn, yp = 0, self.width + if self.centred[1]: + yn -= self.width/2 + yp -= self.width/2 - c_y = ">Y" if self.flip_y else "Z").val().Center()).tagPlane("bot") - result.faces(">X").workplane(origin=result.edges(f">X and >Z").val().Center()).tagPlane("top") - else: - result.faces("Z").val().Center()).tagPlane("bot") - result.faces(">X").workplane(origin=result.vertices(f">X and {c_y} and >Z").val().Center()).tagPlane("top") + tag_x = xn + (self.length/2 if self.centre_left_right_tags else 0) + result.copyWorkplane(Cq.Workplane('XZ', origin=(tag_x, yn, self.thickness))).tagPlane("left") + result.copyWorkplane(Cq.Workplane('ZX', origin=(tag_x, yp, self.thickness))).tagPlane("right") + + tag_y = yn + (self.width/2 if self.centre_bot_top_tags else 0) + result.copyWorkplane(Cq.Workplane('ZY', origin=(xn, tag_y, self.thickness))).tagPlane("bot") + result.copyWorkplane(Cq.Workplane('YZ', origin=(xp, tag_y, self.thickness))).tagPlane("top") result.faces(">Z").tag("dir") return result diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 1032772..0da6fd3 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -577,6 +577,17 @@ class WingProfile(Model): Hole(sign * x, tag=tag) for x, tag in joint.hole_loc_tags() ] + def carve_sides(profile): + dy = (segment_thickness + joint.total_thickness) / 4 + return ( + profile + .push([(0,-dy), (0,dy)]) + .rect( + joint.parent_arm_width, + (segment_thickness - joint.total_thickness) / 2, + mode='s', + ) + ) # FIXME: Carve out the sides so light can pass through mbox = MountingBox( length=joint.lip_length, @@ -586,6 +597,7 @@ class WingProfile(Model): hole_diam=joint.hole_diam, centred=(True, True), centre_left_right_tags=True, + profile_callback=carve_sides, ) return mbox -- 2.44.1 From 5db1f0ce79d4d4a18aad271155201749f129018f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 13:05:12 -0700 Subject: [PATCH 156/187] feat: Pre stress joints --- nhf/touhou/houjuu_nue/joints.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 9884a88..b5ebd81 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1051,7 +1051,7 @@ class DiskJoint(Model): angle: float = 0.0, ) -> Cq.Assembly: assert 0 <= angle <= self.movement_angle - deflection = angle - self.neutral_movement_angle + deflection = angle - (self.spring.angle_neutral - self.spring_angle_at_0) spring_name = disk.replace("/", "__Z") + "_spring" ( assembly diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 0da6fd3..931af51 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -27,12 +27,12 @@ ELBOW_PARAMS = dict( actuator=LINEAR_ACTUATOR_50, parent_arm_width=15, ) +WRIST_DISK_PARAMS = dict( + movement_angle=30, + radius_disk=13.0, + radius_housing=15.0, +) WRIST_PARAMS = dict( - disk_joint=DiskJoint( - movement_angle=30, - radius_disk=13.0, - radius_housing=15.0, - ), ) @dataclass(kw_only=True) @@ -1096,6 +1096,7 @@ class WingR(WingProfile): elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( movement_angle=55, + spring_angle_at_0=75, ), flexor_offset_angle=15, flexor_mount_angle_child=-75, @@ -1108,6 +1109,10 @@ class WingR(WingProfile): wrist_bot_loc: Cq.Location = Cq.Location.from2d(403.0, 289.0, 45.0) wrist_height: float = 60.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + disk_joint=DiskJoint( + spring_angle_at_0=120, + **WRIST_DISK_PARAMS, + ), flip=True, angle_neutral=-20.0, child_arm_radius=23.0, @@ -1344,6 +1349,7 @@ class WingL(WingProfile): elbow_rotate: float = 15.0 elbow_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( + spring_angle_at_0=100, movement_angle=50, ), angle_neutral=30.0, @@ -1370,6 +1376,9 @@ class WingL(WingProfile): wrist_bot_loc: Cq.Location = Cq.Location.from2d(460.0, -10.0, -45.0) wrist_height: float = 43.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( + disk_joint=DiskJoint( + **WRIST_DISK_PARAMS, + ), flip=False, hole_pos=[10], lip_length=30, -- 2.44.1 From b7ca30bc28021c5eeb9c03777d61564fedb67c29 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 13:19:56 -0700 Subject: [PATCH 157/187] fix: joint flipping problem on the left --- nhf/touhou/houjuu_nue/__init__.py | 6 +++--- nhf/touhou/houjuu_nue/wing.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index 9d3c155..a3a925c 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -156,9 +156,9 @@ class Parameters(Model): .add(self.wing_r1.assembly(parts, root_offset=9, **kwargs), name="wing_r1") .add(self.wing_r2.assembly(parts, root_offset=7, **kwargs), name="wing_r2") .add(self.wing_r3.assembly(parts, root_offset=6, **kwargs), name="wing_r3") - .add(self.wing_l1.assembly(parts, root_offset=7, **kwargs), name="wing_l1") - .add(self.wing_l2.assembly(parts, root_offset=8, **kwargs), name="wing_l2") - .add(self.wing_l3.assembly(parts, root_offset=9, **kwargs), name="wing_l3") + .add(self.wing_l1.assembly(parts, root_offset=19, **kwargs), name="wing_l1") + .add(self.wing_l2.assembly(parts, root_offset=20, **kwargs), name="wing_l2") + .add(self.wing_l3.assembly(parts, root_offset=21, **kwargs), name="wing_l3") ) for tag in ["r1", "r2", "r3", "l1", "l2", "l3"]: self.harness.add_root_joint_constraint( diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 931af51..080b097 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -325,7 +325,7 @@ class WingProfile(Model): width=self.electronic_board.width, centred=(True, True), thickness=self.spacer_thickness, - flip_y=self.flip, + flip_y=False,#self.flip, generate_reverse_tags=True, ) @submodel(name="spacer-s0-shoulder-act") @@ -404,7 +404,7 @@ class WingProfile(Model): (self.spacer_s0_electronic_mount().generate(), "electronic_mount"), ]: top_tag, bot_tag = "top", "bot" - if self.flip: + if self.flip and tag.startswith("shoulder"): top_tag, bot_tag = bot_tag, top_tag ( result -- 2.44.1 From 57deefbd5fe9f145fe39351e955304e3e8ab19a3 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 13:27:25 -0700 Subject: [PATCH 158/187] fix: Elbow joint output problem --- nhf/touhou/houjuu_nue/joints.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index b5ebd81..4e2834a 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1252,8 +1252,7 @@ class ElbowJoint(Model): ) return mbox.generate() - @target(name="child") - def child_joint(self, generate_mount: bool=False) -> Cq.Assembly: + def child_joint(self, generate_mount: bool=False, generate_tags=True) -> Cq.Assembly: angle = -self.disk_joint.tongue_span / 2 dz = self.disk_joint.disk_thickness / 2 # We need to ensure the disk is on the "other" side so @@ -1290,7 +1289,7 @@ class ElbowJoint(Model): ) # Orientes the hole surface so it faces +X loc_thickness = Cq.Location((-self.lip_thickness, 0, 0), (0, 1, 0), 90) - if self.flexor: + if self.flexor and generate_tags: loc_mount = self.actuator_mount_loc(child=True, unflip=True) result.add( Cq.Edge.makeLine((-1,0,0), (1,0,0)), @@ -1306,12 +1305,15 @@ class ElbowJoint(Model): ) return result + @target(name="child") + def target_child(self) -> Cq.Assembly: + return self.child_joint(generate_tags=False) + @target(name="parent-lower") def parent_joint_lower(self) -> Cq.Workplane: return self.disk_joint.housing_lower() - @target(name="parent-upper") - def parent_joint_upper(self, generate_mount: bool=False): + def parent_joint_upper(self, generate_mount: bool=False, generate_tags=True): axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) housing_dz = self.disk_joint.housing_upper_dz conn_h = self.disk_joint.total_thickness @@ -1355,7 +1357,7 @@ class ElbowJoint(Model): #.constrain("connector", "Fixed") #.solve() ) - if self.flexor: + if self.flexor and generate_tags: loc_mount = self.actuator_mount_loc(child=False, unflip=True) result.add( Cq.Edge.makeLine((-1,0,0), (1,0,0)), @@ -1371,6 +1373,10 @@ class ElbowJoint(Model): ) return result + @target(name="parent-upper") + def target_parent_upper(self) -> Cq.Assembly: + return self.parent_joint_upper(generate_tags=False) + @assembly() def assembly(self, angle: float = 0, -- 2.44.1 From 7fc0499ebe673c8c7d3892060d95917d8b6ba4fe Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 25 Jul 2024 22:40:44 -0700 Subject: [PATCH 159/187] feat: Add support structure to wing --- nhf/touhou/houjuu_nue/electronics.py | 2 ++ nhf/touhou/houjuu_nue/wing.py | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index a17c819..300c6e0 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -484,8 +484,10 @@ class ElectronicBoard(Model): width: float = 170.0 mount_holes: list[Hole] = field(default_factory=lambda: [ Hole(x=30, y=80), + Hole(x=30, y=0), Hole(x=30, y=-80), Hole(x=-30, y=80), + Hole(x=-30, y=0), Hole(x=-30, y=-80), ]) panel_thickness: float = 25.4 / 16 diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 080b097..8aa4d0e 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -47,6 +47,7 @@ class WingProfile(Model): # 1/4" acrylic for the spacer. Anything thinner would threathen structural # strength spacer_thickness: float = 25.4 / 4 + rod_width: float = 10.0 shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( )) @@ -290,7 +291,7 @@ class WingProfile(Model): flip_y=self.flip, centre_bot_top_tags=True, ) - @submodel(name="spacer-s0-shoulder") + @submodel(name="spacer-s0-base") def spacer_s0_base(self) -> MountingBox: """ Base side connects to H-S joint @@ -635,6 +636,13 @@ class WingProfile(Model): profile = self.profile_s1() return extrude_with_markers( profile, self.panel_thickness, tags, reverse=front) + @submodel(name="spacer-s1-rod") + def spacer_s1_rod(self) -> MountingBox: + return MountingBox( + length=self.s1_thickness, + width=self.rod_width, + thickness=self.panel_thickness, + ) @submodel(name="spacer-s1-shoulder") def spacer_s1_shoulder(self) -> MountingBox: sign = 1#-1 if self.flip else 1 @@ -773,6 +781,13 @@ class WingProfile(Model): ] return extrude_with_markers( profile, self.panel_thickness, tags, reverse=not front) + @submodel(name="spacer-s2-rod") + def spacer_s2_rod(self) -> MountingBox: + return MountingBox( + length=self.s2_thickness, + width=self.rod_width, + thickness=self.panel_thickness, + ) @submodel(name="spacer-s2-elbow") def spacer_s2_elbow(self) -> MountingBox: return self._spacer_from_disk_joint( @@ -907,6 +922,13 @@ class WingProfile(Model): Cq.Location.from2d(0, -self.wrist_h2)), ] return extrude_with_markers(profile, self.panel_thickness, tags, reverse=not front) + @submodel(name="spacer-s3-rod") + def spacer_s3_rod(self) -> MountingBox: + return MountingBox( + length=self.s3_thickness, + width=self.rod_width, + thickness=self.panel_thickness, + ) @submodel(name="spacer-s3-wrist") def spacer_s3_wrist(self) -> MountingBox: return self._spacer_from_disk_joint( -- 2.44.1 From aa1f4efa98cae09de8570c9403222a268dbd542c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 26 Jul 2024 14:07:33 -0700 Subject: [PATCH 160/187] fix: Brittle shoulder joint problem --- nhf/touhou/houjuu_nue/joints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 4e2834a..5700aba 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -650,7 +650,7 @@ class ShoulderJoint(Model): .copyWorkplane(Cq.Workplane('XY')) .box( length=self.child_lip_ext, - width=self.child_lip_width, + width=self.child_lip_width - 2 * self.child_lip_thickness, height=self.height - self.torsion_joint.total_height * 2, combine='cut', centered=(False, True, True), -- 2.44.1 From 1f99b5e41f8e1a025295cb338ee7b072073ce2da Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 27 Jul 2024 19:28:42 -0700 Subject: [PATCH 161/187] fix: Spring slot in disk --- nhf/touhou/houjuu_nue/joints.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 5700aba..098a1cd 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -905,14 +905,20 @@ class DiskJoint(Model): return self.total_thickness / 2 - self.housing_thickness def _disk_cut(self) -> Cq.Workplane: - return ( + rl = Cq.Location((0, 0, 0), (0, 0, 1), self.spring_slot_offset) + r = ( Cq.Solid.makeBox( length=self.spring.tail_length, width=self.spring.thickness, - height=self.spring.height-self.disk_bot_thickness, + height=self.disk_thickness - self.disk_bot_thickness, ) - .located(Cq.Location((0, self.spring.radius_inner, self.disk_bot_thickness))) - .rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset) + .moved(rl * Cq.Location((0, self.spring.radius_inner, self.disk_bot_thickness))) + #.rotate((0, 0, 0), (0, 0, 1), self.spring_slot_offset) + ) + return ( + Cq.Workplane() + .union(r) + .val() ) @target(name="disk") @@ -1267,8 +1273,10 @@ class ElbowJoint(Model): loc_rot_neutral = Cq.Location.rot2d(self.angle_neutral) loc_disk = flip_x * flip_z * Cq.Location((-self.child_arm_radius, 0, 0)) loc_cut_rel = Cq.Location((0, self.disk_joint.spring.radius_inner, -self.disk_joint.disk_bot_thickness)) - disk_cut = self.disk_joint._disk_cut().located( - loc_lip.inverse * loc_cut_rel * loc_disk) + loc_disk_orient = Cq.Location((0, 0, -dz), (0,0,1), angle) + disk_cut = self.disk_joint._disk_cut().moved( + #Cq.Location(0,0,0)) + loc_lip.inverse * loc_disk * loc_disk_orient) #lip_extra = Cq.Solid.makeBox( # length=self.child_lip_extra_length, # width=self.total_thickness, @@ -1281,7 +1289,7 @@ class ElbowJoint(Model): result = ( Cq.Assembly() .add(self.disk_joint.disk(), name="disk", - loc=loc_rot_neutral * Cq.Location((0, 0, -dz), (0,0,1), angle)) + loc=loc_rot_neutral * loc_disk_orient) .add(self.lip().cut(disk_cut), name="lip", loc=loc_rot_neutral * loc_disk.inverse * loc_lip) #.add(lip_extra, name="lip_extra", -- 2.44.1 From f33224c2163eb5952503fd7465280f6617f4407a Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 29 Jul 2024 01:06:27 -0700 Subject: [PATCH 162/187] feat: Use 10mm actuator on both sides --- nhf/touhou/houjuu_nue/electronics.py | 21 +++++++++++---------- nhf/touhou/houjuu_nue/test.py | 6 ++++++ nhf/touhou/houjuu_nue/wing.py | 16 ++++++++++------ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 300c6e0..f8ea616 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -302,19 +302,20 @@ LINEAR_ACTUATOR_21 = LinearActuator( stroke_length=21, front_hole_ext=4, back_hole_ext=4, - segment1_length=75/2, - segment2_length=75/2, + segment1_length=34, + segment2_length=34, ) LINEAR_ACTUATOR_10 = LinearActuator( - # FIXME: Measure - mass=0.0, + mass=41.3, stroke_length=10, - front_hole_ext=4.5/2, - back_hole_ext=4.5/2, - segment1_length=30.0, - segment2_length=30.0, - segment1_width=15.0, - segment2_width=21.0, + front_hole_ext=4.02, + back_hole_ext=4.67, + segment1_length=13.29, + segment1_width=15.88, + segment1_height=12.07, + segment2_length=42.52, + segment2_width=20.98, + segment2_height=14.84, ) LINEAR_ACTUATOR_HEX_NUT = HexNut( mass=0.8, diff --git a/nhf/touhou/houjuu_nue/test.py b/nhf/touhou/houjuu_nue/test.py index 465891c..6e8fd7e 100644 --- a/nhf/touhou/houjuu_nue/test.py +++ b/nhf/touhou/houjuu_nue/test.py @@ -14,6 +14,12 @@ class TestElectronics(unittest.TestCase): self.assertAlmostEqual( ME.LINEAR_ACTUATOR_30.conn_length, 83.9 ) + self.assertAlmostEqual( + ME.LINEAR_ACTUATOR_10.conn_length, 64.5 + ) + self.assertAlmostEqual( + ME.LINEAR_ACTUATOR_21.conn_length, 76.0 + ) def test_flexor(self): flexor = ME.Flexor( diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 8aa4d0e..49b3283 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -44,6 +44,8 @@ class WingProfile(Model): root_joint: RootJoint = field(default_factory=lambda: RootJoint()) panel_thickness: float = 25.4 / 16 + # s0 is armoured + panel_thickness_s0: float = 25.4 / 8 # 1/4" acrylic for the spacer. Anything thinner would threathen structural # strength spacer_thickness: float = 25.4 / 4 @@ -242,7 +244,7 @@ class WingProfile(Model): return sketch def outer_shell_s0(self) -> Cq.Workplane: - t = self.panel_thickness + t = self.panel_thickness_s0 profile = Cq.Wire.assembleEdges(self.outer_profile_s0().edges().vals()) result = ( Cq.Workplane('XZ') @@ -254,7 +256,7 @@ class WingProfile(Model): plane.moveTo(0, self.root_height + t*2).tagPlane("top") return result def inner_shell_s0(self) -> Cq.Workplane: - t = self.panel_thickness + t = self.panel_thickness_s0 #profile = Cq.Wire.assembleEdges(self.inner_profile_s0()) result = ( Cq.Workplane('XZ') @@ -364,7 +366,7 @@ class WingProfile(Model): ] result = extrude_with_markers( self.profile_s0(top=top), - self.panel_thickness, + self.panel_thickness_s0, tags, reverse=not top, ) @@ -1139,13 +1141,15 @@ class WingR(WingProfile): angle_neutral=-20.0, child_arm_radius=23.0, parent_arm_radius=30.0, - flexor_offset_angle=30.0, - flexor_child_arm_radius=50.0, + flexor_line_length=50.0, + flexor_line_slack=3.0, + flexor_offset_angle=45.0, + flexor_child_arm_radius=None, flexor_mount_angle_parent=20, flexor_mount_angle_child=-40, hole_pos=[10, 20], lip_length=50, - actuator=LINEAR_ACTUATOR_21, + actuator=LINEAR_ACTUATOR_10, #flexor_pos_smaller=False, **WRIST_PARAMS )) -- 2.44.1 From c846c049321cce64e1c91488eb39460bb27fcd2f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 29 Jul 2024 10:49:03 -0700 Subject: [PATCH 163/187] feat: Use a low profile cut to route cables --- nhf/touhou/houjuu_nue/wing.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 49b3283..eb1aeaf 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -62,8 +62,8 @@ class WingProfile(Model): shoulder_base_bezier_x: float = -30.0 shoulder_base_bezier_y: float = 30.0 - s0_hole_loc: Cq.Location = Cq.Location.from2d(-25, 33) - s0_hole_diam: float = 15.0 + s0_hole_width: float = 40.0 + s0_hole_height: float = 10.0 s0_top_hole: bool = False s0_bot_hole: bool = True @@ -235,11 +235,17 @@ class WingProfile(Model): ) top = top == self.flip if (self.s0_top_hole and top) or (self.s0_bot_hole and not top): + assert self.base_width > self.s0_hole_width + x = (self.base_width - self.s0_hole_width) / 2 sketch = ( sketch .reset() - .push([self.s0_hole_loc.to2d_pos()]) - .circle(self.s0_hole_diam / 2, mode='s') + .polygon([ + (-x, 0), + (-x, self.s0_hole_height), + (-self.base_width + x, self.s0_hole_height), + (-self.base_width + x, 0), + ], mode='s') ) return sketch -- 2.44.1 From f1f10369b4a7c6edfbba36d69e2c8656316b26c7 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 31 Jul 2024 12:42:55 -0700 Subject: [PATCH 164/187] fix: Spool retainment issue in shoulder --- nhf/touhou/houjuu_nue/joints.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 098a1cd..5461668 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -390,11 +390,12 @@ class ShoulderJoint(Model): angle_neutral: float = -15.0 angle_max_deflection: float = 65.0 - spool_radius: float = 14.0 + spool_radius: float = 12.0 spool_groove_depth: float = 1.0 spool_base_height: float = 3.0 spool_height: float = 5.0 - spool_groove_inset: float = 2.0 + spool_cap_height: float = 1.0 + spool_groove_inset: float = 3.0 flip: bool = False actuator: LinearActuator = LINEAR_ACTUATOR_21 @@ -554,17 +555,19 @@ class ShoulderJoint(Model): Generates the spool piece which holds the line in tension """ t = self.spool_groove_depth - bulk = Cq.Solid.makeCylinder( - radius=self.spool_radius, + radius_core_inner = self.torsion_joint.radius_rider - self.child_core_thickness + spindle = Cq.Solid.makeCone( + radius1=self.spool_radius, + radius2=radius_core_inner, height=self.spool_height, - ).located(Cq.Location((0, 0, self.spool_base_height))) - base = Cq.Solid.makeCylinder( - radius=self.spool_radius - t, - height=self.spool_base_height, ) - hole_x = self.spool_radius - (t + self.spool_groove_inset) + cap = Cq.Solid.makeCylinder( + radius=radius_core_inner, + height=self.spool_cap_height + ).located(Cq.Location((0,0,self.spool_height))) + hole_x = radius_core_inner - self.spool_groove_inset slot = Cq.Solid.makeBox( - length=t + self.spool_groove_inset, + length=t, width=t, height=self.spool_base_height, ).located(Cq.Location((hole_x, -t/2, 0))) @@ -577,7 +580,7 @@ class ShoulderJoint(Model): radius=self.torsion_joint.radius_axle, height=self.spool_height + self.spool_base_height, ) - return bulk.fuse(base).cut(slot, hole, centre_hole) + return spindle.fuse(cap).cut(slot, hole, centre_hole) @target(name="child") def child(self) -> Cq.Assembly: @@ -618,6 +621,11 @@ class ShoulderJoint(Model): width=self.child_lip_width - self.child_lip_thickness * 2, height=self.child_lip_height, ).located(Cq.Location((0, self.child_lip_thickness, 0)))) + .cut(Cq.Solid.makeBox( + length=self.child_lip_ext - self.child_guard_ext - self.child_lip_thickness, + width=self.child_lip_width, + height=self.child_lip_height - self.child_lip_thickness * 2, + ).located(Cq.Location((0, 0, self.child_lip_thickness)))) .located(Cq.Location(( self.child_guard_ext, -self.child_lip_width / 2, -- 2.44.1 From 7307ae213dba5580d09c31f7b11fd57beba139f3 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 1 Aug 2024 00:26:25 -0700 Subject: [PATCH 165/187] feat: Move shoulder actuator position --- nhf/touhou/houjuu_nue/wing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index eb1aeaf..f8cfa9d 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -366,9 +366,9 @@ class WingProfile(Model): ("shoulder_right", self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), ("shoulder_act", - self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(150, -40)), + self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(100, -20)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), - ("electronic_mount", Cq.Location.from2d(-45, 75, 64)), + ("electronic_mount", Cq.Location.from2d(-35, 65, 60)), ] result = extrude_with_markers( self.profile_s0(top=top), -- 2.44.1 From 2ccb4160dbadef97ec0cc5fe9d151fc3bc7ddc5c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 1 Aug 2024 22:38:45 -0700 Subject: [PATCH 166/187] feat: Use double layer for electronic mount --- nhf/parts/box.py | 12 ++++++++-- nhf/parts/fasteners.py | 14 +++++++++-- nhf/touhou/houjuu_nue/electronics.py | 9 +++++++ nhf/touhou/houjuu_nue/wing.py | 36 +++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 69a40f8..5fbf21e 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -33,12 +33,20 @@ class Hole: y: float = 0.0 diam: Optional[float] = None tag: Optional[str] = None + face: Optional[Cq.Face] = None @property def rev_tag(self) -> str: assert self.tag is not None return self.tag + "_rev" + def cutting_geometry(self, default_diam: Optional[float]=None) -> Cq.Face: + if self.face is not None: + return self.face + diam = self.diam if self.diam is not None else default_diam + assert diam is not None + return Cq.Face.makeFromWires(Cq.Wire.makeCircle(diam/2, Cq.Vector(), Cq.Vector(0,0,1))) + @dataclass class MountingBox(Model): """ @@ -84,8 +92,8 @@ class MountingBox(Model): .rect(self.length, self.width) ) for hole in self.holes: - diam = hole.diam if hole.diam else self.hole_diam - result.push([(hole.x, hole.y)]).circle(diam / 2, mode='s') + face = hole.cutting_geometry(default_diam=self.hole_diam) + result.push([(hole.x, hole.y)]).each(lambda l:face.moved(l), mode='s') if self.profile_callback: result = self.profile_callback(result) return result diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index 9dfd20f..02bd598 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -103,12 +103,15 @@ class HexNut(Item): def role(self) -> Role: return Role.CONNECTION + @property + def radius(self) -> float: + return self.width / math.sqrt(3) + def generate(self) -> Cq.Workplane: - r = self.width / math.sqrt(3) result = ( Cq.Workplane("XY") .sketch() - .regularPolygon(r=r, n=6) + .regularPolygon(r=self.radius, n=6) .circle(r=self.diam_thread/2, mode='s') .finalize() .extrude(self.thickness) @@ -117,3 +120,10 @@ class HexNut(Item): result.faces(">Z").tag("top") result.copyWorkplane(Cq.Workplane('XY')).tagPlane("dirX", direction="+X") return result + + def cutting_face(self) -> Cq.Face: + return ( + Cq.Sketch() + .regularPolygon(r=self.radius, n=6) + ._faces + ) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index f8ea616..d29fd17 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -335,6 +335,15 @@ LINEAR_ACTUATOR_BRACKET = MountingBracket() BATTERY_BOX = BatteryBox18650() +# Acrylic hex nut +ELECTRONIC_MOUNT_HEXNUT = HexNut( + mass=0.8, + diam_thread=4, + pitch=0.7, + thickness=3.57, + width=6.81, +) + @dataclass(kw_only=True) class Flexor: """ diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index f8cfa9d..03867f4 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -18,7 +18,8 @@ from nhf.touhou.houjuu_nue.electronics import ( LINEAR_ACTUATOR_10, LINEAR_ACTUATOR_21, LINEAR_ACTUATOR_50, - ElectronicBoard + ElectronicBoard, + ELECTRONIC_MOUNT_HEXNUT, ) import nhf.utils @@ -327,6 +328,9 @@ class WingProfile(Model): ) @submodel(name="spacer-s0-electronic") def spacer_s0_electronic_mount(self) -> MountingBox: + """ + This one has circular holes for the screws + """ return MountingBox( holes=self.electronic_board.mount_holes, hole_diam=self.electronic_board.mount_hole_diam, @@ -337,6 +341,26 @@ class WingProfile(Model): flip_y=False,#self.flip, generate_reverse_tags=True, ) + @submodel(name="spacer-s0-electronic2") + def spacer_s0_electronic_mount2(self) -> MountingBox: + """ + This one has hexagonal holes + """ + face = ELECTRONIC_MOUNT_HEXNUT.cutting_face() + holes = [ + Hole(x=h.x, y=h.y, face=face, tag=h.tag) + for h in self.electronic_board.mount_holes + ] + return MountingBox( + holes=holes, + hole_diam=self.electronic_board.mount_hole_diam, + length=self.root_height, + width=self.electronic_board.width, + centred=(True, True), + thickness=self.spacer_thickness, + flip_y=False,#self.flip, + generate_reverse_tags=True, + ) @submodel(name="spacer-s0-shoulder-act") def spacer_s0_shoulder_act(self) -> MountingBox: dx = self.shoulder_joint.draft_height @@ -424,6 +448,16 @@ class WingProfile(Model): .constrain(f"{tag}?{top_tag}", f"top?{tag}", "Plane") .constrain(f"{tag}?dir", f"top?{tag}_dir", "Axis") ) + result.addS( + self.spacer_s0_electronic_mount2().generate(), + name="electronic_mount2", + role=Role.STRUCTURE | Role.CONNECTION, + material=self.mat_bracket) + for hole in self.electronic_board.mount_holes: + result.constrain( + f"electronic_mount?{hole.tag}", + f"electronic_mount2?{hole.tag}_rev", + "Plane") if not ignore_electronics: result.add(self.electronic_board.assembly(), name="electronic_board") for hole in self.electronic_board.mount_holes: -- 2.44.1 From 0ad5b17f07d5de3268aa8a87d171a4f2e939bb7c Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 2 Aug 2024 00:24:25 -0700 Subject: [PATCH 167/187] feat: Use double layer electronic mount --- nhf/touhou/houjuu_nue/electronics.py | 10 ++++------ nhf/touhou/houjuu_nue/wing.py | 21 +++++---------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index d29fd17..da980db 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -493,12 +493,10 @@ class ElectronicBoard(Model): length: float = 70.0 width: float = 170.0 mount_holes: list[Hole] = field(default_factory=lambda: [ - Hole(x=30, y=80), - Hole(x=30, y=0), - Hole(x=30, y=-80), - Hole(x=-30, y=80), - Hole(x=-30, y=0), - Hole(x=-30, y=-80), + Hole(x=25, y=70), + Hole(x=25, y=-70), + Hole(x=-25, y=70), + Hole(x=-25, y=-70), ]) panel_thickness: float = 25.4 / 16 mount_panel_thickness: float = 25.4 / 4 diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 03867f4..e0fbb20 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -462,22 +462,11 @@ class WingProfile(Model): result.add(self.electronic_board.assembly(), name="electronic_board") for hole in self.electronic_board.mount_holes: assert hole.tag - nut_name = f"electronic_board_{hole.tag}_nut" - ( - result - .addS( - self.electronic_board.nut.assembly(), - name=nut_name) - .constrain( - f"electronic_mount?{hole.rev_tag}", - f'{nut_name}?top', - "Plane" - ) - .constrain( - f"electronic_mount?{hole.tag}", - f'electronic_board/{hole.tag}_spacer?bot', - "Plane" - ) + result.constrain( + f"electronic_mount2?{hole.tag}", + f'electronic_board/{hole.tag}_spacer?top', + "Plane", + param=0 ) return result.solve() -- 2.44.1 From 40ebf93dc5a77ec9f351582a53834ce2c2df3d36 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 2 Aug 2024 00:28:12 -0700 Subject: [PATCH 168/187] feat: Simplify geometry in electronic mount --- nhf/touhou/houjuu_nue/electronics.py | 10 +++++----- nhf/touhou/houjuu_nue/wing.py | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index da980db..8d8120d 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -493,10 +493,10 @@ class ElectronicBoard(Model): length: float = 70.0 width: float = 170.0 mount_holes: list[Hole] = field(default_factory=lambda: [ - Hole(x=25, y=70), - Hole(x=25, y=-70), - Hole(x=-25, y=70), - Hole(x=-25, y=-70), + Hole(x=25, y=75), + Hole(x=25, y=-75), + Hole(x=-25, y=75), + Hole(x=-25, y=-75), ]) panel_thickness: float = 25.4 / 16 mount_panel_thickness: float = 25.4 / 4 @@ -526,7 +526,7 @@ class ElectronicBoard(Model): result = ( Cq.Assembly() .addS(panel.generate(), name="panel", - role=Role.STRUCTURE, material=self.material) + role=Role.ELECTRONIC | Role.STRUCTURE, material=self.material) ) for hole in panel.holes: spacer_name = f"{hole.tag}_spacer" diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index e0fbb20..3a3ff62 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -351,6 +351,12 @@ class WingProfile(Model): Hole(x=h.x, y=h.y, face=face, tag=h.tag) for h in self.electronic_board.mount_holes ] + def post(sketch: Cq.Sketch) -> Cq.Sketch: + return ( + sketch + .push([(0,0)]) + .rect(70, 130, mode='s') + ) return MountingBox( holes=holes, hole_diam=self.electronic_board.mount_hole_diam, @@ -360,6 +366,7 @@ class WingProfile(Model): thickness=self.spacer_thickness, flip_y=False,#self.flip, generate_reverse_tags=True, + profile_callback=post, ) @submodel(name="spacer-s0-shoulder-act") def spacer_s0_shoulder_act(self) -> MountingBox: -- 2.44.1 From c2161b6171410819e352f9874ed95a622eae9f21 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Fri, 2 Aug 2024 01:43:37 -0700 Subject: [PATCH 169/187] feat: Move hole position to avoid collision --- nhf/touhou/houjuu_nue/joints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 5461668..77be185 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -353,8 +353,8 @@ class ShoulderJoint(Model): parent_conn_hole_diam: float = 4.0 # Position of the holes relative centre line parent_conn_hole_pos: list[Tuple[float, float]] = field(default_factory=lambda: [ - (20, 8), - (20, -8), + (15, 8), + (15, -8), ]) # Distance from centre of lips to the axis -- 2.44.1 From 5eeeace852877e99b75e70082ad66e8bcffb6ee4 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 3 Aug 2024 23:31:36 -0700 Subject: [PATCH 170/187] fix: Spool obstruction problem, elbow fasteners --- nhf/parts/fasteners.py | 33 +++++ nhf/touhou/houjuu_nue/electronics.py | 6 + nhf/touhou/houjuu_nue/joints.py | 179 ++++++++++++++++++++------- nhf/touhou/houjuu_nue/wing.py | 19 +-- 4 files changed, 186 insertions(+), 51 deletions(-) diff --git a/nhf/parts/fasteners.py b/nhf/parts/fasteners.py index 02bd598..ab79802 100644 --- a/nhf/parts/fasteners.py +++ b/nhf/parts/fasteners.py @@ -1,6 +1,7 @@ from dataclasses import dataclass import math import cadquery as Cq +from typing import Optional from nhf import Item, Role import nhf.utils @@ -127,3 +128,35 @@ class HexNut(Item): .regularPolygon(r=self.radius, n=6) ._faces ) + +@dataclass(frozen=True) +class Washer(Item): + diam_thread: float + diam_outer: float + thickness: float + material_name: Optional[float] = None + + def __post_init__(self): + assert self.diam_outer > self.diam_thread + @property + def name(self): + suffix = (" " + self.material_name) if self.material_name else "" + return f"Washer M{int(self.diam_thread)}{suffix}" + + @property + def role(self) -> Role: + return Role.CONNECTION + + def generate(self) -> Cq.Workplane: + result = ( + Cq.Workplane('XY') + .cylinder( + radius=self.diam_outer/2, + height=self.thickness, + ) + .faces(">Z") + .hole(self.diam_thread) + ) + result.faces("Z").tag("top") + return result diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 8d8120d..79a351d 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -547,3 +547,9 @@ class ElectronicBoard(Model): ) ) return result.solve() + +@dataclass(frozen=True) +class LightStrip: + + width: float = 10.0 + height: float = 4.5 diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 77be185..5ceac76 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -6,7 +6,7 @@ from nhf import Material, Role from nhf.build import Model, target, assembly from nhf.parts.box import MountingBox from nhf.parts.springs import TorsionSpring -from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob +from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob, Washer from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes from nhf.touhou.houjuu_nue.electronics import ( @@ -67,6 +67,28 @@ ELBOW_TORSION_SPRING = TorsionSpring( right_handed=False, ) +ELBOW_AXLE_BOLT = FlatHeadBolt( + mass=0.0, + diam_head=6.87, + height_head=3.06, + diam_thread=4.0, + height_thread=20.0, +) +ELBOW_AXLE_WASHER = Washer( + mass=0.0, + diam_outer=8.96, + diam_thread=4.0, + thickness=1.02, + material_name="Nylon" +) +ELBOW_AXLE_HEX_NUT = HexNut( + mass=0.0, + diam_thread=4.0, + pitch=0.7, + thickness=3.6, # or 2.64 for metal + width=6.89, +) + @dataclass class RootJoint(Model): """ @@ -390,12 +412,13 @@ class ShoulderJoint(Model): angle_neutral: float = -15.0 angle_max_deflection: float = 65.0 - spool_radius: float = 12.0 - spool_groove_depth: float = 1.0 - spool_base_height: float = 3.0 - spool_height: float = 5.0 - spool_cap_height: float = 1.0 - spool_groove_inset: float = 3.0 + spool_radius_diff: float = 2.0 + # All the heights here are mirrored for the bottom as well + spool_cap_height: float = 3.0 + spool_core_height: float = 2.0 + + spool_line_thickness: float = 1.2 + spool_groove_radius: float = 10.0 flip: bool = False actuator: LinearActuator = LINEAR_ACTUATOR_21 @@ -403,11 +426,17 @@ class ShoulderJoint(Model): def __post_init__(self): assert self.parent_lip_length * 2 < self.height assert self.child_guard_ext > self.torsion_joint.radius_rider - assert self.spool_groove_depth < self.spool_radius < self.torsion_joint.radius_rider - self.child_core_thickness - assert self.spool_base_height > self.spool_groove_depth + assert self.spool_groove_radius < self.spool_inner_radius < self.spool_outer_radius assert self.child_lip_height < self.height assert self.draft_length <= self.actuator.stroke_length + @property + def spool_outer_radius(self): + return self.torsion_joint.radius_rider - self.child_core_thickness + @property + def spool_inner_radius(self): + return self.spool_outer_radius - self.spool_radius_diff + @property def radius(self): return self.torsion_joint.radius @@ -417,7 +446,7 @@ class ShoulderJoint(Model): """ Amount of wires that need to draft on the spool """ - return (self.spool_radius - self.spool_groove_depth / 2) * math.radians(self.angle_max_deflection) + return self.spool_inner_radius * math.radians(self.angle_max_deflection) @property def draft_height(self): @@ -444,16 +473,20 @@ class ShoulderJoint(Model): def _max_contraction_angle(self) -> float: return 180 - self.angle_max_deflection + self.angle_neutral - def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid: - """ - Generates a cylindrical sector which cuts away overlapping regions of the child and parent - """ + def _contraction_cut_angle(self) -> float: aspect = self.child_guard_width / self.parent_arm_width theta = math.radians(self._max_contraction_angle) #theta_p = math.atan(math.sin(theta) / (math.cos(theta) + aspect)) theta_p = math.atan2(math.sin(theta), math.cos(theta) + aspect) angle = math.degrees(theta_p) assert 0 <= angle <= 90 + return angle + + def _contraction_cut_geometry(self, parent: bool = False, mirror: bool=False) -> Cq.Solid: + """ + Generates a cylindrical sector which cuts away overlapping regions of the child and parent + """ + angle = self._contraction_cut_angle() # outer radius of the cut, overestimated cut_radius = math.sqrt(self.child_guard_width ** 2 + self.parent_arm_width ** 2) span = 180 @@ -550,37 +583,39 @@ class ShoulderJoint(Model): joint = self.torsion_joint return self.height - 2 * joint.total_height + 2 * joint.rider_disk_height - def _spool(self) -> Cq.Workplane: + def _spool(self) -> Cq.Compound: """ Generates the spool piece which holds the line in tension """ - t = self.spool_groove_depth - radius_core_inner = self.torsion_joint.radius_rider - self.child_core_thickness + t = self.spool_line_thickness spindle = Cq.Solid.makeCone( - radius1=self.spool_radius, - radius2=radius_core_inner, - height=self.spool_height, + radius1=self.spool_inner_radius, + radius2=self.spool_outer_radius, + height=self.spool_core_height, ) cap = Cq.Solid.makeCylinder( - radius=radius_core_inner, + radius=self.spool_outer_radius, height=self.spool_cap_height - ).located(Cq.Location((0,0,self.spool_height))) - hole_x = radius_core_inner - self.spool_groove_inset - slot = Cq.Solid.makeBox( - length=t, + ).moved(Cq.Location((0,0,self.spool_core_height))) + cut_height = self.spool_cap_height + self.spool_core_height + cut_hole = Cq.Solid.makeCylinder( + radius=t / 2, + height=cut_height, + ).moved(Cq.Location((self.spool_groove_radius, 0, 0))) + cut_slot = Cq.Solid.makeBox( + length=self.spool_outer_radius - self.spool_groove_radius, width=t, - height=self.spool_base_height, - ).located(Cq.Location((hole_x, -t/2, 0))) - hole = Cq.Solid.makeBox( - length=t, - width=t, - height=self.spool_height + self.spool_base_height, - ).located(Cq.Location((hole_x, -t/2, 0))) - centre_hole = Cq.Solid.makeCylinder( + height=self.spool_core_height, + ).moved(Cq.Location((self.spool_groove_radius, -t/2, 0))) + cut_centre_hole = Cq.Solid.makeCylinder( radius=self.torsion_joint.radius_axle, - height=self.spool_height + self.spool_base_height, + height=cut_height, + ) + top = spindle.fuse(cap).cut(cut_hole, cut_centre_hole, cut_slot) + return ( + top + .fuse(top.located(Cq.Location((0,0,0), (1,0, 0), 180))) ) - return spindle.fuse(cap).cut(slot, hole, centre_hole) @target(name="child") def child(self) -> Cq.Assembly: @@ -611,6 +646,14 @@ class ShoulderJoint(Model): .assemble() .circle(radius_core_inner, mode='s') ) + angle_line_span = -self.angle_neutral + self.angle_max_deflection + 90 + angle_line = 180 - angle_line_span + # leave space for the line to rotate + spool_cut = Cq.Solid.makeCylinder( + radius=joint.radius_rider * 2, + height=self.spool_core_height * 2, + angleDegrees=angle_line_span, + ).moved(Cq.Location((0,0,-self.spool_core_height), (0,0,1), angle_line)) lip_extension = ( Cq.Solid.makeBox( length=self.child_lip_ext - self.child_guard_ext, @@ -676,7 +719,8 @@ class ShoulderJoint(Model): .toPending() .extrude(dh * 2) .translate(Cq.Vector(0, 0, -dh)) - .union(core_guard) + .cut(spool_cut) + .union(core_guard, tol=TOL) ) assert self.child_lip_width / 2 <= joint.radius_rider sign = 1 if self.flip else -1 @@ -695,8 +739,8 @@ class ShoulderJoint(Model): loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180) loc_axis_rotate_bot = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_bot + self.angle_neutral) loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral) - spool_dz = self.height / 2 - self.torsion_joint.total_height - spool_angle = 180 + self.angle_neutral + spool_dz = 0 + spool_angle = -self.angle_neutral loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) if self.flip else Cq.Location() result = ( Cq.Assembly() @@ -848,6 +892,7 @@ class DiskJoint(Model): housing_thickness: float = 4.0 disk_thickness: float = 8.0 + tongue_thickness: float = 10.0 # Amount by which the wall carves in wall_inset: float = 2.0 @@ -868,6 +913,9 @@ class DiskJoint(Model): generate_inner_wall: bool = False + axle_bolt: FlatHeadBolt = ELBOW_AXLE_BOLT + axle_washer: Washer = ELBOW_AXLE_WASHER + axle_hex_nut: HexNut = ELBOW_AXLE_HEX_NUT def __post_init__(self): super().__init__(name="disk-joint") @@ -876,6 +924,11 @@ class DiskJoint(Model): assert self.housing_upper_carve_offset > 0 assert self.spring_tail_hole_height > self.spring.thickness + assert self.tongue_thickness <= self.total_thickness + + assert self.axle_bolt.diam_thread == self.axle_washer.diam_thread + assert self.axle_bolt.diam_thread == self.axle_hex_nut.diam_thread + assert self.axle_bolt.height_thread > self.total_thickness, "Bolt is not long enough" @property def neutral_movement_angle(self) -> Optional[float]: @@ -932,10 +985,21 @@ class DiskJoint(Model): @target(name="disk") def disk(self) -> Cq.Workplane: radius_tongue = self.radius_disk + self.tongue_length - tongue = ( + outer_tongue = ( + Cq.Solid.makeCylinder( + height=self.tongue_thickness, + radius=radius_tongue, + angleDegrees=self.tongue_span, + ).cut(Cq.Solid.makeCylinder( + height=self.tongue_thickness, + radius=self.radius_housing, + )) + .moved(Cq.Location((0,0,(self.disk_thickness - self.tongue_thickness) / 2))) + ) + inner_tongue = ( Cq.Solid.makeCylinder( height=self.disk_thickness, - radius=radius_tongue, + radius=self.radius_housing, angleDegrees=self.tongue_span, ).cut(Cq.Solid.makeCylinder( height=self.disk_thickness, @@ -949,7 +1013,8 @@ class DiskJoint(Model): radius=self.radius_disk, centered=(True, True, False) ) - .union(tongue, tol=TOL) + .union(inner_tongue, tol=TOL) + .union(outer_tongue, tol=TOL) .copyWorkplane(Cq.Workplane('XY')) .cylinder( height=self.disk_thickness, @@ -995,6 +1060,7 @@ class DiskJoint(Model): )) ) result.faces(">Z").tag("mate") + result.faces("Z").workplane().tagPlane("dirX", direction="+X") result = result.cut( self @@ -1028,6 +1094,7 @@ class DiskJoint(Model): ) theta = math.radians(carve_angle) result.faces("Z").tag("top") p_xy = result.copyWorkplane(Cq.Workplane('XY')) p_xy.tagPlane("dirX", direction="+X") p_xy.tagPlane("dir", direction=(math.cos(theta), math.sin(theta), 0)) @@ -1063,6 +1130,8 @@ class DiskJoint(Model): housing_upper: str, disk: str, angle: float = 0.0, + fasteners: bool = True, + fastener_prefix: str = "fastener", ) -> Cq.Assembly: assert 0 <= angle <= self.movement_angle deflection = angle - (self.spring.angle_neutral - self.spring_angle_at_0) @@ -1084,6 +1153,16 @@ class DiskJoint(Model): #.constrain(f"{housing_lower}?dirX", f"{disk}?dir", "Axis", param=angle) #.constrain(f"{housing_lower}?dirY", f"{disk}?dir", "Axis", param=angle - 90) ) + if fasteners: + tag_bolt = f"{fastener_prefix}_bolt" + tag_nut = f"{fastener_prefix}_nut" + ( + assembly + .add(self.axle_bolt.assembly(), name=tag_bolt) + .add(self.axle_hex_nut.assembly(), name=tag_nut) + .constrain(f"{housing_lower}?bot", f"{tag_nut}?bot", "Plane") + .constrain(f"{housing_upper}?top", f"{tag_bolt}?root", "Plane", param=0) + ) return ( assembly ) @@ -1136,7 +1215,9 @@ class ElbowJoint(Model): lip_thickness: float = 5.0 lip_length: float = 60.0 - hole_pos: list[float] = field(default_factory=lambda: [12, 24]) + # Carve which allows light to go through + lip_side_depression_width: float = 10.0 + hole_pos: list[float] = field(default_factory=lambda: [15, 24]) parent_arm_width: float = 10.0 # Angle of the beginning of the parent arm parent_arm_angle: float = 180.0 @@ -1254,6 +1335,17 @@ class ElbowJoint(Model): Hole(x=-sign * x, tag=f"conn_bot{i}") ] ] + def post(sketch: Cq.Sketch) -> Cq.Sketch: + y_outer = self.disk_joint.total_thickness / 2 + y_inner = self.disk_joint.tongue_thickness / 2 + y = (y_outer + y_inner) / 2 + width = self.lip_side_depression_width + height = y_outer - y_inner + return ( + sketch + .push([(0, y), (0, -y)]) + .rect(width, height, mode='s') + ) mbox = MountingBox( length=self.lip_length, width=self.disk_joint.total_thickness, @@ -1263,6 +1355,7 @@ class ElbowJoint(Model): centred=(True, True), generate_side_tags=False, generate_reverse_tags=True, + profile_callback=post, ) return mbox.generate() @@ -1332,7 +1425,7 @@ class ElbowJoint(Model): def parent_joint_upper(self, generate_mount: bool=False, generate_tags=True): axial_offset = Cq.Location((self.parent_arm_radius, 0, 0)) housing_dz = self.disk_joint.housing_upper_dz - conn_h = self.disk_joint.total_thickness + conn_h = self.disk_joint.tongue_thickness conn_w = self.parent_arm_width connector = ( Cq.Solid.makeBox( diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 3a3ff62..7e6262c 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -19,6 +19,7 @@ from nhf.touhou.houjuu_nue.electronics import ( LINEAR_ACTUATOR_21, LINEAR_ACTUATOR_50, ElectronicBoard, + LightStrip, ELECTRONIC_MOUNT_HEXNUT, ) import nhf.utils @@ -52,6 +53,8 @@ class WingProfile(Model): spacer_thickness: float = 25.4 / 4 rod_width: float = 10.0 + light_strip: LightStrip = LightStrip() + shoulder_joint: ShoulderJoint = field(default_factory=lambda: ShoulderJoint( )) shoulder_angle_bias: float = 0.0 @@ -370,9 +373,8 @@ class WingProfile(Model): ) @submodel(name="spacer-s0-shoulder-act") def spacer_s0_shoulder_act(self) -> MountingBox: - dx = self.shoulder_joint.draft_height return MountingBox( - holes=[Hole(x=dx), Hole(x=-dx)], + holes=[Hole(x=0)], hole_diam=self.shoulder_joint.actuator.back_hole_diam, length=self.root_height, width=10.0, @@ -616,16 +618,17 @@ class WingProfile(Model): Hole(sign * x, tag=tag) for x, tag in joint.hole_loc_tags() ] + tongue_thickness = joint.disk_joint.tongue_thickness + carve_width = joint.lip_side_depression_width + assert carve_width >= self.light_strip.width + carve_height = (segment_thickness - tongue_thickness) / 2 + assert carve_height >= self.light_strip.height def carve_sides(profile): - dy = (segment_thickness + joint.total_thickness) / 4 + dy = (segment_thickness + tongue_thickness) / 4 return ( profile .push([(0,-dy), (0,dy)]) - .rect( - joint.parent_arm_width, - (segment_thickness - joint.total_thickness) / 2, - mode='s', - ) + .rect(carve_width, carve_height, mode='s') ) # FIXME: Carve out the sides so light can pass through mbox = MountingBox( -- 2.44.1 From 7c2054e4652e0c97bc41440f8e55d8d11a5b60f4 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sat, 3 Aug 2024 23:56:55 -0700 Subject: [PATCH 171/187] fix: Overthick elbow and wrist --- nhf/parts/box.py | 1 + nhf/touhou/houjuu_nue/joints.py | 23 ++++++---- nhf/touhou/houjuu_nue/wing.py | 79 +++++++++++++++++++-------------- 3 files changed, 61 insertions(+), 42 deletions(-) diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 5fbf21e..7270d29 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -75,6 +75,7 @@ class MountingBox(Model): profile_callback: Optional[Callable[[Cq.Sketch], Cq.Sketch]] = None def __post_init__(self): + assert self.thickness > 0 for i, hole in enumerate(self.holes): if hole.tag is None: hole.tag = f"conn{i}" diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 5ceac76..f98822d 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -72,7 +72,7 @@ ELBOW_AXLE_BOLT = FlatHeadBolt( diam_head=6.87, height_head=3.06, diam_thread=4.0, - height_thread=20.0, + height_thread=15.0, ) ELBOW_AXLE_WASHER = Washer( mass=0.0, @@ -888,11 +888,10 @@ class DiskJoint(Model): radius_housing: float = 22.0 radius_disk: float = 20.0 - radius_axle: float = 3.0 - housing_thickness: float = 4.0 - disk_thickness: float = 8.0 - tongue_thickness: float = 10.0 + housing_thickness: float = 2.0 + disk_thickness: float = 6.0 + tongue_thickness: float = 12.0 # Amount by which the wall carves in wall_inset: float = 2.0 @@ -924,7 +923,6 @@ class DiskJoint(Model): assert self.housing_upper_carve_offset > 0 assert self.spring_tail_hole_height > self.spring.thickness - assert self.tongue_thickness <= self.total_thickness assert self.axle_bolt.diam_thread == self.axle_washer.diam_thread assert self.axle_bolt.diam_thread == self.axle_hex_nut.diam_thread @@ -937,6 +935,10 @@ class DiskJoint(Model): return a return None + @property + def radius_axle(self) -> float: + return self.axle_bolt.diam_thread + @property def total_thickness(self) -> float: return self.housing_thickness * 2 + self.disk_thickness @@ -1268,8 +1270,11 @@ class ElbowJoint(Model): yield -x, f"conn_bot{i}" @property - def total_thickness(self): - return self.disk_joint.total_thickness + def total_thickness(self) -> float: + candidate1 = self.disk_joint.axle_bolt.height_thread + candidate2 = self.disk_joint.total_thickness + self.disk_joint.axle_hex_nut.thickness + head_thickness = self.disk_joint.axle_bolt.height_head + return head_thickness + max(candidate1, candidate2) @property def motion_span(self) -> float: @@ -1338,6 +1343,8 @@ class ElbowJoint(Model): def post(sketch: Cq.Sketch) -> Cq.Sketch: y_outer = self.disk_joint.total_thickness / 2 y_inner = self.disk_joint.tongue_thickness / 2 + if y_outer < y_inner: + return sketch y = (y_outer + y_inner) / 2 width = self.lip_side_depression_width height = y_outer - y_inner diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 7e6262c..179593d 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -29,6 +29,11 @@ ELBOW_PARAMS = dict( actuator=LINEAR_ACTUATOR_50, parent_arm_width=15, ) +ELBOW_DISK_PARAMS = dict( + housing_thickness=2.5, + disk_thickness=6.8, + tongue_thickness=12.3, +) WRIST_DISK_PARAMS = dict( movement_angle=30, radius_disk=13.0, @@ -116,8 +121,8 @@ class WingProfile(Model): self.wrist_axle_loc = self.wrist_bot_loc * \ Cq.Location.from2d(0, self.wrist_height * self.wrist_axle_pos) - assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness) - assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) + #assert self.elbow_joint.total_thickness < min(self.s1_thickness, self.s2_thickness) + #assert self.wrist_joint.total_thickness < min(self.s2_thickness, self.s3_thickness) self.shoulder_joint.angle_neutral = -self.shoulder_angle_neutral - self.shoulder_angle_bias self.shoulder_axle_loc = Cq.Location.from2d(self.shoulder_tip_x, self.shoulder_tip_y - self.shoulder_width / 2, 0) @@ -642,6 +647,29 @@ class WingProfile(Model): profile_callback=carve_sides, ) return mbox + def _actuator_mount(self, thickness: float, joint: ElbowJoint) -> MountingBox: + def post(sketch: Cq.Sketch) -> Cq.Sketch: + x = thickness / 2 - self.light_strip.height / 2 + w = self.light_strip.height + h = self.light_strip.width + return ( + sketch + .push([(x, 0), (-x, 0)]) + .rect(w, h, mode='s') + #.push([(0, x), (0, -x)]) + #.rect(h, w, mode='s') + ) + + return MountingBox( + length=thickness, + width=thickness, + thickness=self.spacer_thickness, + holes=[Hole(x=0,y=0)], + centred=(True, True), + hole_diam=joint.hole_diam, + centre_left_right_tags=True, + profile_callback=post, + ) @target(name="profile-s1", kind=TargetKind.DXF) def profile_s1(self) -> Cq.Sketch: @@ -709,14 +737,9 @@ class WingProfile(Model): ) @submodel(name="spacer-s1-elbow-act") def spacer_s1_elbow_act(self) -> MountingBox: - return MountingBox( - length=self.s1_thickness, - width=self.s1_thickness, - thickness=self.spacer_thickness, - holes=[Hole(x=0,y=0)], - centred=(True, True), - hole_diam=self.elbow_joint.hole_diam, - centre_left_right_tags=True, + return self._actuator_mount( + thickness=self.s1_thickness, + joint=self.elbow_joint ) @assembly() def assembly_s1(self) -> Cq.Assembly: @@ -838,14 +861,9 @@ class WingProfile(Model): ) @submodel(name="spacer-s2-elbow-act") def spacer_s2_elbow_act(self) -> MountingBox: - return MountingBox( - length=self.s2_thickness, - width=self.s2_thickness, - thickness=self.spacer_thickness, - holes=[Hole(x=0,y=0)], - centred=(True, True), - hole_diam=self.elbow_joint.hole_diam, - centre_left_right_tags=True, + return self._actuator_mount( + thickness=self.s2_thickness, + joint=self.elbow_joint ) @submodel(name="spacer-s2-wrist") def spacer_s2_wrist(self) -> MountingBox: @@ -855,14 +873,9 @@ class WingProfile(Model): ) @submodel(name="spacer-s2-wrist-act") def spacer_s2_wrist_act(self) -> MountingBox: - return MountingBox( - length=self.s2_thickness, - width=self.s2_thickness, - thickness=self.spacer_thickness, - holes=[Hole(x=0,y=0)], - centred=(True, True), - hole_diam=self.wrist_joint.hole_diam, - centre_left_right_tags=True, + return self._actuator_mount( + thickness=self.s2_thickness, + joint=self.wrist_joint ) @assembly() def assembly_s2(self) -> Cq.Assembly: @@ -979,14 +992,9 @@ class WingProfile(Model): ) @submodel(name="spacer-s3-wrist-act") def spacer_s3_wrist_act(self) -> MountingBox: - return MountingBox( - length=self.s3_thickness, - width=self.s3_thickness, - thickness=self.spacer_thickness, - holes=[Hole(x=0,y=0)], - centred=(True, True), - hole_diam=self.wrist_joint.hole_diam, - centre_left_right_tags=True, + return self._actuator_mount( + thickness=self.s3_thickness, + joint=self.wrist_joint ) @assembly() def assembly_s3(self) -> Cq.Assembly: @@ -1160,6 +1168,7 @@ class WingR(WingProfile): disk_joint=DiskJoint( movement_angle=55, spring_angle_at_0=75, + **ELBOW_DISK_PARAMS, ), flexor_offset_angle=15, flexor_mount_angle_child=-75, @@ -1416,6 +1425,7 @@ class WingL(WingProfile): disk_joint=DiskJoint( spring_angle_at_0=100, movement_angle=50, + **ELBOW_DISK_PARAMS, ), angle_neutral=30.0, flexor_mount_angle_child=220, @@ -1442,6 +1452,7 @@ class WingL(WingProfile): wrist_height: float = 43.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( + tongue_thickness=8.0, **WRIST_DISK_PARAMS, ), flip=False, -- 2.44.1 From 23ad93c9d4b16b09708fc233156647e655de0fd1 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 4 Aug 2024 00:25:47 -0700 Subject: [PATCH 172/187] feat: Make space for shoulder parent joint --- nhf/touhou/houjuu_nue/joints.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index f98822d..c58116d 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -389,6 +389,8 @@ class ShoulderJoint(Model): # The parent side has arms which connect to the lips parent_arm_width: float = 25.0 parent_arm_height: float = 12.0 + # remove a bit of material from the base so it does not interfere with gluing + parent_arm_base_shift: float = 2.0 # Generates a child guard which covers up the internals. The lip length is # relative to the +X surface of the guard. @@ -515,17 +517,24 @@ class ShoulderJoint(Model): assert self.parent_arm_width <= joint.radius_track * 2 assert self.parent_lip_ext > joint.radius_track + cut_arm = Cq.Solid.makeBox( + self.parent_lip_ext + self.parent_lip_width / 2, + self.parent_arm_width, + self.parent_arm_base_shift, + ) arm = ( Cq.Solid.makeBox( self.parent_lip_ext + self.parent_lip_width / 2, self.parent_arm_width, self.parent_arm_height) + .cut(cut_arm) .located(Cq.Location((0, -self.parent_arm_width/2 , 0))) .cut(Cq.Solid.makeCylinder(joint.radius_track, self.parent_arm_height)) .cut(self._contraction_cut_geometry(parent=True, mirror=top)) ) + t = self.parent_arm_base_shift lip_args = dict( - length=self.parent_lip_length, + length=self.parent_lip_length - t, width=self.parent_lip_width, thickness=self.parent_lip_thickness, hole_diam=self.parent_conn_hole_diam, @@ -534,19 +543,20 @@ class ShoulderJoint(Model): lip1 = MountingBox( **lip_args, holes=[ - Hole(x=self.height / 2 - x, y=-y) + Hole(x=self.height / 2 - x - t, y=-y) for x, y in self.parent_conn_hole_pos ], ) lip2 = MountingBox( **lip_args, holes=[ - Hole(x=self.height / 2 - x, y=y) + Hole(x=self.height / 2 - x - t, y=y) for x, y in self.parent_conn_hole_pos ], ) lip_dy = self.parent_arm_width / 2 - self.parent_lip_thickness # Flip so the lip's holes point to -X + loc_shift = Cq.Location((self.parent_arm_base_shift, 0, 0)) loc_axis = Cq.Location((0,0,0), (0, 1, 0), -90) loc_dir1 = Cq.Location((0,lip_dy,0), (0, 0, 1), -90) loc_dir2 = Cq.Location((0,-lip_dy,0), (0, 0, 1), 90) @@ -562,8 +572,8 @@ class ShoulderJoint(Model): .add(joint.track(), name="track", loc=Cq.Location((0, 0, 0), (0, 0, 1), rot)) .add(arm, name="arm") - .add(lip1.generate(), name=lip_p_tag, loc=loc_pos * loc_dir1 * loc_axis) - .add(lip2.generate(), name=lip_n_tag, loc=loc_pos * loc_dir2 * loc_axis) + .add(lip1.generate(), name=lip_p_tag, loc=loc_pos * loc_dir1 * loc_axis * loc_shift) + .add(lip2.generate(), name=lip_n_tag, loc=loc_pos * loc_dir2 * loc_axis * loc_shift) ) return result -- 2.44.1 From 8efea1d038f56ad203760febc7591ad20b6a28fa Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 4 Aug 2024 01:03:28 -0700 Subject: [PATCH 173/187] feat: Add arduino uno form factor --- nhf/parts/box.py | 2 +- nhf/parts/electronics.py | 72 ++++++++++++++++++++++++++++ nhf/touhou/houjuu_nue/__init__.py | 2 + nhf/touhou/houjuu_nue/electronics.py | 47 +++++++++++++++++- nhf/touhou/houjuu_nue/joints.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 6 ++- 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 nhf/parts/electronics.py diff --git a/nhf/parts/box.py b/nhf/parts/box.py index 7270d29..7376826 100644 --- a/nhf/parts/box.py +++ b/nhf/parts/box.py @@ -40,7 +40,7 @@ class Hole: assert self.tag is not None return self.tag + "_rev" - def cutting_geometry(self, default_diam: Optional[float]=None) -> Cq.Face: + def cutting_geometry(self, default_diam: Optional[float] = None) -> Cq.Face: if self.face is not None: return self.face diam = self.diam if self.diam is not None else default_diam diff --git a/nhf/parts/electronics.py b/nhf/parts/electronics.py new file mode 100644 index 0000000..b20e8e8 --- /dev/null +++ b/nhf/parts/electronics.py @@ -0,0 +1,72 @@ +from dataclasses import dataclass, field +from typing import Tuple +import cadquery as Cq +from nhf import Item, Role +from nhf.parts.box import Hole, MountingBox +import nhf.utils + +@dataclass(frozen=True) +class ArduinoUnoR3(Item): + # From datasheet + mass: float = 25.0 + length: float = 68.6 + width: float = 53.4 + + # with clearance + base_height: float = 5.0 + # aesthetic only for illustrating clearance + total_height: float = 50.0 + roof_height: float = 20.0 + + # This is labeled in mirrored coordinates from top down (i.e. unmirrored from bottom up) + holes: list[Tuple[float, float]] = field(default_factory=lambda: [ + (15.24, 2.54), + (13.74, 50.80), # x coordinate not labeled on schematic + (66.04, 17.78), + (66.04, 45.72), + ]) + hole_diam: float = 3.0 + + @property + def name(self) -> str: + return "Arduino Uno R3" + + @property + def role(self) -> Role: + return Role.ELECTRONIC + + def generate(self) -> Cq.Assembly: + sketch = ( + Cq.Sketch() + .polygon([ + (0,0), + (self.length, 0), + (self.length, self.width), + (0,self.width) + ]) + .push([(x, self.width - y) for x,y in self.holes]) + .circle(self.hole_diam / 2, mode='s') + ) + # pillar thickness + t = 3.0 + pillar_height = self.total_height - self.base_height + pillar = Cq.Solid.makeBox( + t, t, pillar_height + ) + roof = Cq.Solid.makeBox( + self.length, self.width, t + ) + result = ( + Cq.Workplane('XY') + .placeSketch(sketch) + .extrude(self.base_height) + .union(pillar.located(Cq.Location((0, 0, self.base_height)))) + .union(pillar.located(Cq.Location((self.length - t, 0, self.base_height)))) + .union(pillar.located(Cq.Location((self.length - t, self.width - t, self.base_height)))) + .union(pillar.located(Cq.Location((0, self.width - t, self.base_height)))) + .union(roof.located(Cq.Location((0, 0, self.total_height - t)))) + ) + plane = result.copyWorkplane(Cq.Workplane('XY')) + for i, (x, y) in enumerate(self.holes): + plane.moveTo(x, self.width - y).tagPlane(f"conn{i}", direction='-Z') + return result diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index a3a925c..e7d9887 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -37,6 +37,7 @@ import nhf.touhou.houjuu_nue.wing as MW import nhf.touhou.houjuu_nue.trident as MT import nhf.touhou.houjuu_nue.joints as MJ import nhf.touhou.houjuu_nue.harness as MH +import nhf.touhou.houjuu_nue.electronics as ME from nhf.parts.item import Item import nhf.utils @@ -67,6 +68,7 @@ class Parameters(Model): parent_substrate_cull_corners=(1,1,1,1), parent_substrate_cull_edges=(0,0,1,0), ), + electronic_board=ME.ElectronicBoardControl(), shoulder_angle_bias=WING_DEFLECT_EVEN, s0_top_hole=True, s0_bot_hole=True, diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 79a351d..35e74d3 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -11,6 +11,7 @@ from nhf.parts.box import MountingBox, Hole from nhf.parts.fibre import tension_fibre from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut +from nhf.parts.electronics import ArduinoUnoR3 from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON import nhf.utils @@ -509,7 +510,6 @@ class ElectronicBoard(Model): def __post_init__(self): super().__init__(name=self.name) - @submodel(name="panel") def panel(self) -> MountingBox: return MountingBox( holes=self.mount_holes, @@ -528,7 +528,7 @@ class ElectronicBoard(Model): .addS(panel.generate(), name="panel", role=Role.ELECTRONIC | Role.STRUCTURE, material=self.material) ) - for hole in panel.holes: + for hole in self.mount_holes: spacer_name = f"{hole.tag}_spacer" bolt_name = f"{hole.tag}_bolt" ( @@ -548,6 +548,49 @@ class ElectronicBoard(Model): ) return result.solve() +@dataclass +class ElectronicBoardBattery(ElectronicBoard): + name: str = "electronic-board-battery" + + @submodel(name="panel") + def panel_out(self) -> MountingBox: + return self.panel() + +@dataclass +class ElectronicBoardControl(ElectronicBoard): + name: str = "electronic-board-control" + + controller_datum: Cq.Location = Cq.Location.from2d(-25,10, -90) + + controller: ArduinoUnoR3 = ArduinoUnoR3() + + def panel(self) -> MountingBox: + box = super().panel() + def transform(i, x, y): + pos = self.controller_datum * Cq.Location.from2d(x, self.controller.width - y) + x, y = pos.to2d_pos() + return Hole( + x=x, y=y, + diam=self.controller.hole_diam, + tag=f"controller_conn{i}", + ) + box.holes = box.holes.copy() + [ + transform(i, x, y) + for i, (x, y) in enumerate(self.controller.holes) + ] + return box + + @submodel(name="panel") + def panel_out(self) -> MountingBox: + return self.panel() + + def assembly(self) -> Cq.Assembly: + result = super().assembly() + result.add(self.controller.assembly(), name="controller") + for i in range(len(self.controller.holes)): + result.constrain(f"controller?conn{i}", f"panel?controller_conn{i}", "Plane") + return result.solve() + @dataclass(frozen=True) class LightStrip: diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index c58116d..40f8e02 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -455,7 +455,7 @@ class ShoulderJoint(Model): """ Position of the middle of the spool measured from the middle """ - return self.height / 2 - self.torsion_joint.total_height - self.spool_base_height / 2 + return 0 def parent_lip_loc(self, left: bool=True) -> Cq.Location: """ diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 179593d..c72ebdf 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -19,6 +19,7 @@ from nhf.touhou.houjuu_nue.electronics import ( LINEAR_ACTUATOR_21, LINEAR_ACTUATOR_50, ElectronicBoard, + ElectronicBoardBattery, LightStrip, ELECTRONIC_MOUNT_HEXNUT, ) @@ -76,7 +77,7 @@ class WingProfile(Model): s0_top_hole: bool = False s0_bot_hole: bool = True - electronic_board: ElectronicBoard = field(default_factory=lambda: ElectronicBoard()) + electronic_board: ElectronicBoard = field(default_factory=lambda: ElectronicBoardBattery()) s1_thickness: float = 25.0 @@ -156,6 +157,9 @@ class WingProfile(Model): @submodel(name="wrist-joint") def submodel_wrist_joint(self) -> Model: return self.wrist_joint + @submodel(name="electronic-board") + def submodel_electronic_board(self) -> Model: + return self.electronic_board @property def root_height(self) -> float: -- 2.44.1 From 7212a2b0e8ebaa4280c14deb1f389f4d2dff7d4d Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 4 Aug 2024 10:38:50 -0700 Subject: [PATCH 174/187] fix: Resolve winch collision --- nhf/parts/electronics.py | 65 +++++++++++++++++++++- nhf/touhou/houjuu_nue/electronics.py | 81 ++++------------------------ nhf/touhou/houjuu_nue/joints.py | 13 ++++- nhf/touhou/houjuu_nue/wing.py | 80 +++++++++++++++++++++------ 4 files changed, 151 insertions(+), 88 deletions(-) diff --git a/nhf/parts/electronics.py b/nhf/parts/electronics.py index b20e8e8..0a145b3 100644 --- a/nhf/parts/electronics.py +++ b/nhf/parts/electronics.py @@ -21,7 +21,7 @@ class ArduinoUnoR3(Item): # This is labeled in mirrored coordinates from top down (i.e. unmirrored from bottom up) holes: list[Tuple[float, float]] = field(default_factory=lambda: [ (15.24, 2.54), - (13.74, 50.80), # x coordinate not labeled on schematic + (15.24 - 1.270, 50.80), # x coordinate not labeled on schematic (66.04, 17.78), (66.04, 45.72), ]) @@ -70,3 +70,66 @@ class ArduinoUnoR3(Item): for i, (x, y) in enumerate(self.holes): plane.moveTo(x, self.width - y).tagPlane(f"conn{i}", direction='-Z') return result + + +@dataclass(frozen=True) +class BatteryBox18650(Item): + """ + A number of 18650 batteries in series + """ + mass: float = 17.4 + 68.80 * 3 + length: float = 75.70 + width_base: float = 61.46 - 18.48 - 20.18 * 2 + battery_dist: float = 20.18 + height: float = 19.66 + # space from bottom to battery begin + thickness: float = 1.66 + battery_diam: float = 18.48 + battery_height: float = 68.80 + n_batteries: int = 3 + + def __post_init__(self): + assert 2 * self.thickness < min(self.length, self.height) + + @property + def name(self) -> str: + return f"BatteryBox 18650*{self.n_batteries}" + + @property + def role(self) -> Role: + return Role.ELECTRONIC + + def generate(self) -> Cq.Workplane: + width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam + return ( + Cq.Workplane('XY') + .box( + length=self.length, + width=width, + height=self.height, + centered=(True, True, False), + ) + .copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.thickness))) + .box( + length=self.length - self.thickness*2, + width=width - self.thickness*2, + height=self.height - self.thickness, + centered=(True, True, False), + combine='cut', + ) + .copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2))) + .rarray( + xSpacing=1, + ySpacing=self.battery_dist, + xCount=1, + yCount=self.n_batteries, + center=True, + ) + .cylinder( + radius=self.battery_diam/2, + height=self.battery_height, + direct=(1, 0, 0), + centered=(True, True, False), + combine=True, + ) + ) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index 35e74d3..b3d6724 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -11,7 +11,7 @@ from nhf.parts.box import MountingBox, Hole from nhf.parts.fibre import tension_fibre from nhf.parts.item import Item from nhf.parts.fasteners import FlatHeadBolt, HexNut -from nhf.parts.electronics import ArduinoUnoR3 +from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650 from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON import nhf.utils @@ -210,68 +210,6 @@ class MountingBracket(Item): result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid") return result -@dataclass(frozen=True) -class BatteryBox18650(Item): - """ - A number of 18650 batteries in series - """ - mass: float = 17.4 + 68.80 * 3 - length: float = 75.70 - width_base: float = 61.46 - 18.48 - 20.18 * 2 - battery_dist: float = 20.18 - height: float = 19.66 - # space from bottom to battery begin - thickness: float = 1.66 - battery_diam: float = 18.48 - battery_height: float = 68.80 - n_batteries: int = 3 - - def __post_init__(self): - assert 2 * self.thickness < min(self.length, self.height) - - @property - def name(self) -> str: - return f"BatteryBox 18650*{self.n_batteries}" - - @property - def role(self) -> Role: - return Role.ELECTRONIC - - def generate(self) -> Cq.Workplane: - width = self.width_base + self.battery_dist * (self.n_batteries - 1) + self.battery_diam - return ( - Cq.Workplane('XY') - .box( - length=self.length, - width=width, - height=self.height, - centered=(True, True, False), - ) - .copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.thickness))) - .box( - length=self.length - self.thickness*2, - width=width - self.thickness*2, - height=self.height - self.thickness, - centered=(True, True, False), - combine='cut', - ) - .copyWorkplane(Cq.Workplane('XY', origin=(-self.battery_height/2, 0, self.thickness + self.battery_diam/2))) - .rarray( - xSpacing=1, - ySpacing=self.battery_dist, - xCount=1, - yCount=self.n_batteries, - center=True, - ) - .cylinder( - radius=self.battery_diam/2, - height=self.battery_height, - direct=(1, 0, 0), - centered=(True, True, False), - combine=True, - ) - ) - LINEAR_ACTUATOR_50 = LinearActuator( mass=40.8, @@ -345,6 +283,15 @@ ELECTRONIC_MOUNT_HEXNUT = HexNut( width=6.81, ) +@dataclass(kw_only=True, frozen=True) +class Winch: + linear_motion_span: float + + actuator: LinearActuator = LINEAR_ACTUATOR_21 + nut: HexNut = LINEAR_ACTUATOR_HEX_NUT + bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT + bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET + @dataclass(kw_only=True) class Flexor: """ @@ -529,17 +476,10 @@ class ElectronicBoard(Model): role=Role.ELECTRONIC | Role.STRUCTURE, material=self.material) ) for hole in self.mount_holes: - spacer_name = f"{hole.tag}_spacer" bolt_name = f"{hole.tag}_bolt" ( result - .add(self.nut.assembly(), name=spacer_name) .add(self.bolt.assembly(), name=bolt_name) - .constrain( - f"{spacer_name}?top", - f"panel?{hole.rev_tag}", - "Plane" - ) .constrain( f"{bolt_name}?root", f"panel?{hole.tag}", @@ -551,6 +491,7 @@ class ElectronicBoard(Model): @dataclass class ElectronicBoardBattery(ElectronicBoard): name: str = "electronic-board-battery" + battery_box: BatteryBox18650 = BATTERY_BOX @submodel(name="panel") def panel_out(self) -> MountingBox: diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 40f8e02..9804a21 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -10,7 +10,7 @@ from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob, Washer from nhf.parts.joints import TorsionJoint, HirthJoint from nhf.parts.box import Hole, MountingBox, box_with_centre_holes from nhf.touhou.houjuu_nue.electronics import ( - Flexor, LinearActuator, LINEAR_ACTUATOR_21, + Winch, Flexor, LinearActuator, LINEAR_ACTUATOR_21, ) import nhf.geometry import nhf.utils @@ -390,7 +390,7 @@ class ShoulderJoint(Model): parent_arm_width: float = 25.0 parent_arm_height: float = 12.0 # remove a bit of material from the base so it does not interfere with gluing - parent_arm_base_shift: float = 2.0 + parent_arm_base_shift: float = 1.0 # Generates a child guard which covers up the internals. The lip length is # relative to the +X surface of the guard. @@ -423,6 +423,7 @@ class ShoulderJoint(Model): spool_groove_radius: float = 10.0 flip: bool = False + winch: Optional[Winch] = None # Initialized later actuator: LinearActuator = LINEAR_ACTUATOR_21 def __post_init__(self): @@ -431,6 +432,10 @@ class ShoulderJoint(Model): assert self.spool_groove_radius < self.spool_inner_radius < self.spool_outer_radius assert self.child_lip_height < self.height assert self.draft_length <= self.actuator.stroke_length + self.winch = Winch( + actuator=self.actuator, + linear_motion_span=self.draft_length, + ) @property def spool_outer_radius(self): @@ -457,6 +462,10 @@ class ShoulderJoint(Model): """ return 0 + @property + def parent_lip_gap(self): + return self.height - self.parent_lip_length * 2 + def parent_lip_loc(self, left: bool=True) -> Cq.Location: """ 2d location of the arm surface on the parent side, relative to axle diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index c72ebdf..81831eb 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -21,6 +21,7 @@ from nhf.touhou.houjuu_nue.electronics import ( ElectronicBoard, ElectronicBoardBattery, LightStrip, + ElectronicBoard, ELECTRONIC_MOUNT_HEXNUT, ) import nhf.utils @@ -287,7 +288,7 @@ class WingProfile(Model): plane.moveTo(t, self.root_height + t*2).tagPlane("top") return result - @submodel(name="spacer-s0-shoulder") + @submodel(name="spacer-s0-shoulder-inner") def spacer_s0_shoulder(self, left: bool=True) -> MountingBox: """ Shoulder side serves double purpose for mounting shoulder joint and @@ -302,6 +303,21 @@ class WingProfile(Model): Hole(x=-x, y=sign * y, tag=f"conn_bot{i}"), ] ] + def post(sketch: Cq.Sketch) -> Cq.Sketch: + """ + Carve out the middle if this is closer to the front + """ + if left: + return sketch + return ( + sketch + .push([(0,0)]) + .rect( + w=self.shoulder_joint.parent_lip_gap, + h=self.shoulder_joint.parent_lip_width, + mode='s' + ) + ) return MountingBox( length=self.shoulder_joint.height, width=self.shoulder_joint.parent_lip_width, @@ -311,7 +327,12 @@ class WingProfile(Model): centred=(True, True), flip_y=self.flip, centre_bot_top_tags=True, + profile_callback=post, ) + @submodel(name="spacer-s0-shoulder-outer") + def spacer_s0_shoulder_outer(self) -> MountingBox: + return self.spacer_s0_shoulder_inner(left=False) + @submodel(name="spacer-s0-base") def spacer_s0_base(self) -> MountingBox: """ @@ -341,11 +362,15 @@ class WingProfile(Model): @submodel(name="spacer-s0-electronic") def spacer_s0_electronic_mount(self) -> MountingBox: """ - This one has circular holes for the screws + This one has hexagonal holes """ + face = ELECTRONIC_MOUNT_HEXNUT.cutting_face() + holes = [ + Hole(x=h.x, y=h.y, face=face, tag=h.tag) + for h in self.electronic_board.mount_holes + ] return MountingBox( - holes=self.electronic_board.mount_holes, - hole_diam=self.electronic_board.mount_hole_diam, + holes=holes, length=self.root_height, width=self.electronic_board.width, centred=(True, True), @@ -356,13 +381,8 @@ class WingProfile(Model): @submodel(name="spacer-s0-electronic2") def spacer_s0_electronic_mount2(self) -> MountingBox: """ - This one has hexagonal holes + This one has circular holes """ - face = ELECTRONIC_MOUNT_HEXNUT.cutting_face() - holes = [ - Hole(x=h.x, y=h.y, face=face, tag=h.tag) - for h in self.electronic_board.mount_holes - ] def post(sketch: Cq.Sketch) -> Cq.Sketch: return ( sketch @@ -370,7 +390,7 @@ class WingProfile(Model): .rect(70, 130, mode='s') ) return MountingBox( - holes=holes, + holes=self.electronic_board.mount_holes, hole_diam=self.electronic_board.mount_hole_diam, length=self.root_height, width=self.electronic_board.width, @@ -408,7 +428,7 @@ class WingProfile(Model): ("shoulder_right", self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), ("shoulder_act", - self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(100, -20)), + self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(120, -40)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("electronic_mount", Cq.Location.from2d(-35, 65, 60)), ] @@ -474,17 +494,26 @@ class WingProfile(Model): for hole in self.electronic_board.mount_holes: result.constrain( f"electronic_mount?{hole.tag}", - f"electronic_mount2?{hole.tag}_rev", + f"electronic_mount2?{hole.rev_tag}", "Plane") if not ignore_electronics: result.add(self.electronic_board.assembly(), name="electronic_board") for hole in self.electronic_board.mount_holes: assert hole.tag + nut_name = f"{hole.tag}_nut" + ( + result + .add(self.electronic_board.nut.assembly(), name=nut_name) + .constrain( + f"{nut_name}?top", + f"electronic_mount?{hole.tag}", + "Plane", param=0 + ) + ) result.constrain( f"electronic_mount2?{hole.tag}", - f'electronic_board/{hole.tag}_spacer?top', + f'electronic_board/panel?{hole.rev_tag}', "Plane", - param=0 ) return result.solve() @@ -1073,6 +1102,27 @@ class WingProfile(Model): result.add(self.assembly_s0( ignore_electronics=ignore_electronics ), name="s0") + if not ignore_electronics: + tag_act = "shoulder_act" + tag_bolt = "shoulder_act_bolt" + tag_nut = "shoulder_act_nut" + tag_bracket = "shoulder_act_bracket" + winch = self.shoulder_joint.winch + ( + result + .add(winch.actuator.assembly(pos=0), name=tag_act) + .add(winch.bracket.assembly(), name=tag_bracket) + .add(winch.bolt.assembly(), name=tag_bolt) + .add(winch.nut.assembly(), name=tag_nut) + .constrain(f"{tag_bolt}?root", f"{tag_bracket}?conn_top", + "Plane", param=0) + .constrain(f"{tag_nut}?bot", f"{tag_bracket}?conn_bot", + "Plane") + .constrain(f"{tag_act}/back?conn", f"{tag_bracket}?conn_mid", + "Plane", param=0) + .constrain("s0/shoulder_act?conn0", f"{tag_bracket}?conn_side", + "Plane") + ) if "root" in parts: result.addS(self.root_joint.assembly( offset=root_offset, -- 2.44.1 From c4a5f5770f72b71c0ee275086becd0080497f56b Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 4 Aug 2024 14:21:11 -0700 Subject: [PATCH 175/187] feat: Produce outer and inner shell for s0 --- nhf/touhou/houjuu_nue/electronics.py | 1 + nhf/touhou/houjuu_nue/wing.py | 106 +++++++++++++++------------ 2 files changed, 59 insertions(+), 48 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index b3d6724..b5cc1aa 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -129,6 +129,7 @@ class LinearActuator(Item): combine='cut', ) ) + back.faces(">X").tag("dir") back.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn') result = ( Cq.Assembly() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 81831eb..1024e1a 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -170,23 +170,21 @@ class WingProfile(Model): def shoulder_height(self) -> float: return self.shoulder_joint.height - def outer_profile_s0(self) -> Cq.Sketch: + def outer_profile_s0(self) -> Cq.Edge: """ The outer boundary of s0 top/bottom slots """ tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y - return ( - Cq.Sketch() - .spline([ - (0, 0), - (-30.0, 80.0), - (tip_x, tip_y) - ]) - #.segment( - # (tip_x, tip_y), - # (tip_x - 10, tip_y), - #) + return Cq.Edge.makeSpline( + [ + Cq.Vector(*p) + for p in [ + (0, 0), + (-30.0, 80.0), + (tip_x, tip_y), + ] + ] ) def inner_profile_s0(self) -> Cq.Edge: """ @@ -199,18 +197,17 @@ class WingProfile(Model): dx1 = self.shoulder_base_bezier_x dy1 = self.shoulder_base_bezier_y sw = self.shoulder_width + points = [ + (tip_x, tip_y - sw), + (tip_x + dx2, tip_y - sw + dy2), + (-self.base_width + dx1, dy1), + (-self.base_width, 0), + ] return Cq.Edge.makeBezier( - [ - Cq.Vector(*p) - for p in [ - (tip_x, tip_y - sw), - (tip_x + dx2, tip_y - sw + dy2), - (-self.base_width + dx1, dy1), - (-self.base_width, 0), - ] - ] + [Cq.Vector(x, y) for x, y in points] ) + @property def shoulder_angle_neutral(self) -> float: """ @@ -224,24 +221,16 @@ class WingProfile(Model): def profile_s0(self, top: bool = True) -> Cq.Sketch: tip_x = self.shoulder_tip_x tip_y = self.shoulder_tip_y - dx2 = self.shoulder_tip_bezier_x - dy2 = self.shoulder_tip_bezier_y - dx1 = self.shoulder_base_bezier_x - dy1 = self.shoulder_base_bezier_y sw = self.shoulder_width sketch = ( - self.outer_profile_s0() + Cq.Sketch() + .edge(self.outer_profile_s0()) .segment((-self.base_width, 0), (0, 0)) .segment( (tip_x, tip_y), (tip_x, tip_y - sw), ) - .bezier([ - (tip_x, tip_y - sw), - (tip_x + dx2, tip_y - sw + dy2), - (-self.base_width + dx1, dy1), - (-self.base_width, 0), - ]) + .edge(self.inner_profile_s0()) .assemble() .push([self.shoulder_axle_loc.to2d_pos()]) .circle(self.shoulder_joint.radius, mode='a') @@ -265,7 +254,7 @@ class WingProfile(Model): def outer_shell_s0(self) -> Cq.Workplane: t = self.panel_thickness_s0 - profile = Cq.Wire.assembleEdges(self.outer_profile_s0().edges().vals()) + profile = self.outer_profile_s0() result = ( Cq.Workplane('XZ') .rect(t, self.root_height + t*2, centered=(False, False)) @@ -277,17 +266,35 @@ class WingProfile(Model): return result def inner_shell_s0(self) -> Cq.Workplane: t = self.panel_thickness_s0 - #profile = Cq.Wire.assembleEdges(self.inner_profile_s0()) + profile = self.inner_profile_s0() result = ( Cq.Workplane('XZ') + .moveTo(-t, 0) .rect(t, self.root_height + t*2, centered=(False, False)) - .sweep(self.inner_profile_s0()) + .sweep(profile, normal=(0,-1,0)) ) plane = result.copyWorkplane(Cq.Workplane('XZ')) - plane.moveTo(t, 0).tagPlane("bot") - plane.moveTo(t, self.root_height + t*2).tagPlane("top") + plane.moveTo(0, 0).tagPlane("bot") + plane.moveTo(0, self.root_height + t*2).tagPlane("top") return result + @target(name="profile-s0-outer-shell", kind=TargetKind.DXF) + def outer_shell_s0_profile(self) -> Cq.Sketch: + """ + This part should be laser cut and then bent on a falsework to create the required shape. + """ + length = self.outer_profile_s0().Length() + height = self.root_height + self.panel_thickness_s0 * 2 + return Cq.Sketch().rect(length, height) + @target(name="profile-s0-inner-shell", kind=TargetKind.DXF) + def inner_shell_s0_profile(self) -> Cq.Sketch: + """ + This part should be laser cut and then bent on a falsework to create the required shape. + """ + length = self.inner_profile_s0().Length() + height = self.root_height + self.panel_thickness_s0 * 2 + return Cq.Sketch().rect(length, height) + @submodel(name="spacer-s0-shoulder-inner") def spacer_s0_shoulder(self, left: bool=True) -> MountingBox: """ @@ -331,7 +338,7 @@ class WingProfile(Model): ) @submodel(name="spacer-s0-shoulder-outer") def spacer_s0_shoulder_outer(self) -> MountingBox: - return self.spacer_s0_shoulder_inner(left=False) + return self.spacer_s0_shoulder(left=False) @submodel(name="spacer-s0-base") def spacer_s0_base(self) -> MountingBox: @@ -428,7 +435,7 @@ class WingProfile(Model): ("shoulder_right", self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), ("shoulder_act", - self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(120, -40)), + self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(120, -40, -30)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("electronic_mount", Cq.Location.from2d(-35, 65, 60)), ] @@ -436,11 +443,11 @@ class WingProfile(Model): self.profile_s0(top=top), self.panel_thickness_s0, tags, - reverse=not top, + reverse=top, ) - h = self.panel_thickness if top else 0 + h = 0 if top else self.panel_thickness_s0 result.copyWorkplane(Cq.Workplane('XZ')).moveTo(0, h).tagPlane("corner") - result.copyWorkplane(Cq.Workplane('XZ')).moveTo(-self.base_width, h).tagPlane("corner_left") + result.copyWorkplane(Cq.Workplane('XZ')).moveTo(-self.base_width, self.panel_thickness_s0 - h).tagPlane("corner_left") return result @assembly() @@ -449,9 +456,9 @@ class WingProfile(Model): ignore_electronics: bool=False) -> Cq.Assembly: result = ( Cq.Assembly() - .addS(self.surface_s0(top=True), name="bot", + .addS(self.surface_s0(top=False), name="bot", material=self.mat_panel, role=self.role_panel) - .addS(self.surface_s0(top=False), name="top", + .addS(self.surface_s0(top=True), name="top", material=self.mat_panel, role=self.role_panel, loc=Cq.Location((0, 0, self.root_height + self.panel_thickness))) .constrain("bot", "Fixed") @@ -462,10 +469,10 @@ class WingProfile(Model): material=self.mat_panel, role=self.role_panel) .constrain("bot?corner", "outer_shell?bot", "Plane", param=0) .constrain("top?corner", "outer_shell?top", "Plane", param=0) - #.addS(self.inner_shell_s0(), name="inner_shell", - # material=self.mat_panel, role=self.role_panel) - #.constrain("bot?corner_left", "inner_shell?bot", "Point") - #.constrain("top?corner_left", "inner_shell?top", "Point") + .addS(self.inner_shell_s0(), name="inner_shell", + material=self.mat_panel, role=self.role_panel) + .constrain("bot?corner_left", "inner_shell?bot", "Plane", param=0) + .constrain("top?corner_left", "inner_shell?top", "Plane", param=0) ) for o, tag in [ (self.spacer_s0_shoulder(left=True).generate(), "shoulder_left"), @@ -1122,6 +1129,9 @@ class WingProfile(Model): "Plane", param=0) .constrain("s0/shoulder_act?conn0", f"{tag_bracket}?conn_side", "Plane") + # Directional constraints should be provided by the line + .constrain(f"{tag_bracket}?conn_mid", "s0/shoulder_act?top", "Axis", param=0) + .constrain(f"{tag_act}/back?dir", "s0/shoulder_act?conn0", "Axis", param=0) ) if "root" in parts: result.addS(self.root_joint.assembly( -- 2.44.1 From 556a35392dfb60a090775dd594cf69fcb0fe8d91 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 4 Aug 2024 14:32:10 -0700 Subject: [PATCH 176/187] feat: Truncate inner panel to avoid collision --- nhf/touhou/houjuu_nue/wing.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 1024e1a..7651450 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -59,6 +59,7 @@ class WingProfile(Model): # strength spacer_thickness: float = 25.4 / 4 rod_width: float = 10.0 + panel_s0_inner_trunc = 0.05 light_strip: LightStrip = LightStrip() @@ -186,7 +187,7 @@ class WingProfile(Model): ] ] ) - def inner_profile_s0(self) -> Cq.Edge: + def inner_profile_s0(self, trunc: float=0.0) -> Cq.Edge: """ The inner boundary of s0 """ @@ -203,9 +204,21 @@ class WingProfile(Model): (-self.base_width + dx1, dy1), (-self.base_width, 0), ] - return Cq.Edge.makeBezier( + bezier = Cq.Edge.makeBezier( [Cq.Vector(x, y) for x, y in points] ) + if trunc == 0.0: + return bezier + + tip = bezier.positionAt(d=trunc, mode='parameter') + tangent = bezier.tangentAt(locationParam=trunc, mode='parameter') + points = [ + tip, + tip + tangent, + Cq.Vector(-self.base_width + dx1, dy1), + Cq.Vector(-self.base_width, 0), + ] + return Cq.Edge.makeBezier(points) @property @@ -266,7 +279,7 @@ class WingProfile(Model): return result def inner_shell_s0(self) -> Cq.Workplane: t = self.panel_thickness_s0 - profile = self.inner_profile_s0() + profile = self.inner_profile_s0(trunc=self.panel_s0_inner_trunc) result = ( Cq.Workplane('XZ') .moveTo(-t, 0) @@ -291,7 +304,7 @@ class WingProfile(Model): """ This part should be laser cut and then bent on a falsework to create the required shape. """ - length = self.inner_profile_s0().Length() + length = self.inner_profile_s0(trunc=self.panel_s0_inner_trunc).Length() height = self.root_height + self.panel_thickness_s0 * 2 return Cq.Sketch().rect(length, height) -- 2.44.1 From 96f546b8b1086b26003906f26f66720499b9a6c8 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Sun, 4 Aug 2024 14:35:44 -0700 Subject: [PATCH 177/187] feat: Move actuator and controller --- nhf/touhou/houjuu_nue/electronics.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/houjuu_nue/electronics.py b/nhf/touhou/houjuu_nue/electronics.py index b5cc1aa..1b42c54 100644 --- a/nhf/touhou/houjuu_nue/electronics.py +++ b/nhf/touhou/houjuu_nue/electronics.py @@ -502,7 +502,7 @@ class ElectronicBoardBattery(ElectronicBoard): class ElectronicBoardControl(ElectronicBoard): name: str = "electronic-board-control" - controller_datum: Cq.Location = Cq.Location.from2d(-25,10, -90) + controller_datum: Cq.Location = Cq.Location.from2d(-25, 23, -90) controller: ArduinoUnoR3 = ArduinoUnoR3() diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 7651450..6576129 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -448,7 +448,7 @@ class WingProfile(Model): ("shoulder_right", self.shoulder_axle_loc * axle_rotate * self.shoulder_joint.parent_lip_loc(left=False)), ("shoulder_act", - self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(120, -40, -30)), + self.shoulder_axle_loc * axle_rotate * Cq.Location.from2d(110, -40, -25)), ("base", Cq.Location.from2d(base_dx, base_dy, 90)), ("electronic_mount", Cq.Location.from2d(-35, 65, 60)), ] -- 2.44.1 From b101340a7ded750d6a709f243feeb21685a1f9c5 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 5 Aug 2024 09:09:28 -0700 Subject: [PATCH 178/187] feat: Smaller elbow joints --- nhf/touhou/houjuu_nue/joints.py | 4 ++-- nhf/touhou/houjuu_nue/wing.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 9804a21..87bc098 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1235,10 +1235,10 @@ class ElbowJoint(Model): parent_arm_radius: float = 40.0 lip_thickness: float = 5.0 - lip_length: float = 60.0 + lip_length: float = 40.0 # Carve which allows light to go through lip_side_depression_width: float = 10.0 - hole_pos: list[float] = field(default_factory=lambda: [15, 24]) + hole_pos: list[float] = field(default_factory=lambda: [12.5]) parent_arm_width: float = 10.0 # Angle of the beginning of the parent arm parent_arm_angle: float = 180.0 diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 6576129..96482ea 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -1272,8 +1272,8 @@ class WingR(WingProfile): flexor_child_arm_radius=None, flexor_mount_angle_parent=20, flexor_mount_angle_child=-40, - hole_pos=[10, 20], - lip_length=50, + hole_pos=[10], + lip_length=30, actuator=LINEAR_ACTUATOR_10, #flexor_pos_smaller=False, **WRIST_PARAMS @@ -1516,7 +1516,7 @@ class WingL(WingProfile): flexor_parent_angle_fix=None, flexor_child_arm_radius=50.0, parent_arm_radius=50.0, - child_arm_radius=50.0, + child_arm_radius=40.0, flexor_pos_smaller=False, flip=True, **ELBOW_PARAMS -- 2.44.1 From 8d613a3d08b98e0ed5a237257ecd9779ac6c16c9 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 5 Aug 2024 09:28:24 -0700 Subject: [PATCH 179/187] feat: Use same tongue thickness for wrist --- nhf/touhou/houjuu_nue/wing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 96482ea..ace2008 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -40,6 +40,7 @@ WRIST_DISK_PARAMS = dict( movement_angle=30, radius_disk=13.0, radius_housing=15.0, + tongue_thickness=8.0, ) WRIST_PARAMS = dict( ) @@ -1529,7 +1530,6 @@ class WingL(WingProfile): wrist_height: float = 43.0 wrist_joint: ElbowJoint = field(default_factory=lambda: ElbowJoint( disk_joint=DiskJoint( - tongue_thickness=8.0, **WRIST_DISK_PARAMS, ), flip=False, -- 2.44.1 From b3db6ab0041b6591b8847847300f48e7d1a3f062 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 5 Aug 2024 11:08:39 -0700 Subject: [PATCH 180/187] fix: Add missing s3 extra output --- nhf/touhou/houjuu_nue/wing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index ace2008..4909134 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -1430,6 +1430,7 @@ class WingR(WingProfile): ) return profile + @target(name="profile-s3-extra", kind=TargetKind.DXF) def profile_s3_extra(self) -> Cq.Sketch: """ Implements the blade part on Nue's wing -- 2.44.1 From 749d7ebf42de10a70dfa7528358fd94f2de78c80 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 6 Aug 2024 00:09:08 -0700 Subject: [PATCH 181/187] fix: Axle diameter --- nhf/touhou/houjuu_nue/joints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 87bc098..ca6003e 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -956,7 +956,7 @@ class DiskJoint(Model): @property def radius_axle(self) -> float: - return self.axle_bolt.diam_thread + return self.axle_bolt.diam_thread / 2 @property def total_thickness(self) -> float: -- 2.44.1 From dfc745617fdfbaad2614795ed16c9b8fea359057 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 7 Aug 2024 08:16:44 -0700 Subject: [PATCH 182/187] fix: Spool obstruction on right --- nhf/touhou/houjuu_nue/joints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index ca6003e..9a6378e 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -760,7 +760,7 @@ class ShoulderJoint(Model): loc_axis_rotate_top = Cq.Location((0, 0, 0), (0, 0, 1), self.axis_rotate_top + self.angle_neutral) spool_dz = 0 spool_angle = -self.angle_neutral - loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) if self.flip else Cq.Location() + loc_spool_flip = Cq.Location((0,0,0),(0,1,0),180) #if self.flip else Cq.Location() result = ( Cq.Assembly() .add(core, name="core", loc=Cq.Location()) -- 2.44.1 From 363a67841eda72b95cb0df135128dd86c80fabf9 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Tue, 6 Aug 2024 23:46:54 -0700 Subject: [PATCH 183/187] fix: Tongue overthick problem --- nhf/touhou/houjuu_nue/joints.py | 2 +- nhf/touhou/houjuu_nue/wing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index 9a6378e..ef03702 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -1362,7 +1362,7 @@ class ElbowJoint(Model): def post(sketch: Cq.Sketch) -> Cq.Sketch: y_outer = self.disk_joint.total_thickness / 2 y_inner = self.disk_joint.tongue_thickness / 2 - if y_outer < y_inner: + if y_outer <= y_inner: return sketch y = (y_outer + y_inner) / 2 width = self.lip_side_depression_width diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 4909134..5728eca 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -34,7 +34,7 @@ ELBOW_PARAMS = dict( ELBOW_DISK_PARAMS = dict( housing_thickness=2.5, disk_thickness=6.8, - tongue_thickness=12.3, + tongue_thickness=2.5 * 2 + 6.8, ) WRIST_DISK_PARAMS = dict( movement_angle=30, -- 2.44.1 From 17001f87dadd6a349832db4069d9c4d582c7303f Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 12 Aug 2024 00:30:14 -0700 Subject: [PATCH 184/187] fix: s3 blade geometry --- nhf/touhou/houjuu_nue/wing.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index 5728eca..fdc4eb5 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -1380,6 +1380,7 @@ class WingR(WingProfile): self, axle_loc: Cq.Location, radius: float, + core_radius: float, angle_span: float, bot: bool = False) -> Cq.Sketch: """ @@ -1391,13 +1392,15 @@ class WingR(WingProfile): axle_loc = axle_loc * Cq.Location.rot2d(-90 if bot else 90) loc_h = Cq.Location.from2d(radius, 0) loc_offset = axle_loc * Cq.Location.from2d(0, margin) + core_start = axle_loc * Cq.Location.rot2d(-angle_span) * Cq.Location.from2d(0, core_radius) + core_end = axle_loc * Cq.Location.rot2d(180) * Cq.Location.from2d(0, core_radius) start = axle_loc * loc_h mid = axle_loc * Cq.Location.rot2d(-sign * angle_span/2) * loc_h end = axle_loc * Cq.Location.rot2d(-sign * angle_span) * loc_h return ( Cq.Sketch() .segment( - loc_offset.to2d_pos(), + core_start.to2d_pos(), start.to2d_pos(), ) .arc( @@ -1407,11 +1410,15 @@ class WingR(WingProfile): ) .segment( end.to2d_pos(), + core_end.to2d_pos(), + ) + .segment( + core_start.to2d_pos(), axle_loc.to2d_pos(), ) .segment( axle_loc.to2d_pos(), - loc_offset.to2d_pos(), + core_end.to2d_pos(), ) .assemble() ) @@ -1425,6 +1432,7 @@ class WingR(WingProfile): profile = self._child_joint_extension_profile( axle_loc=self.wrist_axle_loc, radius=self.wrist_height, + core_radius=3, angle_span=self.wrist_joint.motion_span, bot=False, ) @@ -1435,24 +1443,26 @@ class WingR(WingProfile): """ Implements the blade part on Nue's wing """ - left_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(-1) + margin = 5 + blade_margin = 10 + + left_top_loc = self.wrist_axle_loc * Cq.Location.rot2d(-15) * Cq.Location.from2d(margin, 0) hole_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_hole_angle) right_bot_loc = self.arrow_bot_loc * Cq.Location.rot2d(self.blade_angle) - h_loc = Cq.Location.from2d(0, self.arrow_height) # Law of sines, uses the triangle of (wrist_bot_loc, arrow_bot_loc, ?) theta_wp = math.radians(90 - self.blade_wrist_approx_tangent_angle) theta_b = math.radians(self.blade_angle) h_blade = math.sin(theta_wp) / math.sin(math.pi - theta_b - theta_wp) * self.arrow_height - h_blade_loc = Cq.Location.from2d(0, h_blade) + h_blade_loc = Cq.Location.from2d(0, h_blade + blade_margin) return ( Cq.Sketch() .segment( self.arrow_bot_loc.to2d_pos(), - (left_bot_loc * h_loc).to2d_pos(), + left_top_loc.to2d_pos(), ) .segment( - (self.arrow_bot_loc * h_loc).to2d_pos(), + self.wrist_axle_loc.to2d_pos(), ) .segment( (right_bot_loc * h_blade_loc).to2d_pos(), -- 2.44.1 From 66cdd1b3596d68dbcf43c38c55dc004628660c39 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Thu, 15 Aug 2024 11:06:39 -0700 Subject: [PATCH 185/187] feat: Shrink the mount --- nhf/touhou/houjuu_nue/harness.py | 2 +- nhf/touhou/houjuu_nue/joints.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/houjuu_nue/harness.py b/nhf/touhou/houjuu_nue/harness.py index 1f31dfc..516715a 100644 --- a/nhf/touhou/houjuu_nue/harness.py +++ b/nhf/touhou/houjuu_nue/harness.py @@ -53,7 +53,7 @@ BASE_POS_Y = 100.0 @dataclass(kw_only=True) class Harness(Model): thickness: float = 25.4 / 8 - width: float = 220.0 + width: float = 200.0 height: float = 304.8 fillet: float = 10.0 diff --git a/nhf/touhou/houjuu_nue/joints.py b/nhf/touhou/houjuu_nue/joints.py index ef03702..c3df763 100644 --- a/nhf/touhou/houjuu_nue/joints.py +++ b/nhf/touhou/houjuu_nue/joints.py @@ -103,12 +103,12 @@ class RootJoint(Model): base_height=4.0, n_tooth=24, )) - parent_width: float = 85 + parent_width: float = 60 parent_thickness: float = 10 parent_corner_fillet: float = 5 - parent_corner_cbore_diam: float = 12 + parent_corner_cbore_diam: float = 12.6 parent_corner_cbore_depth: float = 2 - parent_corner_inset: float = 12 + parent_corner_inset: float = 7.5 parent_mount_thickness: float = 25.4 / 16 parent_substrate_thickness: float = 25.4 / 16 -- 2.44.1 From 114aec49449ca3f20bba0cfbe359251731f3fb3e Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Wed, 4 Sep 2024 16:27:51 -0700 Subject: [PATCH 186/187] fix: Mount output --- nhf/touhou/houjuu_nue/wing.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/nhf/touhou/houjuu_nue/wing.py b/nhf/touhou/houjuu_nue/wing.py index fdc4eb5..3b7ba65 100644 --- a/nhf/touhou/houjuu_nue/wing.py +++ b/nhf/touhou/houjuu_nue/wing.py @@ -380,14 +380,18 @@ class WingProfile(Model): centred=(True, True), flip_y=self.flip, ) - @submodel(name="spacer-s0-electronic") - def spacer_s0_electronic_mount(self) -> MountingBox: + + def spacer_s0_electronic_mount(self, circle: bool = False) -> MountingBox: """ This one has hexagonal holes """ - face = ELECTRONIC_MOUNT_HEXNUT.cutting_face() + face = self.electronic_board.nut.cutting_face() + diam = self.electronic_board.bolt.diam_thread holes = [ - Hole(x=h.x, y=h.y, face=face, tag=h.tag) + Hole( + x=h.x, y=h.y, tag=h.tag, + face=None if circle else face, + diam=diam if circle else None) for h in self.electronic_board.mount_holes ] return MountingBox( @@ -399,6 +403,12 @@ class WingProfile(Model): flip_y=False,#self.flip, generate_reverse_tags=True, ) + @submodel(name="spacer-s0-electronic-hex") + def spacer_s0_electronic_mount_hex(self) -> MountingBox: + return self.spacer_s0_electronic_mount(False) + @submodel(name="spacer-s0-electronic-circle") + def spacer_s0_electronic_mount_circle(self) -> MountingBox: + return self.spacer_s0_electronic_mount(True) @submodel(name="spacer-s0-electronic2") def spacer_s0_electronic_mount2(self) -> MountingBox: """ -- 2.44.1 From 03a8f3b653a09310a7b5ee1743bcf92d14d019c4 Mon Sep 17 00:00:00 2001 From: Leni Aniva Date: Mon, 21 Oct 2024 22:20:29 -0700 Subject: [PATCH 187/187] feat: Model export --- nhf/touhou/houjuu_nue/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nhf/touhou/houjuu_nue/__init__.py b/nhf/touhou/houjuu_nue/__init__.py index e7d9887..dc759b3 100644 --- a/nhf/touhou/houjuu_nue/__init__.py +++ b/nhf/touhou/houjuu_nue/__init__.py @@ -198,3 +198,7 @@ if __name__ == '__main__': if sys.argv[1] == 'stat': print(p.stat()) + elif sys.argv[1] == 'model': + file_name = sys.argv[2] + a = p.wings_harness_assembly() + a.save(file_name, exportType='STEP') -- 2.44.1