Skip to content

Commit b85e8fa

Browse files
authored
Merge pull request #15 from ferrency/fix-kerf
Fix kerf
2 parents a49d86a + f6225d0 commit b85e8fa

File tree

2 files changed

+149
-167
lines changed

2 files changed

+149
-167
lines changed

quickjoint.inx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
<item value="in">in</item>
2727
<item value="cm">cm</item>
2828
</param>
29-
<param name="edgefeatures" type="boolean" _gui-text="Features on edges">False</param>
29+
<param name="featureStart" type="boolean" _gui-text="Feature at start">False</param>
30+
<param name="featureEnd" type="boolean" _gui-text="Feature at end">False</param>
3031
<param name="flipside" type="boolean" _gui-text="Flip side">False</param>
3132
<effect needs-live-preview="true">
3233
<object-type>path</object-type>

quickjoint.py

Lines changed: 147 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
2424
'''
2525
import inkex, cmath
26-
from inkex.paths import Path, ZoneClose, Move
26+
from inkex.paths import Path, ZoneClose, Move, Line, line
2727
from lxml import etree
2828

29-
debugEn = False
29+
debugEn = False
3030
def debugMsg(input):
3131
if debugEn:
3232
inkex.utils.debug(input)
@@ -40,9 +40,38 @@ def linesNumber(path):
4040
debugMsg('Number of lines : ' + str(retval))
4141
return retval
4242

43-
def to_complex(point):
44-
return complex(point.x, point.y)
45-
43+
class QuickJointPath (Path):
44+
def Move(self, point):
45+
'''Append an absolute move instruction to the path, to the specified complex point'''
46+
debugMsg("- move: " + str(point))
47+
self.append(Move(point.real, point.imag))
48+
49+
def Line(self, point):
50+
'''Add an absolute line instruction to the path, to the specified complex point'''
51+
debugMsg("- line: " + str(point))
52+
self.append(Line(point.real, point.imag))
53+
54+
def close(self):
55+
'''Add a Close Path instriction to the path'''
56+
self.append(ZoneClose())
57+
58+
def line(self, vector):
59+
'''Append a relative line command to the path, using the specified vector'''
60+
self.append(line(vector.real, vector.imag))
61+
62+
def get_line(self, n):
63+
'''Return the end points of the nth line in the path as complex numbers, as well as whether that line closes the path.'''
64+
65+
start = complex(self[n].x, self[n].y)
66+
# If the next point in the path closes the path, go back to the start.
67+
end = None
68+
closePath = False
69+
if isinstance(self[n+1], ZoneClose):
70+
end = complex(self[0].x, self[0].y)
71+
closePath = True
72+
else:
73+
end = complex(self[n+1].x, self[n+1].y)
74+
return (start, end, closePath)
4675

4776
class QuickJoint(inkex.Effect):
4877
def add_arguments(self, pars):
@@ -52,19 +81,11 @@ def add_arguments(self, pars):
5281
pars.add_argument('-t', '--thickness', type=float, default=3.0, help='Material thickness')
5382
pars.add_argument('-k', '--kerf', type=float, default=0.14, help='Measured kerf of cutter')
5483
pars.add_argument('-u', '--units', default='mm', help='Measurement units')
55-
pars.add_argument('-e', '--edgefeatures', type=inkex.Boolean, default=False, help='Allow tabs to go right to edges')
5684
pars.add_argument('-f', '--flipside', type=inkex.Boolean, default=False, help='Flip side of lines that tabs are drawn onto')
5785
pars.add_argument('-a', '--activetab', default='', help='Tab or slot menus')
86+
pars.add_argument('-S', '--featureStart', type=inkex.Boolean, default=False, help='Tab/slot instead of space on the start edge')
87+
pars.add_argument('-E', '--featureEnd', type=inkex.Boolean, default=False, help='Tab/slot instead of space on the end edge')
5888

59-
def to_complex(self, command, line):
60-
debugMsg('To complex: ' + command + ' ' + str(line))
61-
62-
return complex(line[0], line[1])
63-
64-
def get_length(self, line):
65-
polR, polPhi = cmath.polar(line)
66-
return polR
67-
6889
def draw_parallel(self, start, guideLine, stepDistance):
6990
polR, polPhi = cmath.polar(guideLine)
7091
polR = stepDistance
@@ -81,192 +102,149 @@ def draw_perpendicular(self, start, guideLine, stepDistance, invert = False):
81102
debugMsg(polPhi)
82103
debugMsg(cmath.rect(polR, polPhi))
83104
return (cmath.rect(polR, polPhi) + start)
105+
84106

85-
def draw_box(self, start, guideLine, xDistance, yDistance, kerf):
86-
polR, polPhi = cmath.polar(guideLine)
107+
def draw_box(self, start, lengthVector, height, kerf):
108+
109+
# Kerf is a provided as a positive kerf width. Although tabs
110+
# need to be made larger by the width of the kerf, slots need
111+
# to be made narrower instead, since the cut widens them.
112+
113+
# Calculate kerfed height and length vectors
114+
heightEdge = self.draw_perpendicular(0, lengthVector, height - kerf, self.flipside)
115+
lengthEdge = self.draw_parallel(lengthVector, lengthVector, -kerf)
87116

88-
#Kerf expansion
89-
if self.flipside:
90-
start -= cmath.rect(kerf / 2, polPhi)
91-
start -= cmath.rect(kerf / 2, polPhi + (cmath.pi / 2))
92-
else:
93-
start -= cmath.rect(kerf / 2, polPhi)
94-
start -= cmath.rect(kerf / 2, polPhi - (cmath.pi / 2))
95-
96-
lines = []
97-
lines.append(['M', [start.real, start.imag]])
117+
debugMsg("draw_box; lengthEdge: " + str(lengthEdge) + ", heightEdge: " + str(heightEdge))
98118

99-
#Horizontal
100-
polR = xDistance
101-
move = cmath.rect(polR + kerf, polPhi) + start
102-
lines.append(['L', [move.real, move.imag]])
103-
start = move
119+
cursor = self.draw_parallel(start, lengthEdge, kerf/2)
120+
cursor = self.draw_parallel(cursor, heightEdge, kerf/2)
104121

105-
#Vertical
106-
polR = yDistance
107-
if self.flipside:
108-
polPhi += (cmath.pi / 2)
109-
else:
110-
polPhi -= (cmath.pi / 2)
111-
move = cmath.rect(polR + kerf, polPhi) + start
112-
lines.append(['L', [move.real, move.imag]])
113-
start = move
122+
path = QuickJointPath()
123+
path.Move(cursor)
114124

115-
#Horizontal
116-
polR = xDistance
117-
if self.flipside:
118-
polPhi += (cmath.pi / 2)
119-
else:
120-
polPhi -= (cmath.pi / 2)
121-
move = cmath.rect(polR + kerf, polPhi) + start
122-
lines.append(['L', [move.real, move.imag]])
123-
start = move
125+
cursor += lengthEdge
126+
path.Line(cursor)
124127

125-
lines.append(['Z', []])
128+
cursor += heightEdge
129+
path.Line(cursor)
126130

127-
return lines
128-
129-
def draw_tabs(self, path, line):
130-
#Male tab creation
131-
start = to_complex(path[line])
131+
cursor -= lengthEdge
132+
path.Line(cursor)
132133

133-
closePath = False
134-
#Line is between last and first (closed) nodes
135-
end = None
136-
if isinstance(path[line+1], ZoneClose):
137-
end = to_complex(path[0])
138-
closePath = True
139-
else:
140-
end = to_complex(path[line+1])
141-
142-
debugMsg('start')
143-
debugMsg(start)
144-
debugMsg('end')
145-
debugMsg(end)
146-
147-
debugMsg('5-')
148-
149-
if self.edgefeatures:
150-
segCount = (self.numtabs * 2) - 1
151-
drawValley = False
152-
else:
153-
segCount = (self.numtabs * 2)
154-
drawValley = False
155-
156-
distance = end - start
157-
debugMsg('distance ' + str(distance))
158-
debugMsg('segCount ' + str(segCount))
134+
cursor -= heightEdge
135+
path.Line(cursor)
159136

160-
try:
161-
if self.edgefeatures:
162-
segLength = self.get_length(distance) / segCount
163-
else:
164-
segLength = self.get_length(distance) / (segCount + 1)
165-
except:
166-
debugMsg('in except')
167-
segLength = self.get_length(distance)
137+
path.close()
168138

169-
debugMsg('segLength - ' + str(segLength))
170-
newLines = []
139+
return path
140+
141+
142+
def draw_tabs(self, path, line):
143+
cursor, segCount, segment, closePath = self.get_segments(path, line, self.numtabs)
171144

172-
# when handling firlt line need to set M back
173-
if isinstance(path[line], Move):
174-
newLines.append(['M', [start.real, start.imag]])
145+
# Calculate kerf-compensated vectors for the parallel portion of tab and space
146+
tabLine = self.draw_parallel(segment, segment, self.kerf)
147+
spaceLine = self.draw_parallel(segment, segment, -self.kerf)
148+
endspaceLine = segment
149+
150+
# Calculate vectors for tabOut and tabIn: perpendicular away and towards baseline
151+
tabOut = self.draw_perpendicular(0, segment, self.thickness, not self.flipside)
152+
tabIn = self.draw_perpendicular(0, segment, self.thickness, self.flipside)
175153

176-
if self.edgefeatures == False:
177-
newLines.append(['L', [start.real, start.imag]])
178-
start = self.draw_parallel(start, distance, segLength)
179-
newLines.append(['L', [start.real, start.imag]])
180-
debugMsg('Initial - ' + str(start))
154+
debugMsg("draw_tabs; tabLine=" + str(tabLine) + " spaceLine=" + str(spaceLine) + " segment=" + str(segment))
155+
156+
drawTab = self.featureStart
157+
newLines = QuickJointPath()
158+
159+
# First line is a move or line to our start point
160+
if isinstance(path[line], Move):
161+
newLines.Move(cursor)
162+
else:
163+
newLines.Line(cursor)
181164

182-
183165
for i in range(segCount):
184-
if drawValley == True:
185-
#Vertical
186-
start = self.draw_perpendicular(start, distance, self.thickness, self.flipside)
187-
newLines.append(['L', [start.real, start.imag]])
188-
debugMsg('ValleyV - ' + str(start))
189-
drawValley = False
190-
#Horizontal
191-
start = self.draw_parallel(start, distance, segLength)
192-
newLines.append(['L', [start.real, start.imag]])
193-
debugMsg('ValleyH - ' + str(start))
166+
debugMsg("i = " + str(i))
167+
if drawTab == True:
168+
debugMsg("- tab")
169+
newLines.line(tabOut)
170+
newLines.line(tabLine)
171+
newLines.line(tabIn)
194172
else:
195-
#Vertical
196-
start = self.draw_perpendicular(start, distance, self.thickness, not self.flipside)
197-
newLines.append(['L', [start.real, start.imag]])
198-
debugMsg('HillV - ' + str(start))
199-
drawValley = True
200-
#Horizontal
201-
start = self.draw_parallel(start, distance, segLength)
202-
newLines.append(['L', [start.real, start.imag]])
203-
debugMsg('HillH - ' + str(start))
204-
205-
if self.edgefeatures == True:
206-
start = self.draw_perpendicular(start, distance, self.thickness, self.flipside)
207-
newLines.append(['L', [start.real, start.imag]])
208-
debugMsg('Final - ' + str(start))
209-
173+
if i == 0 or i == segCount - 1:
174+
debugMsg("- endspace")
175+
newLines.line(endspaceLine)
176+
else:
177+
debugMsg("- space")
178+
newLines.line(spaceLine)
179+
drawTab = not drawTab
180+
210181
if closePath:
211-
newLines.append(['Z', []])
182+
newLines.close
212183
return newLines
213184

214-
215-
def draw_slots(self, path):
216-
#Female slot creation
185+
def add_new_path_from_lines(self, lines, line_style):
186+
slot_id = self.svg.get_unique_id('slot')
187+
g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':slot_id})
217188

218-
start = to_complex(path[0])
219-
end = to_complex(path[1])
189+
line_atts = { 'style':line_style, 'id':slot_id+'-inner-close-tab', 'd':str(Path(lines)) }
190+
etree.SubElement(g, inkex.addNS('path','svg'), line_atts )
220191

221-
if self.edgefeatures:
222-
segCount = (self.numslots * 2) - 1
223-
else:
224-
segCount = (self.numslots * 2)
192+
def get_segments(self, path, line, num):
225193

226-
distance = end - start
227-
debugMsg('distance ' + str(distance))
228-
debugMsg('segCount ' + str(segCount))
194+
# Calculate number of segments, including all features and spaces
195+
segCount = num * 2 - 1
196+
if not self.featureStart: segCount = segCount + 1
197+
if not self.featureEnd: segCount = segCount + 1
198+
199+
start, end, closePath = QuickJointPath(path).get_line(line)
229200

230-
try:
231-
if self.edgefeatures:
232-
segLength = self.get_length(distance) / segCount
233-
else:
234-
segLength = self.get_length(distance) / (segCount + 1)
235-
except:
236-
segLength = self.get_length(distance)
201+
# Calculate the length of each feature prior to kerf compensation.
202+
# Here we divide the specified edge into equal portions, one for each feature or space.
203+
204+
# Because the specified edge has no kerf compensation, the
205+
# actual length we end up with will be smaller by a kerf. We
206+
# need to use that distance to calculate our segment vector.
207+
edge = end - start
208+
edge = self.draw_parallel(edge, edge, -self.kerf)
209+
segVector = edge / segCount
237210

238-
debugMsg('segLength - ' + str(segLength))
239-
newLines = []
211+
debugMsg("get_segments; start=" + str(start) + " end=" + str(end) + " edge=" + str(edge) + " segCount=" + str(segCount) + " segVector=" + str(segVector))
240212

213+
return (start, segCount, segVector, closePath)
214+
215+
def draw_slots(self, path):
216+
# Female slot creation
217+
218+
cursor, segCount, segVector, closePath = self.get_segments(path, 0, self.numslots)
219+
220+
# I'm having a really hard time wording why this is necessary, but it is.
221+
# get_segments returns a vector based on a narrower edge; adjust that edge to fit within the edge we were given.
222+
cursor = self.draw_parallel(cursor, segVector, self.kerf/2)
223+
224+
newLines = []
241225
line_style = str(inkex.Style({ 'stroke': '#000000', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('0.1mm')) }))
242-
226+
drawSlot = self.featureStart
227+
243228
for i in range(segCount):
244-
if (self.edgefeatures and (i % 2) == 0) or (not self.edgefeatures and (i % 2)):
245-
newLines = self.draw_box(start, distance, segLength, self.thickness, self.kerf)
246-
debugMsg(newLines)
247-
248-
slot_id = self.svg.get_unique_id('slot')
249-
g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':slot_id})
250-
251-
line_atts = { 'style':line_style, 'id':slot_id+'-inner-close-tab', 'd':str(Path(newLines)) }
252-
etree.SubElement(g, inkex.addNS('path','svg'), line_atts )
253-
254-
#Find next point
255-
polR, polPhi = cmath.polar(distance)
256-
polR = segLength
257-
start = cmath.rect(polR, polPhi) + start
258-
229+
if drawSlot:
230+
self.add_new_path_from_lines(self.draw_box(cursor, segVector, self.thickness, self.kerf), line_style)
231+
cursor = cursor + segVector
232+
drawSlot = not drawSlot
233+
debugMsg("i: " + str(i) + ", cursor: " + str(cursor))
234+
# (We don't modify the path so we don't need to close it)
235+
259236
def effect(self):
260237
self.side = self.options.side
261238
self.numtabs = self.options.numtabs
262239
self.numslots = self.options.numslots
263240
self.thickness = self.svg.unittouu(str(self.options.thickness) + self.options.units)
264241
self.kerf = self.svg.unittouu(str(self.options.kerf) + self.options.units)
265242
self.units = self.options.units
266-
self.edgefeatures = self.options.edgefeatures
243+
self.featureStart = self.options.featureStart
244+
self.featureEnd = self.options.featureEnd
267245
self.flipside = self.options.flipside
268246
self.activetab = self.options.activetab
269-
247+
270248
for id, node in self.svg.selected.items():
271249
debugMsg(node)
272250
debugMsg('1')
@@ -296,5 +274,8 @@ def effect(self):
296274
elif self.activetab == 'slotpage':
297275
newPath = self.draw_slots(p)
298276

277+
278+
279+
299280
if __name__ == '__main__':
300281
QuickJoint().run()

0 commit comments

Comments
 (0)