Added Delete Aboce

This commit is contained in:
Mario Voigt 2020-08-13 19:26:42 +02:00
parent 311a84f534
commit f25dc34b5c
5 changed files with 582 additions and 0 deletions

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<_name>Delete Above</_name>
<id>fablabchemnitz.de.deleteabove</id>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">fablabchemnitz_delete_above.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Inkscape plugin: delete the selected object(s) and any objects on top of them.
I.E. delete the selected node(s) and any other nodes that are completely
within the bounding-boxes of the selected nodes that have a greater z-order
Simply put, you select a "background" rectangle, then use this plugin to
delete it and everything that appears on top of it.
"""
import collections
import subprocess
import os
import pprint
import inkex
Point = collections.namedtuple('Point', ['x', 'y'])
Box = collections.namedtuple('Box', ['tl', 'tr', 'br', 'bl'])
# tl=top left, tr=top right, br=bottom right, bl=bottom left
nodeInfo = collections.namedtuple('nodeInfo',
['x', 'y', 'width', 'height'])
def contains(outer, inner):
""" Return true if the Box inner is completely within the Box outer """
return inner.tl.x >= outer.tl.x and inner.tl.y >= outer.tl.y and \
inner.br.x <= outer.br.x and inner.br.y <= outer.br.y
class DeleteAboveEffect(inkex.Effect):
""" Delete the selected node and everything above it """
node_info = None
def load_node_info(self):
""" Ask inkscape for information about all object bounding boxes """
node_info = dict()
command = ['inkscape', '--query-all', self.options.input_file]
check_output = subprocess.check_output
with open(os.devnull, 'w') as null:
for line in check_output(command, stderr=null).splitlines():
raw_id, raw_x, raw_y, raw_width, raw_height = line.split(b',')
node_info[raw_id.decode("UTF-8")] = nodeInfo(float(raw_x),
float(raw_y),
float(raw_width),
float(raw_height))
self.node_info = node_info
return
def remove(self, node):
""" Remove the specified node from the document tree """
parent = node.getparent()
if parent is None:
return
parent.remove(node)
def bbox(self, node):
""" Return the bounding-box of the given node """
node_id = node.get('id')
#inkex.utils.debug("Check if " + str(node_id) + " is in " + str(self.node_info))
info = self.node_info[node_id]
x = info.x
y = info.y
width = info.width
height = info.height
return Box(Point(x, y),
Point(x + width, y),
Point(x + width, y + height),
Point(x, y + height))
def effect(self):
"""
For every selected node, remove it and all items on top of it
"""
self.load_node_info()
nodes_to_remove = list()
for id, node in self.svg.selected.items():
selected_node_bbox = self.bbox(node)
nodes_to_remove.append(node)
# search the document tree for the selected node
# when found, every subsequent node will be "above" it.
# (i.e. svg documents draw from the background up, so a background
# node will appear first, then nodes that are progressively
# closer to the viewer will appear subsequently in the svg file)
found_selected_node = False
for node in self.document.getiterator():
if not found_selected_node:
if node == node:
found_selected_node = True
continue
# Hereafter we are iterating over all nodes above the
# selected node. We need to delete them if they appear to
# be "on top of" the selection (i.e. within the bounding box
# of the selection)
try:
node_bbox = self.bbox(node)
except KeyError:
continue
if contains(selected_node_bbox, node_bbox):
nodes_to_remove.append(node)
# Now we remove the items we've previously found. Search and remove
# need to be separate bulk steps because tree search is disrupted by
# tree modification
for condemned_node in set(nodes_to_remove):
self.remove(condemned_node)
if __name__ == '__main__':
if False:
# Some tools for debug use
PPRINTER = pprint.PrettyPrinter(indent=4)
FMT = PPRINTER.pformat
DUMP = lambda obj: inkex.debug(FMT(obj))
DeleteAboveEffect().run()

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Path Intersections</name>
<id>fablabchemnitz.de.path_intersections</id>
<param name="desc" type="description">This plugin - initially called "Precut" - was found deeply on web and was nearly lost in translation. Ported to Python 3.0 for InkScape 1.0. This tool finds path intersections within the complete SVG document. Intersections are going to be marked with little squares.</param>
<param name="color" type="color" appearance="colorbutton" gui-text="Error highlight color?">4012452351</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Nesting/Cut Optimization"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">fablabchemnitz_path_intersections.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,397 @@
#!/usr/bin/env python3
# This file is part of Precut.
#
# Precut 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.
#
# Precut 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 Precut. If not, see <http://www.gnu.org/licenses/>.
# please, stick to pep8 formatting for this file
"""
Migrator: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Date: 13.08.2020
This plugin - initially called "Precut" - was found deeply on web and was nearly lost in translation. Ported to Python 3.0 for InkScape 1.0. This tool finds path intersections within the complete SVG document. Intersections are going to be marked with little squares.
"""
"""
What do we want to check?
=========================
* any text objects that are not converted to a path?
* can be implemented as tag blacklist
* any outlines? they need to be converted to paths
* check for crossing paths
* this is the hardest
* for lines, this is easy, and can, for example, be done with shapely:
>>> from shapely.geometry import LineString
>>> l1 = LineString([(0, 0), (1, 1)])
>>> l2 = LineString([(0, 1), (1, 0)])
>>> l1.intersects(l2)
True
>>> p = l1.intersection(l2)
>>> p.x
0.5
>>> p.y
0.5
* check for self-intersection, too (=> line.is_simple)
* need to split each complex subpath into its segments
* then, when doing intersections, remove the `boundary`
from the intersection set, because two adjacent
segments from a subpath always intersect in their boundary
* handle the commands M, Z, L, C, Q, A (parsed via simplepath)
* M: moveto
* Z: closepath (straight closing line)
* L: lineto
* C: curveto (cubic bezier)
* Q: curveto (quadratic bezier)
* A: elliptical arc (circles, ellipsis)
* paths need to have a minimum distance to other paths
* if two paths are connected ("T-junction"), this junction needs to be
exempt from the distance check.
"""
from lxml import etree
import inkex
from inkex import bezier
from inkex.paths import Path
from inkex import Color
import sys
import logging
from shapely.geometry import LineString, MultiLineString, Point, MultiPoint, GeometryCollection
from shapely import speedups
if speedups.available:
speedups.enable()
logger = logging.getLogger(__name__)
def take_N(seq, n):
"""
split ``seq`` into slices of length ``n``. the total
length of ``seq` must be a multiple of ``n``.
"""
if len(seq) % n != 0:
raise ValueError("len=%d, n=%d, (%s)" % (len(seq), n, seq))
sub = []
for elem in seq:
sub.append(elem)
if len(sub) == n:
yield sub
sub = []
def linear_interp(a, b, t):
"""
linearly interpolate between ``a`` and ``b``. ``t`` must be a
a float between 0 and 1.
"""
return (1 - t) * a + t * b
def sample(start, stop, num):
"""
interpolate between start and stop, and yield ``num`` samples
"""
if num == 0:
return
delta = 1.0 / num
t = 0
for i in range(num):
yield linear_interp(start, stop, t)
t += delta
class CheckerResult(object):
def __init__(self, msg, elem, extra=None, max_len=50):
self.msg = msg
self.elem = elem
self.extra = extra
self.max_len = max_len
def fmt(self, s):
s = ", ".join(["%s: %s" % (k, v) for k, v in s.items()])
if len(s) > self.max_len:
return s[:50] + u""
return s
def __unicode__(self):
msg, elem, extra = self.msg, self.elem, self.extra
if extra:
return "%s: %s (%s)" % (msg, elem.get("id"), self.fmt(extra))
else:
return "%s: %s" % (msg, elem.get("id"))
def __repr__(self):
return "<CheckerResult: %s>" % self.msg
class Checker(object):
def __call__(self, elem):
"""
run a check on ``elem`` and yield (elem, message) tuples
for each failed check
"""
raise NotImplementedError("please implement __call__")
def collect(self):
"""
run a second stage check on aggregated data
"""
return []
class StyleChecker(Checker):
def __call__(self, elem):
style = elem.get("style")
if style is None:
return
parsed = dict(inkex.Style.parse_str(style))
if "stroke" in parsed and parsed["stroke"] != "none":
yield CheckerResult("element with stroke found", elem)
class ElemBlacklistChecker(Checker):
blacklist = ["text"]
def __call__(self, elem):
_, tag = elem.tag.rsplit("}", 1)
if tag in self.blacklist:
yield CheckerResult("'%s' element found in document" % tag, elem)
class Subpath(object):
def __init__(self):
self.points = []
self.cursor = None
def __len__(self):
return len(self.points)
@property
def last_point(self):
if self.points:
return self.points[-1]
@property
def first_point(self):
if self.points:
return self.points[0]
def moveto(self, point):
assert len(self) == 0, "moveto may only be called at the beginning of a subpath"
self.points.append(point)
self.cursor = point
def lineto(self, point):
self.points.append(point)
self.cursor = point
def curveto(self, points):
for p in self.approx_curve([self.cursor] + points):
self.lineto(p)
def closepath(self):
self.lineto(self.first_point)
def add_points(self, points):
self.points.extend(points)
self.cursor = points[-1]
def as_linestring(self):
return LineString(self.points)
def approx_curve(self, points):
for four in take_N(points, 4):
# TODO: automatically set number of samples depending on length
for t in sample(0, 1, 50):
yield bezier.bezierpointatt(four, t)
class IntersectionChecker(Checker):
def __init__(self):
self.paths = []
def __call__(self, elem):
# logger.debug(elem.attrib)
path = elem.get("d")
if path is None:
return []
parsed = Path(path).to_arrays()
self.paths.append((parsed, elem))
# logger.debug(parsed)
return []
def get_line_strings(self):
# logger.debug("paths: %s", self.paths)
for path, elem in self.paths:
logger.debug("new path, %s", elem.get("id"))
current_subpath = Subpath()
for cmd, coords in path:
logger.debug(" new command: %s", cmd)
if cmd != "A":
points = list(take_N(coords, n=2))
else:
points = list(take_N(coords, n=7))
logger.debug(" points: %s", points)
if cmd == "M":
# M starts a new subpath
if len(current_subpath) > 1:
yield current_subpath, elem
current_subpath = Subpath()
current_subpath.moveto(points[0])
# more than one point means the rest of the points are to
# be treated as if cmd was L:
# http://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands
if len(points) > 1:
points = points[1:]
cmd = "L"
if cmd == "L":
current_subpath.add_points(points)
if cmd == "Z":
current_subpath.closepath()
if cmd == "C":
current_subpath.curveto(points)
if cmd == "Q":
logger.warning("quadratic beziers are not supported yet")
# current_subpath.moveto(points[-1])
if cmd == "A":
logger.warning("elliptic arcs are not supported yet")
if len(current_subpath) > 1:
yield current_subpath, elem
current_subpath = Subpath()
def collect(self):
return self.check_intersections()
def check_intersections(self):
checks_done = MultiLineString()
for subpath, elem in self.get_line_strings():
line = subpath.as_linestring()
if not line.is_simple:
# TODO: find location of self-intersection and introduce some
# tolerance
# checks_done = checks_done.union(line)
yield CheckerResult("self-intersection found", elem)
# continue
if checks_done.intersects(line):
intersection = checks_done.intersection(line)
yield CheckerResult("intersection found", elem, extra={"intersection": intersection})
checks_done = checks_done.union(line)
class ErrorVisualization(object):
def __init__(self, svg, effect, color, group_id="precut_errors"):
self.svg = svg
self.color = color
g = svg.find(".//%s[@id='%s']" % (inkex.addNS("g", "svg"), group_id))
if g is not None:
self.g = g
else:
parent = svg
attrs = {"id": group_id, "style": "opacity:.5", inkex.addNS("label", "inkscape"): "Precut Errors"}
self.g = etree.SubElement(parent, inkex.addNS("g", "svg"), attrs)
def getColorString(self, pickerColor):
longcolor = int(pickerColor)
if longcolor < 0:
longcolor = longcolor & 0xFFFFFFFF
return '#' + format(longcolor >> 8, '06X')
def fmt_point(self, point):
return "%s %s" % point
def convert(self, geom):
"""
convert a shapely geometry to SVG
"""
def vis_line_string(geom):
path = []
point_iter = iter(geom.coords)
head = next(point_iter)
tail = list(point_iter)
path.append("M%s" % self.fmt_point(head))
for point in tail:
path.append("L%s" % self.fmt_point(point))
attrs = {"d": " ".join(path), "style": "stroke:%s;stroke-width:5px;" % self.getColorString(self.color)}
etree.SubElement(self.g, inkex.addNS("path", "svg"), attrs)
def vis_point(geom):
x, y = geom.x, geom.y
x1, y1 = x - 5, y - 5
x2, y2 = x - 5, y + 5
x3, y3 = x + 5, y + 5
x4, y4 = x + 5, y - 5
vis_line_string(LineString([(x1, y1), (x2, y2), (x3, y3), (x4, y4), (x1, y1)]))
def vis_geom_collection(geom):
for g in geom.geoms:
self.convert(g)
converters = {
LineString: vis_line_string,
Point: vis_point,
MultiLineString: vis_geom_collection,
MultiPoint: vis_geom_collection,
GeometryCollection: vis_geom_collection,
}
converters[geom.__class__](geom)
def add_error(self, geom):
self.convert(geom)
class Precut(inkex.Effect):
def __init__(self, *args, **kwargs):
self.check_result = []
self.checkers = [ElemBlacklistChecker(), StyleChecker(), IntersectionChecker()]
inkex.Effect.__init__(self, *args, **kwargs)
self.arg_parser.add_argument("--color", type=Color, default='4012452351', help="Error highlight color")
def walk(self, elem):
if elem.get("id") == "precut_errors":
return
for child in elem.iterchildren():
self.visit(child)
self.walk(child)
def visit(self, elem):
logger.debug("visiting %s", elem)
for checker in self.checkers:
self.check_result.extend(checker(elem))
def effect(self):
svg = self.document.getroot()
self.walk(svg)
vis = ErrorVisualization(svg, self, color=self.options.color)
# additional "collect" pass for "global" analysis
for checker in self.checkers:
self.check_result.extend(checker.collect())
for res in self.check_result:
#print >>sys.stderr, unicode(res).encode("utf8")
#print(sys.stderr, str(res.encode("utf8")))
if res.extra and "intersection" in res.extra:
# TODO: add visualization for other kinds of errors
vis.add_error(res.extra["intersection"])
if __name__ == "__main__":
logging.basicConfig(stream=sys.stderr, level=logging.WARNING, format="%(levelname)s %(message)s")
Precut().run()

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Optimize Sequence: Travel Distances</name>
<id>fablabchemnitz.de.reorder_sequence</id>
<param indent="5" name="splashpage" type="description" appearance="header">EggBot Plot Optimization Tool</param>
<param name="Header" type="description" xml:space="preserve">
This utility will re-order objects within each layer of your
document, to reduce pen-up travel distance and time.
</param>
<param name="reordering" gui-text="Group Handling" type="optiongroup" appearance="combo" indent="2">
<option value="2">Reorder within groups</option>
<option value="1">Preserve groups</option>
<option value="3">Break apart groups</option>
</param>
<param name="Footer" type="description" indent="3" xml:space="preserve">
v 2.5. Copyright 2019, Evil Mad Scientist</param>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Nesting/Cut Optimization"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">fablabchemnitz_reorder_sequence.py</command>
</script>
</inkscape-extension>