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") )