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)