diff --git a/README.md b/README.md
index f6fdec1..f3534d7 100755
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# TabbedBoxMaker: A free Inkscape extension for generating tab-jointed box patterns
-_version 1.1 - 9 Aug 2021_
+_version 1.2 - 27 Nov 2022_
Original box maker by Elliot White (formerly of twot.eu, domain name now squatted)
@@ -155,3 +155,4 @@ version | Date | Notes
0.99 | (4 June 2020) | Upgraded to support Inkscape v1.0, minor fixes and a tidy up of the parameters dialog layout
1.0 | (17 June 2020) | v1.0 final released: fixes and dogbone added - Mills now supported!
1.1 | (9 Aug 2021) | v1.1 with fixes for newer Inkscape versions - sorry for the delays
+1.2 | (18 Dec 2022) | v1.2 retructure as python package
diff --git a/boxmaker.py b/boxmaker.py
index ebc8f4f..7626b33 100755
--- a/boxmaker.py
+++ b/boxmaker.py
@@ -1,6 +1,6 @@
-#! /usr/bin/env python -t
+#!/usr/bin/env python
'''
-Generates Inkscape SVG file containing box components needed to
+Generates Inkscape SVG file containing box components needed to
CNC (laser/mill) cut a box with tabbed joints taking kerf and clearance into account
Original Tabbed Box Maker Copyright (C) 2011 elliot white
@@ -10,16 +10,16 @@
- Ability to generate 6, 5, 4, 3 or 2-panel cutouts
- Ability to also generate evenly spaced dividers within the box
including tabbed joints to box sides and slots to slot into each other
-
+
23/06/2015 by Paul Hutchison:
- Updated for Inkscape's 0.91 breaking change (unittouu)
-
+
v0.93 - 15/8/2016 by Paul Hutchison:
- Added Hairline option and fixed open box height bug
-
+
v0.94 - 05/01/2017 by Paul Hutchison:
- Added option for keying dividers into walls/floor/none
-
+
v0.95 - 2017-04-20 by Jim McBeath
- Added optional dimples
@@ -58,684 +58,9 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see .
'''
-__version__ = "1.0" ### please report bugs, suggestions etc at https://github.com/paulh-rnd/TabbedBoxMaker ###
-
-import os,sys,inkex,simplestyle,gettext,math
-from copy import deepcopy
-_ = gettext.gettext
-
-linethickness = 1 # default unless overridden by settings
-
-def log(text):
- if 'SCHROFF_LOG' in os.environ:
- f = open(os.environ.get('SCHROFF_LOG'), 'a')
- f.write(text + "\n")
-
-def newGroup(canvas):
- # Create a new group and add element created from line string
- panelId = canvas.svg.get_unique_id('panel')
- group = canvas.svg.get_current_layer().add(inkex.Group(id=panelId))
- return group
-
-def getLine(XYstring):
- line = inkex.PathElement()
- line.style = { 'stroke': '#000000', 'stroke-width' : str(linethickness), 'fill': 'none' }
- line.path = XYstring
- #inkex.etree.SubElement(parent, inkex.addNS('path','svg'), drw)
- return line
-
-# jslee - shamelessly adapted from sample code on below Inkscape wiki page 2015-07-28
-# http://wiki.inkscape.org/wiki/index.php/Generating_objects_from_extensions
-def getCircle(r, c):
- (cx, cy) = c
- log("putting circle at (%d,%d)" % (cx,cy))
- circle = inkex.PathElement.arc((cx, cy), r)
- circle.style = { 'stroke': '#000000', 'stroke-width': str(linethickness), 'fill': 'none' }
-
- # ell_attribs = {'style':simplestyle.formatStyle(style),
- # inkex.addNS('cx','sodipodi') :str(cx),
- # inkex.addNS('cy','sodipodi') :str(cy),
- # inkex.addNS('rx','sodipodi') :str(r),
- # inkex.addNS('ry','sodipodi') :str(r),
- # inkex.addNS('start','sodipodi') :str(0),
- # inkex.addNS('end','sodipodi') :str(2*math.pi),
- # inkex.addNS('open','sodipodi') :'true', #all ellipse sectors we will draw are open
- # inkex.addNS('type','sodipodi') :'arc',
- # 'transform' :'' }
- #inkex.etree.SubElement(parent, inkex.addNS('path','svg'), ell_attribs )
- return circle
-
-def dimpleStr(tabVector,vectorX,vectorY,dirX,dirY,dirxN,diryN,ddir,isTab):
- ds=''
- if not isTab:
- ddir = -ddir
- if dimpleHeight>0 and tabVector!=0:
- if tabVector>0:
- dimpleStart=(tabVector-dimpleLength)/2-dimpleHeight
- tabSgn=1
- else:
- dimpleStart=(tabVector+dimpleLength)/2+dimpleHeight
- tabSgn=-1
- Vxd=vectorX+dirxN*dimpleStart
- Vyd=vectorY+diryN*dimpleStart
- ds+='L '+str(Vxd)+','+str(Vyd)+' '
- Vxd=Vxd+(tabSgn*dirxN-ddir*dirX)*dimpleHeight
- Vyd=Vyd+(tabSgn*diryN-ddir*dirY)*dimpleHeight
- ds+='L '+str(Vxd)+','+str(Vyd)+' '
- Vxd=Vxd+tabSgn*dirxN*dimpleLength
- Vyd=Vyd+tabSgn*diryN*dimpleLength
- ds+='L '+str(Vxd)+','+str(Vyd)+' '
- Vxd=Vxd+(tabSgn*dirxN+ddir*dirX)*dimpleHeight
- Vyd=Vyd+(tabSgn*diryN+ddir*dirY)*dimpleHeight
- ds+='L '+str(Vxd)+','+str(Vyd)+' '
- return ds
-
-def side(group,root,startOffset,endOffset,tabVec,length,direction,isTab,isDivider,numDividers,dividerSpacing):
- rootX, rootY = root
- startOffsetX, startOffsetY = startOffset
- endOffsetX, endOffsetY = endOffset
- dirX, dirY = direction
- notTab=0 if isTab else 1
-
- if (tabSymmetry==1): # waffle-block style rotationally symmetric tabs
- divisions=int((length-2*thickness)/nomTab)
- if divisions%2: divisions+=1 # make divs even
- divisions=float(divisions)
- tabs=divisions/2 # tabs for side
- else:
- divisions=int(length/nomTab)
- if not divisions%2: divisions-=1 # make divs odd
- divisions=float(divisions)
- tabs=(divisions-1)/2 # tabs for side
-
- if (tabSymmetry==1): # waffle-block style rotationally symmetric tabs
- gapWidth=tabWidth=(length-2*thickness)/divisions
- elif equalTabs:
- gapWidth=tabWidth=length/divisions
- else:
- tabWidth=nomTab
- gapWidth=(length-tabs*nomTab)/(divisions-tabs)
-
- if isTab: # kerf correction
- gapWidth-=kerf
- tabWidth+=kerf
- first=halfkerf
- else:
- gapWidth+=kerf
- tabWidth-=kerf
- first=-halfkerf
- firstholelenX=0
- firstholelenY=0
- s=[]
- h=[]
- firstVec=0; secondVec=tabVec
- dividerEdgeOffsetX = dividerEdgeOffsetY = thickness
- notDirX=0 if dirX else 1 # used to select operation on x or y
- notDirY=0 if dirY else 1
- if (tabSymmetry==1):
- dividerEdgeOffsetX = dirX*thickness;
- #dividerEdgeOffsetY = ;
- vectorX = rootX + (startOffsetX*thickness if notDirX else 0)
- vectorY = rootY + (startOffsetY*thickness if notDirY else 0)
- s='M '+str(vectorX)+','+str(vectorY)+' '
- vectorX = rootX+(startOffsetX if startOffsetX else dirX)*thickness
- vectorY = rootY+(startOffsetY if startOffsetY else dirY)*thickness
- if notDirX: endOffsetX=0
- if notDirY: endOffsetY=0
- else:
- (vectorX,vectorY)=(rootX+startOffsetX*thickness,rootY+startOffsetY*thickness)
- dividerEdgeOffsetX=dirY*thickness
- dividerEdgeOffsetY=dirX*thickness
- s='M '+str(vectorX)+','+str(vectorY)+' '
- if notDirX: vectorY=rootY # set correct line start for tab generation
- if notDirY: vectorX=rootX
-
- # generate line as tab or hole using:
- # last co-ord:Vx,Vy ; tab dir:tabVec ; direction:dirx,diry ; thickness:thickness
- # divisions:divs ; gap width:gapWidth ; tab width:tabWidth
-
- for tabDivision in range(1,int(divisions)):
- if ((tabDivision%2) ^ (not isTab)) and numDividers>0 and not isDivider: # draw holes for divider tabs to key into side walls
- w=gapWidth if isTab else tabWidth
- if tabDivision==1 and tabSymmetry==0:
- w-=startOffsetX*thickness
- holeLenX=dirX*w+notDirX*firstVec+first*dirX
- holeLenY=dirY*w+notDirY*firstVec+first*dirY
- if first:
- firstholelenX=holeLenX
- firstholelenY=holeLenY
- for dividerNumber in range(1,int(numDividers)+1):
- Dx=vectorX+-dirY*dividerSpacing*dividerNumber+notDirX*halfkerf+dirX*dogbone*halfkerf-dogbone*first*dirX
- Dy=vectorY+dirX*dividerSpacing*dividerNumber-notDirY*halfkerf+dirY*dogbone*halfkerf-dogbone*first*dirY
- if tabDivision==1 and tabSymmetry==0:
- Dx+=startOffsetX*thickness
- h='M '+str(Dx)+','+str(Dy)+' '
- Dx=Dx+holeLenX
- Dy=Dy+holeLenY
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx+notDirX*(secondVec-kerf)
- Dy=Dy+notDirY*(secondVec+kerf)
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx-holeLenX
- Dy=Dy-holeLenY
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx-notDirX*(secondVec-kerf)
- Dy=Dy-notDirY*(secondVec+kerf)
- h+='L '+str(Dx)+','+str(Dy)+' '
- group.add(getLine(h))
- if tabDivision%2:
- if tabDivision==1 and numDividers>0 and isDivider: # draw slots for dividers to slot into each other
- for dividerNumber in range(1,int(numDividers)+1):
- Dx=vectorX+-dirY*dividerSpacing*dividerNumber-dividerEdgeOffsetX+notDirX*halfkerf
- Dy=vectorY+dirX*dividerSpacing*dividerNumber-dividerEdgeOffsetY+notDirY*halfkerf
- h='M '+str(Dx)+','+str(Dy)+' '
- Dx=Dx+dirX*(first+length/2)
- Dy=Dy+dirY*(first+length/2)
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx+notDirX*(thickness-kerf)
- Dy=Dy+notDirY*(thickness-kerf)
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx-dirX*(first+length/2)
- Dy=Dy-dirY*(first+length/2)
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx-notDirX*(thickness-kerf)
- Dy=Dy-notDirY*(thickness-kerf)
- h+='L '+str(Dx)+','+str(Dy)+' '
- group.add(getLine(h))
- # draw the gap
- vectorX+=dirX*(gapWidth+(isTab&dogbone&1 ^ 0x1)*first+dogbone*kerf*isTab)+notDirX*firstVec
- vectorY+=dirY*(gapWidth+(isTab&dogbone&1 ^ 0x1)*first+dogbone*kerf*isTab)+notDirY*firstVec
- s+='L '+str(vectorX)+','+str(vectorY)+' '
- if dogbone and isTab:
- vectorX-=dirX*halfkerf
- vectorY-=dirY*halfkerf
- s+='L '+str(vectorX)+','+str(vectorY)+' '
- # draw the starting edge of the tab
- s+=dimpleStr(secondVec,vectorX,vectorY,dirX,dirY,notDirX,notDirY,1,isTab)
- vectorX+=notDirX*secondVec
- vectorY+=notDirY*secondVec
- s+='L '+str(vectorX)+','+str(vectorY)+' '
- if dogbone and notTab:
- vectorX-=dirX*halfkerf
- vectorY-=dirY*halfkerf
- s+='L '+str(vectorX)+','+str(vectorY)+' '
-
- else:
- # draw the tab
- vectorX+=dirX*(tabWidth+dogbone*kerf*notTab)+notDirX*firstVec
- vectorY+=dirY*(tabWidth+dogbone*kerf*notTab)+notDirY*firstVec
- s+='L '+str(vectorX)+','+str(vectorY)+' '
- if dogbone and notTab:
- vectorX-=dirX*halfkerf
- vectorY-=dirY*halfkerf
- s+='L '+str(vectorX)+','+str(vectorY)+' '
- # draw the ending edge of the tab
- s+=dimpleStr(secondVec,vectorX,vectorY,dirX,dirY,notDirX,notDirY,-1,isTab)
- vectorX+=notDirX*secondVec
- vectorY+=notDirY*secondVec
- s+='L '+str(vectorX)+','+str(vectorY)+' '
- if dogbone and isTab:
- vectorX-=dirX*halfkerf
- vectorY-=dirY*halfkerf
- s+='L '+str(vectorX)+','+str(vectorY)+' '
- (secondVec,firstVec)=(-secondVec,-firstVec) # swap tab direction
- first=0
-
- #finish the line off
- s+='L '+str(rootX+endOffsetX*thickness+dirX*length)+','+str(rootY+endOffsetY*thickness+dirY*length)+' '
-
- if isTab and numDividers>0 and tabSymmetry==0 and not isDivider: # draw last for divider joints in side walls
- for dividerNumber in range(1,int(numDividers)+1):
- Dx=vectorX+-dirY*dividerSpacing*dividerNumber+notDirX*halfkerf+dirX*dogbone*halfkerf-dogbone*first*dirX
- # Dy=vectorY+dirX*dividerSpacing*dividerNumber-notDirY*halfkerf+dirY*dogbone*halfkerf-dogbone*first*dirY
- # Dx=vectorX+-dirY*dividerSpacing*dividerNumber-dividerEdgeOffsetX+notDirX*halfkerf
- Dy=vectorY+dirX*dividerSpacing*dividerNumber-dividerEdgeOffsetY+notDirY*halfkerf
- h='M '+str(Dx)+','+str(Dy)+' '
- Dx=Dx+firstholelenX
- Dy=Dy+firstholelenY
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx+notDirX*(thickness-kerf)
- Dy=Dy+notDirY*(thickness-kerf)
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx-firstholelenX
- Dy=Dy-firstholelenY
- h+='L '+str(Dx)+','+str(Dy)+' '
- Dx=Dx-notDirX*(thickness-kerf)
- Dy=Dy-notDirY*(thickness-kerf)
- h+='L '+str(Dx)+','+str(Dy)+' '
- group.add(getLine(h))
- # for dividerNumber in range(1,int(numDividers)+1):
- # Dx=vectorX+-dirY*dividerSpacing*dividerNumber+notDirX*halfkerf+dirX*dogbone*halfkerf
- # Dy=vectorY+dirX*dividerSpacing*dividerNumber-notDirY*halfkerf+dirY*dogbone*halfkerf
- # # Dx=vectorX+dirX*dogbone*halfkerf
- # # Dy=vectorY+dirX*dividerSpacing*dividerNumber-dirX*halfkerf+dirY*dogbone*halfkerf
- # h='M '+str(Dx)+','+str(Dy)+' '
- # Dx=rootX+endOffsetX*thickness+dirX*length
- # Dy+=dirY*tabWidth+notDirY*firstVec+first*dirY
- # h+='L '+str(Dx)+','+str(Dy)+' '
- # Dx+=notDirX*(secondVec-kerf)
- # Dy+=notDirY*(secondVec+kerf)
- # h+='L '+str(Dx)+','+str(Dy)+' '
- # Dx-=vectorX
- # Dy-=(dirY*tabWidth+notDirY*firstVec+first*dirY)
- # h+='L '+str(Dx)+','+str(Dy)+' '
- # Dx-=notDirX*(secondVec-kerf)
- # Dy-=notDirY*(secondVec+kerf)
- # h+='L '+str(Dx)+','+str(Dy)+' '
- # group.add(getLine(h))
- group.add(getLine(s))
- return s
-
-
-class BoxMaker(inkex.Effect):
- def __init__(self):
- # Call the base class constructor.
- inkex.Effect.__init__(self)
- # Define options
- self.arg_parser.add_argument('--schroff',action='store',type=int,
- dest='schroff',default=0,help='Enable Schroff mode')
- self.arg_parser.add_argument('--rail_height',action='store',type=float,
- dest='rail_height',default=10.0,help='Height of rail')
- self.arg_parser.add_argument('--rail_mount_depth',action='store',type=float,
- dest='rail_mount_depth',default=17.4,help='Depth at which to place hole for rail mount bolt')
- self.arg_parser.add_argument('--rail_mount_centre_offset',action='store',type=float,
- dest='rail_mount_centre_offset',default=0.0,help='How far toward row centreline to offset rail mount bolt (from rail centreline)')
- self.arg_parser.add_argument('--rows',action='store',type=int,
- dest='rows',default=0,help='Number of Schroff rows')
- self.arg_parser.add_argument('--hp',action='store',type=int,
- dest='hp',default=0,help='Width (TE/HP units) of Schroff rows')
- self.arg_parser.add_argument('--row_spacing',action='store',type=float,
- dest='row_spacing',default=10.0,help='Height of rail')
- self.arg_parser.add_argument('--unit',action='store',type=str,
- dest='unit',default='mm',help='Measure Units')
- self.arg_parser.add_argument('--inside',action='store',type=int,
- dest='inside',default=0,help='Int/Ext Dimension')
- self.arg_parser.add_argument('--length',action='store',type=float,
- dest='length',default=100,help='Length of Box')
- self.arg_parser.add_argument('--width',action='store',type=float,
- dest='width',default=100,help='Width of Box')
- self.arg_parser.add_argument('--depth',action='store',type=float,
- dest='height',default=100,help='Height of Box')
- self.arg_parser.add_argument('--tab',action='store',type=float,
- dest='tab',default=25,help='Nominal Tab Width')
- self.arg_parser.add_argument('--equal',action='store',type=int,
- dest='equal',default=0,help='Equal/Prop Tabs')
- self.arg_parser.add_argument('--tabsymmetry',action='store',type=int,
- dest='tabsymmetry',default=0,help='Tab style')
- self.arg_parser.add_argument('--tabtype',action='store',type=int,
- dest='tabtype',default=0,help='Tab type: regular or dogbone')
- self.arg_parser.add_argument('--dimpleheight',action='store',type=float,
- dest='dimpleheight',default=0,help='Tab Dimple Height')
- self.arg_parser.add_argument('--dimplelength',action='store',type=float,
- dest='dimplelength',default=0,help='Tab Dimple Tip Length')
- self.arg_parser.add_argument('--hairline',action='store',type=int,
- dest='hairline',default=0,help='Line Thickness')
- self.arg_parser.add_argument('--thickness',action='store',type=float,
- dest='thickness',default=10,help='Thickness of Material')
- self.arg_parser.add_argument('--kerf',action='store',type=float,
- dest='kerf',default=0.5,help='Kerf (width of cut)')
- self.arg_parser.add_argument('--style',action='store',type=int,
- dest='style',default=25,help='Layout/Style')
- self.arg_parser.add_argument('--spacing',action='store',type=float,
- dest='spacing',default=25,help='Part Spacing')
- self.arg_parser.add_argument('--boxtype',action='store',type=int,
- dest='boxtype',default=25,help='Box type')
- self.arg_parser.add_argument('--div_l',action='store',type=int,
- dest='div_l',default=25,help='Dividers (Length axis)')
- self.arg_parser.add_argument('--div_w',action='store',type=int,
- dest='div_w',default=25,help='Dividers (Width axis)')
- self.arg_parser.add_argument('--keydiv',action='store',type=int,
- dest='keydiv',default=3,help='Key dividers into walls/floor')
-
- def effect(self):
- global group,nomTab,equalTabs,tabSymmetry,dimpleHeight,dimpleLength,thickness,kerf,halfkerf,dogbone,divx,divy,hairline,linethickness,keydivwalls,keydivfloor
-
- # Get access to main SVG document element and get its dimensions.
- svg = self.document.getroot()
-
- # Get the attributes:
- widthDoc = self.svg.unittouu(svg.get('width'))
- heightDoc = self.svg.unittouu(svg.get('height'))
-
- # Get script's option values.
- hairline=self.options.hairline
- unit=self.options.unit
- inside=self.options.inside
- schroff=self.options.schroff
- kerf = self.svg.unittouu( str(self.options.kerf) + unit )
- halfkerf=kerf/2
-
- # Set the line thickness
- if hairline:
- linethickness=self.svg.unittouu('0.002in')
- else:
- linethickness=1
-
- if schroff:
- rows=self.options.rows
- rail_height=self.svg.unittouu(str(self.options.rail_height)+unit)
- row_centre_spacing=self.svg.unittouu(str(122.5)+unit)
- row_spacing=self.svg.unittouu(str(self.options.row_spacing)+unit)
- rail_mount_depth=self.svg.unittouu(str(self.options.rail_mount_depth)+unit)
- rail_mount_centre_offset=self.svg.unittouu(str(self.options.rail_mount_centre_offset)+unit)
- rail_mount_radius=self.svg.unittouu(str(2.5)+unit)
-
- ## minimally different behaviour for schroffmaker.inx vs. boxmaker.inx
- ## essentially schroffmaker.inx is just an alternate interface with different
- ## default settings, some options removed, and a tiny amount of extra logic
- if schroff:
- ## schroffmaker.inx
- X = self.svg.unittouu(str(self.options.hp * 5.08) + unit)
- # 122.5mm vertical distance between mounting hole centres of 3U Schroff panels
- row_height = rows * (row_centre_spacing + rail_height)
- # rail spacing in between rows but never between rows and case panels
- row_spacing_total = (rows - 1) * row_spacing
- Y = row_height + row_spacing_total
- else:
- ## boxmaker.inx
- X = self.svg.unittouu( str(self.options.length + kerf) + unit )
- Y = self.svg.unittouu( str(self.options.width + kerf) + unit )
-
- Z = self.svg.unittouu( str(self.options.height + kerf) + unit )
- thickness = self.svg.unittouu( str(self.options.thickness) + unit )
- nomTab = self.svg.unittouu( str(self.options.tab) + unit )
- equalTabs=self.options.equal
- tabSymmetry=self.options.tabsymmetry
- dimpleHeight=self.options.dimpleheight
- dimpleLength=self.options.dimplelength
- dogbone = 1 if self.options.tabtype == 1 else 0
- layout=self.options.style
- spacing = self.svg.unittouu( str(self.options.spacing) + unit )
- boxtype = self.options.boxtype
- divx = self.options.div_l
- divy = self.options.div_w
- keydivwalls = 0 if self.options.keydiv == 3 or self.options.keydiv == 1 else 1
- keydivfloor = 0 if self.options.keydiv == 3 or self.options.keydiv == 2 else 1
- initOffsetX=0
- initOffsetY=0
-
- if inside: # if inside dimension selected correct values to outside dimension
- X+=thickness*2
- Y+=thickness*2
- Z+=thickness*2
-
- # check input values mainly to avoid python errors
- # TODO restrict values to *correct* solutions
- # TODO restrict divisions to logical values
- error=0
-
- if min(X,Y,Z)==0:
- inkex.errormsg(_('Error: Dimensions must be non zero'))
- error=1
- if max(X,Y,Z)>max(widthDoc,heightDoc)*10: # crude test
- inkex.errormsg(_('Error: Dimensions Too Large'))
- error=1
- if min(X,Y,Z)<3*nomTab:
- inkex.errormsg(_('Error: Tab size too large'))
- error=1
- if nomTabmin(X,Y,Z)/3: # crude test
- inkex.errormsg(_('Error: Material too thick'))
- error=1
- if kerf>min(X,Y,Z)/3: # crude test
- inkex.errormsg(_('Error: Kerf too large'))
- error=1
- if spacing>max(X,Y,Z)*10: # crude test
- inkex.errormsg(_('Error: Spacing too large'))
- error=1
- if spacing 0=holes 1=tabs
- # tabbed= 0=no tabs 1=tabs on this side
- # (sides: a=top, b=right, c=bottom, d=left)
- # pieceType: 1=XY, 2=XZ, 3=ZY
- tpFace=1
- bmFace=1
- ftFace=2
- bkFace=2
- ltFace=3
- rtFace=3
-
- def reduceOffsets(aa, start, dx, dy, dz):
- for ix in range(start+1,len(aa)):
- (s,x,y,z) = aa[ix]
- aa[ix] = (s-1, x-dx, y-dy, z-dz)
-
- # note first two pieces in each set are the X-divider template and Y-divider template respectively
- pieces=[]
- if layout==1: # Diagramatic Layout
- rr = deepcopy([row0, row1z, row2])
- cc = deepcopy([col0, col1z, col2xz, col3xzz])
- if not hasFt: reduceOffsets(rr, 0, 0, 0, 1) # remove row0, shift others up by Z
- if not hasLt: reduceOffsets(cc, 0, 0, 0, 1)
- if not hasRt: reduceOffsets(cc, 2, 0, 0, 1)
- if hasBk: pieces.append([cc[1], rr[2], X,Z, bkTabInfo, bkTabbed, bkFace])
- if hasLt: pieces.append([cc[0], rr[1], Z,Y, ltTabInfo, ltTabbed, ltFace])
- if hasBm: pieces.append([cc[1], rr[1], X,Y, bmTabInfo, bmTabbed, bmFace])
- if hasRt: pieces.append([cc[2], rr[1], Z,Y, rtTabInfo, rtTabbed, rtFace])
- if hasTp: pieces.append([cc[3], rr[1], X,Y, tpTabInfo, tpTabbed, tpFace])
- if hasFt: pieces.append([cc[1], rr[0], X,Z, ftTabInfo, ftTabbed, ftFace])
- elif layout==2: # 3 Piece Layout
- rr = deepcopy([row0, row1y])
- cc = deepcopy([col0, col1z])
- if hasBk: pieces.append([cc[1], rr[1], X,Z, bkTabInfo, bkTabbed, bkFace])
- if hasLt: pieces.append([cc[0], rr[0], Z,Y, ltTabInfo, ltTabbed, ltFace])
- if hasBm: pieces.append([cc[1], rr[0], X,Y, bmTabInfo, bmTabbed, bmFace])
- elif layout==3: # Inline(compact) Layout
- rr = deepcopy([row0])
- cc = deepcopy([col0, col1x, col2xx, col3xxz, col4, col5])
- if not hasTp: reduceOffsets(cc, 0, 1, 0, 0) # remove col0, shift others left by X
- if not hasBm: reduceOffsets(cc, 1, 1, 0, 0)
- if not hasLt: reduceOffsets(cc, 2, 0, 0, 1)
- if not hasRt: reduceOffsets(cc, 3, 0, 0, 1)
- if not hasBk: reduceOffsets(cc, 4, 1, 0, 0)
- if hasBk: pieces.append([cc[4], rr[0], X,Z, bkTabInfo, bkTabbed, bkFace])
- if hasLt: pieces.append([cc[2], rr[0], Z,Y, ltTabInfo, ltTabbed, ltFace])
- if hasTp: pieces.append([cc[0], rr[0], X,Y, tpTabInfo, tpTabbed, tpFace])
- if hasBm: pieces.append([cc[1], rr[0], X,Y, bmTabInfo, bmTabbed, bmFace])
- if hasRt: pieces.append([cc[3], rr[0], Z,Y, rtTabInfo, rtTabbed, rtFace])
- if hasFt: pieces.append([cc[5], rr[0], X,Z, ftTabInfo, ftTabbed, ftFace])
-
- for idx, piece in enumerate(pieces): # generate and draw each piece of the box
- (xs,xx,xy,xz)=piece[0]
- (ys,yx,yy,yz)=piece[1]
- x=xs*spacing+xx*X+xy*Y+xz*Z+initOffsetX # root x co-ord for piece
- y=ys*spacing+yx*X+yy*Y+yz*Z+initOffsetY # root y co-ord for piece
- dx=piece[2]
- dy=piece[3]
- tabs=piece[4]
- a=tabs>>3&1; b=tabs>>2&1; c=tabs>>1&1; d=tabs&1 # extract tab status for each side
- tabbed=piece[5]
- atabs=tabbed>>3&1; btabs=tabbed>>2&1; ctabs=tabbed>>1&1; dtabs=tabbed&1 # extract tabbed flag for each side
- xspacing=(X-thickness)/(divy+1)
- yspacing=(Y-thickness)/(divx+1)
- xholes = 1 if piece[6]<3 else 0
- yholes = 1 if piece[6]!=2 else 0
- wall = 1 if piece[6]>1 else 0
- floor = 1 if piece[6]==1 else 0
- railholes = 1 if piece[6]==3 else 0
-
- group = newGroup(self)
-
- if schroff and railholes:
- log("rail holes enabled on piece %d at (%d, %d)" % (idx, x+thickness,y+thickness))
- log("abcd = (%d,%d,%d,%d)" % (a,b,c,d))
- log("dxdy = (%d,%d)" % (dx,dy))
- rhxoffset = rail_mount_depth + thickness
- if idx == 1:
- rhx=x+rhxoffset
- elif idx == 3:
- rhx=x-rhxoffset+dx
- else:
- rhx=0
- log("rhxoffset = %d, rhx= %d" % (rhxoffset, rhx))
- rystart=y+(rail_height/2)+thickness
- if rows == 1:
- log("just one row this time, rystart = %d" % rystart)
- rh1y=rystart+rail_mount_centre_offset
- rh2y=rh1y+(row_centre_spacing-rail_mount_centre_offset)
- group.add(getCircle(rail_mount_radius,(rhx,rh1y)))
- group.add(getCircle(rail_mount_radius,(rhx,rh2y)))
- else:
- for n in range(0,rows):
- log("drawing row %d, rystart = %d" % (n+1, rystart))
- # if holes are offset (eg. Vector T-strut rails), they should be offset
- # toward each other, ie. toward the centreline of the Schroff row
- rh1y=rystart+rail_mount_centre_offset
- rh2y=rh1y+row_centre_spacing-rail_mount_centre_offset
- group.add(getCircle(rail_mount_radius,(rhx,rh1y)))
- group.add(getCircle(rail_mount_radius,(rhx,rh2y)))
- rystart+=row_centre_spacing+row_spacing+rail_height
-
- # generate and draw the sides of each piece
- side(group,(x,y),(d,a),(-b,a),atabs * (-thickness if a else thickness),dx,(1,0),a,0,(keydivfloor|wall) * (keydivwalls|floor) * divx*yholes*atabs,yspacing) # side a
- side(group,(x+dx,y),(-b,a),(-b,-c),btabs * (thickness if b else -thickness),dy,(0,1),b,0,(keydivfloor|wall) * (keydivwalls|floor) * divy*xholes*btabs,xspacing) # side b
- if atabs:
- side(group,(x+dx,y+dy),(-b,-c),(d,-c),ctabs * (thickness if c else -thickness),dx,(-1,0),c,0,0,0) # side c
- else:
- side(group,(x+dx,y+dy),(-b,-c),(d,-c),ctabs * (thickness if c else -thickness),dx,(-1,0),c,0,(keydivfloor|wall) * (keydivwalls|floor) * divx*yholes*ctabs,yspacing) # side c
- if btabs:
- side(group,(x,y+dy),(d,-c),(d,a),dtabs * (-thickness if d else thickness),dy,(0,-1),d,0,0,0) # side d
- else:
- side(group,(x,y+dy),(d,-c),(d,a),dtabs * (-thickness if d else thickness),dy,(0,-1),d,0,(keydivfloor|wall) * (keydivwalls|floor) * divy*xholes*dtabs,xspacing) # side d
- if idx==0:
- # remove tabs from dividers if not required
- if not keydivfloor:
- a=c=1
- atabs=ctabs=0
- if not keydivwalls:
- b=d=1
- btabs=dtabs=0
+import tabbedboxmaker.inkex
- y=4*spacing+1*Y+2*Z # root y co-ord for piece
- for n in range(0,divx): # generate X dividers
- group = newGroup(self)
- x=n*(spacing+X) # root x co-ord for piece
- side(group,(x,y),(d,a),(-b,a),keydivfloor*atabs*(-thickness if a else thickness),dx,(1,0),a,1,0,0) # side a
- side(group,(x+dx,y),(-b,a),(-b,-c),keydivwalls*btabs*(thickness if b else -thickness),dy,(0,1),b,1,divy*xholes,xspacing) # side b
- side(group,(x+dx,y+dy),(-b,-c),(d,-c),keydivfloor*ctabs*(thickness if c else -thickness),dx,(-1,0),c,1,0,0) # side c
- side(group,(x,y+dy),(d,-c),(d,a),keydivwalls*dtabs*(-thickness if d else thickness),dy,(0,-1),d,1,0,0) # side d
- elif idx==1:
- y=5*spacing+1*Y+3*Z # root y co-ord for piece
- for n in range(0,divy): # generate Y dividers
- group = newGroup(self)
- x=n*(spacing+Z) # root x co-ord for piece
- side(group,(x,y),(d,a),(-b,a),keydivwalls*atabs*(-thickness if a else thickness),dx,(1,0),a,1,divx*yholes,yspacing) # side a
- side(group,(x+dx,y),(-b,a),(-b,-c),keydivfloor*btabs*(thickness if b else -thickness),dy,(0,1),b,1,0,0) # side b
- side(group,(x+dx,y+dy),(-b,-c),(d,-c),keydivwalls*ctabs*(thickness if c else -thickness),dx,(-1,0),c,1,0,0) # side c
- side(group,(x,y+dy),(d,-c),(d,a),keydivfloor*dtabs*(-thickness if d else thickness),dy,(0,-1),d,1,0,0) # side d
-# Create effect instance and apply it.
-effect = BoxMaker()
+effect = tabbedboxmaker.inkex.InkexBoxMaker()
effect.run()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..1d5f05c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,63 @@
+[project]
+name = "tabbedboxmaker"
+description = 'A free Inkscape extension for generating tab-jointed box patterns'
+readme = "README.md"
+requires-python = ">=3.7"
+license = "GPL-2.0"
+keywords = []
+authors = [
+ {name = "Paul Hutchison"},
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+]
+dependencies = []
+dynamic = ["version"]
+
+[project.urls]
+Documentation = "https://github.com/paulh-rnd/TabbedBoxMaker#readme"
+Issues = "https://github.com/paulh-rnd/TabbedBoxMaker/issues"
+Source = "https://github.com/paulh-rnd/TabbedBoxMaker"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.version]
+path = "tabbedboxmaker/__about__.py"
+
+[tool.hatch.envs.default]
+dependencies = [
+ "inkex",
+ "pytest",
+ "pytest-cov",
+]
+[tool.hatch.envs.default.scripts]
+cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=tabbedboxmaker --cov=tests {args}"
+no-cov = "cov --no-cov {args}"
+tests = "python -munittest discover tests/"
+
+[[tool.hatch.envs.test.matrix]]
+python = ["37", "38", "39", "310", "311"]
+
+[tool.coverage.run]
+branch = true
+parallel = true
+omit = [
+ "tabbedboxmaker/__about__.py",
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "no cov",
+ "if __name__ == .__main__.:",
+ "if TYPE_CHECKING:",
+]
diff --git a/schroffmaker.inx b/schroffmaker.inx
index 09e8c12..0405e2b 100755
--- a/schroffmaker.inx
+++ b/schroffmaker.inx
@@ -26,7 +26,6 @@
3.0
0.1
- 0.01
0
0
diff --git a/tabbedboxmaker/__about__.py b/tabbedboxmaker/__about__.py
new file mode 100644
index 0000000..f5a8e64
--- /dev/null
+++ b/tabbedboxmaker/__about__.py
@@ -0,0 +1,4 @@
+# SPDX-FileCopyrightText: 2022-present Manuel Desbonnet
+#
+# SPDX-License-Identifier: GPL-2.0
+__version__ = '1.2.0'
diff --git a/tabbedboxmaker/__init__.py b/tabbedboxmaker/__init__.py
new file mode 100755
index 0000000..c24718b
--- /dev/null
+++ b/tabbedboxmaker/__init__.py
@@ -0,0 +1,638 @@
+#! /usr/bin/env python -t
+'''
+Generates Inkscape SVG file containing box components needed to
+CNC (laser/mill) cut a box with tabbed joints taking kerf and clearance into account
+
+Original Tabbed Box Maker Copyright (C) 2011 elliot white
+
+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 3 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, see .
+'''
+
+from typing import List, Tuple
+
+import argparse
+import gettext
+import os
+from copy import deepcopy
+_ = gettext.gettext
+
+
+# SVG path reference: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
+
+class AbstractShape(object):
+ pass
+
+
+class PathSegment(object):
+ def __init__(self, segtype: str, *args):
+ self.type = segtype
+ self.args = args
+
+class LineSegment(PathSegment):
+ def __init__(self, toX: float, toY: float) -> None:
+ super().__init__('line', toX, toY)
+
+ def extend_boundingbox(self, boundingbox) -> Tuple[Tuple[float,float], Tuple[float,float]]:
+ # bounding box is a nested 2-tuple:
+ # ( (minx, miny), (maxx, maxy) )
+ sx = self.args[0]
+ sy = self.args[1]
+ return (
+ (min(sx, boundingbox[0][0]), min(sy, boundingbox[0][1])),
+ (max(sx, boundingbox[1][0]), max(sy, boundingbox[1][1])),
+ )
+
+ def translate(self, dx: float, dy: float):
+ self.args = (self.args[0]+dx, self.args[1]+dy)
+
+class Path(AbstractShape):
+ """An abstract path object"""
+ def __init__(self, initial_x: float, initial_y: float):
+ self.initial_x = initial_x
+ self.initial_y = initial_y
+ self.segments = []
+
+ def __repr__(self):
+ return f'{__class__}({self.initial_x}, {self.initial_y}, segments={len(self.segments)})'
+
+ def add(self, seg: PathSegment) -> None:
+ self.segments.append(seg)
+
+ def add_multiple(self, segs: List[PathSegment]) -> None:
+ self.segments.extend(segs)
+
+ def boundingbox(self) -> (float, float, float, float):
+ boundingbox = ((self.initial_x, self.initial_y), (self.initial_x, self.initial_y))
+ for s in self.segments:
+ boundingbox = s.extend_boundingbox(boundingbox)
+ return boundingbox
+
+ def translate(self, dx: float, dy: float):
+ self.initial_x += dx;
+ self.initial_y += dy;
+ for s in self.segments:
+ s.translate(dx, dy)
+
+def default_tab_width(X: float, Y: float, Z: float, thickness: float):
+ """Calculate a default tab length based on the dimensions of the box"""
+
+ # A rather arbitrary value being the smaller of 1/3 the min dimension or
+ # three times the thickness.
+
+ min_dim = min(X, Y, Z)
+ return min(min_dim/3, thickness*3)
+
+
+class TabbedBox(object):
+
+ @staticmethod
+ def add_args(arg_parser: argparse.ArgumentParser) -> None:
+ """Add ArgumentParser args needed to configure a boxmaker run"""
+ arg_parser.add_argument('--unit',action='store',type=str,
+ dest='unit',default='mm',help='Measure Units')
+ arg_parser.add_argument('--inside',action='store',type=int,
+ dest='inside',default=0,help='Int/Ext Dimension')
+ arg_parser.add_argument('--tab',action='store',type=float,
+ dest='tab', help='Nominal Tab Width (default based on box dimensions)')
+ arg_parser.add_argument('--equal',action='store',type=int,
+ dest='equal',default=0,help='Equal/Prop Tabs')
+ arg_parser.add_argument('--tabsymmetry',action='store',type=int,
+ dest='tabsymmetry',default=0,help='Tab style')
+ arg_parser.add_argument('--tabtype',action='store',type=int,
+ dest='tabtype',default=0,help='Tab type: regular or dogbone')
+ arg_parser.add_argument('--dimpleheight',action='store',type=float,
+ dest='dimpleheight',default=0,help='Tab Dimple Height')
+ arg_parser.add_argument('--dimplelength',action='store',type=float,
+ dest='dimplelength',default=0,help='Tab Dimple Tip Length')
+ arg_parser.add_argument('--kerf',action='store',type=float,
+ dest='kerf',default=0.5,help='Kerf (width of cut)')
+ arg_parser.add_argument('--style',action='store',type=int,
+ dest='style',default=1,help='Layout/Style')
+ arg_parser.add_argument('--spacing',action='store',type=float,
+ dest='spacing',default=1,help='Part Spacing')
+ arg_parser.add_argument('--boxtype',action='store',type=int,
+ dest='boxtype',default=1,help='Box type')
+ arg_parser.add_argument('--div_l',action='store',type=int,
+ dest='div_l',default=0,help='Dividers (Length axis)')
+ arg_parser.add_argument('--div_w',action='store',type=int,
+ dest='div_w',default=0,help='Dividers (Width axis)')
+ arg_parser.add_argument('--keydiv',action='store',type=int,
+ dest='keydiv',default=3,help='Key dividers into walls/floor')
+
+ def __init__(self, args: argparse.Namespace=None) -> None:
+
+ if args is not None:
+ self.cfg = args
+ else:
+ self.cfg = argparse.ArgumentParser()
+ self.add_args(self.cfg)
+
+ def dimple(self, tabVector,vectorX,vectorY,dirX,dirY,dirxN,diryN,ddir,isTab) -> List[PathSegment]:
+ segs = []
+ if not isTab:
+ ddir = -ddir
+ if self.cfg.dimpleheight>0 and tabVector!=0:
+ if tabVector>0:
+ dimpleStart=(tabVector-self.cfg.dimplelength)/2-self.cfg.dimpleheight
+ tabSgn=1
+ else:
+ dimpleStart=(tabVector+self.cfg.dimplelength)/2+self.cfg.dimpleheight
+ tabSgn=-1
+ Vxd=vectorX+dirxN*dimpleStart
+ Vyd=vectorY+diryN*dimpleStart
+ segs.append(LineSegment(Vxd, Vyd))
+ Vxd=Vxd+(tabSgn*dirxN-ddir*dirX)*self.cfg.dimpleheight
+ Vyd=Vyd+(tabSgn*diryN-ddir*dirY)*self.cfg.dimpleheight
+ segs.append(LineSegment(Vxd, Vyd))
+ Vxd=Vxd+tabSgn*dirxN*self.cfg.dimplelength
+ Vyd=Vyd+tabSgn*diryN*self.cfg.dimplelength
+ segs.append(LineSegment(Vxd, Vyd))
+ Vxd=Vxd+(tabSgn*dirxN+ddir*dirX)*self.cfg.dimpleheight
+ Vyd=Vyd+(tabSgn*diryN+ddir*dirY)*self.cfg.dimpleheight
+ segs.append(LineSegment(Vxd, Vyd))
+ return segs
+
+ def side(self, thickness, root,startOffset,endOffset,tabVec,length,direction,isTab,isDivider,numDividers,dividerSpacing) -> List[Path]:
+ rootX, rootY = root
+ startOffsetX, startOffsetY = startOffset
+ endOffsetX, endOffsetY = endOffset
+ dirX, dirY = direction
+ notTab=0 if isTab else 1
+
+ halfkerf = self.cfg.kerf/2
+
+ tab_width = self.cfg.tab if self.cfg.tab is not None else self.cfg.default_tab
+
+ if (self.cfg.tabsymmetry==1): # waffle-block style rotationally symmetric tabs
+ divisions=int((length-2*thickness)/tab_width)
+ if divisions%2: divisions+=1 # make divs even
+ divisions=float(divisions)
+ tabs=divisions/2 # tabs for side
+ else:
+ divisions=int(length/tab_width)
+ if not divisions%2: divisions-=1 # make divs odd
+ divisions=float(divisions)
+ tabs=(divisions-1)/2 # tabs for side
+
+ if (self.cfg.tabsymmetry==1): # waffle-block style rotationally symmetric tabs
+ gapWidth=tabWidth=(length-2*thickness)/divisions
+ elif self.cfg.equalTabs:
+ gapWidth=tabWidth=length/divisions
+ else:
+ tabWidth=tab_width
+ gapWidth=(length-tabs*tab_width)/(divisions-tabs)
+
+ if isTab: # kerf correction
+ gapWidth-=self.cfg.kerf
+ tabWidth+=self.cfg.kerf
+ first=halfkerf
+ else:
+ gapWidth+=self.cfg.kerf
+ tabWidth-=self.cfg.kerf
+ first=-halfkerf
+ firstholelenX=0
+ firstholelenY=0
+ firstVec=0; secondVec=tabVec
+ dividerEdgeOffsetX = dividerEdgeOffsetY = thickness
+ notDirX=0 if dirX else 1 # used to select operation on x or y
+ notDirY=0 if dirY else 1
+ paths = []
+ p = None
+ if (self.cfg.tabsymmetry==1):
+ dividerEdgeOffsetX = dirX*thickness;
+ #dividerEdgeOffsetY = ;
+ vectorX = rootX + (startOffsetX*thickness if notDirX else 0)
+ vectorY = rootY + (startOffsetY*thickness if notDirY else 0)
+ p = Path(vectorX, vectorY)
+ vectorX = rootX+(startOffsetX if startOffsetX else dirX)*thickness
+ vectorY = rootY+(startOffsetY if startOffsetY else dirY)*thickness
+ if notDirX: endOffsetX=0
+ if notDirY: endOffsetY=0
+ else:
+ (vectorX,vectorY)=(rootX+startOffsetX*thickness,rootY+startOffsetY*thickness)
+ dividerEdgeOffsetX=dirY*thickness
+ dividerEdgeOffsetY=dirX*thickness
+ p = Path(vectorX, vectorY)
+ if notDirX: vectorY=rootY # set correct line start for tab generation
+ if notDirY: vectorX=rootX
+
+ # generate line as tab or hole using:
+ # last co-ord:Vx,Vy ; tab dir:tabVec ; direction:dirx,diry ; thickness:thickness
+ # divisions:divs ; gap width:gapWidth ; tab width:tabWidth
+
+ for tabDivision in range(1,int(divisions)):
+ if ((tabDivision%2) ^ (not isTab)) and numDividers>0 and not isDivider: # draw holes for divider tabs to key into side walls
+ w=gapWidth if isTab else tabWidth
+ if tabDivision==1 and self.cfg.tabsymmetry==0:
+ w-=startOffsetX*thickness
+ holeLenX=dirX*w+notDirX*firstVec+first*dirX
+ holeLenY=dirY*w+notDirY*firstVec+first*dirY
+ if first:
+ firstholelenX=holeLenX
+ firstholelenY=holeLenY
+ for dividerNumber in range(1,int(numDividers)+1):
+ Dx=vectorX+-dirY*dividerSpacing*dividerNumber+notDirX*halfkerf+dirX*self.cfg.dogbone*halfkerf-self.cfg.dogbone*first*dirX
+ Dy=vectorY+dirX*dividerSpacing*dividerNumber-notDirY*halfkerf+dirY*self.cfg.dogbone*halfkerf-self.cfg.dogbone*first*dirY
+ if tabDivision==1 and self.cfg.tabsymmetry==0:
+ Dx+=startOffsetX*thickness
+ hole = Path(Dx, Dy)
+ Dx=Dx+holeLenX
+ Dy=Dy+holeLenY
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx+notDirX*(secondVec-self.cfg.kerf)
+ Dy=Dy+notDirY*(secondVec+self.cfg.kerf)
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx-holeLenX
+ Dy=Dy-holeLenY
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx-notDirX*(secondVec-self.cfg.kerf)
+ Dy=Dy-notDirY*(secondVec+self.cfg.kerf)
+ hole.add(LineSegment(Dx, Dy))
+ paths.append(hole)
+ if tabDivision%2:
+ if tabDivision==1 and numDividers>0 and isDivider: # draw slots for dividers to slot into each other
+ for dividerNumber in range(1,int(numDividers)+1):
+ Dx=vectorX+-dirY*dividerSpacing*dividerNumber-dividerEdgeOffsetX+notDirX*halfkerf
+ Dy=vectorY+dirX*dividerSpacing*dividerNumber-dividerEdgeOffsetY+notDirY*halfkerf
+ hole = Path(Dx, Dy)
+ Dx=Dx+dirX*(first+length/2)
+ Dy=Dy+dirY*(first+length/2)
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx+notDirX*(thickness-self.cfg.kerf)
+ Dy=Dy+notDirY*(thickness-self.cfg.kerf)
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx-dirX*(first+length/2)
+ Dy=Dy-dirY*(first+length/2)
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx-notDirX*(thickness-self.cfg.kerf)
+ Dy=Dy-notDirY*(thickness-self.cfg.kerf)
+ hole.add(LineSegment(Dx, Dy))
+ paths.append(hole)
+ # draw the gap
+ vectorX+=dirX*(gapWidth+(isTab&self.cfg.dogbone&1 ^ 0x1)*first+self.cfg.dogbone*self.cfg.kerf*isTab)+notDirX*firstVec
+ vectorY+=dirY*(gapWidth+(isTab&self.cfg.dogbone&1 ^ 0x1)*first+self.cfg.dogbone*self.cfg.kerf*isTab)+notDirY*firstVec
+ p.add(LineSegment(vectorX, vectorY))
+ if self.cfg.dogbone and isTab:
+ vectorX-=dirX*halfkerf
+ vectorY-=dirY*halfkerf
+ p.add(LineSegment(vectorX, vectorY))
+ # draw the starting edge of the tab
+ p.add_multiple(self.dimple(secondVec,vectorX,vectorY,dirX,dirY,notDirX,notDirY,1,isTab))
+ vectorX+=notDirX*secondVec
+ vectorY+=notDirY*secondVec
+ p.add(LineSegment(vectorX, vectorY))
+ if self.cfg.dogbone and notTab:
+ vectorX-=dirX*halfkerf
+ vectorY-=dirY*halfkerf
+ p.add(LineSegment(vectorX, vectorY))
+ else:
+ # draw the tab
+ vectorX+=dirX*(tabWidth+self.cfg.dogbone*self.cfg.kerf*notTab)+notDirX*firstVec
+ vectorY+=dirY*(tabWidth+self.cfg.dogbone*self.cfg.kerf*notTab)+notDirY*firstVec
+ p.add(LineSegment(vectorX, vectorY))
+ if self.cfg.dogbone and notTab:
+ vectorX-=dirX*halfkerf
+ vectorY-=dirY*halfkerf
+ p.add(LineSegment(vectorX, vectorY))
+ # draw the ending edge of the tab
+ p.add_multiple(self.dimple(secondVec,vectorX,vectorY,dirX,dirY,notDirX,notDirY,-1,isTab))
+ vectorX+=notDirX*secondVec
+ vectorY+=notDirY*secondVec
+ p.add(LineSegment(vectorX, vectorY))
+ if self.cfg.dogbone and isTab:
+ vectorX-=dirX*halfkerf
+ vectorY-=dirY*halfkerf
+ p.add(LineSegment(vectorX, vectorY))
+ (secondVec,firstVec)=(-secondVec,-firstVec) # swap tab direction
+ first=0
+
+ #finish the line off
+ p.add(LineSegment(rootX+endOffsetX*thickness+dirX*length, rootY+endOffsetY*thickness+dirY*length))
+
+ if isTab and numDividers>0 and self.cfg.tabsymmetry==0 and not isDivider: # draw last for divider joints in side walls
+ for dividerNumber in range(1,int(numDividers)+1):
+ Dx=vectorX+-dirY*dividerSpacing*dividerNumber+notDirX*halfkerf+dirX*self.cfg.dogbone*halfkerf-self.cfg.dogbone*first*dirX
+ # Dy=vectorY+dirX*dividerSpacing*dividerNumber-notDirY*halfkerf+dirY*dogbone*halfkerf-dogbone*first*dirY
+ # Dx=vectorX+-dirY*dividerSpacing*dividerNumber-dividerEdgeOffsetX+notDirX*halfkerf
+ Dy=vectorY+dirX*dividerSpacing*dividerNumber-dividerEdgeOffsetY+notDirY*halfkerf
+ hole = Path(Dx, Dy)
+ Dx=Dx+firstholelenX
+ Dy=Dy+firstholelenY
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx+notDirX*(thickness-self.cfg.kerf)
+ Dy=Dy+notDirY*(thickness-self.cfg.kerf)
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx-firstholelenX
+ Dy=Dy-firstholelenY
+ hole.add(LineSegment(Dx, Dy))
+ Dx=Dx-notDirX*(thickness-self.cfg.kerf)
+ Dy=Dy-notDirY*(thickness-self.cfg.kerf)
+ hole.add(LineSegment(Dx, Dy))
+ paths.append(hole)
+ # for dividerNumber in range(1,int(numDividers)+1):
+ # Dx=vectorX+-dirY*dividerSpacing*dividerNumber+notDirX*halfkerf+dirX*dogbone*halfkerf
+ # Dy=vectorY+dirX*dividerSpacing*dividerNumber-notDirY*halfkerf+dirY*dogbone*halfkerf
+ # # Dx=vectorX+dirX*dogbone*halfkerf
+ # # Dy=vectorY+dirX*dividerSpacing*dividerNumber-dirX*halfkerf+dirY*dogbone*halfkerf
+ # h='M '+str(Dx)+','+str(Dy)+' '
+ # Dx=rootX+endOffsetX*thickness+dirX*length
+ # Dy+=dirY*tabWidth+notDirY*firstVec+first*dirY
+ # h+='L '+str(Dx)+','+str(Dy)+' '
+ # Dx+=notDirX*(secondVec-kerf)
+ # Dy+=notDirY*(secondVec+kerf)
+ # h+='L '+str(Dx)+','+str(Dy)+' '
+ # Dx-=vectorX
+ # Dy-=(dirY*tabWidth+notDirY*firstVec+first*dirY)
+ # h+='L '+str(Dx)+','+str(Dy)+' '
+ # Dx-=notDirX*(secondVec-kerf)
+ # Dy-=notDirY*(secondVec+kerf)
+ # h+='L '+str(Dx)+','+str(Dy)+' '
+ # group.add(getLine(h))
+ paths.append(p)
+ return paths
+
+ def make(self, X: float, Y: float, Z: float, thickness: float) -> List[List[Path]]:
+ # For code spacing consistency, we use two-character abbreviations for the six box faces,
+ # where each abbreviation is the first and last letter of the face name:
+ # tp=top, bm=bottom, ft=front, bk=back, lt=left, rt=right
+
+ if self.cfg.inside: # if inside dimension selected correct values to outside dimension
+ X+=thickness*2
+ Y+=thickness*2
+ Z+=thickness*2
+
+ self.cfg.default_tab = default_tab_width(X, Y, Z, thickness)
+
+ # Some internally generated cfg - mostly alternative names for better clarity
+ self.cfg.dogbone = self.cfg.tabtype
+ self.cfg.equalTabs = self.cfg.equal
+ self.cfg.divx = self.cfg.div_l
+ self.cfg.divy = self.cfg.div_w
+ self.cfg.keydivwalls = 0 if (self.cfg.keydiv == 3 or self.cfg.keydiv == 1) else 1
+ self.cfg.keydivfloor = 0 if (self.cfg.keydiv == 3 or self.cfg.keydiv == 2) else 1
+
+
+ # Determine which faces the box has based on the box type
+ hasTp=hasBm=hasFt=hasBk=hasLt=hasRt = True
+ if self.cfg.boxtype==2: hasTp=False
+ elif self.cfg.boxtype==3: hasTp=hasFt=False
+ elif self.cfg.boxtype==4: hasTp=hasFt=hasRt=False
+ elif self.cfg.boxtype==5: hasTp=hasBm=False
+ elif self.cfg.boxtype==6: hasTp=hasFt=hasBk=hasRt=False
+ # else self.cfg.boxtype==1, full box, has all sides
+
+ # Determine where the tabs go based on the tab style
+ if self.cfg.tabsymmetry==2: # Antisymmetric (deprecated)
+ tpTabInfo=0b0110
+ bmTabInfo=0b1100
+ ltTabInfo=0b1100
+ rtTabInfo=0b0110
+ ftTabInfo=0b1100
+ bkTabInfo=0b1001
+ elif self.cfg.tabsymmetry==1: # Rotationally symmetric (Waffle-blocks)
+ tpTabInfo=0b1111
+ bmTabInfo=0b1111
+ ltTabInfo=0b1111
+ rtTabInfo=0b1111
+ ftTabInfo=0b1111
+ bkTabInfo=0b1111
+ else: # XY symmetric
+ tpTabInfo=0b0000
+ bmTabInfo=0b0000
+ ltTabInfo=0b1111
+ rtTabInfo=0b1111
+ ftTabInfo=0b1010
+ bkTabInfo=0b1010
+
+ def fixTabBits(tabbed, tabInfo, bit):
+ newTabbed = tabbed & ~bit
+ if self.cfg.inside:
+ newTabInfo = tabInfo | bit # set bit to 1 to use tab base line
+ else:
+ newTabInfo = tabInfo & ~bit # set bit to 0 to use tab tip line
+ return newTabbed, newTabInfo
+
+ # Update the tab bits based on which sides of the box don't exist
+ tpTabbed=bmTabbed=ltTabbed=rtTabbed=ftTabbed=bkTabbed=0b1111
+ if not hasTp:
+ bkTabbed, bkTabInfo = fixTabBits(bkTabbed, bkTabInfo, 0b0010)
+ ftTabbed, ftTabInfo = fixTabBits(ftTabbed, ftTabInfo, 0b1000)
+ ltTabbed, ltTabInfo = fixTabBits(ltTabbed, ltTabInfo, 0b0001)
+ rtTabbed, rtTabInfo = fixTabBits(rtTabbed, rtTabInfo, 0b0100)
+ tpTabbed=0
+ if not hasBm:
+ bkTabbed, bkTabInfo = fixTabBits(bkTabbed, bkTabInfo, 0b1000)
+ ftTabbed, ftTabInfo = fixTabBits(ftTabbed, ftTabInfo, 0b0010)
+ ltTabbed, ltTabInfo = fixTabBits(ltTabbed, ltTabInfo, 0b0100)
+ rtTabbed, rtTabInfo = fixTabBits(rtTabbed, rtTabInfo, 0b0001)
+ bmTabbed=0
+ if not hasFt:
+ tpTabbed, tpTabInfo = fixTabBits(tpTabbed, tpTabInfo, 0b1000)
+ bmTabbed, bmTabInfo = fixTabBits(bmTabbed, bmTabInfo, 0b1000)
+ ltTabbed, ltTabInfo = fixTabBits(ltTabbed, ltTabInfo, 0b1000)
+ rtTabbed, rtTabInfo = fixTabBits(rtTabbed, rtTabInfo, 0b1000)
+ ftTabbed=0
+ if not hasBk:
+ tpTabbed, tpTabInfo = fixTabBits(tpTabbed, tpTabInfo, 0b0010)
+ bmTabbed, bmTabInfo = fixTabBits(bmTabbed, bmTabInfo, 0b0010)
+ ltTabbed, ltTabInfo = fixTabBits(ltTabbed, ltTabInfo, 0b0010)
+ rtTabbed, rtTabInfo = fixTabBits(rtTabbed, rtTabInfo, 0b0010)
+ bkTabbed=0
+ if not hasLt:
+ tpTabbed, tpTabInfo = fixTabBits(tpTabbed, tpTabInfo, 0b0100)
+ bmTabbed, bmTabInfo = fixTabBits(bmTabbed, bmTabInfo, 0b0001)
+ bkTabbed, bkTabInfo = fixTabBits(bkTabbed, bkTabInfo, 0b0001)
+ ftTabbed, ftTabInfo = fixTabBits(ftTabbed, ftTabInfo, 0b0001)
+ ltTabbed=0
+ if not hasRt:
+ tpTabbed, tpTabInfo = fixTabBits(tpTabbed, tpTabInfo, 0b0001)
+ bmTabbed, bmTabInfo = fixTabBits(bmTabbed, bmTabInfo, 0b0100)
+ bkTabbed, bkTabInfo = fixTabBits(bkTabbed, bkTabInfo, 0b0100)
+ ftTabbed, ftTabInfo = fixTabBits(ftTabbed, ftTabInfo, 0b0100)
+ rtTabbed=0
+
+ # Layout positions are specified in a grid of rows and columns
+ row0=(1,0,0,0) # top row
+ row1y=(2,0,1,0) # second row, offset by Y
+ row1z=(2,0,0,1) # second row, offset by Z
+ row2=(3,0,1,1) # third row, always offset by Y+Z
+
+ col0=(1,0,0,0) # left column
+ col1x=(2,1,0,0) # second column, offset by X
+ col1z=(2,0,0,1) # second column, offset by Z
+ col2xx=(3,2,0,0) # third column, offset by 2*X
+ col2xz=(3,1,0,1) # third column, offset by X+Z
+ col3xzz=(4,1,0,2) # fourth column, offset by X+2*Z
+ col3xxz=(4,2,0,1) # fourth column, offset by 2*X+Z
+ col4=(5,2,0,2) # fifth column, always offset by 2*X+2*Z
+ col5=(6,3,0,2) # sixth column, always offset by 3*X+2*Z
+
+ # layout format:(rootx),(rooty),Xlength,Ylength,tabInfo,tabbed,pieceType
+ # root= (spacing,X,Y,Z) * values in tuple
+ # tabInfo= 0=holes 1=tabs
+ # tabbed= 0=no tabs 1=tabs on this side
+ # (sides: a=top, b=right, c=bottom, d=left)
+ # pieceType: 1=XY, 2=XZ, 3=ZY
+ tpFace=1
+ bmFace=1
+ ftFace=2
+ bkFace=2
+ ltFace=3
+ rtFace=3
+
+ def reduceOffsets(aa, start, dx, dy, dz):
+ for ix in range(start+1,len(aa)):
+ (s,x,y,z) = aa[ix]
+ aa[ix] = (s-1, x-dx, y-dy, z-dz)
+
+ # note first two pieces in each set are the X-divider template and Y-divider template respectively
+ pieces=[]
+ if self.cfg.style==1: # Diagramatic Layout
+ rr = deepcopy([row0, row1z, row2])
+ cc = deepcopy([col0, col1z, col2xz, col3xzz])
+ if not hasFt: reduceOffsets(rr, 0, 0, 0, 1) # remove row0, shift others up by Z
+ if not hasLt: reduceOffsets(cc, 0, 0, 0, 1)
+ if not hasRt: reduceOffsets(cc, 2, 0, 0, 1)
+ if hasBk: pieces.append([cc[1], rr[2], X,Z, bkTabInfo, bkTabbed, bkFace])
+ if hasLt: pieces.append([cc[0], rr[1], Z,Y, ltTabInfo, ltTabbed, ltFace])
+ if hasBm: pieces.append([cc[1], rr[1], X,Y, bmTabInfo, bmTabbed, bmFace])
+ if hasRt: pieces.append([cc[2], rr[1], Z,Y, rtTabInfo, rtTabbed, rtFace])
+ if hasTp: pieces.append([cc[3], rr[1], X,Y, tpTabInfo, tpTabbed, tpFace])
+ if hasFt: pieces.append([cc[1], rr[0], X,Z, ftTabInfo, ftTabbed, ftFace])
+ elif self.cfg.style==2: # 3 Piece Layout
+ rr = deepcopy([row0, row1y])
+ cc = deepcopy([col0, col1z])
+ if hasBk: pieces.append([cc[1], rr[1], X,Z, bkTabInfo, bkTabbed, bkFace])
+ if hasLt: pieces.append([cc[0], rr[0], Z,Y, ltTabInfo, ltTabbed, ltFace])
+ if hasBm: pieces.append([cc[1], rr[0], X,Y, bmTabInfo, bmTabbed, bmFace])
+ elif self.cfg.style==3: # Inline(compact) Layout
+ rr = deepcopy([row0])
+ cc = deepcopy([col0, col1x, col2xx, col3xxz, col4, col5])
+ if not hasTp: reduceOffsets(cc, 0, 1, 0, 0) # remove col0, shift others left by X
+ if not hasBm: reduceOffsets(cc, 1, 1, 0, 0)
+ if not hasLt: reduceOffsets(cc, 2, 0, 0, 1)
+ if not hasRt: reduceOffsets(cc, 3, 0, 0, 1)
+ if not hasBk: reduceOffsets(cc, 4, 1, 0, 0)
+ if hasBk: pieces.append([cc[4], rr[0], X,Z, bkTabInfo, bkTabbed, bkFace])
+ if hasLt: pieces.append([cc[2], rr[0], Z,Y, ltTabInfo, ltTabbed, ltFace])
+ if hasTp: pieces.append([cc[0], rr[0], X,Y, tpTabInfo, tpTabbed, tpFace])
+ if hasBm: pieces.append([cc[1], rr[0], X,Y, bmTabInfo, bmTabbed, bmFace])
+ if hasRt: pieces.append([cc[3], rr[0], Z,Y, rtTabInfo, rtTabbed, rtFace])
+ if hasFt: pieces.append([cc[5], rr[0], X,Z, ftTabInfo, ftTabbed, ftFace])
+ groups = []
+ for idx, piece in enumerate(pieces): # generate and draw each piece of the box
+ groups.extend(self.piece(X, Y, Z, thickness, idx, *piece))
+ return groups
+
+ def piece(
+ self, X: float, Y: float, Z: float, thickness: float, idx: int,
+ rootx: List[float], rooty: List[float], dx: float, dy: float,
+ tabs: int, tabbed: int, pieceType: int,
+ ):
+
+ (xs,xx,xy,xz)=rootx
+ (ys,yx,yy,yz)=rooty
+
+ initOffsetX=0 # TODO: These look redundant, remove?
+ initOffsetY=0
+
+ x=xs*self.cfg.spacing+xx*X+xy*Y+xz*Z+initOffsetX # root x co-ord for piece
+ y=ys*self.cfg.spacing+yx*X+yy*Y+yz*Z+initOffsetY # root y co-ord for piece
+ a=tabs>>3&1; b=tabs>>2&1; c=tabs>>1&1; d=tabs&1 # extract tab status for each side
+ atabs=tabbed>>3&1; btabs=tabbed>>2&1; ctabs=tabbed>>1&1; dtabs=tabbed&1 # extract tabbed flag for each side
+ xspacing=(X-thickness)/(self.cfg.divy+1)
+ yspacing=(Y-thickness)/(self.cfg.divx+1)
+ xholes = 1 if pieceType<3 else 0
+ yholes = 1 if pieceType!=2 else 0
+ wall = 1 if pieceType>1 else 0
+ floor = 1 if pieceType==1 else 0
+
+ groups = []
+ sides = []
+ groups.append(sides)
+
+ # generate and draw the sides of each piece
+ sides.extend( # side a
+ self.side(thickness, (x,y), (d,a), (-b,a), atabs * (-thickness if a else thickness),
+ dx, (1,0), a, 0,
+ (self.cfg.keydivfloor|wall) * (self.cfg.keydivwalls|floor) * self.cfg.divx*yholes*atabs,
+ yspacing)
+ )
+ sides.extend(
+ self.side(thickness, (x+dx,y),(-b,a),(-b,-c),btabs * (thickness if b else -thickness),dy,(0,1),b,0,(self.cfg.keydivfloor|wall) * (self.cfg.keydivwalls|floor) * self.cfg.divy*xholes*btabs,xspacing) # side b
+ )
+ if atabs:
+ sides.extend(
+ self.side(thickness, (x+dx,y+dy),(-b,-c),(d,-c),ctabs * (thickness if c else -thickness),dx,(-1,0),c,0,0,0) # side c
+ )
+ else:
+ sides.extend(
+ self.side(thickness, (x+dx,y+dy),(-b,-c),(d,-c),ctabs * (thickness if c else -thickness),dx,(-1,0),c,0,(self.cfg.keydivfloor|wall) * (self.cfg.keydivwalls|floor) * self.cfg.divx*yholes*ctabs,yspacing) # side c
+ )
+ if btabs:
+ sides.extend(
+ self.side(thickness, (x,y+dy),(d,-c),(d,a),dtabs * (-thickness if d else thickness),dy,(0,-1),d,0,0,0) # side d
+ )
+ else:
+ sides.extend(
+ self.side(thickness, (x,y+dy),(d,-c),(d,a),dtabs * (-thickness if d else thickness),dy,(0,-1),d,0,(self.cfg.keydivfloor|wall) * (self.cfg.keydivwalls|floor) * self.cfg.divy*xholes*dtabs,xspacing) # side d
+ )
+
+ if idx==0:
+ # remove tabs from dividers if not required
+ if not self.cfg.keydivfloor:
+ a=c=1
+ atabs=ctabs=0
+ if not self.cfg.keydivwalls:
+ b=d=1
+ btabs=dtabs=0
+
+ y=4*self.cfg.spacing+1*Y+2*Z # root y co-ord for piece
+ for n in range(0,self.cfg.divx): # generate X dividers
+ #group = newGroup(self)
+ tab = []
+ groups.append(tab)
+ x=n*(self.cfg.spacing+X) # root x co-ord for piece
+ tab.extend(
+ self.side(thickness, (x,y),(d,a),(-b,a),self.cfg.keydivfloor*atabs*(-thickness if a else thickness),dx,(1,0),a,1,0,0) # side a
+ )
+ tab.extend(
+ self.side(thickness, (x+dx,y),(-b,a),(-b,-c),self.cfg.keydivwalls*btabs*(thickness if b else -thickness),dy,(0,1),b,1,self.cfg.divy*xholes,xspacing) # side b
+ )
+ tab.extend(
+ self.side(thickness, (x+dx,y+dy),(-b,-c),(d,-c),self.cfg.keydivfloor*ctabs*(thickness if c else -thickness),dx,(-1,0),c,1,0,0) # side c
+ )
+ tab.extend(
+ self.side(thickness, (x,y+dy),(d,-c),(d,a),self.cfg.keydivwalls*dtabs*(-thickness if d else thickness),dy,(0,-1),d,1,0,0) # side d
+ )
+ elif idx==1:
+ y=5*self.cfg.spacing+1*Y+3*Z # root y co-ord for piece
+ for n in range(0,self.cfg.divy): # generate Y dividers
+ #group = newGroup(self)
+ tab = []
+ groups.append(tab)
+ x=n*(self.cfg.spacing+Z) # root x co-ord for piece
+ tab.extend(
+ self.side(thickness, (x,y),(d,a),(-b,a),self.cfg.keydivwalls*atabs*(-thickness if a else thickness),dx,(1,0),a,1,self.cfg.divx*yholes,yspacing) # side a
+ )
+ tab.extend(
+ self.side(thickness, (x+dx,y),(-b,a),(-b,-c),self.cfg.keydivfloor*btabs*(thickness if b else -thickness),dy,(0,1),b,1,0,0) # side b
+ )
+ tab.extend(
+ self.side(thickness, (x+dx,y+dy),(-b,-c),(d,-c),self.cfg.keydivwalls*ctabs*(thickness if c else -thickness),dx,(-1,0),c,1,0,0) # side c
+ )
+ tab.extend(
+ self.side(thickness, (x,y+dy),(d,-c),(d,a),self.cfg.keydivfloor*dtabs*(-thickness if d else thickness),dy,(0,-1),d,1,0,0) # side d
+ )
+ return groups
diff --git a/tabbedboxmaker/inkex.py b/tabbedboxmaker/inkex.py
new file mode 100644
index 0000000..ddd915c
--- /dev/null
+++ b/tabbedboxmaker/inkex.py
@@ -0,0 +1,222 @@
+#! /usr/bin/env python -t
+'''
+Generates Inkscape SVG file containing box components needed to
+CNC (laser/mill) cut a box with tabbed joints taking kerf and clearance into account
+
+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 3 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, see .
+'''
+
+import gettext
+import inkex
+import os
+import tabbedboxmaker
+import tabbedboxmaker.schroff
+
+_ = gettext.gettext
+
+linethickness = 1 # default unless overridden by settings
+
+def log(text):
+ if 'SCHROFF_LOG' in os.environ:
+ f = open(os.environ.get('SCHROFF_LOG'), 'a')
+ f.write(text + "\n")
+
+def newGroup(canvas):
+ # Create a new group and add element created from line string
+ panelId = canvas.svg.get_unique_id('panel')
+ group = canvas.svg.get_current_layer().add(inkex.Group(id=panelId))
+ return group
+
+def getLine(XYstring):
+ line = inkex.PathElement()
+ line.style = { 'stroke': '#000000', 'stroke-width' : str(linethickness), 'fill': 'none' }
+ line.path = XYstring
+ #inkex.etree.SubElement(parent, inkex.addNS('path','svg'), drw)
+ return line
+
+# jslee - shamelessly adapted from sample code on below Inkscape wiki page 2015-07-28
+# http://wiki.inkscape.org/wiki/index.php/Generating_objects_from_extensions
+def getCircle(r, c):
+ (cx, cy) = c
+ log("putting circle at (%d,%d)" % (cx,cy))
+ circle = inkex.PathElement.arc((cx, cy), r)
+ circle.style = { 'stroke': '#000000', 'stroke-width': str(linethickness), 'fill': 'none' }
+
+ # ell_attribs = {'style':simplestyle.formatStyle(style),
+ # inkex.addNS('cx','sodipodi') :str(cx),
+ # inkex.addNS('cy','sodipodi') :str(cy),
+ # inkex.addNS('rx','sodipodi') :str(r),
+ # inkex.addNS('ry','sodipodi') :str(r),
+ # inkex.addNS('start','sodipodi') :str(0),
+ # inkex.addNS('end','sodipodi') :str(2*math.pi),
+ # inkex.addNS('open','sodipodi') :'true', #all ellipse sectors we will draw are open
+ # inkex.addNS('type','sodipodi') :'arc',
+ # 'transform' :'' }
+ #inkex.etree.SubElement(parent, inkex.addNS('path','svg'), ell_attribs )
+ return circle
+
+
+class SvgExporter(object):
+ """Export AbstractShape objects as SVG"""
+
+ # Is there a better way to do this?
+ # That is, to extend the capability of the AbstractShape classes and enable
+ # writing SVG, but in such a way that users of the base class are not
+ # affected. I think this rules out inheritance. (The only way I can see
+ # to use inheritance would be with a factory).
+ # The approach here feels kludgy though.
+ def export(self, shape, inkex_group) -> None:
+ """Write the shape into the given inkex_group"""
+ try:
+ export_method = 'export_' + shape.__class__.__name__
+ exporter = getattr(self, export_method)
+ exporter(shape, inkex_group)
+ except AttributeError:
+ print(f'No exporter for shape type "{type(shape)}" (looking for "{export_method}")')
+ raise
+
+ def export_Circle(self, circle, inkex_group) -> None:
+ inkex_group.add(getCircle(circle.radius, circle.centre))
+
+ def export_Path(self, path, inkex_group) -> None:
+ d = f'M {path.initial_x}, {path.initial_y} '
+ for seg in path.segments:
+ if seg.type == 'line':
+ d += f'L {seg.args[0]} {seg.args[1]} '
+ else:
+ raise Exception(f'Unsupported segment type "{seg.type}"')
+ inkex_group.add(getLine(d))
+
+
+class InkexBoxMaker(inkex.Effect):
+ def __init__(self):
+ inkex.Effect.__init__(self)
+
+ tabbedboxmaker.schroff.SchroffBox.add_args(self.arg_parser)
+
+ # Add inkex plugin specific args.
+ self.arg_parser.add_argument('--length',action='store',type=float,
+ dest='length',default=100,help='Length of Box')
+ self.arg_parser.add_argument('--width',action='store',type=float,
+ dest='width',default=100,help='Width of Box')
+ self.arg_parser.add_argument('--depth',action='store',type=float,
+ dest='height',default=100,help='Height of Box')
+ self.arg_parser.add_argument('--thickness',action='store',type=float,
+ dest='thickness',default=10,help='Thickness of Material')
+ self.arg_parser.add_argument('--hairline',action='store',type=int,
+ dest='hairline',default=0,help='Line Thickness')
+
+
+ def _to_svg_units(self, value: float, unit: str) -> float:
+ return self.svg.unittouu(str(value) + unit)
+
+ def effect(self):
+ global linethickness
+
+ # Get access to main SVG document element and get its dimensions.
+ svg = self.document.getroot()
+
+ # Get the attributes:
+ widthDoc = self.svg.unittouu(svg.get('width'))
+ heightDoc = self.svg.unittouu(svg.get('height'))
+
+ # Get script's option values.
+
+ # Set the line thickness
+ hairline=self.options.hairline
+ if hairline:
+ linethickness=self.svg.unittouu('0.002in')
+ else:
+ linethickness=1
+
+ unit = self.options.unit
+
+ ## minimally different behaviour for schroffmaker.inx vs. boxmaker.inx
+ ## essentially schroffmaker.inx is just an alternate interface with different
+ ## default settings, some options removed, and a tiny amount of extra logic
+ if self.options.schroff:
+ self.options.rail_height = self._to_svg_units(self.options.rail_height, unit)
+ self.options.row_centre_spacing = self._to_svg_units(122.5, unit) # TODO(manuel): Fixed number with variable unit? Feels wrong.
+ self.options.row_spacing = self._to_svg_units(self.options.row_spacing, unit)
+ self.options.rail_mount_depth = self._to_svg_units(self.options.rail_mount_depth, unit)
+ self.options.rail_mount_centre_offset = self._to_svg_units(self.options.rail_mount_centre_offset, unit)
+ self.options.rail_mount_radius=self._to_svg_units(2.5, unit) # TODO(manuel): Same - fixed number with variable unit.
+
+ X = self._to_svg_units(self.options.hp * 5.08, unit) # TODO(manuel): Same - fixed number with variable unit.
+ # 122.5mm vertical distance between mounting hole centres of 3U Schroff panels
+ row_height = self.options.rows * (self.options.row_centre_spacing + self.options.rail_height)
+ # rail spacing in between rows but never between rows and case panels
+ row_spacing_total = (self.options.rows - 1) * self.options.row_spacing
+ Y = row_height + row_spacing_total
+ else:
+ ## boxmaker.inx
+ X = self._to_svg_units(self.options.length + self.options.kerf, unit)
+ Y = self._to_svg_units(self.options.width + self.options.kerf, unit)
+
+ Z = self._to_svg_units(self.options.height + self.options.kerf, unit)
+
+ self.options.kerf = self._to_svg_units(self.options.kerf, self.options.unit)
+ self.options.thickness = self._to_svg_units(self.options.thickness, unit)
+ self.options.spacing = self._to_svg_units(self.options.spacing, unit)
+
+ # check input values mainly to avoid python errors
+ # TODO restrict values to *correct* solutions
+ # TODO restrict divisions to logical values
+ error=0
+
+ if min(X,Y,Z)==0:
+ inkex.errormsg(_('Error: Dimensions must be non zero'))
+ error=1
+ if max(X,Y,Z)>max(widthDoc,heightDoc)*10: # crude test
+ inkex.errormsg(_('Error: Dimensions Too Large'))
+ error=1
+ if self.options.tab is not None:
+ self.options.tab = self._to_svg_units(self.options.tab, unit)
+ if min(X,Y,Z)<3*self.options.tab:
+ inkex.errormsg(_('Error: Tab size too large'))
+ error=1
+ if self.options.tabmin(X,Y,Z)/3: # crude test
+ inkex.errormsg(_('Error: Material too thick'))
+ error=1
+ if self.options.kerf>min(X,Y,Z)/3: # crude test
+ inkex.errormsg(_('Error: Kerf too large'))
+ error=1
+ if self.options.spacing>max(X,Y,Z)*10: # crude test
+ inkex.errormsg(_('Error: Spacing too large'))
+ error=1
+ if self.options.spacing.
+'''
+
+from typing import List
+
+import argparse
+import os
+
+from . import (TabbedBox, AbstractShape, Path)
+
+def log(text):
+ if 'SCHROFF_LOG' in os.environ:
+ f = open(os.environ.get('SCHROFF_LOG'), 'a')
+ f.write(text + "\n")
+
+
+class Circle(AbstractShape):
+ def __init__(self, cX: float, cY: float, r: float) -> None:
+ self.centre = (cX, cY)
+ self.radius = r
+
+ def __repr__(self):
+ return f'{__class__}({self.initial_x}, {self.initial_y}, r={self.radius})'
+
+class SchroffBox(TabbedBox):
+
+ @staticmethod
+ def add_args(arg_parser: argparse.ArgumentParser) -> None:
+ """Add ArgumentParser args needed to configure a schroff boxmaker run"""
+
+ super(SchroffBox, SchroffBox).add_args(arg_parser)
+ arg_parser.add_argument('--schroff', action='store', type=int,
+ dest='schroff', default=0, help='Enable Schroff mode')
+ arg_parser.add_argument('--rail_height', action='store', type=float,
+ dest='rail_height', default=10.0, help='Height of rail')
+ arg_parser.add_argument('--rail_mount_depth', action='store',
+ type=float, dest='rail_mount_depth', default=17.4,
+ help='Depth at which to place hole for rail mount bolt')
+ arg_parser.add_argument('--rail_mount_centre_offset', action='store',
+ type=float, dest='rail_mount_centre_offset', default=0.0,
+ help='How far toward row centreline to offset rail mount bolt (from rail centreline)')
+ arg_parser.add_argument('--rows', action='store', type=int,
+ dest='rows', default=0, help='Number of Schroff rows')
+ arg_parser.add_argument('--hp', action='store', type=int,
+ dest='hp', default=0,help='Width (TE/HP units) of Schroff rows')
+ arg_parser.add_argument('--row_spacing', action='store', type=float,
+ dest='row_spacing', default=10.0,help='Height of rail')
+
+ def piece(
+ self, X: float, Y: float, Z: float, thickness: float, idx: int,
+ rootx: List[float], rooty: List[float], dx: float, dy: float,
+ tabs: int, tabbed: int, pieceType: int,
+ ):
+ groups = super().piece(
+ X, Y, Z, thickness, idx,
+ rootx, rooty, dx, dy, tabs, tabbed, pieceType
+ )
+
+ initOffsetX=0 # TODO: These look redundant, remove?
+ initOffsetY=0
+
+ (xs,xx,xy,xz)=rootx
+ (ys,yx,yy,yz)=rooty
+
+ x=xs*self.cfg.spacing+xx*X+xy*Y+xz*Z+initOffsetX # root x co-ord for piece
+ y=ys*self.cfg.spacing+yx*X+yy*Y+yz*Z+initOffsetY # root y co-ord for piece
+
+ railholes = 1 if pieceType==3 else 0
+ if self.cfg.schroff and railholes:
+# log("rail holes enabled on piece %d at (%d, %d)" % (idx, x+thickness,y+thickness))
+# log("abcd = (%d,%d,%d,%d)" % (a,b,c,d))
+# log("dxdy = (%d,%d)" % (dx,dy))
+ rhxoffset = self.cfg.rail_mount_depth + thickness
+ if idx == 1:
+ rhx=x+rhxoffset
+ elif idx == 3:
+ rhx=x-rhxoffset+dx
+ else:
+ rhx=0
+ log("rhxoffset = %d, rhx= %d" % (rhxoffset, rhx))
+ rystart=y+(self.cfg.rail_height/2)+thickness
+ holes=[]
+ if self.cfg.rows == 1:
+ log("just one row this time, rystart = %d" % rystart)
+ rh1y=rystart+self.cfg.rail_mount_centre_offset
+ rh2y=rh1y+(self.cfg.row_centre_spacing-self.cfg.rail_mount_centre_offset)
+ holes.append(Circle(rhx, rh1y, self.cfg.rail_mount_radius))
+ holes.append(Circle(rhx, rh2y, self.cfg.rail_mount_radius))
+ else:
+ for n in range(0, self.cfg.rows):
+ log("drawing row %d, rystart = %d" % (n+1, rystart))
+ # if holes areo ffset (eg. Vector T-strut rails), they should be offset
+ # toward each other, ie. toward the centreline of the Schroff row
+ rh1y=rystart+self.cfg.rail_mount_centre_offset
+ rh2y=rh1y+self.cfg.row_centre_spacing-self.cfg.rail_mount_centre_offset
+ holes.append(Circle(rhx, rh1y, self.cfg.rail_mount_radius))
+ holes.append(Circle(rhx, rh2y, self.cfg.rail_mount_radius))
+ rystart+=self.cfg.row_centre_spacing+self.cfg.row_spacing+self.cfg.rail_height
+ # Add to the start of the 'sides' group entry - the first group - to
+ # keep the generated SVG the same as before separating the scrhoff code.
+ groups[0] = holes + groups[0]
+
+ return groups
diff --git a/tests/expected/default_tabs.svg b/tests/expected/default_tabs.svg
new file mode 100644
index 0000000..2d7b569
--- /dev/null
+++ b/tests/expected/default_tabs.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/fully_enclosed.svg b/tests/expected/fully_enclosed.svg
new file mode 100644
index 0000000..e271e06
--- /dev/null
+++ b/tests/expected/fully_enclosed.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/inline_layout.svg b/tests/expected/inline_layout.svg
new file mode 100644
index 0000000..2714b6a
--- /dev/null
+++ b/tests/expected/inline_layout.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/open_top.svg b/tests/expected/open_top.svg
new file mode 100644
index 0000000..d17f717
--- /dev/null
+++ b/tests/expected/open_top.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/opposite_ends_open.svg b/tests/expected/opposite_ends_open.svg
new file mode 100644
index 0000000..5b5b236
--- /dev/null
+++ b/tests/expected/opposite_ends_open.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/outside_measurement.svg b/tests/expected/outside_measurement.svg
new file mode 100644
index 0000000..cfebe47
--- /dev/null
+++ b/tests/expected/outside_measurement.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/schroff/schroff_hole_centre_offset.svg b/tests/expected/schroff/schroff_hole_centre_offset.svg
new file mode 100644
index 0000000..f9b3c3d
--- /dev/null
+++ b/tests/expected/schroff/schroff_hole_centre_offset.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/schroff/schroff_one_row.svg b/tests/expected/schroff/schroff_one_row.svg
new file mode 100644
index 0000000..4939387
--- /dev/null
+++ b/tests/expected/schroff/schroff_one_row.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/schroff/schroff_two_rows.svg b/tests/expected/schroff/schroff_two_rows.svg
new file mode 100644
index 0000000..d1e8815
--- /dev/null
+++ b/tests/expected/schroff/schroff_two_rows.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/schroff/schroff_with_spacing.svg b/tests/expected/schroff/schroff_with_spacing.svg
new file mode 100644
index 0000000..7c884e5
--- /dev/null
+++ b/tests/expected/schroff/schroff_with_spacing.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/three_sides_open.svg b/tests/expected/three_sides_open.svg
new file mode 100644
index 0000000..1c3244d
--- /dev/null
+++ b/tests/expected/three_sides_open.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/threepiece_layout.svg b/tests/expected/threepiece_layout.svg
new file mode 100644
index 0000000..119549e
--- /dev/null
+++ b/tests/expected/threepiece_layout.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/two_panels_only.svg b/tests/expected/two_panels_only.svg
new file mode 100644
index 0000000..ff39f36
--- /dev/null
+++ b/tests/expected/two_panels_only.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/two_sides_open.svg b/tests/expected/two_sides_open.svg
new file mode 100644
index 0000000..e59fb04
--- /dev/null
+++ b/tests/expected/two_sides_open.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_dimple.svg b/tests/expected/with_dimple.svg
new file mode 100644
index 0000000..58c2bf0
--- /dev/null
+++ b/tests/expected/with_dimple.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_dividers_keyed_all.svg b/tests/expected/with_dividers_keyed_all.svg
new file mode 100644
index 0000000..28e1323
--- /dev/null
+++ b/tests/expected/with_dividers_keyed_all.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_dividers_keyed_floor.svg b/tests/expected/with_dividers_keyed_floor.svg
new file mode 100644
index 0000000..8dc3079
--- /dev/null
+++ b/tests/expected/with_dividers_keyed_floor.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_dividers_keyed_none.svg b/tests/expected/with_dividers_keyed_none.svg
new file mode 100644
index 0000000..2a0c32d
--- /dev/null
+++ b/tests/expected/with_dividers_keyed_none.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_dividers_keyed_walls.svg b/tests/expected/with_dividers_keyed_walls.svg
new file mode 100644
index 0000000..6da3e4e
--- /dev/null
+++ b/tests/expected/with_dividers_keyed_walls.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_dogbone.svg b/tests/expected/with_dogbone.svg
new file mode 100644
index 0000000..1f5a03f
--- /dev/null
+++ b/tests/expected/with_dogbone.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_nonzero_kerf.svg b/tests/expected/with_nonzero_kerf.svg
new file mode 100644
index 0000000..0a7c297
--- /dev/null
+++ b/tests/expected/with_nonzero_kerf.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_rotate_symmetry_tabs.svg b/tests/expected/with_rotate_symmetry_tabs.svg
new file mode 100644
index 0000000..e07d9b7
--- /dev/null
+++ b/tests/expected/with_rotate_symmetry_tabs.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/expected/with_thick_lines.svg b/tests/expected/with_thick_lines.svg
new file mode 100644
index 0000000..83b23dd
--- /dev/null
+++ b/tests/expected/with_thick_lines.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/tests/test_path.py b/tests/test_path.py
new file mode 100644
index 0000000..770143e
--- /dev/null
+++ b/tests/test_path.py
@@ -0,0 +1,15 @@
+import io
+import os
+import re
+import unittest
+import tabbedboxmaker
+
+class TestPath(unittest.TestCase):
+
+ def test_boundingbox(self):
+ path = tabbedboxmaker.Path(0,0)
+ path.add(tabbedboxmaker.LineSegment(1,1))
+ path.add(tabbedboxmaker.LineSegment(1,-1))
+ bb = path.boundingbox()
+ self.assertEqual(bb, ((0,-1), (1,1)))
+
diff --git a/tests/test_schroff.py b/tests/test_schroff.py
new file mode 100644
index 0000000..da4de90
--- /dev/null
+++ b/tests/test_schroff.py
@@ -0,0 +1,132 @@
+import io
+import os
+import re
+import unittest
+import tabbedboxmaker.inkex
+
+blank_svg=b'''
+
+
+
+'''
+
+
+def mask_panel_ids(svgin: str) -> str:
+ return re.sub(r'"panel\d+"', '"panelTEST"', svgin)
+
+class TestSchroffBox(unittest.TestCase):
+
+ def test_schroff(self):
+ # See schroff.inx for arg descriptions
+ cases = [
+ {
+ 'label': 'schroff_one_row',
+ 'args': [
+ '--unit=mm', '--inside=1', '--schroff=1', '--rows=1',
+ '--hp=84', '--length=0', '--width=0', '--depth=65',
+ '--rail_height=10', '--rail_mount_depth=17.4',
+ '--rail_mount_centre_offset=0', '--row_spacing=0',
+ '--tab=6', '--equal=0', '--thickness=3', '--kerf=0.1',
+ '--div_l=0', '--div_w=0', '--style=1', '--boxtype=2',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'schroff_two_rows',
+ 'args': [
+ '--unit=mm', '--inside=1', '--schroff=1', '--rows=2',
+ '--hp=84', '--length=0', '--width=0', '--depth=65',
+ '--rail_height=10', '--rail_mount_depth=17.4',
+ '--rail_mount_centre_offset=0', '--row_spacing=0',
+ '--tab=6', '--equal=0', '--thickness=3', '--kerf=0.1',
+ '--div_l=0', '--div_w=0', '--style=1', '--boxtype=2',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'schroff_hole_centre_offset',
+ 'args': [
+ '--unit=mm', '--inside=1', '--schroff=1', '--rows=1',
+ '--hp=84', '--length=0', '--width=0', '--depth=65',
+ '--rail_height=10', '--rail_mount_depth=17.4',
+ '--rail_mount_centre_offset=5', '--row_spacing=0',
+ '--tab=6', '--equal=0', '--thickness=3', '--kerf=0.1',
+ '--div_l=0', '--div_w=0', '--style=1', '--boxtype=2',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'schroff_with_spacing',
+ 'args': [
+ '--unit=mm', '--inside=1', '--schroff=1', '--rows=1',
+ '--hp=84', '--length=0', '--width=0', '--depth=65',
+ '--rail_height=10', '--rail_mount_depth=17.4',
+ '--rail_mount_centre_offset=0', '--row_spacing=10',
+ '--tab=6', '--equal=0', '--thickness=3', '--kerf=0.1',
+ '--div_l=0', '--div_w=0', '--style=1', '--boxtype=2',
+ '--spacing=1'],
+ },
+ ]
+
+ for case in cases:
+ with self.subTest(label=case['label']):
+ infh = io.BytesIO(blank_svg)
+ outfh = io.BytesIO()
+ expected_output_dir = os.path.join(
+ os.path.dirname(__file__), 'expected', 'schroff'
+ )
+ expected_file = os.path.join(
+ expected_output_dir, case['label'] + '.svg'
+ )
+ expected = ''
+ with open(expected_file, 'r') as f:
+ expected = mask_panel_ids(f.read())
+
+ tbm = tabbedboxmaker.inkex.InkexBoxMaker()
+
+ tbm.parse_arguments(case['args'])
+ tbm.options.input_file = infh
+ tbm.options.output = outfh
+
+ tbm.load_raw()
+ tbm.save_raw(tbm.effect())
+
+ output = mask_panel_ids(outfh.getvalue().decode('utf-8'))
+
+ # Set self.maxDiff to None to see full diff.
+ #self.maxDiff = None
+ self.assertEqual(expected, output)
diff --git a/tests/test_tabbedbox.py b/tests/test_tabbedbox.py
new file mode 100755
index 0000000..56cf950
--- /dev/null
+++ b/tests/test_tabbedbox.py
@@ -0,0 +1,278 @@
+import io
+import os
+import re
+import unittest
+import tabbedboxmaker.inkex
+
+blank_svg=b'''
+
+
+
+'''
+
+
+def mask_panel_ids(svgin: str) -> str:
+ return re.sub(r'"panel\d+"', '"panelTEST"', svgin)
+
+class TestTabbedBox(unittest.TestCase):
+
+ def test_tabbed(self):
+ # See boxmaker.inx for arg descriptions
+ cases = [
+ {
+ 'label': 'fully_enclosed',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=1', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'open_top',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'two_sides_open',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=3', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'three_sides_open',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=4', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'opposite_ends_open',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=5', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'two_panels_only',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=6', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'outside_measurement',
+ 'args': [
+ '--unit=mm', '--inside=0', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_dogbone',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=1',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_dimple',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0.2', '--dimplelength=0.2',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_rotate_symmetry_tabs',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=1', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_thick_lines',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=0', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_nonzero_kerf',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0.1', '--style=1',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'threepiece_layout',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=2',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'inline_layout',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=3',
+ '--boxtype=2', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_dividers_keyed_all',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=1', '--div_w=1', '--keydiv=0',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_dividers_keyed_floor',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=1', '--div_w=1', '--keydiv=1',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_dividers_keyed_walls',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=1', '--div_w=1', '--keydiv=2',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'with_dividers_keyed_none',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tab=6', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=2', '--div_l=1', '--div_w=1', '--keydiv=3',
+ '--spacing=1'],
+ },
+ {
+ 'label': 'default_tabs',
+ 'args': [
+ '--unit=mm', '--inside=1', '--length=80', '--width=100',
+ '--depth=40', '--equal=0', '--tabtype=0',
+ '--tabsymmetry=0', '--dimpleheight=0', '--dimplelength=0',
+ '--hairline=1', '--thickness=3', '--kerf=0', '--style=1',
+ '--boxtype=1', '--div_l=0', '--div_w=0', '--keydiv=1',
+ '--spacing=1'],
+ },
+ ]
+
+ for case in cases:
+ with self.subTest(label=case['label']):
+ infh = io.BytesIO(blank_svg)
+ outfh = io.BytesIO()
+ expected_output_dir = os.path.join(
+ os.path.dirname(__file__), 'expected'
+ )
+ expected_file = os.path.join(
+ expected_output_dir, case['label'] + '.svg'
+ )
+ expected = ''
+ with open(expected_file, 'r') as f:
+ expected = mask_panel_ids(f.read())
+
+ tbm = tabbedboxmaker.inkex.InkexBoxMaker()
+
+ tbm.parse_arguments(case['args'])
+ tbm.options.input_file = infh
+ tbm.options.output = outfh
+
+ tbm.load_raw()
+ tbm.save_raw(tbm.effect())
+
+ output = mask_panel_ids(outfh.getvalue().decode('utf-8'))
+
+ # Set self.maxDiff to None to see full diff.
+ #self.maxDiff = None
+ self.assertEqual(expected, output)