added back some more extensions
This commit is contained in:
parent
bc2301079d
commit
3277fba958
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Box Maker - Elliptical Box</name>
|
||||||
|
<id>fablabchemnitz.de.box_maker_elliptical_box</id>
|
||||||
|
<param name="unit" type="optiongroup" appearance="combo" gui-text="Unit">
|
||||||
|
<option value="mm">mm</option>
|
||||||
|
<option value="cm">cm</option>
|
||||||
|
<option value="in">in</option>
|
||||||
|
<option value="pt">pt</option>
|
||||||
|
<option value="px">px</option>
|
||||||
|
<option value="pc">pc</option>
|
||||||
|
</param>
|
||||||
|
<param name="thickness" type="float" min="1.0" max="100.0" gui-text="Material thickness">3.0</param>
|
||||||
|
<param name="height" type="float" min="0.0" max="10000.0" gui-text="Height">30.0</param>
|
||||||
|
<param name="width" type="float" min="0.0" max="10000.0" gui-text="Width">50.0</param>
|
||||||
|
<param name="depth" type="float" min="0.0" max="10000.0" gui-text="Depth">40.0</param>
|
||||||
|
<param name="cut_dist" type="float" min="0.1" max="100.0" gui-text="Cut distance">1.5</param>
|
||||||
|
<param name="auto_cut_dist" type="bool" gui-text="Automatically vary cut distance according to curvature">false</param>
|
||||||
|
<param name="cut_nr" type="int" min="1" max="100" gui-text="Number of cuts">4</param>
|
||||||
|
<param name="lid_angle" type="float" min="0.0" max="360.0" gui-text="Lid angle">90</param>
|
||||||
|
<param name="invert_lid_notches" type="bool" gui-text="Invert lid notch pattern (this will create a lid without sideways support)">false</param>
|
||||||
|
<param name="central_rib_lid" type="bool" gui-text="Create central rib in the lid(requires an even number of cuts)">false</param>
|
||||||
|
<param name="central_rib_body" type="bool" gui-text="Create central rib in the body(requires an even number of cuts)">false</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_elliptical_box.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,278 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from inkscape_helper.Coordinate import Coordinate
|
||||||
|
import inkscape_helper.Effect as eff
|
||||||
|
import inkscape_helper.SVG as svg
|
||||||
|
from inkscape_helper.Ellipse import Ellipse
|
||||||
|
|
||||||
|
from inkscape_helper.Line import Line
|
||||||
|
from inkscape_helper.EllipticArc import EllipticArc
|
||||||
|
|
||||||
|
from math import *
|
||||||
|
import inkex
|
||||||
|
|
||||||
|
#Note: keep in mind that SVG coordinates start in the top-left corner i.e. with an inverted y-axis
|
||||||
|
|
||||||
|
# first define some SVG primitives
|
||||||
|
greenStyle = svg.green_style
|
||||||
|
|
||||||
|
def _makeCurvedSurface(topLeft, w, h, cutSpacing, hCutCount, thickness, parent, invertNotches = False, centralRib = False):
|
||||||
|
width = Coordinate(w, 0)
|
||||||
|
height = Coordinate(0, h)
|
||||||
|
wCutCount = int(floor(w / cutSpacing))
|
||||||
|
if wCutCount % 2 == 0:
|
||||||
|
wCutCount += 1 # make sure we have an odd number of cuts
|
||||||
|
xCutDist = w / wCutCount
|
||||||
|
xSpacing = Coordinate(xCutDist, 0)
|
||||||
|
ySpacing = Coordinate(0, cutSpacing)
|
||||||
|
cut = height / hCutCount - ySpacing
|
||||||
|
plateThickness = Coordinate(0, thickness)
|
||||||
|
notchEdges = []
|
||||||
|
topHCuts = []
|
||||||
|
bottomHCuts = []
|
||||||
|
|
||||||
|
p = svg.Path()
|
||||||
|
|
||||||
|
for cutIndex in range(wCutCount):
|
||||||
|
if (cutIndex % 2 == 1) != invertNotches: # make a notch here
|
||||||
|
inset = plateThickness
|
||||||
|
else:
|
||||||
|
inset = Coordinate(0, 0)
|
||||||
|
|
||||||
|
# A-column of cuts
|
||||||
|
aColStart = topLeft + xSpacing * cutIndex
|
||||||
|
notchEdges.append((aColStart - topLeft).x)
|
||||||
|
|
||||||
|
if cutIndex > 0: # no cuts at x == 0
|
||||||
|
p.move_to(aColStart, True)
|
||||||
|
p.line_to(cut / 2)
|
||||||
|
|
||||||
|
for j in range(hCutCount - 1):
|
||||||
|
pos = aColStart + cut / 2 + ySpacing + (cut + ySpacing) * j
|
||||||
|
p.move_to(pos, True)
|
||||||
|
p.line_to(cut)
|
||||||
|
|
||||||
|
p.move_to(aColStart + height - cut / 2, True)
|
||||||
|
p.line_to(cut / 2)
|
||||||
|
|
||||||
|
|
||||||
|
# B-column of cuts, offset by half the cut length; these cuts run in the opposite direction
|
||||||
|
bColStart = topLeft + xSpacing * cutIndex + xSpacing / 2
|
||||||
|
for j in reversed(range(hCutCount)):
|
||||||
|
end = bColStart + ySpacing / 2 + (cut + ySpacing) * j
|
||||||
|
start = end + cut
|
||||||
|
if centralRib and hCutCount % 2 == 0 and cutIndex % 2 == 1:
|
||||||
|
holeTopLeft = start + (ySpacing - plateThickness - xSpacing) / 2
|
||||||
|
if j == hCutCount // 2 - 1:
|
||||||
|
start -= plateThickness / 2
|
||||||
|
p.move_to(holeTopLeft + plateThickness + xSpacing, True)
|
||||||
|
p.line_to(-xSpacing)
|
||||||
|
|
||||||
|
p.move_to(holeTopLeft, True)
|
||||||
|
p.line_to(xSpacing)
|
||||||
|
|
||||||
|
elif j == hCutCount // 2:
|
||||||
|
end += plateThickness / 2
|
||||||
|
if j == 0: # first row
|
||||||
|
end += inset
|
||||||
|
elif j == hCutCount - 1: # last row
|
||||||
|
start -= inset
|
||||||
|
p.move_to(start, True)
|
||||||
|
p.line_to(end, True)
|
||||||
|
|
||||||
|
#horizontal cuts (should be done last)
|
||||||
|
topHCuts.append((aColStart + inset, aColStart + inset + xSpacing))
|
||||||
|
bottomHCuts.append((aColStart + height - inset, aColStart + height - inset + xSpacing))
|
||||||
|
|
||||||
|
# draw the outline
|
||||||
|
for c in reversed(bottomHCuts):
|
||||||
|
p.move_to(c[1], True)
|
||||||
|
p.line_to(c[0], True)
|
||||||
|
|
||||||
|
p.move_to(topLeft + height, True)
|
||||||
|
p.line_to(-height)
|
||||||
|
|
||||||
|
for c in topHCuts:
|
||||||
|
p.move_to(c[0], True)
|
||||||
|
p.line_to(c[1], True)
|
||||||
|
|
||||||
|
p.move_to(topLeft + width, True)
|
||||||
|
p.line_to(height)
|
||||||
|
|
||||||
|
|
||||||
|
group = svg.group(parent)
|
||||||
|
p.path(group)
|
||||||
|
|
||||||
|
notchEdges.append(w)
|
||||||
|
return notchEdges
|
||||||
|
|
||||||
|
def _makeNotchedEllipse(center, ellipse, start_theta, thickness, notches, parent, invertNotches):
|
||||||
|
start_theta += pi # rotate 180 degrees to put the lid on the topside
|
||||||
|
|
||||||
|
ell_radius = Coordinate(ellipse.x_radius, ellipse.y_radius)
|
||||||
|
ell_radius_t = ell_radius + Coordinate(thickness, thickness)
|
||||||
|
|
||||||
|
theta = ellipse.theta_from_dist(start_theta, notches[0])
|
||||||
|
ell_point = center + ellipse.coordinate_at_theta(theta)
|
||||||
|
prev_offset = ellipse.tangent(theta) * thickness
|
||||||
|
|
||||||
|
p = svg.Path()
|
||||||
|
p.move_to(ell_point, absolute=True)
|
||||||
|
|
||||||
|
for n in range(len(notches) - 1):
|
||||||
|
theta = ellipse.theta_from_dist(start_theta, notches[n + 1])
|
||||||
|
ell_point = center + ellipse.coordinate_at_theta(theta)
|
||||||
|
notch_offset = ellipse.tangent(theta) * thickness
|
||||||
|
notch_point = ell_point + notch_offset
|
||||||
|
|
||||||
|
if (n % 2 == 0) != invertNotches:
|
||||||
|
p.arc_to(ell_radius, ell_point, absolute=True)
|
||||||
|
prev_offset = notch_offset
|
||||||
|
|
||||||
|
else:
|
||||||
|
p.line_to(prev_offset)
|
||||||
|
p.arc_to(ell_radius_t, notch_point, absolute=True)
|
||||||
|
p.line_to(-notch_offset)
|
||||||
|
|
||||||
|
p.path(parent)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class EllipticalBox(eff.Effect):
|
||||||
|
"""
|
||||||
|
Creates a new layer with the drawings for a parametrically generaded box.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
options = [
|
||||||
|
['unit', str, 'mm', 'Unit, one of: cm, mm, in, ft, ...'],
|
||||||
|
['thickness', float, '3.0', 'Material thickness'],
|
||||||
|
['width', float, '100', 'Box width'],
|
||||||
|
['height', float, '100', 'Box height'],
|
||||||
|
['depth', float, '100', 'Box depth'],
|
||||||
|
['cut_dist', float, '1.5', 'Distance between cuts on the wrap around. Note that this value will change slightly to evenly fill up the available space.'],
|
||||||
|
['auto_cut_dist', inkex.Boolean, 'false', 'Automatically set the cut distance based on the curvature.'], # in progress
|
||||||
|
['cut_nr', int, '3', 'Number of cuts across the depth of the box.'],
|
||||||
|
['lid_angle', float, '120', 'Angle that forms the lid (in degrees, measured from centerpoint of the ellipse)'],
|
||||||
|
['body_ribcount', int, '0', 'Number of ribs in the body'],
|
||||||
|
['lid_ribcount', int, '0', 'Number of ribs in the lid'],
|
||||||
|
['invert_lid_notches', inkex.Boolean, 'false', 'Invert the notch pattern on the lid (keeps the lid from sliding sideways)'],
|
||||||
|
['central_rib_lid', inkex.Boolean, 'false', 'Create a central rib in the lid'],
|
||||||
|
['central_rib_body', inkex.Boolean, 'false', 'Create a central rib in the body']
|
||||||
|
]
|
||||||
|
eff.Effect.__init__(self, options)
|
||||||
|
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
"""
|
||||||
|
Draws as basic elliptical box, based on provided parameters
|
||||||
|
"""
|
||||||
|
|
||||||
|
# input sanity check
|
||||||
|
error = False
|
||||||
|
if min(self.options.height, self.options.width, self.options.depth) == 0:
|
||||||
|
eff.errormsg('Error: Dimensions must be non zero')
|
||||||
|
error = True
|
||||||
|
|
||||||
|
if self.options.cut_nr < 1:
|
||||||
|
eff.errormsg('Error: Number of cuts should be at least 1')
|
||||||
|
error = True
|
||||||
|
|
||||||
|
if (self.options.central_rib_lid or self.options.central_rib_body) and self.options.cut_nr % 2 == 1:
|
||||||
|
eff.errormsg('Error: Central rib is only valid with an even number of cuts')
|
||||||
|
error = True
|
||||||
|
|
||||||
|
if self.options.unit not in self.knownUnits:
|
||||||
|
eff.errormsg('Error: unknown unit. '+ self.options.unit)
|
||||||
|
error = True
|
||||||
|
|
||||||
|
if error:
|
||||||
|
exit()
|
||||||
|
|
||||||
|
|
||||||
|
# convert units
|
||||||
|
unit = self.options.unit
|
||||||
|
H = self.svg.unittouu(str(self.options.height) + unit)
|
||||||
|
W = self.svg.unittouu(str(self.options.width) + unit)
|
||||||
|
D = self.svg.unittouu(str(self.options.depth) + unit)
|
||||||
|
thickness = self.svg.unittouu(str(self.options.thickness) + unit)
|
||||||
|
cutSpacing = self.svg.unittouu(str(self.options.cut_dist) + unit)
|
||||||
|
cutNr = self.options.cut_nr
|
||||||
|
|
||||||
|
doc_root = self.document.getroot()
|
||||||
|
layer = svg.layer(doc_root, 'Elliptical Box')
|
||||||
|
|
||||||
|
ell = Ellipse(W/2, H/2)
|
||||||
|
|
||||||
|
#body and lid
|
||||||
|
lidAngleRad = self.options.lid_angle * 2 * pi / 360
|
||||||
|
lid_start_theta = ell.theta_at_angle(pi / 2 - lidAngleRad / 2)
|
||||||
|
lid_end_theta = ell.theta_at_angle(pi / 2 + lidAngleRad / 2)
|
||||||
|
|
||||||
|
lidLength = ell.dist_from_theta(lid_start_theta, lid_end_theta)
|
||||||
|
bodyLength = ell.dist_from_theta(lid_end_theta, lid_start_theta)
|
||||||
|
|
||||||
|
# do not put elements right at the edge of the page
|
||||||
|
xMargin = 3
|
||||||
|
yMargin = 3
|
||||||
|
|
||||||
|
bottom_grp = svg.group(layer)
|
||||||
|
top_grp = svg.group(layer)
|
||||||
|
|
||||||
|
bodyNotches = _makeCurvedSurface(Coordinate(xMargin, yMargin), bodyLength, D, cutSpacing, cutNr,
|
||||||
|
thickness, bottom_grp, False, self.options.central_rib_body)
|
||||||
|
lidNotches = _makeCurvedSurface(Coordinate(xMargin, D + 2 * yMargin), lidLength, D, cutSpacing, cutNr,
|
||||||
|
thickness, top_grp, not self.options.invert_lid_notches,
|
||||||
|
self.options.central_rib_lid)
|
||||||
|
|
||||||
|
# create elliptical sides
|
||||||
|
sidesGrp = svg.group(layer)
|
||||||
|
|
||||||
|
elCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + H / 2 + thickness + 3 * yMargin)
|
||||||
|
|
||||||
|
# indicate the division between body and lid
|
||||||
|
p = svg.Path()
|
||||||
|
if self.options.invert_lid_notches:
|
||||||
|
p.move_to(elCenter + ell.coordinate_at_theta(lid_start_theta + pi), True)
|
||||||
|
p.line_to(elCenter, True)
|
||||||
|
p.line_to(elCenter + ell.coordinate_at_theta(lid_end_theta + pi), True)
|
||||||
|
|
||||||
|
else:
|
||||||
|
angleA = ell.theta_from_dist(lid_start_theta, lidNotches[1])
|
||||||
|
angleB = ell.theta_from_dist(lid_start_theta, lidNotches[-2])
|
||||||
|
|
||||||
|
p.move_to(elCenter + ell.coordinate_at_theta(angleA + pi), True)
|
||||||
|
p.line_to(elCenter, True)
|
||||||
|
p.line_to(elCenter + ell.coordinate_at_theta(angleB + pi), True)
|
||||||
|
|
||||||
|
_makeNotchedEllipse(elCenter, ell, lid_end_theta, thickness, bodyNotches, sidesGrp, False)
|
||||||
|
_makeNotchedEllipse(elCenter, ell, lid_start_theta, thickness, lidNotches, sidesGrp,
|
||||||
|
not self.options.invert_lid_notches)
|
||||||
|
|
||||||
|
p.path(sidesGrp, greenStyle)
|
||||||
|
|
||||||
|
# ribs
|
||||||
|
if self.options.central_rib_lid or self.options.central_rib_body:
|
||||||
|
innerRibCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + 1.5 * (H + 2 *thickness) + 4 * yMargin)
|
||||||
|
innerRibGrp = svg.group(layer)
|
||||||
|
|
||||||
|
outerRibCenter = Coordinate(2 * xMargin + 1.5 * (W + 2 * thickness),
|
||||||
|
2 * D + 1.5 * (H + 2 * thickness) + 4 * yMargin)
|
||||||
|
outerRibGrp = svg.group(layer)
|
||||||
|
|
||||||
|
|
||||||
|
if self.options.central_rib_lid:
|
||||||
|
_makeNotchedEllipse(innerRibCenter, ell, lid_start_theta, thickness, lidNotches, innerRibGrp, False)
|
||||||
|
_makeNotchedEllipse(outerRibCenter, ell, lid_start_theta, thickness, lidNotches, outerRibGrp, True)
|
||||||
|
|
||||||
|
if self.options.central_rib_body:
|
||||||
|
spacer = Coordinate(0, 10)
|
||||||
|
_makeNotchedEllipse(innerRibCenter + spacer, ell, lid_end_theta, thickness, bodyNotches, innerRibGrp, False)
|
||||||
|
_makeNotchedEllipse(outerRibCenter + spacer, ell, lid_end_theta, thickness, bodyNotches, outerRibGrp, True)
|
||||||
|
|
||||||
|
if self.options.central_rib_lid or self.options.central_rib_body:
|
||||||
|
svg.text(sidesGrp, elCenter, 'side (duplicate this)')
|
||||||
|
svg.text(innerRibGrp, innerRibCenter, 'inside rib')
|
||||||
|
svg.text(outerRibGrp, outerRibCenter, 'outside rib')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
EllipticalBox().run()
|
@ -0,0 +1,92 @@
|
|||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
from PathSegment import *
|
||||||
|
from math import hypot
|
||||||
|
|
||||||
|
class BezierCurve(PathSegment):
|
||||||
|
nr_points = 10
|
||||||
|
def __init__(self, P): # number of points is limited to 3 or 4
|
||||||
|
|
||||||
|
if len(P) == 3: # quadratic
|
||||||
|
self.B = lambda t : (1 - t)**2 * P[0] + 2 * (1 - t) * t * P[1] + t**2 * P[2]
|
||||||
|
self.Bd = lambda t : 2 * (1 - t) * (P[1] - P[0]) + 2 * t * (P[2] - P[1])
|
||||||
|
self.Bdd = lambda t : 2 * (P[2] - 2 * P[1] + P[0])
|
||||||
|
elif len(P) == 4: #cubic
|
||||||
|
self.B = lambda t : (1 - t)**3 * P[0] + 3 * (1 - t)**2 * t * P[1] + 3 * (1 - t) * t**2 * P[2] + t**3 * P[3]
|
||||||
|
self.Bd = lambda t : 3 * (1 - t)**2 * (P[1] - P[0]) + 6 * (1 - t) * t * (P[2] - P[1]) + 3 * t**2 * (P[3] - P[2])
|
||||||
|
self.Bdd = lambda t : 6 * (1 - t) * (P[2] - 2 * P[1] + P[0]) + 6 * t * (P[3] - 2 * P[2] + P[1])
|
||||||
|
|
||||||
|
self.tangent = lambda t : self.Bd(t)
|
||||||
|
# self.curvature = lambda t : (Bd(t).x * Bdd(t).y - Bd(t).y * Bdd(t).x) / hypot(Bd(t).x, Bd(t).y)**3
|
||||||
|
|
||||||
|
|
||||||
|
self.distances = [0] # cumulative distances for each 't'
|
||||||
|
prev_pt = self.B(0)
|
||||||
|
for i in range(self.nr_points):
|
||||||
|
t = (i + 1) / self.nr_points
|
||||||
|
pt = self.B(t)
|
||||||
|
self.distances.append(self.distances[-1] + hypot(prev_pt.x - pt.x, prev_pt.y - pt.y))
|
||||||
|
prev_pt = pt
|
||||||
|
self._length = self.distances[-1]
|
||||||
|
|
||||||
|
def curvature(self, t):
|
||||||
|
n = self.Bd(t).x * self.Bdd(t).y - self.Bd(t).y * self.Bdd(t).x
|
||||||
|
d = hypot(self.Bd(t).x, self.Bd(t).y)**3
|
||||||
|
if d == 0:
|
||||||
|
return n * float('inf')
|
||||||
|
else:
|
||||||
|
return n / d
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def quadratic(cls, start, c, end):
|
||||||
|
bezier = cls()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def cubic(cls, start, c1, c2, end):
|
||||||
|
bezier = cls()
|
||||||
|
|
||||||
|
def __make_eq__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
return self._length
|
||||||
|
|
||||||
|
def subdivide(self, part_length, start_offset=0):
|
||||||
|
nr_parts = int((self.length - start_offset) // part_length)
|
||||||
|
k_o = start_offset / self.length
|
||||||
|
k2t = lambda k : k_o + k * part_length / self.length
|
||||||
|
points = [self.pathpoint_at_t(k2t(k)) for k in range(nr_parts + 1)]
|
||||||
|
return(points, self.length - points[-1].c_dist)
|
||||||
|
|
||||||
|
|
||||||
|
def pathpoint_at_t(self, t):
|
||||||
|
"""pathpoint on the curve from t=0 to point at t."""
|
||||||
|
step = 1 / self.nr_points
|
||||||
|
pt_idx = int(t / step)
|
||||||
|
length = self.distances[pt_idx]
|
||||||
|
ip_fact = (t - pt_idx * step) / step
|
||||||
|
|
||||||
|
if ip_fact > 0 and t < 1: # not a perfect match, need to interpolate
|
||||||
|
length += ip_fact * (self.distances[pt_idx + 1] - self.distances[pt_idx])
|
||||||
|
|
||||||
|
return PathPoint(t, self.B(t), self.tangent(t), self.curvature(t), length)
|
||||||
|
|
||||||
|
|
||||||
|
def t_at_length(self, length):
|
||||||
|
"""interpolated t where the curve is at the given length"""
|
||||||
|
if length == self.length:
|
||||||
|
return 1
|
||||||
|
i_small = 0
|
||||||
|
i_big = self.nr_points + 1
|
||||||
|
|
||||||
|
while i_big - i_small > 1: # binary search
|
||||||
|
i_half = i_small + (i_big - i_small) // 2
|
||||||
|
if self.distances[i_half] <= length:
|
||||||
|
i_small = i_half
|
||||||
|
else:
|
||||||
|
i_big = i_half
|
||||||
|
|
||||||
|
small_dist = self.distances[i_small]
|
||||||
|
return i_small / self.nr_points + (length - small_dist) * (self.distances[i_big] - small_dist) # interpolated length
|
||||||
|
|
@ -0,0 +1,104 @@
|
|||||||
|
from math import *
|
||||||
|
|
||||||
|
def inner_product(a, b):
|
||||||
|
return a.x * b.x + a.y * b.y
|
||||||
|
|
||||||
|
class Coordinate(object):
|
||||||
|
"""
|
||||||
|
Basic (x, y) coordinate class (or should it be called vector?) which allows some simple operations.
|
||||||
|
"""
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.x = float(x)
|
||||||
|
self.y = float(y)
|
||||||
|
|
||||||
|
#polar coordinates
|
||||||
|
@property
|
||||||
|
def t(self):
|
||||||
|
angle = atan2(self.y, self.x)
|
||||||
|
if angle < 0:
|
||||||
|
angle += pi * 2
|
||||||
|
return angle
|
||||||
|
|
||||||
|
@t.setter
|
||||||
|
def t(self, value):
|
||||||
|
length = self.r
|
||||||
|
self.x = cos(value) * length
|
||||||
|
self.y = sin(value) * length
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def r(self):
|
||||||
|
return hypot(self.x, self.y)
|
||||||
|
|
||||||
|
@r.setter
|
||||||
|
def r(self, value):
|
||||||
|
angle = self.t
|
||||||
|
self.x = cos(angle) * value
|
||||||
|
self.y = sin(angle) * value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "(%f, %f)" % (self.x, self.y)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.x == other.x and self.y == other.y
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
return Coordinate(self.x + other.x, self.y + other.y)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
return Coordinate(self.x - other.x, self.y - other.y)
|
||||||
|
|
||||||
|
def __mul__(self, factor):
|
||||||
|
return Coordinate(self.x * factor, self.y * factor)
|
||||||
|
|
||||||
|
def __neg__(self):
|
||||||
|
return Coordinate(-self.x, -self.y)
|
||||||
|
|
||||||
|
def __rmul__(self, other):
|
||||||
|
return self * other
|
||||||
|
|
||||||
|
def __div__(self, quotient):
|
||||||
|
return Coordinate(self.x / quotient, self.y / quotient)
|
||||||
|
|
||||||
|
def __truediv__(self, quotient):
|
||||||
|
return self.__div__(quotient)
|
||||||
|
|
||||||
|
def dot(self, other):
|
||||||
|
"""dot product"""
|
||||||
|
return self.x * other.x + self.y * other.y
|
||||||
|
|
||||||
|
def cross_norm(self, other):
|
||||||
|
""""the norm of the cross product"""
|
||||||
|
self.x * other.y - self.y * other.x
|
||||||
|
|
||||||
|
def close_enough_to(self, other, limit=1E-9):
|
||||||
|
return (self - other).r < limit
|
||||||
|
|
||||||
|
|
||||||
|
class IntersectionError(ValueError):
|
||||||
|
"""Raised when two lines do not intersect."""
|
||||||
|
|
||||||
|
def on_segment(pt, start, end):
|
||||||
|
"""Check if pt is between start and end. The three points are presumed to be collinear."""
|
||||||
|
pt -= start
|
||||||
|
end -= start
|
||||||
|
ex, ey = end.x, end.y
|
||||||
|
px, py = pt.x, pt.y
|
||||||
|
px *= cmp(ex, 0)
|
||||||
|
py *= cmp(ey, 0)
|
||||||
|
return px >= 0 and px <= abs(ex) and py >= 0 and py <= abs(ey)
|
||||||
|
|
||||||
|
def intersection (s1, e1, s2, e2, on_segments = True):
|
||||||
|
D = (s1.x - e1.x) * (s2.y - e2.y) - (s1.y - e1.y) * (s2.x - e2.x)
|
||||||
|
if D == 0:
|
||||||
|
raise IntersectionError("Lines from {s1} to {e1} and {s2} to {e2} are parallel")
|
||||||
|
N1 = s1.x * e1.y - s1.y * e1.x
|
||||||
|
N2 = s2.x * e2.y - s2.y * e2.x
|
||||||
|
I = ((s2 - e2) * N1 - (s1 - e1) * N2) / D
|
||||||
|
|
||||||
|
if on_segments and not (on_segment(I, s1, e1) and on_segment(I, s2, e2)):
|
||||||
|
raise IntersectionError("Intersection {0} is not on line segments [{1} -> {2}] [{3} -> {4}]".format(I, s1, e1, s2, e2))
|
||||||
|
return I
|
@ -0,0 +1,36 @@
|
|||||||
|
import inkex
|
||||||
|
|
||||||
|
errormsg = inkex.errormsg
|
||||||
|
debug = inkex.debug
|
||||||
|
|
||||||
|
class Effect(inkex.Effect):
|
||||||
|
"""
|
||||||
|
Provides some extra features to inkex.Effect:
|
||||||
|
- Allows you to pass a list of options in stead of setting them one by one
|
||||||
|
- acces to unittouu() that is compatible between Inkscape versions 0.48 and 0.91
|
||||||
|
"""
|
||||||
|
def __init__(self, options=None):
|
||||||
|
inkex.Effect.__init__(self)
|
||||||
|
self.knownUnits = ['in', 'pt', 'px', 'mm', 'cm', 'm', 'km', 'pc', 'yd', 'ft']
|
||||||
|
|
||||||
|
if options != None:
|
||||||
|
for opt in options:
|
||||||
|
if len(opt) == 2:
|
||||||
|
self.arg_parser.add_argument('--' + opt[0], type = opt[1])
|
||||||
|
else:
|
||||||
|
self.arg_parser.add_argument('--' + opt[0], type = opt[1],default = opt[2], help = opt[3])
|
||||||
|
|
||||||
|
try:
|
||||||
|
inkex.Effect.unittouu # unitouu has moved since Inkscape 0.91
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
def unittouu(self, unit):
|
||||||
|
return inkex.unittouu(unit)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
@ -0,0 +1,157 @@
|
|||||||
|
from __future__ import division
|
||||||
|
from math import *
|
||||||
|
from inkscape_helper.Coordinate import Coordinate
|
||||||
|
|
||||||
|
class Ellipse(object):
|
||||||
|
"""Used as a base class for EllipticArc."""
|
||||||
|
nr_points = 1024 #used for piecewise linear circumference calculation (ellipse circumference is tricky to calculate)
|
||||||
|
# approximate circumfere: c = pi * (3 * (a + b) - sqrt(10 * a * b + 3 * (a ** 2 + b ** 2)))
|
||||||
|
|
||||||
|
def __init__(self, x_radius, y_radius):
|
||||||
|
self.y_radius = y_radius
|
||||||
|
self.x_radius = x_radius
|
||||||
|
self.distances = [0]
|
||||||
|
theta = 0
|
||||||
|
self.angle_step = 2 * pi / self.nr_points
|
||||||
|
|
||||||
|
for i in range(self.nr_points):
|
||||||
|
prev_dist = self.distances[-1]
|
||||||
|
prev_coord = self.coordinate_at_theta(theta)
|
||||||
|
theta += self.angle_step
|
||||||
|
x, y = x_radius * cos(theta), y_radius * sin(theta)
|
||||||
|
self.distances.append(prev_dist + hypot(prev_coord.x - x, prev_coord.y - y))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def circumference(self):
|
||||||
|
return self.distances[-1]
|
||||||
|
|
||||||
|
def curvature(self, theta):
|
||||||
|
c = self.coordinate_at_theta(theta)
|
||||||
|
return (self.x_radius*self.y_radius)/((cos(theta)**2*self.y_radius**2 + sin(theta)**2*self.x_radius**2)**(3/2))
|
||||||
|
|
||||||
|
def tangent(self, theta):
|
||||||
|
angle = self.theta_at_angle(theta)
|
||||||
|
return Coordinate(cos(angle), sin(angle))
|
||||||
|
|
||||||
|
def coordinate_at_theta(self, theta):
|
||||||
|
"""Coordinate of the point at theta."""
|
||||||
|
return Coordinate(self.x_radius * cos(theta), self.y_radius * sin(theta))
|
||||||
|
|
||||||
|
def dist_from_theta(self, theta_start, theta_end):
|
||||||
|
"""Distance accross the surface from point at angle theta_end to point at angle theta_end. Measured in positive (CCW) sense."""
|
||||||
|
#print 'thetas ', theta_start, theta_end # TODO: figure out why are there so many with same start and end?
|
||||||
|
# make sure thetas are between 0 and 2 * pi
|
||||||
|
theta_start %= 2 * pi
|
||||||
|
theta_end %= 2 * pi
|
||||||
|
i1 = int(theta_start / self.angle_step)
|
||||||
|
p1 = theta_start % self.angle_step
|
||||||
|
l1 = self.distances[i1 + 1] - self.distances[i1]
|
||||||
|
i2 = int(theta_end / self.angle_step)
|
||||||
|
p2 = theta_end % self.angle_step
|
||||||
|
l2 = self.distances[i2 + 1] - self.distances[i2]
|
||||||
|
if theta_start <= theta_end:
|
||||||
|
len = self.distances[i2] - self.distances[i1] + l2 * p2 - l1 * p1
|
||||||
|
else:
|
||||||
|
len = self.circumference + self.distances[i2] - self.distances[i1]
|
||||||
|
return len
|
||||||
|
|
||||||
|
def theta_from_dist(self, theta_start, dist):
|
||||||
|
"""Returns the angle that you get when starting at theta_start and moving a distance (dist) in CCW direction"""
|
||||||
|
si = int(theta_start / self.angle_step) % self.nr_points
|
||||||
|
p = theta_start % self.angle_step
|
||||||
|
|
||||||
|
piece_length = self.distances[si + 1] - self.distances[si]
|
||||||
|
start_dist = self.distances[si] + p * piece_length
|
||||||
|
end_dist = dist + start_dist
|
||||||
|
|
||||||
|
if end_dist > self.circumference: # wrap around zero angle
|
||||||
|
end_dist -= self.circumference
|
||||||
|
|
||||||
|
min_idx = 0
|
||||||
|
max_idx = self.nr_points
|
||||||
|
while max_idx - min_idx > 1: # binary search
|
||||||
|
half_idx = min_idx + (max_idx - min_idx) // 2
|
||||||
|
if self.distances[half_idx] < end_dist:
|
||||||
|
min_idx = half_idx
|
||||||
|
else:
|
||||||
|
max_idx = half_idx
|
||||||
|
step_dist = self.distances[max_idx] - self.distances[min_idx]
|
||||||
|
return (min_idx + (end_dist - self.distances[min_idx]) / step_dist) * self.angle_step
|
||||||
|
|
||||||
|
def theta_at_angle(self, angle):
|
||||||
|
cf = 0
|
||||||
|
if angle > pi / 2:
|
||||||
|
cf = pi
|
||||||
|
if angle > 3 * pi / 2:
|
||||||
|
cf = 2 * pi
|
||||||
|
return atan(self.x_radius/self.y_radius * tan(angle)) + cf
|
||||||
|
|
||||||
|
def skewTransform(self, l, a2, b2):
|
||||||
|
x0 = a2**2
|
||||||
|
x1 = b2**2
|
||||||
|
x2 = l**2
|
||||||
|
x3 = x0*x2
|
||||||
|
x4 = x0 - x1 + x3
|
||||||
|
x5 = 2*a2*b2
|
||||||
|
x6 = x0 + x1 + x3
|
||||||
|
x7 = sqrt((-x5 + x6)*(x5 + x6))
|
||||||
|
x9 = 1/(x4 - x7)
|
||||||
|
x10 = x6 - x7
|
||||||
|
x11 = l*x10
|
||||||
|
x12 = b2**4
|
||||||
|
x13 = 4*x12
|
||||||
|
x14 = x10**2
|
||||||
|
x15 = 4*x1
|
||||||
|
x16 = sqrt(-x10*x15 + x13 + x14*x2 + x14)
|
||||||
|
x17 = 2*atan(x9*(x11 - x16))
|
||||||
|
x18 = sqrt(2)
|
||||||
|
x19 = sqrt(x10)
|
||||||
|
x20 = b2*x18*x19/2
|
||||||
|
x21 = x0/2
|
||||||
|
x22 = x1/2
|
||||||
|
x23 = x2*x21
|
||||||
|
x24 = x21 - x22 + x23
|
||||||
|
x25 = x7/2
|
||||||
|
x27 = 1/(x24 - x25)
|
||||||
|
x28 = x21 + x22 + x23
|
||||||
|
x29 = x28 - x25
|
||||||
|
x30 = l*x29
|
||||||
|
x31 = x14/4
|
||||||
|
x32 = 2*x1
|
||||||
|
x33 = sqrt(x12 + x2*x31 - x29*x32 + x31)
|
||||||
|
x34 = 2*atan(x27*(x30 - x33))
|
||||||
|
x35 = x20*sqrt(1/(-x1*cos(x34)**2 + x29))*sin(x34)
|
||||||
|
x36 = x18/2
|
||||||
|
x37 = -x19*x36
|
||||||
|
x39 = 2*atan(x9*(x11 + x16))
|
||||||
|
x40 = 2*atan(x27*(x30 + x33))
|
||||||
|
x41 = x20*sqrt(1/(-x1*cos(x40)**2 + x29))*sin(x40)
|
||||||
|
x42 = 1/(x4 + x7)
|
||||||
|
x43 = x6 + x7
|
||||||
|
x44 = l*x43
|
||||||
|
x45 = x43**2
|
||||||
|
x46 = sqrt(x13 - x15*x43 + x2*x45 + x45)
|
||||||
|
x47 = 2*atan(x42*(x44 - x46))
|
||||||
|
x48 = sqrt(x43)
|
||||||
|
x49 = b2*x18*x48/2
|
||||||
|
x50 = 1/(x24 + x25)
|
||||||
|
x51 = x25 + x28
|
||||||
|
x52 = l*x51
|
||||||
|
x53 = x45/4
|
||||||
|
x54 = sqrt(x12 + x2*x53 - x32*x51 + x53)
|
||||||
|
x55 = 2*atan(x50*(x52 - x54))
|
||||||
|
x56 = x49*sqrt(1/(-x1*cos(x55)**2 + x51))*sin(x55)
|
||||||
|
x57 = -x36*x48
|
||||||
|
x59 = 2*atan(x42*(x44 + x46))
|
||||||
|
x60 = 2*atan(x50*(x52 + x54))
|
||||||
|
x61 = x49*sqrt(1/(-x1*cos(x60)**2 + x51))*sin(x60)
|
||||||
|
|
||||||
|
#solutions (alpha, a1, b1)
|
||||||
|
(x17, -x35, x19*x36)
|
||||||
|
(x17, x35, x37)
|
||||||
|
(x39, -x41, x19*x36)
|
||||||
|
(x39, x41, x37)
|
||||||
|
(x47, -x56, x36*x48)
|
||||||
|
(x47, x56, x57)
|
||||||
|
(x59, -x61, x36*x48)
|
||||||
|
(x59, x61, x57)
|
@ -0,0 +1,125 @@
|
|||||||
|
from inkscape_helper.PathSegment import *
|
||||||
|
from inkscape_helper.Coordinate import Coordinate
|
||||||
|
from inkscape_helper.Ellipse import Ellipse
|
||||||
|
|
||||||
|
from math import sqrt, pi
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
class EllipticArc(PathSegment):
|
||||||
|
|
||||||
|
ell_dict = {}
|
||||||
|
|
||||||
|
def __init__(self, start, end, rx, ry, axis_rot, pos_dir=True, large_arc=False):
|
||||||
|
self.rx = rx
|
||||||
|
self.ry = ry
|
||||||
|
# calculate ellipse center
|
||||||
|
# the center is on two ellipses one with its center at the start point, the other at the end point
|
||||||
|
# for simplicity take the one ellipse at the origin and the other with offset (tx, ty),
|
||||||
|
# find the center and translate back to the original offset at the end
|
||||||
|
axis_rot *= pi / 180 # convert to radians
|
||||||
|
# start and end are mutable objects, copy to avoid modifying them
|
||||||
|
r_start = copy.copy(start)
|
||||||
|
r_end = copy.copy(end)
|
||||||
|
r_start.t -= axis_rot
|
||||||
|
r_end.t -= axis_rot
|
||||||
|
end_o = r_end - r_start # offset end vector
|
||||||
|
|
||||||
|
tx = end_o.x
|
||||||
|
ty = end_o.y
|
||||||
|
|
||||||
|
# some helper variables for the intersection points
|
||||||
|
# used sympy to come up with the equations
|
||||||
|
ff = (rx**2*ty**2 + ry**2*tx**2)
|
||||||
|
cx = rx**2*ry*tx*ty**2 + ry**3*tx**3
|
||||||
|
cy = rx*ty*ff
|
||||||
|
sx = rx*ty*sqrt(4*rx**4*ry**2*ty**2 - rx**4*ty**4 + 4*rx**2*ry**4*tx**2 - 2*rx**2*ry**2*tx**2*ty**2 - ry**4*tx**4)
|
||||||
|
sy = ry*tx*sqrt(-ff*(-4*rx**2*ry**2 + rx**2*ty**2 + ry**2*tx**2))
|
||||||
|
|
||||||
|
# intersection points
|
||||||
|
c1 = Coordinate((cx - sx) / (2*ry*ff), (cy + sy) / (2*rx*ff))
|
||||||
|
c2 = Coordinate((cx + sx) / (2*ry*ff), (cy - sy) / (2*rx*ff))
|
||||||
|
|
||||||
|
if end_o.cross_norm(c1 - r_start) < 0: # c1 is to the left of end_o
|
||||||
|
left = c1
|
||||||
|
right = c2
|
||||||
|
else:
|
||||||
|
left = c2
|
||||||
|
right = c1
|
||||||
|
|
||||||
|
if pos_dir != large_arc: #center should be on the left of end_o
|
||||||
|
center_o = left
|
||||||
|
else: #center should be on the right of end_o
|
||||||
|
center_o = right
|
||||||
|
|
||||||
|
#re-use ellipses with same rx, ry to save some memory
|
||||||
|
if (rx, ry) in self.ell_dict:
|
||||||
|
self.ellipse = self.ell_dict[(rx, ry)]
|
||||||
|
else:
|
||||||
|
self.ellipse = Ellipse(rx, ry)
|
||||||
|
self.ell_dict[(rx, ry)] = self.ellipse
|
||||||
|
|
||||||
|
self.start = start
|
||||||
|
self.end = end
|
||||||
|
self.axis_rot = axis_rot
|
||||||
|
self.pos_dir = pos_dir
|
||||||
|
self.large_arc = large_arc
|
||||||
|
self.start_theta = self.ellipse.theta_at_angle((-center_o).t)
|
||||||
|
self.end_theta = self.ellipse.theta_at_angle((end_o - center_o).t)
|
||||||
|
|
||||||
|
# translate center back to original offset
|
||||||
|
center_o.t += axis_rot
|
||||||
|
self.center = center_o + start
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
return self.ellipse.dist_from_theta(self.start_theta, self.end_theta)
|
||||||
|
|
||||||
|
def t_to_theta(self, t):
|
||||||
|
"""convert t (always between 0 and 1) to angle theta"""
|
||||||
|
start = self.start_theta
|
||||||
|
end = self.end_theta
|
||||||
|
|
||||||
|
if self.pos_dir and end < start:
|
||||||
|
end += 2 * pi
|
||||||
|
|
||||||
|
if not self.pos_dir and start < end:
|
||||||
|
end -= 2 * pi
|
||||||
|
arc_size = end - start
|
||||||
|
|
||||||
|
return (start + (end - start) * t) % (2 * pi)
|
||||||
|
|
||||||
|
def theta_to_t(self, theta):
|
||||||
|
full_arc_size = (self.end_theta - self.start_theta + 2 * pi) % (2 * pi)
|
||||||
|
theta_arc_size = (theta - self.start_theta + 2 * pi) % (2 * pi)
|
||||||
|
return theta_arc_size / full_arc_size
|
||||||
|
|
||||||
|
def curvature(self, t):
|
||||||
|
theta = self.t_to_theta(t)
|
||||||
|
return self.ellipse.curvature(theta)
|
||||||
|
|
||||||
|
def tangent(self, t):
|
||||||
|
theta = self.t_to_theta(t)
|
||||||
|
return self.ellipse.tangent(theta)
|
||||||
|
|
||||||
|
def t_at_length(self, length):
|
||||||
|
"""interpolated t where the curve is at the given length"""
|
||||||
|
theta = self.ellipse.theta_from_dist(length, self.start_theta)
|
||||||
|
return self.theta_to_t(theta)
|
||||||
|
|
||||||
|
def length_at_t(self, t):
|
||||||
|
return self.ellipse.dist_from_theta(self.start_theta, self.t_to_theta(t))
|
||||||
|
|
||||||
|
def pathpoint_at_t(self, t):
|
||||||
|
"""pathpoint on the curve from t=0 to point at t."""
|
||||||
|
centered = self.ellipse.coordinate_at_theta(self.t_to_theta(t))
|
||||||
|
centered.t += self.axis_rot
|
||||||
|
return PathPoint(t, centered + self.center, self.tangent(t), self.curvature(t), self.length_at_t(t))
|
||||||
|
|
||||||
|
# identical to Bezier code
|
||||||
|
def subdivide(self, part_length, start_offset=0):
|
||||||
|
nr_parts = int((self.length - start_offset) // part_length)
|
||||||
|
k_o = start_offset / self.length
|
||||||
|
k2t = lambda k : k_o + k * part_length / self.length
|
||||||
|
points = [self.pathpoint_at_t(k2t(k)) for k in range(nr_parts + 1)]
|
||||||
|
return(points, self.length - points[-1].c_dist)
|
@ -0,0 +1,25 @@
|
|||||||
|
from inkscape_helper.PathSegment import *
|
||||||
|
|
||||||
|
class Line(PathSegment):
|
||||||
|
|
||||||
|
def __init__(self, start, end):
|
||||||
|
self.start = start
|
||||||
|
self.end = end
|
||||||
|
self.pp = lambda t : PathPoint(t, self.start + t * (self.end - self.start), self.end - self.start, 0, t * self.length)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
return (self.end - self.start).r
|
||||||
|
|
||||||
|
def subdivide(self, part_length, start_offset=0): # note: start_offset should be smaller than part_length
|
||||||
|
nr_parts = int((self.length - start_offset) // part_length)
|
||||||
|
k_o = start_offset / self.length
|
||||||
|
k2t = lambda k : k_o + k * part_length / self.length
|
||||||
|
points = [self.pp(k2t(k)) for k in range(nr_parts + 1)]
|
||||||
|
return(points, self.length - points[-1].c_dist)
|
||||||
|
|
||||||
|
def pathpoint_at_t(self, t):
|
||||||
|
return self.pp(t)
|
||||||
|
|
||||||
|
def t_at_length(self, length):
|
||||||
|
return length / self.length
|
@ -0,0 +1,49 @@
|
|||||||
|
import copy
|
||||||
|
|
||||||
|
class Matrix(object):
|
||||||
|
"""
|
||||||
|
Matrix class with some basic matrix operations
|
||||||
|
"""
|
||||||
|
def __init__(self, array):
|
||||||
|
columns = len(array[0])
|
||||||
|
for r in array[1:]: #make sure each row has same number of columns
|
||||||
|
assert len(r) == columns
|
||||||
|
|
||||||
|
self.array = copy.copy(array)
|
||||||
|
self.rows = len(array)
|
||||||
|
self.columns = columns
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
a = ['[' + ', '.join([str(i) for i in r]) + ']' for r in self.array]
|
||||||
|
return '[\n' + ',\n'.join(a) + '\n]'
|
||||||
|
|
||||||
|
def minor(self, row, col):
|
||||||
|
return Matrix([[self[r][c] for c in range(self.columns) if c != col] for r in range(self.rows) if r != row])
|
||||||
|
|
||||||
|
def det(self):
|
||||||
|
if self.rows != self.columns:
|
||||||
|
raise TypeError, 'Can only calculate determinant for a square matrix'
|
||||||
|
if self.rows == 1:
|
||||||
|
return self[0][0]
|
||||||
|
if self.rows == 2:
|
||||||
|
return self[0][0] * self[1][1] - self[0][1] * self[1][0]
|
||||||
|
det = 0
|
||||||
|
for i in range(self.columns):
|
||||||
|
det += (-1)**i * self.array[0][i] * self.minor(0, i).det()
|
||||||
|
return det
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self.array[index]
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
if self.rows != other.rows or self.columns != other.columns:
|
||||||
|
raise TypeError, 'Both matrices should have equal dimensions. Is ({} x {}) and ({} x {}).'.format(self.rows, self.columns, other.rows, other.columns)
|
||||||
|
return Matrix([[self[r][c] + other[r][c] for c in range(self.columns)] for r in range(self.rows)])
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
if self.columns != other.rows:
|
||||||
|
raise TypeError, 'Left matrix should have same number of columns as right matrix has rows. Is ({} x {}) and ({} x {}).'.format(self.rows, self.columns, other.rows, other.columns)
|
||||||
|
return Matrix([[sum([self[r][i] * other[i][c] for i in range(self.columns)]) for c in range(other.columns)] for r in range(self.rows)])
|
@ -0,0 +1,29 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
PathPoint = namedtuple('PathPoint', 't coord tangent curvature c_dist')
|
||||||
|
|
||||||
|
class PathSegment(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lenth(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def subdivide(self, part_length):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def pathpoint_at_t(self, t):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def t_at_length(self, length):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# also need:
|
||||||
|
|
||||||
|
# find a way do do curvature dependent spacing
|
||||||
|
# - based on deviation from a standard radius?
|
||||||
|
# - or ratio between thickness and curvature?
|
||||||
|
#def point_at_distance(d):
|
||||||
|
# pass
|
@ -0,0 +1,93 @@
|
|||||||
|
import inkex
|
||||||
|
import simplestyle
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
def _format_1st(command, is_absolute):
|
||||||
|
"""Small helper function for the Path class"""
|
||||||
|
return command.upper() if is_absolute else command.lower()
|
||||||
|
|
||||||
|
default_style = str(inkex.Style(
|
||||||
|
{'stroke': '#000000',
|
||||||
|
'stroke-width': '0.1',
|
||||||
|
'fill': 'none'
|
||||||
|
}))
|
||||||
|
|
||||||
|
red_style = str(inkex.Style(
|
||||||
|
{'stroke': '#FF0000',
|
||||||
|
'stroke-width': '0.1',
|
||||||
|
'fill': 'none'
|
||||||
|
}))
|
||||||
|
|
||||||
|
green_style = str(inkex.Style(
|
||||||
|
{'stroke': '#00FF00',
|
||||||
|
'stroke-width': '0.1',
|
||||||
|
'fill': 'none'
|
||||||
|
}))
|
||||||
|
|
||||||
|
blue_style = str(inkex.Style(
|
||||||
|
{'stroke': '#0000FF',
|
||||||
|
'stroke-width': '0.1',
|
||||||
|
'fill': 'none'
|
||||||
|
}))
|
||||||
|
|
||||||
|
def layer(parent, layer_name):
|
||||||
|
layer = etree.SubElement(parent, 'g')
|
||||||
|
layer.set(inkex.addNS('label', 'inkscape'), layer_name)
|
||||||
|
layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def group(parent):
|
||||||
|
return etree.SubElement(parent, 'g')
|
||||||
|
|
||||||
|
def text(parent, coordinate, txt, style=default_style):
|
||||||
|
text = etree.Element(inkex.addNS('text', 'svg'))
|
||||||
|
text.text = txt
|
||||||
|
text.set('x', str(coordinate.x))
|
||||||
|
text.set('y', str(coordinate.y))
|
||||||
|
style = {'text-align': 'center', 'text-anchor': 'middle'}
|
||||||
|
text.set('style', str(inkex.Style(style)))
|
||||||
|
parent.append(text)
|
||||||
|
|
||||||
|
class Path(object):
|
||||||
|
"""
|
||||||
|
Generates SVG paths
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.nodes = []
|
||||||
|
|
||||||
|
def move_to(self, coord, absolute=False):
|
||||||
|
self.nodes.append("{0} {1} {2}".format(_format_1st('m', absolute), coord.x, coord.y))
|
||||||
|
|
||||||
|
def line_to(self, coord, absolute=False):
|
||||||
|
self.nodes.append("{0} {1} {2}".format(_format_1st('l', absolute), coord.x, coord.y))
|
||||||
|
|
||||||
|
def h_line_to(self, dist, absolute=False):
|
||||||
|
self.nodes.append("{0} {1}".format(_format_1st('h', absolute), dist))
|
||||||
|
|
||||||
|
def v_line_to(self, dist, absolute=False):
|
||||||
|
self.nodes.append("{0} {1}".format(_format_1st('v', absolute), dist))
|
||||||
|
|
||||||
|
def arc_to(self, radius, coord, rotation=0, pos_sweep=True, large_arc=False, absolute=False):
|
||||||
|
self.nodes.append("{0} {1} {2} {3} {4} {5} {6} {7}"
|
||||||
|
.format(_format_1st('a', absolute), radius.x, radius.y, rotation,
|
||||||
|
1 if large_arc else 0, 1 if pos_sweep else 0, coord.x, coord.y))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.nodes.append('z')
|
||||||
|
|
||||||
|
def path(self, parent, style=default_style):
|
||||||
|
attribs = {'style': style,
|
||||||
|
'd': ' '.join(self.nodes)}
|
||||||
|
etree.SubElement(parent, inkex.addNS('path', 'svg'), attribs)
|
||||||
|
|
||||||
|
def curve(self, parent, segments, style, closed=True):
|
||||||
|
pathStr = ' '.join(segments)
|
||||||
|
if closed:
|
||||||
|
pathStr += ' z'
|
||||||
|
attributes = {
|
||||||
|
'style': style,
|
||||||
|
'd': pathStr}
|
||||||
|
etree.SubElement(parent, inkex.addNS('path', 'svg'), attributes)
|
||||||
|
|
||||||
|
def remove_last(self):
|
||||||
|
self.nodes.pop()
|
21
extensions/fablabchemnitz/box_maker_elliptical_box/meta.json
Normal file
21
extensions/fablabchemnitz/box_maker_elliptical_box/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Box Maker - Elliptical Box",
|
||||||
|
"id": "fablabchemnitz.de.box_maker_elliptical_box",
|
||||||
|
"path": "box_maker_elliptical_box",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": " Elliptical Box Maker",
|
||||||
|
"original_id": "be.fablab-leuven.inkscape.elliptical_box",
|
||||||
|
"license": "MIT License",
|
||||||
|
"license_url": "https://github.com/BvdP/elliptical-box-maker/blob/master/LICENSE",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_elliptical_box",
|
||||||
|
"fork_url": "https://github.com/BvdP/elliptical-box-maker",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Elliptical+Box",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/BvdP",
|
||||||
|
"github.com/eridur-de"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,89 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Box Maker - Lasercut Box</name>
|
||||||
|
<id>fablabchemnitz.de.box_maker_lasercut_box</id>
|
||||||
|
<param name="tab" type="notebook">
|
||||||
|
<page name="Dimensions" gui-text="Dimensions">
|
||||||
|
<label xml:space="preserve">Dimensions can measure the external or internal size of the box.
|
||||||
|
The corner cubes can be omitted.
|
||||||
|
The document units should be set to mm instead of pixels (assuming you mean to really cut it out with a laser).
|
||||||
|
</label>
|
||||||
|
<param name="int_ext" gui-text="Dimensions are" type="optiongroup" appearance="combo">
|
||||||
|
<option value="true">External</option>
|
||||||
|
<option value="false">Internal</option>
|
||||||
|
</param>
|
||||||
|
<param name="units" gui-text="Units" type="optiongroup" appearance="combo">
|
||||||
|
<option value="in">in</option>
|
||||||
|
<option value="cm">cm</option>
|
||||||
|
<option value="mm">mm</option>
|
||||||
|
<option value="px">px</option>
|
||||||
|
<option value="pt">pt</option>
|
||||||
|
</param>
|
||||||
|
<param name="width" type="float" min="0.1" max="1000.0" gui-text="Width">50.0</param>
|
||||||
|
<param name="height" type="float" min="0.1" max="1000.0" gui-text="Height">30.0</param>
|
||||||
|
<param name="depth" type="float" min="0.1" max="1000.0" gui-text="Depth">15</param>
|
||||||
|
<param name="thickness" type="float" min="0.0" max="20.0" precision="2" gui-text="Material Thickness">3.0</param>
|
||||||
|
<param name="ntab_W" type="int" min="1" max="299" gui-text="Width Tab Count">11</param>
|
||||||
|
<param name="ntab_H" type="int" min="1" max="299" gui-text="Height Tab Count">11</param>
|
||||||
|
<param name="ntab_D" type="int" min="1" max="299" gui-text="Depth Tab Count">6</param>
|
||||||
|
<param name="corners" type="bool" gui-text="Include corners">true</param>
|
||||||
|
<param name="halftabs" type="bool" gui-text="Use half-sized tabs near corners">true</param>
|
||||||
|
</page>
|
||||||
|
<page name="Usage2" gui-text="Laser Kerf">
|
||||||
|
<label xml:space="preserve">The kerf is the amount lost to the burning width of the laser.
|
||||||
|
Typically in the range 0.1 to 0.4 mm. Check with your laser vendor. This will depend on the material being cut and the speed of the laser.
|
||||||
|
|
||||||
|
Setting the kerf to 0 will create a box that will use the least material. However the material lost to the laser will make for a loose fit and probably require glue.
|
||||||
|
|
||||||
|
If the Kerf is greater than zero then you can have a calculated "Precise fit" or add dimples for a "Press fit".
|
||||||
|
A "Precise fit" will change the width of the tabs to allow for the material burned away by the laser and make a tight fit.
|
||||||
|
Dimples will add notches for a "Press fit" (see next tab)
|
||||||
|
|
||||||
|
The pattern will be spread out, using more material, because a common laser cut line cannot be used.
|
||||||
|
</label>
|
||||||
|
<param name="kerf_size" type="float" min="0.0" max="3.0" precision="2" gui-text="Kerf (amount lost to laser)">0.0</param>
|
||||||
|
<param name="linewidth" type="bool" gui-text="Display Line width = kerf">false</param>
|
||||||
|
</page>
|
||||||
|
<page name="Usage3" gui-text="Dimples">
|
||||||
|
<label xml:space="preserve">Dimples are used so that a press-fit can be
|
||||||
|
made in deformable materials like wood.
|
||||||
|
|
||||||
|
If Dimple is checked then no "precise fit" kerf adjustment is made to the tabs. Instead a small dimple is added to each notch to enable a press fit.
|
||||||
|
- the dimple size is equal to the kerf size.
|
||||||
|
|
||||||
|
Dimples are useful for flexible materials like wood but are not good for rigid materials like perspex.
|
||||||
|
|
||||||
|
Dimple style is half rounds or triangles. Triangles are cheaper to cut, Half rounds fit better.
|
||||||
|
</label>
|
||||||
|
<param name="dimples" type="bool" gui-text="Dimples instead of tight fit">false</param>
|
||||||
|
<param name="dstyle" gui-text="Dimple Style" type="optiongroup" appearance="combo">
|
||||||
|
<option value="false">Dimples</option>
|
||||||
|
<option value="true">Triangles</option>
|
||||||
|
</param>
|
||||||
|
</page>
|
||||||
|
<page name="Usage4" gui-text="Misc">
|
||||||
|
<label xml:space="preserve">Colours:
|
||||||
|
The color scheme used by Ponoko is used here:
|
||||||
|
Specifically:
|
||||||
|
- Blue (0,0,255) is the lasercut line color
|
||||||
|
- Orange is a non-printing annotation
|
||||||
|
The lines are all 0.1mm in width - as required by Ponoko.
|
||||||
|
|
||||||
|
Annotations can be shown. They describe the kerf amount only and are shown on each piece.
|
||||||
|
This can be helpful if printing tests fits for different materials.
|
||||||
|
</label>
|
||||||
|
<param name="annotation" type="bool" gui-text="Include annotation">true</param>
|
||||||
|
</page>
|
||||||
|
</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_lasercut_box.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,473 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
Copyright (C)2011 Mark Schafer <neon.mark(a)gmaildotcom>
|
||||||
|
|
||||||
|
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
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Build a tabbed box for lasercutting with tight fit, and minimal material use options.
|
||||||
|
# User defines:
|
||||||
|
# - internal or external dimensions,
|
||||||
|
# - number of tabs,
|
||||||
|
# - amount lost to laser (kerf),
|
||||||
|
# - include corner cubes or not,
|
||||||
|
# - dimples, or perfect fit (accounting for kerf).
|
||||||
|
# If zero kerf - will be perfectly packed for minimal laser cuts and material size.
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
# add option to pack multiple boxes (if zero kerf) - new tab maybe?
|
||||||
|
# add option for little circles at sharp corners for acrylic
|
||||||
|
# annotations: - add overlap value as markup - Ponoko annotation color
|
||||||
|
# choose colours from a dictionary
|
||||||
|
|
||||||
|
### Versions
|
||||||
|
# 0.1 February 2011 - basic lasercut box with dimples etc
|
||||||
|
# 0.2 changes to unittouu for Inkscape 0.91
|
||||||
|
# 0.3 Option to avoid half-sized tabs at corners. <juergen@fabmail.org>
|
||||||
|
|
||||||
|
__version__ = "0.3"
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
from inkex.paths import Path
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
class BoxMakerLasercutBox(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("-i", "--int_ext", type = inkex.Boolean, default=False, help="Are the Dimensions for External or Internal sizing.")
|
||||||
|
pars.add_argument("-x", "--width", type=float, default=50.0, help="The Box Width - in the X dimension")
|
||||||
|
pars.add_argument("-y", "--height", type=float, default=30.0, help="The Box Height - in the Y dimension")
|
||||||
|
pars.add_argument("-z", "--depth", type=float, default=15.0, help="The Box Depth - in the Z dimension")
|
||||||
|
pars.add_argument("-t", "--thickness", type=float, default=3.0, help="Material Thickness - critical to know")
|
||||||
|
pars.add_argument("-u", "--units", default="cm", help="The unit of the box dimensions")
|
||||||
|
pars.add_argument("-c", "--corners", type = inkex.Boolean, default=True, help="The corner cubes can be removed for a different look")
|
||||||
|
pars.add_argument("-H", "--halftabs", type = inkex.Boolean, default=True, help="Start/End with half-sized tabs - Avoid with very small tabs")
|
||||||
|
pars.add_argument("-p", "--ntab_W", type=int, default=11, help="Number of tabs in Width")
|
||||||
|
pars.add_argument("-q", "--ntab_H", type=int, default=11, help="Number of tabs in Height")
|
||||||
|
pars.add_argument("-r", "--ntab_D", type=int, default=6, help="Number of tabs in Depth")
|
||||||
|
pars.add_argument("-k", "--kerf_size", type=float,default=0.0, help="Kerf size - amount lost to laser for this material. 0 = loose fit")
|
||||||
|
pars.add_argument("-d", "--dimples", type=inkex.Boolean, default=False, help="Add dimples for press fitting wooden materials")
|
||||||
|
pars.add_argument("-s", "--dstyle", type=inkex.Boolean, default=False, help="Dimples can be triangles(cheaper) or half rounds(better)")
|
||||||
|
pars.add_argument("-g", "--linewidth", type=inkex.Boolean, default=False, help="Use the kerf value as the drawn line width")
|
||||||
|
pars.add_argument("-j", "--annotation", type=inkex.Boolean, default=True, help="Show Kerf value as annotation")
|
||||||
|
#dummy for the doc tab - which is named
|
||||||
|
pars.add_argument("--tab", default="use", help="The selected UI-tab when OK was pressed")
|
||||||
|
#internal useful variables
|
||||||
|
|
||||||
|
def annotation(self, x, y, text):
|
||||||
|
""" Draw text at this location
|
||||||
|
- change to path
|
||||||
|
- use annotation color """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def thickness_line(self, dimple, vert_horiz, pos_neg):
|
||||||
|
""" called to draw dimples (also draws simple lines if no dimple)
|
||||||
|
- pos_neg is 1, -1 for direction
|
||||||
|
- vert_horiz is v or h """
|
||||||
|
if dimple and self.kerf > 0.0: # we need a dimple
|
||||||
|
# size is radius = kerf
|
||||||
|
# short line, half circle, short line
|
||||||
|
#[ 'C', [x1,y1, x2,y2, x,y] ] x1 is first handle, x2 is second
|
||||||
|
lines = []
|
||||||
|
radius = self.kerf
|
||||||
|
if self.thick - 2 * radius < 0.2: # correct for large dimples(kerf) on small thicknesses
|
||||||
|
radius = (self.thick - 0.2) / 2
|
||||||
|
short = 0.1
|
||||||
|
else:
|
||||||
|
short = self.thick/2 - radius
|
||||||
|
if vert_horiz == 'v': # vertical line
|
||||||
|
# first short line
|
||||||
|
lines.append(['v', [pos_neg*short]])
|
||||||
|
# half circle
|
||||||
|
if pos_neg == 1: # only the DH_sides need reversed tabs to interlock
|
||||||
|
if self.dimple_tri:
|
||||||
|
lines.append(['l', [radius, pos_neg*radius]])
|
||||||
|
lines.append(['l', [-radius, pos_neg*radius]])
|
||||||
|
else:
|
||||||
|
lines.append(['c', [radius, 0, radius, pos_neg*2*radius, 0, pos_neg*2*radius]])
|
||||||
|
else:
|
||||||
|
if self.dimple_tri:
|
||||||
|
lines.append(['l', [-radius, pos_neg*radius]])
|
||||||
|
lines.append(['l', [radius, pos_neg*radius]])
|
||||||
|
else:
|
||||||
|
lines.append(['c', [-radius, 0, -radius, pos_neg*2*radius, 0, pos_neg*2*radius]])
|
||||||
|
# last short line
|
||||||
|
lines.append(['v', [pos_neg*short]])
|
||||||
|
else: # horizontal line
|
||||||
|
# first short line
|
||||||
|
lines.append(['h', [pos_neg*short]])
|
||||||
|
# half circle
|
||||||
|
if self.dimple_tri:
|
||||||
|
lines.append(['l', [pos_neg*radius, radius]])
|
||||||
|
lines.append(['l', [pos_neg*radius, -radius]])
|
||||||
|
else:
|
||||||
|
lines.append(['c', [0, radius, pos_neg*2*radius, radius, pos_neg*2*radius, 0]])
|
||||||
|
# last short line
|
||||||
|
lines.append(['h', [pos_neg*short]])
|
||||||
|
return lines
|
||||||
|
|
||||||
|
# No dimple - so much easier
|
||||||
|
else: # return a straight v or h line same as thickness
|
||||||
|
if vert_horiz == 'v':
|
||||||
|
return [ ['v', [pos_neg*self.thick]] ]
|
||||||
|
else:
|
||||||
|
return [ ['h', [pos_neg*self.thick]] ]
|
||||||
|
|
||||||
|
|
||||||
|
def draw_WH_lid(self, startx, starty, masktop=False):
|
||||||
|
""" Return an SVG path for the top or bottom of box
|
||||||
|
- the Width * Height dimension """
|
||||||
|
line_path = []
|
||||||
|
line_path.append(['M', [startx, starty]])
|
||||||
|
# top row of tabs
|
||||||
|
if masktop and self.kerf ==0.0: # don't draw top for packing with no extra cuts
|
||||||
|
line_path.append(['m', [self.boxW, 0]])
|
||||||
|
else:
|
||||||
|
if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 - self.pf/2, 0]])
|
||||||
|
for i in range(int(self.Wtabs)):
|
||||||
|
line_path.append(['h', [self.boxW/self.Wtabs/4 - self.pf/2]])
|
||||||
|
#line_path.append(['v', [0, -thick]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'v', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['h', [self.boxW/self.Wtabs/2 + self.pf]])
|
||||||
|
line_path.append(['v', [self.thick]])
|
||||||
|
line_path.append(['h', [self.boxW/self.Wtabs/4 - self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 - self.pf/2, 0]])
|
||||||
|
# right hand vertical drop
|
||||||
|
if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 - self.pf/2]])
|
||||||
|
for i in range(int(self.Htabs)):
|
||||||
|
line_path.append(['v', [self.boxH/self.Htabs/4 - self.pf/2]])
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
line_path.append(['v', [self.boxH/self.Htabs/2 + self.pf]])
|
||||||
|
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'h', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['v', [self.boxH/self.Htabs/4 - self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 - self.pf/2]])
|
||||||
|
# bottom row (in reverse)
|
||||||
|
if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 + self.pf/2, 0]])
|
||||||
|
for i in range(int(self.Wtabs)):
|
||||||
|
line_path.append(['h', [-self.boxW/self.Wtabs/4 + self.pf/2]])
|
||||||
|
line_path.append(['v', [self.thick]])
|
||||||
|
line_path.append(['h', [-self.boxW/self.Wtabs/2 - self.pf]])
|
||||||
|
#line_path.append(['v', [0, -thick]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'v', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['h', [-self.boxW/self.Wtabs/4 + self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 + self.pf/2, 0]])
|
||||||
|
# up the left hand side
|
||||||
|
if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 + self.pf/2]])
|
||||||
|
for i in range(int(self.Htabs)):
|
||||||
|
line_path.append(['v', [-self.boxH/self.Htabs/4 + self.pf/2]])
|
||||||
|
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'h', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['v', [-self.boxH/self.Htabs/2 - self.pf]])
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
line_path.append(['v', [-self.boxH/self.Htabs/4 + self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 + self.pf/2]])
|
||||||
|
return line_path
|
||||||
|
|
||||||
|
|
||||||
|
def draw_WD_side(self, startx, starty, mask=False, corners=True):
|
||||||
|
""" Return an SVG path for the long side of box
|
||||||
|
- the Width * Depth dimension """
|
||||||
|
# Draw side of the box (placed below the lid)
|
||||||
|
line_path = []
|
||||||
|
# top row of tabs
|
||||||
|
if corners:
|
||||||
|
line_path.append(['M', [startx - self.thick, starty]])
|
||||||
|
line_path.append(['v', [-self.thick]])
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
else:
|
||||||
|
line_path.append(['M', [startx, starty]])
|
||||||
|
line_path.append(['v', [-self.thick]])
|
||||||
|
#
|
||||||
|
if self.kerf > 0.0: # if fit perfectly - don't draw double line
|
||||||
|
if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 + self.pf/2, 0]])
|
||||||
|
for i in range(int(self.Wtabs)):
|
||||||
|
line_path.append(['h', [self.boxW/self.Wtabs/4 + self.pf/2]])
|
||||||
|
line_path.append(['v', [self.thick]])
|
||||||
|
line_path.append(['h', [self.boxW/self.Wtabs/2 - self.pf]])
|
||||||
|
#line_path.append(['v', [0, -thick]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'v', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['h', [self.boxW/self.Wtabs/4 + self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 + self.pf/2, 0]])
|
||||||
|
if corners: line_path.append(['h', [self.thick]])
|
||||||
|
else: # move to skipped drawn lines
|
||||||
|
if corners:
|
||||||
|
line_path.append(['m', [self.boxW + self.thick, 0]])
|
||||||
|
else:
|
||||||
|
line_path.append(['m', [self.boxW, 0]])
|
||||||
|
#
|
||||||
|
line_path.append(['v', [self.thick]])
|
||||||
|
if not corners: line_path.append(['h', [self.thick]])
|
||||||
|
# RHS
|
||||||
|
if not self.ht: line_path.append(['l', [0, self.boxD/self.Dtabs/4 + self.pf/2]])
|
||||||
|
for i in range(int(self.Dtabs)):
|
||||||
|
line_path.append(['v', [self.boxD/self.Dtabs/4 + self.pf/2]])
|
||||||
|
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'h', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['v', [self.boxD/self.Dtabs/2 - self.pf]])
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
line_path.append(['v', [self.boxD/self.Dtabs/4 + self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [0, self.boxD/self.Dtabs/4 + self.pf/2]])
|
||||||
|
#
|
||||||
|
if corners:
|
||||||
|
line_path.append(['v', [self.thick]])
|
||||||
|
line_path.append(['h', [-self.thick]])
|
||||||
|
else:
|
||||||
|
line_path.append(['h', [-self.thick]])
|
||||||
|
line_path.append(['v', [self.thick]])
|
||||||
|
# base
|
||||||
|
if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 - self.pf/2, 0]])
|
||||||
|
for i in range(int(self.Wtabs)):
|
||||||
|
line_path.append(['h', [-self.boxW/self.Wtabs/4 - self.pf/2]])
|
||||||
|
#line_path.append(['v', [0, -thick]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'v', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['h', [-self.boxW/self.Wtabs/2 + self.pf]])
|
||||||
|
line_path.append(['v', [self.thick]])
|
||||||
|
line_path.append(['h', [-self.boxW/self.Wtabs/4 - self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 - self.pf/2, 0]])
|
||||||
|
#
|
||||||
|
if corners:
|
||||||
|
line_path.append(['h', [-self.thick]])
|
||||||
|
line_path.append(['v', [-self.thick]])
|
||||||
|
else:
|
||||||
|
line_path.append(['v', [-self.thick]])
|
||||||
|
line_path.append(['h', [-self.thick]])
|
||||||
|
# LHS
|
||||||
|
if not self.ht: line_path.append(['l', [0, -self.boxD/self.Dtabs/4 - self.pf/2]])
|
||||||
|
for i in range(int(self.Dtabs)):
|
||||||
|
line_path.append(['v', [-self.boxD/self.Dtabs/4 - self.pf/2]])
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
line_path.append(['v', [-self.boxD/self.Dtabs/2 + self.pf]])
|
||||||
|
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'h', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['v', [-self.boxD/self.Dtabs/4 - self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [0, -self.boxD/self.Dtabs/4 - self.pf/2]])
|
||||||
|
#
|
||||||
|
if not corners: line_path.append(['h', [self.thick]])
|
||||||
|
return line_path
|
||||||
|
|
||||||
|
|
||||||
|
def draw_HD_side(self, startx, starty, corners, mask=False):
|
||||||
|
""" Return an SVG path for the short side of box
|
||||||
|
- the Height * Depth dimension """
|
||||||
|
line_path = []
|
||||||
|
# top row of tabs
|
||||||
|
line_path.append(['M', [startx, starty]])
|
||||||
|
if not(mask and corners and self.kerf == 0.0):
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
else:
|
||||||
|
line_path.append(['m', [self.thick, 0]])
|
||||||
|
if not self.ht: line_path.append(['l', [self.boxD/self.Dtabs/4 - self.pf/2, 0]])
|
||||||
|
for i in range(int(self.Dtabs)):
|
||||||
|
line_path.append(['h', [self.boxD/self.Dtabs/4 - self.pf/2]])
|
||||||
|
line_path.append(['v', [-self.thick]])
|
||||||
|
line_path.append(['h', [self.boxD/self.Dtabs/2 + self.pf]])
|
||||||
|
#line_path.append(['v', [0, thick]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'v', 1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['h', [self.boxD/self.Dtabs/4 - self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [self.boxD/self.Dtabs/4 - self.pf/2, 0]])
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
#
|
||||||
|
if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 + self.pf/2]])
|
||||||
|
for i in range(int(self.Htabs)):
|
||||||
|
line_path.append(['v', [self.boxH/self.Htabs/4 + self.pf/2]])
|
||||||
|
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'h', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['v', [self.boxH/self.Htabs/2 - self.pf]])
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
line_path.append(['v', [self.boxH/self.Htabs/4 + self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 + self.pf/2]])
|
||||||
|
line_path.append(['h', [-self.thick]])
|
||||||
|
#
|
||||||
|
if not self.ht: line_path.append(['l', [-self.boxD/self.Dtabs/4 + self.pf/2, 0]])
|
||||||
|
for i in range(int(self.Dtabs)):
|
||||||
|
line_path.append(['h', [-self.boxD/self.Dtabs/4 + self.pf/2]])
|
||||||
|
#line_path.append(['v', [0, thick]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'v', 1): # this is the weird +1 instead of -1 dimple
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['h', [-self.boxD/self.Dtabs/2 - self.pf]])
|
||||||
|
line_path.append(['v', [-self.thick]])
|
||||||
|
line_path.append(['h', [-self.boxD/self.Dtabs/4 + self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [-self.boxD/self.Dtabs/4 + self.pf/2, 0]])
|
||||||
|
line_path.append(['h', [-self.thick]])
|
||||||
|
#
|
||||||
|
if self.kerf > 0.0: # if fit perfectly - don't draw double line
|
||||||
|
if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 - self.pf/2]])
|
||||||
|
for i in range(int(self.Htabs)):
|
||||||
|
line_path.append(['v', [-self.boxH/self.Htabs/4 - self.pf/2]])
|
||||||
|
line_path.append(['h', [self.thick]])
|
||||||
|
line_path.append(['v', [-self.boxH/self.Htabs/2 + self.pf]])
|
||||||
|
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
|
||||||
|
for l in self.thickness_line(self.dimple, 'h', -1):
|
||||||
|
line_path.append(l)
|
||||||
|
line_path.append(['v', [-self.boxH/self.Htabs/4 - self.pf/2]])
|
||||||
|
if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 - self.pf/2]])
|
||||||
|
return line_path
|
||||||
|
|
||||||
|
###--------------------------------------------
|
||||||
|
### The main function called by the inkscape UI
|
||||||
|
def effect(self):
|
||||||
|
|
||||||
|
self.stroke_width = self.svg.unittouu('1px') #default for visiblity
|
||||||
|
self.line_style = {'stroke': '#0000FF', # Ponoko blue
|
||||||
|
'fill': 'none',
|
||||||
|
'stroke-width': self.stroke_width,
|
||||||
|
'stroke-linecap': 'butt',
|
||||||
|
'stroke-linejoin': 'miter'}
|
||||||
|
|
||||||
|
|
||||||
|
# document dimensions (for centering)
|
||||||
|
docW = self.svg.unittouu(self.document.getroot().get('width'))
|
||||||
|
docH = self.svg.unittouu(self.document.getroot().get('height'))
|
||||||
|
# extract fields from UI
|
||||||
|
self.boxW = self.svg.unittouu(str(self.options.width) + self.options.units)
|
||||||
|
self.boxH = self.svg.unittouu(str(self.options.height) + self.options.units)
|
||||||
|
self.boxD = self.svg.unittouu(str(self.options.depth) + self.options.units)
|
||||||
|
self.thick = self.svg.unittouu(str(self.options.thickness) + self.options.units)
|
||||||
|
self.kerf = self.svg.unittouu(str(self.options.kerf_size) + self.options.units)
|
||||||
|
if self.kerf < 0.01: self.kerf = 0.0 # snap to 0 for UI error when setting spinner to 0.0
|
||||||
|
self.Wtabs = self.options.ntab_W
|
||||||
|
self.Htabs = self.options.ntab_H
|
||||||
|
self.Dtabs = self.options.ntab_D
|
||||||
|
self.dimple = self.options.dimples
|
||||||
|
line_width = self.options.linewidth
|
||||||
|
corners = self.options.corners
|
||||||
|
self.dimple_tri = self.options.dstyle
|
||||||
|
self.annotation = self.options.annotation
|
||||||
|
self.ht = self.options.halftabs
|
||||||
|
if not self.ht:
|
||||||
|
self.Wtabs += 0.5
|
||||||
|
self.Htabs += 0.5
|
||||||
|
self.Dtabs += 0.5
|
||||||
|
|
||||||
|
# Correct for thickness in dimensions
|
||||||
|
if self.options.int_ext: # external so add thickness
|
||||||
|
self.boxW -= self.thick*2
|
||||||
|
self.boxH -= self.thick*2
|
||||||
|
self.boxD -= self.thick*2
|
||||||
|
# adjust for laser kerf (precise measurement)
|
||||||
|
self.boxW += self.kerf
|
||||||
|
self.boxH += self.kerf
|
||||||
|
self.boxD += self.kerf
|
||||||
|
|
||||||
|
# Precise fit or dimples (if kerf > 0.0)
|
||||||
|
if self.dimple == False: # and kerf > 0.0:
|
||||||
|
self.pf = self.kerf
|
||||||
|
else:
|
||||||
|
self.pf = 0.0
|
||||||
|
|
||||||
|
# set the stroke width and line style
|
||||||
|
sw = self.kerf
|
||||||
|
if self.kerf == 0.0: sw = self.stroke_width
|
||||||
|
ls = self.line_style
|
||||||
|
if line_width: # user wants drawn line width to be same as kerf size
|
||||||
|
ls['stroke-width'] = sw
|
||||||
|
line_style = str(inkex.Style(ls))
|
||||||
|
|
||||||
|
###---------------------------
|
||||||
|
### create the inkscape object
|
||||||
|
box_id = self.svg.get_unique_id('box')
|
||||||
|
self.box = g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':box_id})
|
||||||
|
|
||||||
|
#Set local position for drawing (will transform to center of doc at end)
|
||||||
|
lower_pos = 0
|
||||||
|
left_pos = 0
|
||||||
|
# Draw Lid (using SVG path definitions)
|
||||||
|
line_path = self.draw_WH_lid(left_pos, lower_pos)
|
||||||
|
# Add to scene
|
||||||
|
line_atts = { 'style':line_style, 'id':box_id+'-lid', 'd':str(Path(line_path)) }
|
||||||
|
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
|
||||||
|
|
||||||
|
# draw the side of the box directly below
|
||||||
|
if self.kerf > 0.0:
|
||||||
|
lower_pos += self.boxH + (3*self.thick)
|
||||||
|
else: # kerf = 0 so don't draw extra lines and fit perfectly
|
||||||
|
lower_pos += self.boxH + self.thick # at lower edge of lid
|
||||||
|
left_pos += 0
|
||||||
|
# Draw side of the box (placed below the lid)
|
||||||
|
line_path = self.draw_WD_side(left_pos, lower_pos, corners=corners)
|
||||||
|
# Add to scene
|
||||||
|
line_atts = { 'style':line_style, 'id':box_id+'-longside1', 'd':str(Path(line_path)) }
|
||||||
|
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
|
||||||
|
|
||||||
|
# draw the bottom of the box directly below
|
||||||
|
if self.kerf > 0.0:
|
||||||
|
lower_pos += self.boxD + (3*self.thick)
|
||||||
|
else: # kerf = 0 so don't draw extra lines and fit perfectly
|
||||||
|
lower_pos += self.boxD + self.thick # at lower edge
|
||||||
|
left_pos += 0
|
||||||
|
# Draw base of the box
|
||||||
|
line_path = self.draw_WH_lid(left_pos, lower_pos, True)
|
||||||
|
# Add to scene
|
||||||
|
line_atts = { 'style':line_style, 'id':box_id+'-base', 'd':str(Path(line_path)) }
|
||||||
|
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
|
||||||
|
|
||||||
|
# draw the second side of the box directly below
|
||||||
|
if self.kerf > 0.0:
|
||||||
|
lower_pos += self.boxH + (3*self.thick)
|
||||||
|
else: # kerf = 0 so don't draw extra lines and fit perfectly
|
||||||
|
lower_pos += self.boxH + self.thick # at lower edge of lid
|
||||||
|
left_pos += 0
|
||||||
|
# Draw side of the box (placed below the lid)
|
||||||
|
line_path = self.draw_WD_side(left_pos, lower_pos, corners=corners)
|
||||||
|
# Add to scene
|
||||||
|
line_atts = { 'style':line_style, 'id':box_id+'-longside2', 'd':str(Path(line_path)) }
|
||||||
|
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
|
||||||
|
|
||||||
|
# draw next on RHS of lid
|
||||||
|
if self.kerf > 0.0:
|
||||||
|
left_pos += self.boxW + (2*self.thick) # adequate space (could be a param for separation when kerf > 0)
|
||||||
|
else:
|
||||||
|
left_pos += self.boxW # right at right edge of lid
|
||||||
|
lower_pos = 0
|
||||||
|
# Side of the box (placed next to the lid)
|
||||||
|
line_path = self.draw_HD_side(left_pos, lower_pos, corners)
|
||||||
|
# Add to scene
|
||||||
|
line_atts = { 'style':line_style, 'id':box_id+'-endface2', 'd':str(Path(line_path)) }
|
||||||
|
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
|
||||||
|
|
||||||
|
# draw next on RHS of base
|
||||||
|
if self.kerf > 0.0:
|
||||||
|
lower_pos += self.boxH + self.boxD + 6*self.thick
|
||||||
|
else:
|
||||||
|
lower_pos += self.boxH +self.boxD + 2*self.thick
|
||||||
|
# Side of the box (placed next to the lid)
|
||||||
|
line_path = self.draw_HD_side(left_pos, lower_pos, corners, True)
|
||||||
|
# Add to scene
|
||||||
|
line_atts = { 'style':line_style, 'id':box_id+'-endface1', 'd':str(Path(line_path)) }
|
||||||
|
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
|
||||||
|
|
||||||
|
|
||||||
|
###----------------------------------------
|
||||||
|
# Transform entire drawing to center of doc
|
||||||
|
lower_pos += self.boxH*2 + self.boxD*2 + 2*self.thick
|
||||||
|
left_pos += self.boxH + 2*self.thick
|
||||||
|
g.set( 'transform', 'translate(%f,%f)' % ( (docW-left_pos)/2, (docH-lower_pos)/2))
|
||||||
|
|
||||||
|
# The implementation algorithm has added intermediate short lines and doubled up when using h,v with extra zeros
|
||||||
|
#self.thin(g) # remove short straight lines
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
BoxMakerLasercutBox().run()
|
22
extensions/fablabchemnitz/box_maker_lasercut_box/meta.json
Normal file
22
extensions/fablabchemnitz/box_maker_lasercut_box/meta.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Box Maker - Lasercut Box",
|
||||||
|
"id": "fablabchemnitz.de.box_maker_lasercut_box",
|
||||||
|
"path": "box_maker_lasercut_box",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Lasercut Box",
|
||||||
|
"original_id": "org.inkscape.LasercutBox",
|
||||||
|
"license": "GNU GPL v2",
|
||||||
|
"license_url": "https://github.com/Neon22/inkscape-LasercutBox/blob/master/LICENSE",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_lasercut_box",
|
||||||
|
"fork_url": "https://github.com/Neon22/inkscape-LasercutBox",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Lasercut+Box",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/Neon22",
|
||||||
|
"github.com/jnweiger",
|
||||||
|
"github.com/eridur-de"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
1
extensions/fablabchemnitz/box_maker_path_to_flex/.gitignore
vendored
Normal file
1
extensions/fablabchemnitz/box_maker_path_to_flex/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/DebugPath2Flex.txt
|
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Box Maker - Path To Flex</name>
|
||||||
|
<id>fablabchemnitz.de.box_maker_path_to_flex</id>
|
||||||
|
<param name="unit" type="optiongroup" appearance="combo" gui-text="Unit">
|
||||||
|
<option value="mm">mm</option>
|
||||||
|
<option value="cm">cm</option>
|
||||||
|
<option value="in">in</option>
|
||||||
|
<option value="pt">pt</option>
|
||||||
|
<option value="px">px</option>
|
||||||
|
<option value="pc">pc</option>
|
||||||
|
</param>
|
||||||
|
<param name="thickness" type="float" min="1.0" max="10.0" gui-text="Material thickness">3.0</param>
|
||||||
|
<param name="zc" type="float" min="15.0" max="1000.0" gui-text="Structure height">50.0</param>
|
||||||
|
<param name="notch_interval" type="int" min="2" max="10" gui-text="Interval between notches">2</param>
|
||||||
|
<param name="max_size_flex" type="float" min="200.0" max="100000.0" gui-text="Limit length of flex band">1000.0</param>
|
||||||
|
<param name="Mode_Debug" type="bool" gui-text="Debugging information output">true</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_path_to_flex.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
File diff suppressed because it is too large
Load Diff
21
extensions/fablabchemnitz/box_maker_path_to_flex/meta.json
Normal file
21
extensions/fablabchemnitz/box_maker_path_to_flex/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Box Maker - Path To Flex",
|
||||||
|
"id": "fablabchemnitz.de.box_maker_path_to_flex",
|
||||||
|
"path": "box_maker_path_to_flex",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Paths To Flex",
|
||||||
|
"original_id": "fr.fablab-lannion.inkscape.Path2Flex",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://github.com/thierry7100/Path2flex/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/box_maker_path_to_flex",
|
||||||
|
"fork_url": "https://github.com/thierry7100/Path2flex",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Path+To+Flex",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/thierry7100",
|
||||||
|
"github.com/eridur-de"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
30
extensions/fablabchemnitz/jpeg_export/jpeg_export.inx
Normal file
30
extensions/fablabchemnitz/jpeg_export/jpeg_export.inx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>JPEG Export</name>
|
||||||
|
<id>fablabchemnitz.de.jpeg_export</id>
|
||||||
|
<param name="path" type="path" gui-text="Export path" gui-description="Full path to your file, e.g. 'C:\Users\Username\Documents\myimage.jpg'" filetypes="jpg" mode="file_new">C:\Users\</param>
|
||||||
|
<param name="bgcol" type="string" gui-text="Background color (leave blank for white)" gui-description="Background color hex code, e.g. '#abc123'"></param>
|
||||||
|
<param name="quality" type="int" min="0" max="100" gui-text="Quality %" gui-description="JPG compression quality">100</param>
|
||||||
|
<param name="density" type="int" min="30" max="2400" gui-text="Resolution (ppi)" gui-description="Recommended: 90 (screens) or 300 (print)">90</param>
|
||||||
|
<param name="page" type="bool" gui-text="Export whole page" gui-description="If checked, the whole page will be exported, else the selection.">true</param>
|
||||||
|
<param name="fast" type="bool" gui-text="Fast export (suggested)" gui-description="Will use an approximate bounding box. If unchecked, export will take longer.">true</param>
|
||||||
|
<label appearance="header">Usage</label>
|
||||||
|
<label xml:space="preserve">Select the objects in the drawing that you wish to export, or make a check at "Export whole page".
|
||||||
|
|
||||||
|
Enter a name for your JPG file (with full path) and choose a background color for the exported image (JPG format does not support transparency).
|
||||||
|
|
||||||
|
Leave background color field blank for white.
|
||||||
|
|
||||||
|
This extension requires that imagemagick is installed, more info and download at http://www.imagemagick.org.</label>
|
||||||
|
<effect needs-live-preview="false">
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Import/Export/Transfer"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">jpeg_export.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
201
extensions/fablabchemnitz/jpeg_export/jpeg_export.py
Normal file
201
extensions/fablabchemnitz/jpeg_export/jpeg_export.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
#!/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, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# Author: Giacomo Mirabassi <giacomo@mirabassi.it>
|
||||||
|
# Version: 0.2
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import math
|
||||||
|
import inkex
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
inkex.localization.localize
|
||||||
|
|
||||||
|
class JPEGExport(inkex.EffectExtension):
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("--path", default="")
|
||||||
|
pars.add_argument("--bgcol", default="#ffffff")
|
||||||
|
pars.add_argument("--quality",type=int, default="90")
|
||||||
|
pars.add_argument("--density", type=int, default="90")
|
||||||
|
pars.add_argument("--page", type=inkex.Boolean, default=False)
|
||||||
|
pars.add_argument("--fast", type=inkex.Boolean, default=True)
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
"""get selected item coords and call command line command to export as a png"""
|
||||||
|
# The user must supply a directory to export:
|
||||||
|
if not self.options.path:
|
||||||
|
inkex.errormsg(_('Please indicate a file name and path to export the jpg.'))
|
||||||
|
exit()
|
||||||
|
if not os.path.basename(self.options.path):
|
||||||
|
inkex.errormsg(_('Please indicate a file name.'))
|
||||||
|
exit()
|
||||||
|
if not os.path.dirname(self.options.path):
|
||||||
|
inkex.errormsg(_('Please indicate a directory other than your system\'s base directory.'))
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Test if the directory exists and filename is valid:
|
||||||
|
filebase = os.path.dirname(self.options.path)
|
||||||
|
if not os.path.exists(filebase):
|
||||||
|
inkex.errormsg(_('The directory "%s" does not exist.') % filebase)
|
||||||
|
exit()
|
||||||
|
filename = os.path.splitext(os.path.basename(self.options.path))
|
||||||
|
filename_base = filename[0]
|
||||||
|
filename_ending = filename[1]
|
||||||
|
if self.get_valid_filename(filename_base) != filename_base:
|
||||||
|
inkex.errormsg(_('The file name "%s" is invalid.') % filename_base)
|
||||||
|
return
|
||||||
|
if filename_ending != 'jpg' or filename_ending != 'jpeg':
|
||||||
|
filename_ending = 'jpg'
|
||||||
|
outfile = os.path.join(filebase, filename_base + '.' + filename_ending)
|
||||||
|
|
||||||
|
shutil.copy(self.options.input_file, self.options.input_file + ".svg") #make a file copy with file ending to suppress import warnings
|
||||||
|
curfile = self.options.input_file + ".svg"
|
||||||
|
#inkex.utils.debug("curfile:" + curfile)
|
||||||
|
|
||||||
|
# Test if color is valid
|
||||||
|
_rgbhexstring = re.compile(r'#[a-fA-F0-9]{6}$')
|
||||||
|
if not _rgbhexstring.match(self.options.bgcol):
|
||||||
|
inkex.errormsg(_('Please indicate the background color like this: \"#abc123\" or leave the field empty for white.'))
|
||||||
|
exit()
|
||||||
|
|
||||||
|
bgcol = self.options.bgcol
|
||||||
|
|
||||||
|
if self.options.page == False:
|
||||||
|
if len(self.svg.selected) == 0:
|
||||||
|
inkex.errormsg(_('Please select something.'))
|
||||||
|
exit()
|
||||||
|
|
||||||
|
coords=self.processSelected()
|
||||||
|
self.exportArea(int(coords[0]),int(coords[1]),int(coords[2]),int(coords[3]),curfile,outfile,bgcol)
|
||||||
|
|
||||||
|
elif self.options.page == True:
|
||||||
|
self.exportPage(curfile,outfile,bgcol)
|
||||||
|
|
||||||
|
def processSelected(self):
|
||||||
|
"""Iterate trough nodes and find the bounding coordinates of the selected area"""
|
||||||
|
startx=None
|
||||||
|
starty=None
|
||||||
|
endx=None
|
||||||
|
endy=None
|
||||||
|
nodelist=[]
|
||||||
|
root=self.document.getroot();
|
||||||
|
toty=self.svg.unittouu(root.attrib['height'])
|
||||||
|
scale = self.svg.unittouu('1px')
|
||||||
|
props=['x', 'y', 'width', 'height']
|
||||||
|
|
||||||
|
for id in self.svg.selected:
|
||||||
|
if self.options.fast == True:
|
||||||
|
nodelist.append(self.svg.getElementById(id))
|
||||||
|
else: # uses command line
|
||||||
|
rawprops=[]
|
||||||
|
for prop in props:
|
||||||
|
command=("inkscape", "--query-id", id, "--query-"+prop, self.options.input_file)
|
||||||
|
proc=subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
|
||||||
|
proc.wait()
|
||||||
|
rawprops.append(math.ceil(self.svg.unittouu(proc.stdout.read())))
|
||||||
|
proc.stdout.close()
|
||||||
|
proc.stderr.close()
|
||||||
|
nodeEndX = rawprops[0] + rawprops[2]
|
||||||
|
nodeStartY = toty - rawprops[1] - rawprops[3]
|
||||||
|
nodeEndY = toty - rawprops[1]
|
||||||
|
|
||||||
|
if rawprops[0] < startx or startx is None:
|
||||||
|
startx = rawprops[0]
|
||||||
|
|
||||||
|
if nodeStartY < starty or starty is None:
|
||||||
|
starty = nodeStartY
|
||||||
|
|
||||||
|
if nodeEndX > endx or endx is None:
|
||||||
|
endx = nodeEndX
|
||||||
|
|
||||||
|
if nodeEndY > endy or endy is None:
|
||||||
|
endy = nodeEndY
|
||||||
|
|
||||||
|
|
||||||
|
if self.options.fast == True:
|
||||||
|
bbox = sum([node.bounding_box() for node in nodelist], None)
|
||||||
|
#inkex.utils.debug(bbox) - see transform.py
|
||||||
|
'''
|
||||||
|
width = property(lambda self: self.x.size)
|
||||||
|
height = property(lambda self: self.y.size)
|
||||||
|
top = property(lambda self: self.y.minimum)
|
||||||
|
left = property(lambda self: self.x.minimum)
|
||||||
|
bottom = property(lambda self: self.y.maximum)
|
||||||
|
right = property(lambda self: self.x.maximum)
|
||||||
|
center_x = property(lambda self: self.x.center)
|
||||||
|
center_y = property(lambda self: self.y.center)
|
||||||
|
'''
|
||||||
|
startx = math.ceil(bbox.left)
|
||||||
|
endx = math.ceil(bbox.right)
|
||||||
|
h = -bbox.top + bbox.bottom
|
||||||
|
starty = toty - math.ceil(bbox.top) -h
|
||||||
|
endy = toty - math.ceil(bbox.top)
|
||||||
|
|
||||||
|
coords = [startx / scale, starty / scale, endx / scale, endy / scale]
|
||||||
|
return coords
|
||||||
|
|
||||||
|
def exportArea(self, x0, y0, x1, y1, curfile, outfile, bgcol):
|
||||||
|
tmp = self.getTmpPath()
|
||||||
|
command="inkscape --export-area %s:%s:%s:%s -d %s --export-filename \"%sjpinkexp.png\" -b \"%s\" \"%s\"" % (x0, y0, x1, y1, self.options.density, tmp, bgcol, curfile)
|
||||||
|
p = subprocess.Popen(command, shell=True)
|
||||||
|
return_code = p.wait()
|
||||||
|
self.tojpeg(outfile)
|
||||||
|
#inkex.utils.debug("command:" + command)
|
||||||
|
#inkex.utils.debug("Errorcode:" + str(return_code))
|
||||||
|
|
||||||
|
def exportPage(self, curfile, outfile, bgcol):
|
||||||
|
tmp = self.getTmpPath()
|
||||||
|
command = "inkscape --export-area-drawing -d %s --export-filename \"%sjpinkexp.png\" -b \"%s\" \"%s\"" % (self.options.density, tmp, bgcol, curfile)
|
||||||
|
p = subprocess.Popen(command, shell=True)
|
||||||
|
return_code = p.wait()
|
||||||
|
self.tojpeg(outfile)
|
||||||
|
#inkex.utils.debug("command:" + command)
|
||||||
|
#inkex.utils.debug("Errorcode:" + str(return_code))
|
||||||
|
|
||||||
|
def tojpeg(self, outfile):
|
||||||
|
tmp = self.getTmpPath()
|
||||||
|
if os.name == 'nt':
|
||||||
|
outfile = outfile.replace("\\","\\\\")
|
||||||
|
# set the ImageMagick command to run based on what's installed
|
||||||
|
if shutil.which('magick'):
|
||||||
|
command = "magick \"%sjpinkexp.png\" -sampling-factor 4:4:4 -strip -interlace JPEG -colorspace RGB -quality %s -density %s \"%s\" " % (tmp, self.options.quality, self.options.density, outfile)
|
||||||
|
# inkex.utils.debug(command)
|
||||||
|
elif shutil.which('convert'):
|
||||||
|
command = "convert \"%sjpinkexp.png\" -sampling-factor 4:4:4 -strip -interlace JPEG -colorspace RGB -quality %s -density %s \"%s\" " % (tmp, self.options.quality, self.options.density, outfile)
|
||||||
|
# inkex.utils.debug(command)
|
||||||
|
else:
|
||||||
|
inkex.errormsg(_('ImageMagick does not appear to be installed.'))
|
||||||
|
exit()
|
||||||
|
p = subprocess.Popen(command, shell=True)
|
||||||
|
return_code = p.wait()
|
||||||
|
#inkex.utils.debug("command:" + command)
|
||||||
|
#inkex.utils.debug("Errorcode:" + str(return_code))
|
||||||
|
|
||||||
|
def getTmpPath(self):
|
||||||
|
"""Define the temporary folder path depending on the operating system"""
|
||||||
|
if os.name == 'nt':
|
||||||
|
return os.getenv('TEMP') + '\\'
|
||||||
|
else:
|
||||||
|
return '/tmp/'
|
||||||
|
|
||||||
|
def get_valid_filename(self, s):
|
||||||
|
s = str(s).strip().replace(" ", "_")
|
||||||
|
return re.sub(r"(?u)[^-\w.]", "", s)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
JPEGExport().run()
|
21
extensions/fablabchemnitz/jpeg_export/meta.json
Normal file
21
extensions/fablabchemnitz/jpeg_export/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "JPEG Export",
|
||||||
|
"id": "fablabchemnitz.de.jpeg_export",
|
||||||
|
"path": "jpeg_export",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "JPEG Export",
|
||||||
|
"original_id": "id.giac.export.jpg",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://github.com/giacmir/Inkscape-JPEG-export-extension/blob/master/jpegexport.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/jpeg_export",
|
||||||
|
"fork_url": "https://github.com/giacmir/Inkscape-JPEG-export-extension",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/JPEG+Export",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/giacmir",
|
||||||
|
"github.com/eridur-de"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>LaserDraw Export (lyz)</name>
|
||||||
|
<id>fablabchemnitz.de.laserdraw_export.lyz</id>
|
||||||
|
<!--
|
||||||
|
<param name="area_select" type="optiongroup" appearance="combo" gui-text="Area: ">
|
||||||
|
<option value="page_area">Page Area (use for engrave and cut)</option>
|
||||||
|
<option value="object_area">Object Area (for single operations only)</option>
|
||||||
|
</param>
|
||||||
|
-->
|
||||||
|
<label>Formatting can be used to break a design into separate cutting and engraving files. Red lines indicate cutting, blue lines indicate engraving, black indicates raster engraving.</label>
|
||||||
|
<param name="cut_select" type="optiongroup" appearance="combo" gui-text="Type: ">
|
||||||
|
<option value="vector_red">Vector Cuts (export red lines)</option>
|
||||||
|
<option value="vector_blue">Vector Engrave (export blue lines)</option>
|
||||||
|
<option value="raster">Raster Engrave (export everything else)</option>
|
||||||
|
<option value="all">All (export vectors and raster engrave to one file)</option>
|
||||||
|
<option value="image">Image (export all items as raster)</option>
|
||||||
|
</param>
|
||||||
|
<param name="resolution" type="int" min="100" max="1000" gui-text="Raster Image Resolution">1000</param>
|
||||||
|
<param name="margin" type="float" precision="1" min="0" max="9999" gui-text="Laser Draw Margin Size">2.0</param>
|
||||||
|
<param name="txt2paths" type="bool" gui-text="Convert Text to Paths">false</param>
|
||||||
|
<param name="inkscape_version" type="optiongroup" appearance="combo" gui-text="Inkscape Version:">
|
||||||
|
<option value="100">1.00 or Newer</option>
|
||||||
|
<option value="92">0.92 to 0.99</option>
|
||||||
|
<option value="91">0.91 or Older</option>
|
||||||
|
</param>
|
||||||
|
<output>
|
||||||
|
<extension>.lyz</extension>
|
||||||
|
<mimetype>image/lyz</mimetype>
|
||||||
|
<filetypename>Laser Draw LYZ (*.lyz)</filetypename>
|
||||||
|
<filetypetooltip>LaserDraw LYZ Output</filetypetooltip>
|
||||||
|
<dataloss>true</dataloss>
|
||||||
|
</output>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">lyz_export.py</command>
|
||||||
|
<helper_extension>org.inkscape.output.svg.inkscape</helper_extension>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
286
extensions/fablabchemnitz/laserdraw_export/lyz_bezmisc.py
Normal file
286
extensions/fablabchemnitz/laserdraw_export/lyz_bezmisc.py
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru
|
||||||
|
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import math
|
||||||
|
import cmath
|
||||||
|
|
||||||
|
def rootWrapper(a,b,c,d):
|
||||||
|
if a:
|
||||||
|
# Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots
|
||||||
|
a,b,c = (b/a, c/a, d/a)
|
||||||
|
m = 2.0*a**3 - 9.0*a*b + 27.0*c
|
||||||
|
k = a**2 - 3.0*b
|
||||||
|
n = m**2 - 4.0*k**3
|
||||||
|
w1 = -.5 + .5*cmath.sqrt(-3.0)
|
||||||
|
w2 = -.5 - .5*cmath.sqrt(-3.0)
|
||||||
|
if n < 0:
|
||||||
|
m1 = pow(complex((m+cmath.sqrt(n))/2),1./3)
|
||||||
|
n1 = pow(complex((m-cmath.sqrt(n))/2),1./3)
|
||||||
|
else:
|
||||||
|
if m+math.sqrt(n) < 0:
|
||||||
|
m1 = -pow(-(m+math.sqrt(n))/2,1./3)
|
||||||
|
else:
|
||||||
|
m1 = pow((m+math.sqrt(n))/2,1./3)
|
||||||
|
if m-math.sqrt(n) < 0:
|
||||||
|
n1 = -pow(-(m-math.sqrt(n))/2,1./3)
|
||||||
|
else:
|
||||||
|
n1 = pow((m-math.sqrt(n))/2,1./3)
|
||||||
|
x1 = -1./3 * (a + m1 + n1)
|
||||||
|
x2 = -1./3 * (a + w1*m1 + w2*n1)
|
||||||
|
x3 = -1./3 * (a + w2*m1 + w1*n1)
|
||||||
|
return (x1,x2,x3)
|
||||||
|
elif b:
|
||||||
|
det=c**2.0-4.0*b*d
|
||||||
|
if det:
|
||||||
|
return (-c+cmath.sqrt(det))/(2.0*b),(-c-cmath.sqrt(det))/(2.0*b)
|
||||||
|
else:
|
||||||
|
return -c/(2.0*b),
|
||||||
|
elif c:
|
||||||
|
return 1.0*(-d/c),
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def bezierparameterize(xxx_todo_changeme):
|
||||||
|
#parametric bezier
|
||||||
|
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme
|
||||||
|
x0=bx0
|
||||||
|
y0=by0
|
||||||
|
cx=3*(bx1-x0)
|
||||||
|
bx=3*(bx2-bx1)-cx
|
||||||
|
ax=bx3-x0-cx-bx
|
||||||
|
cy=3*(by1-y0)
|
||||||
|
by=3*(by2-by1)-cy
|
||||||
|
ay=by3-y0-cy-by
|
||||||
|
|
||||||
|
return ax,ay,bx,by,cx,cy,x0,y0
|
||||||
|
#ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
|
||||||
|
|
||||||
|
def linebezierintersect(xxx_todo_changeme1, xxx_todo_changeme2):
|
||||||
|
#parametric line
|
||||||
|
((lx1,ly1),(lx2,ly2)) = xxx_todo_changeme1
|
||||||
|
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme2
|
||||||
|
dd=lx1
|
||||||
|
cc=lx2-lx1
|
||||||
|
bb=ly1
|
||||||
|
aa=ly2-ly1
|
||||||
|
|
||||||
|
if aa:
|
||||||
|
coef1=cc/aa
|
||||||
|
coef2=1
|
||||||
|
else:
|
||||||
|
coef1=1
|
||||||
|
coef2=aa/cc
|
||||||
|
|
||||||
|
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
|
||||||
|
#cubic intersection coefficients
|
||||||
|
a=coef1*ay-coef2*ax
|
||||||
|
b=coef1*by-coef2*bx
|
||||||
|
c=coef1*cy-coef2*cx
|
||||||
|
d=coef1*(y0-bb)-coef2*(x0-dd)
|
||||||
|
|
||||||
|
roots = rootWrapper(a,b,c,d)
|
||||||
|
retval = []
|
||||||
|
for i in roots:
|
||||||
|
if type(i) is complex and i.imag==0:
|
||||||
|
i = i.real
|
||||||
|
if type(i) is not complex and 0<=i<=1:
|
||||||
|
retval.append(bezierpointatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),i))
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def bezierpointatt(xxx_todo_changeme3,t):
|
||||||
|
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme3
|
||||||
|
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
|
||||||
|
x=ax*(t**3)+bx*(t**2)+cx*t+x0
|
||||||
|
y=ay*(t**3)+by*(t**2)+cy*t+y0
|
||||||
|
return x,y
|
||||||
|
|
||||||
|
def bezierslopeatt(xxx_todo_changeme4,t):
|
||||||
|
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme4
|
||||||
|
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
|
||||||
|
dx=3*ax*(t**2)+2*bx*t+cx
|
||||||
|
dy=3*ay*(t**2)+2*by*t+cy
|
||||||
|
return dx,dy
|
||||||
|
|
||||||
|
def beziertatslope(xxx_todo_changeme5, xxx_todo_changeme6):
|
||||||
|
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme5
|
||||||
|
(dy,dx) = xxx_todo_changeme6
|
||||||
|
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
|
||||||
|
#quadratic coefficents of slope formula
|
||||||
|
if dx:
|
||||||
|
slope = 1.0*(dy/dx)
|
||||||
|
a=3*ay-3*ax*slope
|
||||||
|
b=2*by-2*bx*slope
|
||||||
|
c=cy-cx*slope
|
||||||
|
elif dy:
|
||||||
|
slope = 1.0*(dx/dy)
|
||||||
|
a=3*ax-3*ay*slope
|
||||||
|
b=2*bx-2*by*slope
|
||||||
|
c=cx-cy*slope
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
roots = rootWrapper(0,a,b,c)
|
||||||
|
retval = []
|
||||||
|
for i in roots:
|
||||||
|
if type(i) is complex and i.imag==0:
|
||||||
|
i = i.real
|
||||||
|
if type(i) is not complex and 0<=i<=1:
|
||||||
|
retval.append(i)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def tpoint(xxx_todo_changeme7, xxx_todo_changeme8,t):
|
||||||
|
(x1,y1) = xxx_todo_changeme7
|
||||||
|
(x2,y2) = xxx_todo_changeme8
|
||||||
|
return x1+t*(x2-x1),y1+t*(y2-y1)
|
||||||
|
def beziersplitatt(xxx_todo_changeme9,t):
|
||||||
|
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme9
|
||||||
|
m1=tpoint((bx0,by0),(bx1,by1),t)
|
||||||
|
m2=tpoint((bx1,by1),(bx2,by2),t)
|
||||||
|
m3=tpoint((bx2,by2),(bx3,by3),t)
|
||||||
|
m4=tpoint(m1,m2,t)
|
||||||
|
m5=tpoint(m2,m3,t)
|
||||||
|
m=tpoint(m4,m5,t)
|
||||||
|
|
||||||
|
return ((bx0,by0),m1,m4,m),(m,m5,m3,(bx3,by3))
|
||||||
|
|
||||||
|
'''
|
||||||
|
Approximating the arc length of a bezier curve
|
||||||
|
according to <http://www.cit.gu.edu.au/~anthony/info/graphics/bezier.curves>
|
||||||
|
|
||||||
|
if:
|
||||||
|
L1 = |P0 P1| +|P1 P2| +|P2 P3|
|
||||||
|
L0 = |P0 P3|
|
||||||
|
then:
|
||||||
|
L = 1/2*L0 + 1/2*L1
|
||||||
|
ERR = L1-L0
|
||||||
|
ERR approaches 0 as the number of subdivisions (m) increases
|
||||||
|
2^-4m
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
Jens Gravesen <gravesen@mat.dth.dk>
|
||||||
|
"Adaptive subdivision and the length of Bezier curves"
|
||||||
|
mat-report no. 1992-10, Mathematical Institute, The Technical
|
||||||
|
University of Denmark.
|
||||||
|
'''
|
||||||
|
def pointdistance(xxx_todo_changeme10, xxx_todo_changeme11):
|
||||||
|
(x1,y1) = xxx_todo_changeme10
|
||||||
|
(x2,y2) = xxx_todo_changeme11
|
||||||
|
return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2))
|
||||||
|
def Gravesen_addifclose(b, len, error = 0.001):
|
||||||
|
box = 0
|
||||||
|
for i in range(1,4):
|
||||||
|
box += pointdistance(b[i-1], b[i])
|
||||||
|
chord = pointdistance(b[0], b[3])
|
||||||
|
if (box - chord) > error:
|
||||||
|
first, second = beziersplitatt(b, 0.5)
|
||||||
|
Gravesen_addifclose(first, len, error)
|
||||||
|
Gravesen_addifclose(second, len, error)
|
||||||
|
else:
|
||||||
|
len[0] += (box / 2.0) + (chord / 2.0)
|
||||||
|
def bezierlengthGravesen(b, error = 0.001):
|
||||||
|
len = [0]
|
||||||
|
Gravesen_addifclose(b, len, error)
|
||||||
|
return len[0]
|
||||||
|
|
||||||
|
# balf = Bezier Arc Length Function
|
||||||
|
balfax,balfbx,balfcx,balfay,balfby,balfcy = 0,0,0,0,0,0
|
||||||
|
def balf(t):
|
||||||
|
retval = (balfax*(t**2) + balfbx*t + balfcx)**2 + (balfay*(t**2) + balfby*t + balfcy)**2
|
||||||
|
return math.sqrt(retval)
|
||||||
|
|
||||||
|
def Simpson(f, a, b, n_limit, tolerance):
|
||||||
|
n = 2
|
||||||
|
multiplier = (b - a)/6.0
|
||||||
|
endsum = f(a) + f(b)
|
||||||
|
interval = (b - a)/2.0
|
||||||
|
asum = 0.0
|
||||||
|
bsum = f(a + interval)
|
||||||
|
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
|
||||||
|
est0 = 2.0 * est1
|
||||||
|
#print multiplier, endsum, interval, asum, bsum, est1, est0
|
||||||
|
while n < n_limit and abs(est1 - est0) > tolerance:
|
||||||
|
n *= 2
|
||||||
|
multiplier /= 2.0
|
||||||
|
interval /= 2.0
|
||||||
|
asum += bsum
|
||||||
|
bsum = 0.0
|
||||||
|
est0 = est1
|
||||||
|
for i in range(1, n, 2):
|
||||||
|
bsum += f(a + (i * interval))
|
||||||
|
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
|
||||||
|
#print multiplier, endsum, interval, asum, bsum, est1, est0
|
||||||
|
return est1
|
||||||
|
|
||||||
|
def bezierlengthSimpson(xxx_todo_changeme12, tolerance = 0.001):
|
||||||
|
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme12
|
||||||
|
global balfax,balfbx,balfcx,balfay,balfby,balfcy
|
||||||
|
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
|
||||||
|
balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy
|
||||||
|
return Simpson(balf, 0.0, 1.0, 4096, tolerance)
|
||||||
|
|
||||||
|
def beziertatlength(xxx_todo_changeme13, l = 0.5, tolerance = 0.001):
|
||||||
|
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme13
|
||||||
|
global balfax,balfbx,balfcx,balfay,balfby,balfcy
|
||||||
|
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
|
||||||
|
balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy
|
||||||
|
t = 1.0
|
||||||
|
tdiv = t
|
||||||
|
curlen = Simpson(balf, 0.0, t, 4096, tolerance)
|
||||||
|
targetlen = l * curlen
|
||||||
|
diff = curlen - targetlen
|
||||||
|
while abs(diff) > tolerance:
|
||||||
|
tdiv /= 2.0
|
||||||
|
if diff < 0:
|
||||||
|
t += tdiv
|
||||||
|
else:
|
||||||
|
t -= tdiv
|
||||||
|
curlen = Simpson(balf, 0.0, t, 4096, tolerance)
|
||||||
|
diff = curlen - targetlen
|
||||||
|
return t
|
||||||
|
|
||||||
|
#default bezier length method
|
||||||
|
bezierlength = bezierlengthSimpson
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import timing
|
||||||
|
#print linebezierintersect(((,),(,)),((,),(,),(,),(,)))
|
||||||
|
#print linebezierintersect(((0,1),(0,-1)),((-1,0),(-.5,0),(.5,0),(1,0)))
|
||||||
|
tol = 0.00000001
|
||||||
|
curves = [((0,0),(1,5),(4,5),(5,5)),
|
||||||
|
((0,0),(0,0),(5,0),(10,0)),
|
||||||
|
((0,0),(0,0),(5,1),(10,0)),
|
||||||
|
((-10,0),(0,0),(10,0),(10,10)),
|
||||||
|
((15,10),(0,0),(10,0),(-5,10))]
|
||||||
|
'''
|
||||||
|
for curve in curves:
|
||||||
|
timing.start()
|
||||||
|
g = bezierlengthGravesen(curve,tol)
|
||||||
|
timing.finish()
|
||||||
|
gt = timing.micro()
|
||||||
|
|
||||||
|
timing.start()
|
||||||
|
s = bezierlengthSimpson(curve,tol)
|
||||||
|
timing.finish()
|
||||||
|
st = timing.micro()
|
||||||
|
|
||||||
|
print g, gt
|
||||||
|
print s, st
|
||||||
|
'''
|
||||||
|
for curve in curves:
|
||||||
|
print(beziertatlength(curve,0.5))
|
35
extensions/fablabchemnitz/laserdraw_export/lyz_cspsubdiv.py
Normal file
35
extensions/fablabchemnitz/laserdraw_export/lyz_cspsubdiv.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from lyz_bezmisc import *
|
||||||
|
from lyz_ffgeom import *
|
||||||
|
|
||||||
|
def maxdist(xxx_todo_changeme):
|
||||||
|
((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y)) = xxx_todo_changeme
|
||||||
|
p0 = Point(p0x,p0y)
|
||||||
|
p1 = Point(p1x,p1y)
|
||||||
|
p2 = Point(p2x,p2y)
|
||||||
|
p3 = Point(p3x,p3y)
|
||||||
|
|
||||||
|
s1 = Segment(p0,p3)
|
||||||
|
return max(s1.distanceToPoint(p1),s1.distanceToPoint(p2))
|
||||||
|
|
||||||
|
def cspsubdiv(csp,flat):
|
||||||
|
for sp in csp:
|
||||||
|
subdiv(sp,flat)
|
||||||
|
|
||||||
|
def subdiv(sp,flat,i=1):
|
||||||
|
while i < len(sp):
|
||||||
|
p0 = sp[i-1][1]
|
||||||
|
p1 = sp[i-1][2]
|
||||||
|
p2 = sp[i][0]
|
||||||
|
p3 = sp[i][1]
|
||||||
|
|
||||||
|
b = (p0,p1,p2,p3)
|
||||||
|
m = maxdist(b)
|
||||||
|
if m <= flat:
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
one, two = beziersplitatt(b,0.5)
|
||||||
|
sp[i-1][2] = one[1]
|
||||||
|
sp[i][0] = two[2]
|
||||||
|
p = [one[2],one[3],two[1]]
|
||||||
|
sp[i:1] = [p]
|
166
extensions/fablabchemnitz/laserdraw_export/lyz_cubicsuperpath.py
Normal file
166
extensions/fablabchemnitz/laserdraw_export/lyz_cubicsuperpath.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
cubicsuperpath.py
|
||||||
|
|
||||||
|
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import lyz_simplepath as simplepath
|
||||||
|
from math import *
|
||||||
|
|
||||||
|
def matprod(mlist):
|
||||||
|
prod=mlist[0]
|
||||||
|
for m in mlist[1:]:
|
||||||
|
a00=prod[0][0]*m[0][0]+prod[0][1]*m[1][0]
|
||||||
|
a01=prod[0][0]*m[0][1]+prod[0][1]*m[1][1]
|
||||||
|
a10=prod[1][0]*m[0][0]+prod[1][1]*m[1][0]
|
||||||
|
a11=prod[1][0]*m[0][1]+prod[1][1]*m[1][1]
|
||||||
|
prod=[[a00,a01],[a10,a11]]
|
||||||
|
return prod
|
||||||
|
def rotmat(teta):
|
||||||
|
return [[cos(teta),-sin(teta)],[sin(teta),cos(teta)]]
|
||||||
|
def applymat(mat, pt):
|
||||||
|
x=mat[0][0]*pt[0]+mat[0][1]*pt[1]
|
||||||
|
y=mat[1][0]*pt[0]+mat[1][1]*pt[1]
|
||||||
|
pt[0]=x
|
||||||
|
pt[1]=y
|
||||||
|
def norm(pt):
|
||||||
|
return sqrt(pt[0]*pt[0]+pt[1]*pt[1])
|
||||||
|
|
||||||
|
def ArcToPath(p1,params):
|
||||||
|
A=p1[:]
|
||||||
|
rx,ry,teta,longflag,sweepflag,x2,y2=params[:]
|
||||||
|
teta = teta*pi/180.0
|
||||||
|
B=[x2,y2]
|
||||||
|
if rx==0 or ry==0 or A==B:
|
||||||
|
return([[A[:],A[:],A[:]],[B[:],B[:],B[:]]])
|
||||||
|
mat=matprod((rotmat(teta),[[1/rx,0],[0,1/ry]],rotmat(-teta)))
|
||||||
|
applymat(mat, A)
|
||||||
|
applymat(mat, B)
|
||||||
|
k=[-(B[1]-A[1]),B[0]-A[0]]
|
||||||
|
d=k[0]*k[0]+k[1]*k[1]
|
||||||
|
k[0]/=sqrt(d)
|
||||||
|
k[1]/=sqrt(d)
|
||||||
|
d=sqrt(max(0,1-d/4))
|
||||||
|
if longflag==sweepflag:
|
||||||
|
d*=-1
|
||||||
|
O=[(B[0]+A[0])/2+d*k[0],(B[1]+A[1])/2+d*k[1]]
|
||||||
|
OA=[A[0]-O[0],A[1]-O[1]]
|
||||||
|
OB=[B[0]-O[0],B[1]-O[1]]
|
||||||
|
start=acos(OA[0]/norm(OA))
|
||||||
|
if OA[1]<0:
|
||||||
|
start*=-1
|
||||||
|
end=acos(OB[0]/norm(OB))
|
||||||
|
if OB[1]<0:
|
||||||
|
end*=-1
|
||||||
|
|
||||||
|
if sweepflag and start>end:
|
||||||
|
end +=2*pi
|
||||||
|
if (not sweepflag) and start<end:
|
||||||
|
end -=2*pi
|
||||||
|
|
||||||
|
NbSectors=int(abs(start-end)*2/pi)+1
|
||||||
|
dTeta=(end-start)/NbSectors
|
||||||
|
#v=dTeta*2/pi*0.552
|
||||||
|
#v=dTeta*2/pi*4*(sqrt(2)-1)/3
|
||||||
|
v = 4*tan(dTeta/4)/3
|
||||||
|
#if not sweepflag:
|
||||||
|
# v*=-1
|
||||||
|
p=[]
|
||||||
|
for i in range(0,NbSectors+1,1):
|
||||||
|
angle=start+i*dTeta
|
||||||
|
v1=[O[0]+cos(angle)-(-v)*sin(angle),O[1]+sin(angle)+(-v)*cos(angle)]
|
||||||
|
pt=[O[0]+cos(angle) ,O[1]+sin(angle) ]
|
||||||
|
v2=[O[0]+cos(angle)- v *sin(angle),O[1]+sin(angle)+ v *cos(angle)]
|
||||||
|
p.append([v1,pt,v2])
|
||||||
|
p[ 0][0]=p[ 0][1][:]
|
||||||
|
p[-1][2]=p[-1][1][:]
|
||||||
|
|
||||||
|
mat=matprod((rotmat(teta),[[rx,0],[0,ry]],rotmat(-teta)))
|
||||||
|
for pts in p:
|
||||||
|
applymat(mat, pts[0])
|
||||||
|
applymat(mat, pts[1])
|
||||||
|
applymat(mat, pts[2])
|
||||||
|
return(p)
|
||||||
|
|
||||||
|
def CubicSuperPath(simplepath):
|
||||||
|
csp = []
|
||||||
|
subpath = -1
|
||||||
|
subpathstart = []
|
||||||
|
last = []
|
||||||
|
lastctrl = []
|
||||||
|
for s in simplepath:
|
||||||
|
cmd, params = s
|
||||||
|
if cmd == 'M':
|
||||||
|
if last:
|
||||||
|
csp[subpath].append([lastctrl[:],last[:],last[:]])
|
||||||
|
subpath += 1
|
||||||
|
csp.append([])
|
||||||
|
subpathstart = params[:]
|
||||||
|
last = params[:]
|
||||||
|
lastctrl = params[:]
|
||||||
|
elif cmd == 'L':
|
||||||
|
csp[subpath].append([lastctrl[:],last[:],last[:]])
|
||||||
|
last = params[:]
|
||||||
|
lastctrl = params[:]
|
||||||
|
elif cmd == 'C':
|
||||||
|
csp[subpath].append([lastctrl[:],last[:],params[:2]])
|
||||||
|
last = params[-2:]
|
||||||
|
lastctrl = params[2:4]
|
||||||
|
elif cmd == 'Q':
|
||||||
|
q0=last[:]
|
||||||
|
q1=params[0:2]
|
||||||
|
q2=params[2:4]
|
||||||
|
x0= q0[0]
|
||||||
|
x1=1./3*q0[0]+2./3*q1[0]
|
||||||
|
x2= 2./3*q1[0]+1./3*q2[0]
|
||||||
|
x3= q2[0]
|
||||||
|
y0= q0[1]
|
||||||
|
y1=1./3*q0[1]+2./3*q1[1]
|
||||||
|
y2= 2./3*q1[1]+1./3*q2[1]
|
||||||
|
y3= q2[1]
|
||||||
|
csp[subpath].append([lastctrl[:],[x0,y0],[x1,y1]])
|
||||||
|
last = [x3,y3]
|
||||||
|
lastctrl = [x2,y2]
|
||||||
|
elif cmd == 'A':
|
||||||
|
arcp=ArcToPath(last[:],params[:])
|
||||||
|
arcp[ 0][0]=lastctrl[:]
|
||||||
|
last=arcp[-1][1]
|
||||||
|
lastctrl = arcp[-1][0]
|
||||||
|
csp[subpath]+=arcp[:-1]
|
||||||
|
elif cmd == 'Z':
|
||||||
|
csp[subpath].append([lastctrl[:],last[:],last[:]])
|
||||||
|
last = subpathstart[:]
|
||||||
|
lastctrl = subpathstart[:]
|
||||||
|
#append final superpoint
|
||||||
|
csp[subpath].append([lastctrl[:],last[:],last[:]])
|
||||||
|
return csp
|
||||||
|
|
||||||
|
def unCubicSuperPath(csp):
|
||||||
|
a = []
|
||||||
|
for subpath in csp:
|
||||||
|
if subpath:
|
||||||
|
a.append(['M',subpath[0][1][:]])
|
||||||
|
for i in range(1,len(subpath)):
|
||||||
|
a.append(['C',subpath[i-1][2][:] + subpath[i][0][:] + subpath[i][1][:]])
|
||||||
|
return a
|
||||||
|
|
||||||
|
def parsePath(d):
|
||||||
|
return CubicSuperPath(simplepath.parsePath(d))
|
||||||
|
|
||||||
|
def formatPath(p):
|
||||||
|
return simplepath.formatPath(unCubicSuperPath(p))
|
571
extensions/fablabchemnitz/laserdraw_export/lyz_export.py
Normal file
571
extensions/fablabchemnitz/laserdraw_export/lyz_export.py
Normal file
@ -0,0 +1,571 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
This file output script for Inkscape creates Laser Draw (LaserDRW) LYZ files.
|
||||||
|
|
||||||
|
File history:
|
||||||
|
0.1 Initial code (2/5/2017)
|
||||||
|
0.2 - Added support for rectangle, circle and ellipse (2/7/2017)
|
||||||
|
- Added option to automatically convert text to paths
|
||||||
|
0.3 - Fixed x,y translation when view box is used in SVG file for scaling (2/10/2017)
|
||||||
|
0.4 - Changed limits in resolution to 100 dpi minimum and 1000 dpi maximum (if you get an out of memory error in LaserDRW try reducing the resolution)
|
||||||
|
0.5 - Removed some messages that were not needed
|
||||||
|
- Fixed default resolution in inx files
|
||||||
|
0.6 - Made compatible with Python 3 and Inkscape 1.0
|
||||||
|
|
||||||
|
Copyright (C) 2017-2020 Scorch www.scorchworks.com
|
||||||
|
Derived from dxf_outlines.py by Aaron Spike and Alvin Penner
|
||||||
|
|
||||||
|
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 math
|
||||||
|
import tempfile, os, sys, shutil
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
import zipfile
|
||||||
|
import re
|
||||||
|
|
||||||
|
import lyz_inkex as inkex
|
||||||
|
import lyz_simplestyle as simplestyle
|
||||||
|
import lyz_simpletransform as simpletransform
|
||||||
|
import lyz_cubicsuperpath as cubicsuperpath
|
||||||
|
import lyz_cspsubdiv as cspsubdiv
|
||||||
|
from lyz_library import LYZ_CLASS
|
||||||
|
|
||||||
|
## Subprocess timout stuff ######
|
||||||
|
from threading import Timer
|
||||||
|
def run_external(cmd, timeout_sec):
|
||||||
|
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||||
|
kill_proc = lambda p: p.kill()
|
||||||
|
timer = Timer(timeout_sec, kill_proc, [proc])
|
||||||
|
try:
|
||||||
|
timer.start()
|
||||||
|
stdout,stderr = proc.communicate()
|
||||||
|
finally:
|
||||||
|
timer.cancel()
|
||||||
|
##################################
|
||||||
|
|
||||||
|
class LYZExport(inkex.Effect):
|
||||||
|
def __init__(self):
|
||||||
|
inkex.Effect.__init__(self)
|
||||||
|
self.flatness = 0.01
|
||||||
|
self.lines =[]
|
||||||
|
self.Cut_Type = {}
|
||||||
|
self.Xsize=40
|
||||||
|
self.Ysize=40
|
||||||
|
self.margin = 2
|
||||||
|
self.PNG_DATA = None
|
||||||
|
self.png_area = "--export-area-page"
|
||||||
|
self.timout = 60 #timeout time for external calls to Inkscape in seconds
|
||||||
|
|
||||||
|
self.OptionParser.add_option("--area_select", type="string", default="page_area")
|
||||||
|
self.OptionParser.add_option("--cut_select", type="string", default="zip")
|
||||||
|
self.OptionParser.add_option("--resolution", type="int", default=1000)
|
||||||
|
self.OptionParser.add_option("--margin", type="float", default=2.00)
|
||||||
|
self.OptionParser.add_option("--inkscape_version", type="int", default=100)
|
||||||
|
self.OptionParser.add_option("--txt2paths", type="inkbool", default=False)
|
||||||
|
|
||||||
|
self.layers = ['0']
|
||||||
|
self.layer = '0'
|
||||||
|
self.layernames = []
|
||||||
|
self.PYTHON_VERSION = sys.version_info[0]
|
||||||
|
|
||||||
|
|
||||||
|
def stream_binary_data(self,filename):
|
||||||
|
# Change the format for STDOUT to binary to support
|
||||||
|
# writing the binary output file through STDOUT
|
||||||
|
if os.name == 'nt': #if sys.platform == "win32":
|
||||||
|
try:
|
||||||
|
import msvcrt
|
||||||
|
#msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
||||||
|
msvcrt.setmode(1, os.O_BINARY)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
# Open the temporary file for reading
|
||||||
|
out = open(filename,'rb')
|
||||||
|
# Send the contents of the temp file to STDOUT
|
||||||
|
if self.PYTHON_VERSION < 3:
|
||||||
|
sys.stdout.write(out.read())
|
||||||
|
else:
|
||||||
|
sys.stdout.buffer.write(out.read())
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
|
||||||
|
def output(self):
|
||||||
|
#create OS temp folder
|
||||||
|
self.tmp_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
if (self.cut_select=="image" ):
|
||||||
|
LYZ=LYZ_CLASS()
|
||||||
|
LYZ.setup_new_header()
|
||||||
|
LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
|
||||||
|
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
|
||||||
|
LYZ.set_margin(self.margin)
|
||||||
|
image_file = os.path.join(self.tmp_dir, "image.lyz")
|
||||||
|
LYZ.write_file(image_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="all" ) or (self.cut_select=="zip" ):
|
||||||
|
LYZ=LYZ_CLASS()
|
||||||
|
LYZ.setup_new_header()
|
||||||
|
LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
|
||||||
|
for line in self.lines:
|
||||||
|
ID=line[7]
|
||||||
|
if (self.Cut_Type[ID]=="cut") or (self.Cut_Type[ID]=="engrave"):
|
||||||
|
LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
|
||||||
|
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
|
||||||
|
LYZ.set_margin(self.margin)
|
||||||
|
all_file = os.path.join(self.tmp_dir, "all.lyz")
|
||||||
|
LYZ.write_file(all_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="raster" ) or (self.cut_select=="zip" ):
|
||||||
|
LYZ=LYZ_CLASS()
|
||||||
|
LYZ.setup_new_header()
|
||||||
|
LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
|
||||||
|
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
|
||||||
|
LYZ.set_margin(self.margin)
|
||||||
|
raster_file = os.path.join(self.tmp_dir, "01_raster_engrave.lyz")
|
||||||
|
LYZ.write_file(raster_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="vector_red" ) or (self.cut_select=="zip" ):
|
||||||
|
LYZ=LYZ_CLASS()
|
||||||
|
LYZ.setup_new_header()
|
||||||
|
for line in self.lines:
|
||||||
|
ID=line[7]
|
||||||
|
if (self.Cut_Type[ID]=="cut"):
|
||||||
|
LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
|
||||||
|
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
|
||||||
|
LYZ.set_margin(self.margin)
|
||||||
|
cut_file = os.path.join(self.tmp_dir, "03_vector_cut.lyz")
|
||||||
|
LYZ.write_file(cut_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="vector_blue" ) or (self.cut_select=="zip" ):
|
||||||
|
LYZ=LYZ_CLASS()
|
||||||
|
LYZ.setup_new_header()
|
||||||
|
for line in self.lines:
|
||||||
|
ID=line[7]
|
||||||
|
if (self.Cut_Type[ID]=="engrave"):
|
||||||
|
LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
|
||||||
|
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
|
||||||
|
LYZ.set_margin(self.margin)
|
||||||
|
engrave_file = os.path.join(self.tmp_dir, "02_vector_engrave.lyz")
|
||||||
|
LYZ.write_file(engrave_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="image" ):
|
||||||
|
self.stream_binary_data(image_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="all" ):
|
||||||
|
self.stream_binary_data(all_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="raster" ):
|
||||||
|
self.stream_binary_data(raster_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="vector_red" ):
|
||||||
|
self.stream_binary_data(cut_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="vector_blue"):
|
||||||
|
self.stream_binary_data(engrave_file)
|
||||||
|
|
||||||
|
if (self.cut_select=="zip" ):
|
||||||
|
# Add image LYZ file? Encode zip file names?
|
||||||
|
zip_file = os.path.join(self.tmp_dir, "lyz_files.zip")
|
||||||
|
z = zipfile.ZipFile(zip_file, 'w')
|
||||||
|
z.write(all_file , os.path.basename(all_file) )
|
||||||
|
z.write(raster_file , os.path.basename(raster_file) )
|
||||||
|
z.write(cut_file , os.path.basename(cut_file) )
|
||||||
|
z.write(engrave_file, os.path.basename(engrave_file))
|
||||||
|
z.write(sys.argv[-1], "design.svg" )
|
||||||
|
z.close()
|
||||||
|
self.stream_binary_data(zip_file)
|
||||||
|
|
||||||
|
#Delete the temp folder and file
|
||||||
|
shutil.rmtree(self.tmp_dir)
|
||||||
|
|
||||||
|
def dxf_line(self,csp,pen_width=0.025,color=None,path_id="",layer="none"):
|
||||||
|
x1 = csp[0][0]
|
||||||
|
y1 = csp[0][1]
|
||||||
|
x2 = csp[1][0]
|
||||||
|
y2 = csp[1][1]
|
||||||
|
self.lines.append([x1,-y1,x2,-y2,layer,pen_width,color,path_id])
|
||||||
|
|
||||||
|
def colmod(self,r,g,b,path_id):
|
||||||
|
if (r,g,b) ==(255,0,0):
|
||||||
|
self.Cut_Type[path_id]="cut"
|
||||||
|
(r,g,b) = (255,255,255)
|
||||||
|
elif (r,g,b)==(0,0,255):
|
||||||
|
self.Cut_Type[path_id]="engrave"
|
||||||
|
(r,g,b) = (255,255,255)
|
||||||
|
else:
|
||||||
|
self.Cut_Type[path_id]="raster"
|
||||||
|
(r,g,b) = (0,0,0)
|
||||||
|
|
||||||
|
return '%02x%02x%02x' % (r,g,b)
|
||||||
|
|
||||||
|
def process_shape(self, node, mat):
|
||||||
|
rgb = (0,0,0)
|
||||||
|
path_id = node.get('id')
|
||||||
|
style = node.get('style')
|
||||||
|
self.Cut_Type[path_id]="raster" # Set default type to raster
|
||||||
|
|
||||||
|
color_props_fill = ('fill', 'stop-color', 'flood-color', 'lighting-color')
|
||||||
|
color_props_stroke = ('stroke',)
|
||||||
|
color_props = color_props_fill + color_props_stroke
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
## The following is ripped off from Coloreffect.py ##
|
||||||
|
#####################################################
|
||||||
|
if style:
|
||||||
|
declarations = style.split(';')
|
||||||
|
for i,decl in enumerate(declarations):
|
||||||
|
parts = decl.split(':', 2)
|
||||||
|
if len(parts) == 2:
|
||||||
|
(prop, col) = parts
|
||||||
|
prop = prop.strip().lower()
|
||||||
|
#if prop in color_props:
|
||||||
|
if prop == 'stroke':
|
||||||
|
col= col.strip()
|
||||||
|
if simplestyle.isColor(col):
|
||||||
|
c=simplestyle.parseColor(col)
|
||||||
|
new_val='#'+self.colmod(c[0],c[1],c[2],path_id)
|
||||||
|
else:
|
||||||
|
new_val = col
|
||||||
|
if new_val != col:
|
||||||
|
declarations[i] = prop + ':' + new_val
|
||||||
|
node.set('style', ';'.join(declarations))
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
if node.tag == inkex.addNS('path','svg'):
|
||||||
|
d = node.get('d')
|
||||||
|
if not d:
|
||||||
|
return
|
||||||
|
p = cubicsuperpath.parsePath(d)
|
||||||
|
elif node.tag == inkex.addNS('rect','svg'):
|
||||||
|
x = float(node.get('x'))
|
||||||
|
y = float(node.get('y'))
|
||||||
|
width = float(node.get('width'))
|
||||||
|
height = float(node.get('height'))
|
||||||
|
#d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y, x+width,y+height, x,y+height)
|
||||||
|
#p = cubicsuperpath.parsePath(d)
|
||||||
|
rx = 0.0
|
||||||
|
ry = 0.0
|
||||||
|
if node.get('rx'):
|
||||||
|
rx=float(node.get('rx'))
|
||||||
|
if node.get('ry'):
|
||||||
|
ry=float(node.get('ry'))
|
||||||
|
|
||||||
|
if max(rx,ry) > 0.0:
|
||||||
|
if rx==0.0 or ry==0.0:
|
||||||
|
rx = max(rx,ry)
|
||||||
|
ry = rx
|
||||||
|
#msg = "rx = %f ry = %f " %(rx,ry)
|
||||||
|
#inkex.errormsg(msg)
|
||||||
|
L1 = "M %f,%f %f,%f " %(x+rx , y , x+width-rx , y )
|
||||||
|
C1 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+width , y+ry )
|
||||||
|
L2 = "M %f,%f %f,%f " %(x+width , y+ry , x+width , y+height-ry)
|
||||||
|
C2 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+width-rx , y+height )
|
||||||
|
L3 = "M %f,%f %f,%f " %(x+width-rx , y+height , x+rx , y+height )
|
||||||
|
C3 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x , y+height-ry)
|
||||||
|
L4 = "M %f,%f %f,%f " %(x , y+height-ry, x , y+ry )
|
||||||
|
C4 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+rx , y )
|
||||||
|
d = L1 + C1 + L2 + C2 + L3 + C3 + L4 + C4
|
||||||
|
else:
|
||||||
|
d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y, x+width,y+height, x,y+height)
|
||||||
|
p = cubicsuperpath.parsePath(d)
|
||||||
|
|
||||||
|
elif node.tag == inkex.addNS('circle','svg'):
|
||||||
|
cx = float(node.get('cx') )
|
||||||
|
cy = float(node.get('cy'))
|
||||||
|
r = float(node.get('r'))
|
||||||
|
d = "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" %(cx+r,cy, r,r,cx,cy+r, r,r,cx-r,cy, r,r,cx,cy-r, r,r,cx+r,cy)
|
||||||
|
p = cubicsuperpath.parsePath(d)
|
||||||
|
|
||||||
|
elif node.tag == inkex.addNS('ellipse','svg'):
|
||||||
|
cx = float(node.get('cx'))
|
||||||
|
cy = float(node.get('cy'))
|
||||||
|
rx = float(node.get('rx'))
|
||||||
|
ry = float(node.get('ry'))
|
||||||
|
d = "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" %(cx+rx,cy, rx,ry,cx,cy+ry, rx,ry,cx-rx,cy, rx,ry,cx,cy-ry, rx,ry,cx+rx,cy)
|
||||||
|
p = cubicsuperpath.parsePath(d)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
trans = node.get('transform')
|
||||||
|
if trans:
|
||||||
|
mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
|
||||||
|
simpletransform.applyTransformToPath(mat, p)
|
||||||
|
|
||||||
|
###################################################
|
||||||
|
## Break Curves down into small lines
|
||||||
|
###################################################
|
||||||
|
f = self.flatness
|
||||||
|
is_flat = 0
|
||||||
|
while is_flat < 1:
|
||||||
|
try:
|
||||||
|
cspsubdiv.cspsubdiv(p, f)
|
||||||
|
is_flat = 1
|
||||||
|
except IndexError:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
f += 0.1
|
||||||
|
if f>2 :
|
||||||
|
break
|
||||||
|
#something has gone very wrong.
|
||||||
|
###################################################
|
||||||
|
for sub in p:
|
||||||
|
for i in range(len(sub)-1):
|
||||||
|
s = sub[i]
|
||||||
|
e = sub[i+1]
|
||||||
|
self.dxf_line([s[1],e[1]],0.025,rgb,path_id)
|
||||||
|
|
||||||
|
|
||||||
|
def process_clone(self, node):
|
||||||
|
trans = node.get('transform')
|
||||||
|
x = node.get('x')
|
||||||
|
y = node.get('y')
|
||||||
|
mat = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||||||
|
if trans:
|
||||||
|
mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
|
||||||
|
if x:
|
||||||
|
mat = simpletransform.composeTransform(mat, [[1.0, 0.0, float(x)], [0.0, 1.0, 0.0]])
|
||||||
|
if y:
|
||||||
|
mat = simpletransform.composeTransform(mat, [[1.0, 0.0, 0.0], [0.0, 1.0, float(y)]])
|
||||||
|
# push transform
|
||||||
|
if trans or x or y:
|
||||||
|
self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], mat))
|
||||||
|
# get referenced node
|
||||||
|
refid = node.get(inkex.addNS('href','xlink'))
|
||||||
|
refnode = self.getElementById(refid[1:])
|
||||||
|
if refnode is not None:
|
||||||
|
if refnode.tag == inkex.addNS('g','svg'):
|
||||||
|
self.process_group(refnode)
|
||||||
|
elif refnode.tag == inkex.addNS('use', 'svg'):
|
||||||
|
self.process_clone(refnode)
|
||||||
|
else:
|
||||||
|
self.process_shape(refnode, self.groupmat[-1])
|
||||||
|
# pop transform
|
||||||
|
if trans or x or y:
|
||||||
|
self.groupmat.pop()
|
||||||
|
|
||||||
|
def process_group(self, group):
|
||||||
|
if group.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
|
||||||
|
style = group.get('style')
|
||||||
|
if style:
|
||||||
|
style = simplestyle.parseStyle(style)
|
||||||
|
if style.has_key('display'):
|
||||||
|
if style['display'] == 'none' and self.options.layer_option and self.options.layer_option=='visible':
|
||||||
|
return
|
||||||
|
layer = group.get(inkex.addNS('label', 'inkscape'))
|
||||||
|
|
||||||
|
layer = layer.replace(' ', '_')
|
||||||
|
if layer in self.layers:
|
||||||
|
self.layer = layer
|
||||||
|
trans = group.get('transform')
|
||||||
|
if trans:
|
||||||
|
self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], simpletransform.parseTransform(trans)))
|
||||||
|
for node in group:
|
||||||
|
if node.tag == inkex.addNS('g','svg'):
|
||||||
|
self.process_group(node)
|
||||||
|
elif node.tag == inkex.addNS('use', 'svg'):
|
||||||
|
self.process_clone(node)
|
||||||
|
else:
|
||||||
|
self.process_shape(node, self.groupmat[-1])
|
||||||
|
if trans:
|
||||||
|
self.groupmat.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def Make_PNG(self):
|
||||||
|
#create OS temp folder
|
||||||
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
svg_temp_file = os.path.join(tmp_dir, "LYZimage.svg")
|
||||||
|
png_temp_file = os.path.join(tmp_dir, "LYZpngdata.png")
|
||||||
|
|
||||||
|
dpi = "%d" %(self.options.resolution)
|
||||||
|
if self.inkscape_version >= 100:
|
||||||
|
cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, \
|
||||||
|
"--export-background","rgb(255, 255, 255)","--export-background-opacity", \
|
||||||
|
"255" ,"--export-type=png", "--export-filename=%s" %(png_temp_file), svg_temp_file ]
|
||||||
|
else:
|
||||||
|
cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, \
|
||||||
|
"--export-background","rgb(255, 255, 255)","--export-background-opacity", \
|
||||||
|
"255" ,"--export-png", png_temp_file, svg_temp_file ]
|
||||||
|
|
||||||
|
if (self.cut_select=="raster") or (self.cut_select=="all") or (self.cut_select=="zip"):
|
||||||
|
self.document.write(svg_temp_file)
|
||||||
|
#cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, "--export-background","rgb(255, 255, 255)","--export-background-opacity", "255" ,"--export-png", png_temp_file, svg_temp_file ]
|
||||||
|
#p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||||
|
#stdout, stderr = p.communicate()
|
||||||
|
run_external(cmd, self.timout)
|
||||||
|
else:
|
||||||
|
shutil.copyfile(sys.argv[-1], svg_temp_file)
|
||||||
|
#cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, "--export-background","rgb(255, 255, 255)","--export-background-opacity", "255" ,"--export-png", png_temp_file, svg_temp_file ]
|
||||||
|
#p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||||
|
#stdout, stderr = p.communicate()
|
||||||
|
run_external(cmd, self.timout)
|
||||||
|
try:
|
||||||
|
with open(png_temp_file, 'rb') as f:
|
||||||
|
self.PNG_DATA = f.read()
|
||||||
|
except:
|
||||||
|
inkex.errormsg("PNG generation timed out.\nTry saving again.\n\n")
|
||||||
|
|
||||||
|
#Delete the temp folder and any files
|
||||||
|
shutil.rmtree(tmp_dir)
|
||||||
|
|
||||||
|
def unit2mm(self, string):
|
||||||
|
# Returns mm given a string representation of units in another system
|
||||||
|
# a dictionary of unit to user unit conversion factors
|
||||||
|
uuconv = {'in': 25.4,
|
||||||
|
'pt': 25.4/72.0,
|
||||||
|
'px': 25.4/self.inkscape_dpi,
|
||||||
|
'mm': 1.0,
|
||||||
|
'cm': 10.0,
|
||||||
|
'm' : 1000.0,
|
||||||
|
'km': 1000.0*1000.0,
|
||||||
|
'pc': 25.4/6.0,
|
||||||
|
'yd': 25.4*12*3,
|
||||||
|
'ft': 25.4*12}
|
||||||
|
|
||||||
|
unit = re.compile('(%s)$' % '|'.join(uuconv.keys()))
|
||||||
|
param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
|
||||||
|
|
||||||
|
p = param.match(string)
|
||||||
|
u = unit.search(string)
|
||||||
|
if p:
|
||||||
|
retval = float(p.string[p.start():p.end()])
|
||||||
|
else:
|
||||||
|
inkex.errormsg("Size was not determined returning zero value")
|
||||||
|
retval = 0.0
|
||||||
|
if u:
|
||||||
|
retunit = u.string[u.start():u.end()]
|
||||||
|
else:
|
||||||
|
inkex.errormsg("units not understood assuming px at %d dpi" %(self.inkscape_dpi))
|
||||||
|
retunit = 'px'
|
||||||
|
|
||||||
|
try:
|
||||||
|
return retval * uuconv[retunit]
|
||||||
|
except KeyError:
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
msg = ""
|
||||||
|
#area_select = self.options.area_select # "page_area", "object_area"
|
||||||
|
area_select = "page_area"
|
||||||
|
self.cut_select = self.options.cut_select # "vector_red", "vector_blue", "raster", "all", "image", "Zip"
|
||||||
|
self.margin = self.options.margin # float value
|
||||||
|
#self.inkscape_dpi = self.options.inkscape_dpi # float value
|
||||||
|
self.inkscape_version = self.options.inkscape_version # float value
|
||||||
|
self.txt2paths = self.options.txt2paths # boolean Value
|
||||||
|
|
||||||
|
|
||||||
|
if self.inkscape_version > 91:
|
||||||
|
self.inkscape_dpi = 96
|
||||||
|
else:
|
||||||
|
self.inkscape_dpi = 90
|
||||||
|
|
||||||
|
if (self.txt2paths):
|
||||||
|
#create OS temp folder
|
||||||
|
tmp_dir = tempfile.mkdtemp()
|
||||||
|
txt2path_file = os.path.join(tmp_dir, "txt2path.svg")
|
||||||
|
if self.inkscape_version >= 100:
|
||||||
|
cmd = [ "inkscape", "--export-text-to-path","--export-plain-svg", "--export-filename=%s" %(txt2path_file), sys.argv[-1] ]
|
||||||
|
else:
|
||||||
|
cmd = [ "inkscape", "--export-text-to-path","--export-plain-svg",txt2path_file, sys.argv[-1] ]
|
||||||
|
|
||||||
|
run_external(cmd, self.timout)
|
||||||
|
self.document.parse(txt2path_file)
|
||||||
|
#Delete the temp folder and any files
|
||||||
|
shutil.rmtree(tmp_dir)
|
||||||
|
|
||||||
|
h_uu = self.unittouu(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0])
|
||||||
|
w_uu = self.unittouu(self.document.getroot().xpath('@width' , namespaces=inkex.NSS)[0])
|
||||||
|
|
||||||
|
h_mm = self.unit2mm(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0])
|
||||||
|
w_mm = self.unit2mm(self.document.getroot().xpath('@width', namespaces=inkex.NSS)[0])
|
||||||
|
|
||||||
|
try:
|
||||||
|
view_box_str = self.document.getroot().xpath('@viewBox', namespaces=inkex.NSS)[0]
|
||||||
|
view_box_list = view_box_str.split(' ')
|
||||||
|
Wpix = float(view_box_list[2])
|
||||||
|
Hpix = float(view_box_list[3])
|
||||||
|
scale = h_mm/Hpix
|
||||||
|
Dx = float(view_box_list[0]) / ( float(view_box_list[2])/w_mm )
|
||||||
|
Dy = float(view_box_list[1]) / ( float(view_box_list[3])/h_mm )
|
||||||
|
except:
|
||||||
|
#inkex.errormsg("Using Default Inkscape Scale")
|
||||||
|
scale = 25.4/self.inkscape_dpi
|
||||||
|
Dx = 0
|
||||||
|
Dy = 0
|
||||||
|
|
||||||
|
for node in self.document.getroot().xpath('//svg:g', namespaces=inkex.NSS):
|
||||||
|
if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
|
||||||
|
layer = node.get(inkex.addNS('label', 'inkscape'))
|
||||||
|
self.layernames.append(layer.lower())
|
||||||
|
# if self.options.layer_name and self.options.layer_option and self.options.layer_option=='name' and not layer.lower() in self.options.layer_name:
|
||||||
|
# continue
|
||||||
|
layer = layer.replace(' ', '_')
|
||||||
|
if layer and not layer in self.layers:
|
||||||
|
self.layers.append(layer)
|
||||||
|
|
||||||
|
#self.groupmat = [[[scale, 0.0, 0.0], [0.0, -scale, h_mm]]]
|
||||||
|
self.groupmat = [[[scale, 0.0, 0.0-Dx],
|
||||||
|
[0.0 , -scale, h_mm+Dy]]]
|
||||||
|
#doc = self.document.getroot()
|
||||||
|
self.process_group(self.document.getroot())
|
||||||
|
#################################################
|
||||||
|
|
||||||
|
# msg = msg + self.getDocumentUnit() + "\n"
|
||||||
|
# msg = msg + "scale = %f\n" %(scale)
|
||||||
|
msg = msg + "Height(mm)= %f\n" %(h_mm)
|
||||||
|
msg = msg + "Width (mm)= %f\n" %(w_mm)
|
||||||
|
# msg = msg + "h_uu = %f\n" %(h_uu)
|
||||||
|
# msg = msg + "w_uu = %f\n" %(w_uu)
|
||||||
|
|
||||||
|
#inkex.errormsg(msg)
|
||||||
|
|
||||||
|
if (area_select=="object_area"):
|
||||||
|
self.png_area = "--export-area-drawing"
|
||||||
|
xmin= self.lines[0][0]+0.0
|
||||||
|
xmax= self.lines[0][0]+0.0
|
||||||
|
ymin= self.lines[0][1]+0.0
|
||||||
|
ymax= self.lines[0][1]+0.0
|
||||||
|
for line in self.lines:
|
||||||
|
x1= line[0]
|
||||||
|
y1= line[1]
|
||||||
|
x2= line[2]
|
||||||
|
y2= line[3]
|
||||||
|
xmin = min(min(xmin,x1),x2)
|
||||||
|
ymin = min(min(ymin,y1),y2)
|
||||||
|
xmax = max(max(xmax,x1),x2)
|
||||||
|
ymax = max(max(ymax,y1),y2)
|
||||||
|
else:
|
||||||
|
self.png_area = "--export-area-page"
|
||||||
|
xmin= 0.0
|
||||||
|
xmax= w_mm
|
||||||
|
ymin= -h_mm
|
||||||
|
ymax= 0.0
|
||||||
|
|
||||||
|
self.Xsize=xmax-xmin
|
||||||
|
self.Ysize=ymax-ymin
|
||||||
|
Xcenter=(xmax+xmin)/2.0
|
||||||
|
Ycenter=(ymax+ymin)/2.0
|
||||||
|
for ii in range(len(self.lines)):
|
||||||
|
self.lines[ii][0] = self.lines[ii][0]-Xcenter
|
||||||
|
self.lines[ii][1] = self.lines[ii][1]-Ycenter
|
||||||
|
self.lines[ii][2] = self.lines[ii][2]-Xcenter
|
||||||
|
self.lines[ii][3] = self.lines[ii][3]-Ycenter
|
||||||
|
|
||||||
|
if (self.cut_select=="raster") or \
|
||||||
|
(self.cut_select=="all" ) or \
|
||||||
|
(self.cut_select=="image" ) or \
|
||||||
|
(self.cut_select=="zip" ):
|
||||||
|
self.Make_PNG()
|
||||||
|
|
||||||
|
LYZExport().affect()
|
@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>LaserDraw Export (zip)</name>
|
||||||
|
<id>fablabchemnitz.de.laserdraw_export.zip</id>
|
||||||
|
<!--
|
||||||
|
<param name="area_select" type="optiongroup" appearance="combo" gui-text="Area: ">
|
||||||
|
<option value="page_area">Page Area (use for engrave and cut)</option>
|
||||||
|
<option value="object_area">Object Area (for single operations only)</option>
|
||||||
|
</param>
|
||||||
|
-->
|
||||||
|
<label>Formatting can be used to break a design into separate cutting and engraving files. Red lines indicate cutting, blue lines indicate engraving, black indicates raster engraving.</label>
|
||||||
|
<!--
|
||||||
|
<param name="cut_select" type="optiongroup" appearance="combo" gui-text="Type: ">
|
||||||
|
<option value="vector_red"> Vector Cuts (export red lines) </option>
|
||||||
|
<option value="vector_blue"> Vector Engrave (export blue lines) </option>
|
||||||
|
<option value="raster" > Raster Engrave (export everything else) </option>
|
||||||
|
<option value="all" > All (export vectors and raster engrave to one file) </option>
|
||||||
|
<option value="image" > Image (export all items as raster) </option>
|
||||||
|
</param>
|
||||||
|
-->
|
||||||
|
<param name="resolution" type="int" min="100" max="1000" gui-text="Raster Image Resolution">1000</param>
|
||||||
|
<param name="margin" type="float" precision="1" min="0" max="9999" gui-text="Laser Draw Margin Size">2.0</param>
|
||||||
|
<param name="txt2paths" type="bool" gui-text="Convert Text to Paths">false</param>
|
||||||
|
<!--
|
||||||
|
<param name="texthelp" type="description">
|
||||||
|
Depending on your Inkscape version the internal resolution need to be 72, 90 or 96 to get the properly scaled output.</param>
|
||||||
|
<param name="inkscape_version" type="int" min="72" max="96" gui-text="Inkscape Internal Resolution">1</param>
|
||||||
|
-->
|
||||||
|
<param name="inkscape_version" type="optiongroup" appearance="combo" gui-text="Inkscape Version:">
|
||||||
|
<option value="100">1.00 or Newer</option>
|
||||||
|
<option value="92">0.92 to 0.99</option>
|
||||||
|
<option value="91">0.91 or Older</option>
|
||||||
|
</param>
|
||||||
|
<output>
|
||||||
|
<extension>.zip</extension>
|
||||||
|
<mimetype>application/x-zip</mimetype>
|
||||||
|
<filetypename>Laser Draw LYZ (ZIP)(*.zip)</filetypename>
|
||||||
|
<filetypetooltip>LaserDraw LYZ Output Zipped</filetypetooltip>
|
||||||
|
<dataloss>true</dataloss>
|
||||||
|
</output>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">lyz_export.py</command>
|
||||||
|
<helper_extension>org.inkscape.output.svg.inkscape</helper_extension>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
138
extensions/fablabchemnitz/laserdraw_export/lyz_ffgeom.py
Normal file
138
extensions/fablabchemnitz/laserdraw_export/lyz_ffgeom.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
ffgeom.py
|
||||||
|
Copyright (C) 2005 Aaron Cyril Spike, aaron@ekips.org
|
||||||
|
|
||||||
|
This file is part of FretFind 2-D.
|
||||||
|
|
||||||
|
FretFind 2-D 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.
|
||||||
|
|
||||||
|
FretFind 2-D 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 FretFind 2-D; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
try:
|
||||||
|
NaN = float('NaN')
|
||||||
|
except ValueError:
|
||||||
|
PosInf = 1e300000
|
||||||
|
NaN = PosInf/PosInf
|
||||||
|
|
||||||
|
class Point:
|
||||||
|
precision = 5
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.__coordinates = {'x' : float(x), 'y' : float(y)}
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.__coordinates[key]
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.__coordinates[key] = float(value)
|
||||||
|
def __repr__(self):
|
||||||
|
return '(%s, %s)' % (round(self['x'],self.precision),round(self['y'],self.precision))
|
||||||
|
def copy(self):
|
||||||
|
return Point(self['x'],self['y'])
|
||||||
|
def translate(self, x, y):
|
||||||
|
self['x'] += x
|
||||||
|
self['y'] += y
|
||||||
|
def move(self, x, y):
|
||||||
|
self['x'] = float(x)
|
||||||
|
self['y'] = float(y)
|
||||||
|
|
||||||
|
class Segment:
|
||||||
|
def __init__(self, e0, e1):
|
||||||
|
self.__endpoints = [e0, e1]
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.__endpoints[key]
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.__endpoints[key] = value
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self.__endpoints)
|
||||||
|
def copy(self):
|
||||||
|
return Segment(self[0],self[1])
|
||||||
|
def translate(self, x, y):
|
||||||
|
self[0].translate(x,y)
|
||||||
|
self[1].translate(x,y)
|
||||||
|
def move(self,e0,e1):
|
||||||
|
self[0] = e0
|
||||||
|
self[1] = e1
|
||||||
|
def delta_x(self):
|
||||||
|
return self[1]['x'] - self[0]['x']
|
||||||
|
def delta_y(self):
|
||||||
|
return self[1]['y'] - self[0]['y']
|
||||||
|
#alias functions
|
||||||
|
run = delta_x
|
||||||
|
rise = delta_y
|
||||||
|
def slope(self):
|
||||||
|
if self.delta_x() != 0:
|
||||||
|
return self.delta_x() / self.delta_y()
|
||||||
|
return NaN
|
||||||
|
def intercept(self):
|
||||||
|
if self.delta_x() != 0:
|
||||||
|
return self[1]['y'] - (self[0]['x'] * self.slope())
|
||||||
|
return NaN
|
||||||
|
def distanceToPoint(self, p):
|
||||||
|
s2 = Segment(self[0],p)
|
||||||
|
c1 = dot(s2,self)
|
||||||
|
if c1 <= 0:
|
||||||
|
return Segment(p,self[0]).length()
|
||||||
|
c2 = dot(self,self)
|
||||||
|
if c2 <= c1:
|
||||||
|
return Segment(p,self[1]).length()
|
||||||
|
return self.perpDistanceToPoint(p)
|
||||||
|
def perpDistanceToPoint(self, p):
|
||||||
|
len = self.length()
|
||||||
|
if len == 0: return NaN
|
||||||
|
return math.fabs(((self[1]['x'] - self[0]['x']) * (self[0]['y'] - p['y'])) - \
|
||||||
|
((self[0]['x'] - p['x']) * (self[1]['y'] - self[0]['y']))) / len
|
||||||
|
def angle(self):
|
||||||
|
return math.pi * (math.atan2(self.delta_y(), self.delta_x())) / 180
|
||||||
|
def length(self):
|
||||||
|
return math.sqrt((self.delta_x() ** 2) + (self.delta_y() ** 2))
|
||||||
|
def pointAtLength(self, len):
|
||||||
|
if self.length() == 0: return Point(NaN, NaN)
|
||||||
|
ratio = len / self.length()
|
||||||
|
x = self[0]['x'] + (ratio * self.delta_x())
|
||||||
|
y = self[0]['y'] + (ratio * self.delta_y())
|
||||||
|
return Point(x, y)
|
||||||
|
def pointAtRatio(self, ratio):
|
||||||
|
if self.length() == 0: return Point(NaN, NaN)
|
||||||
|
x = self[0]['x'] + (ratio * self.delta_x())
|
||||||
|
y = self[0]['y'] + (ratio * self.delta_y())
|
||||||
|
return Point(x, y)
|
||||||
|
def createParallel(self, p):
|
||||||
|
return Segment(Point(p['x'] + self.delta_x(), p['y'] + self.delta_y()), p)
|
||||||
|
def intersect(self, s):
|
||||||
|
return intersectSegments(self, s)
|
||||||
|
|
||||||
|
def intersectSegments(s1, s2):
|
||||||
|
x1 = s1[0]['x']
|
||||||
|
x2 = s1[1]['x']
|
||||||
|
x3 = s2[0]['x']
|
||||||
|
x4 = s2[1]['x']
|
||||||
|
|
||||||
|
y1 = s1[0]['y']
|
||||||
|
y2 = s1[1]['y']
|
||||||
|
y3 = s2[0]['y']
|
||||||
|
y4 = s2[1]['y']
|
||||||
|
|
||||||
|
denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1))
|
||||||
|
num1 = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3))
|
||||||
|
num2 = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3))
|
||||||
|
|
||||||
|
num = num1
|
||||||
|
|
||||||
|
if denom != 0:
|
||||||
|
x = x1 + ((num / denom) * (x2 - x1))
|
||||||
|
y = y1 + ((num / denom) * (y2 - y1))
|
||||||
|
return Point(x, y)
|
||||||
|
return Point(NaN, NaN)
|
||||||
|
|
||||||
|
def dot(s1, s2):
|
||||||
|
return s1.delta_x() * s2.delta_x() + s1.delta_y() * s2.delta_y()
|
399
extensions/fablabchemnitz/laserdraw_export/lyz_inkex.py
Normal file
399
extensions/fablabchemnitz/laserdraw_export/lyz_inkex.py
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
inkex.py
|
||||||
|
A helper module for creating Inkscape extensions
|
||||||
|
|
||||||
|
Copyright (C) 2005,2010 Aaron Spike <aaron@ekips.org> and contributors
|
||||||
|
|
||||||
|
Contributors:
|
||||||
|
Aurelio A. Heckert <aurium(a)gmail.com>
|
||||||
|
Bulia Byak <buliabyak@users.sf.net>
|
||||||
|
Nicolas Dufour, nicoduf@yahoo.fr
|
||||||
|
Peter J. R. Moulder <pjrm@users.sourceforge.net>
|
||||||
|
|
||||||
|
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 copy
|
||||||
|
import gettext
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from math import *
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
# a dictionary of all of the xmlns prefixes in a standard inkscape doc
|
||||||
|
NSS = {
|
||||||
|
u'sodipodi' :u'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
|
||||||
|
u'cc' :u'http://creativecommons.org/ns#',
|
||||||
|
u'ccOLD' :u'http://web.resource.org/cc/',
|
||||||
|
u'svg' :u'http://www.w3.org/2000/svg',
|
||||||
|
u'dc' :u'http://purl.org/dc/elements/1.1/',
|
||||||
|
u'rdf' :u'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
||||||
|
u'inkscape' :u'http://www.inkscape.org/namespaces/inkscape',
|
||||||
|
u'xlink' :u'http://www.w3.org/1999/xlink',
|
||||||
|
u'xml' :u'http://www.w3.org/XML/1998/namespace'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def localize():
|
||||||
|
domain = 'inkscape'
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
import locale
|
||||||
|
current_locale, encoding = locale.getdefaultlocale()
|
||||||
|
os.environ['LANG'] = current_locale
|
||||||
|
try:
|
||||||
|
localdir = os.environ['INKSCAPE_LOCALEDIR']
|
||||||
|
trans = gettext.translation(domain, localdir, [current_locale], fallback=True)
|
||||||
|
except KeyError:
|
||||||
|
trans = gettext.translation(domain, fallback=True)
|
||||||
|
elif sys.platform.startswith('darwin'):
|
||||||
|
try:
|
||||||
|
localdir = os.environ['INKSCAPE_LOCALEDIR']
|
||||||
|
trans = gettext.translation(domain, localdir, fallback=True)
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
localdir = os.environ['PACKAGE_LOCALE_DIR']
|
||||||
|
trans = gettext.translation(domain, localdir, fallback=True)
|
||||||
|
except KeyError:
|
||||||
|
trans = gettext.translation(domain, fallback=True)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
localdir = os.environ['PACKAGE_LOCALE_DIR']
|
||||||
|
trans = gettext.translation(domain, localdir, fallback=True)
|
||||||
|
except KeyError:
|
||||||
|
trans = gettext.translation(domain, fallback=True)
|
||||||
|
#sys.stderr.write(str(localdir) + "\n")
|
||||||
|
trans.install()
|
||||||
|
|
||||||
|
|
||||||
|
def debug(what):
|
||||||
|
sys.stderr.write(str(what) + "\n")
|
||||||
|
return what
|
||||||
|
|
||||||
|
|
||||||
|
def errormsg(msg):
|
||||||
|
"""Intended for end-user-visible error messages.
|
||||||
|
|
||||||
|
(Currently just writes to stderr with an appended newline, but could do
|
||||||
|
something better in future: e.g. could add markup to distinguish error
|
||||||
|
messages from status messages or debugging output.)
|
||||||
|
|
||||||
|
Note that this should always be combined with translation:
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
...
|
||||||
|
inkex.errormsg("This extension requires two selected paths.")
|
||||||
|
"""
|
||||||
|
#if isinstance(msg, unicode):
|
||||||
|
# sys.stderr.write(msg.encode("utf-8") + "\n")
|
||||||
|
#else:
|
||||||
|
# sys.stderr.write((unicode(msg, "utf-8", errors='replace') + "\n").encode("utf-8"))
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
def are_near_relative(a, b, eps):
|
||||||
|
return (a-b <= a*eps) and (a-b >= -a*eps)
|
||||||
|
|
||||||
|
|
||||||
|
def check_inkbool(option, opt, value):
|
||||||
|
if str(value).capitalize() == 'True':
|
||||||
|
return True
|
||||||
|
elif str(value).capitalize() == 'False':
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise optparse.OptionValueError("option %s: invalid inkbool value: %s" % (opt, value))
|
||||||
|
|
||||||
|
|
||||||
|
def addNS(tag, ns=None):
|
||||||
|
val = tag
|
||||||
|
if ns is not None and len(ns) > 0 and ns in NSS and len(tag) > 0 and tag[0] != '{':
|
||||||
|
val = "{%s}%s" % (NSS[ns], tag)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
class InkOption(optparse.Option):
|
||||||
|
TYPES = optparse.Option.TYPES + ("inkbool",)
|
||||||
|
TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
|
||||||
|
TYPE_CHECKER["inkbool"] = check_inkbool
|
||||||
|
|
||||||
|
|
||||||
|
class Effect:
|
||||||
|
"""A class for creating Inkscape SVG Effects"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.document = None
|
||||||
|
self.original_document = None
|
||||||
|
self.ctx = None
|
||||||
|
self.selected = {}
|
||||||
|
self.doc_ids = {}
|
||||||
|
self.options = None
|
||||||
|
self.args = None
|
||||||
|
self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile", option_class=InkOption)
|
||||||
|
self.OptionParser.add_option("--id", dest="ids", default=[], help="id attribute of object to manipulate")
|
||||||
|
self.OptionParser.add_option("--selected-nodes", dest="selected_nodes", default=[], help="id:subpath:position of selected nodes, if any")
|
||||||
|
# TODO write a parser for this
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
"""Apply some effects on the document. Extensions subclassing Effect
|
||||||
|
must override this function and define the transformations
|
||||||
|
in it."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getoptions(self,args=sys.argv[1:]):
|
||||||
|
"""Collect command line arguments"""
|
||||||
|
self.options, self.args = self.OptionParser.parse_args(args)
|
||||||
|
|
||||||
|
def parse(self, filename=None, encoding=None):
|
||||||
|
"""Parse document in specified file or on stdin"""
|
||||||
|
|
||||||
|
# First try to open the file from the function argument
|
||||||
|
if filename is not None:
|
||||||
|
try:
|
||||||
|
stream = open(filename, 'r')
|
||||||
|
except IOError:
|
||||||
|
errormsg("Unable to open specified file: %s" % filename)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# If it wasn't specified, try to open the file specified as
|
||||||
|
# an object member
|
||||||
|
elif self.svg_file is not None:
|
||||||
|
try:
|
||||||
|
stream = open(self.svg_file, 'r')
|
||||||
|
except IOError:
|
||||||
|
errormsg("Unable to open object member file: %s" % self.svg_file)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# Finally, if the filename was not specified anywhere, use
|
||||||
|
# standard input stream
|
||||||
|
else:
|
||||||
|
stream = sys.stdin
|
||||||
|
|
||||||
|
if encoding == None:
|
||||||
|
p = etree.XMLParser(huge_tree=True, recover=True)
|
||||||
|
else:
|
||||||
|
p = etree.XMLParser(huge_tree=True, recover=True, encoding=encoding)
|
||||||
|
|
||||||
|
self.document = etree.parse(stream, parser=p)
|
||||||
|
self.original_document = copy.deepcopy(self.document)
|
||||||
|
stream.close()
|
||||||
|
|
||||||
|
# defines view_center in terms of document units
|
||||||
|
def getposinlayer(self):
|
||||||
|
#defaults
|
||||||
|
self.current_layer = self.document.getroot()
|
||||||
|
self.view_center = (0.0, 0.0)
|
||||||
|
|
||||||
|
layerattr = self.document.xpath('//sodipodi:namedview/@inkscape:current-layer', namespaces=NSS)
|
||||||
|
if layerattr:
|
||||||
|
layername = layerattr[0]
|
||||||
|
layer = self.document.xpath('//svg:g[@id="%s"]' % layername, namespaces=NSS)
|
||||||
|
if layer:
|
||||||
|
self.current_layer = layer[0]
|
||||||
|
|
||||||
|
xattr = self.document.xpath('//sodipodi:namedview/@inkscape:cx', namespaces=NSS)
|
||||||
|
yattr = self.document.xpath('//sodipodi:namedview/@inkscape:cy', namespaces=NSS)
|
||||||
|
if xattr and yattr:
|
||||||
|
x = self.unittouu(xattr[0] + 'px')
|
||||||
|
y = self.unittouu(yattr[0] + 'px')
|
||||||
|
doc_height = self.unittouu(self.getDocumentHeight())
|
||||||
|
if x and y:
|
||||||
|
self.view_center = (float(x), doc_height - float(y))
|
||||||
|
# FIXME: y-coordinate flip, eliminate it when it's gone in Inkscape
|
||||||
|
|
||||||
|
def getselected(self):
|
||||||
|
"""Collect selected nodes"""
|
||||||
|
for i in self.options.ids:
|
||||||
|
path = '//*[@id="%s"]' % i
|
||||||
|
for node in self.document.xpath(path, namespaces=NSS):
|
||||||
|
self.selected[i] = node
|
||||||
|
|
||||||
|
def getElementById(self, id):
|
||||||
|
path = '//*[@id="%s"]' % id
|
||||||
|
el_list = self.document.xpath(path, namespaces=NSS)
|
||||||
|
if el_list:
|
||||||
|
return el_list[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getParentNode(self, node):
|
||||||
|
for parent in self.document.getiterator():
|
||||||
|
if node in parent.getchildren():
|
||||||
|
return parent
|
||||||
|
|
||||||
|
def getdocids(self):
|
||||||
|
docIdNodes = self.document.xpath('//@id', namespaces=NSS)
|
||||||
|
for m in docIdNodes:
|
||||||
|
self.doc_ids[m] = 1
|
||||||
|
|
||||||
|
def getNamedView(self):
|
||||||
|
return self.document.xpath('//sodipodi:namedview', namespaces=NSS)[0]
|
||||||
|
|
||||||
|
def createGuide(self, posX, posY, angle):
|
||||||
|
atts = {
|
||||||
|
'position': str(posX)+','+str(posY),
|
||||||
|
'orientation': str(sin(radians(angle)))+','+str(-cos(radians(angle)))
|
||||||
|
}
|
||||||
|
guide = etree.SubElement(
|
||||||
|
self.getNamedView(),
|
||||||
|
addNS('guide','sodipodi'), atts)
|
||||||
|
return guide
|
||||||
|
|
||||||
|
def output(self):
|
||||||
|
"""Serialize document into XML on stdout"""
|
||||||
|
original = etree.tostring(self.original_document)
|
||||||
|
result = etree.tostring(self.document)
|
||||||
|
if original != result:
|
||||||
|
self.document.write(sys.stdout)
|
||||||
|
|
||||||
|
def affect(self, args=sys.argv[1:], output=True):
|
||||||
|
"""Affect an SVG document with a callback effect"""
|
||||||
|
self.svg_file = args[-1]
|
||||||
|
localize()
|
||||||
|
self.getoptions(args)
|
||||||
|
self.parse()
|
||||||
|
self.getposinlayer()
|
||||||
|
self.getselected()
|
||||||
|
self.getdocids()
|
||||||
|
self.effect()
|
||||||
|
if output:
|
||||||
|
self.output()
|
||||||
|
|
||||||
|
def uniqueId(self, old_id, make_new_id=True):
|
||||||
|
new_id = old_id
|
||||||
|
if make_new_id:
|
||||||
|
while new_id in self.doc_ids:
|
||||||
|
new_id += random.choice('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
||||||
|
self.doc_ids[new_id] = 1
|
||||||
|
return new_id
|
||||||
|
|
||||||
|
def xpathSingle(self, path):
|
||||||
|
try:
|
||||||
|
retval = self.document.xpath(path, namespaces=NSS)[0]
|
||||||
|
except:
|
||||||
|
errormsg("No matching node for expression: %s" % path)
|
||||||
|
retval = None
|
||||||
|
return retval
|
||||||
|
|
||||||
|
# a dictionary of unit to user unit conversion factors
|
||||||
|
__uuconv = {'in': 96.0, 'pt': 1.33333333333, 'px': 1.0, 'mm': 3.77952755913, 'cm': 37.7952755913,
|
||||||
|
'm': 3779.52755913, 'km': 3779527.55913, 'pc': 16.0, 'yd': 3456.0, 'ft': 1152.0}
|
||||||
|
|
||||||
|
# Fault tolerance for lazily defined SVG
|
||||||
|
def getDocumentWidth(self):
|
||||||
|
width = self.document.getroot().get('width')
|
||||||
|
if width:
|
||||||
|
return width
|
||||||
|
else:
|
||||||
|
viewbox = self.document.getroot().get('viewBox')
|
||||||
|
if viewbox:
|
||||||
|
return viewbox.split()[2]
|
||||||
|
else:
|
||||||
|
return '0'
|
||||||
|
|
||||||
|
# Fault tolerance for lazily defined SVG
|
||||||
|
def getDocumentHeight(self):
|
||||||
|
"""Returns a string corresponding to the height of the document, as
|
||||||
|
defined in the SVG file. If it is not defined, returns the height
|
||||||
|
as defined by the viewBox attribute. If viewBox is not defined,
|
||||||
|
returns the string '0'."""
|
||||||
|
height = self.document.getroot().get('height')
|
||||||
|
if height:
|
||||||
|
return height
|
||||||
|
else:
|
||||||
|
viewbox = self.document.getroot().get('viewBox')
|
||||||
|
if viewbox:
|
||||||
|
return viewbox.split()[3]
|
||||||
|
else:
|
||||||
|
return '0'
|
||||||
|
|
||||||
|
def getDocumentUnit(self):
|
||||||
|
"""Returns the unit used for in the SVG document.
|
||||||
|
In the case the SVG document lacks an attribute that explicitly
|
||||||
|
defines what units are used for SVG coordinates, it tries to calculate
|
||||||
|
the unit from the SVG width and viewBox attributes.
|
||||||
|
Defaults to 'px' units."""
|
||||||
|
svgunit = 'px' # default to pixels
|
||||||
|
|
||||||
|
svgwidth = self.getDocumentWidth()
|
||||||
|
viewboxstr = self.document.getroot().get('viewBox')
|
||||||
|
if viewboxstr:
|
||||||
|
unitmatch = re.compile('(%s)$' % '|'.join(self.__uuconv.keys()))
|
||||||
|
param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
|
||||||
|
|
||||||
|
p = param.match(svgwidth)
|
||||||
|
u = unitmatch.search(svgwidth)
|
||||||
|
|
||||||
|
width = 100 # default
|
||||||
|
viewboxwidth = 100 # default
|
||||||
|
svgwidthunit = 'px' # default assume 'px' unit
|
||||||
|
if p:
|
||||||
|
width = float(p.string[p.start():p.end()])
|
||||||
|
else:
|
||||||
|
errormsg("SVG Width not set correctly! Assuming width = 100")
|
||||||
|
if u:
|
||||||
|
svgwidthunit = u.string[u.start():u.end()]
|
||||||
|
|
||||||
|
viewboxnumbers = []
|
||||||
|
for t in viewboxstr.split():
|
||||||
|
try:
|
||||||
|
viewboxnumbers.append(float(t))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if len(viewboxnumbers) == 4: # check for correct number of numbers
|
||||||
|
viewboxwidth = viewboxnumbers[2]
|
||||||
|
|
||||||
|
svgunitfactor = self.__uuconv[svgwidthunit] * width / viewboxwidth
|
||||||
|
|
||||||
|
# try to find the svgunitfactor in the list of units known. If we don't find something, ...
|
||||||
|
eps = 0.01 # allow 1% error in factor
|
||||||
|
for key in self.__uuconv:
|
||||||
|
if are_near_relative(self.__uuconv[key], svgunitfactor, eps):
|
||||||
|
# found match!
|
||||||
|
svgunit = key
|
||||||
|
|
||||||
|
return svgunit
|
||||||
|
|
||||||
|
def unittouu(self, string):
|
||||||
|
"""Returns userunits given a string representation of units in another system"""
|
||||||
|
unit = re.compile('(%s)$' % '|'.join(self.__uuconv.keys()))
|
||||||
|
param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
|
||||||
|
|
||||||
|
p = param.match(string)
|
||||||
|
u = unit.search(string)
|
||||||
|
if p:
|
||||||
|
retval = float(p.string[p.start():p.end()])
|
||||||
|
else:
|
||||||
|
retval = 0.0
|
||||||
|
if u:
|
||||||
|
try:
|
||||||
|
return retval * (self.__uuconv[u.string[u.start():u.end()]] / self.__uuconv[self.getDocumentUnit()])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else: # default assume 'px' unit
|
||||||
|
return retval / self.__uuconv[self.getDocumentUnit()]
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def uutounit(self, val, unit):
|
||||||
|
return val / (self.__uuconv[unit] / self.__uuconv[self.getDocumentUnit()])
|
||||||
|
|
||||||
|
def addDocumentUnit(self, value):
|
||||||
|
"""Add document unit when no unit is specified in the string """
|
||||||
|
try:
|
||||||
|
float(value)
|
||||||
|
return value + self.getDocumentUnit()
|
||||||
|
except ValueError:
|
||||||
|
return value
|
441
extensions/fablabchemnitz/laserdraw_export/lyz_library.py
Normal file
441
extensions/fablabchemnitz/laserdraw_export/lyz_library.py
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
This script reads and writes Laser Draw (LaserDRW) LYZ files.
|
||||||
|
|
||||||
|
File history:
|
||||||
|
0.1 Initial code (2/5/2017)
|
||||||
|
|
||||||
|
Copyright (C) 2017 Scorch www.scorchworks.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
'''
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from time import time
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
def show(byte_in):
|
||||||
|
print("%03d" %ord(byte_in))
|
||||||
|
|
||||||
|
|
||||||
|
def possible_values(loc,len,type,bf):
|
||||||
|
cur= bf.tell()
|
||||||
|
vals=""
|
||||||
|
if type=='d' or type=='Q' or type=='>d' or type=='>Q':
|
||||||
|
tl=8
|
||||||
|
elif type=='i' or type=='f' or type=='l' or type=='L' or type=='I':
|
||||||
|
tl=4
|
||||||
|
elif type=='>i' or type=='>f' or type=='>l' or type=='>L' or type=='>I':
|
||||||
|
tl=4
|
||||||
|
elif type=='h' or type=='H' or type=='>h' or type=='>H':
|
||||||
|
tl=2
|
||||||
|
|
||||||
|
for i in range(tl):
|
||||||
|
for j in range(i,(len-tl+1),tl):
|
||||||
|
bf.seek(loc+j)
|
||||||
|
vals = vals + "\t"+ str( struct.unpack(type,bf.read(tl))[0] )
|
||||||
|
vals = vals+"\n"
|
||||||
|
bf.seek(cur)
|
||||||
|
return vals
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
class LYZ_CLASS:
|
||||||
|
def __init__(self):
|
||||||
|
# DEFINE HEADER FIELDS
|
||||||
|
self.header_fields = []
|
||||||
|
|
||||||
|
self.header_data = []
|
||||||
|
self.feature_list = []
|
||||||
|
self.left_over = ""
|
||||||
|
self.EOF = ""
|
||||||
|
|
||||||
|
########################## Description ,location,length,type,default value
|
||||||
|
self.header_fields.append(["EXTENSION" , 9999999, 4, 't', ".LYZ" ]) #0
|
||||||
|
self.header_fields.append(["LENGTH" , 9999999, 4, 'i', 221 ]) #1
|
||||||
|
self.header_fields.append(["N FEATURES" , 9999999, 4, 'i', 0 ]) #2
|
||||||
|
self.header_fields.append(["?A(4)" , 9999999, 4, 'i', 0 ]) #3
|
||||||
|
self.header_fields.append(["CREATOR" , 9999999, 50, 't',"Creater: LaserDraw.exe(Lihuiyu software Co., Ltd.)"]) #4
|
||||||
|
self.header_fields.append(["?B(14)" , 9999999, 14, 'z',[0,0,0,0,0,0,0,0,0,2,0,0,0,128] ]) #5
|
||||||
|
self.header_fields.append(["DESC" , 9999999, 37, 't',"Description: LaserDraw Graphics File."]) #6
|
||||||
|
self.header_fields.append(["?C(1)" , 9999999, 1, 'z', [0] ]) #7
|
||||||
|
self.header_fields.append(["?D(1)" , 9999999, 1, 'z', [0] ]) #8
|
||||||
|
self.header_fields.append(["Time(8)" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ]) #9
|
||||||
|
self.header_fields.append(["TIME(8)" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ]) #10
|
||||||
|
self.header_fields.append(["?G(8)" , 9999999, 4, 'z', [0,0,0,0] ]) #11
|
||||||
|
self.header_fields.append(["?H(8)" , 9999999, 4, 'z', [0,0,0,0] ]) #12
|
||||||
|
self.header_fields.append(["?I(18)" , 9999999, 18, 'z', [176,8,210,125,0,65,206,0,0,19,17,126,0,36,35,23,0,2] ]) #13
|
||||||
|
self.header_fields.append(["OFFSET" , 9999999, 8, 'd', 0.0 ]) #14 #was 84
|
||||||
|
self.header_fields.append(["X SIZE" , 9999999, 8, 'd', 42.0 ]) #15
|
||||||
|
self.header_fields.append(["Y SIZE" , 9999999, 8, 'd', 42.0 ]) #16
|
||||||
|
self.header_fields.append(["BORDER1" , 9999999, 8, 'd', 1.0 ]) #17
|
||||||
|
self.header_fields.append(["BORDER2" , 9999999, 8, 'd', 1.0 ]) #18
|
||||||
|
self.header_fields.append(["BORDER3" , 9999999, 8, 'd', 1.0 ]) #19
|
||||||
|
self.header_fields.append(["BORDER4" , 9999999, 8, 'd', 1.0 ]) #20
|
||||||
|
|
||||||
|
# DEFINE FEATURE FIELDS
|
||||||
|
self.feature_fields=[]
|
||||||
|
########################## Description ,location,length,type,default value
|
||||||
|
self.feature_fields.append(["?a(4)" , 9999999, 4, 'i', 0 ]) #0
|
||||||
|
self.feature_fields.append(["SHAPE TYPE", 9999999, 1, 'b', 10 ]) #1
|
||||||
|
#SHAPE TYPE NUMBERS
|
||||||
|
#0, circle
|
||||||
|
#1 square
|
||||||
|
#2 Square Rounded Corners
|
||||||
|
#3 Square Bevel Corners
|
||||||
|
#4 triangle
|
||||||
|
#5 diamond
|
||||||
|
#8 Star
|
||||||
|
#10 line
|
||||||
|
#12 PNG
|
||||||
|
#22 line text
|
||||||
|
self.feature_fields.append(["AC Density", 9999999, 4, 'z', [75,0,0,0] ]) #2 [ACdensity,color 0 or 8, ?,?]
|
||||||
|
self.feature_fields.append(["?b(1)" , 9999999, 1, 'z', [134] ]) #3 #solid fill 134
|
||||||
|
self.feature_fields.append(["AC cnt" , 9999999, 1, 'z', [2] ]) #4 This needs to be 2 for lines
|
||||||
|
self.feature_fields.append(["?c(1)" , 9999999, 1, 'z', [0] ]) #5
|
||||||
|
self.feature_fields.append(["?d(1)" , 9999999, 1, 'z', [6] ]) #6
|
||||||
|
self.feature_fields.append(["?e(3)" , 9999999, 3, 'z', [0 ,0 ,0 ] ]) #7
|
||||||
|
self.feature_fields.append(["?f(4)" , 9999999, 4, 'i', 16 ]) #8
|
||||||
|
self.feature_fields.append(["ZOOM" , 9999999, 8, 'd', 96 ]) #9
|
||||||
|
self.feature_fields.append(["?g(8)" , 9999999, 8, 'd', 0 ]) #10
|
||||||
|
self.feature_fields.append(["?h(8)" , 9999999, 8, 'd', 0 ]) #11
|
||||||
|
self.feature_fields.append(["?i(8)" , 9999999, 8, 'd', 0 ]) #12
|
||||||
|
self.feature_fields.append(["?j(8)" , 9999999, 8, 'd', 0 ]) #13
|
||||||
|
self.feature_fields.append(["X cent Loc", 9999999, 8, 'd', 0 ]) #14 To the Right of the center of the laser area
|
||||||
|
self.feature_fields.append(["Y cent Loc", 9999999, 8, 'd', 0 ]) #15 Down from the center of the laser area
|
||||||
|
self.feature_fields.append(["Width" , 9999999, 8, 'd', 0 ]) #16
|
||||||
|
self.feature_fields.append(["Height" , 9999999, 8, 'd', 0 ]) #17
|
||||||
|
self.feature_fields.append(["Pen Width" , 9999999, 8, 'd', 0.025 ]) #18
|
||||||
|
self.feature_fields.append(["AC Line" , 9999999, 8, 'd', 0.127 ]) #19
|
||||||
|
self.feature_fields.append(["Rot(deg)" , 9999999, 8, 'd', 0 ]) #20
|
||||||
|
self.feature_fields.append(["Corner Rad", 9999999, 8, 'd', 0 ]) #21
|
||||||
|
self.feature_fields.append(["?k(8)" , 9999999, 8, 'd', 0 ]) #22
|
||||||
|
self.feature_fields.append(["?l(8)" , 9999999, 8, 'd', 0 ]) #23
|
||||||
|
self.feature_fields.append(["?m(8)" , 9999999, 8, 'd', 0 ]) #24
|
||||||
|
self.feature_fields.append(["?n(4)" , 9999999, 4, 'i', 4 ]) #25
|
||||||
|
self.feature_fields.append(["?o(4)" , 9999999, 4, 'i', 0 ]) #26
|
||||||
|
self.feature_fields.append(["?p(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #27
|
||||||
|
self.feature_fields.append(["?q(4)" , 9999999, 4, 'z', [255,255,255,0 ] ]) #28
|
||||||
|
self.feature_fields.append(["?r(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #29
|
||||||
|
self.feature_fields.append(["string_len", 9999999, 4, 'i', 0 ]) #30
|
||||||
|
self.feature_fields.append(["filename" , 9999999, 4, 'x', "\000" ]) #31
|
||||||
|
self.feature_fields.append(["?u(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #32
|
||||||
|
self.feature_fields.append(["ACtexture1", 9999999, 4, 'z', [255,255,255,255] ]) #33 [0 ,0 ,0 ,0 ]
|
||||||
|
self.feature_fields.append(["ACtexture2", 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #34
|
||||||
|
self.feature_fields.append(["?v(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #35
|
||||||
|
self.feature_fields.append(["?w(4)" , 9999999, 4, 'z', [2 ,0 ,0 ,0 ] ]) #36
|
||||||
|
self.feature_fields.append(["data length", 9999999, 4, 'i', 2 ]) #37 needs to be 2 for line
|
||||||
|
|
||||||
|
self.feature_appendix = []
|
||||||
|
for i in range(13):
|
||||||
|
self.feature_appendix.append([])
|
||||||
|
## Appendix values for Line
|
||||||
|
self.feature_appendix[10].append(["line X1", 9999999, 4, 'i', -10000 ]) #position as 1000*value
|
||||||
|
self.feature_appendix[10].append(["line Y1", 9999999, 4, 'i', -10000 ]) #position as 1000*value
|
||||||
|
self.feature_appendix[10].append(["line X2", 9999999, 4, 'i', 10000 ]) #position as 1000*value
|
||||||
|
self.feature_appendix[10].append(["line Y2", 9999999, 4, 'i', 10000 ]) #position as 1000*value
|
||||||
|
self.feature_appendix[10].append(["lineEND", 9999999, 4, 'i', 0 ])
|
||||||
|
##Appendix values for PNG
|
||||||
|
self.feature_appendix[12].append(["PNGdata", 9999999, 0, 't', "" ])
|
||||||
|
self.feature_appendix[12].append(["PNGend" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ])
|
||||||
|
|
||||||
|
|
||||||
|
def lyz_read(self,loc,len,type,bf):
|
||||||
|
#try:
|
||||||
|
if 1==1:
|
||||||
|
#bf.seek(loc)
|
||||||
|
if type=='t':
|
||||||
|
data = bf.read(len)
|
||||||
|
elif type == 'z':
|
||||||
|
data = []
|
||||||
|
for i in range(len):
|
||||||
|
data.append(ord(bf.read(1)))
|
||||||
|
elif type == 'x':
|
||||||
|
data = ""
|
||||||
|
for i in range(0,len,4):
|
||||||
|
data_temp = bf.read(4)
|
||||||
|
data = data + data_temp[0]
|
||||||
|
else:
|
||||||
|
data = struct.unpack(type, bf.read(len))[0]
|
||||||
|
return data
|
||||||
|
#except:
|
||||||
|
# print("Error Reading data (lyz_read)")
|
||||||
|
# return []
|
||||||
|
|
||||||
|
|
||||||
|
def lyz_write(self,data,type,bf):
|
||||||
|
#print("type,data: ",type,data)
|
||||||
|
if type=='t':
|
||||||
|
#print("data:",data)
|
||||||
|
#bf.write(data)
|
||||||
|
try:
|
||||||
|
bf.write(data)
|
||||||
|
except:
|
||||||
|
bf.write(data.encode())
|
||||||
|
elif type == 'z':
|
||||||
|
for i in range(len(data)):
|
||||||
|
#bf.write(chr(data[i]))
|
||||||
|
bf.write(struct.pack('B',data[i]))
|
||||||
|
elif type == 'x':
|
||||||
|
for char in data:
|
||||||
|
bf.write(char.encode())
|
||||||
|
bf.write(struct.pack('B',0))
|
||||||
|
bf.write(struct.pack('B',0))
|
||||||
|
bf.write(struct.pack('B',0))
|
||||||
|
else:
|
||||||
|
bf.write(struct.pack(type,data))
|
||||||
|
|
||||||
|
def read_header(self,f):
|
||||||
|
self.header_data=[]
|
||||||
|
for line in self.header_fields:
|
||||||
|
#pos = line[1]
|
||||||
|
len = line[2]
|
||||||
|
typ = line[3]
|
||||||
|
self.header_data.append(self.lyz_read(None,len,typ,f))
|
||||||
|
|
||||||
|
|
||||||
|
def read_feature(self,f):
|
||||||
|
feature_data=[]
|
||||||
|
for i in range(len(self.feature_fields)):
|
||||||
|
length = self.feature_fields[i][2]
|
||||||
|
typ = self.feature_fields[i][3]
|
||||||
|
|
||||||
|
if i==31 and feature_data[1]==12:
|
||||||
|
string_length = feature_data[-1]*4
|
||||||
|
feature_data.append(self.lyz_read(None,string_length,typ,f))
|
||||||
|
else:
|
||||||
|
feature_data.append(self.lyz_read(None,length,typ,f))
|
||||||
|
|
||||||
|
#if i==30 and feature_data[1]==12:
|
||||||
|
# self.feature_fields[i+1][2] = feature_data[-1]*4
|
||||||
|
|
||||||
|
|
||||||
|
feat_type = feature_data[1]
|
||||||
|
if feat_type==10 or feat_type==12:
|
||||||
|
for i in range(len(self.feature_appendix[feat_type])):
|
||||||
|
if feat_type==12 and i==0:
|
||||||
|
length = feature_data[-1]
|
||||||
|
else:
|
||||||
|
length = self.feature_appendix[feat_type][i][2]
|
||||||
|
typ = self.feature_appendix[feat_type][i][3]
|
||||||
|
feature_data.append(self.lyz_read(None,length,typ,f))
|
||||||
|
return feature_data
|
||||||
|
|
||||||
|
|
||||||
|
def setup_new_header(self):
|
||||||
|
self.header_data=[]
|
||||||
|
for line in self.header_fields:
|
||||||
|
data = line[4]
|
||||||
|
self.header_data.append(data)
|
||||||
|
|
||||||
|
|
||||||
|
def add_line(self,x1,y1,x2,y2,Pen_Width=.025):
|
||||||
|
feature_data=[]
|
||||||
|
for line in self.feature_fields:
|
||||||
|
data = line[4]
|
||||||
|
feature_data.append(data)
|
||||||
|
feature_data.append(int(x1*1000.0))
|
||||||
|
feature_data.append(int(y1*1000.0))
|
||||||
|
feature_data.append(int(x2*1000.0))
|
||||||
|
feature_data.append(int(y2*1000.0))
|
||||||
|
feature_data.append(0)
|
||||||
|
feature_data[1]=10 #set type to line
|
||||||
|
feature_data[4]=[2] #Not sure what this is for lines but it needs to be 2
|
||||||
|
feature_data[18]=Pen_Width
|
||||||
|
|
||||||
|
self.header_data[2]=self.header_data[2]+1
|
||||||
|
self.feature_list.append(feature_data)
|
||||||
|
|
||||||
|
def add_png(self,PNG_DATA,Xsixe,Ysize):
|
||||||
|
filename="filename"
|
||||||
|
feature_data=[]
|
||||||
|
for line in self.feature_fields:
|
||||||
|
data = line[4]
|
||||||
|
feature_data.append(data)
|
||||||
|
feature_data.append(PNG_DATA)
|
||||||
|
feature_data.append([0,0,0,0,0,0,0,0])
|
||||||
|
feature_data[1] = 12 # set type to PNG
|
||||||
|
feature_data[3] = [150]
|
||||||
|
feature_data[2] = [75, 4, 0, 144]
|
||||||
|
feature_data[4] = [0] # Number of Anti-Counterfeit lines
|
||||||
|
feature_data[6] = [12] # if this is not set to [12] the image does not get passed to the engrave window
|
||||||
|
feature_data[16] = Xsixe # set PNG width
|
||||||
|
feature_data[17] = Ysize # set PNG height
|
||||||
|
feature_data[18] = 1.0
|
||||||
|
feature_data[26] = 16777215
|
||||||
|
feature_data[30]= len(filename) # set filename length
|
||||||
|
feature_data[31]= filename # set filename
|
||||||
|
feature_data[33]=[0 ,0 ,0 ,0 ]
|
||||||
|
feature_data[34]=[255,255,255,255]
|
||||||
|
feature_data[36]=[226, 29, 5, 175]
|
||||||
|
feature_data[37]= len(PNG_DATA) # set PNG data length
|
||||||
|
|
||||||
|
self.header_data[2]=self.header_data[2]+1
|
||||||
|
self.feature_list.append(feature_data)
|
||||||
|
|
||||||
|
def set_size(self,Xsize,Ysize):
|
||||||
|
self.header_data[15]=Xsize
|
||||||
|
self.header_data[16]=Ysize
|
||||||
|
|
||||||
|
def set_margin(self,margin):
|
||||||
|
self.header_data[17]=margin/2
|
||||||
|
self.header_data[18]=margin/2
|
||||||
|
self.header_data[19]=margin/2
|
||||||
|
self.header_data[20]=margin/2
|
||||||
|
|
||||||
|
def find_PNG(self,f):
|
||||||
|
self.PNGstart = -1
|
||||||
|
self.PNGend = -1
|
||||||
|
f.seek(0)
|
||||||
|
loc=0
|
||||||
|
flag = True
|
||||||
|
while flag:
|
||||||
|
byte=f.read(1)
|
||||||
|
if byte=="":
|
||||||
|
flag=False
|
||||||
|
if byte =="P":
|
||||||
|
if byte =="N":
|
||||||
|
if byte =="G":
|
||||||
|
self.PNGstart = f.tell()-4
|
||||||
|
if byte =="E":
|
||||||
|
if byte =="N":
|
||||||
|
if byte =="D":
|
||||||
|
self.PNGend = f.tell()+4
|
||||||
|
flag = False
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(self, file_name):
|
||||||
|
with open(file_name, "rb") as f:
|
||||||
|
self.find_PNG(f)
|
||||||
|
PNGlen = self.PNGend-self.PNGstart
|
||||||
|
self.png_message = "PNGlen: ",PNGlen
|
||||||
|
self.read_header(f)
|
||||||
|
for i in range(self.header_data[2]):
|
||||||
|
data = self.read_feature(f)
|
||||||
|
self.feature_list.append(data)
|
||||||
|
|
||||||
|
self.left_over = f.read( self.header_data[1]-4-f.tell() )
|
||||||
|
|
||||||
|
self.EOF = ""
|
||||||
|
byte = f.read(1)
|
||||||
|
while byte!="":
|
||||||
|
self.EOF=self.EOF+byte
|
||||||
|
byte = f.read(1)
|
||||||
|
#print(possible_values(200+217,348-200,'d',f))
|
||||||
|
|
||||||
|
|
||||||
|
def write_file(self, file_name):
|
||||||
|
with open(file_name, "wb") as f:
|
||||||
|
for i in range(len(self.header_fields)):
|
||||||
|
typ = self.header_fields[i][3]
|
||||||
|
data = self.header_data[i]
|
||||||
|
self.lyz_write(data,typ,f)
|
||||||
|
|
||||||
|
for j in range(len(self.feature_list)):
|
||||||
|
for i in range(len(self.feature_fields)):
|
||||||
|
typ = self.feature_fields[i][3]
|
||||||
|
data = self.feature_list[j][i]
|
||||||
|
#print(j,i," typ,data: ",typ,data)
|
||||||
|
self.lyz_write(data,typ,f)
|
||||||
|
|
||||||
|
feat_type=self.feature_list[j][1]
|
||||||
|
if feat_type==10 or feat_type==12:
|
||||||
|
appendix_data=[]
|
||||||
|
for i in range(len(self.feature_appendix[feat_type])):
|
||||||
|
typ = self.feature_appendix[feat_type][i][3]
|
||||||
|
data = self.feature_list[j][i+len(self.feature_fields)] #appendix_data
|
||||||
|
#print(j,i," typ,data: "typ,data)
|
||||||
|
self.lyz_write(data,typ,f)
|
||||||
|
|
||||||
|
f.write("@EOF".encode())
|
||||||
|
length=f.tell()
|
||||||
|
f.seek(4)
|
||||||
|
f.write(struct.pack('i',length))
|
||||||
|
|
||||||
|
|
||||||
|
def print_header(self):
|
||||||
|
print("\nHEADER DATA:")
|
||||||
|
print("--------------------")
|
||||||
|
for i in range(len(self.header_data)):
|
||||||
|
print("%11s : " %(self.header_fields[i][0]),self.header_data[i])
|
||||||
|
|
||||||
|
|
||||||
|
def print_features(self):
|
||||||
|
for i in range(len(self.feature_list)):
|
||||||
|
print("\nFEATURE #%d:" %(i+1))
|
||||||
|
print("--------------------")
|
||||||
|
feature = self.feature_list[i]
|
||||||
|
for j in range(len(self.feature_fields)):
|
||||||
|
try:
|
||||||
|
print("%11s : " %(self.feature_fields[j][0]),feature[j])
|
||||||
|
except:
|
||||||
|
print("error")
|
||||||
|
feat_type = feature[1]
|
||||||
|
if feat_type==10 or feat_type==12:
|
||||||
|
print("---LINE COORDS---")
|
||||||
|
for j in range(len(self.feature_appendix[feat_type])):
|
||||||
|
jj = j+len(self.feature_fields)
|
||||||
|
if feat_type==12 and jj==38:
|
||||||
|
print("%11s : " %(self.feature_appendix[feat_type][j][0]),"....")
|
||||||
|
else:
|
||||||
|
print("%11s : " %(self.feature_appendix[feat_type][j][0]),feature[jj])
|
||||||
|
print("--------------------")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
###############################
|
||||||
|
try:
|
||||||
|
file_name = sys.argv[1]
|
||||||
|
print("input: ",file_name)
|
||||||
|
except:
|
||||||
|
file_name = ""
|
||||||
|
###############################
|
||||||
|
try:
|
||||||
|
file_out = sys.argv[2]
|
||||||
|
print("output: ",file_name)
|
||||||
|
except:
|
||||||
|
file_out = ""
|
||||||
|
###############################
|
||||||
|
|
||||||
|
if file_name=="test":
|
||||||
|
LYZ=LYZ_CLASS()
|
||||||
|
LYZ.setup_new_header()
|
||||||
|
#image_file = "squigles.png"
|
||||||
|
#image_file = "drawing_mod.png"
|
||||||
|
image_file = "temp.png"
|
||||||
|
with open(image_file, 'rb') as f:
|
||||||
|
PNG_DATA = f.read()
|
||||||
|
LYZ.add_png(PNG_DATA,20,20)
|
||||||
|
|
||||||
|
LYZ.add_line(5,5,-10,-10,0.025)
|
||||||
|
#LYZ.print_header()
|
||||||
|
#LYZ.print_features()
|
||||||
|
LYZ.write_file("test.lyz")
|
||||||
|
else:
|
||||||
|
if file_name!="":
|
||||||
|
LYZ=LYZ_CLASS()
|
||||||
|
LYZ.read_file(file_name)
|
||||||
|
LYZ.print_header()
|
||||||
|
LYZ.print_features()
|
||||||
|
print("LEFTOVER :", LYZ.left_over)
|
||||||
|
print("EOF :",LYZ.EOF)
|
||||||
|
|
||||||
|
if file_out!="":
|
||||||
|
LYZ.write_file(file_out)
|
209
extensions/fablabchemnitz/laserdraw_export/lyz_simplepath.py
Normal file
209
extensions/fablabchemnitz/laserdraw_export/lyz_simplepath.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
simplepath.py
|
||||||
|
functions for digesting paths into a simple list structure
|
||||||
|
|
||||||
|
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import re, math
|
||||||
|
|
||||||
|
def lexPath(d):
|
||||||
|
"""
|
||||||
|
returns and iterator that breaks path data
|
||||||
|
identifies command and parameter tokens
|
||||||
|
"""
|
||||||
|
offset = 0
|
||||||
|
length = len(d)
|
||||||
|
delim = re.compile(r'[ \t\r\n,]+')
|
||||||
|
command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]')
|
||||||
|
parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
|
||||||
|
while 1:
|
||||||
|
m = delim.match(d, offset)
|
||||||
|
if m:
|
||||||
|
offset = m.end()
|
||||||
|
if offset >= length:
|
||||||
|
break
|
||||||
|
m = command.match(d, offset)
|
||||||
|
if m:
|
||||||
|
yield [d[offset:m.end()], True]
|
||||||
|
offset = m.end()
|
||||||
|
continue
|
||||||
|
m = parameter.match(d, offset)
|
||||||
|
if m:
|
||||||
|
yield [d[offset:m.end()], False]
|
||||||
|
offset = m.end()
|
||||||
|
continue
|
||||||
|
#TODO: create new exception
|
||||||
|
raise Exception('Invalid path data!')
|
||||||
|
'''
|
||||||
|
pathdefs = {commandfamily:
|
||||||
|
[
|
||||||
|
implicitnext,
|
||||||
|
#params,
|
||||||
|
[casts,cast,cast],
|
||||||
|
[coord type,x,y,0]
|
||||||
|
]}
|
||||||
|
'''
|
||||||
|
pathdefs = {
|
||||||
|
'M':['L', 2, [float, float], ['x','y']],
|
||||||
|
'L':['L', 2, [float, float], ['x','y']],
|
||||||
|
'H':['H', 1, [float], ['x']],
|
||||||
|
'V':['V', 1, [float], ['y']],
|
||||||
|
'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']],
|
||||||
|
'S':['S', 4, [float, float, float, float], ['x','y','x','y']],
|
||||||
|
'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']],
|
||||||
|
'T':['T', 2, [float, float], ['x','y']],
|
||||||
|
'A':['A', 7, [float, float, float, int, int, float, float], ['r','r','a',0,'s','x','y']],
|
||||||
|
'Z':['L', 0, [], []]
|
||||||
|
}
|
||||||
|
def parsePath(d):
|
||||||
|
"""
|
||||||
|
Parse SVG path and return an array of segments.
|
||||||
|
Removes all shorthand notation.
|
||||||
|
Converts coordinates to absolute.
|
||||||
|
"""
|
||||||
|
retval = []
|
||||||
|
lexer = lexPath(d)
|
||||||
|
|
||||||
|
pen = (0.0,0.0)
|
||||||
|
subPathStart = pen
|
||||||
|
lastControl = pen
|
||||||
|
lastCommand = ''
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
token, isCommand = next(lexer)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
params = []
|
||||||
|
needParam = True
|
||||||
|
if isCommand:
|
||||||
|
if not lastCommand and token.upper() != 'M':
|
||||||
|
raise Exception('Invalid path, must begin with moveto.')
|
||||||
|
else:
|
||||||
|
command = token
|
||||||
|
else:
|
||||||
|
#command was omited
|
||||||
|
#use last command's implicit next command
|
||||||
|
needParam = False
|
||||||
|
if lastCommand:
|
||||||
|
if lastCommand.isupper():
|
||||||
|
command = pathdefs[lastCommand][0]
|
||||||
|
else:
|
||||||
|
command = pathdefs[lastCommand.upper()][0].lower()
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid path, no initial command.')
|
||||||
|
numParams = pathdefs[command.upper()][1]
|
||||||
|
while numParams > 0:
|
||||||
|
if needParam:
|
||||||
|
try:
|
||||||
|
token, isCommand = next(lexer)
|
||||||
|
if isCommand:
|
||||||
|
raise Exception('Invalid number of parameters')
|
||||||
|
except StopIteration:
|
||||||
|
raise Exception('Unexpected end of path')
|
||||||
|
cast = pathdefs[command.upper()][2][-numParams]
|
||||||
|
param = cast(token)
|
||||||
|
if command.islower():
|
||||||
|
if pathdefs[command.upper()][3][-numParams]=='x':
|
||||||
|
param += pen[0]
|
||||||
|
elif pathdefs[command.upper()][3][-numParams]=='y':
|
||||||
|
param += pen[1]
|
||||||
|
params.append(param)
|
||||||
|
needParam = True
|
||||||
|
numParams -= 1
|
||||||
|
#segment is now absolute so
|
||||||
|
outputCommand = command.upper()
|
||||||
|
|
||||||
|
#Flesh out shortcut notation
|
||||||
|
if outputCommand in ('H','V'):
|
||||||
|
if outputCommand == 'H':
|
||||||
|
params.append(pen[1])
|
||||||
|
if outputCommand == 'V':
|
||||||
|
params.insert(0,pen[0])
|
||||||
|
outputCommand = 'L'
|
||||||
|
if outputCommand in ('S','T'):
|
||||||
|
params.insert(0,pen[1]+(pen[1]-lastControl[1]))
|
||||||
|
params.insert(0,pen[0]+(pen[0]-lastControl[0]))
|
||||||
|
if outputCommand == 'S':
|
||||||
|
outputCommand = 'C'
|
||||||
|
if outputCommand == 'T':
|
||||||
|
outputCommand = 'Q'
|
||||||
|
|
||||||
|
#current values become "last" values
|
||||||
|
if outputCommand == 'M':
|
||||||
|
subPathStart = tuple(params[0:2])
|
||||||
|
pen = subPathStart
|
||||||
|
if outputCommand == 'Z':
|
||||||
|
pen = subPathStart
|
||||||
|
else:
|
||||||
|
pen = tuple(params[-2:])
|
||||||
|
|
||||||
|
if outputCommand in ('Q','C'):
|
||||||
|
lastControl = tuple(params[-4:-2])
|
||||||
|
else:
|
||||||
|
lastControl = pen
|
||||||
|
lastCommand = command
|
||||||
|
|
||||||
|
retval.append([outputCommand,params])
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def formatPath(a):
|
||||||
|
"""Format SVG path data from an array"""
|
||||||
|
return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a])
|
||||||
|
|
||||||
|
def translatePath(p, x, y):
|
||||||
|
for cmd,params in p:
|
||||||
|
defs = pathdefs[cmd]
|
||||||
|
for i in range(defs[1]):
|
||||||
|
if defs[3][i] == 'x':
|
||||||
|
params[i] += x
|
||||||
|
elif defs[3][i] == 'y':
|
||||||
|
params[i] += y
|
||||||
|
|
||||||
|
def scalePath(p, x, y):
|
||||||
|
for cmd,params in p:
|
||||||
|
defs = pathdefs[cmd]
|
||||||
|
for i in range(defs[1]):
|
||||||
|
if defs[3][i] == 'x':
|
||||||
|
params[i] *= x
|
||||||
|
elif defs[3][i] == 'y':
|
||||||
|
params[i] *= y
|
||||||
|
elif defs[3][i] == 'r': # radius parameter
|
||||||
|
params[i] *= x
|
||||||
|
elif defs[3][i] == 's': # sweep-flag parameter
|
||||||
|
if x*y < 0:
|
||||||
|
params[i] = 1 - params[i]
|
||||||
|
elif defs[3][i] == 'a': # x-axis-rotation angle
|
||||||
|
if y < 0:
|
||||||
|
params[i] = - params[i]
|
||||||
|
|
||||||
|
def rotatePath(p, a, cx = 0, cy = 0):
|
||||||
|
if a == 0:
|
||||||
|
return p
|
||||||
|
for cmd,params in p:
|
||||||
|
defs = pathdefs[cmd]
|
||||||
|
for i in range(defs[1]):
|
||||||
|
if defs[3][i] == 'x':
|
||||||
|
x = params[i] - cx
|
||||||
|
y = params[i + 1] - cy
|
||||||
|
r = math.sqrt((x**2) + (y**2))
|
||||||
|
if r != 0:
|
||||||
|
theta = math.atan2(y, x) + a
|
||||||
|
params[i] = (r * math.cos(theta)) + cx
|
||||||
|
params[i + 1] = (r * math.sin(theta)) + cy
|
242
extensions/fablabchemnitz/laserdraw_export/lyz_simplestyle.py
Normal file
242
extensions/fablabchemnitz/laserdraw_export/lyz_simplestyle.py
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
simplestyle.py
|
||||||
|
Two simple functions for working with inline css
|
||||||
|
and some color handling on top.
|
||||||
|
|
||||||
|
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
svgcolors={
|
||||||
|
'aliceblue':'#f0f8ff',
|
||||||
|
'antiquewhite':'#faebd7',
|
||||||
|
'aqua':'#00ffff',
|
||||||
|
'aquamarine':'#7fffd4',
|
||||||
|
'azure':'#f0ffff',
|
||||||
|
'beige':'#f5f5dc',
|
||||||
|
'bisque':'#ffe4c4',
|
||||||
|
'black':'#000000',
|
||||||
|
'blanchedalmond':'#ffebcd',
|
||||||
|
'blue':'#0000ff',
|
||||||
|
'blueviolet':'#8a2be2',
|
||||||
|
'brown':'#a52a2a',
|
||||||
|
'burlywood':'#deb887',
|
||||||
|
'cadetblue':'#5f9ea0',
|
||||||
|
'chartreuse':'#7fff00',
|
||||||
|
'chocolate':'#d2691e',
|
||||||
|
'coral':'#ff7f50',
|
||||||
|
'cornflowerblue':'#6495ed',
|
||||||
|
'cornsilk':'#fff8dc',
|
||||||
|
'crimson':'#dc143c',
|
||||||
|
'cyan':'#00ffff',
|
||||||
|
'darkblue':'#00008b',
|
||||||
|
'darkcyan':'#008b8b',
|
||||||
|
'darkgoldenrod':'#b8860b',
|
||||||
|
'darkgray':'#a9a9a9',
|
||||||
|
'darkgreen':'#006400',
|
||||||
|
'darkgrey':'#a9a9a9',
|
||||||
|
'darkkhaki':'#bdb76b',
|
||||||
|
'darkmagenta':'#8b008b',
|
||||||
|
'darkolivegreen':'#556b2f',
|
||||||
|
'darkorange':'#ff8c00',
|
||||||
|
'darkorchid':'#9932cc',
|
||||||
|
'darkred':'#8b0000',
|
||||||
|
'darksalmon':'#e9967a',
|
||||||
|
'darkseagreen':'#8fbc8f',
|
||||||
|
'darkslateblue':'#483d8b',
|
||||||
|
'darkslategray':'#2f4f4f',
|
||||||
|
'darkslategrey':'#2f4f4f',
|
||||||
|
'darkturquoise':'#00ced1',
|
||||||
|
'darkviolet':'#9400d3',
|
||||||
|
'deeppink':'#ff1493',
|
||||||
|
'deepskyblue':'#00bfff',
|
||||||
|
'dimgray':'#696969',
|
||||||
|
'dimgrey':'#696969',
|
||||||
|
'dodgerblue':'#1e90ff',
|
||||||
|
'firebrick':'#b22222',
|
||||||
|
'floralwhite':'#fffaf0',
|
||||||
|
'forestgreen':'#228b22',
|
||||||
|
'fuchsia':'#ff00ff',
|
||||||
|
'gainsboro':'#dcdcdc',
|
||||||
|
'ghostwhite':'#f8f8ff',
|
||||||
|
'gold':'#ffd700',
|
||||||
|
'goldenrod':'#daa520',
|
||||||
|
'gray':'#808080',
|
||||||
|
'grey':'#808080',
|
||||||
|
'green':'#008000',
|
||||||
|
'greenyellow':'#adff2f',
|
||||||
|
'honeydew':'#f0fff0',
|
||||||
|
'hotpink':'#ff69b4',
|
||||||
|
'indianred':'#cd5c5c',
|
||||||
|
'indigo':'#4b0082',
|
||||||
|
'ivory':'#fffff0',
|
||||||
|
'khaki':'#f0e68c',
|
||||||
|
'lavender':'#e6e6fa',
|
||||||
|
'lavenderblush':'#fff0f5',
|
||||||
|
'lawngreen':'#7cfc00',
|
||||||
|
'lemonchiffon':'#fffacd',
|
||||||
|
'lightblue':'#add8e6',
|
||||||
|
'lightcoral':'#f08080',
|
||||||
|
'lightcyan':'#e0ffff',
|
||||||
|
'lightgoldenrodyellow':'#fafad2',
|
||||||
|
'lightgray':'#d3d3d3',
|
||||||
|
'lightgreen':'#90ee90',
|
||||||
|
'lightgrey':'#d3d3d3',
|
||||||
|
'lightpink':'#ffb6c1',
|
||||||
|
'lightsalmon':'#ffa07a',
|
||||||
|
'lightseagreen':'#20b2aa',
|
||||||
|
'lightskyblue':'#87cefa',
|
||||||
|
'lightslategray':'#778899',
|
||||||
|
'lightslategrey':'#778899',
|
||||||
|
'lightsteelblue':'#b0c4de',
|
||||||
|
'lightyellow':'#ffffe0',
|
||||||
|
'lime':'#00ff00',
|
||||||
|
'limegreen':'#32cd32',
|
||||||
|
'linen':'#faf0e6',
|
||||||
|
'magenta':'#ff00ff',
|
||||||
|
'maroon':'#800000',
|
||||||
|
'mediumaquamarine':'#66cdaa',
|
||||||
|
'mediumblue':'#0000cd',
|
||||||
|
'mediumorchid':'#ba55d3',
|
||||||
|
'mediumpurple':'#9370db',
|
||||||
|
'mediumseagreen':'#3cb371',
|
||||||
|
'mediumslateblue':'#7b68ee',
|
||||||
|
'mediumspringgreen':'#00fa9a',
|
||||||
|
'mediumturquoise':'#48d1cc',
|
||||||
|
'mediumvioletred':'#c71585',
|
||||||
|
'midnightblue':'#191970',
|
||||||
|
'mintcream':'#f5fffa',
|
||||||
|
'mistyrose':'#ffe4e1',
|
||||||
|
'moccasin':'#ffe4b5',
|
||||||
|
'navajowhite':'#ffdead',
|
||||||
|
'navy':'#000080',
|
||||||
|
'oldlace':'#fdf5e6',
|
||||||
|
'olive':'#808000',
|
||||||
|
'olivedrab':'#6b8e23',
|
||||||
|
'orange':'#ffa500',
|
||||||
|
'orangered':'#ff4500',
|
||||||
|
'orchid':'#da70d6',
|
||||||
|
'palegoldenrod':'#eee8aa',
|
||||||
|
'palegreen':'#98fb98',
|
||||||
|
'paleturquoise':'#afeeee',
|
||||||
|
'palevioletred':'#db7093',
|
||||||
|
'papayawhip':'#ffefd5',
|
||||||
|
'peachpuff':'#ffdab9',
|
||||||
|
'peru':'#cd853f',
|
||||||
|
'pink':'#ffc0cb',
|
||||||
|
'plum':'#dda0dd',
|
||||||
|
'powderblue':'#b0e0e6',
|
||||||
|
'purple':'#800080',
|
||||||
|
'rebeccapurple':'#663399',
|
||||||
|
'red':'#ff0000',
|
||||||
|
'rosybrown':'#bc8f8f',
|
||||||
|
'royalblue':'#4169e1',
|
||||||
|
'saddlebrown':'#8b4513',
|
||||||
|
'salmon':'#fa8072',
|
||||||
|
'sandybrown':'#f4a460',
|
||||||
|
'seagreen':'#2e8b57',
|
||||||
|
'seashell':'#fff5ee',
|
||||||
|
'sienna':'#a0522d',
|
||||||
|
'silver':'#c0c0c0',
|
||||||
|
'skyblue':'#87ceeb',
|
||||||
|
'slateblue':'#6a5acd',
|
||||||
|
'slategray':'#708090',
|
||||||
|
'slategrey':'#708090',
|
||||||
|
'snow':'#fffafa',
|
||||||
|
'springgreen':'#00ff7f',
|
||||||
|
'steelblue':'#4682b4',
|
||||||
|
'tan':'#d2b48c',
|
||||||
|
'teal':'#008080',
|
||||||
|
'thistle':'#d8bfd8',
|
||||||
|
'tomato':'#ff6347',
|
||||||
|
'turquoise':'#40e0d0',
|
||||||
|
'violet':'#ee82ee',
|
||||||
|
'wheat':'#f5deb3',
|
||||||
|
'white':'#ffffff',
|
||||||
|
'whitesmoke':'#f5f5f5',
|
||||||
|
'yellow':'#ffff00',
|
||||||
|
'yellowgreen':'#9acd32'
|
||||||
|
}
|
||||||
|
|
||||||
|
def parseStyle(s):
|
||||||
|
"""Create a dictionary from the value of an inline style attribute"""
|
||||||
|
if s is None:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
return dict([[x.strip() for x in i.split(":")] for i in s.split(";") if len(i.strip())])
|
||||||
|
|
||||||
|
def formatStyle(a):
|
||||||
|
"""Format an inline style attribute from a dictionary"""
|
||||||
|
return ";".join([att+":"+str(val) for att,val in a.iteritems()])
|
||||||
|
|
||||||
|
def isColor(c):
|
||||||
|
"""Determine if its a color we can use. If not, leave it unchanged."""
|
||||||
|
if c.startswith('#') and (len(c)==4 or len(c)==7):
|
||||||
|
return True
|
||||||
|
if c.lower() in svgcolors.keys():
|
||||||
|
return True
|
||||||
|
#might be "none" or some undefined color constant or rgb()
|
||||||
|
#however, rgb() shouldnt occur at this point
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parseColor(c):
|
||||||
|
"""Creates a rgb int array"""
|
||||||
|
tmp = svgcolors.get(c.lower())
|
||||||
|
if tmp is not None:
|
||||||
|
c = tmp
|
||||||
|
elif c.startswith('#') and len(c)==4:
|
||||||
|
c='#'+c[1:2]+c[1:2]+c[2:3]+c[2:3]+c[3:]+c[3:]
|
||||||
|
elif c.startswith('rgb('):
|
||||||
|
# remove the rgb(...) stuff
|
||||||
|
tmp = c.strip()[4:-1]
|
||||||
|
numbers = [number.strip() for number in tmp.split(',')]
|
||||||
|
converted_numbers = []
|
||||||
|
if len(numbers) == 3:
|
||||||
|
for num in numbers:
|
||||||
|
if num.endswith(r'%'):
|
||||||
|
converted_numbers.append(int(float(num[0:-1])*255/100))
|
||||||
|
else:
|
||||||
|
converted_numbers.append(int(num))
|
||||||
|
return tuple(converted_numbers)
|
||||||
|
else:
|
||||||
|
return (0,0,0)
|
||||||
|
try:
|
||||||
|
r=int(c[1:3],16)
|
||||||
|
g=int(c[3:5],16)
|
||||||
|
b=int(c[5:],16)
|
||||||
|
except:
|
||||||
|
# unknown color ...
|
||||||
|
# Return a default color. Maybe not the best thing to do but probably
|
||||||
|
# better than raising an exception.
|
||||||
|
return(0,0,0)
|
||||||
|
return (r,g,b)
|
||||||
|
|
||||||
|
def formatColoria(a):
|
||||||
|
"""int array to #rrggbb"""
|
||||||
|
return '#%02x%02x%02x' % (a[0],a[1],a[2])
|
||||||
|
|
||||||
|
def formatColorfa(a):
|
||||||
|
"""float array to #rrggbb"""
|
||||||
|
return '#%02x%02x%02x' % (int(round(a[0]*255)),int(round(a[1]*255)),int(round(a[2]*255)))
|
||||||
|
|
||||||
|
def formatColor3i(r,g,b):
|
||||||
|
"""3 ints to #rrggbb"""
|
||||||
|
return '#%02x%02x%02x' % (r,g,b)
|
||||||
|
|
||||||
|
def formatColor3f(r,g,b):
|
||||||
|
"""3 floats to #rrggbb"""
|
||||||
|
return '#%02x%02x%02x' % (int(round(r*255)),int(round(g*255)),int(round(b*255)))
|
@ -0,0 +1,259 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
|
||||||
|
Copyright (C) 2010 Alvin Penner, penner@vaxxine.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.
|
||||||
|
barraud@math.univ-lille1.fr
|
||||||
|
|
||||||
|
This code defines several functions to make handling of transform
|
||||||
|
attribute easier.
|
||||||
|
'''
|
||||||
|
import inkex, cubicsuperpath, bezmisc, simplestyle
|
||||||
|
import copy, math, re
|
||||||
|
|
||||||
|
def parseTransform(transf,mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
|
||||||
|
if transf=="" or transf==None:
|
||||||
|
return(mat)
|
||||||
|
stransf = transf.strip()
|
||||||
|
result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?",stransf)
|
||||||
|
#-- translate --
|
||||||
|
if result.group(1)=="translate":
|
||||||
|
args=result.group(2).replace(',',' ').split()
|
||||||
|
dx=float(args[0])
|
||||||
|
if len(args)==1:
|
||||||
|
dy=0.0
|
||||||
|
else:
|
||||||
|
dy=float(args[1])
|
||||||
|
matrix=[[1,0,dx],[0,1,dy]]
|
||||||
|
#-- scale --
|
||||||
|
if result.group(1)=="scale":
|
||||||
|
args=result.group(2).replace(',',' ').split()
|
||||||
|
sx=float(args[0])
|
||||||
|
if len(args)==1:
|
||||||
|
sy=sx
|
||||||
|
else:
|
||||||
|
sy=float(args[1])
|
||||||
|
matrix=[[sx,0,0],[0,sy,0]]
|
||||||
|
#-- rotate --
|
||||||
|
if result.group(1)=="rotate":
|
||||||
|
args=result.group(2).replace(',',' ').split()
|
||||||
|
a=float(args[0])*math.pi/180
|
||||||
|
if len(args)==1:
|
||||||
|
cx,cy=(0.0,0.0)
|
||||||
|
else:
|
||||||
|
cx,cy=list(map(float,args[1:]))
|
||||||
|
matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]]
|
||||||
|
matrix=composeTransform(matrix,[[1,0,-cx],[0,1,-cy]])
|
||||||
|
#-- skewX --
|
||||||
|
if result.group(1)=="skewX":
|
||||||
|
a=float(result.group(2))*math.pi/180
|
||||||
|
matrix=[[1,math.tan(a),0],[0,1,0]]
|
||||||
|
#-- skewY --
|
||||||
|
if result.group(1)=="skewY":
|
||||||
|
a=float(result.group(2))*math.pi/180
|
||||||
|
matrix=[[1,0,0],[math.tan(a),1,0]]
|
||||||
|
#-- matrix --
|
||||||
|
if result.group(1)=="matrix":
|
||||||
|
a11,a21,a12,a22,v1,v2=result.group(2).replace(',',' ').split()
|
||||||
|
matrix=[[float(a11),float(a12),float(v1)], [float(a21),float(a22),float(v2)]]
|
||||||
|
|
||||||
|
matrix=composeTransform(mat,matrix)
|
||||||
|
if result.end() < len(stransf):
|
||||||
|
return(parseTransform(stransf[result.end():], matrix))
|
||||||
|
else:
|
||||||
|
return matrix
|
||||||
|
|
||||||
|
def formatTransform(mat):
|
||||||
|
return ("matrix(%f,%f,%f,%f,%f,%f)" % (mat[0][0], mat[1][0], mat[0][1], mat[1][1], mat[0][2], mat[1][2]))
|
||||||
|
|
||||||
|
def invertTransform(mat):
|
||||||
|
det = mat[0][0]*mat[1][1] - mat[0][1]*mat[1][0]
|
||||||
|
if det !=0: # det is 0 only in case of 0 scaling
|
||||||
|
# invert the rotation/scaling part
|
||||||
|
a11 = mat[1][1]/det
|
||||||
|
a12 = -mat[0][1]/det
|
||||||
|
a21 = -mat[1][0]/det
|
||||||
|
a22 = mat[0][0]/det
|
||||||
|
# invert the translational part
|
||||||
|
a13 = -(a11*mat[0][2] + a12*mat[1][2])
|
||||||
|
a23 = -(a21*mat[0][2] + a22*mat[1][2])
|
||||||
|
return [[a11,a12,a13],[a21,a22,a23]]
|
||||||
|
else:
|
||||||
|
return[[0,0,-mat[0][2]],[0,0,-mat[1][2]]]
|
||||||
|
|
||||||
|
def composeTransform(M1,M2):
|
||||||
|
a11 = M1[0][0]*M2[0][0] + M1[0][1]*M2[1][0]
|
||||||
|
a12 = M1[0][0]*M2[0][1] + M1[0][1]*M2[1][1]
|
||||||
|
a21 = M1[1][0]*M2[0][0] + M1[1][1]*M2[1][0]
|
||||||
|
a22 = M1[1][0]*M2[0][1] + M1[1][1]*M2[1][1]
|
||||||
|
|
||||||
|
v1 = M1[0][0]*M2[0][2] + M1[0][1]*M2[1][2] + M1[0][2]
|
||||||
|
v2 = M1[1][0]*M2[0][2] + M1[1][1]*M2[1][2] + M1[1][2]
|
||||||
|
return [[a11,a12,v1],[a21,a22,v2]]
|
||||||
|
|
||||||
|
def composeParents(node, mat):
|
||||||
|
trans = node.get('transform')
|
||||||
|
if trans:
|
||||||
|
mat = composeTransform(parseTransform(trans), mat)
|
||||||
|
if node.getparent().tag == inkex.addNS('g','svg'):
|
||||||
|
mat = composeParents(node.getparent(), mat)
|
||||||
|
return mat
|
||||||
|
|
||||||
|
def applyTransformToNode(mat,node):
|
||||||
|
m=parseTransform(node.get("transform"))
|
||||||
|
newtransf=formatTransform(composeTransform(mat,m))
|
||||||
|
node.set("transform", newtransf)
|
||||||
|
|
||||||
|
def applyTransformToPoint(mat,pt):
|
||||||
|
x = mat[0][0]*pt[0] + mat[0][1]*pt[1] + mat[0][2]
|
||||||
|
y = mat[1][0]*pt[0] + mat[1][1]*pt[1] + mat[1][2]
|
||||||
|
pt[0]=x
|
||||||
|
pt[1]=y
|
||||||
|
|
||||||
|
def applyTransformToPath(mat,path):
|
||||||
|
for comp in path:
|
||||||
|
for ctl in comp:
|
||||||
|
for pt in ctl:
|
||||||
|
applyTransformToPoint(mat,pt)
|
||||||
|
|
||||||
|
def fuseTransform(node):
|
||||||
|
if node.get('d')==None:
|
||||||
|
#FIXME: how do you raise errors?
|
||||||
|
raise AssertionError('can not fuse "transform" of elements that have no "d" attribute')
|
||||||
|
t = node.get("transform")
|
||||||
|
if t == None:
|
||||||
|
return
|
||||||
|
m = parseTransform(t)
|
||||||
|
d = node.get('d')
|
||||||
|
p = cubicsuperpath.parsePath(d)
|
||||||
|
applyTransformToPath(m,p)
|
||||||
|
node.set('d', cubicsuperpath.formatPath(p))
|
||||||
|
del node.attrib["transform"]
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
##-- Some functions to compute a rough bbox of a given list of objects.
|
||||||
|
##-- this should be shipped out in an separate file...
|
||||||
|
|
||||||
|
def boxunion(b1,b2):
|
||||||
|
if b1 is None:
|
||||||
|
return b2
|
||||||
|
elif b2 is None:
|
||||||
|
return b1
|
||||||
|
else:
|
||||||
|
return((min(b1[0],b2[0]), max(b1[1],b2[1]), min(b1[2],b2[2]), max(b1[3],b2[3])))
|
||||||
|
|
||||||
|
def roughBBox(path):
|
||||||
|
xmin,xMax,ymin,yMax = path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1]
|
||||||
|
for pathcomp in path:
|
||||||
|
for ctl in pathcomp:
|
||||||
|
for pt in ctl:
|
||||||
|
xmin = min(xmin,pt[0])
|
||||||
|
xMax = max(xMax,pt[0])
|
||||||
|
ymin = min(ymin,pt[1])
|
||||||
|
yMax = max(yMax,pt[1])
|
||||||
|
return xmin,xMax,ymin,yMax
|
||||||
|
|
||||||
|
def refinedBBox(path):
|
||||||
|
xmin,xMax,ymin,yMax = path[0][0][1][0],path[0][0][1][0],path[0][0][1][1],path[0][0][1][1]
|
||||||
|
for pathcomp in path:
|
||||||
|
for i in range(1, len(pathcomp)):
|
||||||
|
cmin, cmax = cubicExtrema(pathcomp[i-1][1][0], pathcomp[i-1][2][0], pathcomp[i][0][0], pathcomp[i][1][0])
|
||||||
|
xmin = min(xmin, cmin)
|
||||||
|
xMax = max(xMax, cmax)
|
||||||
|
cmin, cmax = cubicExtrema(pathcomp[i-1][1][1], pathcomp[i-1][2][1], pathcomp[i][0][1], pathcomp[i][1][1])
|
||||||
|
ymin = min(ymin, cmin)
|
||||||
|
yMax = max(yMax, cmax)
|
||||||
|
return xmin,xMax,ymin,yMax
|
||||||
|
|
||||||
|
def cubicExtrema(y0, y1, y2, y3):
|
||||||
|
cmin = min(y0, y3)
|
||||||
|
cmax = max(y0, y3)
|
||||||
|
d1 = y1 - y0
|
||||||
|
d2 = y2 - y1
|
||||||
|
d3 = y3 - y2
|
||||||
|
if (d1 - 2*d2 + d3):
|
||||||
|
if (d2*d2 > d1*d3):
|
||||||
|
t = (d1 - d2 + math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3)
|
||||||
|
if (t > 0) and (t < 1):
|
||||||
|
y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
|
||||||
|
cmin = min(cmin, y)
|
||||||
|
cmax = max(cmax, y)
|
||||||
|
t = (d1 - d2 - math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3)
|
||||||
|
if (t > 0) and (t < 1):
|
||||||
|
y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
|
||||||
|
cmin = min(cmin, y)
|
||||||
|
cmax = max(cmax, y)
|
||||||
|
elif (d3 - d1):
|
||||||
|
t = -d1/(d3 - d1)
|
||||||
|
if (t > 0) and (t < 1):
|
||||||
|
y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
|
||||||
|
cmin = min(cmin, y)
|
||||||
|
cmax = max(cmax, y)
|
||||||
|
return cmin, cmax
|
||||||
|
|
||||||
|
def computeBBox(aList,mat=[[1,0,0],[0,1,0]]):
|
||||||
|
bbox=None
|
||||||
|
for node in aList:
|
||||||
|
m = parseTransform(node.get('transform'))
|
||||||
|
m = composeTransform(mat,m)
|
||||||
|
#TODO: text not supported!
|
||||||
|
d = None
|
||||||
|
if node.get("d"):
|
||||||
|
d = node.get('d')
|
||||||
|
elif node.get('points'):
|
||||||
|
d = 'M' + node.get('points')
|
||||||
|
elif node.tag in [ inkex.addNS('rect','svg'), 'rect', inkex.addNS('image','svg'), 'image' ]:
|
||||||
|
d = 'M' + node.get('x', '0') + ',' + node.get('y', '0') + \
|
||||||
|
'h' + node.get('width') + 'v' + node.get('height') + \
|
||||||
|
'h-' + node.get('width')
|
||||||
|
elif node.tag in [ inkex.addNS('line','svg'), 'line' ]:
|
||||||
|
d = 'M' + node.get('x1') + ',' + node.get('y1') + \
|
||||||
|
' ' + node.get('x2') + ',' + node.get('y2')
|
||||||
|
elif node.tag in [ inkex.addNS('circle','svg'), 'circle', \
|
||||||
|
inkex.addNS('ellipse','svg'), 'ellipse' ]:
|
||||||
|
rx = node.get('r')
|
||||||
|
if rx is not None:
|
||||||
|
ry = rx
|
||||||
|
else:
|
||||||
|
rx = node.get('rx')
|
||||||
|
ry = node.get('ry')
|
||||||
|
cx = float(node.get('cx', '0'))
|
||||||
|
cy = float(node.get('cy', '0'))
|
||||||
|
x1 = cx - float(rx)
|
||||||
|
x2 = cx + float(rx)
|
||||||
|
d = 'M %f %f ' % (x1, cy) + \
|
||||||
|
'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x2, cy) + \
|
||||||
|
'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x1, cy)
|
||||||
|
|
||||||
|
if d is not None:
|
||||||
|
p = cubicsuperpath.parsePath(d)
|
||||||
|
applyTransformToPath(m,p)
|
||||||
|
bbox=boxunion(refinedBBox(p),bbox)
|
||||||
|
|
||||||
|
elif node.tag == inkex.addNS('use','svg') or node.tag=='use':
|
||||||
|
refid=node.get(inkex.addNS('href','xlink'))
|
||||||
|
path = '//*[@id="%s"]' % refid[1:]
|
||||||
|
refnode = node.xpath(path)
|
||||||
|
bbox=boxunion(computeBBox(refnode,m),bbox)
|
||||||
|
|
||||||
|
bbox=boxunion(computeBBox(node,m),bbox)
|
||||||
|
return bbox
|
||||||
|
|
||||||
|
|
||||||
|
def computePointInNode(pt, node, mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
|
||||||
|
if node.getparent() is not None:
|
||||||
|
applyTransformToPoint(invertTransform(composeParents(node, mat)), pt)
|
||||||
|
return pt
|
21
extensions/fablabchemnitz/laserdraw_export/meta.json
Normal file
21
extensions/fablabchemnitz/laserdraw_export/meta.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "LaserDraw Export (<various>)",
|
||||||
|
"id": "fablabchemnitz.de.laserdraw_export.<various>",
|
||||||
|
"path": "laserdraw_export",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "LaserDraw (LaserDRW) <various>",
|
||||||
|
"original_id": "com.scorchworks.output.<various>",
|
||||||
|
"license": "GNU GPL v2",
|
||||||
|
"license_url": "https://www.scorchworks.com/LaserDRW_extension/LaserDRW_extension-0.06.zip",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/laserdraw_export",
|
||||||
|
"fork_url": "https://www.scorchworks.com/LaserDRW_extension/LaserDRW_extension-0.06.zip",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55019345",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"scorchworks.com",
|
||||||
|
"github.com/eridur-de"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
22
extensions/fablabchemnitz/nextgenerator/meta.json
Normal file
22
extensions/fablabchemnitz/nextgenerator/meta.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "NextGenerator",
|
||||||
|
"id": "fablabchemnitz.de.nextgenerator",
|
||||||
|
"path": "nextgenerator",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "NextGenerator",
|
||||||
|
"original_id": "de.vektorrascheln.extension.next_gen",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://gitlab.com/Moini/nextgenerator/-/blob/master/LICENSE",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/nextgenerator",
|
||||||
|
"fork_url": "https://gitlab.com/Moini/nextgenerator",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/NextGenerator",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"Aurélio A. Heckert",
|
||||||
|
"gitlab.com/Moini",
|
||||||
|
"github.com/eridur-de"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
69
extensions/fablabchemnitz/nextgenerator/nextgenerator.inx
Normal file
69
extensions/fablabchemnitz/nextgenerator/nextgenerator.inx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>NextGenerator</name>
|
||||||
|
<id>fablabchemnitz.de.nextgenerator</id>
|
||||||
|
<param name="tab" type="notebook">
|
||||||
|
<page name="config" gui-text="Options">
|
||||||
|
<label appearance="header">Input options</label>
|
||||||
|
<param gui-text="CSV file:" gui-description="A file with comma-separated values, one line per exported file. The first line contains the column names. Put quotation marks around your variables if they contain a comma." name="csv_file" type="path" mode="file" filetypes="csv">/path/to/file.csv</param>
|
||||||
|
<spacer />
|
||||||
|
<label>Non-text values to replace (see Help tab):</label>
|
||||||
|
<param name="extra-vars" type="string" gui-text="" />
|
||||||
|
<param gui-text="Number of sets in the template:" gui-description="How many datasets fit into one template file - e.g. if you have an A4 page with 8 visitor badges for 8 different persons which use the same variable names, choose '8'. See 'Help' tab for more info." name="num_sets" type="int" min="1" max="10000">1</param>
|
||||||
|
<spacer />
|
||||||
|
<separator />
|
||||||
|
<spacer />
|
||||||
|
<label appearance="header">Output options</label>
|
||||||
|
<param name="format" type="optiongroup" appearance="combo" gui-text="Export file format:">
|
||||||
|
<option value="png">PNG</option>
|
||||||
|
<option value="pdf">PDF</option>
|
||||||
|
<option value="svg">SVG</option>
|
||||||
|
<option value="ps">PS</option>
|
||||||
|
<option value="eps">EPS</option>
|
||||||
|
</param>
|
||||||
|
<param gui-text="DPI (for PNG and filters):" gui-description="The resolution for your exported raster images" name="dpi" type="int" min="1" max="10000">300</param>
|
||||||
|
<param gui-text="File name pattern:" gui-description="The pattern for the names of the generated files. If 'Number of sets' is 1, it should contain at least one unique column variable (in the form of '%VAR_my_variable_name%'), or a unique combination of column variables, so new files won't overwrite those that have already been generated. Do not include the file extension, it will be added automatically. If 'Number of sets' is greater than 1, use any name you like. The exported files will be numbered automatically." name="file_pattern" type="string">%VAR_my_variable_name%</param>
|
||||||
|
<param gui-text="Save in:" gui-description="The name of the folder where the generated images should be saved" name="output_folder" type="path" mode="folders">/tmp</param>
|
||||||
|
</page>
|
||||||
|
<page name="help" gui-text="Help">
|
||||||
|
<param name="helptabs" type="notebook">
|
||||||
|
<page name="help1" gui-text="Replacing text">
|
||||||
|
<label xml:space="preserve">In your SVG file, create any texts that you want to replace by clicking (not clicking and dragging) on the canvas. If you want to limit the width of the text, use the diamond-shaped handle at the end of the first line to indicate a maximum width. The text will auto-flow into the next line if it is longer. Make sure there is enough space for your texts.
|
||||||
|
|
||||||
|
As (or into) the text, type '%VAR_my_variable_name%' (without the quotes) as a placeholder, where 'my_variable_name' is the title of the corresponding column in your CSV data file.</label>
|
||||||
|
</page>
|
||||||
|
<page name="help2" gui-text="Replacing attributes">
|
||||||
|
<label xml:space="preserve">If you want to replace attribute values in your SVG file (e.g. a color, or the name of a linked image file), you can assign them to columns in the field labeled "Non-text values to replace" in JSON format like this (no linebreaks allowed):
|
||||||
|
|
||||||
|
{"background_color":"#ff0000", "photo":"image.png"}
|
||||||
|
|
||||||
|
All objects that use the red color (#ff0000) will then be exported using the colors in the column 'background_color'. The linked image 'image.png' will be replaced by the image files listed in the column 'photo' (make sure to replace the complete image path).</label>
|
||||||
|
</page>
|
||||||
|
<page name="help3" gui-text="Multiple sets">
|
||||||
|
<label xml:space="preserve">If you intend to have multiple sets in a single template – for example, multiple conference badges on a larger page that will later be cut – make sure to group each set together.
|
||||||
|
Also make sure to use a variable only exactly once per set. If you do not adhere to this, your variables will get out of sync and your generated files will contain incorrect data.</label>
|
||||||
|
</page>
|
||||||
|
</param>
|
||||||
|
</page>
|
||||||
|
<page name="about" gui-text="About">
|
||||||
|
<label appearance="header">NextGenerator</label>
|
||||||
|
<label indent="1">Version 1.1.1</label>
|
||||||
|
<spacer />
|
||||||
|
<label xml:space="preserve">An Inkscape extension to automatically replace values (text, attribute values) in an SVG file and to then export the result to various file formats. This is useful e.g. for generating images for name badges and other similar items.
|
||||||
|
|
||||||
|
This extension is a Python rewrite of the Generator bash script extension by Aurélio A. Heckert. It is compatible with Inkscape starting from version 1.0 and requires Python 3.</label>
|
||||||
|
</page>
|
||||||
|
</param>
|
||||||
|
<effect needs-live-preview="false">
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Import/Export/Transfer" />
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
<menu-tip>Automatically replace values and export the result.</menu-tip>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">nextgenerator.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
190
extensions/fablabchemnitz/nextgenerator/nextgenerator.py
Normal file
190
extensions/fablabchemnitz/nextgenerator/nextgenerator.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# coding=utf-8
|
||||||
|
#
|
||||||
|
# NextGenerator - an Inkscape extension to export images with automatically replaced values
|
||||||
|
# Copyright (C) 2008 Aurélio A. Heckert (original Generator extension in Bash)
|
||||||
|
# 2019-2021 Maren Hachmann (Python rewrite, update for Inkscape 1.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 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.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
An Inkscape extension to automatically replace values (text, attribute values)
|
||||||
|
in an SVG file and to then export the result to various file formats.
|
||||||
|
|
||||||
|
This is useful e.g. for generating images for name badges and other similar items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import time #for debugging purposes
|
||||||
|
import inkex
|
||||||
|
import html
|
||||||
|
from inkex.command import inkscape
|
||||||
|
|
||||||
|
__version__ = '1.2'
|
||||||
|
|
||||||
|
class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
|
||||||
|
"""Generate image files by replacing variables in the current file"""
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
pars.add_argument("-c", "--csv_file", type=str, dest="csv_file", help="path to a CSV file")
|
||||||
|
pars.add_argument("-e", "--extra-vars", help="additional variables to replace and the corresponding columns, in JSON format")
|
||||||
|
pars.add_argument("-n", "--num_sets", type=int, default="1", help="number of sets in the template")
|
||||||
|
pars.add_argument("-f", "--format", help="file format to export to: png, pdf, svg, ps, eps")
|
||||||
|
pars.add_argument("-d", "--dpi", type=int, default="300", help="dpi value for exported raster images")
|
||||||
|
pars.add_argument("-o", "--output_folder", help="path to output folder")
|
||||||
|
pars.add_argument("-p", "--file_pattern", help="pattern for the output file")
|
||||||
|
|
||||||
|
pars.add_argument("-t", "--tab", default="", help="not needed at all")
|
||||||
|
pars.add_argument("-l", "--helptabs", default="", help="not needed at all")
|
||||||
|
pars.add_argument("-i", "--id", default="", help="not needed at all")
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
|
||||||
|
# load the attributes that should be replaced in addition to textual values
|
||||||
|
if self.options.extra_vars == None:
|
||||||
|
self.options.extra_vars = '{}'
|
||||||
|
|
||||||
|
extra_vars = json.loads(self.options.extra_vars)
|
||||||
|
|
||||||
|
|
||||||
|
# load the CSV file
|
||||||
|
# spaces around commas will be stripped
|
||||||
|
csv.register_dialect('generator', 'excel', skipinitialspace=True)
|
||||||
|
|
||||||
|
with open(self.options.csv_file, newline='', encoding='utf-8-sig') as csvfile:
|
||||||
|
|
||||||
|
data = csv.DictReader(csvfile, dialect='generator')
|
||||||
|
|
||||||
|
if self.options.num_sets == 1:
|
||||||
|
for row in data:
|
||||||
|
export_base_name = self.options.file_pattern
|
||||||
|
self.new_doc = self.document
|
||||||
|
for i, (key, value) in enumerate(row.items()):
|
||||||
|
search_string = "%VAR_" + key + "%"
|
||||||
|
# replace any occurrances of %VAR_my_variable_name% in the SVG file source code
|
||||||
|
self.new_doc = self.new_doc.replace(search_string, html.escape(value))
|
||||||
|
# build the file name, still without file extension
|
||||||
|
export_base_name = export_base_name.replace(search_string, value)
|
||||||
|
for key, svg_cont in extra_vars.items():
|
||||||
|
if key in row.keys():
|
||||||
|
# replace any attributes and other SVG content by the values from the CSV file
|
||||||
|
self.new_doc = self.new_doc.replace(svg_cont, row[key])
|
||||||
|
else:
|
||||||
|
inkex.errormsg("The replacements in the generated images may be incomplete. Please check your entry '{key}' in the field for the non-text values.").format(key=key)
|
||||||
|
if self.export(export_base_name) != True:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif self.options.num_sets > 1:
|
||||||
|
# we need a list to access specific rows and to be able to count it
|
||||||
|
data = list(data)
|
||||||
|
|
||||||
|
# check if user's indication of num_sets is compatible with file
|
||||||
|
for key in data[0].keys():
|
||||||
|
num_occurr = self.document.count("%VAR_" + key + "%")
|
||||||
|
# We ignore keys that don't appear in the document
|
||||||
|
if num_occurr != 0 and num_occurr != self.options.num_sets:
|
||||||
|
return inkex.errormsg("There are {0} occurrances of the variable '{1}' in the document, but the number of sets you indicated is {2}. Please make sure that each set contains all variables and that there are just as many sets in your document as you indicate.".format(num_occurr, key, self.options.num_sets))
|
||||||
|
|
||||||
|
# abusing negative floor division which rounds to the next lowest number to figure out how many pages we will get
|
||||||
|
num_exports = -((-len(data))//self.options.num_sets)
|
||||||
|
# now we hope that the document is properly prepared and the stacking order cycles through datasets - if not, the result will be nonsensical, but we can't know.
|
||||||
|
|
||||||
|
for export_file_num in range(num_exports):
|
||||||
|
# we only number the export files if there are sets
|
||||||
|
export_base_name = "".join([x if x.isalnum() else "_" for x in self.options.file_pattern]) + '_{}'.format(str(export_file_num))
|
||||||
|
self.new_doc = self.document
|
||||||
|
|
||||||
|
for set_num in range(self.options.num_sets):
|
||||||
|
|
||||||
|
# number of the data row in the CSV file
|
||||||
|
n = export_file_num * self.options.num_sets + set_num
|
||||||
|
if n < len(data):
|
||||||
|
dataset = data[n]
|
||||||
|
else:
|
||||||
|
# no more values available, stop trying to replace them
|
||||||
|
break
|
||||||
|
|
||||||
|
for i, (key, value) in enumerate(dataset.items()):
|
||||||
|
search_string = "%VAR_" + key + "%"
|
||||||
|
# replace the next occurrance of %VAR_my_variable_name% in the SVG file source code
|
||||||
|
self.new_doc = self.new_doc.replace(search_string, html.escape(value), 1)
|
||||||
|
|
||||||
|
for key, svg_cont in extra_vars.items():
|
||||||
|
if key in dataset.keys():
|
||||||
|
# replace any attributes and other SVG content by the values from the CSV file
|
||||||
|
self.new_doc = self.new_doc.replace(svg_cont, dataset[key], 1)
|
||||||
|
else:
|
||||||
|
inkex.errormsg(_("The replacements in the generated images may be incomplete. Please check your entry '{key}' in the field for the non-text values.").format(key=key))
|
||||||
|
self.export(export_base_name)
|
||||||
|
|
||||||
|
def export(self, export_base_name):
|
||||||
|
|
||||||
|
export_file_name = '{0}.{1}'.format(export_base_name, self.options.format)
|
||||||
|
|
||||||
|
if os.path.exists(self.options.output_folder):
|
||||||
|
export_file_path = os.path.join(self.options.output_folder, export_file_name)
|
||||||
|
else:
|
||||||
|
inkex.errormsg("The selected output folder does not exist.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if self.options.format == 'svg':
|
||||||
|
# would use this, but it cannot overwrite, nor handle strings for writing...:
|
||||||
|
# write_svg(self.new_doc, export_file_path)
|
||||||
|
with open(export_file_path, 'w') as f:
|
||||||
|
f.write(self.new_doc)
|
||||||
|
else:
|
||||||
|
|
||||||
|
actions = {
|
||||||
|
'png' : 'export-dpi:{dpi};export-filename:{file_name};export-do;FileClose'.\
|
||||||
|
format(dpi=self.options.dpi, file_name=export_file_path),
|
||||||
|
'pdf' : 'export-dpi:{dpi};export-pdf-version:1.5;export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
|
||||||
|
format(dpi=self.options.dpi, file_name=export_file_path),
|
||||||
|
'ps' : 'export-dpi:{dpi};export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
|
||||||
|
format(dpi=self.options.dpi, file_name=export_file_path),
|
||||||
|
'eps' : 'export-dpi:{dpi};export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
|
||||||
|
format(dpi=self.options.dpi, file_name=export_file_path),
|
||||||
|
}
|
||||||
|
|
||||||
|
# create a temporary svg file from our string
|
||||||
|
temp_svg_name = '{0}.{1}'.format(export_base_name, 'svg')
|
||||||
|
temp_svg_path = os.path.join(self.tempdir, temp_svg_name)
|
||||||
|
#inkex.utils.debug("temp_svg_path=" + temp_svg_path)
|
||||||
|
with open(temp_svg_path, 'w') as f:
|
||||||
|
f.write(self.new_doc)
|
||||||
|
#inkex.utils.debug("self.new_doc=" + self.new_doc)
|
||||||
|
# let Inkscape do the exporting
|
||||||
|
# self.debug(actions[self.options.format])
|
||||||
|
cli_output = inkscape(temp_svg_path, actions=actions[self.options.format])
|
||||||
|
|
||||||
|
if len(cli_output) > 0:
|
||||||
|
self.debug(_("Inkscape returned the following output when trying to run the file export; the file export may still have worked:"))
|
||||||
|
self.debug(cli_output)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def load(self, stream):
|
||||||
|
return str(stream.read(), 'utf-8')
|
||||||
|
|
||||||
|
def save(self, stream):
|
||||||
|
# must be implemented, but isn't needed.
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
NextGenerator().run()
|
20
extensions/fablabchemnitz/raster_perspective/meta.json
Normal file
20
extensions/fablabchemnitz/raster_perspective/meta.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Raster Perspective",
|
||||||
|
"id": "fablabchemnitz.de.raster_perspective",
|
||||||
|
"path": "raster_perspective",
|
||||||
|
"dependent_extensions": null,
|
||||||
|
"original_name": "Perspective",
|
||||||
|
"original_id": "org.test.filter.imagePerspective",
|
||||||
|
"license": "GNU GPL v3",
|
||||||
|
"license_url": "https://github.com/s1291/InkRasterPerspective/blob/master/LICENSE",
|
||||||
|
"comment": "",
|
||||||
|
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/raster_perspective",
|
||||||
|
"fork_url": "https://github.com/s1291/InkRasterPerspective",
|
||||||
|
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Raster+Perspective",
|
||||||
|
"inkscape_gallery_url": null,
|
||||||
|
"main_authors": [
|
||||||
|
"github.com/s1291"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Raster Perspective</name>
|
||||||
|
<id>fablabchemnitz.de.raster_perspective</id>
|
||||||
|
<effect needs-live-preview="true">
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Transformations"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">raster_perspective.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright (C) 2022 Samir OUCHENE, samirmath01@gmail.com
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import io
|
||||||
|
import inkex
|
||||||
|
from inkex import Image
|
||||||
|
from PIL import Image as PIL_Image
|
||||||
|
from PIL.Image import Transform, Resampling
|
||||||
|
import base64
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from base64 import decodebytes
|
||||||
|
except ImportError:
|
||||||
|
from base64 import decodestring as decodebytes
|
||||||
|
|
||||||
|
|
||||||
|
class RasterPerspective(inkex.Effect):
|
||||||
|
def __init__(self):
|
||||||
|
inkex.Effect.__init__(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mime_to_ext(mime):
|
||||||
|
"""Return an extension based on the mime type"""
|
||||||
|
# Most extensions are automatic (i.e. extension is same as minor part of mime type)
|
||||||
|
part = mime.split("/", 1)[1].split("+")[0]
|
||||||
|
return (
|
||||||
|
"."
|
||||||
|
+ {
|
||||||
|
# These are the non-matching ones.
|
||||||
|
"svg+xml": ".svg",
|
||||||
|
"jpeg": ".jpg",
|
||||||
|
"icon": ".ico",
|
||||||
|
}.get(part, part)
|
||||||
|
)
|
||||||
|
|
||||||
|
def extract_image(self, node):
|
||||||
|
"""Extract the node as if it were an image."""
|
||||||
|
xlink = node.get("xlink:href")
|
||||||
|
if not xlink.startswith("data:"):
|
||||||
|
return # Not embedded image data
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = xlink[5:]
|
||||||
|
(mimetype, data) = data.split(";", 1)
|
||||||
|
(base, data) = data.split(",", 1)
|
||||||
|
except ValueError:
|
||||||
|
inkex.errormsg("Invalid image format found")
|
||||||
|
return
|
||||||
|
|
||||||
|
if base != "base64":
|
||||||
|
inkex.errormsg("Can't decode encoding: {}".format(base))
|
||||||
|
return
|
||||||
|
|
||||||
|
file_ext = self.mime_to_ext(mimetype)
|
||||||
|
return decodebytes(data.encode("utf-8"))
|
||||||
|
|
||||||
|
def find_coeffs(self, source_coords, target_coords):
|
||||||
|
matrix = []
|
||||||
|
for s, t in zip(source_coords, target_coords):
|
||||||
|
matrix.append([t[0], t[1], 1, 0, 0, 0, -s[0] * t[0], -s[0] * t[1]])
|
||||||
|
matrix.append([0, 0, 0, t[0], t[1], 1, -s[1] * t[0], -s[1] * t[1]])
|
||||||
|
A = numpy.array(matrix, dtype=float)
|
||||||
|
B = numpy.array(source_coords).reshape(8)
|
||||||
|
res = numpy.linalg.inv(A.T @ A) @ A.T @ B
|
||||||
|
return numpy.array(res).reshape(8)
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
WARN = "Your selection must contain an image and a path with at least 4 points."
|
||||||
|
if len(self.options.ids) < 2:
|
||||||
|
inkex.errormsg(WARN)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
the_image_node, envelope_node = self.svg.selection
|
||||||
|
if str(envelope_node) == "image" and str(the_image_node) == "path":
|
||||||
|
envelope_node, the_image_node = self.svg.selection #switch
|
||||||
|
|
||||||
|
if str(the_image_node) != "image" and str(envelope_node) != "path":
|
||||||
|
inkex.utils.debug(WARN)
|
||||||
|
return
|
||||||
|
img_width, img_height = the_image_node.width, the_image_node.height
|
||||||
|
|
||||||
|
try:
|
||||||
|
unit_to_vp = self.svg.unit_to_viewport
|
||||||
|
except AttributeError:
|
||||||
|
unit_to_vp = self.svg.uutounit
|
||||||
|
|
||||||
|
try:
|
||||||
|
vp_to_unit = self.svg.viewport_to_unit
|
||||||
|
except AttributeError:
|
||||||
|
vp_to_unit = self.svg.unittouu
|
||||||
|
|
||||||
|
img_width = unit_to_vp(img_width)
|
||||||
|
img_height = unit_to_vp(img_height)
|
||||||
|
|
||||||
|
nodes_pts = list(envelope_node.path.control_points)
|
||||||
|
node1 = (unit_to_vp(nodes_pts[0][0]), unit_to_vp(nodes_pts[0][1]))
|
||||||
|
node2 = (unit_to_vp(nodes_pts[1][0]), unit_to_vp(nodes_pts[1][1]))
|
||||||
|
node3 = (unit_to_vp(nodes_pts[2][0]), unit_to_vp(nodes_pts[2][1]))
|
||||||
|
node4 = (unit_to_vp(nodes_pts[3][0]), unit_to_vp(nodes_pts[3][1]))
|
||||||
|
|
||||||
|
nodes = [node1, node2, node3, node4]
|
||||||
|
|
||||||
|
xMax = max([node[0] for node in nodes])
|
||||||
|
xMin = min([node[0] for node in nodes])
|
||||||
|
yMax = max([node[1] for node in nodes])
|
||||||
|
yMin = min([node[1] for node in nodes])
|
||||||
|
# add some assertions (FIXME)
|
||||||
|
|
||||||
|
img_data = self.extract_image(the_image_node)
|
||||||
|
orig_image = PIL_Image.open(io.BytesIO(img_data))
|
||||||
|
pil_img_size = orig_image.size
|
||||||
|
scale = pil_img_size[0] / img_width
|
||||||
|
|
||||||
|
coeffs = self.find_coeffs(
|
||||||
|
[
|
||||||
|
(0, 0),
|
||||||
|
(img_width * scale, 0),
|
||||||
|
(img_width * scale, img_height * scale),
|
||||||
|
(0, img_height * scale),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
(node1[0] - xMin, node1[1] - yMin),
|
||||||
|
(node2[0] - xMin, node2[1] - yMin),
|
||||||
|
(node3[0] - xMin, node3[1] - yMin),
|
||||||
|
(node4[0] - xMin, node4[1] - yMin),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
W, H = xMax - xMin, yMax - yMin
|
||||||
|
|
||||||
|
final_w, final_h = int(W), int(H)
|
||||||
|
|
||||||
|
# Check if the image has transparency
|
||||||
|
hasTransparency = orig_image.mode in ("RGBA", "LA") or (
|
||||||
|
orig_image.mode == "P" and "transparency" in orig_image.info
|
||||||
|
)
|
||||||
|
|
||||||
|
transp_img = orig_image
|
||||||
|
|
||||||
|
# If the original image is not transparent, create a new image with alpha channel
|
||||||
|
if not hasTransparency:
|
||||||
|
transp_img = PIL_Image.new("RGBA", orig_image.size)
|
||||||
|
transp_img.format = "PNG"
|
||||||
|
transp_img.paste(orig_image)
|
||||||
|
|
||||||
|
image = transp_img.transform(
|
||||||
|
(final_w, final_h), Transform.PERSPECTIVE, coeffs, Resampling.BICUBIC
|
||||||
|
)
|
||||||
|
|
||||||
|
obj = inkex.Image()
|
||||||
|
obj.set("x", vp_to_unit(xMin))
|
||||||
|
obj.set("y", vp_to_unit(yMin))
|
||||||
|
obj.set("width", vp_to_unit(final_w))
|
||||||
|
obj.set("height", vp_to_unit(final_h))
|
||||||
|
# embed the transformed image
|
||||||
|
persp_img_data = io.BytesIO()
|
||||||
|
image.save(persp_img_data, transp_img.format)
|
||||||
|
mime = PIL_Image.MIME[transp_img.format]
|
||||||
|
b64 = base64.b64encode(persp_img_data.getvalue()).decode("utf-8")
|
||||||
|
uri = f"data:{mime};base64,{b64}"
|
||||||
|
obj.set("xlink:href", uri)
|
||||||
|
self.svg.add(obj)
|
||||||
|
|
||||||
|
|
||||||
|
RasterPerspective = RasterPerspective()
|
||||||
|
RasterPerspective.run()
|
Loading…
Reference in New Issue
Block a user