-
-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathgears-dev.py
executable file
·729 lines (635 loc) · 35 KB
/
gears-dev.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
#! /usr/bin/env python
# -*- coding: utf-8 -*-
'''
Copyright (C) 2007 Aaron Spike (aaron @ ekips.org)
Copyright (C) 2007 Tavmjong Bah (tavmjong @ free.fr)
Copyright (C) http://cnc-club.ru/forum/viewtopic.php?f=33&t=434&p=2594#p2500
Copyright (C) 2014 Jürgen Weigert ([email protected])
Copyright (C) 2017 David Grimberg (sentinel @ bardicgrove.org)
Copyright (C) 2020 Spadino (spada.andrea @ gmail DOT com)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2014-03-20 [email protected] 0.2 Option --accuracy=0 for automatic added.
2014-03-21 sent upstream: https://bugs.launchpad.net/inkscape/+bug/1295641
2014-03-21 [email protected] 0.3 Fixed center of rotation for gears with odd number of teeth.
2014-04-04 juewei 0.7 Revamped calc_unit_factor().
2014-04-05 juewei 0.7a Correctly positioned rack gear.
The geometry above the meshing line is wrong.
2014-04-06 juewei 0.7b Undercut detection added. Reference:
http://nptel.ac.in/courses/IIT-MADRAS/Machine_Design_II/pdf/2_2.pdf
Manually merged https://github.com/jnweiger/inkscape-gears-dev/pull/15
2014-04-07 juewei 0.7c Manually merged https://github.com/jnweiger/inkscape-gears-dev/pull/17
2014-04-09 juewei 0.8 Fixed https://github.com/jnweiger/inkscape-gears-dev/issues/19
Ring gears are ready for production now. Thanks neon22 for driving this.
Profile shift implemented (Advanced Tab), fixing
https://github.com/jnweiger/inkscape-gears-dev/issues/9
2015-05-29 juewei 0.9 ported to inkscape 0.91
AttributeError: 'module' object inkex has no attribute 'uutounit
Fixed https://github.com/jnweiger/inkscape-gears-dev
2020-7-4 spadino 1.0 ported to inkscape 1.0
2022-04-04 juewei+ssentinel 1.1 Fixed calc_circular_pitch to use unit_factor freeing the
Tooth system from any specific unitary system. Previously Module was tied
to millimeters while Circular and Diametral Pitch were tied to inches. Now
they take their units from the Unit input just like all the other measures.
'''
import inkex, simplestyle
from os import devnull # for debugging
from math import pi, cos, sin, tan, radians, degrees, ceil, asin, acos, sqrt
from lxml import etree
two_pi = 2 * pi
__version__ = '1.2' # CAUTION: keep in sync with gears-dev.inx:69
def uutounit(self,nn,uu):
try:
return self.svg.uutounit(nn,uu) # inkscape 1.0 <
except:
return inkex.uutounit(nn,uu) # inkscape 0.91 >
def linspace(a,b,n):
""" return list of linear interp of a to b in n steps
- if a and b are ints - you'll get an int result.
- n must be an integer
"""
return [a+x*(b-a)/(n-1) for x in range(0,n)]
def involute_intersect_angle(Rb, R):
" "
Rb, R = float(Rb), float(R)
return (sqrt(R**2 - Rb**2) / (Rb)) - (acos(Rb / R))
def point_on_circle(radius, angle):
" return xy coord of the point at distance radius from origin at angle "
x = radius * cos(angle)
y = radius * sin(angle)
return (x, y)
def points_to_bbox(p):
""" from a list of points (x,y pairs)
- return the lower-left xy and upper-right xy
"""
llx = urx = p[0][0]
lly = ury = p[0][1]
for x in p[1:]:
if x[0] < llx: llx = x[0]
elif x[0] > urx: urx = x[0]
if x[1] < lly: lly = x[1]
elif x[1] > ury: ury = x[1]
return (llx, lly, urx, ury)
def points_to_bbox_center(p):
""" from a list of points (x,y pairs)
- find midpoint of bounding box around all points
- return (x,y)
"""
bbox = points_to_bbox(p)
return ((bbox[0]+bbox[2])/2.0, (bbox[1]+bbox[3])/2.0)
def points_to_svgd(p):
" convert list of points into a closed SVG path list"
f = p[0]
p = p[1:]
svgd = 'M%.4f,%.4f' % f
for x in p:
svgd += 'L%.4f,%.4f' % x
svgd += 'z'
return svgd
def draw_SVG_circle(parent, r, cx, cy, name, style):
" add an SVG circle entity to parent "
circ_attribs = {'style': str(inkex.Style(style)),
'cx': str(cx), 'cy': str(cy),
'r': str(r),
inkex.addNS('label','inkscape'):name}
circle = etree.SubElement(parent, inkex.addNS('circle','svg'), circ_attribs )
### Undercut support functions
def undercut_min_teeth(pitch_angle, k=1.0):
""" computes the minimum tooth count for a
spur gear so that no undercut with the given pitch_angle (in deg)
and an addendum = k * metric_module, where 0 < k < 1
Note:
The return value should be rounded upwards for perfect safety. E.g.
min_teeth = int(math.ceil(undercut_min_teeth(20.0))) # 18, not 17
"""
x = sin(radians(pitch_angle))
return 2*k /(x*x)
def undercut_max_k(teeth, pitch_angle=20.0):
""" computes the maximum k value for a given teeth count and pitch_angle
so that no undercut occurs.
"""
x = sin(radians(pitch_angle))
return 0.5 * teeth * x * x
def undercut_min_angle(teeth, k=1.0):
""" computes the minimum pitch angle, to that the given teeth count (and
profile shift) cause no undercut.
"""
return degrees(asin(min(0.856, sqrt(2.0*k/teeth)))) # max 59.9 deg
def have_undercut(teeth, pitch_angle=20.0, k=1.0):
""" returns true if the specified number of teeth would
cause an undercut.
"""
return (teeth < undercut_min_teeth(pitch_angle, k))
## gather all basic gear calculations in one place
def gear_calculations(num_teeth, circular_pitch, pressure_angle, clearance=0, ring_gear=False, profile_shift=0.):
""" Put base calcs for spur/ring gears in one place.
- negative profile shifting helps against undercut.
"""
diametral_pitch = pi / circular_pitch
pitch_diameter = num_teeth / diametral_pitch
pitch_radius = pitch_diameter / 2.0
addendum = 1 / diametral_pitch
#dedendum = 1.157 / diametral_pitch # auto calc clearance
dedendum = addendum
dedendum *= 1+profile_shift
addendum *= 1-profile_shift
if ring_gear:
addendum = addendum + clearance # our method
else:
dedendum = dedendum + clearance # our method
#
#
base_radius = pitch_diameter * cos(radians(pressure_angle)) / 2.0
outer_radius = pitch_radius + addendum
root_radius = pitch_radius - dedendum
# Tooth thickness: Tooth width along pitch circle.
tooth_thickness = ( pi * pitch_diameter ) / ( 2.0 * num_teeth )
# we don't use these
working_depth = 2 / diametral_pitch
whole_depth = 2.157 / diametral_pitch
#outside_diameter = (num_teeth + 2) / diametral_pitch
#
return (pitch_radius, base_radius,
addendum, dedendum, outer_radius, root_radius,
tooth_thickness
)
def generate_rack_points(tooth_count, pitch, addendum, pressure_angle,
base_height, tab_length, clearance=0, draw_guides=False):
""" Return path (suitable for svg) of the Rack gear.
- rack gear uses straight sides
- involute on a circle of infinite radius is a simple linear ramp
- the meshing circle touches at y = 0,
- the highest elevation of the teeth is at y = +addendum
- the lowest elevation of the teeth is at y = -addendum-clearance
- the base_height extends downwards from the lowest elevation.
- we generate this middle tooth exactly centered on the y=0 line.
(one extra tooth on the right hand side, if number of teeth is even)
"""
spacing = 0.5 * pitch # rolling one pitch distance on the spur gear pitch_diameter.
# roughly center rack in drawing, exact position is so that it meshes
# nicely with the spur gear.
# -0.5*spacing has a gap in the center.
# +0.5*spacing has a tooth in the center.
fudge = +0.5 * spacing
tas = tan(radians(pressure_angle)) * addendum
tasc = tan(radians(pressure_angle)) * (addendum+clearance)
base_top = addendum+clearance
base_bot = addendum+clearance+base_height
x_lhs = -pitch * int(0.5*tooth_count-.5) - spacing - tab_length - tasc + fudge
#inkex.utils.debug("angle=%s spacing=%s"%(pressure_angle, spacing))
# Start with base tab on LHS
points = [] # make list of points
points.append((x_lhs, base_bot))
points.append((x_lhs, base_top))
x = x_lhs + tab_length+tasc
# An involute on a circle of infinite radius is a simple linear ramp.
# We need to add curve at bottom and use clearance.
for i in range(tooth_count):
# move along path, generating the next 'tooth'
# pitch line is at y=0. the left edge hits the pitch line at x
points.append((x-tasc, base_top))
points.append((x+tas, -addendum))
points.append((x+spacing-tas, -addendum))
points.append((x+spacing+tasc, base_top))
x += pitch
x -= spacing # remove last adjustment
# add base on RHS
x_rhs = x+tasc+tab_length
points.append((x_rhs, base_top))
points.append((x_rhs, base_bot))
# We don't close the path here. Caller does it.
# points.append((x_lhs, base_bot))
# Draw line representing the pitch circle of infinite diameter
guide_path = None
if draw_guides:
p = []
p.append( (x_lhs + 0.5 * tab_length, 0) )
p.append( (x_rhs - 0.5 * tab_length, 0) )
guide_path = points_to_svgd(p)
# return points ready for use in an SVG 'path'
return (points, guide_path)
def generate_spur_points(teeth, base_radius, pitch_radius, outer_radius, root_radius, accuracy_involute, accuracy_circular):
""" given a set of core gear params
- generate the svg path for the gear
"""
half_thick_angle = two_pi / (4.0 * teeth ) #?? = pi / (2.0 * teeth)
pitch_to_base_angle = involute_intersect_angle( base_radius, pitch_radius )
pitch_to_outer_angle = involute_intersect_angle( base_radius, outer_radius ) - pitch_to_base_angle
start_involute_radius = max(base_radius, root_radius)
radii = linspace(start_involute_radius, outer_radius, accuracy_involute)
angles = [involute_intersect_angle(base_radius, r) for r in radii]
centers = [(x * two_pi / float( teeth) ) for x in range( teeth ) ]
points = []
for c in centers:
# Angles
pitch1 = c - half_thick_angle
base1 = pitch1 - pitch_to_base_angle
offsetangles1 = [ base1 + x for x in angles]
points1 = [ point_on_circle( radii[i], offsetangles1[i]) for i in range(0,len(radii)) ]
pitch2 = c + half_thick_angle
base2 = pitch2 + pitch_to_base_angle
offsetangles2 = [ base2 - x for x in angles]
points2 = [ point_on_circle( radii[i], offsetangles2[i]) for i in range(0,len(radii)) ]
points_on_outer_radius = [ point_on_circle(outer_radius, x) for x in linspace(offsetangles1[-1], offsetangles2[-1], accuracy_circular) ]
if root_radius > base_radius:
pitch_to_root_angle = pitch_to_base_angle - involute_intersect_angle(base_radius, root_radius )
root1 = pitch1 - pitch_to_root_angle
root2 = pitch2 + pitch_to_root_angle
points_on_root = [point_on_circle (root_radius, x) for x in linspace(root2, root1+(two_pi/float(teeth)), accuracy_circular) ]
p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root[1:-1] # [::-1] reverses list; [1:-1] removes first and last element
else:
points_on_root = [point_on_circle (root_radius, x) for x in linspace(base2, base1+(two_pi/float(teeth)), accuracy_circular) ]
p_tmp = points1 + points_on_outer_radius[1:-1] + points2[::-1] + points_on_root # [::-1] reverses list
points.extend( p_tmp )
return (points)
def generate_spokes_path(root_radius, spoke_width, spoke_count, mount_radius, mount_hole,
unit_factor, unit_label):
""" given a set of constraints
- generate the svg path for the gear spokes
- lies between mount_radius (inner hole) and root_radius (bottom of the teeth)
- spoke width also defines the spacing at the root_radius
- mount_radius is adjusted so that spokes fit if there is room
- if no room (collision) then spokes not drawn
"""
# Spokes
collision = False # assume we draw spokes
messages = [] # messages to send back about changes.
path = ''
r_outer = root_radius - spoke_width
# checks for collision with spokes
# check for mount hole collision with inner spokes
if mount_radius <= mount_hole/2:
adj_factor = (r_outer - mount_hole/2) / 5
if adj_factor < 0.1:
# not enough reasonable room
collision = True
else:
mount_radius = mount_hole/2 + adj_factor # small fix
messages.append("Mount support too small. Auto increased to %2.2f%s." % (mount_radius/unit_factor*2, unit_label))
# then check to see if cross-over on spoke width
if spoke_width * spoke_count +0.5 >= two_pi * mount_radius:
adj_factor = 1.2 # wrong value. its probably one of the points distances calculated below
mount_radius += adj_factor
messages.append("Too many spokes. Increased Mount support by %2.3f%s" % (adj_factor/unit_factor, unit_label))
# check for collision with outer rim
if r_outer <= mount_radius:
# not enough room to draw spokes so cancel
collision = True
if collision: # don't draw spokes if no room.
messages.append("Not enough room for Spokes. Decrease Spoke width.")
else: # draw spokes
for i in range(spoke_count):
points = []
start_a, end_a = i * two_pi / spoke_count, (i+1) * two_pi / spoke_count
# inner circle around mount
asin_factor = spoke_width/mount_radius/2
# check if need to clamp radius
asin_factor = max(-1.0, min(1.0, asin_factor)) # no longer needed - resized above
a = asin(asin_factor)
points += [ point_on_circle(mount_radius, start_a + a), point_on_circle(mount_radius, end_a - a)]
# is inner circle too small
asin_factor = spoke_width/r_outer/2
# check if need to clamp radius
asin_factor = max(-1.0, min(1.0, asin_factor)) # no longer needed - resized above
a = asin(asin_factor)
points += [point_on_circle(r_outer, end_a - a), point_on_circle(r_outer, start_a + a) ]
path += (
"M %f,%f" % points[0] +
"A %f,%f %s %s %s %f,%f" % tuple((mount_radius, mount_radius, 0, 0 if spoke_count!=1 else 1, 1 ) + points[1]) +
"L %f,%f" % points[2] +
"A %f,%f %s %s %s %f,%f" % tuple((r_outer, r_outer, 0, 0 if spoke_count!=1 else 1, 0 ) + points[3]) +
"Z"
)
return (path, messages)
class Gears(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
# an alternate way to get debug info:
# could use inkex.utils.debug(string) instead...
# try:
# self.tty = open("/dev/tty", 'w')
# except:
# self.tty = open(devnull, 'w') # '/dev/null' for POSIX, 'nul' for Windows.
# # print >>self.tty, "gears-dev " + __version__
self.arg_parser.add_argument("-t", "--teeth",
action="store", type=int,
dest="teeth", default=24,
help="Number of teeth")
self.arg_parser.add_argument("-s", "--system",
action="store", type=str,
dest="system", default='CP',
help="Select system: 'CP' (Cyclic Pitch (default)), 'DP' (Diametral Pitch), 'MM' (Metric Module)")
self.arg_parser.add_argument("-d", "--dimension",
action="store", type=float,
dest="dimension", default=1.0,
help="Tooth size, depending on system (which defaults to CP)")
self.arg_parser.add_argument("-a", "--angle",
action="store", type=float,
dest="angle", default=20.0,
help="Pressure Angle (common values: 14.5, 20, 25 degrees)")
self.arg_parser.add_argument("-p", "--profile-shift",
action="store", type=float,
dest="profile_shift", default=20.0,
help="Profile shift [in percent of the module]. Negative values help against undercut")
self.arg_parser.add_argument("-u", "--units",
action="store", type=str,
dest="units", default='mm',
help="Units this dialog is using")
self.arg_parser.add_argument("-A", "--accuracy",
action="store", type=int,
dest="accuracy", default=0,
help="Accuracy of involute: automatic: 5..20 (default), best: 20(default), medium 10, low: 5; good acuracy is important with a low tooth count")
# Clearance: Radial distance between top of tooth on one gear to bottom of gap on another.
self.arg_parser.add_argument("--clearance",
action="store", type=float,
dest="clearance", default=0.0,
help="Clearance between bottom of gap of this gear and top of tooth of another")
self.arg_parser.add_argument("--annotation",
action="store", type=inkex.Boolean,
dest="annotation", default=False,
help="Draw annotation text")
self.arg_parser.add_argument("-i", "--internal-ring",
action="store", type=inkex.Boolean,
dest="internal_ring", default=False,
help="Ring (or Internal) gear style (default: normal spur gear)")
self.arg_parser.add_argument("--mount-hole",
action="store", type=float,
dest="mount_hole", default=5,
help="Mount hole diameter")
self.arg_parser.add_argument("--mount-diameter",
action="store", type=float,
dest="mount_diameter", default=15,
help="Mount support diameter")
self.arg_parser.add_argument("--spoke-count",
action="store", type=int,
dest="spoke_count", default=3,
help="Spokes count")
self.arg_parser.add_argument("--spoke-width",
action="store", type=float,
dest="spoke_width", default=5,
help="Spoke width")
self.arg_parser.add_argument("--holes-rounding",
action="store", type=float,
dest="holes_rounding", default=5,
help="Holes rounding")
self.arg_parser.add_argument("--active-tab",
action="store", type=str,
dest="active_tab", default='',
help="Active tab. Not used now.")
self.arg_parser.add_argument("-x", "--centercross",
action="store", type=inkex.Boolean,
dest="centercross", default=False,
help="Draw cross in center")
self.arg_parser.add_argument("-c", "--pitchcircle",
action="store", type=inkex.Boolean,
dest="pitchcircle", default=False,
help="Draw pitch circle (for mating)")
self.arg_parser.add_argument("-r", "--draw-rack",
action="store", type=inkex.Boolean,
dest="drawrack", default=False,
help="Draw rack gear instead of spur gear")
self.arg_parser.add_argument("--rack-teeth-length",
action="store", type=int,
dest="teeth_length", default=12,
help="Length (in teeth) of rack")
self.arg_parser.add_argument("--rack-base-height",
action="store", type=float,
dest="base_height", default=8,
help="Height of base of rack")
self.arg_parser.add_argument("--rack-base-tab",
action="store", type=float,
dest="base_tab", default=14,
help="Length of tabs on ends of rack")
self.arg_parser.add_argument("--undercut-alert",
action="store", type=inkex.Boolean,
dest="undercut_alert", default=False,
help="Let the user confirm a warning dialog if undercut occurs. This dialog also shows helpful hints against undercut")
def add_text(self, node, text, position, text_height=12):
""" Create and insert a single line of text into the svg under node.
- use 'text' type and label as anootation
- where color is Ponoko Orange - so ignored when lasercutting
"""
line_style = {'font-size': '%dpx' % text_height, 'font-style':'normal', 'font-weight': 'normal',
'fill': '#F6921E', 'font-family': 'Bitstream Vera Sans,sans-serif',
'text-anchor': 'middle', 'text-align': 'center'}
line_attribs = {inkex.addNS('label','inkscape'): 'Annotation',
'style': str(inkex.Style(line_style)),
'x': str(position[0]),
'y': str((position[1] + text_height) * 1.2)
}
line = etree.SubElement(node, inkex.addNS('text','svg'), line_attribs)
line.text = text
def calc_unit_factor(self):
""" return the scale factor for all dimension conversions.
- The document units are always irrelevant as
everything in inkscape is expected to be in 90dpi pixel units
"""
# namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi'))
# doc_units = uutounit(self, 1.0, namedView.get(inkex.addNS('document-units', 'inkscape')))
dialog_units = uutounit(self, 1.0, self.options.units)
unit_factor = 1.0 / dialog_units
return unit_factor
def calc_circular_pitch(self, unit_factor):
""" We use math based on circular pitch.
Expressed in inkscape units which is 90dpi 'pixel' units.
"""
dimension = self.options.dimension
# print >> self.tty, "unit_factor=%s, doc_units=%s, dialog_units=%s (%s), system=%s" % (unit_factor, doc_units, dialog_units, self.options.units, self.options.system)
if self.options.system == 'CP': # circular pitch
circular_pitch = dimension
elif self.options.system == 'DP': # diametral pitch
circular_pitch = pi / dimension
elif self.options.system == 'MM': # module (metric)
circular_pitch = dimension * pi
else:
inkex.utils.debug("unknown system '%s', try CP, DP, MM" % self.options.system)
# circular_pitch defines the size in inches.
# We divide the internal inch factor (px = 90dpi), to remove the inch
# unit.
# The internal inkscape unit is always px,
# it is independent of the doc_units!
return circular_pitch * unit_factor
def effect(self):
""" Calculate Gear factors from inputs.
- Make list of radii, angles, and centers for each tooth and
iterate through them
- Turn on other visual features e.g. cross, rack, annotations, etc
"""
path_stroke = '#000000' # might expose one day
path_fill = 'none' # no fill - just a line
path_stroke_width = 0.6 # might expose one day
path_stroke_light = path_stroke_width * 0.25 # guides are thinner
#
warnings = [] # list of extra messages to be shown in annotations
# calculate unit factor for units defined in dialog.
unit_factor = self.calc_unit_factor()
# User defined options
teeth = self.options.teeth
# Angle of tangent to tooth at circular pitch wrt radial line.
angle = self.options.angle
# Clearance: Radial distance between top of tooth on one gear to
# bottom of gap on another.
clearance = self.options.clearance * unit_factor
mount_hole = self.options.mount_hole * unit_factor
# for spokes
mount_radius = self.options.mount_diameter * 0.5 * unit_factor
spoke_count = self.options.spoke_count
spoke_width = self.options.spoke_width * unit_factor
holes_rounding = self.options.holes_rounding * unit_factor # unused
# visible guide lines
centercross = self.options.centercross # draw center or not (boolean)
pitchcircle = self.options.pitchcircle # draw pitch circle or not (boolean)
# Accuracy of teeth curves
accuracy_involute = 20 # Number of points of the involute curve
accuracy_circular = 9 # Number of points on circular parts
if self.options.accuracy is not None:
if self.options.accuracy == 0:
# automatic
if teeth < 10: accuracy_involute = 20
elif teeth < 30: accuracy_involute = 12
else: accuracy_involute = 6
else:
accuracy_involute = self.options.accuracy
accuracy_circular = max(3, int(accuracy_involute/2) - 1) # never less than three
# print >>self.tty, "accuracy_circular=%s accuracy_involute=%s" % (accuracy_circular, accuracy_involute)
# Pitch (circular pitch): Length of the arc from one tooth to the next)
# Pitch diameter: Diameter of pitch circle.
pitch = self.calc_circular_pitch(unit_factor)
# Replace section below with this call to get the combined gear_calculations() above
(pitch_radius, base_radius, addendum, dedendum,
outer_radius, root_radius, tooth) = gear_calculations(teeth, pitch, angle, clearance, self.options.internal_ring, self.options.profile_shift*0.01)
# Detect Undercut of teeth
## undercut = int(ceil(undercut_min_teeth( angle )))
## needs_undercut = teeth < undercut #? no longer needed ?
if have_undercut(teeth, angle, 1.0):
min_teeth = int(ceil(undercut_min_teeth(angle, 1.0)))
min_angle = undercut_min_angle(teeth, 1.0) + .1
max_k = undercut_max_k(teeth, angle)
msg = "Undercut Warning: This gear (%d teeth) will not work well.\nTry tooth count of %d or more,\nor a pressure angle of %.1f [deg] or more,\nor try a profile shift of %d %%.\nOr other decent combinations." % (teeth, min_teeth, min_angle, int(100. * max_k) - 100.)
# Alas annotation cannot handle the degree symbol. Also it ignore newlines.
# so split and make a list
warnings.extend(msg.split("\n"))
if self.options.undercut_alert:
inkex.utils.debug(msg)
#else:
# print >> self.tty, msg
# All base calcs done. Start building gear
points = generate_spur_points(teeth, base_radius, pitch_radius, outer_radius, root_radius, accuracy_involute, accuracy_circular)
path = points_to_svgd( points )
bbox_center = points_to_bbox_center( points )
# Spokes (add to current path)
if not self.options.internal_ring: # only draw internals if spur gear
spokes_path, msg = generate_spokes_path(root_radius, spoke_width, spoke_count, mount_radius, mount_hole,
unit_factor, self.options.units)
warnings.extend(msg)
path += spokes_path
# Draw mount hole
# A : rx,ry x-axis-rotation, large-arch-flag, sweepflag x,y
r = mount_hole / 2
path += (
"M %f,%f" % (0,r) +
"A %f,%f %s %s %s %f,%f" % (r,r, 0,0,0, 0,-r) +
"A %f,%f %s %s %s %f,%f" % (r,r, 0,0,0, 0,r)
)
else:
# its a ring gear
# which only has an outer ring where width = spoke width
r = outer_radius + spoke_width
path += (
"M %f,%f" % (0,r) +
"A %f,%f %s %s %s %f,%f" % (r,r, 0,0,0, 0,-r) +
"A %f,%f %s %s %s %f,%f" % (r,r, 0,0,0, 0,r)
)
# Embed gear in group to make animation easier:
# Translate group, Rotate path.
t = 'translate(' + str( self.svg.namedview.center[0] ) + ',' + str( self.svg.namedview.center[1] ) + ')'
g_attribs = { inkex.addNS('label','inkscape'):'Gear' + str( teeth ),
inkex.addNS('transform-center-x','inkscape'): str(-bbox_center[0]),
inkex.addNS('transform-center-y','inkscape'): str(-bbox_center[1]),
'transform':t,
'info':'N:'+str(teeth)+'; Pitch:'+ str(pitch) + '; Pressure Angle: '+str(angle) }
# add the group to the current layer
g = etree.SubElement(self.svg.get_current_layer(), 'g', g_attribs )
# Create gear path under top level group
style = { 'stroke': path_stroke, 'fill': path_fill, 'stroke-width': path_stroke_width }
gear_attribs = { 'style': str(inkex.Style(style)), 'd': path }
gear = etree.SubElement(g, inkex.addNS('path','svg'), gear_attribs )
# Add center
if centercross:
style = { 'stroke': path_stroke, 'fill': path_fill, 'stroke-width': path_stroke_light }
cs = str(pitch / 3) # centercross length
d = 'M-'+cs+',0L'+cs+',0M0,-'+cs+'L0,'+cs # 'M-10,0L10,0M0,-10L0,10'
center_attribs = { inkex.addNS('label', 'inkscape'): 'Center cross',
'style': str(inkex.Style(style)), 'd': d }
center = etree.SubElement(g, inkex.addNS('path', 'svg'), center_attribs)
# Add pitch circle (for mating)
if pitchcircle:
style = { 'stroke': path_stroke, 'fill': path_fill, 'stroke-width': path_stroke_light }
draw_SVG_circle(g, pitch_radius, 0, 0, 'Pitch circle', style)
# Add Rack (below)
if self.options.drawrack:
base_height = self.options.base_height * unit_factor
tab_width = self.options.base_tab * unit_factor
tooth_count = self.options.teeth_length
(points, guide_path) = generate_rack_points(tooth_count, pitch, addendum, angle,
base_height, tab_width, clearance, pitchcircle)
path = points_to_svgd(points)
# position below Gear, so that it meshes nicely
# xoff = 0 ## if teeth % 4 == 2.
# xoff = -0.5*pitch ## if teeth % 4 == 0.
# xoff = -0.75*pitch ## if teeth % 4 == 3.
# xoff = -0.25*pitch ## if teeth % 4 == 1.
xoff = (-0.5, -0.25, 0, -0.75)[teeth % 4] * pitch
t = 'translate(' + str( xoff ) + ',' + str( pitch_radius ) + ')'
g_attribs = { inkex.addNS('label', 'inkscape'): 'RackGear' + str(tooth_count),
'transform': t }
rack = etree.SubElement(g, 'g', g_attribs)
# Create SVG Path for gear
style = {'stroke': path_stroke, 'fill': 'none', 'stroke-width': path_stroke_width }
gear_attribs = { 'style': simplestyle.formatStyle(style), 'd': path }
gear = etree.SubElement(
rack, inkex.addNS('path', 'svg'), gear_attribs)
if guide_path is not None:
style2 = { 'stroke': path_stroke, 'fill': 'none', 'stroke-width': path_stroke_light }
gear_attribs2 = { 'style': simplestyle.formatStyle(style2), 'd': guide_path }
gear = etree.SubElement(
rack, inkex.addNS('path', 'svg'), gear_attribs2)
# Add Annotations (above)
if self.options.annotation:
outer_dia = outer_radius * 2
if self.options.internal_ring:
outer_dia += 2 * spoke_width
notes = []
notes.extend(warnings)
#notes.append('Document (%s) scale conversion = %2.4f' % (self.document.getroot().find(inkex.addNS('namedview', 'sodipodi')).get(inkex.addNS('document-units', 'inkscape')), unit_factor))
notes.extend(['Teeth: %d CP: %2.4f(%s) ' % (teeth, pitch / unit_factor, self.options.units),
'DP: %2.3f Module: %2.4f' % (pi / pitch * unit_factor, pitch / pi * 25.4),
'Pressure Angle: %2.2f degrees' % (angle),
'Pitch diameter: %2.3f %s' % (pitch_radius * 2 / unit_factor, self.options.units),
'Outer diameter: %2.3f %s' % (outer_dia / unit_factor, self.options.units),
'Root diameter: %2.3f %s' % (root_radius * 2 / unit_factor, self.options.units),
'Base diameter: %2.3f %s' % (base_radius * 2 / unit_factor, self.options.units)#,
#'Addendum: %2.4f %s' % (addendum / unit_factor, self.options.units),
#'Dedendum: %2.4f %s' % (dedendum / unit_factor, self.options.units)
])
# text height relative to gear size.
# ranges from 10 to 22 over outer radius size 60 to 360
text_height = max(10, min(10+(outer_dia-60)/24, 22))
# position above
y = - outer_radius - (len(notes)+1) * text_height * 1.2
for note in notes:
self.add_text(g, note, [0,y], text_height)
y += text_height * 1.2
if __name__ == '__main__':
e = Gears()
e.run()
# Notes