added more extensions
This commit is contained in:
parent
7cab1a92ea
commit
680edc2e15
20
extensions/fablabchemnitz/random_delete/meta.json
Normal file
20
extensions/fablabchemnitz/random_delete/meta.json
Normal file
@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"name": "Random Delete",
|
||||
"id": "fablabchemnitz.de.random_delete",
|
||||
"path": "random_delete",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "Random Delete",
|
||||
"original_id": "fablabchemnitz.de.random_delete",
|
||||
"license": "GNU GPL v3",
|
||||
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/LICENSE",
|
||||
"comment": "Written by Mario Voigt",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/random_delete",
|
||||
"fork_url": null,
|
||||
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Random+Delete",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
18
extensions/fablabchemnitz/random_delete/random_delete.inx
Normal file
18
extensions/fablabchemnitz/random_delete/random_delete.inx
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Random Delete</name>
|
||||
<id>fablabchemnitz.de.random_delete</id>
|
||||
<param name="prob" type="float" gui-text="Probability of deletion (%):" min="0.0" max="100.0">100.0</param>
|
||||
<label>This extension allows to delete with a certain probability each of the selected objects.</label>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Various"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">random_delete.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
21
extensions/fablabchemnitz/random_delete/random_delete.py
Normal file
21
extensions/fablabchemnitz/random_delete/random_delete.py
Normal file
@ -0,0 +1,21 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
import random
|
||||
import inkex
|
||||
|
||||
class RandomDelete(inkex.Effect):
|
||||
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument("--prob", type=float, default=50, help="Probability of deletion")
|
||||
|
||||
def effect(self):
|
||||
if len(self.svg.selected) > 0:
|
||||
for element in self.svg.selection.values():
|
||||
if random.random() < self.options.prob/100:
|
||||
element.delete()
|
||||
else:
|
||||
self.msg('Please select some paths first.')
|
||||
return
|
||||
|
||||
if __name__ == '__main__':
|
||||
RandomDelete().run()
|
21
extensions/fablabchemnitz/round_corners/meta.json
Normal file
21
extensions/fablabchemnitz/round_corners/meta.json
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name": "Round Corners (Replaced by LPE)",
|
||||
"id": "fablabchemnitz.de.round_corners",
|
||||
"path": "round_corners",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "Round Corners",
|
||||
"original_id": "org.inkscape.jnweiger.round_corners",
|
||||
"license": "GNU GPL v2",
|
||||
"license_url": "https://github.com/jnweiger/inkscape-round-corners/blob/main/LICENSE",
|
||||
"comment": "You can do nearly the same with Live Path Effect!",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/round_corners",
|
||||
"fork_url": "https://github.com/jnweiger/inkscape-round-corners",
|
||||
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=94175520",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"github.com/jnweiger",
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
35
extensions/fablabchemnitz/round_corners/round_corners.inx
Normal file
35
extensions/fablabchemnitz/round_corners/round_corners.inx
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Round Corners (Replaced by LPE)</name>
|
||||
<id>fablabchemnitz.de.round_corners</id>
|
||||
<param name="radius" type="float" gui-text="Radius: [mm]" precision="2" min="0.001" max="999.99">2.0</param>
|
||||
<param name="method" type="optiongroup" appearance="radio" gui-text="Corner type:">
|
||||
<option value="arc">Arc</option>
|
||||
<option value="line">Line</option>
|
||||
</param>
|
||||
<label xml:space="preserve">* Select a path in edit mode.
|
||||
* Select one or more vertices.
|
||||
* Start the extension,
|
||||
- set the radius of the arc.
|
||||
- Apply
|
||||
|
||||
Each selected vertex is replaced by two or more vertices forming
|
||||
a bezier spline that approximates an arc of the given radius.
|
||||
|
||||
When the corner type is set to 'line', the arc is
|
||||
replaced with a straight cut.
|
||||
|
||||
Version: 1.4
|
||||
</label>
|
||||
<effect>
|
||||
<object-type>path</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Modify existing Path(s)"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">round_corners.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
521
extensions/fablabchemnitz/round_corners/round_corners.py
Normal file
521
extensions/fablabchemnitz/round_corners/round_corners.py
Normal file
@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright (C) 2020 Juergen Weigert, jnweiger@gmail.com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# v0.1, 2020-11-08, jw - initial draught, finding and printing selected nodes to the terminal...
|
||||
# v0.2, 2020-11-08, jw - duplicate the selected nodes in their superpaths, write them back.
|
||||
# v0.3, 2020-11-21, jw - find "meta-handles"
|
||||
# v0.4, 2020-11-26, jw - alpha and trim math added. trimming with a striaght line implemented, needs fixes.
|
||||
# Option 'cut' added.
|
||||
# v0.5, 2020-11-28, jw - Cut operation looks correct. Dummy midpoint for large arcs added, looks wrong, of course.
|
||||
# v1.0, 2020-11-30, jw - Code completed. Bot cut and arc work fine.
|
||||
# v1.1, 2020-12-07, jw - Replaced boolean 'cut' with a method selector 'arc'/'line'. Added round_corners_092.inx
|
||||
# and started backport in round_corners.py -- attempting to run the same code everywhere.
|
||||
# v1.2, 2020-12-08, jw - Backporting continued: option parser hack added. Started effect_wrapper() to prepare self.svg
|
||||
# v1.3, 2020-12-12, jw - minimalistic compatibility layer for inkscape 0.92.4 done. It now works in both, 1.0 and 0.92!
|
||||
# v1.4, 2020-12-15, jw - find_roundable_nodes() added for auto selecting nodes, if none were selected.
|
||||
# And fix https://github.com/jnweiger/inkscape-round-corners/issues/2
|
||||
# 2021-01-15, Mario Voigt - removed oboslete InkScape 0.92.* stuff
|
||||
#
|
||||
# Bad side-effect: As the node count increases during operation, the list of
|
||||
# selected nodes is incorrect afterwards. We have no way to give inkscape an update.
|
||||
#
|
||||
"""
|
||||
Rounded Corners
|
||||
|
||||
This extension operates on selected sharp corner nodes and converts them to a fillet (bevel,chamfer).
|
||||
An arc shaped path segment with the given radius is inserted smoothly.
|
||||
The fitted arc is approximated by a bezier spline, as we are doing path operations here.
|
||||
When the sides at the corner are straight lines, the operation never move the sides, it just shortens them to fit the arc.
|
||||
When the sides are curved, the arc is placed on the tanget line, and the curve may thus change in shape.
|
||||
|
||||
Selected smooth nodes are skipped.
|
||||
Cases with insufficient space (180deg turn or too short segments/handles) are warned about.
|
||||
|
||||
References:
|
||||
- https://gitlab.com/inkscape/extensions/-/wikis/home
|
||||
- https://gitlab.com/inkscape/extras/extensions-tutorials/-/blob/master/My-First-Effect-Extension.md
|
||||
- https://gitlab.com/inkscape/extensions/-/wikis/uploads/25063b4ae6c3396fcda428105c5cff89/template_effect.zip
|
||||
- https://inkscape-extensions-guide.readthedocs.io/en/latest/_modules/inkex/elements.html#ShapeElement.get_path
|
||||
- https://inkscape.gitlab.io/extensions/documentation/_modules/inkex/paths.html#CubicSuperPath.to_path
|
||||
|
||||
- https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
|
||||
- https://hansmuller-flex.blogspot.com/2011/10/more-about-approximating-circular-arcs.html
|
||||
- https://itc.ktu.lt/index.php/ITC/article/download/11812/6479 (Riskus' PDF)
|
||||
|
||||
The algorithm of arc_bezier_handles() is based on the approach described in:
|
||||
A. Riškus, "Approximation of a Cubic Bezier Curve by Circular Arcs and Vice Versa,"
|
||||
Information Technology and Control, 35(4), 2006 pp. 371-378.
|
||||
"""
|
||||
|
||||
import inkex
|
||||
import sys
|
||||
import math
|
||||
import pprint
|
||||
import copy
|
||||
import os
|
||||
|
||||
__version__ = '1.4' # Keep in sync with round_corners.inx line 16
|
||||
debug = False # True: babble on controlling tty
|
||||
|
||||
max_trim_factor = 0.90 # 0.5: can cut half of a segment length or handle length away for rounding a corner
|
||||
max_trim_factor_single = 0.98 # 0.98: we can eat up almost everything, as there are no neighbouring trims to be expected.
|
||||
|
||||
class RoundCorners(inkex.EffectExtension):
|
||||
|
||||
def add_arguments(self, pars): # an __init__ in disguise ...
|
||||
try:
|
||||
self.tty = open("/dev/tty", 'w')
|
||||
except:
|
||||
self.tty = open(os.devnull, 'w') # '/dev/null' for POSIX, 'nul' for Windows.
|
||||
if debug: print("RoundedCorners ...", file=self.tty)
|
||||
self.nodes_inserted = {}
|
||||
self.eps = 0.00001 # avoid division by zero
|
||||
self.radius = None
|
||||
self.max_trim_factor = max_trim_factor
|
||||
|
||||
self.skipped_degenerated = 0 # not a useful corner (e.g. 180deg corner)
|
||||
self.skipped_small_count = 0 # not enough room for arc
|
||||
self.skipped_small_len = 1e99 # record the shortest handle (or segment) when skipping.
|
||||
|
||||
pars.add_argument("--radius", type=float, default=2.0, help="Radius [mm] to round selected vertices. Default: 2")
|
||||
pars.add_argument("--method", type=str, default="arc", help="operation: one of 'arc' (default), 'line'")
|
||||
|
||||
|
||||
def effect(self):
|
||||
if debug:
|
||||
# SvgInputMixin __init__: "id:subpath:position of selected nodes, if any"
|
||||
print(self.options.selected_nodes, file=self.tty)
|
||||
|
||||
self.radius = math.fabs(self.options.radius)
|
||||
self.cut = False
|
||||
if self.options.method in ('line'):
|
||||
self.cut = True
|
||||
if len(self.options.selected_nodes) < 1:
|
||||
# find selected objects and construct a list of selected_nodes for them...
|
||||
for p in self.options.ids:
|
||||
self.options.selected_nodes.extend(self.find_roundable_nodes(p))
|
||||
if len(self.options.selected_nodes) < 1:
|
||||
raise inkex.AbortExtension("Could not find nodes inside a path. No path objects selected?")
|
||||
|
||||
if len(self.options.selected_nodes) == 1:
|
||||
# when we only trim one node, we can eat up almost everything,
|
||||
# no need to leave room for rounding neighbour nodes.
|
||||
self.max_trim_factor = max_trim_factor_single
|
||||
|
||||
for node in sorted(self.options.selected_nodes):
|
||||
## we walk through the list sorted, so that node indices are processed within a subpath in ascending numeric order.
|
||||
## that makes adjusting index offsets after node inserts easier.
|
||||
ss = self.round_corner(node)
|
||||
|
||||
|
||||
def find_roundable_nodes(self, path_id):
|
||||
""" select all nodes of all (sub)paths. except for
|
||||
- the last (one or two) nodes of a closed path (which coindide with the first node)
|
||||
- the first and last node of an open path (which cannot be smoothed)
|
||||
"""
|
||||
ret = []
|
||||
elem = self.svg.getElementById(path_id)
|
||||
if elem.tag != '{'+elem.nsmap['svg']+'}path':
|
||||
return ret # ellipse never works.
|
||||
try:
|
||||
csp = elem.path.to_superpath()
|
||||
except:
|
||||
return ret
|
||||
|
||||
for sp_idx in range(0, len(csp)):
|
||||
sp = csp[sp_idx]
|
||||
if len(sp) < 3:
|
||||
continue # subpaths of 2 or less nodes are ignored
|
||||
if self.very_close(sp[0], sp[-1]):
|
||||
idx_s = 0 # closed paths count from 0 to either n-1 or n-2
|
||||
idx_e = len(sp) - 1
|
||||
if self.very_close_xy(sp[-2][1], sp[-1][1]):
|
||||
idx_e = len(sp) - 2
|
||||
else:
|
||||
idx_s = 1 # open paths count from 1 to either n-1
|
||||
idx_e = len(sp) - 1
|
||||
for idx in range(idx_s, idx_e):
|
||||
ret.append("%s:%d:%d" % (path_id, sp_idx, idx))
|
||||
|
||||
if debug:
|
||||
print("find_roundable_nodes: ", self.options.selected_nodes, file=sys.stderr)
|
||||
return ret
|
||||
|
||||
|
||||
def very_close(self, n1, n2):
|
||||
"deep compare. all elements in sub arrays are compared for (very close) numerical equality"
|
||||
return self.very_close_xy(n1[0], n2[0]) and self.very_close_xy(n1[1], n2[1]) and self.very_close_xy(n1[2], n2[2])
|
||||
|
||||
|
||||
def very_close_xy(self, p1, p2):
|
||||
"one 2 element array is compared for (very close) numerical equality"
|
||||
eps = 1e-9
|
||||
return abs(p1[0]-p2[0]) < eps and abs(p1[1]-p2[1]) < eps
|
||||
|
||||
|
||||
def round_corner(self, node_id):
|
||||
""" round the corner at (adjusted) node_idx of subpath
|
||||
Side_effect: store (or increment) in self.inserted["pathname:subpath"] how many points were inserted in that subpath.
|
||||
the adjusted node_idx is computed by adding that number (if exists) to the value of the node_id before doing any manipulation
|
||||
"""
|
||||
s = node_id.split(":")
|
||||
path_id = s[0]
|
||||
subpath_idx = int(s[1])
|
||||
subpath_id = s[0] + ':' + s[1]
|
||||
idx_adjust = self.nodes_inserted.get(subpath_id, 0)
|
||||
node_idx = int(s[2]) + idx_adjust
|
||||
|
||||
elem = self.svg.getElementById(path_id)
|
||||
if elem is None:
|
||||
print("selected_node %s not found in svg document" % node_id, file=sys.stderr)
|
||||
return None
|
||||
|
||||
elem.apply_transform() # modifies path inplace? -- We save later back to the same element. Maybe we should not?
|
||||
path = elem.path
|
||||
s = path.to_superpath()
|
||||
sp = s[subpath_idx]
|
||||
|
||||
## call the actual path manipulator, record how many nodes were inserted.
|
||||
orig_len = len(sp)
|
||||
sp = self.subpath_round_corner(sp, node_idx)
|
||||
idx_adjust += len(sp) - orig_len
|
||||
|
||||
# convert the superpath back to a normal path
|
||||
s[subpath_idx] = sp
|
||||
elem.set_path(s.to_path(curves_only=False))
|
||||
self.nodes_inserted[subpath_id] = idx_adjust
|
||||
|
||||
|
||||
# If we picked up the 'd' attribute of a non-path (e.g. star), we must make sure the object now becomes a path.
|
||||
# Otherwise inkscape uses the sodipodi data and ignores our changed 'd' attribute.
|
||||
if '{'+elem.nsmap['sodipodi']+'}type' in elem.attrib:
|
||||
del(elem.attrib['{'+elem.nsmap['sodipodi']+'}type'])
|
||||
|
||||
# Debugging is no longer available or not yet implemented? This explodes, although it is
|
||||
# documented in https://inkscape.gitlab.io/extensions/documentation/inkex.command.html
|
||||
# inkex.command.write_svg(self.svg, "/tmp/seen.svg")
|
||||
# - AttributeError: module 'inkex' has no attribute 'command'
|
||||
# But hey, we can always resort to good old ET.dump(self.document) ...
|
||||
|
||||
|
||||
def super_node(self, sp, node_idx):
|
||||
""" In case of node_idx 0, we need to use either the last, the second-last or the third last node as a previous node.
|
||||
For a closed subpath, the last node and the first node are identical. Then, the second last node may be still at the
|
||||
same location if it has a handle. If so, we take the third last instead. Gah. It has a certain logic...
|
||||
|
||||
In case of the node_idx being the last node, we already know that the subpath is not closed,
|
||||
we use 0 as the next node.
|
||||
|
||||
The direction sn.prev.dir does not really point to the coordinate of the previous node, but to the end of the
|
||||
next-handle of the prvious node. This is the same when there are straight lines. The absence of handles is
|
||||
denoted by having the same coordinates for handle and node.
|
||||
Same for next.dir, it points to the next.prev handle.
|
||||
|
||||
The exact implementation here is:
|
||||
- sn.next.handle is set to a relative vector that is the tangent of the curve towards the next point.
|
||||
we implement four cases:
|
||||
- if neither node nor next have handles, the connection is a straight line, and next.handle points
|
||||
in the direction of the next node itself.
|
||||
- if the curve between node and next is defined by two handles, then sn.next.handle is in the direction of the
|
||||
nodes own handle,
|
||||
- if the curve between node and next is defined one handle at the node itself, then sn.next.handle is in the
|
||||
direction of the nodes own handle,
|
||||
- if the curve between node and next is defined one handle at the next node, then sn.next.handle is in the
|
||||
direction from the node to the end of that other handle.
|
||||
- when trimming back later, we move along that tangent, instead of following the curve.
|
||||
That is an approximation when the segment is curved, and exact when it is straight.
|
||||
(Finding exact candidate points on curved lines that have tangents with the desired circle
|
||||
is beyond me today. Multiple candidates may exist. Any volunteers?)
|
||||
"""
|
||||
|
||||
prev_idx = node_idx - 1
|
||||
sp_node_idx_ = copy.deepcopy(sp[node_idx]) # if this wraps around, at node_idx=0, we may need to tweak the prev handle
|
||||
if node_idx == 0:
|
||||
prev_idx = len(sp) - 1
|
||||
if self.very_close(sp_node_idx_, sp[prev_idx]):
|
||||
prev_idx = prev_idx - 1 # skip one node, it is the 'close marker'
|
||||
if self.very_close_xy(sp_node_idx_[1], sp[prev_idx][1]):
|
||||
# still no distance, skip more. Needed for https://github.com/jnweiger/inkscape-round-corners/issues/2
|
||||
sp_node_idx_[0] = sp[prev_idx][0] # this sp_node_idx_ must acts as if its prev handle is that one.
|
||||
prev_idx = prev_idx - 1
|
||||
else:
|
||||
self.skipped_degenerated += 1 # path ends here.
|
||||
return None, None
|
||||
|
||||
# if debug: pprint.pprint({'node_idx': node_idx, 'len(sp)':len(sp), 'sp': sp}, stream=self.tty)
|
||||
if node_idx == len(sp)-1:
|
||||
self.skipped_degenerated += 1 # path ends here. On a closed loop, we can never select the last point.
|
||||
return None, None
|
||||
|
||||
next_idx = node_idx + 1
|
||||
if next_idx >= len(sp): next_idx = 0
|
||||
t = sp_node_idx_
|
||||
p = sp[prev_idx]
|
||||
n = sp[next_idx]
|
||||
dir1 = [ p[2][0] - t[1][0], p[2][1] - t[1][1] ] # direction to the previous node (rel coords)
|
||||
dir2 = [ n[0][0] - t[1][0], n[0][1] - t[1][1] ] # direction to the next node (rel coords)
|
||||
dist1 = math.sqrt(dir1[0]*dir1[0] + dir1[1]*dir1[1]) # distance to the previous node
|
||||
dist2 = math.sqrt(dir2[0]*dir2[0] + dir2[1]*dir2[1]) # distance to the next node
|
||||
handle1 = [ t[0][0] - t[1][0], t[0][1] - t[1][1] ] # handle towards previous node (rel coords)
|
||||
handle2 = [ t[2][0] - t[1][0], t[2][1] - t[1][1] ] # handle towards next node (rel coords)
|
||||
if self.very_close_xy(handle1, [ 0, 0 ]): handle1 = dir1
|
||||
if self.very_close_xy(handle2, [ 0, 0 ]): handle2 = dir2
|
||||
|
||||
prev = { 'idx': prev_idx, 'dir':dir1, 'handle':handle1 }
|
||||
next = { 'idx': next_idx, 'dir':dir2, 'handle':handle2 }
|
||||
sn = { 'idx': node_idx, 'prev': prev, 'next': next, 'x': t[1][0], 'y': t[1][1] }
|
||||
|
||||
if dist1 < self.radius:
|
||||
if debug:
|
||||
print("subpath node_idx=%d, dist to prev(%d) is smaller than radius: %g < %g" %
|
||||
(node_idx, prev_idx, dist1, self.radius), file=sys.stderr)
|
||||
pprint.pprint(sn, stream=sys.stderr)
|
||||
if self.skipped_small_len > dist1: self.skipped_small_len = dist1
|
||||
self.skipped_small_count += 1
|
||||
return None, None
|
||||
|
||||
if dist2 < self.radius:
|
||||
if debug:
|
||||
print("subpath node_idx=%d, dist to next(%d) is smaller than radius: %g < %g" %
|
||||
(node_idx, next_idx, dist2, self.radius), file=sys.stderr)
|
||||
pprint.pprint(sn, stream=sys.stderr)
|
||||
if self.skipped_small_len > dist2: self.skipped_small_len = dist2
|
||||
self.skipped_small_count += 1
|
||||
return None, None
|
||||
|
||||
len_h1 = math.sqrt(handle1[0]*handle1[0] + handle1[1]*handle1[1])
|
||||
len_h2 = math.sqrt(handle2[0]*handle2[0] + handle2[1]*handle2[1])
|
||||
prev['hlen'] = len_h1
|
||||
next['hlen'] = len_h2
|
||||
|
||||
if len_h1 < self.radius:
|
||||
if debug:
|
||||
print("subpath node_idx=%d, handle to prev(%d) is shorter than radius: %g < %g" %
|
||||
(node_idx, prev_idx, len_h1, self.radius), file=sys.stderr)
|
||||
pprint.pprint(sn, stream=sys.stderr)
|
||||
if self.skipped_small_len > len_h1: self.skipped_small_len = len_h1
|
||||
self.skipped_small_count += 1
|
||||
return None, None
|
||||
if len_h2 < self.radius:
|
||||
if debug:
|
||||
print("subpath node_idx=%d, handle to next(%d) is shorter than radius: %g < %g" %
|
||||
(node_idx, next_idx, len_h2, self.radius), file=sys.stderr)
|
||||
pprint.pprint(sn, stream=sys.stderr)
|
||||
if self.skipped_small_len > len_h2: self.skipped_small_len = len_h2
|
||||
self.skipped_small_count += 1
|
||||
return None, None
|
||||
|
||||
if len_h1 > dist1: # shorten that handle to dist1, avoid overshooting the point
|
||||
handle1[0] = handle1[0] * dist1 / len_h1
|
||||
handle1[1] = handle1[1] * dist1 / len_h1
|
||||
prev['hlen'] = dist1
|
||||
if len_h2 > dist2: # shorten that handle to dist2, avoid overshooting the point
|
||||
handle2[0] = handle2[0] * dist2 / len_h2
|
||||
handle2[1] = handle2[1] * dist2 / len_h2
|
||||
next['hlen'] = dist2
|
||||
|
||||
return sn, sp_node_idx_
|
||||
|
||||
|
||||
def arc_c_m_from_super_node(self, s):
|
||||
"""
|
||||
Given the supernode s and the radius self.radius, we compute and return two points:
|
||||
c, the center of the arc and m, the midpoint of the arc.
|
||||
|
||||
Method used:
|
||||
- construct the ray c_m_vec that runs though the original point p=[x,y] through c and m.
|
||||
- next.trim_pt, [x,y] and c form a rectangular triangle. Thus we can
|
||||
compute cdist as the length of the hypothenuses under trim and radius.
|
||||
- c is then cdist away from [x,y] along the vector c_m_vec.
|
||||
- m is closer to [x,y] than c by exactly radius.
|
||||
"""
|
||||
|
||||
a = [ s['prev']['trim_pt'][0] - s['x'], s['prev']['trim_pt'][1] - s['y'] ]
|
||||
b = [ s['next']['trim_pt'][0] - s['x'], s['next']['trim_pt'][1] - s['y'] ]
|
||||
|
||||
c_m_vec = [ a[0] + b[0],
|
||||
a[1] + b[1] ]
|
||||
l = math.sqrt( c_m_vec[0]*c_m_vec[0] + c_m_vec[1]*c_m_vec[1] )
|
||||
|
||||
cdist = math.sqrt( self.radius*self.radius + s['trim']*s['trim'] ) # distance [x,y] to circle center c.
|
||||
|
||||
c = [ s['x'] + cdist * c_m_vec[0] / l, # circle center
|
||||
s['y'] + cdist * c_m_vec[1] / l ]
|
||||
|
||||
m = [ s['x'] + (cdist-self.radius) * c_m_vec[0] / l, # spline midpoint
|
||||
s['y'] + (cdist-self.radius) * c_m_vec[1] / l ]
|
||||
|
||||
return (c, m)
|
||||
|
||||
|
||||
def arc_bezier_handles(self, p1, p4, c):
|
||||
"""
|
||||
Compute the control points p2 and p3 between points p1 and p4, so that the cubic bezier spline
|
||||
defined by p1,p2,p3,p2 approximates an arc around center c
|
||||
|
||||
Algorithm based on Aleksas Riškus and Hans Muller. Sorry Pomax, saw your works too, but did not use any.
|
||||
"""
|
||||
x1,y1 = p1
|
||||
x4,y4 = p4
|
||||
xc,yc = c
|
||||
|
||||
ax = x1 - xc
|
||||
ay = y1 - yc
|
||||
bx = x4 - xc
|
||||
by = y4 - yc
|
||||
q1 = ax * ax + ay * ay
|
||||
q2 = q1 + ax * bx + ay * by
|
||||
k2 = 4./3. * (math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx)
|
||||
|
||||
x2 = xc + ax - k2 * ay
|
||||
y2 = yc + ay + k2 * ax
|
||||
x3 = xc + bx + k2 * by
|
||||
y3 = yc + by - k2 * bx
|
||||
|
||||
return ([x2, y2], [x3, y3])
|
||||
|
||||
|
||||
def subpath_round_corner(self, sp, node_idx):
|
||||
sn, sp_node_idx_ = self.super_node(sp, node_idx)
|
||||
if sn is None: return sp # do nothing. stderr messages are already printed.
|
||||
|
||||
# The angle to be rounded is now between the vectors a and b
|
||||
#
|
||||
a = sn['prev']['handle']
|
||||
b = sn['next']['handle']
|
||||
a_len = sn['prev']['hlen']
|
||||
b_len = sn['next']['hlen']
|
||||
try:
|
||||
# From https://de.wikipedia.org/wiki/Schnittwinkel_(Geometrie)
|
||||
# Wikipedia has an abs() in the formula, which extracts the smaller of the two angles.
|
||||
# We don't want that. We need to distinguish betwenn spitzwingklig and stumpfwinklig.
|
||||
#
|
||||
alpha = math.acos( (a[0]*b[0]+a[1]*b[1]) / ( math.sqrt(a[0]*a[0]+a[1]*a[1]) * math.sqrt(b[0]*b[0]+b[1]*b[1]) ) )
|
||||
except:
|
||||
# Division by 0 error means path folds back on itself here. No space to apply a radius between the segments.
|
||||
self.skipped_degenerated += 1
|
||||
return sp
|
||||
|
||||
sn['alpha'] = math.degrees(alpha)
|
||||
|
||||
# find the amount to trim back both sides so that a circle of radius self.radius would perfectly fit.
|
||||
if alpha < self.eps:
|
||||
# path folds back on itself here. No space to apply a radius between the segments.
|
||||
self.skipped_degenerated += 1
|
||||
return sp
|
||||
if abs(alpha - math.pi) < self.eps:
|
||||
# stretched. radius won't be visible, that is just fine. No need to warn about that.
|
||||
return sp
|
||||
trim = self.radius / math.tan(0.5 * alpha)
|
||||
sn['trim'] = trim
|
||||
if trim < 0.0:
|
||||
print("Error: at node_idx=%d: angle=%g°, trim is negative: %g" % (node_idx, math.degrees(alpha), trim), file=sys.stderr)
|
||||
return sp
|
||||
|
||||
# a_len points to the previous node. There we can always allow max_trim_factor_single, as the trim was either already done,
|
||||
# or will not be done. Only at b_len we need to reserve space for the next trim.
|
||||
# FIXME: also allow max_trim_factor_single at b_len, when we find that the very next node will not be rounded.
|
||||
#
|
||||
available_len = min(max_trim_factor_single*a_len, self.max_trim_factor*b_len)
|
||||
if trim > available_len:
|
||||
if debug:
|
||||
if trim > max_trim_factor_single*a_len:
|
||||
print("Skipping where hlen_a %g * max_trim %g < needed_trim %g" % (a_len, max_trim_factor_single, trim), file=self.tty)
|
||||
if trim > self.max_trim_factor*b_len:
|
||||
print("Skipping where hlen_b %g * max_trim %g < needed_trim %g" % (b_len, self.max_trim_factor, trim), file=self.tty)
|
||||
pprint.pprint(sn, stream=self.tty)
|
||||
if self.skipped_small_len > available_len:
|
||||
self.skipped_small_len = available_len
|
||||
self.skipped_small_count += 1
|
||||
return sp
|
||||
trim_pt_p = [ sn['x'] + a[0] * trim / a_len, sn['y'] + a[1] * trim / a_len ]
|
||||
trim_pt_n = [ sn['x'] + b[0] * trim / b_len, sn['y'] + b[1] * trim / b_len ]
|
||||
sn['prev']['trim_pt'] = trim_pt_p
|
||||
sn['next']['trim_pt'] = trim_pt_n
|
||||
|
||||
if debug:
|
||||
pprint.pprint(sn, stream=self.tty)
|
||||
pprint.pprint(self.cut, stream=self.tty)
|
||||
# We replace the node_idx node by two nodes node_a, node_b.
|
||||
# We need an extra middle node node_m if alpha < 90° -- alpha is the angle between the tangents,
|
||||
# as the arc spans the remainder to complete 180° an arc with more than 90° needs the midpoint.
|
||||
|
||||
# We preserve the endpoints of the two outside handles if they are non-0-length.
|
||||
# We know that such handles are long enough (because of the above max_trim_factor checks)
|
||||
# to not flip around when applying the trim.
|
||||
# But we move the endpoints of 0-length outside handles with the point when trimming,
|
||||
# so that they don't end up on the inside.
|
||||
prev_handle = sp_node_idx_[0][:]
|
||||
next_handle = sp_node_idx_[2][:]
|
||||
if self.very_close_xy(prev_handle, sp_node_idx_[1]): prev_handle = trim_pt_p[:]
|
||||
if self.very_close_xy(next_handle, sp_node_idx_[1]): next_handle = trim_pt_n[:]
|
||||
|
||||
p1 = trim_pt_p[:]
|
||||
p7 = trim_pt_n[:]
|
||||
arc_c, p4 = self.arc_c_m_from_super_node(sn)
|
||||
node_a = [ prev_handle, p1[:], p1[:] ] # deep copy, as we may want to modify the second handle later
|
||||
node_b = [ p7[:], p7[:], next_handle ] # deep copy, as we may want to modify the first handle later
|
||||
|
||||
if alpha >= 0.5*math.pi or self.cut:
|
||||
if self.cut == False:
|
||||
# p3,p4,p5 do not exist, we need no midpoint
|
||||
p2, p6 = self.arc_bezier_handles(p1, p7, arc_c)
|
||||
node_a[2] = p2
|
||||
node_b[0] = p6
|
||||
if node_idx == 0:
|
||||
# use prev idx to know about the extra skip. +1 for the node here, +1 for inclusive.
|
||||
# CAUTION: Keep in sync below
|
||||
sp = [node_a] + [node_b] + sp[1:sn['prev']['idx']+2]
|
||||
else:
|
||||
sp = sp[:node_idx] + [node_a] + [node_b] + sp[node_idx+1:]
|
||||
else:
|
||||
p2, p3 = self.arc_bezier_handles(p1, p4, arc_c)
|
||||
p5, p6 = self.arc_bezier_handles(p4, p7, arc_c)
|
||||
node_m = [ p3, p4, p5 ]
|
||||
node_a[2] = p2
|
||||
node_b[0] = p6
|
||||
if node_idx == 0:
|
||||
# use prev idx to know about the extra skip. +1 for the node here, +1 for inclusive.
|
||||
# CAUTION: Keep in sync above
|
||||
sp = [node_a] + [node_m] + [node_b] + sp[1:sn['prev']['idx']+2]
|
||||
else:
|
||||
sp = sp[:node_idx] + [node_a] + [node_m] + [node_b] + sp[node_idx+1:]
|
||||
|
||||
# A closed path is formed by making the last node indentical to the first node.
|
||||
# So, if we trim at the first node, then duplicte that trim on the last node, to keep the loop closed.
|
||||
if node_idx == 0:
|
||||
sp[-1][0] = sp[0][0][:]
|
||||
sp[-1][1] = sp[0][1][:]
|
||||
sp[-1][2] = sp[0][2][:]
|
||||
|
||||
return sp
|
||||
|
||||
|
||||
def clean_up(self): # __fini__
|
||||
if self.tty is not None:
|
||||
self.tty.close()
|
||||
super(RoundCorners, self).clean_up()
|
||||
if self.skipped_degenerated:
|
||||
print("Warning: Skipped %d degenerated nodes (180° turn or end of path?).\n" % self.skipped_degenerated, file=sys.stderr)
|
||||
if self.skipped_small_count:
|
||||
print("Warning: Skipped %d nodes with not enough space (Value %g is too small. Try again with a smaller radius or only one node selected).\n" % (self.skipped_small_count, self.skipped_small_len), file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
RoundCorners().run()
|
21
extensions/fablabchemnitz/source_code_text/meta.json
Normal file
21
extensions/fablabchemnitz/source_code_text/meta.json
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name": "Source Code Text",
|
||||
"id": "fablabchemnitz.de.source_code_text",
|
||||
"path": "source_code_text",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "Source Code Text",
|
||||
"original_id": "org.henry.poster_text",
|
||||
"license": "GNU GPL v2",
|
||||
"license_url": "https://gitlab.com/inkscape/extensions/-/blob/master/LICENSE.txt",
|
||||
"comment": "based on Lorem Ipsum extension",
|
||||
"source_url": "",
|
||||
"fork_url": "https://gist.github.com/om-henners/8c642c87b71daa3ea68222d40167edbc",
|
||||
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Source+Code+Text",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"github.com/om-henners",
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Source Code Text</name>
|
||||
<id>fablabchemnitz.de.source_code_text</id>
|
||||
<param name="directory" type="path" mode="folder" gui-text="Directory to search for code">C:\Users\</param>
|
||||
<param name="pattern" type="string" gui-text="Filename pattern to match">py</param>
|
||||
<param name="wordsperpara" type="int" min="0" max="10000" gui-text="Maximum words per paragraph">0</param>
|
||||
<param name="numparas" type="int" min="0" max="1000" gui-text="Limit number of paragraphs">1</param>
|
||||
<label appearance="header">Help</label>
|
||||
<label>Based on the "Lorem Ipsum" plugin, this plugin searches the base directory for code, and strings it all together by concatenating on whitespace. If a flowed text is selected, Source code is added to it; otherwise a new flowed text object, the size of the page, is created in a new layer.</label>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Text"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">source_code_text.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import re
|
||||
import random
|
||||
import inkex
|
||||
from lxml import etree
|
||||
|
||||
class SourceCodeText(inkex.EffectExtension):
|
||||
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument("--directory", default='~/', help="Default directory")
|
||||
pars.add_argument("--pattern", default='py', help="File extension pattern")
|
||||
pars.add_argument("--wordsperpara", type=int, default=0, help="Maximum words per paragraph")
|
||||
pars.add_argument("--numparas", type=int, default=1, help="Number of paragraphs")
|
||||
|
||||
def text_generation(self):
|
||||
#Get all the matching files. Then yield words one at a time. This can take a while if there are a lot of files, but shouldn't be too bad.
|
||||
matcher = re.compile('.+\.{}$'.format(self.options.pattern))
|
||||
matched_files = []
|
||||
for root, _, names in os.walk(os.path.expanduser(self.options.directory)):
|
||||
for name in names:
|
||||
if matcher.match(name):
|
||||
matched_files.append(os.path.join(root, name))
|
||||
|
||||
random.shuffle(matched_files)
|
||||
for path in matched_files:
|
||||
with open(path, encoding = 'utf-8') as file:
|
||||
for word in file.read().split():
|
||||
yield word
|
||||
|
||||
def add_text(self, node):
|
||||
#Add the text to the node
|
||||
word_generator = self.text_generation()
|
||||
for _ in range(self.options.numparas):
|
||||
words = []
|
||||
para = etree.SubElement(node, inkex.addNS('flowPara','svg'))
|
||||
if self.options.wordsperpara:
|
||||
try:
|
||||
for _, word in zip(range(self.options.wordsperpara), word_generator):
|
||||
words.append(next(word_generator))
|
||||
except: #Exception as e:
|
||||
#inkex.errormsg(e)
|
||||
pass
|
||||
else:
|
||||
words = word_generator
|
||||
|
||||
if words:
|
||||
para.text = ' '.join(words)
|
||||
etree.SubElement(node, inkex.addNS('flowPara','svg'))
|
||||
else:
|
||||
break
|
||||
|
||||
def effect(self):
|
||||
found=0
|
||||
for id, node in self.svg.selected.items():
|
||||
if node.tag == inkex.addNS('flowRoot','svg'):
|
||||
found+=1
|
||||
if found==1:
|
||||
self.addText(node)
|
||||
if not found:
|
||||
#inkex.debug('No "flowRoot" elements selected. Unable to add text.')
|
||||
svg=self.document.getroot()
|
||||
gattribs = {inkex.addNS('label','inkscape'):'lorem ipsum',inkex.addNS('groupmode','inkscape'):'layer'}
|
||||
g=etree.SubElement(svg,inkex.addNS('g','svg'),gattribs)
|
||||
flowRoot=etree.SubElement(g,inkex.addNS('flowRoot','svg'),{inkex.addNS('space','xml'):'preserve'})
|
||||
flowRegion=etree.SubElement(flowRoot,inkex.addNS('flowRegion','svg'))
|
||||
rattribs = {'x':'0','y':'0','width':svg.get('width'),'height':svg.get('height')}
|
||||
rect=etree.SubElement(flowRegion,inkex.addNS('rect','svg'),rattribs)
|
||||
self.add_text(flowRoot)
|
||||
|
||||
if __name__ == '__main__':
|
||||
SourceCodeText().run()
|
@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"name": "Split And Break Bezier At t",
|
||||
"id": "fablabchemnitz.de.split_and_break_bezier_at_t",
|
||||
"path": "split_and_break_bezier_at_t",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "fablabchemnitz.de.split_and_break_bezier_at_t",
|
||||
"original_id": "split_and_break_bezier_at_t",
|
||||
"license": "GNU GPL v3",
|
||||
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/LICENSE",
|
||||
"comment": "written by Mario Voigt",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/split_and_break_bezier_at_t",
|
||||
"fork_url": null,
|
||||
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Split+And+Break+Bezier+At+t",
|
||||
"inkscape_gallery_url": "https://inkscape.org/de/~MarioVoigt/%E2%98%85split-and-break-bezier-at-t",
|
||||
"main_authors": [
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Split And Break Bezier At t</name>
|
||||
<id>fablabchemnitz.de.split_and_break_bezier_at_t</id>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="tab_settings" gui-text="Settings">
|
||||
<param name="split_select" type="optiongroup" appearance="radio" gui-text="Split by" gui-description="Choose to split by length or percentage">
|
||||
<option value="length">length</option>
|
||||
<option value="t">percentage (t)</option>
|
||||
</param>
|
||||
<param name="unit" type="optiongroup" appearance="combo" gui-text="Units">
|
||||
<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="target_length" type="float" min="0.0" precision="4" appearance="full" gui-text="Length">0.5000</param>
|
||||
<param name="target_t" type="float" min="0.0001" max="0.9999" precision="4" appearance="full" gui-text="t">0.5000</param>
|
||||
<param name="keep_start" type="bool" gui-text="Keep start">true</param>
|
||||
<param name="keep_end" type="bool" gui-text="Keep end">true</param>
|
||||
<param name="keep_seg" type="bool" gui-text="Keep only segment where trim applies" gui-description="A path can consist of more than one segment!">false</param>
|
||||
</page>
|
||||
<page name="tab_about" gui-text="About">
|
||||
<label appearance="header">Split And Break Bezier At t</label>
|
||||
<label>Splits a path at value t=0..1 (t=0.5 means 50%) or at a defined length with unit.
|
||||
Applies independently for each sub path in selection. Use 'Path > Reverse' to change the cutting direction.</label>
|
||||
<label>2021 - 2022 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
|
||||
<spacer/>
|
||||
<label appearance="header">Online Documentation</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/splitandbreakbezieratt</label>
|
||||
<spacer/>
|
||||
<label appearance="header">Contributing</label>
|
||||
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
|
||||
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
|
||||
<spacer/>
|
||||
<label appearance="header">MightyScape Extension Collection</label>
|
||||
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
|
||||
</page>
|
||||
<page name="tab_donate" gui-text="Donate">
|
||||
<label appearance="header">Coffee + Pizza</label>
|
||||
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
|
||||
<spacer/>
|
||||
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
|
||||
<spacer/>
|
||||
<label>Thanks for using our extension and helping us!</label>
|
||||
<image>../000_about_fablabchemnitz.svg</image>
|
||||
</page>
|
||||
</param>
|
||||
<effect>
|
||||
<object-type>path</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Paths - Cut/Intersect/Purge"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">split_and_break_bezier_at_t.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Extension for InkScape 1.0
|
||||
|
||||
Author: Mario Voigt / FabLab Chemnitz
|
||||
Mail: mario.voigt@stadtfabrikanten.org
|
||||
Date: 01.06.2021
|
||||
Last patch: 02.06.2021
|
||||
License: GNU GPL v3
|
||||
|
||||
Splits a path at value t=0..1 (t=0.5 means 50%) or at a defined length with unit.
|
||||
Applies independently for each sub path in selection. Use 'Path > Reverse' to change the cutting direction.
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
import inkex
|
||||
from inkex import bezier, CubicSuperPath, PathElement, Path
|
||||
from inkex.bezier import csplength
|
||||
|
||||
class SplitAndBreakBezierAtT(inkex.EffectExtension):
|
||||
|
||||
def breakContours(self, element, breakelements = None):
|
||||
''' this does the same as "CTRL + SHIFT + K" '''
|
||||
if breakelements == None:
|
||||
breakelements = []
|
||||
if element.tag == inkex.addNS('path','svg'):
|
||||
parent = element.getparent()
|
||||
idx = parent.index(element)
|
||||
idSuffix = 0
|
||||
raw = element.path.to_arrays()
|
||||
subPaths, prev = [], 0
|
||||
for i in range(len(raw)): #breaks compound paths into sub paths
|
||||
if raw[i][0] == 'M' and i != 0:
|
||||
subPaths.append(raw[prev:i])
|
||||
prev = i
|
||||
subPaths.append(raw[prev:])
|
||||
for subpath in subPaths:
|
||||
replacedelement = copy.copy(element)
|
||||
oldId = replacedelement.get('id')
|
||||
csp = CubicSuperPath(subpath)
|
||||
if len(subpath) > 1 and csp[0][0] != csp[0][1]: #avoids pointy paths like M "31.4794 57.6024 Z"
|
||||
replacedelement.set('d', csp)
|
||||
replacedelement.set('id', oldId + str(idSuffix))
|
||||
parent.insert(idx, replacedelement)
|
||||
idSuffix += 1
|
||||
breakelements.append(replacedelement)
|
||||
parent.remove(element)
|
||||
for child in element.getchildren():
|
||||
self.breakContours(child, breakelements)
|
||||
return breakelements
|
||||
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument('--tab')
|
||||
pars.add_argument('--split_select', default="t")
|
||||
pars.add_argument('--unit', default="mm")
|
||||
pars.add_argument('--target_length', type=float, default=0.5)
|
||||
pars.add_argument('--target_t', type=float, default=0.5)
|
||||
pars.add_argument('--keep_start', type=inkex.Boolean, default=True)
|
||||
pars.add_argument('--keep_end', type=inkex.Boolean, default=True)
|
||||
pars.add_argument('--keep_seg', type=inkex.Boolean, default=False)
|
||||
|
||||
def effect(self):
|
||||
#if self.options.split_select == "t" and self.options.target_t == 0.0:
|
||||
# inkex.utils.debug("You have seleted 'percentage (t)' but your t parameter is 0.0. It would simply result in element deletion!")
|
||||
# return
|
||||
#if self.options.split_select == "t" and self.options.target_t == 1.0:
|
||||
# inkex.utils.debug("You have seleted 'percentage (t)' but your t parameter is 1.0. It would'nt exist any trim result!")
|
||||
# return
|
||||
|
||||
breakApartElements = None
|
||||
for element in self.svg.selection.filter(PathElement):
|
||||
breakApartElements = self.breakContours(element, breakApartElements)
|
||||
|
||||
if breakApartElements is not None:
|
||||
for element in breakApartElements:
|
||||
csp = element.path.to_superpath()
|
||||
slengths, totalLength = csplength(csp)
|
||||
if totalLength == 0:
|
||||
inkex.utils.debug("{} is invalid: zero length (path d='{}'). Skipping ...".format(element.get('id'), element.path))
|
||||
continue
|
||||
if self.options.split_select == "t":
|
||||
length_at_target_t = self.options.target_t * totalLength
|
||||
elif self.options.split_select == "length":
|
||||
length_at_target_t = self.svg.unittouu(str(self.options.target_length) + self.options.unit)
|
||||
if length_at_target_t > totalLength:
|
||||
inkex.utils.debug("Entered length is larger than length of {}. Skipping ...".format(element.get('id')))
|
||||
continue
|
||||
self.options.target_t = length_at_target_t / totalLength #override
|
||||
|
||||
new = []
|
||||
keep = [] #some copy for the segment where the split applies
|
||||
lengthSum = 0
|
||||
segOfTOccurence = None
|
||||
for seg in csp:
|
||||
new.append([seg[0][:]])
|
||||
for i in range(1,len(seg)):
|
||||
aSeg = seg[i][0]
|
||||
segLength = bezier.cspseglength(new[-1][-1], seg[i])
|
||||
lengthSum += segLength
|
||||
current_t = lengthSum / totalLength
|
||||
#insert a new breaking node in case we are at the desired t parameter
|
||||
if current_t >= self.options.target_t:
|
||||
if segOfTOccurence is None:
|
||||
segOfTOccurence = i
|
||||
t_dist = 1 - ((lengthSum - length_at_target_t) / segLength)
|
||||
result = bezier.cspbezsplitatlength(new[-1][-1], seg[i], t_dist)
|
||||
better_result = [[list(el) for el in elements] for elements in result]
|
||||
new[-1][-1], nxt, seg[i] = better_result
|
||||
new[-1].append(nxt[:])
|
||||
if self.options.keep_start is True and self.options.keep_end is False:
|
||||
if segOfTOccurence == 1:
|
||||
keep.append([seg[i-1][0], seg[i-1][0], seg[i-1][0]])
|
||||
else:
|
||||
keep.append([seg[i-1][1], seg[i-1][1], seg[i-1][1]])
|
||||
keep.append([better_result[0][2], nxt[0], nxt[1]])
|
||||
elif self.options.keep_start is False and self.options.keep_end is True:
|
||||
keep.append([better_result[0][2], nxt[0], nxt[1]])
|
||||
keep.append([better_result[1][2], better_result[2][0], seg[i][1]])
|
||||
elif self.options.keep_start is True and self.options.keep_end is True:
|
||||
if segOfTOccurence == 1:
|
||||
keep.append([seg[i-1][0], seg[i-1][0], seg[i-1][0]])
|
||||
else:
|
||||
keep.append([seg[i-1][1], seg[i-1][1], seg[i-1][1]])
|
||||
keep.append([seg[i-1][2], aSeg, seg[i][1]])
|
||||
|
||||
new[-1].append(seg[i])
|
||||
|
||||
if self.options.keep_seg is False:
|
||||
newpath = CubicSuperPath(new).to_path(curves_only=True).to_arrays()
|
||||
#insert the splitting at the occurence (we add "m 0,0") to break the path
|
||||
newpath.insert(segOfTOccurence + 1, ['m', [0, 0]])
|
||||
element.path = Path(newpath)
|
||||
breakAparts = self.breakContours(element)
|
||||
|
||||
if len(breakAparts) > 0:
|
||||
pathStart = breakAparts[0]
|
||||
if len(breakAparts) > 1:
|
||||
pathEnd = breakAparts[1]
|
||||
if self.options.keep_start is False and len(breakAparts) > 0:
|
||||
pathStart.delete()
|
||||
if self.options.keep_end is False and len(breakAparts) > 1:
|
||||
pathEnd.delete()
|
||||
|
||||
else:
|
||||
element.path = CubicSuperPath(keep)
|
||||
|
||||
#print the breaking point coordinate
|
||||
#for step, (x, y) in enumerate(breakAparts[1].path.end_points):
|
||||
# self.msg("x={},y={}".format(x, y))
|
||||
# break
|
||||
else:
|
||||
inkex.utils.debug("Selection seems to be empty!")
|
||||
return
|
||||
if __name__ == '__main__':
|
||||
SplitAndBreakBezierAtT().run()
|
21
extensions/fablabchemnitz/streaks/meta.json
Normal file
21
extensions/fablabchemnitz/streaks/meta.json
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name": "Streaks",
|
||||
"id": "fablabchemnitz.de.streaks",
|
||||
"path": "streaks",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "streaks",
|
||||
"original_id": "ca.sfu.AT.kurn.Streaks",
|
||||
"license": "GNU GPL v3",
|
||||
"license_url": "https://sourceforge.net/projects/inkscape-streaks/",
|
||||
"comment": "ported to Inkscape v1 by Mario Voigt",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/streaks",
|
||||
"fork_url": "https://sourceforge.net/projects/inkscape-streaks",
|
||||
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Streaks",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"sourceforge.net/andrew-kurn",
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
42
extensions/fablabchemnitz/streaks/streaks.inx
Normal file
42
extensions/fablabchemnitz/streaks/streaks.inx
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Streaks</name>
|
||||
<id>fablabchemnitz.de.streaks</id>
|
||||
<param type="notebook" name="Nmain">
|
||||
<page name="top" gui-text="Color">
|
||||
<label appearance="header">Fills a box with a texture made of vertical line segments.</label>
|
||||
<param name="strokeColor" type="color" appearance="colorbutton" gui-text="Line color">255</param>
|
||||
<separator/>
|
||||
<param name="strokeWidth" type="int" gui-text="Line width (px)">2</param>
|
||||
</page>
|
||||
<page name="main" gui-text="Main">
|
||||
<param max="256" name="blur" type="int" gui-text="Blur">2</param>
|
||||
<param max="1000" name="linno" type="int" gui-text="# of columns">50</param>
|
||||
<param name="xrand" type="bool" gui-text="Lines randomized">true</param>
|
||||
<separator/>
|
||||
<param name="pagep" type="bool" gui-text="Default box to page size?">true</param>
|
||||
<param max="10000" name="cusx" type="int" gui-text="Custom size x">500</param>
|
||||
<param max="10000" name="cusy" type="int" gui-text="Custom size y">500</param>
|
||||
</page>
|
||||
<page name="vert" gui-text="Each column">
|
||||
<param min="1" max="256" name="segLen" type="int" gui-text="# of segments">8</param>
|
||||
<param name="yrand" type="bool" gui-text="Lengths randomized">true</param>
|
||||
<param name="dashp" type="bool" gui-text="Use dashes?">true</param>
|
||||
<param name="blankp" type="bool" gui-text="Use blanks?">true</param>
|
||||
<param name="dotp" type="bool" gui-text="Use dots?">true</param>
|
||||
<param max="1000" name="dots" type="int" gui-text="Dots per height">100</param>
|
||||
<label>This sets the size of a dot relative to the total height. Higher is shorter.</label>
|
||||
</page>
|
||||
</param>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz Shape Generators">
|
||||
<submenu name="Streaks And Blobs" />
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">streaks.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
157
extensions/fablabchemnitz/streaks/streaks.py
Normal file
157
extensions/fablabchemnitz/streaks/streaks.py
Normal file
@ -0,0 +1,157 @@
|
||||
#! /usr/bin/env python3
|
||||
import random
|
||||
rr = random.randint(1,10)
|
||||
import inkex
|
||||
from lxml import etree
|
||||
|
||||
class Streaks(inkex.EffectExtension):
|
||||
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument('--blur', type = int, default = 2)
|
||||
pars.add_argument('--linno', type = int, default = 50)
|
||||
pars.add_argument('--xrand', type = inkex.Boolean, default = True)
|
||||
pars.add_argument('--pagep', type = inkex.Boolean, default = True)
|
||||
pars.add_argument('--cusx', type = int, default = 500)
|
||||
pars.add_argument('--cusy', type = int, default = 500)
|
||||
pars.add_argument('--segLen', type = int, default = 8)
|
||||
pars.add_argument('--yrand', type = inkex.Boolean, default = True)
|
||||
pars.add_argument('--dashp', type = inkex.Boolean, default = True)
|
||||
pars.add_argument('--blankp', type = inkex.Boolean, default = True)
|
||||
pars.add_argument('--dotp', type = inkex.Boolean, default = True)
|
||||
pars.add_argument('--dots', type = int, default = 100)
|
||||
pars.add_argument('--strokeColor', default = 255)
|
||||
pars.add_argument('--strokeWidth', type = int, default = 2)
|
||||
pars.add_argument("--Nmain", default='title')
|
||||
|
||||
def effect(self):
|
||||
blur = int(self.options.blur)
|
||||
linno = int(self.options.linno)
|
||||
xrand = bool(self.options.xrand)
|
||||
pagep = bool(self.options.pagep)
|
||||
cusx = int(self.options.cusx)
|
||||
cusy = int(self.options.cusy)
|
||||
segLen = int(self.options.segLen)
|
||||
yrand = bool(self.options.yrand)
|
||||
dashp = bool(self.options.dashp)
|
||||
blankp = bool(self.options.blankp)
|
||||
dotp = bool(self.options.dotp)
|
||||
dots = int(self.options.dots)
|
||||
strokeColor = int(self.options.strokeColor)
|
||||
strokeWidth = int(self.options.strokeWidth)
|
||||
|
||||
# Get access to main SVG document element and get its dimensions.
|
||||
svg = self.document.getroot()
|
||||
|
||||
if pagep :
|
||||
try :
|
||||
width = self.svg.unittouu(svg.get('width'))
|
||||
height = self.svg.unittouu(svg.attrib['height'])
|
||||
except AttributeError :
|
||||
width = self.unittouu(svg.get('width'))
|
||||
height = self.unittouu(svg.attrib['height'])
|
||||
# inkex.errormsg("Page size %d %d" % (width, height))
|
||||
else :
|
||||
width = cusx
|
||||
height = cusy
|
||||
|
||||
|
||||
# Find defs node.
|
||||
for child in svg :
|
||||
if -1 != child.tag.find("defs") :
|
||||
break
|
||||
else:
|
||||
inkex.errormsg("No defs child found")
|
||||
defs = child
|
||||
|
||||
if blur :
|
||||
filter = etree.SubElement(defs, "filter")
|
||||
filter.set(inkex.addNS('collect', 'inkscape'), 'always')
|
||||
filname = self.svg.get_unique_id('filter')
|
||||
filter.set('id' , filname)
|
||||
|
||||
finfo = etree.SubElement(filter, 'feGaussianBlur')
|
||||
finfo.set(inkex.addNS('collect', 'inkscape'), 'always')
|
||||
finfo.set('stdDeviation', str(blur))
|
||||
|
||||
""" Debug
|
||||
for i in range(len(svg)) :
|
||||
k = svg[i].attrib
|
||||
for ky in k :
|
||||
inkex.errormsg(ky)
|
||||
|
||||
# Clean any old layers
|
||||
flag = False
|
||||
for i in range(len(svg)) :
|
||||
dic = svg[i].attrib
|
||||
for key in dic:
|
||||
if -1 != key.find("label") :
|
||||
if 'Streak Layer' == dic[key] :
|
||||
del svg[i]
|
||||
flag = True
|
||||
if flag :
|
||||
inkex.errormsg("Found old Streak layer")
|
||||
else:
|
||||
inkex.errormsg("Clean")
|
||||
"""
|
||||
# Create a new layer.
|
||||
layer = etree.SubElement(svg, 'g')
|
||||
layer.set(inkex.addNS('label', 'inkscape'), 'Streak Layer')
|
||||
layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
|
||||
|
||||
# Create path element
|
||||
path = etree.Element(inkex.addNS('path','svg'))
|
||||
|
||||
alpha = strokeColor & 255
|
||||
color = (strokeColor >> 8) & int('ffffff', 16)
|
||||
style = {
|
||||
'stroke' : '#%06X' % color,
|
||||
'stroke-width' : "{}px".format(strokeWidth),
|
||||
}
|
||||
#inkex.errormsg("Colour %s" % strokeColor)
|
||||
|
||||
if blur : style['filter'] = 'url(#' + filname +')'
|
||||
|
||||
|
||||
path.set('style', str(inkex.Style(style)))
|
||||
|
||||
pathstring = ''
|
||||
seglim = int(height / segLen)
|
||||
ditlen = int(height / dots)
|
||||
|
||||
xco = 0
|
||||
while xco < width :
|
||||
y = 0
|
||||
flag = random.randint(0, 2)
|
||||
while y < height :
|
||||
if yrand :
|
||||
yinc = random.randint(1, seglim)
|
||||
else :
|
||||
yinc = seglim
|
||||
if flag == 1 and dashp: #Draw dash
|
||||
pathstring += ' M '+str(xco)+','+str(y)+' L '+str(xco)+','+str(min(y + yinc, height))
|
||||
y += yinc + ditlen
|
||||
elif flag == 2 and dotp: #Draw dots
|
||||
ylim = min(y + yinc, height)
|
||||
while y < ylim :
|
||||
pathstring += ' M '+str(xco)+','+str(y)+' L '+str(xco)+','+str(min(y + ditlen, height))
|
||||
y += 2*ditlen
|
||||
elif flag == 0 and blankp :
|
||||
y += yinc #Adding blank space
|
||||
elif not (dashp or dotp or blankp) : #Squiggle if user turns them off
|
||||
sdit = str(2*ditlen)+' '
|
||||
pathstring += ' M '+str(xco)+','+str(y)+' q '+ 2*sdit + '0 ' +sdit
|
||||
for i in range(int(height/(2*ditlen))) :
|
||||
pathstring += 't 0 '+sdit
|
||||
y = height
|
||||
flag = (flag + 1)%3
|
||||
if xrand :
|
||||
xco += random.randint(0, int(2 * width / linno))
|
||||
else :
|
||||
xco += width / linno
|
||||
path.set('d', pathstring)
|
||||
|
||||
# Connect elements together.
|
||||
layer.append(path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
Streaks().run()
|
Loading…
Reference in New Issue
Block a user