feat: Metric threads on handle

This commit is contained in:
Leni Aniva 2024-06-26 11:28:25 -04:00
parent 0c42f71c9f
commit d823a58d88
Signed by: aniva
GPG Key ID: 4D9B1C8D10EA4C50
5 changed files with 537 additions and 44 deletions

View File

@ -3,59 +3,89 @@ This schematics file contains all designs related to tool handles
""" """
from dataclasses import dataclass from dataclasses import dataclass
import cadquery as Cq import cadquery as Cq
import nhf.metric_threads as NMt
@dataclass(frozen=True) @dataclass(frozen=True)
class Handle: class Handle:
""" """
Characteristic of a tool 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 # Outer and inner radius for the handle usually come in standard sizes
radius: float = 38 / 2 diam: float = 38
diam_inner: float = 33
# Inner radius # Major diameter of the internal threads, following ISO metric screw thread
radius_inner: float = 33 / 2 # standard. This determines the wall thickness of the insertion.
diam_threading: float = 27.0
# Wall thickness for the connector thread_pitch: float = 3.0
insertion_thickness: float = 4
# The connector goes in the insertion # Internal cavity diameter. This determines the wall thickness of the connector
connector_thickness: float = 4 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 # Length for the rim on the female connector
rim_length: float = 5 rim_length: float = 5
insertion_length: float = 60 insertion_length: float = 60
# Amount by which the connector goes into the segment
connector_length: float = 60 connector_length: float = 60
def __post_init__(self): def __post_init__(self):
assert self.radius > self.radius_inner assert self.diam > self.diam_inner, "Material thickness cannot be <= 0"
assert self.radius_inner > self.insertion_thickness + self.connector_thickness 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 assert self.insertion_length > self.rim_length
@property @property
def _r1(self): def diam_insertion_internal(self):
""" r = NMt.metric_thread_major_radius(
Radius of inside of insertion self.diam_threading,
""" self.thread_pitch,
return self.radius_inner - self.insertion_thickness internal=True)
return r * 2
@property @property
def _r2(self): def diam_connector_external(self):
""" r = NMt.metric_thread_minor_radius(
Radius of inside of connector self.diam_threading,
""" self.thread_pitch)
return self._r1 - self.connector_thickness return r * 2
def segment(self, length: float): def segment(self, length: float):
result = ( result = (
Cq.Workplane() Cq.Workplane()
.cylinder(radius=self.radius, height=length) .cylinder(
radius=self.diam / 2,
height=length)
) )
result.faces("<Z").tag("mate1") result.faces("<Z").tag("mate1")
result.faces(">Z").tag("mate2") result.faces(">Z").tag("mate2")
return result 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): def insertion(self):
""" """
This type of joint is used to connect two handlebar pieces. Each handlebar This type of joint is used to connect two handlebar pieces. Each handlebar
@ -69,20 +99,24 @@ class Handle:
result = ( result = (
Cq.Workplane('XY') Cq.Workplane('XY')
.cylinder( .cylinder(
radius=self.radius_inner, radius=self.diam_inner / 2,
height=self.insertion_length - self.rim_length, height=self.insertion_length - self.rim_length,
centered=[True, True, False]) centered=[True, True, False])
) )
result.faces(">Z").tag("lip") result.faces(">Z").tag("rim")
if self.rim_length > 0:
result = ( result = (
result.faces(">Z") result.faces(">Z")
.workplane() .workplane()
.circle(self.radius) .circle(self.diam / 2)
.extrude(self.rim_length) .extrude(self.rim_length)
.faces(">Z") .faces(">Z")
.hole(2 * self._r1) .hole(self.diam_insertion_internal)
) )
result.faces(">Z").tag("mate") result.faces(">Z").tag("mate")
if not self.simplify_geometry:
thread = self._internal_thread().val()
result = result.union(thread)
return result return result
def connector(self, solid: bool = False): def connector(self, solid: bool = False):
@ -93,29 +127,40 @@ class Handle:
result = ( result = (
Cq.Workplane('XY') Cq.Workplane('XY')
.cylinder( .cylinder(
radius=self.radius, radius=self.diam / 2,
height=self.connector_length, height=self.connector_length,
) )
) )
for (tag, selector) in [("mate1", "<Z"), ("mate2", ">Z")]: for (tag, selector) in [("mate1", "<Z"), ("mate2", ">Z")]:
result.faces(selector).tag(tag) result.faces(selector).tag(tag)
r1 = self.radius_inner
result = ( result = (
result result
.faces(selector) .faces(selector)
.workplane() .workplane()
.circle(self._r1) .circle(self.diam_connector_external / 2)
.extrude(self.insertion_length) .extrude(self.insertion_length)
) )
if not solid: 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 return result
def one_side_connector(self): def one_side_connector(self):
result = ( result = (
Cq.Workplane('XY') Cq.Workplane('XY')
.cylinder( .cylinder(
radius=self.radius, radius=self.diam / 2,
height=self.rim_length, height=self.rim_length,
) )
) )
@ -125,7 +170,7 @@ class Handle:
result result
.faces("<Z") .faces("<Z")
.workplane() .workplane()
.circle(self._r1) .circle(self.diam_connector_external / 2)
.extrude(self.insertion_length) .extrude(self.insertion_length)
) )
return result return result

422
nhf/metric_threads.py Normal file
View File

@ -0,0 +1,422 @@
# Copyright (c) 2020-2024, Nerius Anthony Landys. All rights reserved.
# neri-engineering 'at' protonmail.com
# https://svn.code.sf.net/p/nl10/code/cq-code/common/metric_threads.py
# This file is public domain. Use it for any purpose, including commercial
# applications. Attribution would be nice, but is not required. There is no
# warranty of any kind, including its correctness, usefulness, or safety.
#
# Simple code example to create meshing M3x0.5 threads:
###############################################################################
#
# male = external_metric_thread(3.0, 0.5, 4.0, z_start= -0.85,
# top_lead_in=True)
#
# # Please note that the female thread is meant for a hole which has
# # radius equal to metric_thread_major_radius(3.0, 0.5, internal=True),
# # which is in fact very slightly larger than a 3.0 diameter hole.
#
# female = internal_metric_thread(3.0, 0.5, 1.5,
# bottom_chamfer=True, base_tube_od= 4.5)
#
###############################################################################
# Left hand threads can be created by employing one of the "mirror" operations.
# Thanks for taking the time to understand and use this code!
import math
import cadquery as cq
###############################################################################
# The functions which have names preceded by '__' are not meant to be called
# externally; the remaining functions are written with the intention that they
# will be called by external code. The first section of code consists of
# lightweight helper functions; the meat and potatoes of this library is last.
###############################################################################
# Return value is in degrees, and currently it's fixed at 30. Essentially this
# results in a typical 60 degree equilateral triangle cutting bit for threads.
def metric_thread_angle():
return 30
# Helper func. to make code more intuitive and succinct. Degrees --> 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

View File

@ -2,6 +2,7 @@ import unittest
import cadquery as Cq import cadquery as Cq
import nhf.joints import nhf.joints
import nhf.handle import nhf.handle
import nhf.metric_threads as NMt
class TestJoints(unittest.TestCase): class TestJoints(unittest.TestCase):
@ -20,8 +21,25 @@ class TestHandle(unittest.TestCase):
def test_handle_assembly(self): def test_handle_assembly(self):
h = nhf.handle.Handle() h = nhf.handle.Handle()
h.connector_insertion_assembly() assembly = h.connector_insertion_assembly()
h.connector_one_side_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__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -89,7 +89,15 @@ class Parameters:
wing_r2_height = 100 wing_r2_height = 100
wing_r3_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): def __post_init__(self):
assert self.wing_root_radius > self.hs_joint_radius,\ assert self.wing_root_radius > self.hs_joint_radius,\

View File

@ -15,17 +15,17 @@ def trident_assembly(
.add(handle.insertion(), name="i0", color=mat_i.color) .add(handle.insertion(), name="i0", color=mat_i.color)
.constrain("i0", "Fixed") .constrain("i0", "Fixed")
.add(segment(), name="s1", color=mat_s.color) .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.insertion(), name="i1", color=mat_i.color)
.add(handle.connector(), name="c1", color=mat_i.color) .add(handle.connector(), name="c1", color=mat_i.color)
.add(handle.insertion(), name="i2", 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("i1?mate", "c1?mate1", "Plane")
.constrain("i2?mate", "c1?mate2", "Plane") .constrain("i2?mate", "c1?mate2", "Plane")
.add(segment(), name="s2", color=mat_s.color) .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) .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) .add(handle.one_side_connector(), name="head", color=mat_i.color)
.constrain("i3?mate", "head?mate", "Plane") .constrain("i3?mate", "head?mate", "Plane")
) )