├── LICENSE ├── README.md ├── clipped.png ├── destructiveclip.inx ├── destructiveclip.py └── readme.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mark Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DestructiveClip 2 | An Inkscape Extension which works like Object|Clip|Set except that the paths clipped are actually **modified**. 3 | 4 | Thus the clipping is included when exported, for example as a DXF file. 5 | Select two or more **paths** then choose Extensions|Modify path|Destructive clip. The topmost path will be used to clip the others. 6 | 7 | Notes:- 8 | * Curves (including Arcs) in paths are not supported (use Extensions|Modify path|Flatten Beziers). 9 | * Non-path objects in the selection will be ignored. Use Object|Ungroup. 10 | * Paths entirely outside the clipping path will remain untouched (rather than modifying them to an empty path) 11 | * Complex paths may take a while (there seems to be no way too show progress) 12 | * Yes, using MBR's to do gross clipping might make it faster 13 | * No, Python is not my first language (C/C++ is) 14 | 15 | Mark Wilson Feb 2016 16 | 17 | # To install 18 | Copy destructiveclip.py & destructiveclip.inx into Inkscape\share\extensions where-ever Inkscape is installed (eg Program Files). 19 | Start Inkscape. "Destructive Clip" should now be available under the Extensions menu, in the Modify Path submenu. 20 | 21 | # Example 22 | clipped.png shows the rectangular blue path clipping the tiled "dovetail" red path. The red path has been *modified* 23 | -------------------------------------------------------------------------------- /clipped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funnypolynomial/DestructiveClip/984c4951fde4f304f18a7b9cb96963ec37790ab4/clipped.png -------------------------------------------------------------------------------- /destructiveclip.inx: -------------------------------------------------------------------------------- 1 | 2 | 3 | <_name>Destructive Clip 4 | com.funnypolynomial.inkscape.extension.destructiveclip 5 | destructiveclip.py 6 | 7 | path 8 | 9 | 10 | 11 | "Destructively" clip selected paths using the topmost as clipping path 12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /destructiveclip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | ---DESTRUCTIVE Clip--- 4 | An Inkscape Extension which works like Object|Clip|Set except that the paths clipped are actually *modified* 5 | Thus the clipping is included when exported, for example as a DXF file. 6 | Select two or more *paths* then choose Extensions|Modify path|Destructive clip. The topmost path will be used to clip the others. 7 | Notes:- 8 | * Curves in paths are not supported (use Flatten Beziers). 9 | * Non-path objects in the selection will be ignored. Use Object|Ungroup. 10 | * Paths entirely outside the clipping path will remain untouched (rather than modifying them to an empty path) 11 | * Complex paths may take a while (there seems to be no way too show progress) 12 | * Yes, using MBR's to do gross clipping might make it faster 13 | * No, Python is not my first language (C/C++ is) 14 | 15 | Mark Wilson Feb 2016 16 | 17 | ---- 18 | 19 | Edits by Windell H. Oskay, www.evilmadscientit.com, August 2020 20 | Update calls to Inkscape 1.0 extension API to avoid deprecation warnings 21 | Minimal standardization of python whitespace 22 | Handle some errors more gracefully 23 | 24 | ''' 25 | 26 | import inkex 27 | import sys 28 | 29 | 30 | class DestructiveClip(inkex.Effect): 31 | 32 | def __init__(self): 33 | self.tolerance = 0.0001 # arbitrary fudge factor 34 | inkex.Effect.__init__(self) 35 | self.error_messages = [] 36 | 37 | self.curve_error = 'Unable to parse path.\nConsider removing curves ' 38 | self.curve_error += 'with Extensions > Modify Path > Flatten Beziers...' 39 | 40 | def approxEqual(self, a, b): 41 | # compare with tiny tolerance 42 | return abs(a-b) <= self.tolerance 43 | 44 | def midPoint(self, line): 45 | # midPoint of line 46 | return [(line[0][0] + line[1][0])/2, (line[0][1] + line[1][1])/2] 47 | 48 | def maxX(self, lineSegments): 49 | # return max X coord of lineSegments 50 | maxx = 0.0 51 | for line in lineSegments: 52 | maxx = max(maxx, line[0][0]) 53 | maxx = max(maxx, line[1][0]) 54 | return maxx 55 | 56 | def simplepathToLineSegments(self, path): 57 | # takes a simplepath and converts to line *segments*, for simplicity. 58 | # Thus [MoveTo P0, LineTo P1, LineTo P2] becomes [[P0-P1],[P1,P2]] 59 | # only handles, Move, Line and Close. 60 | # The simplepath library has already simplified things, normalized relative commands, etc 61 | lineSegments = first = prev = this = [] 62 | errors = set([]) # Similar errors will be stored only once 63 | for cmd in path: 64 | this = cmd[1] 65 | if cmd[0] == 'M': # moveto 66 | if first == []: 67 | first = this 68 | elif cmd[0] == 'L': # lineto 69 | lineSegments.append([prev, this]) 70 | elif cmd[0] == 'Z': # close 71 | lineSegments.append([prev, first]) 72 | first = [] 73 | elif cmd[0] == 'C': 74 | # https://developer.mozilla.org/en/docs/Web/SVG/Tutorial/Paths 75 | lineSegments.append([prev, [this[4], this[5]]]) 76 | errors.add("Curve node detected (svg type C), this node will be handled as a regular node") 77 | else: 78 | errors.add("Invalid node type detected: {}. This script only handle type M, L, Z".format(cmd[0])) 79 | prev = this 80 | return (lineSegments, errors) 81 | 82 | def linesgmentsToSimplePath(self, lineSegments): 83 | # reverses simplepathToLines - converts line segments to Move/Line-to's 84 | path = [] 85 | end = None 86 | for line in lineSegments: 87 | start = line[0] 88 | if end is None: 89 | path.append(['M', start]) # start with a move 90 | elif not (self.approxEqual(end[0], start[0]) and self.approxEqual(end[1], start[1])): 91 | path.append(['M', start]) # only move if previous end not within tolerance of this start 92 | end = line[1] 93 | path.append(['L', end]) 94 | return path 95 | 96 | def lineIntersection(self, L1From, L1To, L2From, L2To): 97 | # returns as [x, y] the intersection of the line L1From-L1To and L2From-L2To, or None 98 | # http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect 99 | 100 | try: 101 | dL1 = [L1To[0] - L1From[0], L1To[1] - L1From[1]] 102 | dL2 = [L2To[0] - L2From[0], L2To[1] - L2From[1]] 103 | except IndexError: 104 | inkex.errormsg(self.curve_error) 105 | sys.exit() 106 | 107 | denominator = -dL2[0]*dL1[1] + dL1[0]*dL2[1] 108 | if not self.approxEqual(denominator, 0.0): 109 | s = (-dL1[1]*(L1From[0] - L2From[0]) + dL1[0]*(L1From[1] - L2From[1]))/denominator 110 | t = (+dL2[0]*(L1From[1] - L2From[1]) - dL2[1]*(L1From[0] - L2From[0]))/denominator 111 | if s >= 0.0 and s <= 1.0 and t >= 0.0 and t <= 1.0: 112 | return [L1From[0] + (t * dL1[0]), L1From[1] + (t * dL1[1])] 113 | else: 114 | return None 115 | 116 | def insideRegion(self, point, lineSegments, lineSegmentsMaxX): 117 | # returns true if point is inside the region defined by lineSegments. lineSegmentsMaxX is the maximum X extent 118 | ray = [point, [lineSegmentsMaxX*2.0, point[1]]] # hz line to right of point, extending well outside MBR 119 | crossings = 0 120 | for line in lineSegments: 121 | if not self.lineIntersection(line[0], line[1], ray[0], ray[1]) is None: 122 | crossings += 1 123 | return (crossings % 2) == 1 # odd number of crossings means inside 124 | 125 | def cullSegmentedLine(self, segmentedLine, lineSegments, lineSegmentsMaxX): 126 | # returns just the segments in segmentedLine which are inside lineSegments 127 | culled = [] 128 | for segment in segmentedLine: 129 | if self.insideRegion(self.midPoint(segment), lineSegments, lineSegmentsMaxX): 130 | culled.append(segment) 131 | return culled 132 | 133 | def clipLine(self, line, lineSegments): 134 | # returns line split where-ever lines in lineSegments cross it 135 | linesWrite = [line] 136 | for segment in lineSegments: 137 | linesRead = linesWrite 138 | linesWrite = [] 139 | for line in linesRead: 140 | intersect = self.lineIntersection(line[0], line[1], segment[0], segment[1]) 141 | if intersect is None: 142 | linesWrite.append(line) 143 | else: # split 144 | linesWrite.append([line[0], intersect]) 145 | linesWrite.append([intersect, line[1]]) 146 | return linesWrite 147 | 148 | def clipLineSegments(self, lineSegmentsToClip, clippingLineSegments): 149 | # return the lines in lineSegmentsToClip clipped by the lines in clippingLineSegments 150 | clippedLines = [] 151 | for lineToClip in lineSegmentsToClip: 152 | clippedLines.extend(self.cullSegmentedLine(self.clipLine(lineToClip, clippingLineSegments), clippingLineSegments, self.maxX(clippingLineSegments))) 153 | return clippedLines 154 | 155 | def effect(self): 156 | clippingLineSegments = None 157 | pathTag = inkex.addNS('path', 'svg') 158 | groupTag = inkex.addNS('g', 'svg') 159 | self.error_messages = [] 160 | for id in self.options.ids: # the selection, top-down 161 | node = self.svg.selected[id] 162 | if node.tag == pathTag: 163 | if clippingLineSegments is None: # first path is the clipper 164 | (clippingLineSegments, errors) = self.simplepathToLineSegments(node.path.to_arrays()) 165 | self.error_messages.extend(['{}: {}'.format(id, err) for err in errors]) 166 | else: 167 | # do all the work! 168 | segmentsToClip, errors = self.simplepathToLineSegments(node.path.to_arrays()) 169 | self.error_messages.extend(['{}: {}'.format(id, err) for err in errors]) 170 | clippedSegments = self.clipLineSegments(segmentsToClip, clippingLineSegments) 171 | if len(clippedSegments) != 0: 172 | path = str(inkex.Path(self.linesgmentsToSimplePath(clippedSegments))) 173 | node.set('d', path) 174 | else: 175 | # don't put back an empty path(?) could perhaps put move, move? 176 | inkex.errormsg('Object {} clipped to nothing, will not be updated.'.format(node.get('id'))) 177 | elif node.tag == groupTag: # we don't look inside groups for paths 178 | inkex.errormsg('Group object {} will be ignored. Please ungroup before running the script.'.format(id)) 179 | else: # something else 180 | inkex.errormsg('Object {} is not of type path ({}), and will be ignored. Current type "{}".'.format(id, pathTag, node.tag)) 181 | 182 | for error in self.error_messages: 183 | inkex.errormsg(error) 184 | 185 | if __name__ == '__main__': 186 | DestructiveClip().run() 187 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | # DestructiveClip 2 | An Inkscape Extension which works like Object|Clip|Set except that the paths clipped are actually **modified**. 3 | 4 | Thus the clipping is included when exported, for example as a DXF file. 5 | Select two or more **paths** then choose Extensions|Modify path|Destructive clip. The topmost path will be used to clip the others. 6 | 7 | Notes:- 8 | * Curves in paths are not supported (use Flatten Beziers). 9 | * Non-path objects in the selection will be ignored. Use Object|Ungroup. 10 | * Paths entirely outside the clipping path will remain untouched (rather than modifying them to an empty path) 11 | * Complex paths may take a while (there seems to be no way too show progress) 12 | * Yes, using MBR's to do gross clipping might make it faster 13 | * No, Python is not my first language (C/C++ is) 14 | 15 | Mark Wilson Feb 2016 16 | 17 | # To install 18 | Copy destructiveclip.py & destructiveclip.inx into Inkscape\share\extensions where-ever Inkscape is installed (eg Program Files). 19 | Start Inkscape. "Destructive Clip" should now be available under the Extensions menu, in the Modify Path submenu. 20 | 21 | # Example 22 | clipped.png shows the rectangular blue path clipping the tiled "dovetail" red path. The red path has been *modified* 23 | --------------------------------------------------------------------------------