diff --git a/extensions/fablabchemnitz/apply_transformations/apply_transformations.inx b/extensions/fablabchemnitz/apply_transformations/apply_transformations.inx
new file mode 100644
index 0000000..2c5daad
--- /dev/null
+++ b/extensions/fablabchemnitz/apply_transformations/apply_transformations.inx
@@ -0,0 +1,16 @@
+
+
+ Apply Transformations
+ fablabchemnitz.de.apply_transformations
+
+ all
+
+
+
+
+
+
+
+
diff --git a/extensions/fablabchemnitz/apply_transformations/apply_transformations.py b/extensions/fablabchemnitz/apply_transformations/apply_transformations.py
new file mode 100644
index 0000000..084c52b
--- /dev/null
+++ b/extensions/fablabchemnitz/apply_transformations/apply_transformations.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+#
+# License: GPL2
+# Copyright Mark "Klowner" Riedesel
+# https://github.com/Klowner/inkscape-applytransforms
+#
+import copy
+import math
+from lxml import etree
+import re
+import inkex
+from inkex.paths import CubicSuperPath, Path
+from inkex.transforms import Transform
+from inkex.styles import Style
+
+NULL_TRANSFORM = Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
+
+
+class ApplyTransformations(inkex.EffectExtension):
+
+ def effect(self):
+ if self.svg.selected:
+ for id, shape in self.svg.selected.items():
+ self.recursiveFuseTransform(shape)
+ else:
+ self.recursiveFuseTransform(self.document.getroot())
+
+ @staticmethod
+ def objectToPath(element):
+ if element.tag == inkex.addNS('g', 'svg'):
+ return element
+
+ if element.tag == inkex.addNS('path', 'svg') or element.tag == 'path':
+ for attName in element.attrib.keys():
+ if ("sodipodi" in attName) or ("inkscape" in attName):
+ del element.attrib[attName]
+ return element
+
+ return element
+
+ def scaleStrokeWidth(self, element, transf):
+ if 'style' in element.attrib:
+ style = element.attrib.get('style')
+ style = dict(Style.parse_str(style))
+ update = False
+ if 'stroke-width' in style:
+ try:
+ stroke_width = self.svg.unittouu(style.get('stroke-width')) / self.svg.unittouu("1px")
+ stroke_width *= math.sqrt(abs(transf.a * transf.d - transf.b * transf.c))
+ style['stroke-width'] = str(stroke_width)
+ update = True
+ except AttributeError as e:
+ pass
+
+ if update:
+ element.attrib['style'] = Style(style).to_str()
+
+ def recursiveFuseTransform(self, element, transf=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
+
+ transf = Transform(transf) @ Transform(element.get("transform", None)) #a, b, c, d = linear transformations / e, f = translations
+
+ if 'transform' in element.attrib:
+ del element.attrib['transform']
+
+ element = ApplyTransformations.objectToPath(element)
+
+ if transf == NULL_TRANSFORM:
+ # Don't do anything if there is effectively no transform applied
+ # reduces alerts for unsupported elements
+ pass
+ elif 'd' in element.attrib:
+ d = element.get('d')
+ p = CubicSuperPath(d)
+ p = Path(p).to_absolute().transform(transf, True)
+ element.set('d', str(Path(CubicSuperPath(p).to_path())))
+
+ self.scaleStrokeWidth(element, transf)
+
+ elif element.tag in [inkex.addNS('polygon', 'svg'),
+ inkex.addNS('polyline', 'svg')]:
+ points = element.get('points')
+ points = points.strip().split(' ')
+ for k, p in enumerate(points):
+ if ',' in p:
+ p = p.split(',')
+ p = [float(p[0]), float(p[1])]
+ p = transf.apply_to_point(p)
+ p = [str(p[0]), str(p[1])]
+ p = ','.join(p)
+ points[k] = p
+ points = ' '.join(points)
+ element.set('points', points)
+
+ self.scaleStrokeWidth(element, transf)
+
+ elif element.tag in [inkex.addNS("ellipse", "svg"), inkex.addNS("circle", "svg")]:
+
+ def isequal(a, b):
+ return abs(a - b) <= transf.absolute_tolerance
+
+ if element.TAG == "ellipse":
+ rx = float(element.get("rx"))
+ ry = float(element.get("ry"))
+ else:
+ rx = float(element.get("r"))
+ ry = rx
+
+ cx = float(element.get("cx"))
+ cy = float(element.get("cy"))
+ sqxy1 = (cx - rx, cy - ry)
+ sqxy2 = (cx + rx, cy - ry)
+ sqxy3 = (cx + rx, cy + ry)
+ newxy1 = transf.apply_to_point(sqxy1)
+ newxy2 = transf.apply_to_point(sqxy2)
+ newxy3 = transf.apply_to_point(sqxy3)
+
+ element.set("cx", (newxy1[0] + newxy3[0]) / 2)
+ element.set("cy", (newxy1[1] + newxy3[1]) / 2)
+ edgex = math.sqrt(
+ abs(newxy1[0] - newxy2[0]) ** 2 + abs(newxy1[1] - newxy2[1]) ** 2
+ )
+ edgey = math.sqrt(
+ abs(newxy2[0] - newxy3[0]) ** 2 + abs(newxy2[1] - newxy3[1]) ** 2
+ )
+
+ if not isequal(edgex, edgey) and (
+ element.TAG == "circle"
+ or not isequal(newxy2[0], newxy3[0])
+ or not isequal(newxy1[1], newxy2[1])
+ ):
+ inkex.utils.errormsg(
+ "Warning: Shape %s (%s) is approximate only, try Object to path first for better results"
+ % (element.TAG, element.get("id"))
+ )
+
+ if element.TAG == "ellipse":
+ element.set("rx", edgex / 2)
+ element.set("ry", edgey / 2)
+ else:
+ element.set("r", edgex / 2)
+
+ # this is unstable at the moment
+ elif element.tag == inkex.addNS("use", "svg"):
+ href = None
+ old_href_key = '{http://www.w3.org/1999/xlink}href'
+ new_href_key = 'href'
+ if element.attrib.has_key(old_href_key) is True: # {http://www.w3.org/1999/xlink}href (which gets displayed as 'xlink:href') attribute is deprecated. the newer attribute is just 'href'
+ href = element.attrib.get(old_href_key)
+ #element.attrib.pop(old_href_key)
+ if element.attrib.has_key(new_href_key) is True:
+ href = element.attrib.get(new_href_key) #we might overwrite the previous deprecated xlink:href but it's okay
+ #element.attrib.pop(new_href_key)
+
+ #get the linked object from href attribute
+ linkedObject = self.document.getroot().xpath("//*[@id = '%s']" % href.lstrip('#')) #we must remove hashtag symbol
+ linkedObjectCopy = copy.copy(linkedObject[0])
+ objectType = linkedObject[0].tag
+
+ if objectType == inkex.addNS("image", "svg"):
+ mask = None #image might have an alpha channel
+ new_mask_id = self.svg.get_unique_id("mask")
+ newMask = None
+ if element.attrib.has_key('mask') is True:
+ mask = element.attrib.get('mask')
+ #element.attrib.pop('mask')
+
+ #get the linked mask from mask attribute. We remove the old and create a new
+ if mask is not None:
+ linkedMask = self.document.getroot().xpath("//*[@id = '%s']" % mask.lstrip('url(#').rstrip(')')) #we must remove hashtag symbol
+ linkedMask[0].delete()
+ maskAttributes = {'id': new_mask_id}
+ newMask = etree.SubElement(self.document.getroot(), inkex.addNS('mask', 'svg'), maskAttributes)
+
+ width = float(linkedObjectCopy.get('width')) * transf.a
+ height = float(linkedObjectCopy.get('height')) * transf.d
+ linkedObjectCopy.set('width', '{:1.6f}'.format(width))
+ linkedObjectCopy.set('height', '{:1.6f}'.format(height))
+ linkedObjectCopy.set('x', '{:1.6f}'.format(transf.e))
+ linkedObjectCopy.set('y', '{:1.6f}'.format(transf.f))
+ if newMask is not None:
+ linkedObjectCopy.set('mask', 'url(#' + new_mask_id + ')')
+ maskRectAttributes = {'x': '{:1.6f}'.format(transf.e), 'y': '{:1.6f}'.format(transf.f), 'width': '{:1.6f}'.format(width), 'height': '{:1.6f}'.format(height), 'style':'fill:#ffffff;'}
+ maskRect = etree.SubElement(newMask, inkex.addNS('rect', 'svg'), maskRectAttributes)
+ self.document.getroot().append(linkedObjectCopy) #for each svg:use we append a copy to the document root
+ element.delete() #then we remove the use object
+ else:
+ #self.recursiveFuseTransform(linkedObjectCopy, transf)
+ self.recursiveFuseTransform(element.unlink(), transf)
+
+ elif element.tag in [inkex.addNS('rect', 'svg'),
+ inkex.addNS('text', 'svg'),
+ inkex.addNS('image', 'svg')]:
+ element.attrib['transform'] = str(transf)
+ inkex.utils.errormsg(
+ "Shape %s (%s) not yet supported. Not all transforms will be applied. Try Object to path first"
+ % (element.TAG, element.get("id"))
+ )
+ else:
+ # e.g.
+ self.scaleStrokeWidth(element, transf)
+
+ for child in element.getchildren():
+ self.recursiveFuseTransform(child, transf)
+
+if __name__ == '__main__':
+ ApplyTransformations().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/apply_transformations/meta.json b/extensions/fablabchemnitz/apply_transformations/meta.json
new file mode 100644
index 0000000..6b968ac
--- /dev/null
+++ b/extensions/fablabchemnitz/apply_transformations/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Apply Transformations",
+ "id": "fablabchemnitz.de.apply_transformations",
+ "path": "apply_transformations",
+ "dependent_extensions": null,
+ "original_name": "Apply Transform",
+ "original_id": "com.klowner.filter.apply_transform",
+ "license": "GNU GPL v2",
+ "license_url": "https://github.com/Klowner/inkscape-applytransforms/blob/master/LICENSE.txt",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/apply_transformations",
+ "fork_url": "https://github.com/Klowner/inkscape-applytransforms",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Apply+Transformations",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/Klowner",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_t_slot/box_maker_t_slot.inx b/extensions/fablabchemnitz/box_maker_t_slot/box_maker_t_slot.inx
new file mode 100644
index 0000000..ba16746
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_t_slot/box_maker_t_slot.inx
@@ -0,0 +1,45 @@
+
+
+ Box Maker - T-Slot
+ fablabchemnitz.de.box_maker_t_slot
+
+
+
+
+
+
+
+
+
+ 80.0
+ 80.0
+ 80.0
+ 10.0
+
+
+
+
+ 6.0
+ 0.0
+ 0.05
+
+
+
+
+
+
+ 5.0
+ 12
+ 3
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_t_slot/box_maker_t_slot.py b/extensions/fablabchemnitz/box_maker_t_slot/box_maker_t_slot.py
new file mode 100644
index 0000000..f971aae
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_t_slot/box_maker_t_slot.py
@@ -0,0 +1,321 @@
+#! /usr/bin/env python3
+'''
+Generates Inkscape SVG file containing box components needed to
+laser cut a tabbed construction box taking kerf and clearance into account
+
+Copyright (C) 2011 elliot white elliot@twot.eu
+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 .
+'''
+__version__ = "0.8" ### please report bugs, suggestions etc to bugs@twot.eu ###
+
+from ink_helper import *
+from lxml import etree
+
+def drill(center, diameter, n_pt):
+ from math import sin, cos, pi
+ center = Vec2(center)
+ radius = diameter / 2.
+ out = Vec2([1, 0])
+ up = Vec2([0, 1])
+ path = Path([center + out * radius])
+ dtheta = (2 * pi) / n_pt
+ for i in range(n_pt + 1):
+ path.append(center + out * radius * cos(i * dtheta) + up * radius * sin(i * dtheta))
+ return path
+
+def t_slot(center, orient, screw_diameter, nut_diameter):
+ '''
+ make one t-slot starting
+ __
+ | |
+ -----------+ +-----+ ------
+ | ^
+ x center | screw_diameter x----------------------> orient
+ | v
+ -----------+ +-----+ ------
+ | |
+ --
+ '''
+ orient = Vec2(orient)
+ out = orient / orient.norm()
+ up = Vec2([out[1], -out[0]])
+ center = Vec2(center)
+ screw_r = screw_diameter / 2.
+ nut_r = nut_diameter / 2.
+ nut_w = screw_diameter
+ path = Path([center + up * screw_r])
+ path.append_from_last(orient)
+ path.append_from_last(up * (nut_r - screw_r))
+ path.append_from_last(out * (nut_w))
+ path.append_from_last(-up * (nut_r - screw_r))
+ path.append_from_last(out * (screw_length - thickness - orient.norm() - nut_w))
+ path.append_from_last(-up * screw_r)
+ path.extend(path.reflect(center, up).reverse())
+ return path
+
+def t_slots(rx, ry, sox, soy, eox, eoy, tabVec, length, dirx, diry, isTab, do_holes):
+ # root startOffset endOffset tabVec length direction isTab
+
+ divs=int(length/nomTab) # divisions
+ if not divs%2: divs-=1 # make divs odd
+ divs=float(divs)
+ tabs=(divs-1)/2 # tabs for side
+
+ if equalTabs:
+ gapWidth=tabWidth=length/divs
+ else:
+ tabWidth=nomTab
+ gapWidth=(length-tabs*nomTab)/(divs-tabs)
+
+ if isTab: # kerf correction
+ gapWidth-=correction
+ tabWidth+=correction
+ first=correction/2
+ else:
+ gapWidth+=correction
+ tabWidth-=correction
+ first=-correction/2
+
+ s=[]
+ firstVec=0; secondVec=tabVec
+ dirxN=0 if dirx else 1 # used to select operation on x or y
+ diryN=0 if diry else 1
+ (Vx,Vy)=(rx+sox*thickness,ry+soy*thickness)
+ nut_diameter = 2 * screw_diameter
+
+ step = Vec2([dirx * (tabWidth + gapWidth + firstVec * 2), diry * (tabWidth + gapWidth + firstVec * 2)])
+ orient = Vec2([-diry * (screw_length - thickness - screw_diameter), dirx * (screw_length - thickness - screw_diameter)])
+ center = Vec2(Vx + dirx * (gapWidth + tabWidth/2.), Vy + diry * (gapWidth + tabWidth/2.)) + (orient / orient.norm()) * thickness
+ slot = t_slot(center, orient, screw_diameter, nut_diameter)
+ hole = drill(center - (orient / orient.norm()) * (thickness * 1.5 + spacing), screw_diameter, 360)
+
+ slots = []
+ holes = []
+ for i in range(0, int(divs / 2), 1):
+ slots.append(slot.translate(step * i))
+ if do_holes:
+ holes.append(hole.translate(step * i))
+ holes.append(hole.translate(step * i - orient / orient.norm() * (Z - thickness) ))
+
+ out = [s.drawXY() for s in slots]
+ out.extend([h.drawXY() for h in holes])
+ return out
+
+def side(rx, ry, sox, soy, eox, eoy, tabVec, length, dirx, diry, isTab):
+ # root startOffset endOffset tabVec length direction isTab
+
+ divs=int(length/nomTab) # divisions
+ if not divs%2: divs-=1 # make divs odd
+ divs=float(divs)
+ tabs=(divs-1)/2 # tabs for side
+
+ if equalTabs:
+ gapWidth=tabWidth=length/divs
+ else:
+ tabWidth=nomTab
+ gapWidth=(length-tabs*nomTab)/(divs-tabs)
+
+ if isTab: # kerf correction
+ gapWidth-=correction
+ tabWidth+=correction
+ first=correction/2
+ else:
+ gapWidth+=correction
+ tabWidth-=correction
+ first=-correction/2
+
+ firstVec=0; secondVec=tabVec
+ dirxN=0 if dirx else 1 # used to select operation on x or y
+ diryN=0 if diry else 1
+ (Vx,Vy)=(rx+sox*thickness,ry+soy*thickness)
+ s='M '+str(Vx)+','+str(Vy)+' '
+
+ if dirxN: Vy=ry # set correct line start
+ if diryN: Vx=rx
+
+ # 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 n in range(1,int(divs)):
+ if n%2:
+ Vx=Vx+dirx*gapWidth+dirxN*firstVec+first*dirx
+ Vy=Vy+diry*gapWidth+diryN*firstVec+first*diry
+ s+='L '+str(Vx)+','+str(Vy)+' '
+ Vx=Vx+dirxN*secondVec
+ Vy=Vy+diryN*secondVec
+ s+='L '+str(Vx)+','+str(Vy)+' '
+ else:
+ Vx=Vx+dirx*tabWidth+dirxN*firstVec
+ Vy=Vy+diry*tabWidth+diryN*firstVec
+ s+='L '+str(Vx)+','+str(Vy)+' '
+ Vx=Vx+dirxN*secondVec
+ Vy=Vy+diryN*secondVec
+ s+='L '+str(Vx)+','+str(Vy)+' '
+
+ (secondVec,firstVec)=(-secondVec,-firstVec) # swap tab direction
+ first=0
+ s+='L '+str(rx+eox*thickness+dirx*length)+','+str(ry+eoy*thickness+diry*length)+' '
+ return s
+
+
+class TSlotBoxMaker(inkex.Effect):
+
+ def add_arguments(self, pars):
+ pars.add_argument('--unit', default='mm',help='Measure Units')
+ pars.add_argument('--inside',type=int, default=0, help='Int/Ext Dimension')
+ pars.add_argument('--length',type=float, default=100, help='Length of Box')
+ pars.add_argument('--width',type=float, default=100, help='Width of Box')
+ pars.add_argument('--depth',type=float, default=100, help='Height of Box')
+ pars.add_argument('--tab',type=float, default=25, help='Nominal Tab Width')
+ pars.add_argument('--equal',type=int, default=0, help='Equal/Prop Tabs')
+ pars.add_argument('--thickness',type=float, default=10, help='Thickness of Material')
+ pars.add_argument('--kerf',type=float, default=0.5, help='Kerf (width) of cut')
+ pars.add_argument('--clearance',type=float, default=0.01, help='Clearance of joints')
+ pars.add_argument('--style',type=int, default=25, help='Layout/Style')
+ pars.add_argument('--spacing',type=float, default=25, help='Part Spacing')
+ pars.add_argument('--screw_length',type=float, default=25, help='Screw Length')
+ pars.add_argument('--screw_diameter',type=float, default=25, help='Screw Diameter')
+
+ def effect(self):
+ global parent,nomTab,equalTabs,thickness,correction, screw_length, screw_diameter, spacing, Z
+
+ # Get access to main SVG document element and get its dimensions.
+ svg = self.document.getroot()
+
+ # Get the attibutes:
+ widthDoc = self.svg.unittouu(svg.get('width'))
+ heightDoc = self.svg.unittouu(svg.get('height'))
+
+ # Create a new layer.
+ layer = etree.SubElement(svg, 'g')
+ layer.set(inkex.addNS('label', 'inkscape'), 'T-Slot Box')
+ layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+
+ parent=self.svg.get_current_layer()
+
+ # Get script's option values.
+ unit=self.options.unit
+ inside=self.options.inside
+ X = self.svg.unittouu( str(self.options.length) + unit )
+ Y = self.svg.unittouu( str(self.options.width) + unit )
+ Z = self.svg.unittouu( str(self.options.depth) + unit )
+ thickness = self.svg.unittouu( str(self.options.thickness) + unit )
+ nomTab = self.svg.unittouu( str(self.options.tab) + unit )
+ equalTabs=self.options.equal
+ kerf = self.svg.unittouu( str(self.options.kerf) + unit )
+ clearance = self.svg.unittouu( str(self.options.clearance) + unit )
+ layout=self.options.style
+ spacing = self.svg.unittouu( str(self.options.spacing) + unit )
+ screw_length = self.svg.unittouu( str(self.options.screw_length) + unit )
+ screw_diameter = self.svg.unittouu( str(self.options.screw_diameter) + unit )
+
+ if inside: # if inside dimension selected correct values to outside dimension
+ X+=thickness*2
+ Y+=thickness*2
+ Z+=thickness*2
+
+ correction=kerf-clearance
+
+ # check input values mainly to avoid python errors
+ # TODO restrict values to *correct* solutions
+ 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 correction>min(X,Y,Z)/3: # crude test
+ inkex.errormsg(_('Error: Kerf/Clearence 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
+ if layout==1: # Diagramatic Layout
+ pieces=[[(2,0,0,1),(3,0,1,1),X,Z,0b1010, False, False],
+ [(1,0,0,0),(2,0,0,1),Z,Y,0b1111, False, False],
+ [(2,0,0,1),(2,0,0,1),X,Y,0b0000, True, True],
+ [(3,1,0,1),(2,0,0,1),Z,Y,0b1111, False, False],
+ [(4,1,0,2),(2,0,0,1),X,Y,0b0000, True, False],
+ [(2,0,0,1),(1,0,0,0),X,Z,0b1010, False, False]]
+ elif layout==2: # 3 Piece Layout
+ pieces=[[(2,0,0,1),(2,0,1,0),X,Z,0b1010, False, False],
+ [(1,0,0,0),(1,0,0,0),Z,Y,0b1111, False, False],
+ [(2,0,0,1),(1,0,0,0),X,Y,0b0000, False, False]]
+ elif layout==3: # Inline(compact) Layout
+ pieces=[[(1,0,0,0),(1,0,0,0),X,Y,0b0000, False, False],
+ [(2,1,0,0),(1,0,0,0),X,Y,0b0000, False, False],
+ [(3,2,0,0),(1,0,0,0),Z,Y,0b0101, False, False],
+ [(4,2,0,1),(1,0,0,0),Z,Y,0b0101, False, False],
+ [(5,2,0,2),(1,0,0,0),X,Z,0b1111, False, False],
+ [(6,3,0,2),(1,0,0,0),X,Z,0b1111, False, False]]
+ elif layout==4: # Diagramatic Layout with Alternate Tab Arrangement
+ pieces=[[(2,0,0,1),(3,0,1,1),X,Z,0b1001, False, False],
+ [(1,0,0,0),(2,0,0,1),Z,Y,0b1100, False, False],
+ [(2,0,0,1),(2,0,0,1),X,Y,0b1100, False, False],
+ [(3,1,0,1),(2,0,0,1),Z,Y,0b0110, False, False],
+ [(4,1,0,2),(2,0,0,1),X,Y,0b0110, False, False],
+ [(2,0,0,1),(1,0,0,0),X,Z,0b1100, False, False]]
+
+ for piece in 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 # root x co-ord for piece
+ y=ys*spacing+yx*X+yy*Y+yz*Z # root y co-ord for piece
+ dx=piece[2]
+ dy=piece[3]
+ tabs=piece[4]
+ slots = piece[5]
+ holes = piece[6]
+ a=tabs>>3&1; b=tabs>>2&1; c=tabs>>1&1; d=tabs&1 # extract tab status for each side
+ # generate and draw the sides of each piece
+ drawS(side(x , y , d, a, -b, a, -thickness if a else thickness, dx, 1, 0, a), layer) # side a
+ drawS(side(x+dx, y , -b, a, -b, -c, thickness if b else -thickness, dy, 0, 1, b), layer) # side b
+ drawS(side(x+dx, y+dy, -b, -c, d, -c, thickness if c else -thickness, dx, -1, 0, c), layer) # side c
+ drawS(side(x , y+dy, d, -c, d, a, -thickness if d else thickness, dy, 0, -1, d), layer) # side d
+
+ # side((rx,ry),(sox,soy),(eox,eoy),tabVec,length,(dirx,diry),isTab):
+ # root startOffset endOffset tabVec length direction isTab
+
+ if slots:
+ [drawS(slot, layer) for slot in t_slots(x , y , d, a, -b, a, -thickness if a else thickness, dx, 1, 0, a, holes)] # slot a
+ [drawS(slot, layer) for slot in t_slots(x+dx, y , -b, a, -b, -c, thickness if b else -thickness, dy, 0, 1, b, holes)] # slot b
+ [drawS(slot, layer) for slot in t_slots(x+dx, y+dy , -b, -c, d, -c, thickness if c else -thickness, dx, -1, 0, c, holes)] # slot c
+ [drawS(slot, layer) for slot in t_slots(x , y+dy , d, -c, d, a, -thickness if d else thickness, dy, 0, -1, d, holes)] # slot d
+
+# Create effect instance and apply it.
+TSlotBoxMaker().run()
diff --git a/extensions/fablabchemnitz/box_maker_t_slot/ink_helper.py b/extensions/fablabchemnitz/box_maker_t_slot/ink_helper.py
new file mode 100644
index 0000000..9be0db0
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_t_slot/ink_helper.py
@@ -0,0 +1,161 @@
+import math
+import inkex
+from lxml import etree
+
+def drawS(XYstring, parent): # Draw lines from a list
+ name='part'
+ style = { 'stroke': '#000000', 'stroke-width':'0.26458333', 'fill': 'none' }
+ drw = {'style':str(inkex.Style(style)),inkex.addNS('label','inkscape'):name,'d':XYstring}
+ etree.SubElement(parent, inkex.addNS('path','svg'), drw )
+ return
+
+class Vec2:
+
+ def __init__(self, x, y=None):
+ if y is None:
+ y = x[1]
+ x = x[0]
+ self.x = x
+ self.y = y
+
+ def norm(self):
+ return math.sqrt(self.x ** 2 + self.y ** 2)
+
+ def __getitem__(self, idx):
+ return [self.x, self.y][idx]
+
+ def __neg__(self):
+ return Vec2(-self.x, -self.y)
+
+ def __add__(self, other):
+ return Vec2(self.x + other[0], self.y + other[1])
+
+ def __sub__(self, other):
+ return self + [-other[0], -other[1]]
+
+ def __mul__(self, scalar):
+ return Vec2(self.x * scalar, self.y * scalar)
+
+ def __truediv__(self, scalar):
+ return Vec2(self.x / scalar, self.y / scalar)
+
+ def dot(self, other):
+ return self.x * other[0] + self.y * other[1]
+
+ def inner(self, other):
+ return self.dot(other)
+
+ def outer(self, other):
+ return [[self[0] * other[0], self[0] * other[1]],
+ [self[1] * other[0], self[1] * other[1]]]
+
+ def __repr__(self):
+ return 'Vec2(%s, %s)' % (self.x, self.y)
+
+ def toXY(self):
+ return '%s,%s ' % (self.x, self.y)
+
+def mat_x_vec(mat, vec):
+ return Vec2(vec.dot(mat[0]), vec.dot(mat[1]))
+
+def sign(x):
+ return 1 if x > 0 else -1
+
+class Path:
+ '''
+ a list of Vec2 points
+ '''
+ def __init__(self, path=()):
+ self.path = [Vec2(p) for p in path]
+
+ def append(self, point):
+ self.path.append(Vec2(point))
+
+ def rotate(self, center, angle):
+ '''
+ angle in degrees
+ '''
+ from math import cos, sin
+ angle *= math.pi / 180.
+ R = [[cos(angle), -sin(angle)],
+ [sin(angle), cos(angle)]]
+ out = [mat_x_vec(R, p - center) + center for p in self.path]
+ return Path(out)
+
+ def translate(self, vec):
+ return Path([p + vec for p in self.path])
+
+ def append_from_last(self, v):
+ self.path.append(self.path[-1] + v)
+
+ def extend(self, points):
+ self.path.extend(points)
+
+ def __getitem__(self, idx):
+ return self.path[idx]
+
+ def reflect(self, center, orient):
+ out = self.translate(-center)
+ R = Vec2(orient).outer(orient)
+ R = [[1 - 2 * R[0][0], 2 * R[0][1]],
+ [2 * R[1][0], 1 - 2 * R[1][1]]]
+ out = Path([mat_x_vec(R, p) for p in out])
+ out = out.translate(center)
+ return out
+
+ def reverse(self):
+ return Path(self.path[::-1])
+
+ def drawXY(self):
+ XYstring = 'M ' + 'L '.join([p.toXY() for p in self.path])
+ return XYstring
+
+ def plot(self, lt='-'):
+ from pylab import plot
+ xs = [l.x for l in self.path]
+ ys = [l.y for l in self.path]
+ plot(xs, ys, lt)
+
+
+def Vec2__test__():
+ v1 = Vec2(1, 1)
+ assert abs(v1.norm() - math.sqrt(2)) < 1e-8
+ assert abs(-v1[0] + 1) < 1e-8
+ assert abs((v1 + v1)[0] - 2) < 1e-8
+ assert abs((v1 - v1)[0] - 0) < 1e-8
+ assert abs((v1 + [1, 2]).x - 2) < 1e-8
+ assert abs((v1 - [1, 2]).x - 0) < 1e-8
+ assert (v1.dot(v1) - v1.norm() ** 2) < 1e-8
+Vec2__test__()
+
+if __name__ == '__main__':
+ from pylab import plot, figure, clf, show, axis
+ from numpy import array
+ mm = 1.
+ center = [0, 30 * mm]
+ orient = [12. * mm, 0]
+ screw_diameter = 3 * mm
+ nut_diameter = 5.5 * mm
+ nut_w = 2.5 * mm
+ screw_length = 16 * mm
+ thickness = 6 * mm
+
+ orient = Vec2(orient)
+ out = orient / math.sqrt(orient[0] ** 2 + orient[1] ** 2)
+ up = Vec2([-out[1], out[0]])
+ center = Vec2(center)
+ screw_r = screw_diameter / 2.
+ nut_r = nut_diameter / 2.
+ path = Path([center + up * screw_r])
+ path.append_from_last(orient)
+ path.append_from_last(up * (nut_r - screw_r))
+ path.append_from_last(out * (nut_w))
+ path.append_from_last(-up * (nut_r - screw_r))
+ path.append_from_last(out * (screw_length - thickness))
+ path.append_from_last(-up * screw_r)
+ rest = path.reflect(center, up).reverse()
+ path.extend(rest)
+ path.plot()
+ rest.plot('o-')
+ axis('equal')
+ show()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/box_maker_t_slot/meta.json b/extensions/fablabchemnitz/box_maker_t_slot/meta.json
new file mode 100644
index 0000000..73f3e40
--- /dev/null
+++ b/extensions/fablabchemnitz/box_maker_t_slot/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Box Maker - T-Slot",
+ "id": "fablabchemnitz.de.box_maker_t_slot",
+ "path": "box_maker_t_slot",
+ "dependent_extensions": null,
+ "original_name": "T-Slot Box Maker",
+ "original_id": "eu.twot.render.my_boxmaker",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/kchimbo/inkscape_tslot_boxmaker/blob/master/LICENSE.md",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_t_slot",
+ "fork_url": "https://github.com/kchimbo/inkscape_tslot_boxmaker",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+T-Slot",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/kchimbo",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/colorize_path_lengths/colorize_path_lengths.inx b/extensions/fablabchemnitz/colorize_path_lengths/colorize_path_lengths.inx
new file mode 100644
index 0000000..ba3f60b
--- /dev/null
+++ b/extensions/fablabchemnitz/colorize_path_lengths/colorize_path_lengths.inx
@@ -0,0 +1,28 @@
+
+
+ Colorize Path Lengths/Slants
+ fablabchemnitz.de.colorize_path_lengths
+
+
+
+
+
+ 12
+ 25
+ 40
+ 60
+ 60
+ 0.1
+ 10
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/colorize_path_lengths/colorize_path_lengths.py b/extensions/fablabchemnitz/colorize_path_lengths/colorize_path_lengths.py
new file mode 100644
index 0000000..b61873b
--- /dev/null
+++ b/extensions/fablabchemnitz/colorize_path_lengths/colorize_path_lengths.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+'''
+pathselection.py
+
+Sunabe kazumichi 2009/9/29
+http://dp48069596.lolipop.jp/
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+'''
+import inkex, locale
+import math
+from lxml import etree
+from inkex import paths
+from inkex import bezier
+from inkex import PathElement
+
+# Set current system locale
+locale.setlocale(locale.LC_ALL, '')
+
+def cspseglength(sp1,sp2, tolerance = 0.001):
+ bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])
+ return bezier.bezierlength(bez, tolerance)
+def csplength(csp):
+ lengths = []
+ for sp in csp:
+ lengths.append([])
+ for i in range(1,len(sp)):
+ l = cspseglength(sp[i-1],sp[i])
+ lengths[-1].append(l)
+ return lengths
+
+def roughBBox(path):
+ xmin,xMax,ymin,yMax=path[0][0][0],path[0][0][0],path[0][0][1],path[0][0][1]
+ for pathcomp in path:
+ for pt in pathcomp:
+ xmin=min(xmin,pt[0])
+ xMax=max(xMax,pt[0])
+ ymin=min(ymin,pt[1])
+ yMax=max(yMax,pt[1])
+ if xMax-xmin==0:
+ tn=0
+ else :
+ tn=(yMax-ymin)/(xMax-xmin)
+ return tn
+
+class ColorizePathLengths(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("-s", "--selection", default=True, help="select path with length or slant")
+ pars.add_argument("-1", "--len1", type=float, default=12)
+ pars.add_argument("-2", "--len2", type=float, default=25)
+ pars.add_argument("-3", "--len3", type=float, default=40)
+ pars.add_argument("-4", "--len4", type=float, default=60)
+ pars.add_argument("-5", "--len5", type=float, default=60)
+ pars.add_argument("-6", "--hor", type=float, default=0.2)
+ pars.add_argument("-7", "--ver", type=float, default=10)
+
+ def effect(self):
+ # loop over all selected paths
+ if self.options.selection=="path_lengthselection":
+ if len(self.svg.selected) > 0:
+ for node in self.svg.selection.filter(PathElement).values():
+ if node.tag == inkex.addNS('path','svg'):
+ l1,l2,l3,l4,l5=[],[],[],[],[]
+ p = paths.CubicSuperPath(inkex.paths.Path(node.get('d')))
+ slengths = csplength(p)
+ b = [slengths, p]
+
+ # path length select
+ for x in range(0, len(slengths)):
+ if sum(b[0][x]) < self.options.len1:
+ l1.append(b[1][x])
+ if self.options.len2 > sum(b[0][x]) >= self.options.len1 :
+ l2.append(b[1][x])
+ if self.options.len3 > sum(b[0][x]) >= self.options.len2 :
+ l3.append(b[1][x])
+ if self.options.len4 > sum(b[0][x]) >= self.options.len3 :
+ l4.append(b[1][x])
+ if sum(b[0][x]) >= self.options.len4 :
+ l5.append(b[1][x])
+
+ # make path
+ lensel = [l1, l2, l3, l4, l5]
+ strlen = ['#FF0001', '#00FF02', '#AAFF03', '#87CEE4', '#000FF5']
+ for i, x in zip(strlen, lensel):
+ s = {'stroke-linejoin': 'miter', 'stroke-width': '0.5px',
+ 'stroke-opacity': '1.0', 'fill-opacity': '1.0',
+ 'stroke': i, 'stroke-linecap': 'butt', 'fill': 'none'}
+ attribs={'style':str(inkex.Style(s)),'d':str(paths.Path(paths.CubicSuperPath(x).to_path().to_arrays()))}
+ etree.SubElement(node.getparent(),inkex.addNS('path','svg'),attribs)
+ else:
+ self.msg('Please select some paths first.')
+ return
+
+ if self.options.selection=="path_slantselection":
+ if len(self.svg.selected) > 0:
+ for node in self.svg.selection.filter(PathElement).values():
+ if node.tag == inkex.addNS('path','svg'):
+ hor1,ver2,slan3=[],[],[]
+ p = paths.CubicSuperPath(inkex.paths.Path(node.get('d')))
+
+ # path slant select
+ for i,x in enumerate(p):
+ tn=roughBBox(x)
+ if tnself.options.ver:
+ ver2.append(p[i])
+ else:
+ slan3.append(p[i])
+
+ # make path
+ slnsel = [hor1, ver2, slan3]
+ strsln = ['#FF0001', '#00FF02', '#000FF5']
+ for i, x in zip(strsln, slnsel):
+ s = {'stroke-linejoin': 'miter', 'stroke-width': '0.5px',
+ 'stroke-opacity': '1.0', 'fill-opacity': '1.0',
+ 'stroke': i, 'stroke-linecap': 'butt', 'fill': 'none'}
+ attribs={'style':str(inkex.Style(s)),'d':str(paths.Path(paths.CubicSuperPath(x).to_path().to_arrays()))}
+ etree.SubElement(node.getparent(), inkex.addNS('path','svg'), attribs)
+ else:
+ self.msg('Please select some paths first.')
+ return
+
+if __name__ == '__main__':
+ ColorizePathLengths().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/colorize_path_lengths/meta.json b/extensions/fablabchemnitz/colorize_path_lengths/meta.json
new file mode 100644
index 0000000..16e86e3
--- /dev/null
+++ b/extensions/fablabchemnitz/colorize_path_lengths/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Colorize Path Lengths/Slants",
+ "id": "fablabchemnitz.de.colorize_path_lengths",
+ "path": "colorize_path_lengths",
+ "dependent_extensions": null,
+ "original_name": "",
+ "original_id": "",
+ "license": "GNU GPL v2",
+ "license_url": "http://dp48069596.lolipop.jp/sd/scripts/script_inkscape/pathselection.zip",
+ "comment": "ported to Inkscape v1 manually by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/colorize_path_lengths",
+ "fork_url": "http://dp48069596.lolipop.jp/sd/scripts/script_inkscape/pathselection.zip",
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55019108",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "Sunabe Kazumichi",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/README.md b/extensions/fablabchemnitz/cutcraft/cutcraft/README.md
new file mode 100644
index 0000000..a05ed7e
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/README.md
@@ -0,0 +1,57 @@
+# cut-craft
+
+
+## Python package
+
+Note that this package is written for Python 3, however requires Python 2 compatibility for Inkscape integration.
+
+The `cutcraft` package contains components in the following categories:
+
+| Folder | Description |
+| --------- | ---------------------------------------------------- |
+| core | Core components (point, line etc). |
+| platforms | Platforms used to construct shapes (circular etc). |
+| shapes | Fundamental 3D shapes (cylinder, cone, sphere etc). |
+| supports | Vertical supports to hold the shape levels apart. |
+
+
+## Core
+
+| Module | Description |
+| --------- | --------------------------------------------------------------------------- |
+| point | A 2D point with `x` and `y` coordinates. |
+| rectangle | Two `point`s defining topleft and bottom right for a rectangle. |
+| trace | An ordered collection of `point`s. |
+| part | A collection of one or more `trace`s. |
+| line | A type of `trace` with two `point`s defining the start and end of the line. |
+| circle | A type of `trace` with `point`s defining a circle. |
+| neopixel | A type of `trace` with the `point`s defining a cutout suitable to fit a variety of [NeoPixels](https://www.adafruit.com/category/168). |
+
+
+## Shapes
+
+| Module | Description |
+| -------- | -------------------------------------- |
+| shape | The core 3D functionality for a shape. |
+| cone | A cone `shape`. |
+| cylinder | A cylinder `shape`. |
+| sphere | A 3D spherical `shape`. |
+
+> Note that the fundamental `shape`s listed above can be used flexibly considering the number of `circle` segments can be specified. For example a `cone` with 4 segments becomes a **pyramid**, and a `cylinder` with 4 segments becomes a **cube**.
+
+
+## Supports
+
+| Module | Description |
+| -------- | --------------------------------------------------- |
+| support | The core support structure functionality. |
+| pier | A pier like `support` to hold `shape` levels apart. |
+| face | A solid face to `support` `shape` levels. |
+
+
+## Python 2 vs 3 Compatibility
+
+The initial aim was to develop only for Python 3, however [Inkscape](https://inkscape.org) currently uses Python 2 as the default interpreter for extensions. As a result, the following should be noted while reviewing the code:
+
+1) The calls to `super()` are written in a way that works with both versions of Python.
+2) The `math.isclose()` function is not available in Python 2 so a local version has been created in [util.py](util.py).
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/__init__.py b/extensions/fablabchemnitz/cutcraft/cutcraft/__init__.py
new file mode 100644
index 0000000..6a4bdf6
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/__init__.py
@@ -0,0 +1,17 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/__init__.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/__init__.py
new file mode 100644
index 0000000..f33ad79
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/__init__.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from .point import Point
+from .line import Line
+from .rectangle import Rectangle
+from .trace import Trace
+from .circle import Circle
+from .part import Part
+from .neopixel import NeoPixel
+from .fingerjoint import FingerJoint
+
+__all__ = ["Point", "Line", "Rectangle", "Trace", "Circle", "Part", "NeoPixel", "FingerJoint"]
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/circle.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/circle.py
new file mode 100644
index 0000000..838605a
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/circle.py
@@ -0,0 +1,131 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from .point import Point
+from .line import Line
+from .trace import Trace
+from ..util import isclose
+from math import pi, sin, cos, asin
+
+class Circle(Trace):
+ def __init__(self, radius, segments, cuts, cutdepth=0.0, start=0.0, end=pi*2.0, rotation=0.0,
+ origin=Point(0.0, 0.0), thickness=0.0, kerf=0.0):
+ super(Circle, self).__init__()
+ self.thickness = thickness
+ self.kerf = kerf
+
+ partial = True if start != 0.0 or end != pi*2.0 else False
+
+ if cuts==0:
+ c = 0.0
+ else:
+ if self.thickness <= 0.0:
+ raise ValueError("cutcraft.circle: parameter 'thickness' not set when 'cuts' greater than zero.")
+ if cutdepth <= 0.0:
+ raise ValueError("cutcraft.circle: parameter 'cutdepth' not set when 'cuts' greater than zero.")
+ c = asin(self.thickness/2/radius)
+ if partial:
+ angles = [[rotation+start+(end-start)/segments*seg, 'SEG'] for seg in range(segments+1)] + \
+ [[rotation+start+(end-start)/(cuts+1)*cut-c, ''] for cut in range(1, cuts+1)]
+ else:
+ angles = [[rotation+end/segments*seg, 'SEG'] for seg in range(segments)] + \
+ [[rotation+end/cuts*cut-c, ''] for cut in range(cuts)]
+ angles = sorted(angles)
+ if angles[0][1] == 'CUT>':
+ angles = angles[1:] + [angles[0]]
+
+ for i, angle in enumerate(angles):
+ angle.append(self._cnext(angles, i, 'SEG'))
+ angle.append(self._cprev(angles, i, 'SEG'))
+ angle.append(self._cnext(angles, i, 'CUT>') if angle[1]=='' else None)
+
+ for i, angle in enumerate(angles):
+ if angle[1] == 'SEG':
+ angle.append([self._pos(angle[0], radius)])
+
+ for i, angle in enumerate(angles):
+ if angle[1] != 'SEG':
+ mult = -1 if angle[1] == '':
+ incut = False
+
+ if atype != 'SEG' or (atype == 'SEG' and not incut):
+ for pos in angle[6]:
+ x = origin.x + pos.x
+ y = origin.y + pos.y
+ if len(self.x)==0 or not (isclose(x, self.x[-1]) and isclose(y, self.y[-1])):
+ self.x.append(x)
+ self.y.append(y)
+
+ return
+
+ def _cnext(self, angles, i, item):
+ if i>=len(angles):
+ i=-1
+ for j, angle in enumerate(angles[i+1:]):
+ if angle[1] == item:
+ return i+1+j
+ for j, angle in enumerate(angles):
+ if angle[1] == item:
+ return j
+ return None
+
+ def _cprev(self, angles, i, item):
+ if i<=0:
+ i=len(angles)
+ for j, angle in enumerate(angles[:i][::-1]):
+ if angle[1] == item:
+ return i-j-1
+ for j, angle in enumerate(angles[::-1]):
+ if angle[1] == item:
+ return j
+ return None
+
+ def _pos(self, angle, radius):
+ return Point(sin(angle)*radius, cos(angle)*radius)
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/fingerjoint.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/fingerjoint.py
new file mode 100644
index 0000000..ef2ca7b
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/fingerjoint.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from math import floor
+
+class FingerJoint(object):
+ def __init__(self, length, fingerwidth, style, thickness=0.0):
+ super(FingerJoint, self).__init__()
+
+ self.thickness = thickness
+
+ if style in ('depth','height'):
+ self.fingers = [0.0] + \
+ [pos + fingerwidth*2.0 for pos in self._fingers(length-fingerwidth*4.0, fingerwidth)] + \
+ [length]
+ elif style=='width':
+ self.fingers = [pos + thickness for pos in self._fingers(length-thickness*2.0, fingerwidth)]
+ else:
+ raise ValueError("cutcraft.core.fingerjoin: invalid value of '{}' for parameter 'style'.".format(style))
+
+ return
+
+ def _fingers(self, length, fingerwidth):
+ count = int(floor(length / fingerwidth))
+ count = count-1 if count%2==0 else count
+ return [length/count*c for c in range(count+1)]
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/line.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/line.py
new file mode 100644
index 0000000..5a53342
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/line.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from .point import Point
+from math import sqrt
+
+def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
+ # Required as Inkscape does not include math.isclose().
+ return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
+
+class Line(object):
+ """ Line class defined by start and end Points. """
+ def __init__(self, point1, point2):
+ self.pts = (point1, point2)
+ self.x = [point1.x, point2.x]
+ self.y = [point1.y, point2.y]
+ return
+
+ def _line(self):
+ # Convert line segment into a line equation (infinite length).
+ p1 = self.pts[0]
+ p2 = self.pts[1]
+ A = (p1.y - p2.y)
+ B = (p2.x - p1.x)
+ C = (p1.x*p2.y - p2.x*p1.y)
+ return A, B, -C
+
+ def intersection(self, other):
+ # Find the intersection of the lines (infinite length - not segments)
+ L1 = self._line()
+ L2 = other._line()
+ D = L1[0] * L2[1] - L1[1] * L2[0]
+ Dx = L1[2] * L2[1] - L1[1] * L2[2]
+ Dy = L1[0] * L2[2] - L1[2] * L2[0]
+ if D != 0:
+ x = Dx / D
+ y = Dy / D
+ return Point(x, y)
+ else:
+ return None
+
+ def normal(self):
+ # Return the unit normal
+ dx = self.x[1] - self.x[0]
+ dy = self.y[1] - self.y[0]
+
+ d = sqrt(dx*dx + dy*dy)
+ if d == 0:
+ raise ValueError("cutcraft.line: parameter 'normal' could not be calculated. Check your entered parameters.")
+ return dx/d, -dy/d
+
+ def addkerf(self, kerf):
+ nx, ny = self.normal()
+ offset = Point(ny*kerf, nx*kerf)
+ self.pts = (self.pts[0] + offset, self.pts[1] + offset)
+ self.x = [self.pts[0].x, self.pts[1].x]
+ self.y = [self.pts[0].y, self.pts[1].y]
+
+ def __eq__(self, other):
+ return (self.pts == other.pts)
+
+ def __ne__(self, other):
+ return (self.pts != other.pts)
+
+ def __repr__(self):
+ return "Line(" + repr(self.pts[0]) + ", " + repr(self.pts[1]) + ")"
+
+ def __str__(self):
+ return "(" + str(self.pts[0]) + ", " + str(self.pts[1]) + ")"
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/neopixel.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/neopixel.py
new file mode 100644
index 0000000..8ab4681
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/neopixel.py
@@ -0,0 +1,65 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from .point import Point
+from .trace import Trace
+from .part import Part
+from math import pi, sin, cos, sqrt
+
+class NeoPixel(Part):
+ rings = [[1, 0.0], [6, 16.0/2.0], [12, 30.0/2.0]]
+ size = 5.5
+
+ """ Line class defined by start and end Points. """
+ def __init__(self, style='rings', origin=Point(0.0, 0.0), scale=1.0, rotate=0.0):
+ super(NeoPixel, self).__init__()
+ self.scale = scale
+
+ if style=='rings':
+ for ring in self.rings:
+ pixels = ring[0]
+ radius = ring[1] * self.scale
+ for pixel in range(pixels):
+ a = rotate + pi*2 * pixel / pixels
+ seg = self._pixel(origin + Point(sin(a) * radius, cos(a) * radius),
+ pi/4 + a)
+ self += seg
+ elif style=='strip':
+ xo = origin.x
+ yo = origin.y
+ xsize = 25.4*2.0*self.scale
+ size = self.size*self.scale
+ seg = Trace() + \
+ Point(xo-xsize/2.0, yo+size/2.0) + \
+ Point(xo-xsize/2.0, yo-size/2.0) + \
+ Point(xo+xsize/2.0, yo-size/2.0) + \
+ Point(xo+xsize/2.0, yo+size/2.0)
+ seg.close()
+ self += seg
+ return
+
+ def _pixel(self, position, rotation):
+ seg = Trace()
+ xo = position.x
+ yo = position.y
+ size = sqrt(2.0*(self.size*self.scale)**2)
+ for corner in range(4):
+ # Points added in counterclockwise direction as this is an inner cut.
+ a = rotation-2.0*pi*corner/4.0
+ seg += Point(xo + sin(a) * size/2.0, yo + cos(a) * size/2.0)
+ seg.close()
+ return seg
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/part.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/part.py
new file mode 100644
index 0000000..a8d1813
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/part.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from copy import deepcopy
+from .point import Point
+from .rectangle import Rectangle
+from .trace import Trace
+
+class Part(object):
+ """ List of traces that make up a part. """
+ def __init__(self):
+ self.traces = []
+ return
+
+ def close(self):
+ """ Close each traces back to their start. """
+ for trace in self.traces:
+ trace.close()
+ return
+
+ def applykerf(self, kerf):
+ """ Apply an offset to allow for the kerf when cutting. """
+ for trace in self.traces:
+ trace.applykerf(kerf)
+ return
+
+ def svg(self):
+ # Generate SVG string for this part.
+ return " ".join([trace.svg() for trace in self.traces])
+
+ def bbox(self):
+ bboxes = [trace.bbox() for trace in self.traces]
+ x = [p1.x for p1, p2 in bboxes] + [p2.x for p1, p2 in bboxes]
+ y = [p1.y for p1, p2 in bboxes] + [p2.y for p1, p2 in bboxes]
+ return Rectangle(Point(min(x), min(y)), Point(max(x), max(y)))
+
+ def area(self):
+ bbox = self.bbox()
+ return bbox.area()
+
+ def size(self):
+ bbox = self.bbox()
+ return bbox.size()
+
+ def __add__(self, other):
+ p = Part()
+ if isinstance(other, Part):
+ p.traces = self.traces + deepcopy(other.traces)
+ elif isinstance(other, Trace):
+ p.traces = deepcopy(self.traces)
+ p.traces.append(other)
+ elif isinstance(other, Point):
+ p.traces = self.traces
+ for trace in p.traces:
+ trace.offset(other)
+ else:
+ raise RuntimeError("Can only add a Part, Trace or Point to an existing Part.")
+ return p
+
+ def __iadd__(self, other):
+ if isinstance(other, Part):
+ self.traces.extend(other.traces)
+ elif isinstance(other, Trace):
+ self.traces.append(deepcopy(other))
+ elif isinstance(other, Point):
+ for trace in self.traces:
+ trace.offset(other)
+ else:
+ raise RuntimeError("Can only add a Part, Trace or Point to an existing Part.")
+ return self
+
+ def __repr__(self):
+ return "Part" + str(self)
+
+ def __str__(self):
+ l = len(self.traces)
+ return "(" + str(l) + " trace" + ("s" if l>1 else "") + ")"
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/point.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/point.py
new file mode 100644
index 0000000..937d7c2
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/point.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from math import sqrt
+
+class Point(object):
+ """ Point (x,y) class suppporting addition for offsets. """
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ def distance(self, other):
+ """ Distance between two points. """
+ x = self.x - other.x
+ y = self.y - other.y
+ return sqrt(x*x+y*y)
+
+ def tup(self):
+ return (self.x, self.y)
+
+ def __eq__(self, other):
+ return (self.x == other.x and self.y == other.y)
+
+ def __ne__(self, other):
+ return (self.x != other.x or self.y != other.y)
+
+ def __add__(self, other):
+ return Point(self.x + other.x, self.y + other.y)
+
+ def __iadd__(self, other):
+ self.x += other.x
+ self.y += other.y
+ return self
+
+ def __sub__(self, other):
+ return Point(self.x - other.x, self.y - other.y)
+
+ def __isub__(self, other):
+ self.x -= other.x
+ self.y -= other.y
+ return self
+
+ def __neg__(self):
+ return Point(-self.x, -self.y)
+
+ def __repr__(self):
+ return "Point(" + str(self.x) + ", " + str(self.y) + ")"
+
+ def __str__(self):
+ return "(" + str(self.x) + ", " + str(self.y) + ")"
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/rectangle.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/rectangle.py
new file mode 100644
index 0000000..133578d
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/rectangle.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from math import ceil, floor
+from .point import Point
+
+class Rectangle(object):
+ """ Rectangle class defined by top-left and bottom-right Points. """
+ def __init__(self, point1, point2):
+ # Correct the input points in case they are not topleft, bottomright as expected.
+ self.topleft = Point(min(point1.x, point2.x), min(point1.y, point2.y))
+ self.bottomright = Point(max(point1.x, point2.x), max(point1.y, point2.y))
+ return
+
+ def size(self):
+ # Calculate the size as: width, height.
+ return self.bottomright.x-self.topleft.x, self.bottomright.y-self.topleft.y
+
+ def area(self):
+ width, height = self.size()
+ return width*height
+
+ def expanded(self):
+ # Expand the current Rectangle out to integer boundary.
+ return Rectangle(Point(floor(self.topleft.x), floor(self.topleft.y)),
+ Point(ceil(self.bottomright.x), ceil(self.bottomright.y)))
+
+ def svg(self):
+ # Generate SVG string for this rectangle.
+ ptx = [self.topleft.x, self.bottomright.x, self.bottomright.x, self.topleft.x]
+ pty = [self.topleft.y, self.topleft.y, self.bottomright.y, self.bottomright.y]
+ return "M {} {} ".format(ptx[0], pty[0]) + \
+ " ".join(["L {} {}".format(x, y) for x, y in zip(ptx[1:], pty[1:])]) + \
+ " L {} {}".format(ptx[0], pty[0])
+
+ def __eq__(self, other):
+ return (self.topleft == other.topleft and self.bottomright == other.bottomright )
+
+ def __ne__(self, other):
+ return (self.topleft != other.topleft or self.bottomright != other.bottomright)
+
+ def __repr__(self):
+ return "Rectangle(" + repr(self.topleft) + ", " + repr(self.bottomright) + ")"
+
+ def __str__(self):
+ return "(" + str(self.topleft) + ", " + str(self.bottomright) + ")"
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/core/trace.py b/extensions/fablabchemnitz/cutcraft/cutcraft/core/trace.py
new file mode 100644
index 0000000..16dfdb3
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/core/trace.py
@@ -0,0 +1,149 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from .point import Point
+from .line import Line
+from ..util import isclose, iscloselist
+
+class Trace(object):
+ """ List of coordinates that make a boundary. """
+ def __init__(self, x=None, y=None):
+ self.x = [] if x is None else x
+ self.y = [] if y is None else y
+ self.closed = False
+ return
+
+ def close(self):
+ """ Close the trace back to the start. """
+ if not self.closed:
+ if isclose(self.x[0], self.x[-1]) and isclose(self.y[0], self.y[-1]):
+ # Start and end should be the same.
+ self.x[-1] = self.x[0]
+ self.y[-1] = self.y[0]
+ else:
+ # Add new end point to close the loop.
+ self.x.append(self.x[0])
+ self.y.append(self.y[0])
+ self.closed = True
+ return
+
+ def applykerf(self, kerf):
+ """ Apply an offset to allow for the kerf when cutting. """
+ self.close()
+
+ # Convert the points to lines.
+ lines = [Line(Point(x1, y1), Point(x2, y2)) for x1, y1, x2, y2 in
+ zip(self.x[:-1], self.y[:-1], self.x[1:], self.y[1:])]
+
+ # Add the kerf to the lines.
+ for line in lines:
+ line.addkerf(kerf)
+
+ # Extract the line intersections as the new points.
+ pts = [line1.intersection(line2) for line1, line2 in zip(lines, lines[-1:] + lines[:-1])]
+ self.clear()
+ self.x += [pt.x for pt in pts]
+ self.y += [pt.y for pt in pts]
+ self.x += self.x[:1]
+ self.y += self.y[:1]
+ return
+
+ def offset(self, pt):
+ """ Move a trace by an x/y offset. """
+ self.x = [x + pt.x for x in self.x]
+ self.y = [y + pt.y for y in self.y]
+
+ def clear(self):
+ self.x = []
+ self.y = []
+
+ def svg(self):
+ # Generate SVG string for this trace.
+ if len(self.x)<2:
+ return ""
+ return "M {} {} ".format(self.x[0], self.y[0]) + \
+ " ".join(["L {} {}".format(x, y) for x, y in zip(self.x[1:], self.y[1:])])
+
+ def bbox(self):
+ return Point(min(self.x), min(self.y)), Point(max(self.x), max(self.y))
+
+ def __len__(self):
+ return len(self.x)
+
+ def __eq__(self, other):
+ return (iscloselist(self.x,other.x) and iscloselist(self.y, other.y))
+
+ def __ne__(self, other):
+ return (not iscloselist(self.x, other.x) or not iscloselist(self.y, other.y))
+
+ def __add__(self, other):
+ new = Trace()
+ if isinstance(other, Point):
+ new.x = self.x + [other.x]
+ new.y = self.y + [other.y]
+ elif isinstance(other, Trace):
+ new.x = self.x + other.x
+ new.y = self.y + other.y
+ else:
+ raise RuntimeError("Can only add a Trace or Point to an existing Trace.")
+ return new
+
+ def __iadd__(self, other):
+ if isinstance(other, Point):
+ self.x.append(other.x)
+ self.y.append(other.y)
+ elif isinstance(other, Trace):
+ self.x += other.x
+ self.y += other.y
+ else:
+ raise RuntimeError("Can only add a Trace or Point to an existing Trace.")
+ return self
+
+ def __repr__(self):
+ return "Trace(" + str(self.x) + ", " + str(self.y) + ")"
+
+ def __str__(self):
+ return "(" + str(self.x) + ", " + str(self.y) + ")"
+
+ def __getitem__(self, key):
+ """ Used to override the slice functionality (eg: reversing). """
+ new = Trace()
+ new.x = self.x[key]
+ new.y = self.y[key]
+ return new
+
+ def __setitem__(self, key, value):
+ """ Used to override the slice functionality. """
+ if isinstance(value, Point):
+ self.x[key] = value.x
+ self.y[key] = value.y
+ else:
+ raise RuntimeError("Can only update a single item in an existing Trace.")
+ return self
+
+ def __delitem__(self, key):
+ """ Used to override the slice functionality (eg: reversing). """
+ del self.x[key]
+ del self.y[key]
+ return self
+
+ def __reversed__(self):
+ """ Used to override the slice functionality (eg: reversing). """
+ new = Trace()
+ new.x = list(reversed(self.x))
+ new.y = list(reversed(self.y))
+ return new
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/__init__.py b/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/__init__.py
new file mode 100644
index 0000000..b1e48b6
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from .platform import Platform
+from .circular import Circular
+from .rollerframe import RollerFrame
+
+__all__ = ["Platform", "Circular", "RollerFrame"]
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/circular.py b/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/circular.py
new file mode 100644
index 0000000..c1475b5
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/circular.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.point import Point
+from ..core.part import Part
+from ..core.circle import Circle
+from .platform import Platform
+from math import pi
+
+class Circular(Platform):
+ """ Circular Platform. """
+ def __init__(self, radius, inradius, segments, cuts, cutdepth, start=0.0, end=pi*2, rotation=0.0,
+ origin=Point(0.0, 0.0), thickness=0.0):
+ super(Circular, self).__init__(thickness)
+ self.radius = radius
+ self.inradius = inradius
+ self.segments = segments
+ outer = Circle(self.radius, segments, cuts, cutdepth=cutdepth, start=start, end=end,
+ rotation=rotation, origin=origin, thickness=thickness)
+ outer.close()
+ inner = Circle(self.inradius, segments, 0, start=start, end=end,
+ rotation=rotation, origin=origin, thickness=thickness)
+ inner.close()
+ self.traces.append(outer)
+ self.traces.append(reversed(inner))
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/platform.py b/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/platform.py
new file mode 100644
index 0000000..711997f
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/platform.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.part import Part
+
+class Platform(Part):
+ def __init__(self, thickness):
+ super(Platform, self).__init__()
+ self.thickness = thickness
+ return
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/rollerframe.py b/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/rollerframe.py
new file mode 100644
index 0000000..970c93a
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/platforms/rollerframe.py
@@ -0,0 +1,378 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.point import Point
+from ..core.part import Part
+from ..core.trace import Trace
+from ..core.circle import Circle
+from ..core.fingerjoint import FingerJoint
+from ..core.neopixel import NeoPixel
+from .platform import Platform
+from ..util import intersection
+from math import pi, sqrt, asin, atan
+
+#import inkex
+
+class RollerFrame(Platform):
+ """ RollerBot Platform. """
+ def __init__(self, supwidth, wheelradius, upperradius, lowerradius,
+ facesize, barsize, primarygapwidth, secondarygapwidth,
+ scale, part_id, thickness=0.0):
+ super(RollerFrame, self).__init__(thickness)
+ self.supwidth = supwidth
+ self.barsize = barsize
+
+ cutdepth = supwidth / 3.0
+ barradius = sqrt(2.0*(barsize/2.0)**2)
+
+ facewidth = primarygapwidth*2.0 + thickness*3.0
+ faceheight = facesize + thickness
+ fjoint = FingerJoint(faceheight, thickness*2.0, 'height', thickness=thickness) # Face
+ bjoint = FingerJoint(faceheight, thickness*2.0, 'depth', thickness=thickness) # Base
+ wjoint = FingerJoint(facewidth, thickness*2.0, 'width', thickness=thickness) # Length
+
+ if part_id<5:
+ # The circular segments for the main body structure.
+
+ # Outer section.
+ x = barsize/2.0
+ y = sqrt(lowerradius**2 - x**2)
+ a = atan(x/y)
+ outer = Circle(upperradius, 72, 5, cutdepth=cutdepth, start=0.0, end=pi, thickness=thickness) + \
+ Point(0.0, -upperradius + cutdepth) + \
+ Point(-thickness, -upperradius + cutdepth) + \
+ Point(-thickness, -upperradius) + \
+ Point(-barsize/2.0, -upperradius) + \
+ Circle(lowerradius, 72, 4, cutdepth=cutdepth, start=pi+a, end=pi*2-a, thickness=thickness) + \
+ Point(-barsize/2.0, upperradius) + \
+ Point(-thickness, upperradius) + \
+ Point(-thickness, upperradius - cutdepth) + \
+ Point(0.0, upperradius - cutdepth)
+ outer.close()
+ self.traces.append(outer)
+
+ if part_id in (0,4):
+ # Central Motor Position.
+ inner = Trace() + \
+ Point(-barsize/2.0, -barsize/2.0) + \
+ Point(-barsize/2.0, barsize/2.0) + \
+ Point(-barsize/6.0, barsize/2.0) + \
+ Point(-barsize/6.0, barsize/2.0-thickness/2.0) + \
+ Point(barsize/6.0, barsize/2.0-thickness/2.0) + \
+ Point(barsize/6.0, barsize/2.0) + \
+ Point(barsize/2.0, barsize/2.0) + \
+ Point(barsize/2.0, -barsize/2.0) + \
+ Point(barsize/6.0, -barsize/2.0) + \
+ Point(barsize/6.0, -barsize/2.0+thickness/2.0) + \
+ Point(-barsize/6.0, -barsize/2.0+thickness/2.0) + \
+ Point(-barsize/6.0, -barsize/2.0)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ # Outer parts are complete, inner parts have cutouts.
+ if part_id in (1,2,3):
+ # Central Motor Position and Bar.
+ inner = Trace() + \
+ Point(-barsize/2.0*1.3, -barsize/2.0) + \
+ Point(-barsize/2.0*1.3, -barsize/2.0*0.55) + \
+ Point(-barsize/2.0, -barsize/2.0*0.55) + \
+ Point(-barsize/2.0, barsize/2.0*0.55) + \
+ Point(-barsize/2.0*1.3, barsize/2.0*0.55) + \
+ Point(-barsize/2.0*1.3, barsize/2.0) + \
+ Point(barsize/2.0, barsize/2.0) + \
+ Point(barsize/2.0, barsize/10.0) + \
+ Point(barsize/2.0*1.2, barsize/20.0) + \
+ Point(barsize/2.0*1.2, -barsize/20.0) + \
+ Point(barsize/2.0, -barsize/10.0) + \
+ Point(barsize/2.0, -barsize/2.0)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ # Upper segment cut-outs.
+ x = supwidth/2.0
+ y = sqrt((upperradius-supwidth)**2 - x**2)
+ a_outer = atan(x/y)
+ y = sqrt((barradius+supwidth)**2 - x**2)
+ a_inner = atan(x/y)
+
+ inner = self._segment(upperradius-supwidth, barradius+supwidth,
+ 0, 1, cutdepth, 0.0, a_outer, a_inner)
+ self.traces.append(reversed(inner))
+
+ fa = (pi/2.0 - self._faceangle(facesize, upperradius)) / 2.0
+ (fx, fy) = intersection(upperradius, angle=fa)
+ if 0:
+ inner = Trace() + \
+ Point(fx, -fy) + \
+ Point(fy, -fy) + \
+ Point(fy, -fx)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ oy = fy-thickness*2.0
+ (ox, oa) = intersection(upperradius, y=oy)
+ if 0:
+ inner = Trace() + \
+ Point(ox, -oy) + \
+ Point(oy, -oy) + \
+ Point(oy, -ox)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ iy = oy
+ (ix, ia) = intersection(upperradius-supwidth, y=iy)
+
+ if part_id==2:
+ inner = Circle(upperradius-supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3+a_outer, end=pi-a_outer,
+ thickness=self.thickness) + \
+ reversed(Circle(barradius+supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3+a_inner, end=pi-a_inner,
+ thickness=self.thickness))
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ # Temporary cut to remove where the face will be installed.
+ oy = fy-thickness
+ (ox, oa) = intersection(upperradius, y=oy)
+ inner = Trace() + \
+ Point(ox, -ox) + \
+ Point(oy, -ox) + \
+ Point(oy, -oy) + \
+ Point(ox, -oy)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ else:
+ inner = Circle(upperradius-supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3*1+a_outer, end=pi/2+ia,
+ thickness=self.thickness) + \
+ reversed(Circle(barradius+supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3*1+a_inner, end=pi/3*2-a_inner,
+ thickness=self.thickness))
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ ia = pi/2 - ia
+ (ix, iy) = intersection(upperradius-supwidth, angle=ia)
+
+ inner = Circle(upperradius-supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/2+ia, end=pi/3*3-a_outer,
+ thickness=self.thickness) + \
+ reversed(Circle(barradius+supwidth, 18, 0, cutdepth=cutdepth,
+ start=pi/3*2+a_inner, end=pi/3*3-a_inner,
+ thickness=self.thickness)) + \
+ Point(ix, -ix)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ # Face and base cutout slots.
+ cy = fy-thickness
+ for (x1, x2) in zip(fjoint.fingers[1::2],fjoint.fingers[2::2]):
+ inner = Trace() + \
+ Point(cy+x1, -cy) + \
+ Point(cy+x2, -cy) + \
+ Point(cy+x2, -cy-thickness) + \
+ Point(cy+x1, -cy-thickness)
+ inner.close()
+ self.traces.append(reversed(inner))
+ for (y1, y2) in zip(bjoint.fingers[1::2],bjoint.fingers[2::2]):
+ inner = Trace() + \
+ Point(cy, -cy-y2) + \
+ Point(cy, -cy-y1) + \
+ Point(cy+thickness, -cy-y1) + \
+ Point(cy+thickness, -cy-y2)
+ inner.close()
+ self.traces.append(reversed(inner))
+
+ if 0:
+ if part_id==2:
+ for seg in range(2):
+ segnext = seg*2+1
+ inner = self._segment(upperradius-supwidth, barradius+supwidth,
+ seg, segnext, cutdepth, 0.0, a_outer, a_inner)
+ self.traces.append(reversed(inner))
+ else:
+ for seg in range(3):
+ segnext = seg+1
+ inner = self._segment(upperradius-supwidth, barradius+supwidth,
+ seg, segnext, cutdepth, 0.0, a_outer, a_inner)
+ self.traces.append(reversed(inner))
+
+ # Lower segment cut-outs.
+ x = supwidth/2.0
+ y = sqrt((lowerradius-supwidth)**2 - x**2)
+ a_outer = atan(x/y)
+ y = sqrt((barradius+supwidth)**2 - x**2)
+ a_inner = atan(x/y)
+
+ for seg in range(3):
+ segnext = seg+1
+ inner = self._segment(lowerradius-supwidth, barradius+supwidth,
+ seg, segnext, cutdepth, pi, a_outer, a_inner)
+ self.traces.append(reversed(inner))
+
+ if part_id in (1,2,3):
+ r_mid = barradius+supwidth + ((upperradius-supwidth) - (barradius+supwidth))/2.0
+ self._slot(barsize/2.0 + supwidth/2.0, cutdepth*1.5, cutdepth)
+ self._slot(barsize/2.0 + supwidth/2.0, -cutdepth*1.5, cutdepth)
+ self._slot(barsize/2.0 + supwidth/2.0, r_mid+cutdepth*1.5, cutdepth)
+ self._slot(barsize/2.0 + supwidth/2.0, r_mid-cutdepth*1.5, cutdepth)
+
+ elif part_id in (5,6):
+ # The board supports.
+ x = primarygapwidth
+ y = ((upperradius-supwidth) + (barradius+supwidth))/2.0 + supwidth*2.0
+ t = Trace() + \
+ Point(0.0, 0.0) + \
+ Point(0.0, supwidth-cutdepth*2.0) + \
+ Point(-cutdepth, supwidth-cutdepth*2.0) + \
+ Point(-cutdepth, supwidth-cutdepth*1.0) + \
+ Point(0.0, supwidth-cutdepth*1.0) + \
+ Point(0.0, supwidth*2.0-cutdepth*2.0) + \
+ Point(-cutdepth, supwidth*2.0-cutdepth*2.0) + \
+ Point(-cutdepth, supwidth*2.0-cutdepth*1.0) + \
+ Point(0.0, supwidth*2.0-cutdepth*1.0) + \
+ Point(0.0, y-supwidth*2.0+cutdepth*1.0) + \
+ Point(-cutdepth, y-supwidth*2.0+cutdepth*1.0) + \
+ Point(-cutdepth, y-supwidth*2.0+cutdepth*2.0) + \
+ Point(0.0, y-supwidth*2.0+cutdepth*2.0) + \
+ Point(0.0, y-supwidth+cutdepth*1.0) + \
+ Point(-cutdepth, y-supwidth+cutdepth*1.0) + \
+ Point(-cutdepth, y-supwidth+cutdepth*2.0) + \
+ Point(0.0, y-supwidth+cutdepth*2.0) + \
+ Point(0.0, y) + \
+ Point(x, y) + \
+ Point(x, y-supwidth-cutdepth*1.0) + \
+ Point(x+cutdepth, y-supwidth-cutdepth*1.0) + \
+ Point(x+cutdepth, y-supwidth-cutdepth*2.0) + \
+ Point(x, y-supwidth-cutdepth*2.0) + \
+ Point(x, supwidth-cutdepth*1.0) + \
+ Point(x+cutdepth, supwidth-cutdepth*1.0) + \
+ Point(x+cutdepth, supwidth-cutdepth*2.0) + \
+ Point(x, supwidth-cutdepth*2.0) + \
+ Point(x, 0.0)
+ t.close()
+ self.traces.append(t)
+
+ elif part_id==7:
+ # The face components.
+ t = Trace(x=[thickness], y=[thickness])
+ for i, pos in enumerate(fjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(pos, thickness)
+ t += Point(pos, 0.0)
+ else:
+ t += Point(pos, 0.0)
+ t += Point(pos, thickness)
+ t += Point(faceheight, thickness)
+ t += Point(faceheight, facewidth-thickness)
+ for i, pos in enumerate(reversed(fjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, facewidth-thickness)
+ t += Point(pos, facewidth)
+ else:
+ t += Point(pos, facewidth)
+ t += Point(pos, facewidth-thickness)
+ t += Point(thickness, facewidth-thickness)
+ for i, pos in enumerate(reversed(wjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ else:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ t.close()
+ self.traces.append(t)
+
+ elif part_id==8:
+ t = Trace(x=[thickness], y=[0.0])
+ t += Point(facewidth-thickness, 0.0)
+ for i, pos in enumerate(bjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(facewidth-thickness, pos)
+ t += Point(facewidth, pos)
+ else:
+ t += Point(facewidth, pos)
+ t += Point(facewidth-thickness, pos)
+ t += Point(facewidth-thickness, faceheight)
+ for i, pos in enumerate(reversed(wjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, faceheight)
+ t += Point(pos, faceheight-thickness)
+ else:
+ t += Point(pos, faceheight-thickness)
+ t += Point(pos, faceheight)
+ t += Point(thickness, faceheight)
+ for i, pos in enumerate(reversed(bjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ else:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ t.close()
+ self.traces.append(t)
+
+ for eye in range(2):
+ np = NeoPixel(style='rings', origin=Point(facewidth/4.0*(1+eye*2), faceheight/2.5), scale=scale, rotate=pi/2)
+ self.traces.extend(np.traces)
+ np = NeoPixel(style='strip', origin=Point(facewidth/2.0, faceheight*0.80), scale=scale)
+ self.traces.extend(np.traces)
+
+ # Camera
+ csize = 8.5*scale
+ t = Trace() + \
+ Point(facewidth/2.0 - csize/2.0, faceheight/2.5 - csize/2.0) + \
+ Point(facewidth/2.0 + csize/2.0, faceheight/2.5 - csize/2.0) + \
+ Point(facewidth/2.0 + csize/2.0, faceheight/2.5 + csize/2.0) + \
+ Point(facewidth/2.0 - csize/2.0, faceheight/2.5 + csize/2.0)
+ t.close()
+ self.traces.append(t)
+
+ def _faceangle(self, size, radius):
+ # Returns total angle required for a face.
+ o = sqrt(2.0*(size**2))*0.5
+ h = radius
+ return 2.0*asin(o/h)
+
+ def _segment(self, r_outer, r_inner, seg, segnext, cutdepth, a_offset, a_outer, a_inner):
+ # Create an inner segment cutout.
+ t = Circle(r_outer, 18, 0, cutdepth=cutdepth,
+ start=a_offset+pi/3*seg+a_outer, end=a_offset+pi/3*segnext-a_outer,
+ thickness=self.thickness) + \
+ reversed(Circle(r_inner, 18, 0, cutdepth=cutdepth,
+ start=a_offset+pi/3*seg+a_inner, end=a_offset+pi/3*segnext-a_inner,
+ thickness=self.thickness))
+ if a_offset == 0.0 and seg == 0:
+ r_mid = r_inner + (r_outer - r_inner)/2.0
+ t += Trace() + \
+ Point(self.supwidth / 2.0, r_mid - self.supwidth) + \
+ Point(self.barsize/2.0 + self.supwidth, r_mid - self.supwidth) + \
+ Point(self.barsize/2.0 + self.supwidth, r_mid + self.supwidth) + \
+ Point(self.supwidth / 2.0, r_mid + self.supwidth)
+ t.close()
+ return t
+
+ def _slot(self, x, y, cutdepth):
+ slot = Trace() + \
+ Point(x-self.thickness/2.0, y+cutdepth/2.0) + \
+ Point(x+self.thickness/2.0, y+cutdepth/2.0) + \
+ Point(x+self.thickness/2.0, y-cutdepth/2.0) + \
+ Point(x-self.thickness/2.0, y-cutdepth/2.0)
+ slot.close()
+ self.traces.append(reversed(slot))
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/__init__.py b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/__init__.py
new file mode 100644
index 0000000..bd49436
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/__init__.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from .shape import Shape
+from .box import Box
+from .cone import Cone
+from .cylinder import Cylinder
+from .sphere import Sphere
+from .rollerbot import RollerBot
+
+__all__ = ["Shape", "Box", "Cone", "Cylinder", "Sphere", "RollerBot"]
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/box.py b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/box.py
new file mode 100644
index 0000000..346d79c
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/box.py
@@ -0,0 +1,161 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.part import Part
+from ..core.point import Point
+from ..core.trace import Trace
+from ..core.fingerjoint import FingerJoint
+from .shape import Shape
+
+class Box(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self, width, depth, height, thickness, kerf, top=True, bottom=True,
+ left=True, right=True, front=True, back=True):
+ super(Box, self).__init__(thickness, kerf)
+
+ self.width = width
+ self.depth = depth
+ self.height = height
+
+ self.faces = []
+ self.parts = []
+
+ for face in range(6):
+ p = self._face(face, width, depth, height, thickness, top, bottom, left, right, front, back)
+ self.faces.append((p, face))
+ self.parts.append((p, face))
+
+ if kerf:
+ for part, _ in self.parts:
+ part.applykerf(kerf)
+
+ def _face(self, face, width, depth, height, thickness, top, bottom, left, right, front, back):
+ faces = (top, bottom, left, right, front, back)
+
+ # Check if the requested face is active for this box.
+ if faces[face] == False:
+ return None
+
+ wjoint = FingerJoint(width, thickness*2.0, 'width', thickness=thickness)
+ djoint = FingerJoint(depth, thickness*2.0, 'depth', thickness=thickness)
+ hjoint = FingerJoint(height, thickness*2.0, 'height', thickness=thickness)
+
+ if face in (0, 1):
+ t = Trace(x=[thickness], y=[thickness])
+ for i, pos in enumerate(djoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(pos, thickness)
+ t += Point(pos, 0.0)
+ else:
+ t += Point(pos, 0.0)
+ t += Point(pos, thickness)
+ t += Point(depth-thickness, thickness)
+ for i, pos in enumerate(wjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(depth-thickness, pos)
+ t += Point(depth, pos)
+ else:
+ t += Point(depth, pos)
+ t += Point(depth-thickness, pos)
+ t += Point(depth-thickness, width-thickness)
+ for i, pos in enumerate(reversed(djoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, width-thickness)
+ t += Point(pos, width)
+ else:
+ t += Point(pos, width)
+ t += Point(pos, width-thickness)
+ t += Point(thickness, width-thickness)
+ for i, pos in enumerate(reversed(wjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ else:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ elif face in (2, 3):
+ t = Trace(x=[0.0], y=[0.0])
+ for i, pos in enumerate(djoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(pos, 0.0)
+ t += Point(pos, thickness)
+ else:
+ t += Point(pos, thickness)
+ t += Point(pos, 0.0)
+ t += Point(depth, 0.0)
+ for i, pos in enumerate(hjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(depth, pos)
+ t += Point(depth-thickness, pos)
+ else:
+ t += Point(depth-thickness, pos)
+ t += Point(depth, pos)
+ t += Point(depth, height)
+ for i, pos in enumerate(reversed(djoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, height)
+ t += Point(pos, height-thickness)
+ else:
+ t += Point(pos, height-thickness)
+ t += Point(pos, height)
+ t += Point(0.0, height)
+ for i, pos in enumerate(reversed(hjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ else:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ pass
+ elif face in (4, 5):
+ t = Trace(x=[thickness], y=[0.0])
+ for i, pos in enumerate(wjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(pos, 0.0)
+ t += Point(pos, thickness)
+ else:
+ t += Point(pos, thickness)
+ t += Point(pos, 0.0)
+ t += Point(width-thickness, 0.0)
+ for i, pos in enumerate(hjoint.fingers[1:-1]):
+ if i%2==0:
+ t += Point(width-thickness, pos)
+ t += Point(width, pos)
+ else:
+ t += Point(width, pos)
+ t += Point(width-thickness, pos)
+ t += Point(width-thickness, height)
+ for i, pos in enumerate(reversed(wjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(pos, height)
+ t += Point(pos, height-thickness)
+ else:
+ t += Point(pos, height-thickness)
+ t += Point(pos, height)
+ t += Point(thickness, height)
+ for i, pos in enumerate(reversed(hjoint.fingers[1:-1])):
+ if i%2==0:
+ t += Point(thickness, pos)
+ t += Point(0.0, pos)
+ else:
+ t += Point(0.0, pos)
+ t += Point(thickness, pos)
+ pass
+
+ t.close()
+
+ return Part() + t
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/cone.py b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/cone.py
new file mode 100644
index 0000000..de0d8e8
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/cone.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.part import Part
+from ..platforms.circular import Circular
+from .shape import Shape
+
+class Cone(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self, height, radius1, radius2, segments, cuts, cutdepth, platforms,
+ thickness, kerf):
+ super(Cone, self).__init__(thickness, kerf)
+
+# levels = [p/(platforms-1)*height for p in range(platforms)]
+# radii = [radius1 + (radius2-radius1)*p/(platforms-1) for p in range(platforms)]
+
+# for level, radius in zip(levels, radii):
+# p = cc.Part()
+# p += cp.Circular(radius, segments, cuts, cutdepth, thickness=thickness, kerf=kerf).part
+# self.parts.append((p, level))
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/cylinder.py b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/cylinder.py
new file mode 100644
index 0000000..945382d
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/cylinder.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.part import Part
+from ..platforms.circular import Circular
+from ..supports.pier import Pier
+from .shape import Shape
+
+class Cylinder(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self, height, radius, inradius, segments, cuts, cutdepth, supwidth, platforms,
+ thickness, kerf):
+ super(Cylinder, self).__init__(thickness, kerf)
+
+ self.platforms = []
+ self.piers = []
+
+ # List of vertical positions for the platforms
+ levels = [float(p)/float(platforms-1)*(height-thickness) for p in range(platforms)]
+
+ for level in levels:
+ p = Circular(radius, inradius, segments, cuts, cutdepth, thickness=thickness)
+ self.platforms.append((p, level))
+ self.parts.append((p, level))
+
+ for _ in range(cuts):
+ p = Pier(height, supwidth, supwidth-cutdepth, [(level, 0.0) for level in levels], thickness=thickness)
+ self.piers.append((p, None))
+ self.parts.append((p, None))
+
+ if kerf:
+ for part, _ in self.parts:
+ part.applykerf(kerf)
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/rollerbot.py b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/rollerbot.py
new file mode 100644
index 0000000..f663941
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/rollerbot.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.part import Part
+from ..platforms.rollerframe import RollerFrame
+from ..supports.pier import Pier
+from .shape import Shape
+
+class RollerBot(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self, width, supwidth, wheelradius, upperradius, lowerradius,
+ facesize, barsize, primarygapwidth, secondarygapwidth, scale,
+ thickness, kerf):
+ super(RollerBot, self).__init__(thickness, kerf)
+
+ self.platforms = []
+ self.piers = []
+
+ cutdepth = supwidth / 3.0
+
+ for level in range(9):
+# for level in range(7):
+ p = RollerFrame(supwidth, wheelradius, upperradius, lowerradius,
+ facesize, barsize, primarygapwidth, secondarygapwidth,
+ scale, level, thickness=thickness)
+ self.platforms.append((p, 0.0))
+ self.parts.append((p, 0.0))
+
+ levels = [0.0, secondarygapwidth+thickness,
+ secondarygapwidth+primarygapwidth+thickness*2.0,
+ secondarygapwidth+primarygapwidth*2.0+thickness*3.0,
+ secondarygapwidth*2.0+primarygapwidth*2.0+thickness*4.0 ]
+ height = secondarygapwidth*2.0+primarygapwidth*2.0+thickness*5.0
+
+ for _ in range(9):
+ p = Pier(height, supwidth, supwidth-cutdepth, [(level, 0.0) for level in levels], thickness=thickness)
+ self.piers.append((p, None))
+ self.parts.append((p, None))
+
+ levels = [0.0, secondarygapwidth+thickness ]
+ height = secondarygapwidth+thickness*2.0
+
+ for _ in range(4):
+ p = Pier(height, supwidth, supwidth-cutdepth, [(level, 0.0) for level in levels], thickness=thickness)
+ self.piers.append((p, None))
+ self.parts.append((p, None))
+
+ if kerf:
+ for part, _ in self.parts:
+ part.applykerf(kerf)
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/shape.py b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/shape.py
new file mode 100644
index 0000000..e26b33e
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/shape.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+class Shape(object):
+ def __init__(self, thickness, kerf):
+ self.thickness = thickness
+ self.kerf = kerf
+ self.parts = []
+ return
+
+ def close(self):
+ for part in self.parts:
+ part[0].close()
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/sphere.py b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/sphere.py
new file mode 100644
index 0000000..b0ed982
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/shapes/sphere.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.part import Part
+from ..platforms.circular import Circular
+from .shape import Shape
+
+class Sphere(Shape):
+ """ List of segments that make up a part. """
+ def __init__(self):
+ super(Sphere, self).__init__()
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/supports/__init__.py b/extensions/fablabchemnitz/cutcraft/cutcraft/supports/__init__.py
new file mode 100644
index 0000000..e5e1200
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/supports/__init__.py
@@ -0,0 +1,21 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from .support import Support
+from .pier import Pier
+
+__all__ = ["Support", "Pier"]
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/supports/pier.py b/extensions/fablabchemnitz/cutcraft/cutcraft/supports/pier.py
new file mode 100644
index 0000000..6a036ea
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/supports/pier.py
@@ -0,0 +1,84 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.trace import Trace
+from .support import Support
+from ..util import isclose
+
+class Pier(Support):
+ """ List of segments that make up a part. """
+ def __init__(self, height, depth, cutdepth, levels, thickness):
+ super(Pier, self).__init__(height, thickness)
+
+ self.depth = depth
+ self.cutdepth = cutdepth
+
+ # The 'levels' list is defined as follows:
+ # [(height1, x-offset1), (height2, x-offset2), ...]
+ # where the values are:
+ # height: The height of the bottom of the platform (0.0=at base level, height-thickness=at top level).
+ # x-offset: The distance from the core axis for this level. Used to create slopes and curves.
+
+ ys, xs = zip(*sorted(levels))
+ if any([(h2) - (h1) < 3*thickness for h1, h2
+ in zip(ys[:-1], ys[1:])]):
+ raise RuntimeError("Pier levels are too close. Try decreasing the number of levels.")
+
+ self.topcut = isclose(ys[-1], height-thickness)
+ self.bottomcut = isclose(ys[0], 0.0)
+ self.vertical = all([isclose(x1, x2) for x1, x2 in zip(xs[:-1], xs[1:])])
+
+ # Starting at the bottom inner point, trace up the uncut side.
+ if self.vertical:
+ tx = [0.0, 0.0]
+ ty = [0.0, height]
+ else:
+ tx = list(xs)
+ ty = list(ys[:-1]) + [height]
+
+ # Add the top points.
+ xtop = xs[0]
+ xbottom = xs[-1]
+ if self.topcut:
+ tx.extend([xtop+depth-cutdepth, xtop+depth-cutdepth, xtop+depth])
+ ty.extend([height, height-thickness, height-thickness])
+ else:
+ tx.extend([xtop+depth])
+ ty.extend([height])
+
+ if self.topcut and self.bottomcut:
+ xs = xs[1:-1]
+ ys = ys[1:-1]
+ elif self.topcut:
+ xs = xs[:-1]
+ ys = ys[:-1]
+ elif self.bottomcut:
+ xs = xs[1:]
+ ys = ys[1:]
+
+ for y, x in zip(reversed(ys), reversed(xs)):
+ tx.extend([x+depth, x+depth-cutdepth, x+depth-cutdepth, x+depth])
+ ty.extend([y+thickness, y+thickness, y, y])
+
+ if self.bottomcut:
+ tx.extend([xbottom+depth, xbottom+depth-cutdepth, xbottom+depth-cutdepth, xbottom])
+ ty.extend([thickness, thickness, 0.0, 0.0])
+ else:
+ tx.extend([xbottom+depth, xbottom])
+ ty.extend([0.0, 0.0])
+
+ self.traces.append(Trace(x=tx, y=ty))
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/supports/support.py b/extensions/fablabchemnitz/cutcraft/cutcraft/supports/support.py
new file mode 100644
index 0000000..2736381
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/supports/support.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from ..core.part import Part
+
+class Support(Part):
+ """ General support. """
+ def __init__(self, height, thickness):
+ super(Support, self).__init__()
+ self.height = height
+ self.thickness = thickness
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraft/util.py b/extensions/fablabchemnitz/cutcraft/cutcraft/util.py
new file mode 100644
index 0000000..d08e7cb
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraft/util.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2018 Michael Matthews
+#
+# This file is part of CutCraft.
+#
+# CutCraft 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.
+#
+# CutCraft 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 CutCraft. If not, see .
+
+from math import pi, atan2, cos, sin, sqrt
+
+def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
+ # Required as Inkscape uses old version of Python that does not include math.isclose().
+ return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
+
+def iscloselist(a, b):
+ # Functionality of isclose() for lists of values.
+ return all([isclose(aval, bval) for aval, bval in zip(a, b)])
+
+def intersection(radius, angle=None, x=None, y=None):
+ # With a circle of a given radius determine the intercepts for an angle, x or y coordinate.
+ if angle:
+ # Returns (x,y) tuple of intercept.
+ return (cos(angle)*radius, sin(angle)*radius)
+ elif x:
+ y = sqrt((radius)**2 - x**2)
+ return (y, atan2(y, x))
+ elif y:
+ x = sqrt((radius)**2 - y**2)
+ return (x, atan2(y, x))
+ else:
+ raise ValueError("Invalid values passed to intersection().")
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraftbox.inx b/extensions/fablabchemnitz/cutcraft/cutcraftbox.inx
new file mode 100644
index 0000000..0bdd1b2
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraftbox.inx
@@ -0,0 +1,32 @@
+
+
+ Cut-Craft Box
+ fablabchemnitz.de.cutcraft.box
+
+
+
+
+
+
+ 60.0
+ 30.0
+ 30.0
+
+ 5.0
+ 0.01
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraftbox.py b/extensions/fablabchemnitz/cutcraft/cutcraftbox.py
new file mode 100644
index 0000000..c719140
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraftbox.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+
+import inkex
+from cutcraftshape import CutCraftShape
+import cutcraft.platforms as cp
+from cutcraft.shapes import Box
+
+class CutCraftBox(CutCraftShape):
+ def __init__(self):
+ CutCraftShape.__init__(self)
+ self.arg_parser.add_argument("--width", type=float, default=6.0, help="Box Width")
+ self.arg_parser.add_argument("--depth", type=float, default=6.0, help="Box Depth")
+ self.arg_parser.add_argument("--height", type=float, default=60.0, help="Box height")
+
+ def effect(self):
+ CutCraftShape.effect(self)
+
+ width = self.svg.unittouu( str(self.options.width) + self.unit )
+ depth = self.svg.unittouu( str(self.options.depth) + self.unit )
+ height = self.svg.unittouu( str(self.options.height) + self.unit )
+
+ shape = Box(width, depth, height, self.thickness, self.kerf)
+
+ self.pack(shape)
+
+if __name__ == '__main__':
+ CutCraftBox().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraftcylinder.inx b/extensions/fablabchemnitz/cutcraft/cutcraftcylinder.inx
new file mode 100644
index 0000000..11f50c6
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraftcylinder.inx
@@ -0,0 +1,36 @@
+
+
+ Cut-Craft Cylinder
+ fablabchemnitz.de.cutcraft.cylinder
+
+
+
+
+
+
+ 60.0
+ 60.0
+ 30.0
+ 3
+ 2
+ 3
+ 6.0
+
+ 5.0
+ 0.01
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraftcylinder.py b/extensions/fablabchemnitz/cutcraft/cutcraftcylinder.py
new file mode 100644
index 0000000..8b08a60
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraftcylinder.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+import inkex
+from cutcraftshape import CutCraftShape
+import cutcraft.platforms as cp
+from cutcraft.shapes import Cylinder
+
+class CutCraftCylinder(CutCraftShape):
+ def __init__(self):
+ CutCraftShape.__init__(self)
+ self.arg_parser.add_argument("--vertices", type=int, default=3, help="Number of vertices")
+ self.arg_parser.add_argument("--levels", type=int, default=3, help="Number of levels")
+ self.arg_parser.add_argument("--supports", type=int, default=3, help="Number of supports")
+ self.arg_parser.add_argument("--supwidth", type=float, default=6.0, help="Support Width")
+ self.arg_parser.add_argument("--height", type=float, default=60.0, help="Cylinder height")
+ self.arg_parser.add_argument("--outer", type=float, default=60.0, help="Diameter of cylinder")
+ self.arg_parser.add_argument("--inner", type=float, default=30.0, help="Diameter of central hole - 0.0 for no hole")
+
+ def effect(self):
+ CutCraftShape.effect(self)
+
+ vertices = self.options.vertices
+ levels = self.options.levels
+ supports = self.options.supports
+ supwidth = self.svg.unittouu( str(self.options.supwidth) + self.unit )
+ height = self.svg.unittouu( str(self.options.height) + self.unit )
+ outer = self.svg.unittouu( str(self.options.outer) + self.unit )
+ inner = self.svg.unittouu( str(self.options.inner) + self.unit )
+
+ if outer<=inner:
+ self._error("ERROR: Outer diameter must be greater than inner diameter.")
+ exit()
+
+ shape = Cylinder(height, outer/2.0, inner/2.0, vertices, supports, supwidth/2.0, supwidth, levels,
+ self.thickness, self.kerf)
+
+ self.pack(shape)
+
+if __name__ == '__main__':
+ CutCraftCylinder().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraftrollerbot.inx b/extensions/fablabchemnitz/cutcraft/cutcraftrollerbot.inx
new file mode 100644
index 0000000..481540f
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraftrollerbot.inx
@@ -0,0 +1,30 @@
+
+
+ Cut-Craft RollerBot
+ fablabchemnitz.de.cutcraft.rollerbot
+
+
+
+
+
+
+ 12.0
+
+ 5.0
+ 0.01
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraftrollerbot.py b/extensions/fablabchemnitz/cutcraft/cutcraftrollerbot.py
new file mode 100644
index 0000000..0e6cd4d
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraftrollerbot.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+import inkex
+from cutcraftshape import CutCraftShape
+import cutcraft.platforms as cp
+from cutcraft.shapes import RollerBot
+
+class CutCraftRollerBot(CutCraftShape):
+ def __init__(self):
+ CutCraftShape.__init__(self)
+ self.arg_parser.add_argument("--supwidth", type=float, default=6.0, help="Support Width")
+
+ def effect(self):
+ CutCraftShape.effect(self)
+
+ supwidth = self.svg.unittouu( str(self.options.supwidth) + self.unit )
+
+ # Constants in the current RollerBot design.
+ wheelradius = self.svg.unittouu( str(100.0) + "mm" )
+ upperradius = self.svg.unittouu( str(92.0) + "mm" )
+ lowerradius = self.svg.unittouu( str(82.0) + "mm" )
+ facesize = self.svg.unittouu( str(50.0) + "mm" )
+ barsize = self.svg.unittouu( str(25.4) + "mm" )
+ scale = self.svg.unittouu( str(1.0) + "mm" )
+
+ primarygapwidth = self.svg.unittouu( str(70.0) + "mm" ) # Must be greater than width of Raspberry PI / Arduino.
+ secondarygapwidth = self.svg.unittouu( str(25.0) + "mm" )
+ width = primarygapwidth*2.0 + secondarygapwidth*2.0 + self.thickness*5.0
+
+ shape = RollerBot(width, supwidth, wheelradius, upperradius, lowerradius,
+ facesize, barsize, primarygapwidth, secondarygapwidth, scale,
+ self.thickness, self.kerf)
+
+ self.pack(shape)
+
+if __name__ == '__main__':
+ CutCraftRollerBot().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutcraft/cutcraftshape.py b/extensions/fablabchemnitz/cutcraft/cutcraftshape.py
new file mode 100644
index 0000000..11d80b6
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/cutcraftshape.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+
+import gettext
+import inkex
+from math import floor
+from cutcraft.core import Point, Rectangle
+from lxml import etree
+
+#TODOS
+'''
+since InkScape 1.0 / Python 3 adjustments are required to fix "TypeError: '<' not supported between instances of 'Pier' and 'Pier'". A __lt__ method has to be implemented
+"for this reasion items = sorted([(p[0].area(),p[0]) for p in shape.parts], reverse=True)" was commented out
+'''
+
+class CutCraftNode(object):
+ def __init__(self, rect):
+ self.children = []
+ self.rectangle = rect
+ self.part = None
+
+ def insert(self, part, shape):
+ if len(self.children)>0:
+ node = self.children[0].insert(part, shape)
+ if node is not None:
+ return node
+ else:
+ return self.children[1].insert(part, shape)
+
+ if self.part is not None:
+ return None
+
+ pwidth, pheight = part.bbox().expanded().size()
+ nwidth, nheight = self.rectangle.expanded().size()
+
+ if pwidth>nwidth or pheight>nheight:
+ # Too small.
+ return None
+ if pwidth==nwidth and pheight==nheight:
+ # This node fits.
+ self.part = part
+ return self
+
+ nleft, ntop = self.rectangle.expanded().topleft.tup()
+ nright, nbottom = self.rectangle.expanded().bottomright.tup()
+
+ if nwidth - pwidth > nheight - pheight:
+ r1 = Rectangle(Point(nleft, ntop),
+ Point(nleft+pwidth, nbottom))
+ r2 = Rectangle(Point(nleft+pwidth+1.0, ntop),
+ Point(nright, nbottom))
+ else:
+ r1 = Rectangle(Point(nleft, ntop),
+ Point(nright, ntop+pheight))
+ r2 = Rectangle(Point(nleft, ntop+pheight+1.0),
+ Point(nright, nbottom))
+
+ self.children = [CutCraftNode(r1), CutCraftNode(r2)]
+
+ return self.children[0].insert(part, shape)
+
+class CutCraftShape(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--active-tab", default="Options", help="The tab selected when OK was pressed")
+ pars.add_argument("--unit", default="mm", help="unit of measure for circular pitch and center diameter")
+ pars.add_argument("--thickness", type=float, default=20.0, help="Material Thickness")
+ pars.add_argument("--kerf", type=float, default=20.0, help="Laser Cutter Kerf")
+ pars.add_argument("--linethickness", default="1px", help="Line Thickness")
+
+ def effect(self):
+ self.unit = self.options.unit
+ self.thickness = self.svg.unittouu( str(self.options.thickness) + self.unit)
+ self.kerf = self.svg.unittouu( str(self.options.kerf) + self.unit)
+ self.linethickness = self.svg.unittouu(self.options.linethickness)
+
+ svg = self.document.getroot()
+ self.docwidth = self.svg.unittouu(svg.get('width'))
+ self.docheight = self.svg.unittouu(svg.get('height'))
+
+ self.parent=self.svg.get_current_layer()
+
+ layer = etree.SubElement(svg, 'g')
+ layer.set(inkex.addNS('label', 'inkscape'), 'newlayer')
+ layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+
+ def _debug(self, string):
+ inkex.debug( gettext.gettext(str(string)) )
+
+ def _error(self, string):
+ inkex.errormsg( gettext.gettext(str(string)) )
+
+ def pack(self, shape):
+ # Pack the individual parts onto the current canvas.
+ line_style = { 'stroke': '#000000',
+ 'stroke-width': str(self.linethickness),
+ 'fill': 'none' }
+
+ #items = sorted([(p[0].area(),p[0]) for p in shape.parts], reverse=True)
+ items = [(p[0].area(),p[0]) for p in shape.parts]
+ #for p in shape.parts:
+ # inkex.utils.debug(p[0])
+
+ rootnode = CutCraftNode(Rectangle(Point(0.0, 0.0), Point(floor(self.docwidth), floor(self.docheight))))
+
+ for i, (_, part) in enumerate(items):
+ node = rootnode.insert(part, self)
+ if node is None:
+ self._error("ERROR: Cannot fit parts onto canvas.\n" +
+ "Try a larger canvas and then manually arrange if required.")
+ exit()
+
+ bbox = part.bbox().expanded()
+ part += -bbox.topleft
+ part += node.rectangle.topleft
+
+ line_attribs = { 'style' : str(inkex.Style(line_style)),
+ inkex.addNS('label','inkscape') : 'Test ' + str(i),
+ 'd' : part.svg() }
+ _ = etree.SubElement(self.parent, inkex.addNS('path','svg'), line_attribs)
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cutcraft/meta.json b/extensions/fablabchemnitz/cutcraft/meta.json
new file mode 100644
index 0000000..9b76b22
--- /dev/null
+++ b/extensions/fablabchemnitz/cutcraft/meta.json
@@ -0,0 +1,25 @@
+[
+ {
+ "name": "",
+ "id": "fablabchemnitz.de.cutcraft.",
+ "path": "cutcraft",
+ "dependent_extensions": null,
+ "original_name": "",
+ "original_id": "cutcraft.",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/m-matthews/cut-craft/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/cutcraft",
+ "fork_url": "https://github.com/m-matthews/cut-craft",
+ "documentation_url": [
+ "https://stadtfabrikanten.org/display/IFM/Cut-Craft+Boxes",
+ "https://stadtfabrikanten.org/display/IFM/Cut-Craft+Cylinder",
+ "https://stadtfabrikanten.org/display/IFM/Cut-Craft+RollerBot"
+ ],
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/m-matthews",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/delaunay_triangulation/delaunay_triangulation.inx b/extensions/fablabchemnitz/delaunay_triangulation/delaunay_triangulation.inx
new file mode 100644
index 0000000..ade405d
--- /dev/null
+++ b/extensions/fablabchemnitz/delaunay_triangulation/delaunay_triangulation.inx
@@ -0,0 +1,56 @@
+
+
+ Delaunay Triangulation
+ fablabchemnitz.de.delaunay_triangulation
+
+
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -1
+
+
+
+
+
+
+
+
+ 255
+
+
+
+ Qbb Qc Qz Q12
+
+
+
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/delaunay_triangulation/delaunay_triangulation.py b/extensions/fablabchemnitz/delaunay_triangulation/delaunay_triangulation.py
new file mode 100644
index 0000000..5d44b74
--- /dev/null
+++ b/extensions/fablabchemnitz/delaunay_triangulation/delaunay_triangulation.py
@@ -0,0 +1,197 @@
+#! /usr/bin/python3
+
+'''
+Copyright (C) 2020 Scott Pakin, scott-ink@pakin.org
+
+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, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+'''
+
+import inkex
+import numpy as np
+import random
+import sys
+from inkex import Group, Line, Polygon, TextElement
+from inkex.styles import Style
+from inkex.transforms import Vector2d
+from scipy.spatial import Delaunay
+
+class DelaunayTriangulation(inkex.EffectExtension):
+ 'Overlay selected objects with triangles.'
+
+ def add_arguments(self, pars):
+ pars.add_argument('--tab', help='The selected UI tab when OK was pressed')
+ pars.add_argument('--joggling', type=inkex.Boolean, default=False, help='Use joggled input instead of merged facets')
+ pars.add_argument('--furthest', type=inkex.Boolean, default=False, help='Furthest-site Delaunay triangulation')
+ pars.add_argument('--elt_type', default='poly', help='Element type to generate ("poly" or "line")')
+ pars.add_argument('--qhull', help='Triangulation options to pass to qhull')
+ pars.add_argument('--fill_type', help='How to fill generated polygons')
+ pars.add_argument('--fill_color', type=inkex.Color, help='Fill color to use with a fill type of "specified"')
+ pars.add_argument('--stroke_type', help='How to stroke generated polygons')
+ pars.add_argument('--stroke_color', type=inkex.Color, help='Stroke color to use with a stroke type of "specified"')
+
+ def _path_points(self, elt):
+ 'Return a list of all points on a path (endpoints, not control points).'
+ pts = set()
+ first = None
+ prev = Vector2d()
+ for cmd in elt.path.to_absolute():
+ if first is None:
+ first = cmd.end_point(first, prev)
+ ep = cmd.end_point(first, prev)
+ pts.add((ep.x, ep.y))
+ prev = ep
+ return pts
+
+ def _create_styles(self, n):
+ 'Return a style to use for the generated objects.'
+ # Use either the first or the last element's stroke for line caps,
+ # stroke widths, etc.
+ fstyle = self.svg.selection.first().style
+ lstyle = self.svg.selection[-1].style
+ if self.options.stroke_type == 'last_sel':
+ style = Style(lstyle)
+ else:
+ style = Style(fstyle)
+
+ # Apply the specified fill color.
+ if self.options.fill_type == 'first_sel':
+ fcolor = fstyle.get_color('fill')
+ style.set_color(fcolor, 'fill')
+ elif self.options.fill_type == 'last_sel':
+ fcolor = lstyle.get_color('fill')
+ style.set_color(fcolor, 'fill')
+ elif self.options.fill_type == 'specified':
+ style.set_color(self.options.fill_color, 'fill')
+ elif self.options.fill_type == 'random':
+ pass # Handled below
+ else:
+ sys.exit(inkex.utils.errormsg(_('Internal error: Unrecognized fill type "%s".')) % self.options.fill_type)
+
+ # Apply the specified stroke color.
+ if self.options.stroke_type == 'first_sel':
+ scolor = fstyle.get_color('stroke')
+ style.set_color(scolor, 'stroke')
+ elif self.options.stroke_type == 'last_sel':
+ scolor = lstyle.get_color('stroke')
+ style.set_color(scolor, 'stroke')
+ elif self.options.stroke_type == 'specified':
+ style.set_color(self.options.stroke_color, 'stroke')
+ elif self.options.stroke_type == 'random':
+ pass # Handled below
+ else:
+ sys.exit(inkex.utils.errormsg(_('Internal error: Unrecognized stroke type "%s".')) % self.options.stroke_type)
+
+ # Produce n copies of the style.
+ styles = [Style(style) for i in range(n)]
+ if self.options.fill_type == 'random':
+ for s in styles:
+ r = random.randint(0, 255)
+ g = random.randint(0, 255)
+ b = random.randint(0, 255)
+ s.set_color('#%02x%02x%02x' % (r, g, b), 'fill')
+ s['fill-opacity'] = 255
+ if self.options.stroke_type == 'random':
+ for s in styles:
+ r = random.randint(0, 255)
+ g = random.randint(0, 255)
+ b = random.randint(0, 255)
+ s.set_color('#%02x%02x%02x' % (r, g, b), 'stroke')
+ s['stroke-opacity'] = 255
+
+ # Return the list of styles.
+ return [str(s) for s in styles]
+
+ def _create_polygons(self, triangles):
+ 'Render triangles as SVG polygons.'
+ styles = self._create_styles(len(triangles))
+ group = self.svg.get_current_layer().add(Group())
+ for tri, style in zip(triangles, styles):
+ tri_str = ' '.join(['%.10g %.10g' % (pt[0], pt[1]) for pt in tri])
+ poly = Polygon()
+ poly.set('points', tri_str)
+ poly.style = style
+ group.add(poly)
+
+ def _create_lines(self, triangles):
+ 'Render triangles as individual SVG lines.'
+ # First, find all unique lines.
+ lines = set()
+ for tri in triangles:
+ if len(tri) != 3:
+ sys.exit(inkex.utils.errormsg(_('Internal error: Encountered a non-triangle.')))
+ for i, j in [(0, 1), (0, 2), (1, 2)]:
+ xy1 = tuple(tri[i])
+ xy2 = tuple(tri[j])
+ if xy1 < xy2:
+ lines.update([(xy1, xy2)])
+ else:
+ lines.update([(xy2, xy1)])
+
+ # Then, create SVG line elements.
+ styles = self._create_styles(len(lines))
+ group = self.svg.get_current_layer().add(Group())
+ for ([(x1, y1), (x2, y2)], style) in zip(lines, styles):
+ line = Line()
+ line.set('x1', x1)
+ line.set('y1', y1)
+ line.set('x2', x2)
+ line.set('y2', y2)
+ line.style = style
+ group.add(line)
+
+ def effect(self):
+ 'Triangulate a set of objects.'
+
+ # Complain if the selection is empty.
+ if len(self.svg.selection) == 0:
+ return inkex.utils.errormsg(_('Please select at least one object.'))
+
+ # Acquire a set of all points from all selected objects.
+ all_points = set()
+ warned_text = False
+ for obj in self.svg.selection.values():
+ if isinstance(obj, TextElement) and not warned_text:
+ sys.stderr.write('Warning: Text elements are not currently supported. Ignoring all text in the selection.\n')
+ warned_text = True
+ all_points.update(self._path_points(obj.to_path_element()))
+
+ # Use SciPy to perform the Delaunay triangulation.
+ pts = np.array(list(all_points))
+ if len(pts) == 0:
+ return inkex.utils.errormsg(_('No points were found.'))
+ qopts = self.options.qhull
+ if self.options.joggling:
+ qopts = 'QJ ' + qopts
+ simplices = Delaunay(pts, furthest_site=self.options.furthest, qhull_options=qopts).simplices
+
+ # Create either triangles or lines, as request. Either option uses the
+ # style of the first object in the selection.
+ triangles = []
+ for s in simplices:
+ try:
+ triangles.append(pts[s])
+ except IndexError:
+ pass
+ if self.options.elt_type == 'poly':
+ self._create_polygons(triangles)
+ elif self.options.elt_type == 'line':
+ self._create_lines(triangles)
+ else:
+ return inkex.utils.errormsg(_('Internal error: unexpected element type "%s".') % self.options.elt_type)
+
+if __name__ == '__main__':
+ DelaunayTriangulation().run()
diff --git a/extensions/fablabchemnitz/delaunay_triangulation/meta.json b/extensions/fablabchemnitz/delaunay_triangulation/meta.json
new file mode 100644
index 0000000..f649191
--- /dev/null
+++ b/extensions/fablabchemnitz/delaunay_triangulation/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Delaunay Triangulation",
+ "id": "fablabchemnitz.de.delaunay_triangulation",
+ "path": "delaunay_triangulation",
+ "dependent_extensions": null,
+ "original_name": "Delaunay Triangulation",
+ "original_id": "org.pakin.filter.delaunay",
+ "license": "GNU GPL v3",
+ "license_url": "https://inkscape.org/~pakin/%E2%98%85delaunay-triangulation",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/delaunay_triangulation",
+ "fork_url": "https://inkscape.org/~pakin/%E2%98%85delaunay-triangulation",
+ "documentation_url": "",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "inkscape.org/pakin",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/draw_directions_tavel_moves/draw_directions_tavel_moves.inx b/extensions/fablabchemnitz/draw_directions_tavel_moves/draw_directions_tavel_moves.inx
new file mode 100644
index 0000000..6091748
--- /dev/null
+++ b/extensions/fablabchemnitz/draw_directions_tavel_moves/draw_directions_tavel_moves.inx
@@ -0,0 +1,65 @@
+
+
+ Draw Directions / Travel Moves
+ fablabchemnitz.de.draw_directions_tavel_moves
+
+
+
+
+
+
+
+
+ true
+ 10
+
+ false
+ false
+ true
+ true
+ 1.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ../000_about_fablabchemnitz.svg
+
+
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/draw_directions_tavel_moves/draw_directions_tavel_moves.py b/extensions/fablabchemnitz/draw_directions_tavel_moves/draw_directions_tavel_moves.py
new file mode 100644
index 0000000..5a08520
--- /dev/null
+++ b/extensions/fablabchemnitz/draw_directions_tavel_moves/draw_directions_tavel_moves.py
@@ -0,0 +1,306 @@
+#!/usr/bin/env python3
+
+from lxml import etree
+import itertools
+import inkex
+from inkex import Circle, Vector2d
+from inkex.paths import Path
+from inkex.bezier import csplength
+from debugpy.common.timestamp import current
+
+'''
+ToDos
+ - draw numbers for each travel lines next to the line
+'''
+
+class DrawDirectionsTravelMoves(inkex.EffectExtension):
+
+
+ def drawCircle(self, group, color, point):
+ style = inkex.Style({'stroke': 'none', 'fill': color})
+ startCircle = group.add(Circle(cx=str(point[0]), cy=str(point[1]), r=str(self.svg.unittouu(str(so.dotsize/2) + "px"))))
+ startCircle.style = style
+
+
+ def find_group(self, groupId):
+ ''' check if a group with a given id exists or not. Returns None if not found, else returns the group element '''
+ groups = self.document.xpath('//svg:g', namespaces=inkex.NSS)
+ for group in groups:
+ #inkex.utils.debug(str(layer.get('inkscape:label')) + " == " + layerName)
+ if group.get('id') == groupId:
+ return group
+ return None
+
+
+ def createTravelMarker(self, markerId):
+ #add new marker to defs (or overwrite if existent)
+ defs = self.svg.defs
+ for defi in defs:
+ if defi.tag == "{http://www.w3.org/2000/svg}marker" and defi.get('id') == markerId: #delete
+ defi.delete()
+ marker = inkex.Marker(id=markerId)
+ marker.set("inkscape:isstock", "true")
+ marker.set("inkscape:stockid", markerId)
+ marker.set("orient", "auto")
+ marker.set("refY", "0.0")
+ marker.set("refX", "0.0")
+ marker.set("style", "overflow:visible;")
+
+ markerPath = inkex.PathElement(id=self.svg.get_unique_id('markerId-'))
+ markerPath.style = {"fill-rule": "evenodd", "fill": "context-stroke", "stroke-width": str(self.svg.unittouu('1px'))}
+ markerPath.transform = "scale(1.0,1.0) rotate(0) translate(-6.0,0)"
+ arrowHeight = 6.0
+ markerPath.attrib["transform"] = "scale({},{}) rotate(0) translate(-{},0)".format(so.arrow_size, so.arrow_size, arrowHeight)
+ markerPath.path = "M {},0.0 L -3.0,5.0 L -3.0,-5.0 L {},0.0 z".format(arrowHeight, arrowHeight)
+
+ marker.append(markerPath)
+ defs.append(marker) #do not append before letting contain it one path. Otherwise Inkscape is going to crash immediately
+
+
+ def add_arguments(self, pars):
+ pars.add_argument("--nb_main")
+ pars.add_argument("--order", default="separate_groups")
+ pars.add_argument("--draw_dots", type=inkex.Boolean, default=True, help="Start and end point of opened (red + blue) and closed paths (green + yellow)")
+ pars.add_argument("--dotsize", type=int, default=10, help="Dot size (px)")
+ pars.add_argument("--draw_travel_moves", type=inkex.Boolean, default=False)
+ pars.add_argument("--ignore_colors", type=inkex.Boolean, default=False, help="If enabled we connect all lines by order, ignoring the stroke color (normally we have one travel line group per color)")
+ pars.add_argument("--dashed_style", type=inkex.Boolean, default=True)
+ pars.add_argument("--arrow_style", type=inkex.Boolean, default=True)
+ pars.add_argument("--arrow_size", type=float, default=True)
+ pars.add_argument("--debug", type=inkex.Boolean, default=False)
+
+ def effect(self):
+ global so
+ so = self.options
+ dotPrefix = "dots-"
+ lineSuffix = "-travelLine"
+ groupPrefix = "travelLines-"
+ ignoreWord = "ignore"
+
+ if so.nb_main == "tab_remove":
+ #remove dots
+ dots = self.document.xpath("//svg:g[starts-with(@id, '" + dotPrefix + "')]", namespaces=inkex.NSS)
+ for dot in dots:
+ dot.delete()
+
+ #remove travel lines
+ travelLines = self.document.xpath("//svg:path[contains(@id, '" + lineSuffix + "')]", namespaces=inkex.NSS)
+ for travelLine in travelLines:
+ travelLine.delete()
+
+ #remove travel groups/layers
+ travelGroups = self.document.xpath("//svg:g[starts-with(@id, '" + groupPrefix + "')]", namespaces=inkex.NSS)
+ for travelGroup in travelGroups:
+ if len(travelGroup) == 0:
+ travelGroup.delete()
+ return
+
+ #else ...
+
+ selectedPaths = [] #total list of elements to parse
+
+ def parseChildren(element):
+ if isinstance(element, inkex.PathElement) and element not in selectedPaths and lineSuffix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
+ selectedPaths.append(element)
+ children = element.getchildren()
+ if children is not None:
+ for child in children:
+ if isinstance(child, inkex.PathElement) and child not in selectedPaths and lineSuffix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
+ selectedPaths.append(child)
+ parseChildren(child) #go deeper and deeper
+
+ #check if we have selectedPaths elements or if we should parse the whole document instead
+ if len(self.svg.selected) == 0:
+ for element in self.document.getroot().iter(tag=etree.Element):
+ if isinstance(element, inkex.PathElement) and element != self.document.getroot() and lineSuffix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
+ selectedPaths.append(element)
+ else:
+ for element in self.svg.selected.values():
+ parseChildren(element)
+
+ dotGroup = self.svg.add(inkex.Group(id=self.svg.get_unique_id(dotPrefix)))
+ if so.order == "separate_layers":
+ dotGroup.set("inkscape:groupmode", "layer")
+ dotGroup.set("inkscape:label", dotPrefix + "layer")
+ if so.order == "separate_groups":
+ dotGroup.pop("inkscape:groupmode")
+ dotGroup.pop("inkscape:label")
+ if dotGroup.style.get('display') is not None:
+ dotGroup.style.pop("display") #if the group previously has been a layer (which was hidden), a display:none will be added. we don't want that
+
+ if so.arrow_style is True:
+ markerId = "travel_move_arrow"
+ self.createTravelMarker(markerId)
+
+ #collect all different stroke colors to distinguish by groups
+ strokeColors = []
+ strokeColorsAndCounts = {}
+ '''
+ the container for all paths we will loop on. Index:
+ 0 = element
+ 1 = start point
+ 2 = end point
+ '''
+ startEndPath = []
+
+ for element in selectedPaths:
+ strokeColor = element.style.get('stroke')
+ if strokeColor is None or strokeColor == "none":
+ strokeColor = "none"
+ if so.ignore_colors is True:
+ strokeColor = ignoreWord
+ strokeColors.append(strokeColor)
+ groupName = groupPrefix + strokeColor
+ travelGroup = self.find_group(groupName)
+ if travelGroup is None:
+ travelGroup = inkex.Group(id=groupName)
+ self.document.getroot().append(travelGroup)
+ else: #exists
+ self.document.getroot().append(travelGroup)
+ for child in travelGroup:
+ child.delete()
+ if so.order == "separate_layers":
+ travelGroup.set("inkscape:groupmode", "layer")
+ travelGroup.set("inkscape:label", groupName + "-layer")
+ if so.order == "separate_groups":
+ travelGroup.pop("inkscape:groupmode")
+ travelGroup.pop("inkscape:label")
+ if travelGroup.style.get('display') is not None:
+ travelGroup.style.pop("display")
+
+
+ p = element.path.transform(element.composed_transform())
+ points = list(p.end_points)
+ start = points[0]
+ end = points[len(points) - 1]
+
+ startEndPath.append([element, start, end])
+
+ if so.draw_dots is True:
+ if start[0] == end[0] and start[1] == end[1]:
+ self.drawCircle(dotGroup, '#00FF00', start)
+ self.drawCircle(dotGroup, '#FFFF00', points[1]) #draw one point which gives direction of the path
+ else: #open contour with start and end point
+ self.drawCircle(dotGroup, '#FF0000', start)
+ self.drawCircle( dotGroup, '#0000FF', end)
+
+ for strokeColor in strokeColors:
+ if strokeColor in strokeColorsAndCounts:
+ strokeColorsAndCounts[strokeColor] = strokeColorsAndCounts[strokeColor] + 1
+ else:
+ strokeColorsAndCounts[strokeColor] = 1
+
+
+ if so.draw_travel_moves is True:
+ for sc in strokeColorsAndCounts: #loop through all unique stroke colors
+ colorCount = strokeColorsAndCounts[sc] #the total color count per stroke color
+ ran = len(startEndPath) + 1 #one more because the last travel moves goes back to 0,0 (so for 3 elements (1,2,3) the range is 0 to 3 -> makes 4)
+ firstOccurence = True
+ createdMoves = 0
+ for i in range(0, ran): #loop through the item selection. nested loop. so we loop over alle elements again for each color
+ if i < ran - 1:
+ elementStroke = startEndPath[i][0].style.get('stroke')
+ currentElement = startEndPath[i][0]
+ idx = currentElement.getparent().index(currentElement)
+ travelLineId = currentElement.get('id') + lineSuffix + ("-begin" if firstOccurence is True else "")
+ if i == ran or createdMoves == colorCount:
+ elementStroke = startEndPath[i-1][0].style.get('stroke')
+ currentElement = startEndPath[i-1][0]
+ idx = currentElement.getparent().index(currentElement) + 1
+ travelLineId = currentElement.get('id') + lineSuffix + "-end"
+
+ if i < ran - 2:
+ nextElement = startEndPath[i+1][0]
+ elif i < ran - 1:
+ nextElement = startEndPath[i][0]
+ else:
+ nextElement = None
+
+ if elementStroke is None or elementStroke == "none":
+ elementStroke = "none"
+
+ if so.debug is True: inkex.utils.debug("current stroke color {}, element stroke color{}".format(sc, elementStroke))
+ if sc == elementStroke or sc == ignoreWord:
+ if firstOccurence is True:
+ travelStart = Vector2d(0,0)
+ firstOccurence = False
+ else:
+ if i <= ran - 1:
+ travelStart = startEndPath[i-1][2] #end point from this path
+
+ if so.debug is True: inkex.utils.debug("travelStart={}".format(travelStart))
+ if i < ran - 1:
+ travelEnd = startEndPath[i][1]
+ if createdMoves == colorCount:
+ travelEnd = Vector2d(0,0)
+
+ if so.debug is True: inkex.utils.debug("travelEnd={}".format(travelEnd))
+
+ if so.debug is True:
+ if i < ran - 1:
+ inkex.utils.debug("segment={},{}".format(startEndPath[i][2], startEndPath[i][1]))
+ if i == ran - 1:
+ inkex.utils.debug("segment={},{}".format(startEndPath[i-1][1], travelEnd))
+
+ travelLine = inkex.PathElement(id=travelLineId)
+ #if some objects are at svg:svg level this may cause errors
+ #if element.getparent() != self.document.getroot():
+ # travelLine.transform = element.getparent().composed_transform()
+ travelLine.style = {'stroke': ("#000000" if so.ignore_colors is True else sc), 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px')), 'marker-end': 'url(#marker1426)'}
+ if so.dashed_style is True:
+ travelLine.style['stroke-dasharray'] = '1,1'
+ travelLine.style['stroke-dashoffset'] = '0'
+ if so.arrow_style is True:
+ travelLine.style["marker-end"] = "url(#{})".format(markerId)
+
+ #this is a really dirty block of code
+ if so.order == "element_index":
+ #adding the lines at element index requires to apply transformations for start point and end point (in case they are in different groups)
+ pseudo1 = inkex.PathElement()
+ pseudo1.set('d', "M{:0.6f},{:0.6f}".format(travelStart[0],travelStart[1]))
+ pseudo2 = inkex.PathElement()
+ pseudo2.set('d', "M{:0.6f},{:0.6f}".format(travelEnd[0],travelEnd[1]))
+ if nextElement is not None:
+ if currentElement.getparent() == nextElement.getparent():
+ pseudo1.path = pseudo1.path.transform(-currentElement.composed_transform()).to_superpath()
+ pseudo2.path = pseudo2.path.transform(-nextElement.composed_transform()).to_superpath()
+ else:
+ pseudo1.path = pseudo1.path.transform(-currentElement.composed_transform()).to_superpath()
+ pseudo2.path = pseudo2.path.transform(-currentElement.composed_transform()).to_superpath()
+ else:
+ pseudo1.path = pseudo1.path.transform(-currentElement.composed_transform()).to_superpath()
+ pseudo2.path = pseudo2.path.transform(-currentElement.composed_transform()).to_superpath()
+ travelLine.path = pseudo1.path + pseudo2.get('d').replace("M", "L")
+ if so.debug is True: self.msg("travelLine={}".format(travelLine.path))
+
+ #check the length. if zero we do not add
+ slengths, stotal = csplength(travelLine.path.transform(currentElement.composed_transform()).to_superpath()) #get segment lengths and total length of path in document's internal unit
+ if stotal > 0:
+ #finally add the line
+ currentElement.getparent().insert(idx, travelLine)
+ else:
+ if so.debug is True: inkex.utils.debug("Line has length of zero")
+ else:
+ travelLine.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(travelStart[0],travelStart[1],travelEnd[0],travelEnd[1]))
+ #check the length. if zero we do not add
+ slengths, stotal = csplength(travelLine.path.transform(currentElement.composed_transform()).to_superpath()) #get segment lengths and total length of path in document's internal unit
+ if stotal > 0:
+ #finally add the line
+ self.find_group(groupPrefix + sc).add(travelLine)
+ else:
+ if so.debug is True: inkex.utils.debug("Line has length of zero")
+
+ createdMoves += 1 #each time we created a move we count up. we want to compare against the total count of that color
+ if so.debug is True: inkex.utils.debug("createdMoves={}".format(createdMoves))
+ if so.debug is True: inkex.utils.debug("-"*40)
+
+ #cleanup empty groups
+ if len(dotGroup) == 0:
+ dotGroup.delete()
+ travelGroups = self.document.xpath("//svg:g[starts-with(@id, 'travelLines-')]", namespaces=inkex.NSS)
+ for travelGroup in travelGroups:
+ if len(travelGroup) == 0:
+ travelGroup.delete()
+
+if __name__ == '__main__':
+ DrawDirectionsTravelMoves().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/draw_directions_tavel_moves/meta.json b/extensions/fablabchemnitz/draw_directions_tavel_moves/meta.json
new file mode 100644
index 0000000..00927f0
--- /dev/null
+++ b/extensions/fablabchemnitz/draw_directions_tavel_moves/meta.json
@@ -0,0 +1,20 @@
+[
+ {
+ "name": "Draw Directions / Travel Moves",
+ "id": "fablabchemnitz.de.draw_directions_travel_moces",
+ "path": "draw_directions",
+ "dependent_extensions": null,
+ "original_name": "Draw Directions / Travel Moves",
+ "original_id": "fablabchemnitz.de.draw_directions_travel_moces",
+ "license": "GNU GPL v3",
+ "license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE",
+ "comment": "Written by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/draw_directions",
+ "fork_url": null,
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=120524682",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/exponential_distort/exponential_distort.inx b/extensions/fablabchemnitz/exponential_distort/exponential_distort.inx
new file mode 100644
index 0000000..e3e46a1
--- /dev/null
+++ b/extensions/fablabchemnitz/exponential_distort/exponential_distort.inx
@@ -0,0 +1,19 @@
+
+
+ Exponential Distort
+ fablabchemnitz.de.exponential_distort
+ 1.33
+ 0
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/exponential_distort/exponential_distort.py b/extensions/fablabchemnitz/exponential_distort/exponential_distort.py
new file mode 100644
index 0000000..6272423
--- /dev/null
+++ b/extensions/fablabchemnitz/exponential_distort/exponential_distort.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+
+from __future__ import print_function
+import sys
+import math
+import inkex
+from inkex.paths import CubicSuperPath
+
+class ExponentialDistort(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ #pars.add_argument('-a', '--axis', default='x', help='distortion axis. Valid values are "x", "y", or "xy". Default is "x"')
+ pars.add_argument('-x', '--exponent', type=float, default=1.3, help='distortion factor. 1=no distortion, default 1.3')
+ pars.add_argument('-p', '--padding_perc', type=float, default=0, help='pad at origin. Padding 100% runs the exponential curve through [0.5 .. 1.0] -- default 0% runs through [0.0 .. 1.0]')
+
+ def x_exp(self, bbox, x):
+ """ reference implementation ignoring padding. unused. """
+ xmin = bbox[0] # maps to 0
+ xmax = bbox[1] # maps to 1
+ w = xmax-xmin # maps to 1
+ # convert world to math coordinates
+ xm = (x-xmin)/w
+ # apply function with properties f(1.0) == 1.0 and f(0.0) == 0.0
+ xm = xm**self.options.exponent # oh, parabola or logarithm?
+ # convert back from math to world coordinates.
+ return x*w + xmin
+
+ def x_exp_p(self, bbox, x):
+ """ parabola mapping with padding
+ CAUTION: the properties f(1.0) == 1.0 and f(0.0) == 0.0
+ do not really hold, as our x does not run the full range [0.0 .. 1.0]
+ FIXME: if you expect some c**xm here, instead of xm**c, think about c==1 ...
+ """
+ xmin = bbox[0] # maps to 0 when padding=0,
+ xmax = bbox[1] # maps to 1
+ xzero = xmin - (xmax-xmin)*self.options.padding_perc*0.01 # maps to 0, after applying padding
+ w = xmax - xzero
+ w = w * (1+self.options.padding_perc*0.01)
+ # convert world to math coordinates
+ xm = (x-xzero)/w
+ # apply function with properties f(1.0) == 1.0 and f(0.0) == 0.0
+ xm = xm**self.options.exponent # oh, parabola or logarithm?
+ return xm
+
+ def x_exp_p_inplace(self, bbox, xm):
+ """ back from mat to world coordinates, retaining xmin and xmax
+
+ Algorithm: (pre)compute a linear mapping function by explicitly
+ running x_exp_p for the two points xmin and xmax.
+ Then use the resulting linear function to map back any xm into world coordinates x.
+
+ An obvious speedup by factor 3 is waiting for you here.
+ """
+
+ xmin = bbox[0]
+ xmax = bbox[1]
+ ## assert that xmin maps to xmin and xmax maps to xmax, whatever x_exp_p() does to us.
+ f_xmin = self.x_exp_p(bbox, xmin)
+ f_xmax = self.x_exp_p(bbox, xmax)
+ f_x = self.x_exp_p(bbox, xm)
+ x = (f_x - f_xmin) * (xmax-xmin) / (f_xmax-f_xmin) + xmin
+ return x
+
+ def computeBBox(self, pts):
+ """ 'improved' version of simplepath.computeBBox, this one includes b-spline handles."""
+ xmin = None
+ xmax = None
+ ymin = None
+ ymax = None
+ for p in pts:
+ for pp in p:
+ for ppp in pp:
+ if xmin is None: xmin = ppp[0]
+ if xmax is None: xmax = ppp[0]
+ if ymin is None: ymin = ppp[1]
+ if ymax is None: ymax = ppp[1]
+
+ if xmin > ppp[0]: xmin = ppp[0]
+ if xmax < ppp[0]: xmax = ppp[0]
+ if ymin > ppp[1]: ymin = ppp[1]
+ if ymax < ppp[1]: ymax = ppp[1]
+ return (xmin, xmax, ymin, ymax)
+
+ def effect(self):
+
+ if len(self.svg.selected) == 0:
+ inkex.errormsg("Please select an object to perform the " +
+ "exponential-distort transformation on.")
+ return
+
+ for id, node in self.svg.selected.items():
+ type = node.get("{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}type", "path")
+ if node.tag != '{http://www.w3.org/2000/svg}path' or type != 'path':
+ inkex.errormsg(node.tag + " is not a path. Type="+type+". Please use 'Path->Object to Path' first.")
+ else:
+ pts = CubicSuperPath(node.get('d'))
+ bbox = self.computeBBox(pts)
+ ## bbox (60.0, 160.0, 77.0, 197.0)
+ ## pts [[[[60.0, 77.0], [60.0, 77.0], [60.0, 77.0]], [[60.0, 197.0], [60.0, 197.0], [60.0, 197.0]], [[70.0, 197.0], ...
+ for p in pts:
+ for pp in p:
+ for ppp in pp:
+ ppp[0] = self.x_exp_p_inplace(bbox, ppp[0])
+
+ node.set('d', str(pts))
+
+if __name__ == '__main__':
+ ExponentialDistort().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/exponential_distort/meta.json b/extensions/fablabchemnitz/exponential_distort/meta.json
new file mode 100644
index 0000000..ba5f856
--- /dev/null
+++ b/extensions/fablabchemnitz/exponential_distort/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Exponential Distort",
+ "id": "fablabchemnitz.de.exponential_distort",
+ "path": "exponential_distort",
+ "dependent_extensions": null,
+ "original_name": "Exponential Distort",
+ "original_id": "com.github.jnweiger.inkscape.exponential.distort",
+ "license": "MIT License",
+ "license_url": "https://github.com/jnweiger/inkscape-exponential-distort/blob/master/LICENSE",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/exponential_distort",
+ "fork_url": "https://github.com/jnweiger/inkscape-exponential-distort",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Exponential+Distort",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/jnweiger",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/fill_rectangle_with_circles/fill_rectangle_with_circles.inx b/extensions/fablabchemnitz/fill_rectangle_with_circles/fill_rectangle_with_circles.inx
new file mode 100644
index 0000000..1e575aa
--- /dev/null
+++ b/extensions/fablabchemnitz/fill_rectangle_with_circles/fill_rectangle_with_circles.inx
@@ -0,0 +1,19 @@
+
+
+ Fill Rectangle With Circles
+ fablabchemnitz.fill_rectangle_with_circles
+ 3.0
+ 10.0
+ 30.0
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/fill_rectangle_with_circles/fill_rectangle_with_circles.py b/extensions/fablabchemnitz/fill_rectangle_with_circles/fill_rectangle_with_circles.py
new file mode 100644
index 0000000..d16515d
--- /dev/null
+++ b/extensions/fablabchemnitz/fill_rectangle_with_circles/fill_rectangle_with_circles.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+
+# Program allowing the addition of small grey dots in rectangles created using Inkscape.
+
+# Thomas Guzik, thomas.guzik@laposte.net
+# Leo 130 contact@avilab.fr
+# Corentin Bettiol - corentin-bettiol@hotmail.fr
+
+# -Creative Commons License
+# -This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
+# -http://creativecommons.org/licenses/by-nc-sa/4.0/
+
+import inkex
+from lxml import etree
+
+def recup(selection, attrib):
+ l = []
+ for i in selection:
+ selec = i
+ valr = selec.get(attrib)
+ l.append(valr)
+ return l
+
+def generCircle(y, x, r):
+ circle = etree.Element('{http://www.w3.org/2000/svg}circle')
+ circle.set('cy',str(y))
+ circle.set('cx',str(x))
+ circle.set('r',str(r))
+ circle.set('fill','#000000')
+ circle.set('stroke','#000000')
+ circle.set('stroke-width','0')
+ return circle
+
+def toFloat(l):
+ for i in range(len(l)):
+ l[i] = float(l[i])
+ return l
+
+class FillRectangleWithCircle(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument('--radius', type = float, default = 3.0, help = 'Radius to enter')
+ pars.add_argument('--margin', type = float, default = 10.0, help = 'Margin between the edge of the rectangles and the circles')
+ pars.add_argument('--space', type = float, default = 30.0, help = 'Spacing between circles')
+
+ def effect(self):
+ # svg = self.svg.document.getroot()
+ # layer = etree.SubElement(svg, 'g')
+ # layer.set(inkex.addNS('label', 'inkscape'), 'Layer')
+ # layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+ # Should we add the circles on a different layer sheet?
+
+ radius = self.options.radius
+ margin = self.options.margin
+ space = self.options.space
+
+ if str(list(self.svg.selected.values())[0]) == 'rect':
+ selection = (self.svg.selected).values()
+
+ y,x,height,width = [], [], [], []
+
+ if (len(selection))>0:
+ y = toFloat(recup(selection,'y'))
+ x = toFloat(recup(selection,'x'))
+ height = toFloat(recup(selection,'height'))
+ width = toFloat(recup(selection,'width'))
+
+ for i in range(len(selection)):
+ xC = x[i] + margin
+ yC = y[i] + margin
+
+ while xC < (x[i] + width[i] - margin):
+ while yC < (y[i] + height[i] - margin):
+ self.svg.get_current_layer().append(generCircle(yC,xC,radius))
+ yC += (space + radius)
+
+ xC += space + radius
+ yC = y[i] + margin
+ else:
+ inkex.utils.debug("No rectangle(s) have been selected.")
+
+if __name__ == '__main__':
+ FillRectangleWithCircle().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/fill_rectangle_with_circles/meta.json b/extensions/fablabchemnitz/fill_rectangle_with_circles/meta.json
new file mode 100644
index 0000000..ed1436a
--- /dev/null
+++ b/extensions/fablabchemnitz/fill_rectangle_with_circles/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Fill Rectangle With Circles",
+ "id": "fablabchemnitz.de.fill_rectangle_with_circles",
+ "path": "fill_rectangle_with_circles",
+ "dependent_extensions": null,
+ "original_name": "Remplir de cercles",
+ "original_id": "org.ekips.filter.fill_circle",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/thomas-guzik/inkscape-extension-fillsquarewithcircles/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/fill_rectangle_with_circles",
+ "fork_url": "https://github.com/thomas-guzik/inkscape-extension-fillsquarewithcircles",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Fill+Rectangle+With+Circles",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/thomas-guzik",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/fillet_and_chamfer/fillet_and_chamfer.inx b/extensions/fablabchemnitz/fillet_and_chamfer/fillet_and_chamfer.inx
new file mode 100644
index 0000000..78bb543
--- /dev/null
+++ b/extensions/fablabchemnitz/fillet_and_chamfer/fillet_and_chamfer.inx
@@ -0,0 +1,28 @@
+
+
+ Fillet And Chamfer (Replaced by LPE)
+ fablabchemnitz.de.fillet_and_chamfer
+
+
+
+
+ 5.0
+
+
+
+
+
+
+ false
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/fillet_and_chamfer/fillet_and_chamfer.py b/extensions/fablabchemnitz/fillet_and_chamfer/fillet_and_chamfer.py
new file mode 100644
index 0000000..b6ebc65
--- /dev/null
+++ b/extensions/fablabchemnitz/fillet_and_chamfer/fillet_and_chamfer.py
@@ -0,0 +1,293 @@
+#!/usr/bin/env python3
+'''
+Copyright (C) 2018 Tao Wei taowei@buffalo.edu
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+'''
+import inkex
+import math
+import svgpathtools
+from lxml import etree
+
+KAPPA = 4/3. * (math.sqrt(2)-1)
+
+def cround(cnumber, ndigits):
+ return round(cnumber.real, ndigits) + round(cnumber.imag, ndigits)*1j
+
+def round_seg(seg, ndigits):
+ seg.start = cround(seg.start, ndigits)
+ seg.end = cround(seg.end, ndigits)
+ return seg
+
+def round_path(p, ndigits=6):
+ """fix for precision issue"""
+ for seg in p:
+ round_seg(seg, ndigits)
+ return p
+
+def remove_zero_length_segments(p, eps=1e-6):
+ "z will add a zero length line segment"
+ return svgpathtools.Path(*filter(lambda seg: seg.length() > eps, p))
+
+def iscontinuous(p):
+ for seg1, seg2 in zip(p[:-1], p[1:]):
+ if abs(seg1.end-seg2.start) >= 1e-6:
+ return False
+ return True
+
+def isclosedac(p):
+ return abs(p.start-p.end) < 1e-6
+
+def isclosed(p):
+ assert iscontinuous(p)
+ return isclosedac(p)
+
+from svgpathtools.path import Line, CubicBezier, QuadraticBezier, Arc
+
+def d_str(self, useSandT=False, use_closed_attrib=False, rel=False):
+ """Returns a path d-string for the path object.
+ For an explanation of useSandT and use_closed_attrib, see the
+ compatibility notes in the README."""
+
+ if use_closed_attrib:
+ self_closed = self.iscontinuous() and self.isclosed()
+ if self_closed:
+ segments = self[:-1]
+ else:
+ segments = self[:]
+ else:
+ self_closed = False
+ segments = self[:]
+
+ current_pos = None
+ parts = []
+ previous_segment = None
+ end = self[-1].end
+
+ for segment in segments:
+ seg_start = segment.start
+ # If the start of this segment does not coincide with the end of
+ # the last segment or if this segment is actually the close point
+ # of a closed path, then we should start a new subpath here.
+ if current_pos != seg_start or \
+ (self_closed and seg_start == end and use_closed_attrib):
+ if rel:
+ _seg_start = seg_start - current_pos if current_pos is not None else seg_start
+ else:
+ _seg_start = seg_start
+ parts.append('M {},{}'.format(_seg_start.real, _seg_start.imag))
+
+ if isinstance(segment, Line):
+ if rel:
+ _seg_end = segment.end - seg_start
+ else:
+ _seg_end = segment.end
+ parts.append('L {},{}'.format(_seg_end.real, _seg_end.imag))
+ elif isinstance(segment, CubicBezier):
+ if useSandT and segment.is_smooth_from(previous_segment,
+ warning_on=False):
+ if rel:
+ _seg_control2 = segment.control2 - seg_start
+ _seg_end = segment.end - seg_start
+ else:
+ _seg_control2 = segment.control2
+ _seg_end = segment.end
+ args = (_seg_control2.real, _seg_control2.imag,
+ _seg_end.real, _seg_end.imag)
+ parts.append('S {},{} {},{}'.format(*args))
+ else:
+ if rel:
+ _seg_control1 = segment.control1 - seg_start
+ _seg_control2 = segment.control2 - seg_start
+ _seg_end = segment.end - seg_start
+ else:
+ _seg_control1 = segment.control1
+ _seg_control2 = segment.control2
+ _seg_end = segment.end
+ args = (_seg_control1.real, _seg_control1.imag,
+ _seg_control2.real, _seg_control2.imag,
+ _seg_end.real, _seg_end.imag)
+ parts.append('C {},{} {},{} {},{}'.format(*args))
+ elif isinstance(segment, QuadraticBezier):
+ if useSandT and segment.is_smooth_from(previous_segment,
+ warning_on=False):
+ if rel:
+ _seg_end = segment.end - seg_start
+ else:
+ _seg_end = segment.end
+ args = _seg_end.real, _seg_end.imag
+ parts.append('T {},{}'.format(*args))
+ else:
+ if rel:
+ _seg_control = segment.control - seg_start
+ _seg_end = segment.end - seg_start
+ else:
+ _seg_control = segment.control
+ _seg_end = segment.end
+ args = (_seg_control.real, _seg_control.imag,
+ _seg_end.real, _seg_end.imag)
+ parts.append('Q {},{} {},{}'.format(*args))
+
+ elif isinstance(segment, Arc):
+ if rel:
+ _seg_end = segment.end - seg_start
+ else:
+ _seg_end = segment.end
+ args = (segment.radius.real, segment.radius.imag,
+ segment.rotation,int(segment.large_arc),
+ int(segment.sweep),_seg_end.real, _seg_end.imag)
+ parts.append('A {},{} {} {:d},{:d} {},{}'.format(*args))
+ current_pos = segment.end
+ previous_segment = segment
+
+ if self_closed:
+ parts.append('Z')
+
+ s = ' '.join(parts)
+ return s if not rel else s.lower()
+
+class FilletAndChamfer(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("-t", "--fillet_type", default="fillet", help="Selects whether using fillet or chamfer")
+ pars.add_argument("-R", "--radius", type=float, default=60.0, help="The radius")
+ pars.add_argument('--unit', default='px', help='units of measurement')
+ pars.add_argument("--remove", type=inkex.Boolean, default=False, help="If True, control object will be removed")
+
+ def addEle(self, ele, parent, props):
+ # https://inkscape.org/~pacogarcia/%E2%98%85new-version-of-shapes-extension
+ elem = etree.SubElement(parent, ele)
+ for n in props: elem.set(n,props[n])
+ return elem
+
+ def circle(self, c, r):
+ return svgpathtools.parse_path("m %f,%f a %f,%f 0 0 1 -%f,%f %f,%f 0 0 1 -%f,-%f %f,%f 0 0 1 %f,-%f %f,%f 0 0 1 %f,%f z" % tuple((c.real+r, c.imag) + (r,)*16))
+
+ def _calc_fillet_for_joint(self, p, i):
+ seg1 = p[(i) % len(p)]
+ seg2 = p[(i+1) % len(p)]
+
+ ori_p = svgpathtools.Path(seg1, seg2)
+ new_p = svgpathtools.Path()
+
+ # ignore the node if G1 continuity
+ tg1 = seg1.unit_tangent(1.0)
+ tg2 = seg2.unit_tangent(0.0)
+ cosA = abs(tg1.real * tg2.real + tg1.imag * tg2.imag)
+ if abs(cosA - 1.0) < 1e-6:
+ new_p.append(seg1.cropped(self._prev_t, 1.0))
+ self._prev_t = 0.0
+ if self._very_first_t is None:
+ self._very_first_t = 1.0
+ if not isclosedac(p) and i == len(p) - 2:
+ new_p.append(seg2.cropped(0.0, 1.0)) # add last segment if not closed
+ else:
+ cir = self.circle(seg1.end, self.options.radius)
+ # new_p.extend(cir)
+
+ intersects = ori_p.intersect(cir)
+ if len(intersects) != 2:
+ inkex.errormsg("Some fillet or chamfer may not be drawn: %d intersections!" % len(intersects))
+ new_p.append(seg1.cropped(self._prev_t, 1.0))
+ self._prev_t = 0.0
+ if self._very_first_t is None:
+ self._very_first_t = 1.0
+ if not isclosedac(p) and i == len(p) - 2:
+ new_p.append(seg2.cropped(0.0, 1.0)) # add last segment if not closed
+ else:
+ cb = []; segs = []; ts = []
+ for (T1, seg1, t1), (T2, seg2, t2) in intersects:
+ c1 = seg1.point(t1)
+ tg1 = seg1.unit_tangent(t1) * (self.options.radius * KAPPA)
+ cb.extend([c1, tg1])
+ segs.append(seg1); ts.append(t1)
+
+ # cir1 = self.circle(c1, self.options.radius * KAPPA)
+ # new_p.extend(cir1)
+ # new_p.append(svgpathtools.Line(c1, c1+tg1))
+
+ assert len(cb) == 4
+ new_p.append(segs[0].cropped(self._prev_t, ts[0]))
+ if self.options.fillet_type == 'fillet':
+ fillet = svgpathtools.CubicBezier(cb[0], cb[0]+cb[1], cb[2]-cb[3], cb[2])
+ else:
+ fillet = svgpathtools.Line(cb[0], cb[2])
+ new_p.append(fillet)
+ self._prev_t = ts[1]
+ if self._very_first_t is None:
+ self._very_first_t = ts[0]
+
+ if isclosedac(p) and i == len(p) - 1:
+ new_p.append(segs[1].cropped(ts[1], self._very_first_t)) # update first segment if closed
+ elif not isclosedac(p) and i == len(p) - 2:
+ new_p.append(segs[1].cropped(ts[1], 1.0)) # add last segment if not closed
+
+# # fix for the first segment
+# if p.isclosed():
+# new_p[0] = p[0].cropped(ts[1], self._very_first_t)
+
+# new_p.append(segs[0].cropped(ts[0], 1.0))
+# new_p.append(segs[1].cropped(0.0, ts[1]))
+# if self.options.fillet_type == 'fillet':
+# fillet = svgpathtools.CubicBezier(cb[0], cb[0]+cb[1], cb[2]-cb[3], cb[2])
+# else:
+# fillet = svgpathtools.Line(cb[0], cb[2])
+# new_p.append(fillet.reversed())
+
+ return new_p
+
+ def add_fillet_to_path(self, d):
+ p = svgpathtools.parse_path(d)
+ p = remove_zero_length_segments(p) # for z, a zero length line segment is possibly added
+
+ if len(p) <= 1:
+ return d
+
+ new_p = svgpathtools.Path()
+ self._prev_t = 0 # used as cache
+ self._very_first_t = None # update first segment if closed
+ if isclosedac(p):
+ for i in range(len(p)):
+ new_p.extend(self._calc_fillet_for_joint(p, i))
+ if not isclosedac(new_p):
+ del new_p[0] # remove first segment if closed
+ else:
+ for i in range(len(p)-1):
+ new_p.extend(self._calc_fillet_for_joint(p, i))
+ new_p = round_path(new_p, 6)
+ # inkex.errormsg(d_str(new_p, use_closed_attrib=True, rel=True))
+ return d_str(new_p, use_closed_attrib=True, rel=True)
+
+ def effect(self):
+ self.options.radius = self.svg.unittouu(str(self.options.radius) + self.options.unit)
+
+ if self.options.radius == 0:
+ return
+
+ for id, node in self.svg.selected.items():
+ _shape = etree.QName(node.tag).localname
+ if _shape != "path":
+ inkex.errormsg("Fillet and chamfer only operates on path: %s is %s" % (id, _shape))
+ else:
+ # inkex.errormsg(etree.tostring(node))
+ attrib = {k:v for k,v in node.attrib.items()}
+ attrib['d'] = self.add_fillet_to_path(attrib['d'])
+ self.addEle(inkex.addNS('path','svg'), node.getparent(), attrib)
+
+ if self.options.remove:
+ node.delete()
+
+if __name__ == '__main__':
+ FilletAndChamfer().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/fillet_and_chamfer/meta.json b/extensions/fablabchemnitz/fillet_and_chamfer/meta.json
new file mode 100644
index 0000000..bbaf470
--- /dev/null
+++ b/extensions/fablabchemnitz/fillet_and_chamfer/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Fillet And Chamfer (Replaced by LPE)",
+ "id": "fablabchemnitz.de.fillet_and_chamfer",
+ "path": "fillet_and_chamfer",
+ "dependent_extensions": null,
+ "original_name": "Fillet and Chamfer",
+ "original_id": "org.ekips.filter.filletchamfer",
+ "license": "GNU GPL v2",
+ "license_url": "https://github.com/taoari/inkscape-filletandchamfer/blob/master/filletchamfer.py",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/fillet_and_chamfer",
+ "fork_url": "https://github.com/taoari/inkscape-filletandchamfer",
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55019889",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/taoari",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/group_to_layer/group_to_layer.inx b/extensions/fablabchemnitz/group_to_layer/group_to_layer.inx
new file mode 100644
index 0000000..f05a683
--- /dev/null
+++ b/extensions/fablabchemnitz/group_to_layer/group_to_layer.inx
@@ -0,0 +1,17 @@
+
+
+ Group To Layer
+ fablabchemnitz.de.group_to_layer
+ 1
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/group_to_layer/group_to_layer.py b/extensions/fablabchemnitz/group_to_layer/group_to_layer.py
new file mode 100644
index 0000000..e677af7
--- /dev/null
+++ b/extensions/fablabchemnitz/group_to_layer/group_to_layer.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+# Copyright (c) 2012 Stuart Pernsteiner
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import inkex
+
+class GroupToLayer(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument('-d', '--depth', type = int, default = 1, help = 'Convert nested group up to DEPTH layers deep')
+
+ def effect(self):
+ depth = self.options.depth
+ self.tag_g = inkex.addNS('g', 'svg')
+ if len(self.svg.selected) > 0:
+ for node in self.svg.selected.values():
+ self.convert_group(node, depth)
+ else:
+ inkex.errormsg('Please select some objects first.')
+ return
+
+ def convert_group(self, node, depth):
+ if depth <= 0:
+ return
+ if node.tag != self.tag_g:
+ return
+ node.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+ for child in node:
+ self.convert_group(child, depth - 1)
+
+if __name__ == '__main__':
+ GroupToLayer().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/group_to_layer/meta.json b/extensions/fablabchemnitz/group_to_layer/meta.json
new file mode 100644
index 0000000..4782f36
--- /dev/null
+++ b/extensions/fablabchemnitz/group_to_layer/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Group To Layer",
+ "id": "fablabchemnitz.de.group_to_layer",
+ "path": "group_to_layer",
+ "dependent_extensions": null,
+ "original_name": "Group to Layer",
+ "original_id": "org.pernsteiner.inkscape.group_to_layer",
+ "license": "BSD-2-Clause License",
+ "license_url": "http://www.pernsteiner.org/inkscape/group_to_layer/inkscape-group_to_layer-0.1.0.zip",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/group_to_layer",
+ "fork_url": "http://www.pernsteiner.org/inkscape/group_to_layer/",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Group+To+Layer",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "pernsteiner.org/Stuart Pernsteiner",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/perspective_grid/meta.json b/extensions/fablabchemnitz/perspective_grid/meta.json
new file mode 100644
index 0000000..e64d961
--- /dev/null
+++ b/extensions/fablabchemnitz/perspective_grid/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Perspective Grid",
+ "id": "fablabchemnitz.de.perspective_grid",
+ "path": "perspective_grid",
+ "dependent_extensions": null,
+ "original_name": "Perspective Grid",
+ "original_id": "grid.perspective",
+ "license": "GNU GPL v2",
+ "license_url": "https://github.com/cds4/inkscape-grids/blob/master/grid_perspect2.py",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/perspective_grid",
+ "fork_url": "https://github.com/cds4/inkscape-grids",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Perspective+Grid",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/cds4",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/perspective_grid/perspective_grid.inx b/extensions/fablabchemnitz/perspective_grid/perspective_grid.inx
new file mode 100644
index 0000000..1f4e676
--- /dev/null
+++ b/extensions/fablabchemnitz/perspective_grid/perspective_grid.inx
@@ -0,0 +1,32 @@
+
+
+ Perspective Grid
+ fablabchemnitz.de.perspective_grid
+
+
+
+
+
+
+
+ 500
+ 300
+ 150
+ -100.0
+ 600
+ 10
+ 3
+ 2
+ 255
+
+ all
+
+
+
+
+
+
+
+
diff --git a/extensions/fablabchemnitz/perspective_grid/perspective_grid.py b/extensions/fablabchemnitz/perspective_grid/perspective_grid.py
new file mode 100644
index 0000000..fe73f2c
--- /dev/null
+++ b/extensions/fablabchemnitz/perspective_grid/perspective_grid.py
@@ -0,0 +1,279 @@
+#!/usr/bin/env python3
+'''
+Copyright (C) 2013 Carl Sorensen carl.d.sorensen@gmail.com
+Derived from grid_cartesian.py copyright (C) 2007 John Beard john.j.beard@gmail.com
+
+
+##This extension allows you to draw a two-point perspective grid in Inkscape.
+##There is a wide range of options including subdivision, subsubdivions
+##and angles of the triangular axes.
+##Custom line widths are also possible.
+##All elements are grouped with similar elements (eg all x-subdivs)
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+'''
+
+import inkex
+from math import *
+from lxml import etree
+
+def draw_SVG_line(x1, y1, x2, y2, width, stroke, name, parent):
+ style = { 'stroke': stroke, 'stroke-width':"{:0.6f}".format(width), 'fill': 'none' }
+ line_attribs = {'style':str(inkex.Style(style)),
+ inkex.addNS('label','inkscape'):name,
+ 'd':'M '+str(x1)+','+str(y1)+' L '+str(x2)+','+str(y2)}
+ etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
+
+def draw_SVG_rect(x,y,w,h, width, stroke, fill, name, parent):
+ style = { 'stroke': stroke, 'stroke-width':str(width), 'fill':fill}
+ rect_attribs = {'style':str(inkex.Style(style)),
+ inkex.addNS('label','inkscape'):name,
+ 'x':str(x), 'y':str(y), 'width':str(w), 'height':str(h)}
+ etree.SubElement(parent, inkex.addNS('rect','svg'), rect_attribs )
+
+def colorString(pickerColor):
+ longcolor = int(pickerColor)
+ if longcolor < 0:
+ longcolor = longcolor & 0xFFFFFFFF
+ return '#' + format(longcolor >> 8, '06X')
+
+class PerspectiveGrid(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--size_unit", default="", help="Units for geometry")
+ pars.add_argument("--width", type=int, default=500, help="Width of grid window")
+ pars.add_argument("--height", type=int, default=300, help="Height of grid window")
+ pars.add_argument("--p_divs", type=int, default=10, help="Number of divisions in perspective angle")
+ pars.add_argument("--horizon", type=float, default=150, help="Y coordinate of horizon")
+ pars.add_argument("--left_x", type=float, default=-250, help="X coordinate of left perspective point")
+ pars.add_argument("--right_x", type=float, default=750, help="X coordinate of right perspective point")
+ pars.add_argument("--div_th", type=float, default=2, help="Grid division line thickness (px)")
+ pars.add_argument("--border_th", type=float, default=3, help="Border Line thickness (px)")
+ pars.add_argument("--div_color", type=int, help="Grid color")
+
+ def EdgePoints(self,x0, y0, theta):
+ # find the intersection points of the line with the extended
+ # grid bounding box.
+ # Note that y is positive DOWN, not up
+ # Grid bounding box goes from (0,0) to (self.xmax, self.ymax)
+ theta_r = radians(theta)
+ if theta_r == 0:
+ return [[0,y0],[self.xmax,y0],
+ [-100, self.ymax], [self.xmax+100,0]]
+ r_bot = (self.ymax-y0)/sin(theta_r)
+ r_top = -y0/sin(theta_r)
+ r_left = -x0/cos(theta_r)
+ r_right = (self.xmax-x0)/cos(theta_r)
+ return [[0,y0+r_left*sin(theta_r)], # left
+ [self.xmax, y0+r_right*sin(theta_r)], # right
+ [x0+r_bot*cos(theta_r), self.ymax], #bottom
+ [x0+r_top*cos(theta_r), 0]] #top
+
+ def trimmed_coords(self, x1, y1, theta):
+ #find the start and end coordinates for a grid line
+ #starting at (x1, y1) with an angle of theta
+ border_points = self.EdgePoints(x1, y1, theta)
+ left = 0
+ right = 1
+ top = 3
+ bottom = 2
+ x=0
+ y=1
+ if theta > 0:
+ if border_points[left][y] < 0:
+ start_x = border_points[top][x]
+ start_y = border_points[top][y]
+ else:
+ start_x = border_points[left][x]
+ start_y = border_points[left][y]
+ if border_points[right][y] > self.ymax:
+ end_x = border_points[bottom][x]
+ end_y = border_points[bottom][y]
+ else:
+ end_x = border_points[right][x]
+ end_y = border_points[right][y]
+ else:
+ if border_points[left][y] > self.ymax:
+ start_x = border_points[bottom][x]
+ start_y = border_points[bottom][y]
+ else:
+ start_x = border_points[left][x]
+ start_y = border_points[left][y]
+ if border_points[right][y] < 0:
+ end_x = border_points[top][x]
+ end_y = border_points[top][y]
+ else:
+ end_x = border_points[right][x]
+ end_y = border_points[right][y]
+ return [[start_x,start_y],[end_x, end_y]]
+
+ def drawAngledGridLine (self, x1, y1, theta, thickness, color,
+ label, groupName):
+ end_points = self.trimmed_coords(x1, y1, theta)
+ x_start = end_points[0][0]
+ y_start = end_points[0][1]
+ x_end = end_points[1][0]
+ y_end = end_points[1][1]
+
+ if (x_start >= 0 and x_start <= self.xmax and
+ y_start >= 0 and y_start <= self.ymax and
+ x_end >= 0 and x_end <= self.xmax and
+ y_end >= 0 and y_end <= self.ymax):
+ draw_SVG_line(x_start, y_start,
+ x_end, y_end,
+ thickness, colorString(color), label, groupName)
+
+ def perspective_intersection(self, left_theta, right_theta):
+ if right_theta == 0 or left_theta == 0 or left_theta == right_theta:
+ return -100 # outside of bounding box
+ try:
+ r=(self.right_x - self.left_x)/(sin(right_theta)/tan(left_theta)-cos(right_theta))
+ y_int = self.horizon + r*sin(right_theta)
+ if y_int < 0 or y_int > self.ymax :
+ return -100 #above or below bounding box
+ return self.right_x + r*cos(right_theta)
+ except ZeroDivisionError:
+ inkex.errormsg("Perspective angle divisions resulted in division by zero. Please adjust the values for perspective points and/or angle divisions.")
+ exit()
+
+ def effect(self):
+
+ #find the pixel dimensions of the overall grid
+ self.ymax = self.svg.unittouu(str(self.options.height)+self.options.size_unit)
+ self.xmax = self.svg.unittouu(str(self.options.width)+self.options.size_unit)
+ self.horizon = self.svg.unittouu(str(self.options.horizon)+self.options.size_unit)
+ self.left_x = self.svg.unittouu(str(self.options.left_x)+self.options.size_unit)
+ self.right_x = self.svg.unittouu(str(self.options.right_x)+self.options.size_unit)
+
+ # Overwrite thickness values to use px unit properly
+ self.options.div_th = self.svg.unittouu(str(self.options.div_th) + "px")
+ self.options.border_th = self.svg.unittouu(str(self.options.border_th) + "px")
+
+ # Embed grid in group
+ #Put in in the centre of the current view
+ t = 'translate(' + str( self.svg.namedview.center[0]- self.xmax/2.0) + ',' + \
+ str( self.svg.namedview.center[1]- self.ymax/2.0) + ')'
+ g_attribs = {inkex.addNS('label','inkscape'):'Grid_Perspective:Size' + \
+ str( self.xmax)+'x'+str(self.ymax) +
+ ':Horizon'+str(self.horizon) +
+ ':LeftX'+str(self.left_x) +
+ ':RightX'+str(self.right_x),
+ 'transform':t }
+ grid = etree.SubElement(self.svg.get_current_layer(), 'g', g_attribs)
+
+ #Group for vertical gridlines
+ g_attribs = {inkex.addNS('label','inkscape'):'VerticalGridlines'}
+ gv = etree.SubElement(grid, 'g', g_attribs)
+
+ #Group for left point gridlines
+ g_attribs = {inkex.addNS('label','inkscape'):'LeftPointGridlines'}
+ glp = etree.SubElement(grid, 'g', g_attribs)
+
+ #Group for right point gridlines
+ g_attribs = {inkex.addNS('label','inkscape'):'RightPointGridlines'}
+ grp = etree.SubElement(grid, 'g', g_attribs)
+
+ draw_SVG_rect(0, 0, self.xmax, self.ymax, self.options.border_th,
+ colorString(self.options.div_color), 'none',
+ 'Border', grid) #border rectangle
+
+
+ # Calculate the extreme angles for the left and right points
+ try:
+ if self.horizon < 0 :
+ left_theta_min = atan((self.horizon-0)/(0-self.right_x))
+ left_theta_max = atan((self.ymax - self.horizon)/
+ (0-self.left_x))
+ right_theta_min = atan((0-self.horizon)/
+ (self.left_x-self.xmax))
+ right_theta_max = atan((self.horizon - self.ymax)/
+ (self.right_x - self.xmax ))
+ elif self.horizon < self.ymax :
+ left_theta_min = atan((self.horizon-0)/(self.left_x-0))
+ left_theta_max = atan((self.ymax - self.horizon)/
+ (0-self.left_x))
+ right_theta_min = atan((self.horizon-0)/
+ (self.right_x-self.xmax))
+ right_theta_max = atan((self.horizon - self.ymax)/
+ (self.right_x - self.xmax ))
+ else:
+ left_theta_min = atan((self.horizon-0)/(self.left_x-0))
+ left_theta_max = atan((self.ymax - self.horizon)/
+ (0-self.right_x))
+ right_theta_min = atan((self.horizon-0)/
+ (self.right_x-self.xmax))
+ right_theta_max = atan((self.horizon - self.ymax)/
+ (self.left_x - self.xmax ))
+ except ZeroDivisionError:
+ inkex.errormsg("Division by zero error. Please adjust the values accordingly.")
+ exit()
+ left_dtheta = (left_theta_max - left_theta_min)/float(self.options.p_divs)
+ right_dtheta = (right_theta_max - right_theta_min)/float(self.options.p_divs)
+ mid_index = self.options.p_divs/2
+ left_mid_theta = left_theta_min + mid_index * left_dtheta
+ right_mid_theta = right_theta_min + mid_index * right_dtheta
+
+
+ #DO THE PERSPECTIVE DIVISONS========================================
+ for i in range(0,self.options.p_divs+1):
+ left_theta = left_theta_min + i * left_dtheta
+ right_theta = right_theta_min + i * right_dtheta
+ self.drawAngledGridLine(self.left_x, self.horizon,
+ degrees(left_theta),
+ self.options.div_th,
+ self.options.div_color,
+ 'LeftDivPersp'+str(i),
+ glp)
+ self.drawAngledGridLine(self.right_x, self.horizon,
+ degrees(right_theta),
+ self.options.div_th,
+ self.options.div_color,
+ 'RightDivPersp'+str(i),
+ grp)
+ intersection = self.perspective_intersection(left_theta,
+ right_theta_max - i * right_dtheta)
+ if intersection > 0 and intersection < self.xmax:
+ draw_SVG_line(intersection, 0,
+ intersection, self.ymax,
+ self.options.div_th,
+ colorString(self.options.div_color),
+ 'VerticalDiv'+str(i), gv)
+ comment = """
+ intersection = self.perspective_intersection(left_theta, right_mid_theta)
+ if intersection > 0 and intersection < self.xmax:
+ draw_SVG_line(intersection, 0,
+ intersection, self.ymax,
+ self.options.div_th,
+ colorString(self.options.div_color),
+ 'VerticalDiv'+str(i), gv)
+ intersection = self.perspective_intersection(left_theta, right_theta)
+ if intersection > 0 and intersection < self.xmax:
+ draw_SVG_line(intersection, 0,
+ intersection, self.ymax,
+ self.options.div_th,
+ colorString(self.options.div_color),
+ 'VerticalDiv'+str(i), gv)
+ """
+
+ intersection = self.perspective_intersection(left_mid_theta, right_mid_theta)
+ if intersection > 0 and intersection < self.xmax:
+ draw_SVG_line(intersection, 0,
+ intersection, self.ymax,
+ self.options.div_th,
+ colorString(self.options.div_color),
+ 'VerticalDiv'+str(i), gv)
+
+if __name__ == '__main__':
+ PerspectiveGrid().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/rounder/meta.json b/extensions/fablabchemnitz/rounder/meta.json
new file mode 100644
index 0000000..da4893a
--- /dev/null
+++ b/extensions/fablabchemnitz/rounder/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Rounder",
+ "id": "fablabchemnitz.de.rounder",
+ "path": "rounder",
+ "dependent_extensions": null,
+ "original_name": "Rounder",
+ "original_id": "jtx.rounder",
+ "license": "GNU GPL v2",
+ "license_url": "https://inkscape.org/~jabiertxof/%E2%98%85rounder-04",
+ "comment": "bufgixed and ported to Inkscape v1 manually by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/rounder",
+ "fork_url": "https://inkscape.org/~jabiertxof/%E2%98%85rounder-04",
+ "documentation_url": "",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "inkscape.org/jabiertxof",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/rounder/rounder.inx b/extensions/fablabchemnitz/rounder/rounder.inx
new file mode 100644
index 0000000..0a29353
--- /dev/null
+++ b/extensions/fablabchemnitz/rounder/rounder.inx
@@ -0,0 +1,30 @@
+
+
+ Rounder
+ fablabchemnitz.de.rounder
+ 2
+ true
+ false
+ true
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/rounder/rounder.py b/extensions/fablabchemnitz/rounder/rounder.py
new file mode 100644
index 0000000..b5bb2c9
--- /dev/null
+++ b/extensions/fablabchemnitz/rounder/rounder.py
@@ -0,0 +1,165 @@
+#! /usr/bin/python
+'''
+Rounder 0.4
+Based in deprecated "Path Rounder 0.2"
+Based in radiusrand script from Aaron Spike and make it by Jabier Arraiza,
+jabier.arraiza@marker.es
+Copyright (C) 2005 Aaron Spike, aaron@ekips.org
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+'''
+import random
+import math
+import inkex
+from inkex.paths import Path, CubicSuperPath
+import re
+
+class Rounder(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--precision", type=int, default=3, help="Precision")
+ pars.add_argument("--ctrl", type=inkex.Boolean, default = False, help="Round element handles")
+ pars.add_argument("--along", type=inkex.Boolean, default = True, help="Move handles following element movement")
+ pars.add_argument("--half", type=inkex.Boolean, default = False, help="Allow round to half if nearest")
+ pars.add_argument("--paths", type=inkex.Boolean, default = True, help="Affect to paths")
+ pars.add_argument("--widthheight", type=inkex.Boolean, default = False, help="Affect to width and height of objects")
+ pars.add_argument("--position", type=inkex.Boolean, default = False, help="Affect to position of objects")
+ pars.add_argument("--strokewidth", type=inkex.Boolean, default = False, help="Affect to stroke width of objects")
+ pars.add_argument("--opacity", type=inkex.Boolean, default = False, help="Affect to global opacity of objects")
+ pars.add_argument("--strokeopacity", type=inkex.Boolean, default = False, help="Affect to stroke opcacity of objects")
+ pars.add_argument("--fillopacity", type=inkex.Boolean, default = False, help="Affect to fill opcacity of objects")
+
+ def roundFloat(self, n):
+ if self.options.half:
+ if self.options.precision == 0:
+ return str(round(n * 2) / 2)
+ else:
+ return str(round(n * (self.options.precision) * 10 * 2) / ((self.options.precision) * 10 * 2))
+ else:
+ return str(round(n, self.options.precision))
+
+ def roundit(self, p):
+ x = self.roundFloat(p[0])
+ y = self.roundFloat(p[1])
+ return [float(x) - p[0], float(y) - p[1]]
+
+ def path_round_it(self,element):
+ if element.tag == inkex.addNS('path','svg'):
+ d = element.get('d')
+ p = CubicSuperPath(d)
+ for subpath in p:
+ for csp in subpath:
+ delta = self.roundit(csp[1])
+ if self.options.along:
+ csp[0][0]+=delta[0]
+ csp[0][1]+=delta[1]
+ csp[1][0]+=delta[0]
+ csp[1][1]+=delta[1]
+ if self.options.along:
+ csp[2][0]+=delta[0]
+ csp[2][1]+=delta[1]
+ if self.options.ctrl:
+ delta = self.roundit(csp[0])
+ csp[0][0]+=delta[0]
+ csp[0][1]+=delta[1]
+ delta = self.roundit(csp[2])
+ csp[2][0]+=delta[0]
+ csp[2][1]+=delta[1]
+ element.set('d',str(Path(p)))
+ elif element.tag == inkex.addNS('g','svg'):
+ for e in element:
+ self.path_round_it(e)
+
+ def roundStroke(self,matchobj):
+ return 'stroke-width:' + self.roundFloat(float( re.sub(r'[a-zA-Z]', "", matchobj.group(1)))) + matchobj.group(2);
+
+ def roundOpacity(self,matchobj):
+ return matchobj.group(1) + matchobj.group(2) + self.roundFloat(float( matchobj.group(3))) + matchobj.group(4);
+
+ def roundWHXY(self,matchobj):
+ return matchobj.group(1) + self.roundFloat(float( matchobj.group(2))) + matchobj.group(3);
+
+
+ def stroke_round_it(self, element):
+ if element.tag == inkex.addNS('g','svg'):
+ for e in element:
+ self.stroke_round_it(e)
+ else:
+ style = element.get('style')
+ if style:
+ style = re.sub('stroke-width:(.*?)(;|$)',self.roundStroke, style)
+ element.set('style',style)
+ def opacity_round_it(self, element, typeOpacity):
+ if element.tag == inkex.addNS('g','svg'):
+ for e in element:
+ self.opacity_round_it(e, typeOpacity)
+ else:
+ style = element.get('style')
+ if style:
+ style = re.sub('(^|;)(' + typeOpacity + ':)(.*?)(;|$)',self.roundOpacity, style)
+ element.set('style',style)
+
+ def widthheight_round_it(self, element):
+ if element.tag == inkex.addNS('g','svg'):
+ for e in element:
+ self.widthheight_round_it(e)
+ else:
+ width = element.get('width')
+ if width:
+ width = re.sub('^(\-|)([0-9]+\.[0-9]+)(.*?)$',self.roundWHXY, width)
+ element.set('width',width)
+ height = element.get('height')
+ if height:
+ height = re.sub('^(\-|)([0-9]+\.[0-9]+)(.*?)$',self.roundWHXY, height)
+ element.set('height',height)
+
+ def position_round_it(self, element):
+ if element.tag == inkex.addNS('g','svg'):
+ for e in element:
+ self.position_round_it(e)
+ else:
+ x = element.get('x')
+ if x:
+ x = re.sub('^(\-|)([0-9]+\.[0-9]+)(.*?)$',self.roundWHXY, x)
+ element.set('x', x)
+ y = element.get('y')
+ if y:
+ y = re.sub('^(\-|)([0-9]+\.[0-9]+)(.*?)$',self.roundWHXY, y)
+ element.set('y', y)
+
+ def effect(self):
+
+ if len(self.svg.selected) > 0:
+ for element in self.svg.selection.values():
+ if self.options.paths:
+ self.path_round_it(element)
+ if self.options.strokewidth:
+ self.stroke_round_it(element)
+ if self.options.widthheight:
+ self.widthheight_round_it(element)
+ if self.options.position:
+ self.position_round_it(element)
+ if self.options.opacity:
+ self.opacity_round_it(element, "opacity")
+ if self.options.strokeopacity:
+ self.opacity_round_it(element, "stroke-opacity")
+ if self.options.fillopacity:
+ self.opacity_round_it(element, "fill-opacity")
+ else:
+ self.msg('Please select some paths first.')
+ return
+
+if __name__ == '__main__':
+ Rounder().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/set_view_box/meta.json b/extensions/fablabchemnitz/set_view_box/meta.json
new file mode 100644
index 0000000..3b1a45f
--- /dev/null
+++ b/extensions/fablabchemnitz/set_view_box/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Set View Box (Replaced by CTRL + SHIFT + R)",
+ "id": "fablabchemnitz.de.set_view_box",
+ "path": "set_view_box",
+ "dependent_extensions": null,
+ "original_name": "Set viewBox",
+ "original_id": "org.pernsteiner.inkscape.viewbox",
+ "license": "BSD-2-Clause License",
+ "license_url": "http://www.pernsteiner.org/inkscape/viewbox/inkscape-viewbox-0.1.0.zip",
+ "comment": "ported to Inkscape v1 manually by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/set_view_box",
+ "fork_url": "http://www.pernsteiner.org/inkscape/viewbox",
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=74645779",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "pernsteiger.org/Stuart Pernsteiner",
+ "github.com/eridur-de"
+ ]
+ }
+]
diff --git a/extensions/fablabchemnitz/set_view_box/set_view_box.inx b/extensions/fablabchemnitz/set_view_box/set_view_box.inx
new file mode 100644
index 0000000..34eb128
--- /dev/null
+++ b/extensions/fablabchemnitz/set_view_box/set_view_box.inx
@@ -0,0 +1,16 @@
+
+
+ Set View Box
+ fablabchemnitz.de.set_view_box
+
+ rect
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/set_view_box/set_view_box.py b/extensions/fablabchemnitz/set_view_box/set_view_box.py
new file mode 100644
index 0000000..824c9c8
--- /dev/null
+++ b/extensions/fablabchemnitz/set_view_box/set_view_box.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+# Copyright (c) 2012 Stuart Pernsteiner
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import sys
+import inkex
+from inkex import Transform
+
+class SetViewBox(inkex.EffectExtension):
+
+ def effect(self):
+ if len(self.svg.selected) != 1:
+ sys.exit("Error: You must select exactly one rectangle")
+ if list(self.svg.selected.items())[0][1].tag != inkex.addNS('rect','svg'):
+ sys.exit("Error: You must select exactly one rectangle")
+
+ sel = None
+ for pathId in self.svg.selected:
+ sel = self.svg.selected[pathId]
+
+ mat = [[1,0,0],[0,1,0]]
+ cur = sel
+ while cur is not None:
+ curMat = Transform(cur.get('transform'))
+ mat = Transform(curMat) @ Transform(mat)
+ cur = cur.getparent()
+
+ [x,y,w,h] = map(lambda attr: float(sel.get(attr)),
+ ['x','y','width','height'])
+
+ (x1,y1) = transformPoint(mat, (x,y))
+ (x2,y2) = transformPoint(mat, (x+w,y+h))
+
+ ww = x2-x1
+ hh = y2-y1
+
+ format_units = inkex.units.parse_unit(self.svg.get('width'))[1] #get the "Format:" unit at "Display tab"
+
+ root = self.svg.getElement('//svg:svg');
+ root.set('viewBox', '%f %f %f %f' % (x1,y1,ww,hh))
+ root.set('width', str(ww) + format_units)
+ root.set('height', str(hh) + format_units)
+
+def transformPoint(mat, pt):
+ pt2 = [pt[0], pt[1]]
+ Transform(mat).apply_to_point(pt2)
+ return (pt2[0], pt2[1])
+
+if __name__ == '__main__':
+ SetViewBox().run()
diff --git a/extensions/fablabchemnitz/shirt_waist/meta.json b/extensions/fablabchemnitz/shirt_waist/meta.json
new file mode 100644
index 0000000..93b4e5c
--- /dev/null
+++ b/extensions/fablabchemnitz/shirt_waist/meta.json
@@ -0,0 +1,22 @@
+[
+ {
+ "name": "Shirt Waist (Sara May Allington)",
+ "id": "fablabchemnitz.de.shirt_waist",
+ "path": "shirt_waist",
+ "dependent_extensions": null,
+ "original_name": "unknown",
+ "original_id": "unknown",
+ "license": "GNU GPL v2 / CC BY-NC 3.0",
+ "license_url": "https://docs.google.com/file/d/0BxsnjDIHW4yvZUl5SUxLTHZHems/edit",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/shirt_waist",
+ "fork_url": "http://www.taumeta.org/?page_id=247",
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55018402",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "taumeta.org/Susan Spencer",
+ "taumeta.org/Steve Conklin",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/shirt_waist/sewing_patterns.py b/extensions/fablabchemnitz/shirt_waist/sewing_patterns.py
new file mode 100644
index 0000000..a13e99b
--- /dev/null
+++ b/extensions/fablabchemnitz/shirt_waist/sewing_patterns.py
@@ -0,0 +1,820 @@
+#!/usr/bin/env python3
+#
+# sewing_patterns.py
+# Inkscape extension-Effects-Sewing Patterns
+# Copyright (C) 2010, 2011, 2012 Susan Spencer, Steve Conklin < www.taumeta.org >
+#
+# This program is free software:you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version. Attribution must be given in
+# all derived works.
+#
+# 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 < http://www.gnu.org/licenses/ >
+
+import subprocess, math, inkex, gettext
+from lxml import etree
+
+def debug(errmsg, file = 'bell'):
+ #audio files directory: /usr/share/sounds/ubuntu/stereo
+ sounds(file)
+ inkex.errormsg(gettext.gettext(errmsg))
+
+def sounds(file):
+ subprocess.call(['/usr/bin/canberra-gtk-play', '--id', file ])
+
+#---
+class Point():
+ '''Python object class to create variables that represent a named Cartesian point.
+ Accepts id, x, y. Returns object with .id, .x, .y attributes.
+ Example: a5 = Point('a5', 10.0, 15.32) returns variable a5 where a5.id = 'a5', a5.x = 10.0, a5.y = 15.32xt
+ If called with no parameters the defaults are id = '', x = 0, y = 0
+ The python object's id attribute enables it to be represented on the canvas as an svg object with the same id.
+ '''
+ def __init__(self, id = '', x = 0.0, y = 0.0): #if no parameters are passed in then the default values id = '', x = 0, y = 0 are used
+ self.id = id
+ self.x = x
+ self.y = y
+ self.y = y
+
+def patternPointXY(parent, id, x, y):
+ '''Accepts parent, id, x, y. Returns object of class Point. Calls addPoint() & addText() to create a pattern point on canvas.'''
+ # create python variable
+ pnt = Point(id, x, y)
+ # create svg circle red 5px radius
+ addCircle(parent, id, x, y, radius = 5, fill = 'red', stroke = 'red', stroke_width = '1', reference = 'true')
+ #draw label 8px right and 8px above circle
+ addText(parent, id + '_text', x + 8, y-8, id, fontsize = '30', textalign = 'left', textanchor = 'start', reference = 'true') #the id is used for two things here: the text object's id and the text object's content.
+ return pnt # return python variable for use in remainder of pattern
+
+def patternPoint(parent, id, pnt):
+ """Wrapper for patternPointXY. Accepts a Point object instead of X & Y values."""
+ return patternPointXY(parent, id, pnt.x, pnt.y)
+
+def controlPointXY(parent, id, x, y):
+ '''Accepts parent, id, x, y. Returns object of class Point. Calls addPoint() & addText() to create a pattern point with label on canvas.'''
+ # create python variable
+ pnt = Point(id, x, y)
+ # create unfilled grey circle 5px radius
+ addCircle(parent, id, x, y, radius = 5, fill = 'none', stroke = 'gray', stroke_width = '1', reference = 'true')
+ #draw label 8px right and 8px above circle
+ addText(parent, id + '_text', x + 8, y-8, id, fontsize = '30', textalign = 'left', textanchor = 'start', reference = 'true') #the id is used twice: the text object id and the text object content.
+ return pnt # return python variable for use in remainder of pattern
+
+def controlPoint(parent, id, pnt):
+ """Wrapper for controlPointXY. Accepts a Point object instead of X & Y values."""
+ return controlPointXY(parent, id, pnt.x, pnt.y)
+
+def pointList( * args):
+ """Accepts list of args. Returns array of args."""
+ list = []
+ for arg in args:
+ list.append(arg)
+ return list
+
+#---tests for position---
+def isRight(pnt1, pnt2):
+ '''returns 1 if pnt2 is to the right of pnt1'''
+ right = 0
+ if pnt2.x > pnt1.x:
+ right = 1
+ return right
+
+def isLeft(pnt1, pnt2):
+ '''returns 1 if pnt2 is to the left of pnt1'''
+ left = 0
+ if pnt2.x < pnt1.x:
+ left = 1
+ return left
+
+def isAbove(pnt1, pnt2):
+ '''returns 1 if pnt2 is above pnt1'''
+ up = 0
+ if pnt2.y < pnt1.y:
+ up = 1
+ return up
+
+def isBelow(pnt1, pnt2):
+ '''returns 1 if pnt2 is below pnt1'''
+ down = 0
+ if pnt2.y > pnt1.y:
+ down = 1
+ return down
+
+def lowest(pnts):
+ """Accepts array pnts[]. Returns lowest point in array."""
+ low = Point('', pnts[0].x, pnts[0].y)
+ for item in pnts:
+ if isBelow(low, item): #if item is below current low
+ updatePoint('', low, item)
+ return low
+
+def highest(pnts):
+ """Accepts array pnts[]. Returns highest point in array."""
+ high = Point('', pnts[0].x, pnts[0].y)
+ for item in pnts:
+ if isAbove(high, item): #if item is above current high
+ updatePoint(high, item)
+ return high
+
+def leftmost(pnts):
+ """Accepts array pnts[]. Returns leftmost point in array."""
+ left = Point('', pnts[0].x, pnts[0].y)
+ for item in pnts:
+ if isLeft(left, item):
+ updatePoint(left, item)
+ return left
+
+def rightmost(pnts):
+ """Accepts array pnts[]. Returns rightmost point in array."""
+ right = Point('', pnts[0].x, pnts[0].y)
+ for item in pnts:
+ if isRight(right, item):
+ updatePoint(right, item)
+ return right
+
+#---functions to calculate points. These functions do not create SVG objects---
+
+def updatePoint(p1, p2):
+ '''Accepts p1 and p2 of class Point. Updates p1 with x & y values from p2'''
+ p1.x = p2.x
+ p1.y = p2.y
+ return
+
+def right(p1, n):
+ '''Accepts point p1 and float n. Returns (x,y) to the right of p1 at (p1.x + n, p1.y)'''
+ return Point('', p1.x + n, p1.y)
+
+def left(p1, n):
+ '''Accepts point p1 and float n. Returns p2 to the left of p1 at (p1.x-n, p1.y)'''
+ return Point('', p1.x-n, p1.y)
+
+def up(p1, n):
+ '''Accepts point p1 and float n. Returns p2 above p1 at (p1.x, p1.y-n)'''
+ return Point('', p1.x, p1.y-n)
+
+def down(p1, n):
+ '''Accepts point p1 and float n. Returns p2 below p1 at (p1.x, p1.y + n)'''
+ return Point('', p1.x, p1.y + n)
+
+def symmetric(p1, p2, type = 'vertical'):
+ """
+ Accepts p1 and p2 of class Point, and optional type is either 'vertical' or 'horizontal with default 'vertical'.
+ Returns p3 of class Point as "mirror image" of p1 relative to p2
+ If type == 'vertical': pnt is on opposite side of vertical line x = p2.x from p1
+ If type == 'horizontal': pnt is on opposite side of horizontal line y = p2.y from p1
+ """
+ p3 = Point()
+ dx = p2.x - p1.x
+ dy = p2.y - p1.y
+ if (type == 'vertical'):
+ p3.x = p2.x + dx
+ p3.y = p1.y
+ elif (type == 'horizontal'):
+ p3.x = p1.x
+ p3.y = p2.y + dy
+ return p3
+
+def polar(p1, length, angle):
+ '''
+ Adapted from http://www.teacherschoice.com.au/maths_library/coordinates/polar_-_rectangular_conversion.htm
+ Accepts p1 as type Point, length as float, angle as float. angle is in radians
+ Returns p2 as type Point, calculated at length and angle from p1,
+ Angles start at position 3:00 and move clockwise due to y increasing downwards on Cairo Canvas
+ '''
+ id = ''
+ r = length
+ x = p1.x + (r * math.cos(angle))
+ y = p1.y + (r * math.sin(angle))
+ p2 = Point(id, x, y)
+ return p2
+
+def midPoint(p1, p2, n = 0.5):
+ '''Accepts points p1 & p2, and n where 0 < n < 1. Returns point p3 as midpoint b/w p1 & p2'''
+ p3 = Point('', (p1.x + p2.x) * n, (p1.y + p2.y) * n)
+ return p3
+
+
+#---measurements
+def distance(p1, p2):
+ '''Accepts two points p1 & p2. Returns the distance between p1 & p2.'''
+ return ( ((p2.x-p1.x) ** 2) + ((p2.y-p1.y) ** 2) ) ** 0.5
+
+def angleOfDegree(degree):
+ '''Accepts degree. Returns radians.'''
+ return degree * math.pi/180.0
+
+def angleOfLine(p1, p2):
+ """ Accepts points p1 & p2. Returns the angle of the vector between them. Uses atan2."""
+ return math.atan2(p2.y-p1.y, p2.x-p1.x)
+
+def angleOfVector(p1, v, p2):
+ '''Accepts three points o1, v, and p2. Returns radians of the angle formed between the three points.'''
+ return abs(angleOfLine(v, p1)-angleOfLine(v, p2))
+
+def slopeOfLine(p1, p2):
+ """ Accepts two point objects and returns the slope """
+ if ((p2.x-p1.x) != 0):
+ m = (p2.y-p1.y)/(p2.x-p1.x)
+ else:
+ #TODO: better error handling here
+ debug('Vertical Line in slopeOfLine')
+ m = None
+ return m
+
+def slopeOfAngle(radians):
+ '''
+ Accepts angle (radians)
+ Returns the slope as tangent radians
+ '''
+ #get tangent of radians
+ return math.tan(radians)
+
+#---intersections & extensions
+
+def extendLine(p1, p2, length, rotation=0):
+ """
+ Accepts two directed points of a line, and a length to extend the line
+ Finds point along line at length from p2 in direction p1->p2
+ """
+ return onLineAtLength(p2, p1, -length)
+
+def onLineAtLength(p1, p2, length, rotation=0):
+ """
+ Accepts points p1 and p2, distance, and an optional rotation angle.
+ Returns coordinate pair on the line at length measured from p1 towards p2
+ If length is negative, will return a coordinate pair at length measured
+ from p1 in opposite direction from p2.
+ The result is optionally rotated about the first point by the rotation angle in degrees
+ """
+ lineangle = angleOfLine(p1, p2)
+ angle = lineangle + rotation * (math.pi/180)
+ x = (length * math.cos(angle)) + p1.x
+ y = (length * math.sin(angle)) + p1.y
+ return Point('', x, y)
+
+def onLineAtX(p1, p2, x):
+ #on line p1-p2, given x find y
+ if (p1.x == p2.x):# vertical line
+ raise ValueError('Points form a vertical line, infinite answers possible')
+ return None
+ else:
+ m = (p2.y - p1.y)/(p2.x - p1.x)
+ b = p2.y - (m * p2.x)
+ return Point('', x, (m * x) + b)
+
+def onLineAtY(p1, p2, y):
+ #on line p1-p2, find x given y
+ if (p1.y == p2.y): #if horizontal line
+ raise ValueError('Points form a horizontal line, infinite answers possible')
+ return None
+ elif (p1.x == p2.x): # if vertical line
+ return Point('', p1.x, y)
+ else:
+ m = (p1.y - p2.y)/(p1.x - p2.x)
+ b = p2.y - (m * p2.x)
+ return Point('', (y - b)/m, y)
+
+def intersectLines(p1, p2, p3, p4):
+ """
+ Find intersection between two lines. Accepts p1, p2, p3, p4 as class Point. Returns p5 as class Point
+ Intersection does not have to be within the supplied line segments
+ """
+ x, y = 0.0, 0.0
+ if (p1.x == p2.x): #if 1st line vertical, use slope of 2nd line
+ x = p1.x
+ m2 = slopeOfLine(p3, p4)
+ b2 = p3.y-m2 * p3.x
+ y = m2 * x + b2
+ elif (p3.x == p4.x): #if 2nd line vertical, use slope of 1st line
+ x = p3.x
+ m1 = slopeOfLine(p1, p2)
+ b1 = p1.y-m1 * p1.x
+ y = m1 * x + b1
+ else: #otherwise use ratio of difference between points
+ m1 = (p2.y-p1.y)/(p2.x-p1.x)
+ m2 = (p4.y-p3.y)/(p4.x-p3.x)
+ b1 = p1.y-m1 * p1.x
+ b2 = p3.y-m2 * p3.x
+ #if (abs(b1-b2) < 0.01) and (m1 == m2):
+ #x = p1.x
+ #else:
+ #x = (b2-b1)/(m1-m2)
+ if (m1 == m2):
+ #TODO: better error handling here
+ debug(' ** ** * Parallel lines in intersectLines ** ** * ')
+ else:
+ x = (b2-b1)/(m1-m2)
+ y = (m1 * x) + b1 # arbitrary choice, could have used m2 & b2
+ p5 = Point("", x, y)
+ return p5
+
+def intersectLineRay(P1, P2, R1, angle):
+ '''
+ Accepts two points defining a line, and a point and angle defining a ray.
+ Returns point where they intersect.
+ '''
+ #define a line R1-R2 by finding point R2 along ray 25 pixels (arbitary) from R1
+ R2 = polar(R1, 1 * 25, angle)
+ pnt = intersectLines(P1, P2, R1, R2)
+ return pnt
+
+def onRayAtX(P, angle, x):
+ '''
+ Accepts point P and angle of line.
+ Returns point along ray at x
+ '''
+ #convert degrees to slope
+ m = slopeOfAngle(angle)
+ #solve for y
+ #(P.y - y)/(P.x - x) = m
+ y = P.y - m * (P.x - x)
+ return Point('', x, y)
+
+def onRayAtY(P, angle, y):
+ '''
+ Accepts point P and angle of line.
+ Returns point along ray at y
+ '''
+ #convert degrees to slope
+ m = slopeOfAngle(angle)
+ #solve for x
+ #(P.y - y)/(P.x - x) = m
+ x = P.x - (P.y - y)/m
+ return Point('', x, y)
+
+def intersectCircles(C1, r1, C2, r2):
+ """
+ Accepts C1, r1, C2, r2 where C1 & C2 are center points of each circle, and r1 & r2 are the radius of each circle.
+ Returns an array of points of intersection.
+ """
+ x0, y0 = C1.x, C1.y
+ x1, y1 = C2.x, C2.y
+ d = distance(C1, C2) # distance b/w circle centers
+ dx, dy = (x1-x0), (y1-y0) # negate y b/c canvas increases top to bottom
+ pnts = []
+ if (d == 0):
+ #intersections = 0
+ #TODO: better error handling here
+ debug('center of both circles are the same in intersectCircles()')
+ debug('C1 = ', C1.x, C1.y, 'radius1 = ', r1)
+ debug('C2 = ', C2.x, C2.y, 'radius1 = ', r2)
+ return
+ elif (d < abs(r1-r2)):
+ #intersections = 0
+ #TODO: better error handling here
+ debug('one circle is within the other in intersectCircles()')
+ debug('d = ', d)
+ debug('r1 - r2 = ', (r1-r2))
+ debug('d < abs(r1 - r2) ?', (d < abs(r1-r2)))
+ debug('C1 = ', C1.x, C1.y, 'radius1 = ', r1)
+ debug('C2 = ', C2.x, C2.y, 'radius1 = ', r2)
+ return
+ elif (d > (r1 + r2)):
+ #intersections = 0
+ #TODO: better error handling here
+ debug('circles do not intersect in intersectCircles()')
+ debug('d = ', d)
+ debug('r1 + r2 = ', (r1 + r2))
+ debug('d > abs(r1 + r2) ?', (d > abs(r1 + r2)))
+ debug('C1 = ', C1.x, C1.y, 'radius1 = ', r1)
+ debug('C2 = ', C2.x, C2.y, 'radius1 = ', r2)
+ # TODO:possible kluge -check if this is acceptable using a small margin of error between r1 & r2 (0.5 * CM)?:
+ #r2 = d-r1
+ return
+ else:
+ #intersections = 2 or intersections = 1
+ a = ((r1 * r1)-(r2 * r2) + (d * d))/(2.0 * d)
+ x2 = x0 + (dx * a/d)
+ y2 = y0 + (dy * a/d)
+ h = math.sqrt((r1 * r1)-(a * a))
+ rx = -dy * (h/d)
+ ry = dx * (h/d)
+ X1 = x2 + rx
+ Y1 = y2 + ry
+ X2 = x2-rx
+ Y2 = y2-ry
+ pnts.append(Point("", X1, Y1))
+ pnts.append(Point("", X2, Y2))
+ return pnts
+
+def onCircleAtX(C, r, x):
+ """
+ Finds points on circle where p.x=x
+ Accepts C as an object of class Point or xy coords for the center of the circle,
+ r as the radius, and x to find the points on the circle
+ Returns an array P
+ Based on paulbourke.net/geometry/sphereline/sphere_line_intersection.py
+ """
+ #print 'C =', C.x, C.y
+ #print 'r =', r
+ #print 'x =', x
+ P = []
+ if abs(x - C.x) > r:
+ print('abs(x - C.x) > r ...', abs(x - C.x), ' > ', r)
+ print('x is outside radius of circle in intersections.onCircleAtX()')
+ else:
+ translated_x = x - C.x # center of translated circle is (0, 0) as translated_x is the difference b/w C.x & x
+ translated_y1 = abs(math.sqrt(r**2 - translated_x**2))
+ translated_y2 = -(translated_y1)
+ y1 = translated_y1 + C.y # translate back to C.y
+ y2 = translated_y2 + C.y # translate back to C.y
+ P.append(Point('', x, y1))
+ P.append(Point('', x, y2))
+ return P
+
+def onCircleAtY(C, r, y):
+ """
+ Finds points one or two points on circle where P.y=y
+ Accepts C of class Point or coords as circle center, r of type float as radius, and y of type float)
+ Returns an array P
+ Based on paulbourke.net/geometry/sphereline/sphere_line_intersection.py
+ """
+ #print('C =', C.x, C.y)
+ #print('r =', r)
+ #print('x = ', y))
+ P = []
+ if abs(y - C.y) > r:
+ print('abs(y - C.y) > r ...', abs(y - C.y), ' > ', r)
+ print('y is outside radius in onCircleAtY() -- no intersection')
+ else:
+ translated_y = y - C.y
+ translated_x1 = abs(math.sqrt(r**2 - translated_y**2))
+ translated_x2 = -translated_x1
+ x1 = translated_x1 + C.x
+ x2 = translated_x2 + C.x
+ P.append(Point('', x1, y))
+ P.append(Point('', x2, y))
+ return P
+
+def intersectLineCircle(P1, P2, C, r):
+ """
+ Finds intersection of a line segment and a circle.
+ Accepts circle center point object C, radius r, and two line point objects P1 & P2
+ Returns an array P with up to two coordinate pairs as P.intersections P[0] & P[1]
+ Based on paulbourke.net/geometry/sphereline/sphere_line_intersection.py
+ """
+
+ #print('C =', C.x, C.y)
+ #print('P1 =', P1.x, P1.y)
+ #print('P2 =', P2.x, P2.y)
+ #print('r =', r, 'pts', ', ', r / CM, 'cm')
+
+ p1, p2 = Point('', '', ''), Point('', '', '')
+ P = []
+
+ if P1.x == P2.x: #vertical line
+ if abs(P1.x - C.x) > r:
+ print('no intersections for vertical line P1', P1.name, P1.x, P1.y, ', P2', P2.name, P2.x, P2.y, ', and Circle', C.name, C.x, C.y, ', radius', r)
+ return None
+ else:
+ #print('Vertical line')
+ p1.x = P1.x
+ p2.x = P1.x
+ p1.y = C.y + sqrt(r**2 - (P1.x - C.x)**2)
+ p2.y = C.y - sqrt(r**2 - (P1.x - C.x)**2)
+ elif P1.y == P2.y: #horizontal line
+ if abs(P1.y-C.y) > r:
+ print('no intersections for horizontal line P1', P1.name, P1.x, P1.y, ', P2', P2.name, P2.x, P2.y, ', and Circle', C.name, C.x, C.y, ', radius', r)
+ return None
+ else:
+ #print('Horizontal line')
+ p1.y = P1.y
+ p2.y = P1.y
+ p1.x = C.x + sqrt(r**2 - (P1.y - C.y)**2)
+ p2.x = C.x - sqrt(r**2 - (P1.y - C.y)**2)
+ else:
+ a = (P2.x - P1.x)**2 + (P2.y - P1.y)**2
+ b = 2.0 * ((P2.x - P1.x) * (P1.x - C.x)) + ((P2.y - P1.y) * (P1.y - C.y))
+ c = C.x**2 + C.y**2 + P1.x**2 + P1.y**2 - (2.0 * (C.x * P1.x + C.y * P1.y)) - r**2
+ i = b**2 - 4.0 * a * c
+ if i < 0.0:
+ print('no intersections b/w line', P1.name, P1.x, P1.y, '--', P2.name, P2.x, P2.y, 'and Circle', C.name, C.x, C.y, 'with radius', r)
+ return None
+ elif i == 0.0:
+ # one intersection
+ #print('one intersection')
+ mu = -b/(2.0 * a)
+ p1.x, p1.y = P1.x + mu * (P2.x - P1.x), P1.y + mu * (P2.y - P1.y)
+ elif i > 0.0:
+ # two intersections
+ #print('two intersections')
+ # first intersection
+ mu1 = (-b + math.sqrt(i)) / (2.0*a)
+ p1.x, p1.y = P1.x + mu1 * (P2.x - P1.x), P1.y + mu1 * (P2.y - P1.y)
+ # second intersection
+ mu2 = (-b - math.sqrt(i)) / (2.0*a)
+ p2.x, p2.y = P1.x + mu2 * (P2.x - P1.x), P1.y + mu2 * (P2.y - P1.y)
+ P.append(p1)
+ P.append(p2)
+ return P
+
+def intersectChordCircle(C, r, P, chord_length):
+ ''' Accepts center of circle, radius of circle, a point on the circle, and chord length.
+ Returns an array of two points on the circle at chord_length distance away from original point'''
+ d = chord_length
+ # point on circle given chordlength & starting point = 2 * asin(d/2r)
+ d_div_2r = d/(2.0 * r)
+ angle = 2 * asin(d_div_2r)
+ pnts = []
+ pnts.append(polar(C, r, angle))
+ pnts.append(polar(C, r, - angle))
+ return pnts
+
+def intersectLineCurve(P1, P2, curve, n = 100):
+ '''
+ Accepts two points of a line P1 & P2, and an array of connected bezier curves [P11, C11, C12, P12, C21, C22, P22, C31, C32, P32, ...]
+ Returns an array points_found[] of point objects where line intersected with the curve, and tangents_found[] of tangent angle at that point
+ '''
+ # get polar equation for line for P1-P2
+ # point furthest away from 1st point in curve[] is the fixed point & sets the direction of the angle towards the curve
+ #if distance(P1, curve[0]) > = distance(P2, curve[0] ):
+ # fixed_pnt = P1
+ # angle = angleOfLine(P1, P2)
+ #else:
+ # fixed_pnt = P2
+ # angle = angleOfLine(P2, P1)
+ #debug('intersectLineCurve...')
+ #debug('....P1 = ' + P1.id + ' ' + str(P1.x) + ', ' + str(P1.y))
+ #debug('....P2 = ' + P2.id + ' ' + str(P2.x) + ', ' + str(P2.y))
+ #for pnt in curve:
+ #debug( '....curve = ' + pnt.id + ' ' + str(pnt.x) + ', ' + str(pnt.y))
+ fixed_pnt = P1
+ angle = angleOfLine(P1, P2)
+ intersections = 0
+ points_found = []
+ tangents_found = []
+ pnt = Point()
+ j = 0
+ while j <= len(curve) -4: # for each bezier curve in curveArray
+ intersection_estimate = intersectLines(P1, P2, curve[j], curve[j + 3]) # is there an intersection?
+ if intersection_estimate != None or intersection_estimate != '':
+ interpolatedPoints = interpolateCurve(curve[j], curve[j + 1], curve[j + 2], curve[j + 3], n) #interpolate this bezier curve, n = 100
+ k = 0
+ while k < len(interpolatedPoints)-1:
+ length = distance(fixed_pnt, interpolatedPoints[k])
+ pnt_on_line = polar(fixed_pnt, length, angle)
+ range = distance(interpolatedPoints[k], interpolatedPoints[k + 1]) # TODO:improve margin of error
+ length = distance(pnt_on_line, interpolatedPoints[k])
+ #debug(str(k) + 'pntOnCurve = ' + \
+ # str(interpolatedPoints[k].x) + ', ' + str(interpolatedPoints[k].y) + \
+ # 'intersectLineAtLength = ' + str(pnt_on_line.x) + ', ' + str( pnt_on_line.y)\
+ # + 'length = ' + str(length) + 'range = ' + str(range))
+ if ( length <= range):
+ #debug('its close enough!')
+ if k > 1:
+ if (interpolatedPoints[k-1] not in points_found) and (interpolatedPoints[k-2] not in points_found):
+ points_found.append(interpolatedPoints[k])
+ tangents_found.append(angleOfLine(interpolatedPoints[k-1], interpolatedPoints[k + 1]))
+ intersections += 1
+ elif k == 1:
+ if (curve[0] not in intersections):
+ points_found.append(interpolatedPoints[1])
+ tangents_found.append(angleOfLine(curve[0], interpolatedPoints[2]))
+ intersections += 1
+ else:
+ intersections.append(curve[0])
+ tangents_found.append(angleOfLine(curve[0], curve[1]))
+ k += 1
+ j += 3 # skip j up to P3 of the current curve to be used as P0 start of next curve
+ if intersections == 0:
+ #TODO: better error handling here
+ debug('no intersections found in intersectLineCurve(' + P1.id + ', ' + P2.id + ' and curve')
+ #return points_found, tangents_found
+ return points_found
+
+def interpolateCurve(P0, C1, C2, P1, t = 100):
+ '''
+ Accepts curve points P0, C1, C2, P1 & number of interpolations t
+ Returns array of interpolated points of class Point
+ Adapted from http://www.planetclegg.com/projects/WarpingTextToSplines.htm
+ '''
+ # calculate coefficients for two knot points P0 & P1 ; C1 & C2 are the controlpoints.
+ # x coefficients
+ A = P1.x-(3 * C2.x) + (3 * C1.x)-P0.x
+ B = (3 * C2.x)-(6 * C1.x) + (3 * P0.x)
+ C = (3 * C1.x)-(3 * P0.x)
+ D = P0.x
+ # y coefficients
+ E = P1.y-(3 * C2.y) + (3 * C1.y)-P0.y
+ F = (3 * C2.y)-(6 * C1.y) + (3 * P0.y)
+ G = (3 * C1.y)-(3 * P0.y)
+ H = P0.y
+ # calculate interpolated points
+ interpolatedPoints = []
+ maxPoint = float(t)
+ i = 0
+ while ( i <= t):
+ j = i/maxPoint # j can't be an integer, i/t is an integer..always 0.
+ x = A * (j ** 3) + B * (j ** 2) + C * j + D
+ y = E * (j ** 3) + F * (j ** 2) + G * j + H
+ interpolatedPoints.append(Point('', x, y))
+ i += 1
+ return interpolatedPoints
+
+#---rotations
+def slashAndSpread(pivot, angle, *args):
+ """
+ Accepts pivot point, angle of rotation, and the points to be rotated.
+ Accepts positive & negative angles.
+ """
+ if (angle == 0.0):
+ print('Angle = 0 -- Slash and Spread not possible')
+ else:
+ list = []
+ for arg in args:
+ list.append(arg)
+ i = 0
+ for pnt in list:
+ length = distance(pivot, pnt)
+ rotated_pnt = polar(pivot, length, angleOfLine(pivot, pnt) + angle) # if angle > 0 spread clockwise. if angle < 0 spread counterclockwise
+ updatePoint(pnt, rotated_pnt)
+ return
+
+#---darts
+def foldDart(dart, inside_pnt, seam_allowance):
+ '''
+ Accepts dart, and the nearest point in the direction dart will be folded
+ Returns dart.m, dart.oc, dart.ic, dart.angle
+ dart.m = middle dart leg at seamline (to be included in seamline path)
+ dart.oc = inside dart leg at cuttingline (to be included in dartline path)
+ dart.oc = outside dart leg at cuttingline (to be included in dartline path)
+ '''
+ mid_pnt = midPoint(dart.i, dart.o)
+ dart_length = distance(dart, dart.i)
+ i_angle = angleOfLine(dart, dart.i)
+ c_angle = angleOfLine(dart, inside_pnt)
+ dart_angle = abs(angleOfVector(dart.i, dart, dart.o))
+ dart_half_angle = dart_angle/2.0
+
+ #determine which direction the dart will be folded
+ #if ((dart.i.x > dart.x) and (dart.i.y > dart.y)) or ((dart.i.x < dart.x) and (dart.i.y > dart.y)):
+ #x & y vectors not the same sign
+ #dart_half_angle = -dart_half_angle
+ if i_angle > c_angle:
+ dart_half_angle = -dart_half_angle
+ elif dart_angle < c_angle:
+ #dart straddles 0 angle
+ dart_half_angle = -dart_half_angle
+
+ fold_angle = i_angle + dart_half_angle
+ fold_pnt = intersectLineRay(dart.i, inside_pnt, dart, fold_angle)
+ dart.m = onLineAtLength(dart, mid_pnt, distance(dart, fold_pnt)) #dart midpoint at seamline
+ dart.oc = polar(dart, distance(dart, dart.o) + seam_allowance, angleOfLine(dart, dart.o)) #dart outside leg at cuttingline
+ dart.ic = extendLine(dart, dart.i, seam_allowance) #dart inside leg at cuttingline
+ #create or update dart.angles
+ dart.angle = angleOfVector(dart.i, dart, dart.o)
+ return
+
+#---base, pattern & patternpiece groups
+def base(parent, id):
+ '''Create a base group to contain all patterns, parent should be the document'''
+ newBase = addGroup(parent, id)
+ newBase.set(inkex.addNS('label', 'inkscape'), id)
+ newBase.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+ return newBase
+
+def pattern(parent, id):
+ '''Create a pattern group to hold a single pattern, parent should be the base group'''
+ newPattern = addGroup(parent, id)
+ newPattern.set(inkex.addNS('label', 'inkscape'), id)
+ newPattern.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+ return newPattern
+
+def patternPiece(parent, id, name, fabric = 2, interfacing = 0, lining = 0):
+ '''Create a pattern piece group to hold a single pattern piece, parent should be a pattern group'''
+ newPatternPiece = addGroup(parent, id)
+ newPatternPiece.set(inkex.addNS('label', 'inkscape'), name)
+ newPatternPiece.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+ return newPatternPiece
+
+#---svg
+
+def addCircle(parent, id, x, y, radius = 5, fill = 'red', stroke = 'red', stroke_width = '1', reference = 'false'):
+ '''create & write a circle to canvas & set it's attributes'''
+ circ = etree.SubElement(parent, inkex.addNS('circle', 'svg'))
+ circ.set('id', id)
+ circ.set('cx', str(x))
+ circ.set('cy', str(y))
+ circ.set('r', str(radius))
+ circ.set('fill', fill)
+ circ.set('stroke', stroke)
+ circ.set('stroke-width', stroke_width)
+ if reference == 'true':
+ circ.attrib['reference'] = 'true'
+ return
+
+def addSquare(parent, id, w, h, x, y, reference='false'):
+ # create & write a square element, set its attributes
+ square = etree.SubElement(parent, inkex.addNS('rect', 'svg'))
+ square.set('id', id)
+ square.set('width', str(w))
+ square.set('height', str(h))
+ square.set('x', str(x))
+ square.set('y', str(y))
+ square.set('stroke', 'none')
+ square.set('fill', '#000000')
+ square.set('stroke-width', '1')
+ if (reference == 'true'):
+ square.attrib['reference'] = 'true'
+ return
+
+def addText(parent, id, x, y, text, fontsize = '12', textalign = 'left', textanchor = 'start', reference = 'false'):
+ '''Create a text element, set its attributes, then write to canvas.
+ The text element is different than other elements -- > Set attributes first then write to canvas using append method.
+ There is no etree.SubElement() method for creating a text element & placing it into the document in one step.
+ Use inkex's etree.Element() method to create an unattached text svg object,
+ then use a document object's append() method to place it on the document canvas'''
+ #create a text element with inkex's Element()
+ txt = etree.Element(inkex.addNS('text', 'svg'))
+ #set attributes of the text element
+ txt.set('id', id)
+ txt.set('x', str(x))
+ txt.set('y', str(y))
+ txt.text = text
+ style = {'text-align':textalign, 'text-anchor':textanchor, 'font-size':fontsize}
+ txt.set('style', str(inkex.Style(style)))
+ if reference == 'true':
+ txt.attrib['reference'] = 'true'
+ #txt.setAttribute('reference', 'true') #alternative syntax
+ #add to canvas in the parent group
+ parent.append(txt)
+ return
+
+def addLayer(parent, id):
+ '''Create & write an inkscape group-layer to canvas'''
+ new_layer = etree.SubElement(parent, 'g')
+ new_layer.set('id', id)
+ new_layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
+ new_layer.set(inkex.addNS('label', 'inkscape'), '%s layer' % (id))
+ return new_layer
+
+def addGroup(parent, id):
+ '''Create & write an svg group to canvas'''
+ new_layer = etree.SubElement(parent, 'g')
+ new_layer.set('id', id)
+ return new_layer
+
+def addPath(parent, id, path_str, path_type):
+ '''Accepts parent, id, path string, path type. Creates attribute dictionary. Creates & writes path.'''
+ reference = 'false'
+ if path_type == 'seamline':
+ path_style = {'stroke':'green', 'stroke-width':'4', 'fill':'none', 'opacity':'1', 'stroke-dasharray':'15, 5', 'stroke-dashoffset':'0'}
+ elif path_type == 'cuttingline':
+ path_style = {'stroke':'green', 'stroke-width':'4', 'fill':'none', 'opacity':'1'}
+ elif path_type == 'gridline':
+ path_style = {'stroke':'gray', 'stroke-width':'4', 'fill':'none', 'opacity':'1', 'stroke-dasharray':'6, 6', 'stroke-dashoffset':'0'}
+ reference = 'true'
+ elif path_type == 'dartline':
+ path_style = {'stroke':'gray', 'stroke-width':'4', 'fill':'none', 'opacity':'1'}
+ elif path_type == 'grainline':
+ path_style = {'stroke':'DimGray', 'stroke-width':'3', 'fill':'none', 'opacity':'1', \
+ 'marker-start':'url(#ArrowStart)', 'marker-end':'url(#ArrowEnd)'}
+ elif path_type == 'slashline':
+ path_style = {'stroke':'green', 'stroke-width':'4', 'fill':'none', 'opacity':'1'}
+ svg_path = etree.SubElement(parent, inkex.addNS('path', 'svg'))
+ svg_path.set('id', id)
+ svg_path.set('d', path_str)
+ svg_path.set('style', str(inkex.Style(path_style)))
+ if reference == 'true':
+ svg_path.attrib['reference'] = 'true'
+ return svg_path
+
+def formatPath( * args):
+ """
+ Accepts a series of pseudo svg path arguments 'M', 'L', 'C' , and point objects.
+ Returns path_string which is a string formatted for use as the 'd' path attribute in an svg object.
+ """
+ tokens = [] # initialize an empty array
+ # put all the parameters in * args into the array
+ for arg in args:
+ tokens.append(arg)
+ com = ', '
+ path_string = ''
+ i = 0
+ while (i < len(tokens)):
+ cmd = tokens[i]
+ if (cmd == 'M') or (cmd == 'L'):
+ path_string += " %s %g %g" % (cmd, tokens[i + 1].x, tokens[i + 1].y)
+ i = i + 2
+ elif (cmd == 'C'):
+ path_string += " %s %g %g %g %g %g %g" % (cmd, tokens[i + 1].x, \
+ tokens[i + 1].y, tokens[i + 2].x, tokens[i + 2].y, tokens[i + 3].x, tokens[i + 3].y)
+ i = i + 4
+ return path_string
+
+def addDefs(doc):
+ '''Add defs group with markers to the document'''
+ defs = etree.SubElement(doc, inkex.addNS('defs', 'svg'))
+ #add start arrow
+ marker = etree.SubElement(defs, 'marker', {'id':'ArrowStart', 'orient':'auto', 'refX':'0.0', 'refY':'0.0', 'style':'overflow:visible'})
+ etree.SubElement(marker, 'path', {'d':'M 0, 0 L 0, 5 L -20, 0 L 0, -5 z', 'style':'fill:DimGray; stroke:DimGray; stroke-width:0.5'})
+ #add end arrow
+ marker = etree.SubElement(defs, 'marker', {'id':'ArrowEnd', 'orient':'auto', 'refX':'0.0', 'refY':'0.0', 'style':'overflow:visible'})
+ etree.SubElement(marker, 'path', {'d':'M 0, 0 L 0, 5 L 20, 0 L 0, -5 z', 'style':'fill:DimGray; stroke:DimGray; stroke-width:0.5'})
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/shirt_waist/shirt_waist.inx b/extensions/fablabchemnitz/shirt_waist/shirt_waist.inx
new file mode 100644
index 0000000..ef76603
--- /dev/null
+++ b/extensions/fablabchemnitz/shirt_waist/shirt_waist.inx
@@ -0,0 +1,33 @@
+
+
+ Shirt Waist (Sara May Allington)
+ fablabchemnitz.de.shirt_waist
+
+
+
+
+ 15.0
+ 15.5
+ 13.5
+ 39.0
+ 25.0
+ 15.0
+ 13.5
+ 7.75
+ 10.75
+ 20.00
+ 9.50
+ 12.50
+ 8.00
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/shirt_waist/shirt_waist.py b/extensions/fablabchemnitz/shirt_waist/shirt_waist.py
new file mode 100644
index 0000000..37944a8
--- /dev/null
+++ b/extensions/fablabchemnitz/shirt_waist/shirt_waist.py
@@ -0,0 +1,459 @@
+#!/usr/bin/env python3
+#
+# shirt_waist_allington.py
+# Inkscape extension-Effects-Sewing Patterns-Shirt Waist Allington
+# Copyright (C) 2010, 2011, 2012 Susan Spencer, Steve Conklin
+
+'''
+Licensing paragraph:
+
+1. CODE LICENSE: GPL 2.0+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+2. PATTERN LICENSE: CC BY-NC 3.0
+The output of this code is a pattern and is considered a
+visual artwork. The pattern is licensed under
+Attribution-NonCommercial 3.0 (CC BY-NC 3.0)
+
+Items made from the pattern may be sold;
+the pattern may not be sold.
+
+End of Licensing paragraph.
+'''
+
+import math, inkex
+from sewing_patterns import *
+
+class ShirtWaist(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument('--m_unit', default = 'Inches', help = 'Centimeters or Inches?')
+ pars.add_argument('--m_front_waist_length', type = float, default = '15.0', help = 'Front Waist Length')
+ pars.add_argument('--m_back_waist_length', type = float, default = '15.5', help = 'Back Waist Length')
+ pars.add_argument('--m_neck_circumference', type = float, default = '13.5', help = 'Neck Circumference')
+ pars.add_argument('--m_bust_circumference', type = float, default = '39.0', help = 'Bust Circumference')
+ pars.add_argument('--m_waist_circumference', type = float, default = '25.0', help = 'Waist Circumference')
+ pars.add_argument('--m_armscye_circumference', type = float, default = '15.0', help = 'Armscye circumference')
+ pars.add_argument('--m_across_back', type = float, default = '13.5', help = 'Across Back')
+ pars.add_argument('--m_shoulder', type = float, default = '6.5', help = 'Shoulder')
+ pars.add_argument('--m_side', type = float, default = '7.75', help = 'Side')
+ pars.add_argument('--m_upper_front_height', type = float, default = '10.75', help = 'Upper Front Height')
+ pars.add_argument('--m_overarm_length', type = float, default = '20.00', help = 'Overarm Length')
+ pars.add_argument('--m_elbow_height', type = float, default = '9.50', help = 'Elbow Height - from wrist to elbow')
+ pars.add_argument('--m_elbow_circumference', type = float, default = '12.50', help = 'Elbow Circumference - arm bent')
+ pars.add_argument('--m_hand_circumference', type = float, default = '8.00', help = 'Hand Circumference')
+
+ def effect(self):
+
+ def printPoint(pnt):
+ debug(' %s = %f, %f')%pnt.id, pnt.x, pnt.y
+
+ INCH_to_PX = 96.0 #inkscape 1.0 uses 96 pixels per 1 inch
+ CM_to_INCH = 1/2.54
+ CM_to_PX = CM_to_INCH*INCH_to_PX
+ CM = CM_to_PX # CM - shorthand when using centimeters
+ IN = INCH_to_PX # IN - shorthand when using inches
+
+ #all measurements must be converted to px
+ munit = self.options.m_unit
+ if munit == 'Centimeters':
+ MEASUREMENT_CONVERSION = CM
+ else:
+ MEASUREMENT_CONVERSION = IN
+
+ #convert measurements
+ front_waist_length = self.options.m_front_waist_length * MEASUREMENT_CONVERSION
+ neck_circumference = self.options.m_neck_circumference * MEASUREMENT_CONVERSION
+ bust_circumference = self.options.m_bust_circumference * MEASUREMENT_CONVERSION
+ waist_circumference = self.options.m_waist_circumference * MEASUREMENT_CONVERSION
+ armscye_circumference = self.options.m_armscye_circumference * MEASUREMENT_CONVERSION
+ across_back = self.options.m_across_back * MEASUREMENT_CONVERSION
+ shoulder = self.options.m_shoulder * MEASUREMENT_CONVERSION
+ side = self.options.m_side * MEASUREMENT_CONVERSION
+ upper_front_height = self.options.m_upper_front_height * MEASUREMENT_CONVERSION
+ overarm_length = self.options.m_overarm_length * MEASUREMENT_CONVERSION
+ elbow_height = self.options.m_elbow_height * MEASUREMENT_CONVERSION
+ elbow_circumference = self.options.m_elbow_circumference * MEASUREMENT_CONVERSION
+ hand_circumference = self.options.m_hand_circumference * MEASUREMENT_CONVERSION
+
+ #constants
+ ANGLE90 = angleOfDegree(90)
+ ANGLE180 = angleOfDegree(180)
+ SEAM_ALLOWANCE = (5/8.0)*IN
+ BORDER = 1*IN
+ NOTE_HEIGHT = 1*IN
+
+ #get the document, set initial width & height
+ doc = self.document.getroot() # self.document is the canvas seen in Inkscape
+ #add defs group with markers to document
+ addDefs(doc)
+
+ #width_orig = inkex.unittouu(doc.get('width'))
+ #height_orig = inkex.unittouu(doc.get('height'))
+ #doc_width = 4*BORDER+4*SEAM_ALLOWANCE + bust_circumference/2.0
+ #doc_height = 2*BORDER+3*SEAM_ALLOWANCE+(upper_front_height+side)
+ #doc.set('width', str(doc_width)) #temporary document width, doc is resized near end of pattern file
+ #doc.set('height', str(doc_height)) #temporary document height, doc is resized near end of pattern file
+
+
+ #create a base group in the document to hold all patterns
+ pattern_base = base(doc, 'pattern_base')
+ #create a pattern group for each pattern, put pattern group in base group - there can be multiple patterns
+ bodice = pattern(pattern_base, 'bodice')
+ # create a group for each pattern piece, put pattern piece group in pattern group
+ A = patternPiece(bodice, 'A', 'bodice_front', fabric = 2, interfacing = 0, lining = 0)
+ B = patternPiece(bodice, 'B', 'bodice_back', fabric = 2, interfacing = 0, lining = 0)
+ C = patternPiece(bodice, 'C', 'bodice_sleeve', fabric = 2, interfacing = 0, lining = 0)
+ D = patternPiece(bodice, 'D', 'bodice_cuff', fabric = 2, interfacing = 0, lining = 0)
+
+ #pattern notes
+ notes = []
+ notes.append('Define Seam Allowances: Select File/Inkscape Preferences/Steps and set Outset to 56.25px (5/8" seam allowance)')
+ notes.append('Create Seam Allowances: Press CTRL-F, type "cuttingline" in the ID field, click the Find button, press CTRL-)')
+ notes.append('Remove Points & Gridlines: Press CTRL-F, type "reference" in the Attribute field, click Find button, press DELETE')
+ notes.append('Print: Save as a PDF file, open PDF with PDF viewer (Adobe, Evince, Okular, xPDF), print from Print Preview option')
+
+ #pattern points
+ b1 = patternPointXY(B, 'b1', 0, 0) #B
+ b2 = patternPoint(B, 'b2', down(b1, front_waist_length)) #A
+ b3 = patternPoint(B, 'b3', up(b2, side)) #C
+ a1 = patternPoint(A, 'a1', left(b3, bust_circumference/2.0)) #D
+ b4 = patternPoint(B, 'b4', left(b3, across_back/2.0)) #E
+ b5 = patternPoint(B, 'b5', up(b4, armscye_circumference/3.0)) #F
+ b6 = patternPoint(B, 'b6', up(b1, 0.5*IN)) #G
+ b7 = patternPoint(B, 'b7', left(b6, 1.5*IN)) #H
+ b8 = patternPoint(B, 'b8', onLineAtLength(b5, b7, -0.5*IN)) #I
+ a2 = patternPoint(A, 'a2', left(b4, armscye_circumference/4.0)) #J
+ a3 = patternPoint(A, 'a3', midPoint(a2, b4)) #K
+ a4 = patternPoint(A, 'a4', up(a2, 2.5*IN)) #L
+ a5 = patternPoint(A, 'a5', up(b5, 1.5*IN)) #M
+ a6 = patternPoint(A, 'a6', left(a5, 2*IN)) #N
+ a7 = patternPoint(A, 'a7', left(a6, distance(b7, b8))) #O
+ a8 = patternPointXY(A, 'a8', a7.x, b3.y - (upper_front_height - distance(b1, b7))) #P
+ a9 = patternPoint(A, 'a9', down(a8, neck_circumference/4.0)) #Q
+ a10 = patternPoint(A, 'a10', up(a9, 0.5*IN)) #R
+ a11 = patternPoint(A, 'a11', left(a10, (neck_circumference/6.0)+0.25*IN )) #S
+ b9 = patternPoint(B, 'b9', midPoint(a3, b4)) #T on back bodice B
+ a12 = patternPoint(A, 'a12', b9) #T on front bodice A
+ b10 = patternPoint(B, 'b10', down(b9, side)) #U
+ b11 = patternPoint(B , 'b11', right(b10, 1*IN)) #V
+ a13 = patternPoint(A, 'a13', left(b10, 1*IN)) #W
+ a14 = patternPoint(A, 'a14', onLineAtLength(a11, a1, front_waist_length)) #X
+ a15 = patternPoint(A, 'a15', down(a8, distance(a8, a14))) #Y - new point at front waist
+ b12 = patternPoint(B, 'b12', up(b4, distance(b5, b4)/3.0)) #Z - new point at back armscye
+ #temporary armscye curve from a3 to b12 to find top point of side seam
+ length = distance(a3, b12)/3.0
+ temp_b12_c1 = right(a3, length) #don't create an svg controlpoint circle for this point
+ temp_b12_c2 = down(b12, length) #or for this point
+ #find top point of side seam with intersection of side and armscye curve, save to two points a16 and b13
+ curve1 = pointList(a3, temp_b12_c1, temp_b12_c2, b12)
+ intersections = intersectLineCurve(b10, b9, curve1) #this line is directional from b10 to b9
+ b13 = patternPoint(B, 'b13', intersections[0]) # AA on bodice back B -use 1st intersection found, in this case there's only one intersection
+ a16 = patternPoint(A, 'a16', b13) #AA on bodice back A
+
+ #front control points - path runs counterclockwise from front neck center a11
+ #front neck control points from a8 to a11
+ length = distance(a8, a11)/3.0
+ a11.c2 = controlPoint(A, 'a11.c2', right(a11, 1.5*length))
+ a11.c1 = controlPoint(A, 'a11.c1', polar(a8, length, angleOfLine(a8, a11.c2)))
+ #front waist control points from a14 to a15
+ length = distance(a14, a15)/3.0
+ a15.c1 = controlPoint(A, 'a15.c1', polar(a14, length, angleOfLine(a14, a11)+ANGLE90)) #control handle line is perpendicular to line a14-a11
+ a15.c2 = controlPoint(A, 'a15.c2', left(a15, length))
+ #front waist control points from a15 to a13
+ length = distance(a15, a13)/3.0
+ a13.c1 = controlPoint(A, 'a13.c1', right(a15, 1.5*length))
+ a13.c2 = controlPoint(A, 'a13.c2', polar(a13, length, angleOfLine(a13, a13.c1))) #second control aimed at first control point
+ #front side control points from a13 to a12
+ length = distance(a13, a12)/3.0
+ a12.c1 = controlPoint(A, 'a12.c1', up(a13, length))
+ a12.c2 = controlPoint(A, 'a12.c2', down(a12, length))
+ #front armscye control points from a16 to a3 to a4 to 16
+ length1 = distance(a16, a3)/3.0
+ length2 = distance(a3, a4)/3.0
+ length3 = distance(a4, a6)/3.0
+ angle1 = angleOfLine(a16, a3)
+ angle2 = ANGLE180
+ angle3 = (angle1+angle2)/2.0
+ a3.c1 = controlPoint(A, 'a3.c1', polar(a16, length1, angle1))
+ a3.c2 = controlPoint(A, 'a3.c2', polar(a3, length1, angle3-ANGLE180))
+ a4.c1 = controlPoint(A, 'a4.c1', polar(a3, length2, angle3))
+ angle4 = angleOfLine(a3, a6)
+ angle5 = angleOfLine(a4, a6)
+ angle6 = (angle4+angle5)/2.0
+ a4.c2 = controlPoint(A, 'a4.c2', polar(a4, 1.5*length2, angle6-ANGLE180))
+ a6.c1 = controlPoint(A, 'a6.c1', polar(a4, length3, angle6))
+ a6.c2 = controlPoint(A, 'a6.c2', polar(a6, length3/2.0, angleOfLine(a8, a6)+ANGLE90))
+
+ #back control points - path runs clockwise from back nape b1
+ #back neck control points from b7 to b1
+ length = distance(b7, b1)/3.0
+ b1.c1 = controlPoint(B, 'b1.c1', down(b7, length/2.0)) #short control point handle
+ b1.c2 = controlPoint(B, 'b1.c2', left(b1, length*2)) #long control point handle
+ #back side control points from b11 to b9
+ length = distance(b11, b9)/3.0
+ b9.c1 = controlPoint(B, 'b9.c1', up(b11, length))
+ b9.c2 = controlPoint(B, 'b9.c2', down(b9, length))
+ #back armscye points from b13 to b12 to b8
+ length1 = distance(b13, b12)/3.0
+ length2 = distance(b12, b8)/3.0
+ angle1 = angleOfLine(b13, b8)
+ b12.c1 = controlPoint(B, 'b12.c1', polar(b13, length1, angleOfLine(a3.c1, a16)))
+ b12.c2 = controlPoint(B, 'b12.c2', polar(b12, length1, angle1-ANGLE180))
+ b8.c1 = controlPoint(B, 'b8.c1', polar(b12, length2, angle1))
+ b8.c2 = controlPoint(B, 'b8.c2', polar(b8, length2/2.0, angleOfLine(b7, b8)-ANGLE90))
+
+ #sleeve C
+ c1 = patternPointXY(C, 'c1', 0.0, 0.0) #A
+ c2 = patternPoint(C, 'c2', down(c1, overarm_length)) #B
+ c3 = patternPoint(C, 'c3', up(c2, elbow_height)) #C
+ c4 = patternPoint(C, 'c4', right(c2, 1*IN)) #D
+ c5 = patternPoint(C, 'c5', right(c3, 0.5*IN)) #E
+ c6 = patternPoint(C, 'c6', left(c1, 1*IN)) #F
+ c7 = patternPoint(C, 'c7', right(c4, 1*IN)) #G
+ c8 = patternPoint(C, 'c8', right(c7, hand_circumference+2*IN)) #H
+ c9 = patternPoint(C, 'c9', right(c8, 1*IN)) #I
+ c10 = patternPoint(C, 'c10', right(c5, 1*IN) )#J
+ c11 = patternPoint(C, 'c11', right(c10, elbow_circumference)) #K
+ c12 = patternPoint(C, 'c12', right(c11, 0.5*IN)) #L
+ c13 = patternPoint(C, 'c13', right(c1, armscye_circumference)) #M
+ c14 = patternPoint(C, 'c14', right(c13, 2*IN)) #N
+ c15 = patternPoint(C, 'c15', up(c1, 2.5*IN)) #O
+ c16 = patternPoint(C, 'c16', right(c1, 1.5*IN)) #P
+ c17 = patternPoint(C, 'c17', left(c13, 3*IN)) #Q
+ c18 = patternPointXY(C, 'c18', c16.x, c15.y) #R
+ c19 = patternPointXY(C, 'c19', c17.x, c15.y) #S
+ c20 = patternPoint(C, 'c20', midPoint(c16, c17)) #T
+ c21 = patternPoint(C, 'c21', up(c20, distance(c20, c18))) #U - above T
+ c22 = patternPoint(C, 'c22', down(midPoint(c7, c8), 0.75*IN)) #V - was U
+ c23 = patternPoint(C, 'c23', right(c4, distance(c4, c8)*3/5.0)) #W
+ c24 = patternPoint(C, 'c24', up(c23, distance(c4, c3)/3.0)) #X - was V
+ c25 = patternPoint(C, 'c25', down(c23, 0.75*IN)) #Y - new point
+ # sleeve C control points
+ # sleevecap c6 to c18 to c21 to c19 to c13 to c14
+ length1 = distance(c6, c18)/3.0
+ length2 = distance(c18, c21)/3.0
+ c21.c2 = controlPoint(C, 'c21.c2', left(c21, length2))
+ c21.c1 = controlPoint(C, 'c21.c1', polar(c18, length2, angleOfLine(c18, c21.c2)))
+ angle = angleOfLine(c6, c18)+angleOfVector(c18, c6, c1)/2.0
+ c18.c1 = controlPoint(C, 'c18.c1', polar(c6, length1, angle))
+ c18.c2 = controlPoint(C, 'c18.c2', polar(c18, length1, angleOfLine(c21.c1, c18)))
+ length1 = distance(c21, c19)/3.0
+ length2 = distance(c19, c13)/3.0
+ length3 = distance(c13, c14)/3.0
+ c19.c1 = controlPoint(C, 'c19.c1', right(c21, length1))
+ c19.c2 = controlPoint(C, 'c19.c2', polar(c19, length1, angleOfLine(c19, c19.c1)))
+ c13.c1 = controlPoint(C, 'c13.c1', polar(c19, length2, angleOfLine(c19.c2, c19)))
+ angle1 = angleOfLine(c13.c1, c13)/2.0
+ c13.c2 = controlPoint(C, 'c13.c2', polar(c13, length2, angle1+ANGLE180))
+ c14.c1 = controlPoint(C, 'c14.c1', polar(c13, length3, angle1))
+ c14.c2 = controlPoint(C, 'c14.c2', polar(c14, length3, angleOfLine(c18.c1, c6)))
+ # c14 to c12
+ length = distance(c14, c12)/3.0
+ c12.c2 = controlPoint(C, 'c12.c2', polar(c12, length, angleOfLine(c9, c12)))
+ c12.c1 = controlPoint(C, 'c12.c1', polar(c14, length, angleOfLine(c14, c12.c2)))
+ # c9 to c25
+ length = distance(c9, c25)/3.0
+ c25.c2 = controlPoint(C, 'c25.c2', right(c25, length))
+ c25.c1 = controlPoint(C, 'c25.c1', polar(c9, length, angleOfLine(c9, c25.c2)))
+ #c22 to c4
+ length = distance(c22, c4)/3.0
+ c4.c1 = controlPoint(C, 'c4.c1', left(c22, length))
+ c4.c2 = controlPoint(C, 'c4.c2', polar(c4, length, angleOfLine(c4, c4.c1)))
+ #c5 to c6
+ length = distance(c5, c6)/3.0
+ c6.c1 = controlPoint(C, 'c6.c1', polar(c5, length, angleOfLine(c4, c5)))
+ c6.c2 = controlPoint(C, 'c6.c2', polar(c6, length, angleOfLine(c6, c6.c1)))
+
+
+ #cuff D
+ d1 = patternPointXY(D, 'd1', 0, 0)
+ d2 = patternPoint(D, 'd2', right(d1, hand_circumference+2*IN))
+ d3 = patternPoint(D, 'd3', down(d2, 3*IN))
+ d4 = patternPoint(D, 'd4', up(d3, 0.75*IN))
+ d5 = patternPoint(D, 'd5', left(d3, 1*IN))
+ d6 = patternPoint(D, 'd6', down(d1, 3*IN))
+ d7 = patternPoint(D, 'd7', right(d6, 1*IN))
+ d8 = patternPoint(D, 'd8', up(d6, 0.75*IN))
+ length1 = 0.7*distance(d1, d6)
+ length2 = 0.75*IN
+ d9 = patternPointXY(D, 'd9', d1.x+0.5*IN, d1.y+length1)
+ d10 = patternPoint(D, 'd10', right(d9, length2))
+ d11 = patternPointXY(D, 'd11', d2.x-0.5*IN, d2.y+length1)
+ d12 = patternPoint(D, 'd12', left(d11, length2))
+ #cuff D control points
+ length = distance(d4, d5)/3.0
+ d5.c1 = controlPoint(D, 'd5.c1', down(d4, length))
+ d5.c2 = controlPoint(D, 'd5.c2', right(d5, length))
+ d8.c1 = controlPoint(D, 'd8.c1', left(d7, length))
+ d8.c2 = controlPoint(D, 'd8.c2', down(d8, length))
+
+ # all points are defined, now create paths with them...
+ # pattern marks, labels, grainlines, seamlines, cuttinglines, darts, etc.
+
+ #bodice front A
+ #letter
+ pnt1 = Point('', a8.x, a6.c1.y)
+ addText(A, 'A_letter', pnt1.x, pnt1.y, 'A', fontsize = '72')
+ #label
+ pnt2 = down(pnt1, 0.5*IN)
+ addText(A, 'A_label', pnt2.x, pnt2.y, 'Bodice Front', fontsize = '48')
+ #label
+ pnt3 = down(pnt2, 0.5*IN)
+ addText(A, 'A_fabric', pnt3.x, pnt3.y, 'Cut 2 of fabric', fontsize = '38')
+ #grainline points
+ aG1 = down(a11, front_waist_length/3.0)
+ aG2 = polar(aG1, front_waist_length/2.0, angleOfLine(a11, a14))
+ path_str = formatPath('M', aG1, 'L', aG2)
+ A_grainline = addPath(A, 'A_grainline', path_str, 'grainline')
+ # gridline - helpful for troubleshooting during design phase
+ path_str = formatPath('M', a1, 'L', a3, 'M', a4, 'L', a2, 'M', a8, 'L', a15, 'M', a11, 'L', a10, 'M', a7, 'L', a5)
+ A_gridline = addPath(A, 'A_gridline', path_str, 'gridline')
+ #seamline & cuttingline
+ path_str = formatPath('M', a11, 'L', a14, 'C', a15.c1, a15.c2, a15, 'C', a13.c1, a13.c2, a13, 'C', a12.c1, a12.c2, a12)
+ path_str = path_str+formatPath('L', a16, 'C', a3.c1, a3.c2, a3, 'C', a4.c1, a4.c2, a4, 'C', a6.c1, a6.c2, a6, 'L', a8, 'C', a11.c1, a11.c2, a11)
+ A_seamline = addPath(A, 'A_seamline', path_str, 'seamline')
+ A_cuttingline = addPath(A, 'A_cuttingline', path_str, 'cuttingline')
+
+ #bodice back B
+ #letter
+ pnt1 = Point('', b8.x*2/3.0, b8.c2.y)
+ addText(B, 'B_letter', pnt1.x, pnt1.y, 'B', fontsize = '72') #
+ #label
+ pnt2 = down(pnt1, 0.5*IN)
+ addText(B, 'B_name', pnt2.x, pnt2.y, 'Bodice Back', fontsize = '48')
+ #label
+ pnt3 = down(pnt2, 0.5*IN)
+ addText(B, 'B_fabric', pnt3.x, pnt3.y, 'Cut 2 of fabric', fontsize = '38')
+ #grainline points
+ bG1 = down(b7, front_waist_length/3.0)
+ bG2 = down(bG1, front_waist_length/2.0)
+ path_str = formatPath('M', bG1, 'L', bG2)
+ B_grainline = addPath(B, 'B_grainline', path_str, 'grainline')
+ # gridline
+ path_str = formatPath('M', b1, 'L', b2, 'M', b11, 'L', b9, 'M', b9, 'L', b10, 'M', b7, 'L', b6, 'L', b1, 'M', b11, 'L', b10)
+ B_gridline = addPath(B, 'B_gridline', path_str, 'gridline')
+ #seamline & cuttingline
+ path_str = formatPath('M', b1, 'L', b2, 'L', b11, 'C', b9.c1, b9.c2, b9, 'L', b13, 'C', b12.c1, b12.c2, b12, 'C', b8.c1, b8.c2, b8, 'L', b7, 'C', b1.c1, b1.c2, b1)
+ B_seamline = addPath(B, 'B_seamline', path_str, 'seamline')
+ B_cuttingline = addPath(B, 'B_cuttingline', path_str, 'cuttingline')
+
+ #bodice sleeve C
+ #letter
+ pnt1 = Point('', c19.c1.x, c12.c1.y)
+ addText(C, 'C_letter', pnt1.x, pnt1.y, 'C', fontsize = '72') #
+ #label
+ pnt2 = down(pnt1, 0.5*IN)
+ addText(C, 'C_name', pnt2.x, pnt2.y, 'Bodice Sleeve', fontsize = '48')
+ #label
+ pnt3 = down(pnt2, 0.5*IN)
+ addText(C, 'C_fabric', pnt3.x, pnt3.y, 'Cut 2 of fabric', fontsize = '38')
+ #grainline points
+ cG1 = c20
+ cG2 = down(cG1, overarm_length/2.0)
+ path_str = formatPath('M', cG1, 'L', cG2)
+ C_grainline = addPath(C, 'C_grainline', path_str, 'grainline')
+ # gridline
+ path_str = formatPath('M', c15, 'L', c2, 'M', c15, 'L', c19, 'M', c2, 'L', c9, 'M', c3, 'L', c12, 'M', c6, 'L', c14, 'M', c18, 'L', c16, 'M', c19, 'L', c17)
+ C_gridline = addPath(C, 'C_gridline', path_str, 'gridline')
+ # slashline
+ path_str = formatPath('M', c24, 'L', c25)
+ C_slashline = addPath(C, 'C_slashline', path_str, 'slashline')
+ #seamline & cuttingline
+ path_str = formatPath('M', c6, 'C', c18.c1, c18.c2, c18, 'C', c21.c1, c21.c2, c21, 'C', c19.c1, c19.c2, c19, 'C', c13.c1, c13.c2, c13, 'C', c14.c1, c14.c2, c14)
+ path_str += formatPath('C', c12.c1, c12.c2, c12, 'L', c9, 'C', c25.c1, c25.c2, c25, 'L', c22, 'C', c4.c1, c4.c2, c4, 'L', c5, 'C', c6.c1, c6.c2, c6)
+ C_seamline = addPath(C, 'C_seamline', path_str, 'seamline')
+ C_cuttingline = addPath(C, 'C_cuttingline', path_str, 'cuttingline')
+
+ #bodice cuff D
+ #letter
+ pnt1 = Point('', d7.x, d6.y/4.0)
+ addText(D, 'D_letter', pnt1.x, pnt1.y, 'D', fontsize = '38') #
+ #label
+ pnt2 = right(pnt1, 1*IN)
+ addText(D, 'C_name', pnt2.x, pnt2.y, 'Bodice Sleeve Cuff', fontsize = '30')
+ #label
+ pnt3 = right(pnt2, 4*IN)
+ addText(D, 'C_fabric', pnt3.x, pnt3.y, 'Cut 2 of fabric', fontsize = '24')
+ pnt3 = down(pnt3, 0.3*IN)
+ addText(D, 'C_finterfacing', pnt3.x, pnt3.y, 'Cut 2 of interfacing', fontsize = '24')
+ #grainline points
+ pnt1 = midPoint(d1, d6)
+ dG1 = right(pnt1, distance(d1, d2)/4.0)
+ dG2 = right(dG1, distance(d1, d2)/2.0)
+ path_str = formatPath('M', dG1, 'L', dG2)
+ D_grainline = addPath(D, 'D_grainline', path_str, 'grainline')
+ # gridline
+ path_str = formatPath('M', d1, 'L', d2, 'L', d4, 'L', d5, 'L', d7, 'L', d8, 'L', d1)
+ D_gridline = addPath(D, 'D_gridline', path_str, 'gridline')
+ # slashline
+ path_str = formatPath('M', d9, 'L', d10, 'M', d11, 'L', d12)
+ D_slashline = addPath(D, 'D_slashline', path_str, 'slashline')
+ #seamline & cuttingline
+ path_str = formatPath('M', d1, 'L', d2, 'L', d4, 'C', d5.c1, d5.c2, d5, 'L', d7, 'C', d8.c1, d8.c2, d8, 'L', d1)
+ D_seamline = addPath(D, 'D_seamline', path_str, 'seamline')
+ D_cuttingline = addPath(D, 'D_cuttingline', path_str, 'cuttingline')
+
+ #layout patterns on document in rows
+ dx = BORDER+SEAM_ALLOWANCE #left border, allow width for seam allowance
+ dy = BORDER+NOTE_HEIGHT+2*SEAM_ALLOWANCE # print pattern under the note header, allow height for seam allowance plus extra space
+ pattern_buffer = 3*SEAM_ALLOWANCE #between any two patterns need 2 seam allowances plus additional space
+ # first row
+ pattern_offset = dx
+ row_offset = dy
+ #layout bodice front A
+ adx = pattern_offset-a14.x #left border offset dx, translate leftmost A point a14 to this offset
+ ady = row_offset-a8.y #upper height offset dy, translate highest A point a8
+ A.set('transform', 'translate('+str(adx)+' '+str(ady)+')')
+ pattern_offset = adx+a12.x+pattern_buffer
+ #layout bodice front B
+ bdx = pattern_offset-b9.x #translate leftmost B point
+ bdy = row_offset-b6.y #translate highest B point
+ B.set('transform', 'translate('+str(bdx)+' '+str(bdy)+')')
+
+ #2nd row
+ pattern_offset = dx
+ row_offset = ady+a15.y+pattern_offset # row_offset + lowest point from previous row, plus pattern_offset
+ #layout sleeve C
+ cdx = pattern_offset-c6.x
+ cdy = row_offset-c21.y
+ C.set('transform', 'translate('+str(cdx)+' '+str(cdy)+')')
+ pattern_offset = cdx+c14.x+pattern_buffer
+ #layout cuff D
+ ddx = pattern_offset-d1.x
+ ddy = row_offset-d1.y
+ D.set('transform', 'translate('+str(ddx)+' '+str(ddy)+')')
+ #3rd row, use this to calculate document height
+ row_offset = cdy+c25.y
+
+ #resize document to fit pattern piece layout
+ width = ddx+d2.x # use pattern piece that appears farthest to the right in Inkscape canvas
+ doc_width = width+2*SEAM_ALLOWANCE+2*BORDER
+ doc_height = row_offset+SEAM_ALLOWANCE+BORDER
+ root = self.svg.getElement('//svg:svg');
+ root.set('viewBox', '%f %f %f %f' % (0,0,doc_width,doc_height))
+ root.set('width', str(doc_width))
+ root.set('height', str(doc_height))
+
+ #Place notes on document after pattern pieces are transformed so that notes are centered on correct width
+ x = doc_width/2.0
+ y = BORDER
+ i = 0
+ for item in notes:
+ addText(bodice, 'note'+str(i), x, y, item, fontsize = '28', textalign = 'center', textanchor = 'middle', reference = 'false')
+ y = y+0.33*IN
+
+if __name__ == '__main__':
+ ShirtWaist().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/show_path_coordinates/meta.json b/extensions/fablabchemnitz/show_path_coordinates/meta.json
new file mode 100644
index 0000000..a72aa25
--- /dev/null
+++ b/extensions/fablabchemnitz/show_path_coordinates/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Show Path Coordinates",
+ "id": "fablabchemnitz.de.show_path_coordinates",
+ "path": "show_path_coordinates",
+ "dependent_extensions": null,
+ "original_name": "Export XY",
+ "original_id": "org.simarilius.filter.ExportXY",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/jwcliff/Inkscape_Exportxy/blob/master/LICENSE",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/show_path_coordinates",
+ "fork_url": "https://github.com/jwcliff/Inkscape_Exportxy",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Show+Path+Coordinates",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/jwcliff",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/show_path_coordinates/show_path_coordinates.inx b/extensions/fablabchemnitz/show_path_coordinates/show_path_coordinates.inx
new file mode 100644
index 0000000..95028e3
--- /dev/null
+++ b/extensions/fablabchemnitz/show_path_coordinates/show_path_coordinates.inx
@@ -0,0 +1,16 @@
+
+
+ Show Path Coordinates
+ fablabchemnitz.de.show_path_coordinates
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/show_path_coordinates/show_path_coordinates.py b/extensions/fablabchemnitz/show_path_coordinates/show_path_coordinates.py
new file mode 100644
index 0000000..a3abc90
--- /dev/null
+++ b/extensions/fablabchemnitz/show_path_coordinates/show_path_coordinates.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+#
+# curve xy co-ordinate export
+# Authors:
+# Jean Moreno
+# John Cliff
+# Neon22
+# Jens N. Lallensack
+# Mario Voigt
+#
+# Copyright (C) 2011 Jean Moreno
+# Copyright (C) 2011 John Cliff
+# Copyright (C) 2011 Neon22
+# Copyright (C) 2019 Jens N. Lallensack
+# Copyright (C) 2021 Mario Voigt
+
+# Released under GNU GPL v3, see https://www.gnu.org/licenses/gpl-3.0.en.html for details.
+#
+import inkex
+import sys
+from inkex.paths import CubicSuperPath
+from inkex import transforms
+
+class ShowPathCoordinates(inkex.EffectExtension):
+
+ def effect(self):
+ if len(self.svg.selected) > 0:
+ output_all = output_nodes = ""
+ for node in self.svg.selection.filter(inkex.PathElement):
+ node.apply_transform()
+ p = CubicSuperPath(node.get('d'))
+ for subpath in p:
+ for csp in subpath:
+ output_nodes += str(csp[1][0]) + "\t" + str(csp[1][1]) + "\n"
+ output_nodes += "\n"
+ sys.stderr.write(output_nodes.strip())
+ else:
+ inkex.errormsg('Please select some paths first.')
+ return
+
+if __name__ == '__main__':
+ ShowPathCoordinates().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/simple_frame/meta.json b/extensions/fablabchemnitz/simple_frame/meta.json
new file mode 100644
index 0000000..eea060c
--- /dev/null
+++ b/extensions/fablabchemnitz/simple_frame/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Simple Frame",
+ "id": "fablabchemnitz.de.simple_frame",
+ "path": "simple_frame",
+ "dependent_extensions": null,
+ "original_name": "Simple Frame",
+ "original_id": "org.inkscape.estucheria.caja4p",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/redentis/inkscape-extensions/blob/main/papercraft_frame.py",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/simple_frame",
+ "fork_url": "https://github.com/redentis/inkscape-extensions",
+ "documentation_url": "",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/redentis",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/simple_frame/simple_frame.inx b/extensions/fablabchemnitz/simple_frame/simple_frame.inx
new file mode 100644
index 0000000..94400dd
--- /dev/null
+++ b/extensions/fablabchemnitz/simple_frame/simple_frame.inx
@@ -0,0 +1,23 @@
+
+
+ Simple Frame
+ fablabchemnitz.de.simple_frame
+
+
+
+ 100.0
+ 150.0
+ 10.0
+ 20.0
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/simple_frame/simple_frame.py b/extensions/fablabchemnitz/simple_frame/simple_frame.py
new file mode 100644
index 0000000..3514a33
--- /dev/null
+++ b/extensions/fablabchemnitz/simple_frame/simple_frame.py
@@ -0,0 +1,272 @@
+#! /usr/bin/env python3
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+
+__version__ = "0.1"
+
+import inkex
+import math
+
+class GenerateFrame(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--width", type=float, default=100.0, help="Inner width")
+ pars.add_argument("--height", type=float, default=150.0, help="Inner height")
+ pars.add_argument("--depth", type=float, default=10.0, help="Frame depth")
+ pars.add_argument("--border", type=float, default=20.0, help="Frame border width")
+ pars.add_argument("--unit", default="mm", help="Unit of measure")
+
+ def effect(self):
+ center_x = self.svg.unittouu(self.document.getroot().get('width'))/2
+ center_y = self.svg.unittouu(self.document.getroot().get('height'))/2
+
+ _width = self.svg.unittouu(str(self.options.width) + self.options.unit)
+ _height = self.svg.unittouu(str(self.options.height) + self.options.unit)
+ _depth = self.svg.unittouu(str(self.options.depth) + self.options.unit)
+
+ _border = self.svg.unittouu(str(self.options.border) + self.options.unit)
+ _border_hyp = math.sqrt(2 * _border * _border)
+
+ id_frame = self.svg.get_unique_id('papercraft-frame')
+ group = self.svg.get_current_layer().add(inkex.Group(id=id_frame))
+ id_score = self.svg.get_unique_id('papercraft-scores')
+ score_group = group.add(inkex.Group(id=id_score))
+ cut_line = {'stroke': '#FF0000', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px'))}
+ safe_line = {'stroke': '#0000FF', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px'))}
+ valley_score_line = {'stroke': '#00FF00', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px')), 'stroke-dasharray': '1.05999995,0.52999997,0.26499999,0.52999997'}
+ mountain_score_line = {'stroke': '#00FF00', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px')), 'stroke-dasharray': '5,5'}
+
+ # line.path --> M = absolute coordinates
+ # line.path --> l = draws a line from the current point to the specified relative coordinates
+ # line.path --> c = draws a beizer curve from the current point to the specified coordinates
+ # line.path --> q = draw an arc from the current point to the specified coordinates using a point as reference
+ # line.path --> Z = close path
+
+ # outer profile (cut)
+ line = group.add(inkex.PathElement(id=id_frame + '-outer-profile'))
+ line.path = [
+ # top-left
+ ['M', [0, 2 * (_border+_depth)]],
+ ['l', [_border+_depth+_border,0]],
+ ['l', [0,-_depth]],
+ ['l', [_depth,0]],
+ ['l', [_border,-_border]],
+ ['l', [0,-_depth]],
+ ['l', [-_border,-_border]],
+ ['l', [_width+2*_border,0]],
+
+ # top-right
+ ['l', [-_border,_border]],
+ ['l', [0,_depth]],
+ ['l', [_border,_border]],
+ ['l', [_depth,0]],
+ ['l', [0,_depth]],
+ ['l', [_border+_depth+_border,0]],
+ ['l', [0,_height+2*_border]],
+
+ # bottom-right
+ ['l', [-(_border+_depth+_border),0]],
+ ['l', [0,_depth]],
+ ['l', [-_depth,0]],
+ ['l', [-_border,_border]],
+ ['l', [0,_depth]],
+ ['l', [_border,_border]],
+ ['l', [-(_width+2*_border),0]],
+
+ # bottom-left
+ ['l', [_border,-_border]],
+ ['l', [0,-_depth]],
+ ['l', [-_border,-_border]],
+ ['l', [-_depth,0]],
+ ['l', [0,-_depth]],
+ ['l', [-(_border+_depth+_border),0]],
+
+ ['Z', []]
+ ]
+ line.style = cut_line
+
+ line = group.add(inkex.PathElement(id=id_frame + '-inner-profile'))
+ line.path = [
+ ['M', [2*_depth+3*_border, 2*_depth+3*_border]],
+ ['l', [_width,0]],
+ ['l', [0,_height]],
+ ['l', [-_width,0]],
+ ['Z', []]
+ ]
+ line.style = safe_line
+
+ # score lines -- vertical
+ _top_edge = 2 *_border+2*_depth
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-1'))
+ line.path = [
+ ['M', [_border, _top_edge]],
+ ['l', [0, _height+2*_border]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-2'))
+ line.path = [
+ ['M', [_border+_depth, _top_edge]],
+ ['l', [0, _height+2*_border]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-3'))
+ line.path = [
+ ['M', [_border+_depth+_border, _top_edge]],
+ ['l', [0, _height+2*_border]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-4'))
+ line.path = [
+ ['M', [2*(_border+_depth), _top_edge-_depth]],
+ ['l', [0, _height+2*_border+2*_depth]],
+ ]
+ line.style = valley_score_line
+
+ _right_side = _width+4*_border+2*_depth
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-5'))
+ line.path = [
+ ['M', [_right_side, _top_edge-_depth]],
+ ['l', [0, _height+2*_border+2*_depth]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-6'))
+ line.path = [
+ ['M', [_right_side+_depth, _top_edge]],
+ ['l', [0, _height+2*_border]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-7'))
+ line.path = [
+ ['M', [_right_side+_border+_depth, _top_edge]],
+ ['l', [0, _height+2*_border]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-8'))
+ line.path = [
+ ['M', [_right_side+_border+_depth+_depth, _top_edge]],
+ ['l', [0, _height+2*_border]],
+ ]
+ line.style = valley_score_line
+
+ # corners
+ _o1_x = 2*_border + _depth
+ _o1_y = _o1_x + _depth
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-22'))
+ line.path = [
+ ['M', [_o1_x+_depth, _o1_y]],
+ ['l', [-_depth,-_depth]],
+ ]
+ line.style = mountain_score_line
+
+ _o2_x = _o1_x + _width + 2*_border + 2*_depth
+ _o2_y = _o1_y
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-24'))
+ line.path = [
+ ['M', [_o2_x-_depth, _o2_y]],
+ ['l', [_depth,-_depth]],
+ ]
+ line.style = mountain_score_line
+
+ _o3_x = _o1_x
+ _o3_y = _o1_y + _height + 2*_border
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-26'))
+ line.path = [
+ ['M', [_o3_x + _depth, _o3_y]],
+ ['l', [-_depth,_depth]],
+ ]
+ line.style = mountain_score_line
+
+ _o4_x = _o2_x
+ _o4_y = _o3_y
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-28'))
+ line.path = [
+ ['M', [_o4_x - _depth, _o4_y]],
+ ['l', [_depth,_depth]],
+ ]
+ line.style = mountain_score_line
+
+ # horizontals
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-31'))
+ line.path = [
+ ['M', [2*_border+_depth, 2*_border+2*_depth]],
+ ['l', [_width+2*_border+2*_depth,0]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-32'))
+ line.path = [
+ ['M', [2*_border+2*_depth, 2*_border+_depth]],
+ ['l', [_width+2*_border,0]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-33'))
+ line.path = [
+ ['M', [3*_border+2*_depth, _border+_depth]],
+ ['l', [_width,0]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-34'))
+ line.path = [
+ ['M', [3*_border+2*_depth, _border]],
+ ['l', [_width,0]],
+ ]
+ line.style = valley_score_line
+
+ _bottom = _height + 4*_border+2*_depth
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-35'))
+ line.path = [
+ ['M', [2*_border+_depth, _bottom]],
+ ['l', [_width+2*_border+2*_depth,0]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-36'))
+ line.path = [
+ ['M', [2*_border+2*_depth, _bottom+_depth]],
+ ['l', [_width+2*_border,0]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-37'))
+ line.path = [
+ ['M', [3*_border+2*_depth, _bottom+_border+_depth]],
+ ['l', [_width,0]],
+ ]
+ line.style = valley_score_line
+
+ line = score_group.add(inkex.PathElement(id=id_score + '-score-38'))
+ line.path = [
+ ['M', [3*_border+2*_depth, _bottom+_border+2*_depth]],
+ ['l', [_width,0]],
+ ]
+ line.style = valley_score_line
+
+if __name__ == '__main__':
+ GenerateFrame().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/spirograph/spirograph.inx b/extensions/fablabchemnitz/spirograph/spirograph.inx
index abcc413..250803e 100644
--- a/extensions/fablabchemnitz/spirograph/spirograph.inx
+++ b/extensions/fablabchemnitz/spirograph/spirograph.inx
@@ -7,7 +7,7 @@
- 10
+ 10
5
2
diff --git a/extensions/fablabchemnitz/triangle/meta.json b/extensions/fablabchemnitz/triangle/meta.json
new file mode 100644
index 0000000..8c6ecc8
--- /dev/null
+++ b/extensions/fablabchemnitz/triangle/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Triangle",
+ "id": "fablabchemnitz.de.triangle",
+ "path": "triangle",
+ "dependent_extensions": null,
+ "original_name": "Triangle",
+ "original_id": "math.triangle",
+ "license": "GNU GPL v2",
+ "license_url": "https://gitlab.com/inkscape/extensions/-/blob/master/LICENSE.txt",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/triangle",
+ "fork_url": "https://gitlab.com/inkscape/extensions/",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Triangle",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "John Beard:john.j.beard@gmail.com",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/triangle/triangle.inx b/extensions/fablabchemnitz/triangle/triangle.inx
new file mode 100644
index 0000000..fe6df16
--- /dev/null
+++ b/extensions/fablabchemnitz/triangle/triangle.inx
@@ -0,0 +1,37 @@
+
+
+ Triangle
+ fablabchemnitz.de.triangle
+
+
+
+
+
+
+
+
+ 100.0
+ 100.0
+ 100.0
+ 60
+ 30
+ 90
+
+
+
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/triangle/triangle.py b/extensions/fablabchemnitz/triangle/triangle.py
new file mode 100644
index 0000000..bc3f237
--- /dev/null
+++ b/extensions/fablabchemnitz/triangle/triangle.py
@@ -0,0 +1,188 @@
+#! /usr/bin/python3
+#
+# Copyright (C) 2007 John Beard john.j.beard@gmail.com
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+"""
+This extension allows you to draw a triangle given certain information
+ about side length or angles.
+
+Measurements of the triangle
+
+ C(x_c,y_c)
+ /`__
+ / a_c``--__
+ / ``--__ s_a
+ s_b / ``--__
+ /a_a a_b`--__
+ /--------------------------------``B(x_b, y_b)
+ A(x_a,y_a) s_b
+"""
+
+import sys
+from math import acos, asin, cos, pi, sin, sqrt
+
+import inkex
+
+X, Y = range(2)
+
+def draw_SVG_tri(point1, point2, point3, offset, width, name, parent):
+ style = {'stroke': '#000000', 'stroke-width': str(width), 'fill': 'none'}
+ elem = parent.add(inkex.PathElement())
+ elem.update(**{
+ 'style': style,
+ 'inkscape:label': name,
+ 'd': 'M ' + str(point1[X] + offset[X]) + ',' + str(point1[Y] + offset[Y]) +
+ ' L ' + str(point2[X] + offset[X]) + ',' + str(point2[Y] + offset[Y]) +
+ ' L ' + str(point3[X] + offset[X]) + ',' + str(point3[Y] + offset[Y]) +
+ ' L ' + str(point1[X] + offset[X]) + ',' + str(point1[Y] + offset[Y]) + ' z'})
+ return elem
+
+
+def angle_from_3_sides(a, b, c): # return the angle opposite side c
+ cosx = (a * a + b * b - c * c) / (2 * a * b) # use the cosine rule
+ return acos(cosx)
+
+
+def third_side_from_enclosed_angle(s_a, s_b, a_c): # return the side opposite a_c
+ c_squared = s_a * s_a + s_b * s_b - 2 * s_a * s_b * cos(a_c)
+ if c_squared > 0:
+ return sqrt(c_squared)
+ else:
+ return 0 # means we have an invalid or degenerate triangle (zero is caught at the drawing stage)
+
+
+def pt_on_circ(radius, angle): # return the x,y coordinate of the polar coordinate
+ x = radius * cos(angle)
+ y = radius * sin(angle)
+ return [x, y]
+
+
+def v_add(point1, point2): # add an offset to coordinates
+ return [point1[X] + point2[X], point1[Y] + point2[Y]]
+
+
+def is_valid_tri_from_sides(a, b, c): # check whether triangle with sides a,b,c is valid
+ return (a + b) > c and (a + c) > b and (b + c) > a and a > 0 and b > 0 and c > 0 # two sides must always be greater than the third
+ # no zero-length sides, no degenerate case
+
+
+def draw_tri_from_3_sides(s_a, s_b, s_c, offset, width, parent): # draw a triangle from three sides (with a given offset
+ if is_valid_tri_from_sides(s_a, s_b, s_c):
+ a_b = angle_from_3_sides(s_a, s_c, s_b)
+
+ a = (0, 0) # a is the origin
+ b = v_add(a, (s_c, 0)) # point B is horizontal from the origin
+ c = v_add(b, pt_on_circ(s_a, pi - a_b)) # get point c
+ c[1] = -c[1]
+
+ offx = max(b[0], c[0]) / 2 # b or c could be the furthest right
+ offy = c[1] / 2 # c is the highest point
+ offset = (offset[0] - offx, offset[1] - offy) # add the centre of the triangle to the offset
+
+ draw_SVG_tri(a, b, c, offset, width, 'Triangle', parent)
+ else:
+ inkex.errormsg('Invalid Triangle Specifications.')
+
+
+class Triangle(inkex.EffectExtension):
+ def add_arguments(self, pars):
+ pars.add_argument("--unit", default="mm", help="Units")
+ pars.add_argument("--s_a", type=float, default=100.0, help="Side Length a")
+ pars.add_argument("--s_b", type=float, default=100.0, help="Side Length b")
+ pars.add_argument("--s_c", type=float, default=100.0, help="Side Length c")
+ pars.add_argument("--a_a", type=float, default=60.0, help="Angle a")
+ pars.add_argument("--a_b", type=float, default=30.0, help="Angle b")
+ pars.add_argument("--a_c", type=float, default=90.0, help="Angle c")
+ pars.add_argument("--mode", default='3_sides', help="Side Length c")
+
+ def effect(self):
+ tri = self.svg.get_current_layer()
+ offset = self.svg.namedview.center
+ self.options.s_a = self.svg.unittouu(str(self.options.s_a) + self.options.unit)
+ self.options.s_b = self.svg.unittouu(str(self.options.s_b) + self.options.unit)
+ self.options.s_c = self.svg.unittouu(str(self.options.s_c) + self.options.unit)
+ stroke_width = self.svg.unittouu('1px')
+
+ if self.options.mode == '3_sides':
+ s_a = self.options.s_a
+ s_b = self.options.s_b
+ s_c = self.options.s_c
+ draw_tri_from_3_sides(s_a, s_b, s_c, offset, stroke_width, tri)
+
+ elif self.options.mode == 's_ab_a_c':
+ s_a = self.options.s_a
+ s_b = self.options.s_b
+ a_c = self.options.a_c * pi / 180 # in rad
+
+ s_c = third_side_from_enclosed_angle(s_a, s_b, a_c)
+ draw_tri_from_3_sides(s_a, s_b, s_c, offset, stroke_width, tri)
+
+ elif self.options.mode == 's_ab_a_a':
+ s_a = self.options.s_a
+ s_b = self.options.s_b
+ a_a = self.options.a_a * pi / 180 # in rad
+
+ if (a_a < pi / 2.0) and (s_a < s_b) and (s_a > s_b * sin(a_a)): # this is an ambiguous case
+ ambiguous = True # we will give both answers
+ else:
+ ambiguous = False
+
+ sin_a_b = s_b * sin(a_a) / s_a
+
+ if (sin_a_b <= 1) and (sin_a_b >= -1): # check the solution is possible
+ a_b = asin(sin_a_b) # acute solution
+ a_c = pi - a_a - a_b
+ error = False
+ else:
+ sys.stderr.write('Error:Invalid Triangle Specifications.\n') # signal an error
+ error = True
+
+ if not error and (a_b < pi) and (a_c < pi): # check that the solution is valid, if so draw acute solution
+ s_c = third_side_from_enclosed_angle(s_a, s_b, a_c)
+ draw_tri_from_3_sides(s_a, s_b, s_c, offset, stroke_width, tri)
+
+ if not error and ((a_b > pi) or (a_c > pi) or ambiguous): # we want the obtuse solution
+ a_b = pi - a_b
+ a_c = pi - a_a - a_b
+ s_c = third_side_from_enclosed_angle(s_a, s_b, a_c)
+ draw_tri_from_3_sides(s_a, s_b, s_c, offset, stroke_width, tri)
+
+ elif self.options.mode == 's_a_a_ab':
+ s_a = self.options.s_a
+ a_a = self.options.a_a * pi / 180 # in rad
+ a_b = self.options.a_b * pi / 180 # in rad
+
+ a_c = pi - a_a - a_b
+ s_b = s_a * sin(a_b) / sin(a_a)
+ s_c = s_a * sin(a_c) / sin(a_a)
+
+ draw_tri_from_3_sides(s_a, s_b, s_c, offset, stroke_width, tri)
+
+ elif self.options.mode == 's_c_a_ab':
+ s_c = self.options.s_c
+ a_a = self.options.a_a * pi / 180 # in rad
+ a_b = self.options.a_b * pi / 180 # in rad
+
+ a_c = pi - a_a - a_b
+ s_a = s_c * sin(a_a) / sin(a_c)
+ s_b = s_c * sin(a_b) / sin(a_c)
+
+ draw_tri_from_3_sides(s_a, s_b, s_c, offset, stroke_width, tri)
+
+
+if __name__ == '__main__':
+ Triangle().run()
diff --git a/extensions/fablabchemnitz/vertical_horizontal_scale/meta.json b/extensions/fablabchemnitz/vertical_horizontal_scale/meta.json
new file mode 100644
index 0000000..192cf2e
--- /dev/null
+++ b/extensions/fablabchemnitz/vertical_horizontal_scale/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Vertical / Horizontal Scale",
+ "id": "fablabchemnitz.de.vertical_horizontal_scale",
+ "path": "vertical_horizontal_scale",
+ "dependent_extensions": null,
+ "original_name": "Scale",
+ "original_id": "org.inkscape.render.render_scale",
+ "license": "GNU GPL v2",
+ "license_url": "https://github.com/brathering82/inkscape/blob/master/LICENSE",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/vertical_horizontal_scale",
+ "fork_url": "https://github.com/brathering82/inkscape",
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55018500",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/brathering82",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/vertical_horizontal_scale/vertical_horizontal_scale.inx b/extensions/fablabchemnitz/vertical_horizontal_scale/vertical_horizontal_scale.inx
new file mode 100644
index 0000000..b30a06d
--- /dev/null
+++ b/extensions/fablabchemnitz/vertical_horizontal_scale/vertical_horizontal_scale.inx
@@ -0,0 +1,81 @@
+
+
+ Vertical / Horizontal Scale
+ fablabchemnitz.de.vertical_horizontal_scale
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ false
+
+
+
+
+
+
+ 50.0
+
+ 0
+ 90
+ true
+
+
+ true
+
+ 0
+ 40
+
+ false
+
+ 3
+
+ false
+ false
+
+ 0
+ 0
+
+
+
+ 1.00000
+
+ false
+ 0.2
+ 0
+
+ 5.0
+ 0.4
+ 10
+
+ 85
+ 0.2
+ 5
+
+ 60
+ 0.2
+ 1
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/vertical_horizontal_scale/vertical_horizontal_scale.py b/extensions/fablabchemnitz/vertical_horizontal_scale/vertical_horizontal_scale.py
new file mode 100644
index 0000000..c7d1365
--- /dev/null
+++ b/extensions/fablabchemnitz/vertical_horizontal_scale/vertical_horizontal_scale.py
@@ -0,0 +1,542 @@
+#!/usr/bin/env python3
+'''
+Copyright (C)
+2009 Sascha Poczihoski, sascha@junktech.de
+Original author.
+
+2013 Roger Jeurissen, roger@acfd-consultancy.nl
+Added dangling labels and inside/outside scale features.
+
+2015 Paul Rogalinski, pulsar@codewut.de
+Adapted Inkscape 0.91 API changes.
+
+2015 Bit Barrel Media, bitbarrelmedia -at- gmail dot com
+-Changed UI and added the following features:
+ Label offset. This will move the labels side to side and up/down.
+ Option to use the center of a bounding box as the drawing reference.
+ Ability to set line stroke width.
+ Option to add a perpendicular line.
+ Mathematical expression for the number format. For example, to divide the label number by 2, use "n/2".
+ "Draw all labels" checkbox.
+ Option to flip the label orientation.
+ Support for "Draw every x lines" = 0 in order to remove lines.
+
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+TODO
+ - fix bug: special chars in suffix
+
+#for debugging:
+message = "Debug: " + str(i) + "\n"
+inkex.utils.debug(message)
+'''
+
+import math
+import inkex
+from lxml import etree
+
+class VerticalHorizontalScale(inkex.EffectExtension):
+
+
+ def add_arguments(self, pars):
+ # Define string option "--what" with "-w" shortcut and default value "World".
+ pars.add_argument('-f', '--scalefrom', type = int, default = '0', help = 'Number from...')
+ pars.add_argument('-t', '--scaleto', type = int,default = '20', help = 'Number to...')
+ pars.add_argument('--mathexpression', default = '', help = 'Math expression')
+ pars.add_argument('-c', '--reverse', default = 'false', help = 'Reverse order:')
+ pars.add_argument('-p', '--type', default = 'false', help = 'Type:')
+ pars.add_argument('--radius', type = float, default = '100', help = 'Radius')
+ pars.add_argument('--scaleradcount', type = float, default = '90', help = 'Circular count')
+ pars.add_argument('--scaleradbegin', type = float, default = '0', help = 'Circular begin')
+ pars.add_argument('--radmark', default = 'True', help = 'Mark origin')
+ pars.add_argument('--insidetf', type = inkex.Boolean, default = 'False', help = 'Swap inside out')
+ pars.add_argument('--ishorizontal', default = 'False', help = 'Horizontal labels')
+ pars.add_argument('--rotate', default = '0', help = 'Rotate:')
+ pars.add_argument('-b', '--units_per_line', type = float, default = '100', help = 'Units per line')
+ pars.add_argument('-g', '--labellinelength', type = float, default = '100', help = 'Label line - Length')
+ pars.add_argument('-s', '--fontsize', type = float, default = '3', help = 'Font Size:')
+ pars.add_argument('-i', '--suffix', default = '', help = 'Suffix:')
+ pars.add_argument('--drawalllabels', type = inkex.Boolean, default = 'True', help = 'Draw all labels')
+ pars.add_argument('--fliplabel', type = inkex.Boolean, default = 'False', help = 'Flip orientation')
+ pars.add_argument('--labellinestrokewidth', type = float, default = '0.4', help = 'Label line - Stroke width')
+ pars.add_argument('--longlinestrokewidth', type = float, default = '0.2', help = 'Long line - Stroke width')
+ pars.add_argument('--shortlinestrokewidth', type = float, default = '0.2', help = 'Short line - Stroke width')
+ pars.add_argument('--perplinestrokewidth', type = float, default = '0.2', help = 'Perpendicular line - Stroke width')
+
+ # label offset
+ pars.add_argument('-x', '--labeloffseth', type = float, default = '0', help = 'Label offset h:')
+ pars.add_argument('-y', '--labeloffsetv', type = float, default = '-3.5', help = 'Label offset v:')
+
+ # line spacing
+ pars.add_argument('-m', '--mark0', type = int, default = '10', help = 'Label line - Draw every x lines:')
+ pars.add_argument('-n', '--mark1', type = int, default = '5', help = 'Long line - Draw every x lines')
+ pars.add_argument('-o', '--mark2', type = int, default = '1', help = 'Short line - Draw every x lines')
+
+ # line length
+ pars.add_argument('-w', '--mark1wid', type = int, default = '75', help = 'Long line: - Length (units): (\%):')
+ pars.add_argument('-v', '--mark2wid', type = int, help = 'Short line: - Length (units): (\%):')
+ pars.add_argument('-u', '--unit', default = 'mm', help = 'Unit:')
+ pars.add_argument('--useref', type = inkex.Boolean, default = False, help = 'Reference is bounding box center')
+ pars.add_argument('--tab', default = 'global', help = '')
+ pars.add_argument("--perpline", type=inkex.Boolean, default=True, help="Perpendicular line")
+ pars.add_argument('--perplineoffset', type = float, default = '0', help = 'Offset')
+
+
+ def addLabel(self, n, x, y, group, fontsize, phi = 0.0):
+ mathexpression = self.options.mathexpression
+ fliplabel = self.options.fliplabel
+ drawalllabels = self.options.drawalllabels
+ labeloffseth = self.options.labeloffseth
+ labeloffsetv = self.options.labeloffsetv
+ scaletype = self.options.type
+ insidetf = self.options.insidetf
+ rotate = self.options.rotate
+ unit = self.options.unit
+
+ fontsize = self.svg.unittouu(str(fontsize)+unit)
+ labeloffseth = self.svg.unittouu(str(labeloffseth)+unit)
+ labeloffsetv = self.svg.unittouu(str(labeloffsetv)+unit)
+
+ #swapped and horizontal
+ if scaletype == 'straight' and insidetf and rotate=='90':
+ labeloffsetv *= -1
+
+ #swapped and vertical
+ if scaletype == 'straight' and insidetf and rotate=='0':
+ labeloffseth *= -1
+
+ if drawalllabels==True:
+ if fliplabel==True:
+ phi += 180
+
+ if scaletype == 'straight':
+ x = float(x) + labeloffseth
+ y = float(y) - labeloffsetv
+
+ res = self.options.units_per_line
+ pos = n*res + fontsize/2
+ suffix = self.options.suffix
+ text = etree.SubElement(group, inkex.addNS('text','svg'))
+
+ number = n;
+ try:
+ number = eval(mathexpression)
+ except (ValueError, SyntaxError, NameError):
+ pass
+
+ text.text = str(number)+suffix
+ cosphi=math.cos(math.radians(phi))
+ sinphi=math.sin(math.radians(phi))
+ a1 = str(cosphi)
+ a2 = str(-sinphi)
+ a3 = str(sinphi)
+ a4 = str(cosphi)
+ a5 = str((1-cosphi)*x-sinphi*y)
+ a6 = str(sinphi*x+(1-cosphi)*y)
+ fs = str(fontsize)
+ style = {'text-align' : 'center', 'text-anchor': 'middle', 'font-size': fs}
+ text.set('style', str(inkex.Style(style)))
+ text.set('transform', 'matrix({0},{1},{2},{3},{4},{5})'.format(a1,a2,a3,a4,a5,a6))
+ text.set('x', str(float(x)))
+ text.set('y', str(float(y)))
+ group.append(text)
+
+
+ def addLine(self, i, scalefrom, scaleto, group, grpLabel, type=2):
+ reverse = self.options.reverse
+ rotate = self.options.rotate
+ unit = self.options.unit
+ fontsize = self.options.fontsize
+ res = self.options.units_per_line
+ labellinestrokewidth = self.options.labellinestrokewidth
+ longlinestrokewidth = self.options.longlinestrokewidth
+ shortlinestrokewidth = self.options.shortlinestrokewidth
+ insidetf = self.options.insidetf
+
+ perplinestrokewidth = self.options.perplinestrokewidth
+ perplineoffset = self.options.perplineoffset
+
+ factor = 1
+ if insidetf==True:
+ factor = -1
+
+ #vertical
+ if rotate=='0':
+ res *= -1
+
+ label = False
+ if reverse=='true':
+ # Count absolute i for labeling
+ counter = 0
+ for n in range(scalefrom, i):
+ counter += 1
+ n = scaleto-counter-1
+ else:
+ n = i
+
+ #label line
+ if type==0:
+ name = 'label line'
+ stroke = self.svg.unittouu(str(labellinestrokewidth)+unit)
+ line_style = { 'stroke': 'black', 'stroke-width': stroke }
+ x1 = 0
+ y1 = i*res
+ x2 = self.options.labellinelength*factor
+ y2 = i*res
+
+ label = True
+
+ #long line
+ if type==1:
+ name = 'long line'
+ stroke = self.svg.unittouu(str(longlinestrokewidth)+unit)
+ line_style = { 'stroke': 'black', 'stroke-width': stroke }
+ x1 = 0
+ y1 = i*res
+ x2 = self.options.labellinelength*0.01*self.options.mark1wid*factor
+ y2 = i*res
+
+ #short line
+ name = 'short line'
+ if type==2:
+ stroke = self.svg.unittouu(str(shortlinestrokewidth)+unit)
+ line_style = { 'stroke': 'black', 'stroke-width': stroke }
+ x1 = 0
+ y1 = i*res
+ x2 = self.options.labellinelength*0.01*self.options.mark2wid*factor
+ y2 = i*res
+
+ #perpendicular line
+ if type==3:
+ name = 'perpendicular line'
+ stroke = self.svg.unittouu(str(perplinestrokewidth)+unit)
+ line_style = { 'stroke': 'black', 'stroke-width': stroke }
+
+ #if stroke is in px, use this logic:
+ # unitfactor = self.svg.unittouu(str(1)+unit)
+ # strokeoffset = (labellinestrokewidth / 2) / unitfactor
+
+ #if stroke is in units, use this logic:
+ strokeoffset = (labellinestrokewidth / 2)
+
+ x1 = perplineoffset
+ x2 = perplineoffset
+
+ #horizontal
+ if rotate=='90':
+ y2 = ((scaleto-1)*res) + strokeoffset
+ y1 = -strokeoffset
+
+ #vertical
+ else:
+ y2 = ((scaleto-1)*res) - strokeoffset
+ y1 = strokeoffset
+
+
+ x1 = str(self.svg.unittouu(str(x1)+unit) )
+ y1 = str(self.svg.unittouu(str(y1)+unit) )
+ x2 = str(self.svg.unittouu(str(x2)+unit) )
+ y2 = str(self.svg.unittouu(str(y2)+unit) )
+
+ #horizontal
+ if rotate=='90':
+ tx = x1
+ x1 = y1
+ y1 = tx
+
+ tx = x2
+ x2 = y2
+ y2 = tx
+
+ if label==True:
+ self.addLabel(n , x2, y2, grpLabel, fontsize)
+
+ line_attribs = {'style' : str(inkex.Style(line_style)), inkex.addNS('label','inkscape') : name, 'd' : 'M '+x1+','+y1+' L '+x2+','+y2}
+ line = etree.SubElement(group, inkex.addNS('path','svg'), line_attribs )
+
+
+ def addLineRad(self, i, scalefrom, scaleto, group, grpLabel, type=2, ishorizontal=True):
+ height = self.options.labellinelength
+ reverse = self.options.reverse
+ radbegin = self.options.scaleradbegin
+ radcount = self.options.scaleradcount
+ unit = self.options.unit
+ fontsize = self.options.fontsize
+ radius = self.options.radius
+ labeloffseth = self.options.labeloffseth
+ labeloffsetv = self.options.labeloffsetv
+ insidetf = self.options.insidetf
+ labellinestrokewidth = self.options.labellinestrokewidth
+ longlinestrokewidth = self.options.longlinestrokewidth
+ shortlinestrokewidth = self.options.shortlinestrokewidth
+ perplinestrokewidth = self.options.perplinestrokewidth
+ perplineoffset = self.options.perplineoffset
+
+ label = False
+
+ labeloffsetv *= -1
+
+ # Count absolute count for evaluation of increment
+ count = 0
+ for n in range(scalefrom, scaleto):
+ count += 1
+ countstatus = 0
+ for n in range(scalefrom, i):
+ countstatus += 1
+
+ if reverse=='true':
+ counter = 0
+ for n in range(scalefrom, i):
+ counter += 1
+ n = scaleto-counter-1
+ else:
+ n = i
+ inc = radcount / (count-1)
+ irad = countstatus*inc
+ irad = -1 * (radbegin+irad+180)
+
+ dangle = 0
+ if ishorizontal=='false':
+ dangle = 1
+
+ inside = -1
+ if insidetf==True:
+ inside = 1
+
+ #label line
+ if type==0:
+ name = 'label line'
+ stroke = self.svg.unittouu(str(labellinestrokewidth)+unit)
+ line_style = { 'stroke': 'black', 'stroke-width': stroke }
+ x1 = math.sin(math.radians(irad))*radius
+ y1 = math.cos(math.radians(irad))*radius
+ x2 = math.sin(math.radians(irad))*(radius-inside*height)
+ y2 = math.cos(math.radians(irad))*(radius-inside*height)
+
+ label = True
+
+ #long line
+ if type==1:
+ name = 'long line'
+ stroke = self.svg.unittouu(str(longlinestrokewidth)+unit)
+ line_style = { 'stroke': 'black', 'stroke-width': stroke }
+ x1 = math.sin(math.radians(irad))*radius
+ y1 = math.cos(math.radians(irad))*radius
+ x2 = math.sin(math.radians(irad))*(radius-inside*height*self.options.mark1wid*0.01)
+ y2 = math.cos(math.radians(irad))*(radius-inside*height*self.options.mark1wid*0.01)
+
+ #short line
+ if type==2:
+ name = 'short line'
+ stroke = self.svg.unittouu(str(shortlinestrokewidth)+unit)
+ line_style = { 'stroke': 'black', 'stroke-width': stroke }
+ x1 = math.sin(math.radians(irad))*radius
+ y1 = math.cos(math.radians(irad))*radius
+ x2 = math.sin(math.radians(irad))*(radius-inside*height*self.options.mark2wid*0.01)
+ y2 = math.cos(math.radians(irad))*(radius-inside*height*self.options.mark2wid*0.01)
+
+ #perpendicular line
+ if type==3:
+ name = 'perpendicular line'
+ stroke = self.svg.unittouu(str(perplinestrokewidth)+unit)
+ line_style = {'stroke': 'black', 'stroke-width' : stroke, 'fill': 'none'}
+
+ rx = self.svg.unittouu(str(radius+perplineoffset)+unit)
+ ry = rx
+
+ #if stroke is in px, use this logic:
+ #unitfactor = self.svg.unittouu(str(1)+unit)
+ #strokeoffset = math.atan(((labellinestrokewidth / 2) / unitfactor) / radius)
+
+ #if stroke is in units, use this logic:
+ strokeoffset = math.atan((labellinestrokewidth / 2) / radius)
+
+ start = math.radians(radbegin + 270) - strokeoffset
+ end = math.radians(radbegin+radcount + 270) + strokeoffset
+
+ if radcount != 360:
+ line_attribs = {'style':str(inkex.Style(line_style)),
+ inkex.addNS('label','inkscape') :name,
+ inkex.addNS('cx','sodipodi') :str(0),
+ inkex.addNS('cy','sodipodi') :str(0),
+ inkex.addNS('rx','sodipodi') :str(rx),
+ inkex.addNS('ry','sodipodi') :str(ry),
+ inkex.addNS('start','sodipodi') :str(start),
+ inkex.addNS('end','sodipodi') :str(end),
+ inkex.addNS('open','sodipodi') :'true', #all ellipse sectors we will draw are open
+ inkex.addNS('type','sodipodi') :'arc',}
+ else:
+ line_attribs = {'style':str(inkex.Style(line_style)),
+ inkex.addNS('label','inkscape') :name,
+ inkex.addNS('cx','sodipodi') :str(0),
+ inkex.addNS('cy','sodipodi') :str(0),
+ inkex.addNS('rx','sodipodi') :str(rx),
+ inkex.addNS('ry','sodipodi') :str(ry),
+ inkex.addNS('open','sodipodi') :'true', #all ellipse sectors we will draw are open
+ inkex.addNS('type','sodipodi') :'arc',}
+
+ if type!=3:
+ # use user unit
+ x1 = self.svg.unittouu(str(x1)+unit)
+ y1 = self.svg.unittouu(str(y1)+unit)
+ x2 = self.svg.unittouu(str(x2)+unit)
+ y2 = self.svg.unittouu(str(y2)+unit)
+
+ if label==True :
+
+ #if the circle count is 360 degrees, do not draw the last label because it will overwrite the first.
+ if not (radcount==360 and n==360):
+ x2label = math.sin(math.radians(irad + labeloffseth))*(radius-inside*(-labeloffsetv+height*self.options.mark2wid*0.01))
+ y2label = math.cos(math.radians(irad + labeloffseth))*(radius-inside*(-labeloffsetv+height*self.options.mark2wid*0.01))
+ x2label = self.svg.unittouu(str(x2label)+unit)
+ y2label = self.svg.unittouu(str(y2label)+unit)
+ self.addLabel(n , x2label, y2label, grpLabel, fontsize,dangle*(irad+labeloffseth))
+
+ line_attribs = {'style' : str(inkex.Style(line_style)), inkex.addNS('label','inkscape') : name, 'd' : 'M '+str(x1)+','+str(y1)+' L '+str(x2)+','+str(y2)}
+
+ line = etree.SubElement(group, inkex.addNS('path','svg'), line_attribs )
+
+
+ def skipfunc(self, i, markArray, groups):
+
+ skip = True
+ group = groups[0]
+ type = 0
+
+ if markArray[0] != 0:
+ if (i % markArray[0])==0:
+ type = 0 # the labeled line
+ group = groups[0]
+ skip = False
+
+ if markArray[1] != 0 and skip==1:
+ if (i % markArray[1])==0:
+ type = 1 # the long line
+ group = groups[1]
+ skip = False
+
+ if markArray[2] != 0 and skip==1:
+ if (i % markArray[2])==0:
+ type = 2 # the short line
+ group = groups[2]
+ skip = False
+
+ return (skip, group, type)
+
+
+
+ def effect(self):
+ scalefrom = self.options.scalefrom
+ scaleto = self.options.scaleto
+
+ scaleGroup = self.svg.get_current_layer().add(inkex.Group())
+ groups = [None, None, None, None]
+ markArray = [self.options.mark0, self.options.mark1, self.options.mark2]
+
+ # Get access to main SVG document element and get its dimensions.
+ svg = self.document.getroot()
+
+ # Again, there are two ways to get the attributes:
+ width = self.svg.unittouu(svg.get('width'))
+ height = self.svg.unittouu(svg.get('height'))
+
+ centre = self.svg.namedview.center #Put in in the centre of the current view
+
+ if self.options.useref is True:
+ self.bbox = sum([node.bounding_box() for node in self.svg.selected.values() ])
+ try:
+ test = self.bbox[0]
+ except TypeError:
+ pass
+ else:
+ half = (self.bbox[1] - self.bbox[0]) / 2
+ x = self.bbox[0] + half
+
+ half = (self.bbox[3] - self.bbox[2]) / 2
+ y = self.bbox[2] + half
+ centre = (x, y)
+
+ grp_transform = 'translate(' + str(centre) + ')'
+
+ grp_name = 'Label line'
+ grp_attribs = {inkex.addNS('label','inkscape'):grp_name, 'transform':grp_transform }
+ groups[0] = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs)
+
+ if self.options.mark1 > 0:
+ grp_name = 'Long line'
+ grp_attribs = {inkex.addNS('label','inkscape'):grp_name, 'transform':grp_transform }
+ groups[1] = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs)
+
+ if self.options.mark2 > 0:
+ grp_name = 'Short line'
+ grp_attribs = {inkex.addNS('label','inkscape'):grp_name, 'transform':grp_transform }
+ groups[2] = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs)
+
+ if self.options.drawalllabels is True:
+ grp_name = 'Labels'
+ grp_attribs = {inkex.addNS('label','inkscape'):grp_name, 'transform':grp_transform }
+ groups[3] = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs)
+
+ # to allow positive to negative counts
+ if scalefrom < scaleto:
+ scaleto += 1
+ else:
+ temp = scaleto
+ scaleto = scalefrom+1
+ scalefrom = temp
+
+ if self.options.type == 'straight':
+
+ for i in range(scalefrom, scaleto):
+ skip, group, type = self.skipfunc(i, markArray, groups)
+ if skip==False:
+ self.addLine(i, scalefrom, scaleto, group, groups[3], type) # addLabel is called from inside
+
+ #add the perpendicular line
+ if self.options.perpline is True:
+ self.addLine(0, scalefrom, scaleto, groups[0], groups[3], 3)
+
+ elif self.options.type == 'circular':
+
+ for i in range(scalefrom, scaleto):
+ skip, group, type = self.skipfunc(i, markArray, groups)
+ if skip==False:
+ self.addLineRad(i, scalefrom, scaleto, group, groups[3], type, self.options.ishorizontal) # addLabel is called from inside
+
+ #add the perpendicular (circular) line
+ if self.options.perpline is True:
+ self.addLineRad(0, scalefrom, scaleto, groups[0], groups[3], 3, self.options.ishorizontal)
+
+ if self.options.radmark=='true':
+
+ grp_name = 'Radial center'
+ grp_attribs = {inkex.addNS('label','inkscape'):grp_name, 'transform':grp_transform }
+ grpRadMark = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs)
+
+ line_style = { 'stroke': 'black', 'stroke-width': '1' }
+ line_attribs = {'style' : str(inkex.Style(line_style)), inkex.addNS('label','inkscape') : 'name', 'd' : 'M '+str(-10)+','+str(-10)+' L '+str(10)+','+str(10)}
+ line = etree.SubElement(grpRadMark, inkex.addNS('path','svg'), line_attribs )
+
+ line_attribs = {'style' : str(inkex.Style(line_style)), inkex.addNS('label','inkscape') : 'name', 'd' : 'M '+str(-10)+','+str(10)+' L '+str(10)+','+str(-10)}
+ line = etree.SubElement(grpRadMark, inkex.addNS('path','svg'), line_attribs )
+
+ #add all sub groups into a top one
+ for group in groups:
+ if group is not None:
+ scaleGroup.append(group)
+
+if __name__ == '__main__':
+ VerticalHorizontalScale().run()
\ No newline at end of file