Another set of reintegrated extensions

This commit is contained in:
Mario Voigt 2022-09-02 18:19:57 +02:00
parent a38a160484
commit 7cab1a92ea
88 changed files with 7769 additions and 1 deletions

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Apply Transformations</name>
<id>fablabchemnitz.de.apply_transformations</id>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Transformations"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">apply_transformations.py</command>
</script>
</inkscape-extension>

View File

@ -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. <g style="...">
self.scaleStrokeWidth(element, transf)
for child in element.getchildren():
self.recursiveFuseTransform(child, transf)
if __name__ == '__main__':
ApplyTransformations().run()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Box Maker - T-Slot</name>
<id>fablabchemnitz.de.box_maker_t_slot</id>
<param name="unit" gui-text="Unit" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
</param>
<param name="inside" type="optiongroup" appearance="combo" gui-text="Box Dimensions">
<option value="1">Inside</option>
<option value="0">Outside</option>
</param>
<param name="length" type="float" precision="3" min="0.0" max="10000.0" gui-text="Length">80.0</param>
<param name="width" type="float" precision="3" min="0.0" max="10000.0" gui-text="Width">80.0</param>
<param name="depth" type="float" precision="3" min="0.0" max="10000.0" gui-text="Height">80.0</param>
<param name="tab" type="float" precision="2" min="0.0" max="10000.0" gui-text="Minimum/Prefered Tab Width">10.0</param>
<param name="equal" type="optiongroup" appearance="combo" gui-text="Tab Width">
<option value="0">Fixed</option>
<option value="1">Proportional</option>
</param>
<param name="thickness" type="float" precision="2" min="0.0" max="10000.0" gui-text="Material Thickness">6.0</param>
<param name="kerf" type="float" precision="3" min="0.0" max="10000.0" gui-text="Kerf (cut width)">0.0</param>
<param name="clearance" type="float" precision="3" min="0.0" max="10000.0" gui-text="Clearance">0.05</param>
<param name="style" gui-text="Layout/Style" type="optiongroup" appearance="combo">
<option value="1">Diagramatic</option>
<option value="2">3 piece</option>
<option value="3">Inline(compact)</option>
<option value="4">Diag Alternate Tabs</option>
</param>
<param name="spacing" type="float" precision="2" min="0.0" max="10000.0" gui-text="Space Between Parts">5.0</param>
<param name="screw_length" type="float" precision="2" min="0.0" max="10000.0" gui-text="Screw Length">12</param>
<param name="screw_diameter" type="float" precision="2" min="0.0" max="10000.0" gui-text="Screw Diameter">3</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Boxes/Papercraft">
<submenu name="Finger-jointed/Tabbed Boxes"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">box_maker_t_slot.py</command>
</script>
</inkscape-extension>

View File

@ -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 <http://www.gnu.org/licenses/>.
'''
__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 nomTab<thickness:
inkex.errormsg(_('Error: Tab size too small'))
error=1
if thickness==0:
inkex.errormsg(_('Error: Thickness is zero'))
error=1
if thickness>min(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<kerf:
inkex.errormsg(_('Error: Spacing too small'))
error=1
if error: exit()
# layout format:(rootx),(rooty),Xlength,Ylength,tabInfo
# root= (spacing,X,Y,Z) * values in tuple
# tabInfo= <abcd> 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()

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Colorize Path Lengths/Slants</name>
<id>fablabchemnitz.de.colorize_path_lengths</id>
<param name="selection" type="optiongroup" appearance="combo" gui-text="Selection: ">
<option value="path_lengthselection">Length selection</option>
<option value="path_slantselection">Slant selection</option>
</param>
<label>Unit for lengths is px</label>
<param name="len1" type="float" gui-text="Len1: red &lt;" min="1" max="100">12</param>
<param name="len2" type="float" gui-text="Len2: green &lt; =" min="1" max="100">25</param>
<param name="len3" type="float" gui-text="Len3: greenyellow &lt; =" min="1" max="100">40</param>
<param name="len4" type="float" gui-text="Len4: skyblue &lt; =" min="1" max="100">60</param>
<param name="len5" type="float" gui-text="Len5: blue &gt;" min="1" max="100">60</param>
<param name="hor" type="float" gui-text="hor: red &lt; (H/W)" min="0.01" max="100">0.1</param>
<param name="ver" type="float" gui-text="ver: blue &gt;" min="1" max="100">10</param>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Dimensioning/Measuring"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">colorize_path_lengths.py</command>
</script>
</inkscape-extension>

View File

@ -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 tn<self.options.hor:
hor1.append(p[i])
elif tn>self.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()

View File

@ -0,0 +1,21 @@
[
{
"name": "Colorize Path Lengths/Slants",
"id": "fablabchemnitz.de.colorize_path_lengths",
"path": "colorize_path_lengths",
"dependent_extensions": null,
"original_name": "<unknown>",
"original_id": "<unknown>",
"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"
]
}
]

View File

@ -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).

View File

@ -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 <http://www.gnu.org/licenses/>.

View File

@ -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 <http://www.gnu.org/licenses/>.
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"]

View File

@ -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 <http://www.gnu.org/licenses/>.
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, '<CUT'] for cut in range(1, cuts+1)] + \
[[rotation+start+(end-start)/(cuts+1)*cut+c, 'CUT>'] for cut in range(1, cuts+1)]
else:
angles = [[rotation+end/segments*seg, 'SEG'] for seg in range(segments)] + \
[[rotation+end/cuts*cut-c, '<CUT'] for cut in range(cuts)] + \
[[rotation+end/cuts*cut+c, 'CUT>'] 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]=='<CUT' else None)
angle.append(self._cprev(angles, i, '<CUT') if angle[1]=='CUT>' 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] == '<CUT' else 1
a = angle[0] - mult*c
a2 = a + mult*pi/2
# Line from previous to next segment point.
line1 = Line(angles[angle[2]][6][0], angles[angle[3]][6][0])
# Line from origin offset by thickness.
p1 = self._pos(a2, self.thickness/2)
p2 = p1 + self._pos(a, radius)
line2 = Line(p1, p2)
pintersect = line1.intersection(line2)
pinset = pintersect + self._pos(a, -cutdepth)
if angle[1] == '<CUT':
angle.append([pintersect, pinset])
else:
angle.append([pinset, pintersect])
d1 = pinset.distance(Point(0.0,0.0))
d2 = angles[angle[5]][6][1].distance(Point(0.0,0.0))
if d1<d2:
angles[angle[5]][6][1] = pinset - self._pos(a2, self.thickness)
elif d2<d1:
angle[6][0] = angles[angle[5]][6][1] + self._pos(a2, self.thickness)
pass
incut = False
for i, angle in enumerate(angles):
atype = angle[1]
if atype=='<CUT':
incut = True
elif atype=='CUT>':
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
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)]

View File

@ -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 <http://www.gnu.org/licenses/>.
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]) + ")"

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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 <http://www.gnu.org/licenses/>.
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 "") + ")"

View File

@ -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 <http://www.gnu.org/licenses/>.
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) + ")"

View File

@ -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 <http://www.gnu.org/licenses/>.
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) + ")"

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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 <http://www.gnu.org/licenses/>.
from .platform import Platform
from .circular import Circular
from .rollerframe import RollerFrame
__all__ = ["Platform", "Circular", "RollerFrame"]

View File

@ -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 <http://www.gnu.org/licenses/>.
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))

View File

@ -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 <http://www.gnu.org/licenses/>.
from ..core.part import Part
class Platform(Part):
def __init__(self, thickness):
super(Platform, self).__init__()
self.thickness = thickness
return

View File

@ -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 <http://www.gnu.org/licenses/>.
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))

View File

@ -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 <http://www.gnu.org/licenses/>.
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"]

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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 <http://www.gnu.org/licenses/>.
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))

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
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()

View File

@ -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 <http://www.gnu.org/licenses/>.
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__()

View File

@ -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 <http://www.gnu.org/licenses/>.
from .support import Support
from .pier import Pier
__all__ = ["Support", "Pier"]

View File

@ -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 <http://www.gnu.org/licenses/>.
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<n>: The height of the bottom of the platform (0.0=at base level, height-thickness=at top level).
# x-offset<n>: 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))

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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 <http://www.gnu.org/licenses/>.
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().")

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Cut-Craft Box</name>
<id>fablabchemnitz.de.cutcraft.box</id>
<param name="unit" gui-text="Measurement Units" gui-description="Unit of measurement for all subsequent values entered in this dialog" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
</param>
<separator/>
<param name="width" type="float" min="10.0" max="1000.0" precision="3" gui-text="Box Width" gui-description="Cylinder Width">60.0</param>
<param name="depth" type="float" min="10.0" max="1000.0" precision="3" gui-text="Box Depth" gui-description="Cylinder Depth">30.0</param>
<param name="height" type="float" min="10.0" max="1000.0" precision="3" gui-text="Box Height" gui-description="Cylinder Height">30.0</param>
<separator/>
<param name="thickness" type="float" min="0.1" max="1000.0" precision="3" gui-text="Material Thickness" gui-description="Thickness of the material">5.0</param>
<param name="kerf" type="float" min="0.0" max="1000.0" precision="3" gui-text="Laser Cutter Kerf" gui-description="Laser Cutter Kerf (tolerance). Varies based on cutter and material thickness">0.01</param>
<param name="linethickness" gui-text="Line Thickness" gui-description="Thickness of the cutting line on the display" type="optiongroup" appearance="combo">
<option value="1px">1 pixel</option>
<option value="0.002in">hairline</option>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Boxes/Papercraft">
<submenu name="Finger-jointed/Tabbed Boxes" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">cutcraftbox.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Cut-Craft Cylinder</name>
<id>fablabchemnitz.de.cutcraft.cylinder</id>
<param name="unit" gui-text="Measurement Units" gui-description="Unit of measurement for all subsequent values entered in this dialog." type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
</param>
<separator />
<param name="height" type="float" min="10.0" max="1000.0" precision="3" gui-text="Height" gui-description="Cylinder Height">60.0</param>
<param name="outer" type="float" min="0.1" max="1000.0" precision="3" gui-text="Outer diameter" gui-description="Outside diameter of the Cylinder">60.0</param>
<param name="inner" type="float" min="0.1" max="1000.0" precision="3" gui-text="Inner diameter" gui-description="Inside diameter of the Cylinder">30.0</param>
<param name="vertices" type="int" min="3" max="180" gui-text="Number of Vertices (3..180)" gui-description="Number of vertices for the Cylinder (3 = Triangle, 4 = Square, ... 90 = Circular)">3</param>
<param name="levels" type="int" min="2" max="100" gui-text="Number of Levels (2..100)" gui-description="Number of horizontal circular platforms">2</param>
<param name="supports" type="int" min="3" max="18" gui-text="Number of Supports (3..18)" gui-description="Number of vertical supports holding the cylinder together">3</param>
<param name="supwidth" type="float" min="0.1" max="1000.0" gui-text="Support Width" gui-description="Width of the vertical supports holding the cylinder together">6.0</param>
<separator />
<param name="thickness" type="float" min="0.1" max="1000.0" precision="3" gui-text="Material Thickness" gui-description="Thickness of the material">5.0</param>
<param name="kerf" type="float" min="0.0" max="1000.0" precision="3" gui-text="Laser Cutter Kerf" gui-description="Laser Cutter Kerf (tolerance). Varies based on cutter and material thickness">0.01</param>
<param name="linethickness" gui-text="Line Thickness" gui-description="Thickness of the cutting line on the display" type="optiongroup" appearance="combo">
<option value="1px">1 pixel</option>
<option value="0.002in">hairline</option>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Boxes/Papercraft">
<submenu name="Finger-jointed/Tabbed Boxes" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">cutcraftcylinder.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Cut-Craft RollerBot</name>
<id>fablabchemnitz.de.cutcraft.rollerbot</id>
<param name="unit" gui-text="Measurement Units" gui-description="Unit of measurement for all subsequent values entered in this dialog" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
</param>
<separator/>
<param name="supwidth" type="float" min="0.1" max="1000.0" gui-text="Support Width" gui-description="Width of the supports holding the robot together">12.0</param>
<separator/>
<param name="thickness" type="float" min="0.1" max="1000.0" precision="3" gui-text="Material Thickness" gui-description="Thickness of the material">5.0</param>
<param name="kerf" type="float" min="0.0" max="1000.0" precision="3" gui-text="Laser Cutter Kerf" gui-description="Laser Cutter Kerf (tolerance). Varies based on cutter and material thickness">0.01</param>
<param name="linethickness" gui-text="Line Thickness" type="optiongroup" appearance="combo">
<option value="1px">1 pixel</option>
<option value="0.002in">hairline</option>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Boxes/Papercraft">
<submenu name="Finger-jointed/Tabbed Boxes" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">cutcraftrollerbot.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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)

View File

@ -0,0 +1,25 @@
[
{
"name": "<various>",
"id": "fablabchemnitz.de.cutcraft.<various>",
"path": "cutcraft",
"dependent_extensions": null,
"original_name": "<various>",
"original_id": "cutcraft.<various>",
"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"
]
}
]

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension
xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Delaunay Triangulation</name>
<id>fablabchemnitz.de.delaunay_triangulation</id>
<param name="tab" type="notebook">
<page name="Options" gui-text="Options">
<param name="joggling" type="bool" gui-text="Support concavity and holes">false</param>
<param name="furthest" type="bool" gui-text="Use furthest-site triangulation">false</param>
<param name="elt_type" type="optiongroup" appearance="combo" gui-text="Object type to generate">
<option value="poly">Triangles</option>
<option value="line">Individual lines</option>
</param>
<spacer/>
<separator/>
<spacer/>
<hbox>
<param name="fill_type" type="optiongroup" appearance="combo" gui-text="Fill color source">
<option value="first_sel">Same as first object selected</option>
<option value="last_sel">Same as last object selected</option>
<option value="random">Random</option>
<option value="specified">Explicitly specified</option>
</param>
<param name="fill_color" type="color" appearance="colorbutton" gui-text=" " gui-description="Specific fill color">-1</param>
</hbox>
<hbox>
<param name="stroke_type" type="optiongroup" appearance="combo" gui-text="Stroke color source">
<option value="first_sel">Same as first object selected</option>
<option value="last_sel">Same as last object selected</option>
<option value="random">Random</option>
<option value="specified">Explicitly specified</option>
</param>
<param name="stroke_color" type="color" appearance="colorbutton" gui-text=" " gui-description="Specific stroke color">255</param>
</hbox>
</page>
<page name="Advanced" gui-text="Advanced">
<param name="qhull" type="string" gui-text="qhull options">Qbb Qc Qz Q12</param>
<label>If "Support concavity" is enabled on the Options tab, "QJ" will be prepended to the qhull options listed above. The default options are "Qbb Qc Qz Q12". The following website describes the available options.</label>
<label appearance="url">http://www.qhull.org/html/qhull.htm#options</label>
</page>
<page name="Help" gui-text="Help">
<label>This effect uses the Delaunay triangulation algorithm to create triangles from all of the points found in the selected objects.</label>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">delaunay_triangulation.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Draw Directions / Travel Moves</name>
<id>fablabchemnitz.de.draw_directions_tavel_moves</id>
<param name="nb_main" type="notebook">
<page name="tab_draw" gui-text="Draw">
<param name="order" type="optiongroup" appearance="combo" gui-text="Order">
<option value="separate_groups">To separate groups</option>
<option value="separate_layers">To separate layers</option>
<option value="element_index">At element's index</option>
</param>
<separator/>
<param name="draw_dots" type="bool" gui-text="Draw dots" gui-description="Start and end point of opened (red + blue) and closed paths (green + yellow)">true</param>
<param name="dotsize" type="int" min="1" max="9999" gui-text="Dot size (px)">10</param>
<separator/>
<param name="draw_travel_moves" type="bool" gui-text="Draw travel moves">false</param>
<param name="ignore_colors" type="bool" gui-text="Ignore stroke colors" gui-description="If enabled we connect all lines by order, ignoring the stroke color (normally we have one travel line group per color)">false</param>
<param name="dashed_style" type="bool" gui-text="Dashed style">true</param>
<param name="arrow_style" type="bool" gui-text="Arrow style">true</param>
<param name="arrow_size" type="float" min="0.1" max="10.0" precision="2" gui-text="Arrow size (scale)">1.0</param>
<separator/>
<param name="debug" type="bool" gui-text="Print debug info">false</param>
</page>
<page name="tab_remove" gui-text="Remove">
<label>If this page is selected, you can remove all old travel lines/groups Just press 'apply'.</label>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Draw Directions / Travel Moves</label>
<label>Draw travel lines and/or dots to mark start and end points of paths and to show the line order.</label>
<label>2021 - 2022 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/drawdirectionstravelmoves</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">draw_directions_tavel_moves.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Exponential Distort</name>
<id>fablabchemnitz.de.exponential_distort</id>
<param name="exponent" type="float" min="0.01" max="100" precision="2" gui-text="Exponent:">1.33</param>
<param name="padding_perc" type="float" min="0" max="500" precision="0" gui-text="Padding [%]:">0</param>
<label>Apply padding to soften the effect. Without padding the exponential curve runs from 0 to 1. With e.g. 100% padding it runs from 0.5 to 1.</label>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Transformations" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">exponential_distort.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Fill Rectangle With Circles</name>
<id>fablabchemnitz.fill_rectangle_with_circles</id>
<param name="radius" type="float" min="0.01" max="10000" gui-text="Radius to enter">3.0</param>
<param name="margin" type="float" min="0.01" max="10000" gui-text="Margin between the edge of the rectangles and the circles">10.0</param>
<param name="space" type="float" min="0.01" max="10000" gui-text="Spacing between circles">30.0</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Object(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">fill_rectangle_with_circles.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Fillet And Chamfer (Replaced by LPE)</name>
<id>fablabchemnitz.de.fillet_and_chamfer</id>
<param name="fillet_type" type="optiongroup" appearance="combo" gui-text="Fillet or Chamfer:">
<option value="fillet">Fillet</option>
<option value="chamfer">Chamfer</option>
</param>
<param name="radius" type="float" min="0.0" max="1000.0" gui-text="Radius:">5.0</param>
<param name="unit" gui-text="Unit" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
</param>
<param name="remove" type="bool" gui-text="Remove control object">false</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">fillet_and_chamfer.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Group To Layer</name>
<id>fablabchemnitz.de.group_to_layer</id>
<param name="depth" type="int" gui-text="Max nested group depth">1</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Groups and Layers"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">group_to_layer.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -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"
]
}
]

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Perspective Grid</name>
<id>fablabchemnitz.de.perspective_grid</id>
<param name="size_unit" type="optiongroup" appearance="combo" gui-text="Geometry units">
<option value="px">px</option>
<option value="pt">pt</option>
<option value="cm">cm</option>
<option value="mm">mm</option>
<option value="in">in</option>
</param>
<param name="width" type="int" min="1" max="1000" gui-text="Width of window">500</param>
<param name="height" type="int" min="1" max="1000" gui-text="Height of window">300</param>
<param name="horizon" type="float" min="-1000" max="1000" gui-text="Horizon y coordinate">150</param>
<param name="left_x" type="float" min="-1000" max="1000" gui-text="Left perspective point">-100.0</param>
<param name="right_x" type="float" min="-1000" max="1000" gui-text="Right perspective point">600</param>
<param name="p_divs" type="int" min="1" max="1000" gui-text="Perspective angle divisions">10</param>
<param name="border_th" type="float" min="0" max="1000" gui-text="Border Thickness [px]">3</param>
<param name="div_th" type="float" min="0" max="1000" gui-text="Major grid division Thickness [px]">2</param>
<param name="div_color" type="color" appearance="colorbutton" gui-text="Grid color">255</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Grids/Guides"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">perspective_grid.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Rounder</name>
<id>fablabchemnitz.de.rounder</id>
<param name="precision" type="int" min="0" max="20" gui-text="Rounding precision">2</param>
<param name="paths" type="bool" gui-text="Round nodes">true</param>
<param name="ctrl" type="bool" gui-text="Round handles">false</param>
<param name="along" type="bool" gui-text="Move handles following node movement">true</param>
<param name="half" type="bool" gui-text="Allow round to half if nearest">false</param>
<param name="strokewidth" type="bool" gui-text="Round stroke width">false</param>
<param name="widthheight" type="bool" gui-text="Round width and height">false</param>
<param name="position" type="bool" gui-text="Round position X and Y">false</param>
<param name="opacity" type="bool" gui-text="Round global opacity">false</param>
<param name="strokeopacity" type="bool" gui-text="Round stroke opacity">false</param>
<param name="fillopacity" type="bool" gui-text="Round fill opacity">false</param>
<spacer/>
<label>Please note: Rounder only applies to svg:path elements. You cannot use it for rectangle, circle, ellipsis, arc, polygon, line, polyline, etc.</label>
<label appearance="url">https://y.stadtfabrikanten.org/rounder</label>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">rounder.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Set View Box</name>
<id>fablabchemnitz.de.set_view_box</id>
<effect>
<object-type>rect</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Transformations"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">set_view_box.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -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'})

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Shirt Waist (Sara May Allington)</name>
<id>fablabchemnitz.de.shirt_waist</id>
<param name="m_unit" type="optiongroup" appearance="combo" gui-text="Select measurement: ">
<option value="Inches">Inches</option>
<option value="Centimeters">Centimeters</option>
</param>
<param name="m_front_waist_length" type="float" min="1.0" max="100.0" gui-text="Front Waist Length">15.0</param>
<param name="m_back_waist_length" type="float" min="1.0" max="100.0" gui-text="Back Waist Length">15.5</param>
<param name="m_neck_circumference" type="float" min="1.0" max="100.0" gui-text="Neck Circumference">13.5</param>
<param name="m_bust_circumference" type="float" min="1.0" max="100.0" gui-text="Bust Circumference">39.0</param>
<param name="m_waist_circumference" type="float" min="1.0" max="100.0" gui-text="Waist Circumference">25.0</param>
<param name="m_armscye_circumference" type="float" min="1.0" max="100.0" gui-text="Armscye Circumference">15.0</param>
<param name="m_across_back" type="float" min="1.0" max="100.0" gui-text="Across Back">13.5</param>
<param name="m_side" type="float" min="1.0" max="100.0" gui-text="Side">7.75</param>
<param name="m_upper_front_height" type="float" min="1.0" max="100.0" gui-text="Upper Front Height">10.75</param>
<param name="m_overarm_length" type="float" min="1.0" max="100.0" gui-text="Overarm Length">20.00</param>
<param name="m_elbow_height" type="float" min="1.0" max="100.0" gui-text="Elbow height - from wrist to elbow">9.50</param>
<param name="m_elbow_circumference" type="float" min="1.0" max="100.0" gui-text="Elbow Circumference">12.50</param>
<param name="m_hand_circumference" type="float" min="1.0" max="100.0" gui-text="Hand Circumference">8.00</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Dimensioning/Measuring"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">shirt_waist.py</command>
</script>
</inkscape-extension>

View File

@ -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 <www.taumeta.org>
'''
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)
<http://creativecommons.org/licenses/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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Show Path Coordinates</name>
<id>fablabchemnitz.de.show_path_coordinates</id>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Dimensioning/Measuring"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">show_path_coordinates.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
#
# curve xy co-ordinate export
# Authors:
# Jean Moreno <jean.moreno.fr@gmail.com>
# John Cliff <john.cliff@gmail.com>
# Neon22 <https://github.com/Neon22?tab=repositories>
# Jens N. Lallensack <jens.lallensack@gmail.com>
# Mario Voigt <mario.voigt@stadtfabrikanten.org>
#
# 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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Simple Frame</name>
<id>fablabchemnitz.de.simple_frame</id>
<param name="unit" gui-text="Unit:" type="optiongroup" appearance="combo">
<option value="mm">mm</option>
</param>
<param name="width" type="float" min="0.1" max="300.0" gui-text="Inner width:">100.0</param>
<param name="height" type="float" min="0.1" max="300.0" gui-text="Inner height:">150.0</param>
<param name="depth" type="float" min="0.1" max="300.0" gui-text="Depth">10.0</param>
<param name="border" type="float" min="0.1" max="300.0" gui-text="Frame border width">20.0</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Boxes/Papercraft">
<submenu name="Paper/Cardboard Boxes" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">simple_frame.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -7,7 +7,7 @@
<option value="Hypotrochoid">Hypotrochoid</option>
</param>
<label appearance="header">Curve parameters</label>
<param name="radius_R" type="int" min="0" max="1000" gui-text="Fixed circle radius (R):">10</param>
<param name="radius_R" type="int" min="1" max="1000" gui-text="Fixed circle radius (R):">10</param>
<param name="radius_r" type="int" min="-1000" max="1000" gui-text="Rolling circle radius (r):">5</param>
<param name="pencil_distance" type="int" min="-1000" max="1000" gui-text="Pencil distance¹ (d):">2</param>
<label>¹ use d=r for Epi/Hypocycloid.</label>

View File

@ -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"
]
}
]

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Triangle</name>
<id>fablabchemnitz.de.triangle</id>
<param name="unit" type="optiongroup" appearance="combo" gui-text="Units:">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="px">px</option>
<option value="pt">pt</option>
<option value="pc">pc</option>
<option value="in">in</option>
</param>
<param name="s_a" type="float" min="0.01" max="10000" gui-text="Side Length a:">100.0</param>
<param name="s_b" type="float" min="0.01" max="10000" gui-text="Side Length b:">100.0</param>
<param name="s_c" type="float" min="0.01" max="10000" gui-text="Side Length c:">100.0</param>
<param name="a_a" type="float" min="0" max="180" gui-text="Angle a (deg):">60</param>
<param name="a_b" type="float" min="0" max="180" gui-text="Angle b (deg):">30</param>
<param name="a_c" type="float" min="0" max="180" gui-text="Angle c (deg):">90</param>
<param name="mode" type="optiongroup" appearance="combo" gui-text="Mode:">
<option value="3_sides">From Three Sides</option>
<option value="s_ab_a_c">From Sides a, b and Angle c</option>
<option value="s_ab_a_a">From Sides a, b and Angle a</option>
<option value="s_a_a_ab">From Side a and Angles a, b</option>
<option value="s_c_a_ab">From Side c and Angles a, b</option>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from Generator" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">triangle.py</command>
</script>
</inkscape-extension>

View File

@ -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()

View File

@ -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"
]
}
]

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Vertical / Horizontal Scale</name>
<id>fablabchemnitz.de.vertical_horizontal_scale</id>
<param name="tab" type="notebook">
<page name="global" gui-text="Shape">
<label appearance="header">Global</label>
<param name="type" type="optiongroup" appearance="combo" gui-text="Type:">
<option value="straight">Straight</option>
<option value="circular">Circular</option>
</param>
<param name="unit" type="optiongroup" appearance="combo" gui-text="Unit:">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">inch</option>
<option value="px">pixel</option>
<option value="pt">point</option>
</param>
<param name="useref" type="bool" gui-text="Origin from bounding box center">false</param>
<param name="insidetf" type="bool" gui-text="Swap inside out">false</param>
<label appearance="header">Straight</label>
<param name="rotate" type="optiongroup" appearance="combo" gui-text="Orientation:">
<option value="0">Vertical</option>
<option value="90">Horizontal</option>
</param>
<label appearance="header">Circular</label>
<param name="radius" type="float" gui-text="Radius (units):" min="0.000000001" max="32000">50.0</param>
<label>0 deg is at top. Positive values are clockwise.</label>
<param name="scaleradbegin" type="float" gui-text="Start (deg):" min="-360.0" max="360.0">0</param>
<param name="scaleradcount" type="float" gui-text="Count (deg):" min="-360.0" max="360.0">90</param>
<param name="radmark" type="bool" gui-text="Mark origin">true</param>
</page>
<page name="labelopt" gui-text="Labels">
<param name="drawalllabels" type="bool" gui-text="Draw all labels">true</param>
<label appearance="header">Numbers</label>
<param name="scalefrom" type="int" gui-text="Number from:" min="-32000" max="32000">0</param>
<param name="scaleto" type="int" gui-text="Number to:" min="-32000" max="32000">40</param>
<param name="mathexpression" type="string" gui-text="Math expression (number = 'n'):"/>
<param name="reverse" type="bool" gui-text="Reverse order">false</param>
<label appearance="header">Format</label>
<param name="fontsize" type="float" gui-text="Fontsize (units):" min="1" max="32000">3</param>
<param name="suffix" type="string" gui-text="Label suffix:"/>
<param name="ishorizontal" type="bool" gui-text="Horizontal labels (circular only)">false</param>
<param name="fliplabel" type="bool" gui-text="Flip orientation">false</param>
<label appearance="header">Offset (relative to label orientation):</label>
<param name="labeloffseth" type="float" gui-text="Horizontal (units):" min="-32000" max="32000">0</param>
<param name="labeloffsetv" type="float" gui-text="Vertical (units):" min="-32000" max="32000">0</param>
</page>
<page name="lineopt" gui-text="Lines">
<label appearance="header">All lines</label>
<param name="units_per_line" type="float" gui-text="Units per line (straight only):" min="0.00001" max="99999.9000">1.00000</param>
<label appearance="header">Perpendicular line</label>
<param name="perpline" type="bool" gui-text="Draw perpendicular line">false</param>
<param name="perplinestrokewidth" type="float" gui-text="Stroke width (units):" min="0.001" max="32000">0.2</param>
<param name="perplineoffset" type="float" gui-text="Offset (units):" min="-32000" max="32000">0</param>
<label appearance="header">Label line</label>
<param name="labellinelength" type="float" gui-text="Length (units):" min="1" max="32000">5.0</param>
<param name="labellinestrokewidth" type="float" gui-text="Stroke width (units):" min="0.001" max="32000">0.4</param>
<param name="mark0" type="int" gui-text="Draw every x lines (label number based on this):" min="0" max="32000">10</param>
<label appearance="header">Long line</label>
<param name="mark1wid" type="int" gui-text="Length (percentage of label line length):" min="0" max="200">85</param>
<param name="longlinestrokewidth" type="float" gui-text="Stroke width (units):" min="0.001" max="32000">0.2</param>
<param name="mark1" type="int" gui-text="Draw every x lines:" min="0" max="32000">5</param>
<label appearance="header">Short line</label>
<param name="mark2wid" type="int" gui-text="Length (percentage of label line length):" min="0" max="200">60</param>
<param name="shortlinestrokewidth" type="float" gui-text="Stroke width (units):" min="0.001" max="32000">0.2</param>
<param name="mark2" type="int" gui-text="Draw every x lines:" min="0" max="32000">1</param>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Shape Generators">
<submenu name="Scales" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">vertical_horizontal_scale.py</command>
</script>
</inkscape-extension>

View File

@ -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()