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