(the elements from GuiElements.py that require text entry, less FCTextArea for which it might be unwanted behaior) the content is selected. This makes easier the edit of values.
1534 lines
50 KiB
Python
1534 lines
50 KiB
Python
############################################################
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
# http://flatcam.org #
|
|
# Author: Juan Pablo Caram (c) #
|
|
# Date: 2/5/2014 #
|
|
# MIT Licence #
|
|
############################################################
|
|
|
|
from PyQt4 import QtGui, QtCore, Qt
|
|
import FlatCAMApp
|
|
from camlib import *
|
|
from FlatCAMTool import FlatCAMTool
|
|
from ObjectUI import LengthEntry, RadioSet
|
|
|
|
from shapely.geometry import Polygon, LineString, Point, LinearRing
|
|
from shapely.geometry import MultiPoint, MultiPolygon
|
|
from shapely.geometry import box as shply_box
|
|
from shapely.ops import cascaded_union, unary_union
|
|
import shapely.affinity as affinity
|
|
from shapely.wkt import loads as sloads
|
|
from shapely.wkt import dumps as sdumps
|
|
from shapely.geometry.base import BaseGeometry
|
|
|
|
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, sign, dot
|
|
from numpy.linalg import solve
|
|
|
|
#from mpl_toolkits.axes_grid.anchored_artists import AnchoredDrawingArea
|
|
|
|
from rtree import index as rtindex
|
|
|
|
from GUIElements import FCEntry
|
|
|
|
|
|
class BufferSelectionTool(FlatCAMTool):
|
|
"""
|
|
Simple input for buffer distance.
|
|
"""
|
|
|
|
toolName = "Buffer Selection"
|
|
|
|
def __init__(self, app, fcdraw):
|
|
FlatCAMTool.__init__(self, app)
|
|
|
|
self.fcdraw = fcdraw
|
|
|
|
## Title
|
|
title_label = QtGui.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
|
|
self.layout.addWidget(title_label)
|
|
|
|
## Form Layout
|
|
form_layout = QtGui.QFormLayout()
|
|
self.layout.addLayout(form_layout)
|
|
|
|
## Buffer distance
|
|
self.buffer_distance_entry = LengthEntry()
|
|
form_layout.addRow("Buffer distance:", self.buffer_distance_entry)
|
|
|
|
## Buttons
|
|
hlay = QtGui.QHBoxLayout()
|
|
self.layout.addLayout(hlay)
|
|
hlay.addStretch()
|
|
self.buffer_button = QtGui.QPushButton("Buffer")
|
|
hlay.addWidget(self.buffer_button)
|
|
|
|
self.layout.addStretch()
|
|
|
|
## Signals
|
|
self.buffer_button.clicked.connect(self.on_buffer)
|
|
|
|
def on_buffer(self):
|
|
buffer_distance = self.buffer_distance_entry.get_value()
|
|
self.fcdraw.buffer(buffer_distance)
|
|
|
|
|
|
class PaintOptionsTool(FlatCAMTool):
|
|
"""
|
|
Inputs to specify how to paint the selected polygons.
|
|
"""
|
|
|
|
toolName = "Paint Options"
|
|
|
|
def __init__(self, app, fcdraw):
|
|
FlatCAMTool.__init__(self, app)
|
|
|
|
self.app = app
|
|
self.fcdraw = fcdraw
|
|
|
|
## Title
|
|
title_label = QtGui.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
|
|
self.layout.addWidget(title_label)
|
|
|
|
## Form Layout
|
|
form_layout = QtGui.QFormLayout()
|
|
self.layout.addLayout(form_layout)
|
|
|
|
# Tool dia
|
|
ptdlabel = QtGui.QLabel('Tool dia:')
|
|
ptdlabel.setToolTip(
|
|
"Diameter of the tool to\n"
|
|
"be used in the operation."
|
|
)
|
|
|
|
self.painttooldia_entry = LengthEntry()
|
|
form_layout.addRow(ptdlabel, self.painttooldia_entry)
|
|
|
|
# Overlap
|
|
ovlabel = QtGui.QLabel('Overlap:')
|
|
ovlabel.setToolTip(
|
|
"How much (fraction) of the tool\n"
|
|
"width to overlap each tool pass."
|
|
)
|
|
|
|
self.paintoverlap_entry = LengthEntry()
|
|
form_layout.addRow(ovlabel, self.paintoverlap_entry)
|
|
|
|
# Margin
|
|
marginlabel = QtGui.QLabel('Margin:')
|
|
marginlabel.setToolTip(
|
|
"Distance by which to avoid\n"
|
|
"the edges of the polygon to\n"
|
|
"be painted."
|
|
)
|
|
|
|
self.paintmargin_entry = LengthEntry()
|
|
form_layout.addRow(marginlabel, self.paintmargin_entry)
|
|
|
|
# Method
|
|
methodlabel = QtGui.QLabel('Method:')
|
|
methodlabel.setToolTip(
|
|
"Algorithm to paint the polygon:<BR>"
|
|
"<B>Standard</B>: Fixed step inwards.<BR>"
|
|
"<B>Seed-based</B>: Outwards from seed."
|
|
)
|
|
|
|
self.paintmethod_combo = RadioSet([
|
|
{"label": "Standard", "value": "standard"},
|
|
{"label": "Seed-based", "value": "seed"}
|
|
])
|
|
form_layout.addRow(methodlabel, self.paintmethod_combo)
|
|
|
|
## Buttons
|
|
hlay = QtGui.QHBoxLayout()
|
|
self.layout.addLayout(hlay)
|
|
hlay.addStretch()
|
|
self.paint_button = QtGui.QPushButton("Paint")
|
|
hlay.addWidget(self.paint_button)
|
|
|
|
self.layout.addStretch()
|
|
|
|
## Signals
|
|
self.paint_button.clicked.connect(self.on_paint)
|
|
|
|
def on_paint(self):
|
|
|
|
tooldia = self.painttooldia_entry.get_value()
|
|
overlap = self.paintoverlap_entry.get_value()
|
|
margin = self.paintoverlap_entry.get_value()
|
|
method = self.paintmethod_combo.get_value()
|
|
|
|
self.fcdraw.paint(tooldia, overlap, margin, method)
|
|
|
|
|
|
class DrawToolShape(object):
|
|
"""
|
|
Encapsulates "shapes" under a common class.
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_pts(o):
|
|
"""
|
|
Returns a list of all points in the object, where
|
|
the object can be a Polygon, Not a polygon, or a list
|
|
of such. Search is done recursively.
|
|
|
|
:param: geometric object
|
|
:return: List of points
|
|
:rtype: list
|
|
"""
|
|
pts = []
|
|
|
|
## Iterable: descend into each item.
|
|
try:
|
|
for subo in o:
|
|
pts += DrawToolShape.get_pts(subo)
|
|
|
|
## Non-iterable
|
|
except TypeError:
|
|
|
|
## DrawToolShape: descend into .geo.
|
|
if isinstance(o, DrawToolShape):
|
|
pts += DrawToolShape.get_pts(o.geo)
|
|
|
|
## Descend into .exerior and .interiors
|
|
elif type(o) == Polygon:
|
|
pts += DrawToolShape.get_pts(o.exterior)
|
|
for i in o.interiors:
|
|
pts += DrawToolShape.get_pts(i)
|
|
|
|
## Has .coords: list them.
|
|
else:
|
|
pts += list(o.coords)
|
|
|
|
return pts
|
|
|
|
def __init__(self, geo=[]):
|
|
|
|
# Shapely type or list of such
|
|
self.geo = geo
|
|
self.utility = False
|
|
|
|
def get_all_points(self):
|
|
return DrawToolShape.get_pts(self)
|
|
|
|
|
|
class DrawToolUtilityShape(DrawToolShape):
|
|
"""
|
|
Utility shapes are temporary geometry in the editor
|
|
to assist in the creation of shapes. For example it
|
|
will show the outline of a rectangle from the first
|
|
point to the current mouse pointer before the second
|
|
point is clicked and the final geometry is created.
|
|
"""
|
|
|
|
def __init__(self, geo=[]):
|
|
super(DrawToolUtilityShape, self).__init__(geo=geo)
|
|
self.utility = True
|
|
|
|
|
|
class DrawTool(object):
|
|
"""
|
|
Abstract Class representing a tool in the drawing
|
|
program. Can generate geometry, including temporary
|
|
utility geometry that is updated on user clicks
|
|
and mouse motion.
|
|
"""
|
|
def __init__(self, draw_app):
|
|
self.draw_app = draw_app
|
|
self.complete = False
|
|
self.start_msg = "Click on 1st point..."
|
|
self.points = []
|
|
self.geometry = None # DrawToolShape or None
|
|
|
|
def click(self, point):
|
|
"""
|
|
:param point: [x, y] Coordinate pair.
|
|
"""
|
|
return ""
|
|
|
|
def on_key(self, key):
|
|
return None
|
|
|
|
def utility_geometry(self, data=None):
|
|
return None
|
|
|
|
|
|
class FCShapeTool(DrawTool):
|
|
"""
|
|
Abstarct class for tools that create a shape.
|
|
"""
|
|
def __init__(self, draw_app):
|
|
DrawTool.__init__(self, draw_app)
|
|
|
|
def make(self):
|
|
pass
|
|
|
|
|
|
class FCCircle(FCShapeTool):
|
|
"""
|
|
Resulting type: Polygon
|
|
"""
|
|
|
|
def __init__(self, draw_app):
|
|
DrawTool.__init__(self, draw_app)
|
|
self.start_msg = "Click on CENTER ..."
|
|
|
|
def click(self, point):
|
|
self.points.append(point)
|
|
|
|
if len(self.points) == 1:
|
|
return "Click on perimeter to complete ..."
|
|
|
|
if len(self.points) == 2:
|
|
self.make()
|
|
return "Done."
|
|
|
|
return ""
|
|
|
|
def utility_geometry(self, data=None):
|
|
if len(self.points) == 1:
|
|
p1 = self.points[0]
|
|
p2 = data
|
|
radius = sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
|
|
return DrawToolUtilityShape(Point(p1).buffer(radius))
|
|
|
|
return None
|
|
|
|
def make(self):
|
|
p1 = self.points[0]
|
|
p2 = self.points[1]
|
|
radius = distance(p1, p2)
|
|
self.geometry = DrawToolShape(Point(p1).buffer(radius))
|
|
self.complete = True
|
|
|
|
|
|
class FCArc(FCShapeTool):
|
|
def __init__(self, draw_app):
|
|
DrawTool.__init__(self, draw_app)
|
|
self.start_msg = "Click on CENTER ..."
|
|
|
|
# Direction of rotation between point 1 and 2.
|
|
# 'cw' or 'ccw'. Switch direction by hitting the
|
|
# 'o' key.
|
|
self.direction = "cw"
|
|
|
|
# Mode
|
|
# C12 = Center, p1, p2
|
|
# 12C = p1, p2, Center
|
|
# 132 = p1, p3, p2
|
|
self.mode = "c12" # Center, p1, p2
|
|
|
|
self.steps_per_circ = 55
|
|
|
|
def click(self, point):
|
|
self.points.append(point)
|
|
|
|
if len(self.points) == 1:
|
|
return "Click on 1st point ..."
|
|
|
|
if len(self.points) == 2:
|
|
return "Click on 2nd point to complete ..."
|
|
|
|
if len(self.points) == 3:
|
|
self.make()
|
|
return "Done."
|
|
|
|
return ""
|
|
|
|
def on_key(self, key):
|
|
if key == 'o':
|
|
self.direction = 'cw' if self.direction == 'ccw' else 'ccw'
|
|
return 'Direction: ' + self.direction.upper()
|
|
|
|
if key == 'p':
|
|
if self.mode == 'c12':
|
|
self.mode = '12c'
|
|
elif self.mode == '12c':
|
|
self.mode = '132'
|
|
else:
|
|
self.mode = 'c12'
|
|
return 'Mode: ' + self.mode
|
|
|
|
def utility_geometry(self, data=None):
|
|
if len(self.points) == 1: # Show the radius
|
|
center = self.points[0]
|
|
p1 = data
|
|
|
|
return DrawToolUtilityShape(LineString([center, p1]))
|
|
|
|
if len(self.points) == 2: # Show the arc
|
|
|
|
if self.mode == 'c12':
|
|
center = self.points[0]
|
|
p1 = self.points[1]
|
|
p2 = data
|
|
|
|
radius = sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
|
|
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
|
|
stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
|
|
|
|
return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
|
|
self.direction, self.steps_per_circ)),
|
|
Point(center)])
|
|
|
|
elif self.mode == '132':
|
|
p1 = array(self.points[0])
|
|
p3 = array(self.points[1])
|
|
p2 = array(data)
|
|
|
|
center, radius, t = three_point_circle(p1, p2, p3)
|
|
direction = 'cw' if sign(t) > 0 else 'ccw'
|
|
|
|
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
|
|
stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
|
|
|
|
return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
|
|
direction, self.steps_per_circ)),
|
|
Point(center), Point(p1), Point(p3)])
|
|
|
|
else: # '12c'
|
|
p1 = array(self.points[0])
|
|
p2 = array(self.points[1])
|
|
|
|
# Midpoint
|
|
a = (p1 + p2) / 2.0
|
|
|
|
# Parallel vector
|
|
c = p2 - p1
|
|
|
|
# Perpendicular vector
|
|
b = dot(c, array([[0, -1], [1, 0]], dtype=float32))
|
|
b /= norm(b)
|
|
|
|
# Distance
|
|
t = distance(data, a)
|
|
|
|
# Which side? Cross product with c.
|
|
# cross(M-A, B-A), where line is AB and M is test point.
|
|
side = (data[0] - p1[0]) * c[1] - (data[1] - p1[1]) * c[0]
|
|
t *= sign(side)
|
|
|
|
# Center = a + bt
|
|
center = a + b * t
|
|
|
|
radius = norm(center - p1)
|
|
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
|
|
stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
|
|
|
|
return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
|
|
self.direction, self.steps_per_circ)),
|
|
Point(center)])
|
|
|
|
return None
|
|
|
|
def make(self):
|
|
|
|
if self.mode == 'c12':
|
|
center = self.points[0]
|
|
p1 = self.points[1]
|
|
p2 = self.points[2]
|
|
|
|
radius = distance(center, p1)
|
|
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
|
|
stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
|
|
self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
|
|
self.direction, self.steps_per_circ)))
|
|
|
|
elif self.mode == '132':
|
|
p1 = array(self.points[0])
|
|
p3 = array(self.points[1])
|
|
p2 = array(self.points[2])
|
|
|
|
center, radius, t = three_point_circle(p1, p2, p3)
|
|
direction = 'cw' if sign(t) > 0 else 'ccw'
|
|
|
|
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
|
|
stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
|
|
|
|
self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
|
|
direction, self.steps_per_circ)))
|
|
|
|
else: # self.mode == '12c'
|
|
p1 = array(self.points[0])
|
|
p2 = array(self.points[1])
|
|
pc = array(self.points[2])
|
|
|
|
# Midpoint
|
|
a = (p1 + p2) / 2.0
|
|
|
|
# Parallel vector
|
|
c = p2 - p1
|
|
|
|
# Perpendicular vector
|
|
b = dot(c, array([[0, -1], [1, 0]], dtype=float32))
|
|
b /= norm(b)
|
|
|
|
# Distance
|
|
t = distance(pc, a)
|
|
|
|
# Which side? Cross product with c.
|
|
# cross(M-A, B-A), where line is AB and M is test point.
|
|
side = (pc[0] - p1[0]) * c[1] - (pc[1] - p1[1]) * c[0]
|
|
t *= sign(side)
|
|
|
|
# Center = a + bt
|
|
center = a + b * t
|
|
|
|
radius = norm(center - p1)
|
|
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
|
|
stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
|
|
|
|
self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
|
|
self.direction, self.steps_per_circ)))
|
|
self.complete = True
|
|
|
|
|
|
class FCRectangle(FCShapeTool):
|
|
"""
|
|
Resulting type: Polygon
|
|
"""
|
|
|
|
def __init__(self, draw_app):
|
|
DrawTool.__init__(self, draw_app)
|
|
self.start_msg = "Click on 1st corner ..."
|
|
|
|
def click(self, point):
|
|
self.points.append(point)
|
|
|
|
if len(self.points) == 1:
|
|
return "Click on opposite corner to complete ..."
|
|
|
|
if len(self.points) == 2:
|
|
self.make()
|
|
return "Done."
|
|
|
|
return ""
|
|
|
|
def utility_geometry(self, data=None):
|
|
if len(self.points) == 1:
|
|
p1 = self.points[0]
|
|
p2 = data
|
|
return DrawToolUtilityShape(LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
|
|
|
|
return None
|
|
|
|
def make(self):
|
|
p1 = self.points[0]
|
|
p2 = self.points[1]
|
|
#self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
|
|
self.geometry = DrawToolShape(Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
|
|
self.complete = True
|
|
|
|
|
|
class FCPolygon(FCShapeTool):
|
|
"""
|
|
Resulting type: Polygon
|
|
"""
|
|
|
|
def __init__(self, draw_app):
|
|
DrawTool.__init__(self, draw_app)
|
|
self.start_msg = "Click on 1st point ..."
|
|
|
|
def click(self, point):
|
|
self.points.append(point)
|
|
|
|
if len(self.points) > 0:
|
|
return "Click on next point or hit SPACE to complete ..."
|
|
|
|
return ""
|
|
|
|
def utility_geometry(self, data=None):
|
|
if len(self.points) == 1:
|
|
temp_points = [x for x in self.points]
|
|
temp_points.append(data)
|
|
return DrawToolUtilityShape(LineString(temp_points))
|
|
|
|
if len(self.points) > 1:
|
|
temp_points = [x for x in self.points]
|
|
temp_points.append(data)
|
|
return DrawToolUtilityShape(LinearRing(temp_points))
|
|
|
|
return None
|
|
|
|
def make(self):
|
|
# self.geometry = LinearRing(self.points)
|
|
self.geometry = DrawToolShape(Polygon(self.points))
|
|
self.complete = True
|
|
|
|
def on_key(self, key):
|
|
if key == 'backspace':
|
|
if len(self.points) > 0:
|
|
self.points = self.points[0:-1]
|
|
|
|
|
|
class FCPath(FCPolygon):
|
|
"""
|
|
Resulting type: LineString
|
|
"""
|
|
|
|
def make(self):
|
|
self.geometry = DrawToolShape(LineString(self.points))
|
|
self.complete = True
|
|
|
|
def utility_geometry(self, data=None):
|
|
if len(self.points) > 0:
|
|
temp_points = [x for x in self.points]
|
|
temp_points.append(data)
|
|
return DrawToolUtilityShape(LineString(temp_points))
|
|
|
|
return None
|
|
|
|
def on_key(self, key):
|
|
if key == 'backspace':
|
|
if len(self.points) > 0:
|
|
self.points = self.points[0:-1]
|
|
|
|
|
|
class FCSelect(DrawTool):
|
|
def __init__(self, draw_app):
|
|
DrawTool.__init__(self, draw_app)
|
|
self.storage = self.draw_app.storage
|
|
#self.shape_buffer = self.draw_app.shape_buffer
|
|
self.selected = self.draw_app.selected
|
|
self.start_msg = "Click on geometry to select"
|
|
|
|
def click(self, point):
|
|
try:
|
|
_, closest_shape = self.storage.nearest(point)
|
|
except StopIteration:
|
|
return ""
|
|
|
|
if self.draw_app.key != 'control':
|
|
self.draw_app.selected = []
|
|
|
|
self.draw_app.set_selected(closest_shape)
|
|
self.draw_app.app.log.debug("Selected shape containing: " + str(closest_shape.geo))
|
|
|
|
return ""
|
|
|
|
|
|
class FCMove(FCShapeTool):
|
|
def __init__(self, draw_app):
|
|
FCShapeTool.__init__(self, draw_app)
|
|
#self.shape_buffer = self.draw_app.shape_buffer
|
|
self.origin = None
|
|
self.destination = None
|
|
self.start_msg = "Click on reference point."
|
|
|
|
def set_origin(self, origin):
|
|
self.origin = origin
|
|
|
|
def click(self, point):
|
|
if len(self.draw_app.get_selected()) == 0:
|
|
return "Nothing to move."
|
|
|
|
if self.origin is None:
|
|
self.set_origin(point)
|
|
return "Click on final location."
|
|
else:
|
|
self.destination = point
|
|
self.make()
|
|
return "Done."
|
|
|
|
def make(self):
|
|
# Create new geometry
|
|
dx = self.destination[0] - self.origin[0]
|
|
dy = self.destination[1] - self.origin[1]
|
|
self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy))
|
|
for geom in self.draw_app.get_selected()]
|
|
|
|
# Delete old
|
|
self.draw_app.delete_selected()
|
|
|
|
# # Select the new
|
|
# for g in self.geometry:
|
|
# # Note that g is not in the app's buffer yet!
|
|
# self.draw_app.set_selected(g)
|
|
|
|
self.complete = True
|
|
|
|
def utility_geometry(self, data=None):
|
|
"""
|
|
Temporary geometry on screen while using this tool.
|
|
|
|
:param data:
|
|
:return:
|
|
"""
|
|
if self.origin is None:
|
|
return None
|
|
|
|
if len(self.draw_app.get_selected()) == 0:
|
|
return None
|
|
|
|
dx = data[0] - self.origin[0]
|
|
dy = data[1] - self.origin[1]
|
|
|
|
return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy)
|
|
for geom in self.draw_app.get_selected()])
|
|
|
|
|
|
class FCCopy(FCMove):
|
|
def make(self):
|
|
# Create new geometry
|
|
dx = self.destination[0] - self.origin[0]
|
|
dy = self.destination[1] - self.origin[1]
|
|
self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy))
|
|
for geom in self.draw_app.get_selected()]
|
|
self.complete = True
|
|
|
|
|
|
########################
|
|
### Main Application ###
|
|
########################
|
|
class FlatCAMDraw(QtCore.QObject):
|
|
def __init__(self, app, disabled=False):
|
|
assert isinstance(app, FlatCAMApp.App), \
|
|
"Expected the app to be a FlatCAMApp.App, got %s" % type(app)
|
|
|
|
super(FlatCAMDraw, self).__init__()
|
|
|
|
self.app = app
|
|
self.canvas = app.plotcanvas
|
|
self.axes = self.canvas.new_axes("draw")
|
|
|
|
### Drawing Toolbar ###
|
|
self.drawing_toolbar = QtGui.QToolBar()
|
|
self.drawing_toolbar.setDisabled(disabled)
|
|
self.app.ui.addToolBar(self.drawing_toolbar)
|
|
self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:pointer32.png'), "Select 'Esc'")
|
|
self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:circle32.png'), 'Add Circle')
|
|
self.add_arc_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:arc32.png'), 'Add Arc')
|
|
self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:rectangle32.png'), 'Add Rectangle')
|
|
self.add_polygon_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:polygon32.png'), 'Add Polygon')
|
|
self.add_path_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:path32.png'), 'Add Path')
|
|
self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:union32.png'), 'Polygon Union')
|
|
self.intersection_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:intersection32.png'), 'Polygon Intersection')
|
|
self.subtract_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:subtract32.png'), 'Polygon Subtraction')
|
|
self.cutpath_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:cutpath32.png'), 'Cut Path')
|
|
self.move_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:move32.png'), "Move Objects 'm'")
|
|
self.copy_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:copy32.png'), "Copy Objects 'c'")
|
|
self.delete_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share:deleteshape32.png'), "Delete Shape '-'")
|
|
|
|
### Snap Toolbar ###
|
|
self.snap_toolbar = QtGui.QToolBar()
|
|
self.grid_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share:grid32.png'), 'Snap to grid')
|
|
self.grid_gap_x_entry = FCEntry()
|
|
self.grid_gap_x_entry.setMaximumWidth(70)
|
|
self.grid_gap_x_entry.setToolTip("Grid X distance")
|
|
self.snap_toolbar.addWidget(self.grid_gap_x_entry)
|
|
self.grid_gap_y_entry = FCEntry()
|
|
self.grid_gap_y_entry.setMaximumWidth(70)
|
|
self.grid_gap_y_entry.setToolTip("Grid Y distante")
|
|
self.snap_toolbar.addWidget(self.grid_gap_y_entry)
|
|
|
|
self.corner_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share:corner32.png'), 'Snap to corner')
|
|
self.snap_max_dist_entry = FCEntry()
|
|
self.snap_max_dist_entry.setMaximumWidth(70)
|
|
self.snap_max_dist_entry.setToolTip("Max. magnet distance")
|
|
self.snap_toolbar.addWidget(self.snap_max_dist_entry)
|
|
|
|
self.snap_toolbar.setDisabled(disabled)
|
|
self.app.ui.addToolBar(self.snap_toolbar)
|
|
|
|
### Application menu ###
|
|
self.menu = QtGui.QMenu("Drawing")
|
|
self.app.ui.menu.insertMenu(self.app.ui.menutoolaction, self.menu)
|
|
# self.select_menuitem = self.menu.addAction(QtGui.QIcon('share:pointer16.png'), "Select 'Esc'")
|
|
# self.add_circle_menuitem = self.menu.addAction(QtGui.QIcon('share:circle16.png'), 'Add Circle')
|
|
# self.add_arc_menuitem = self.menu.addAction(QtGui.QIcon('share:arc16.png'), 'Add Arc')
|
|
# self.add_rectangle_menuitem = self.menu.addAction(QtGui.QIcon('share:rectangle16.png'), 'Add Rectangle')
|
|
# self.add_polygon_menuitem = self.menu.addAction(QtGui.QIcon('share:polygon16.png'), 'Add Polygon')
|
|
# self.add_path_menuitem = self.menu.addAction(QtGui.QIcon('share:path16.png'), 'Add Path')
|
|
self.union_menuitem = self.menu.addAction(QtGui.QIcon('share:union16.png'), 'Polygon Union')
|
|
self.intersection_menuitem = self.menu.addAction(QtGui.QIcon('share:intersection16.png'), 'Polygon Intersection')
|
|
# self.subtract_menuitem = self.menu.addAction(QtGui.QIcon('share:subtract16.png'), 'Polygon Subtraction')
|
|
self.cutpath_menuitem = self.menu.addAction(QtGui.QIcon('share:cutpath16.png'), 'Cut Path')
|
|
# self.move_menuitem = self.menu.addAction(QtGui.QIcon('share:move16.png'), "Move Objects 'm'")
|
|
# self.copy_menuitem = self.menu.addAction(QtGui.QIcon('share:copy16.png'), "Copy Objects 'c'")
|
|
self.delete_menuitem = self.menu.addAction(QtGui.QIcon('share:deleteshape16.png'), "Delete Shape '-'")
|
|
self.buffer_menuitem = self.menu.addAction(QtGui.QIcon('share:buffer16.png'), "Buffer selection 'b'")
|
|
self.paint_menuitem = self.menu.addAction(QtGui.QIcon('share:paint16.png'), "Paint selection")
|
|
self.menu.addSeparator()
|
|
|
|
self.paint_menuitem.triggered.connect(self.on_paint_tool)
|
|
self.buffer_menuitem.triggered.connect(self.on_buffer_tool)
|
|
self.delete_menuitem.triggered.connect(self.on_delete_btn)
|
|
self.union_menuitem.triggered.connect(self.union)
|
|
self.intersection_menuitem.triggered.connect(self.intersection)
|
|
self.cutpath_menuitem.triggered.connect(self.cutpath)
|
|
|
|
### Event handlers ###
|
|
# Connection ids for Matplotlib
|
|
self.cid_canvas_click = None
|
|
self.cid_canvas_move = None
|
|
self.cid_canvas_key = None
|
|
self.cid_canvas_key_release = None
|
|
|
|
# Connect the canvas
|
|
#self.connect_canvas_event_handlers()
|
|
|
|
self.union_btn.triggered.connect(self.union)
|
|
self.intersection_btn.triggered.connect(self.intersection)
|
|
self.subtract_btn.triggered.connect(self.subtract)
|
|
self.cutpath_btn.triggered.connect(self.cutpath)
|
|
self.delete_btn.triggered.connect(self.on_delete_btn)
|
|
|
|
## Toolbar events and properties
|
|
self.tools = {
|
|
"select": {"button": self.select_btn,
|
|
"constructor": FCSelect},
|
|
"circle": {"button": self.add_circle_btn,
|
|
"constructor": FCCircle},
|
|
"arc": {"button": self.add_arc_btn,
|
|
"constructor": FCArc},
|
|
"rectangle": {"button": self.add_rectangle_btn,
|
|
"constructor": FCRectangle},
|
|
"polygon": {"button": self.add_polygon_btn,
|
|
"constructor": FCPolygon},
|
|
"path": {"button": self.add_path_btn,
|
|
"constructor": FCPath},
|
|
"move": {"button": self.move_btn,
|
|
"constructor": FCMove},
|
|
"copy": {"button": self.copy_btn,
|
|
"constructor": FCCopy}
|
|
}
|
|
|
|
### Data
|
|
self.active_tool = None
|
|
|
|
self.storage = FlatCAMDraw.make_storage()
|
|
self.utility = []
|
|
|
|
## List of selected shapes.
|
|
self.selected = []
|
|
|
|
self.move_timer = QtCore.QTimer()
|
|
self.move_timer.setSingleShot(True)
|
|
|
|
self.key = None # Currently pressed key
|
|
|
|
def make_callback(thetool):
|
|
def f():
|
|
self.on_tool_select(thetool)
|
|
return f
|
|
|
|
for tool in self.tools:
|
|
self.tools[tool]["button"].triggered.connect(make_callback(tool)) # Events
|
|
self.tools[tool]["button"].setCheckable(True) # Checkable
|
|
|
|
# for snap_tool in [self.grid_snap_btn, self.corner_snap_btn]:
|
|
# snap_tool.triggered.connect(lambda: self.toolbar_tool_toggle("grid_snap"))
|
|
# snap_tool.setCheckable(True)
|
|
self.grid_snap_btn.setCheckable(True)
|
|
self.grid_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("grid_snap"))
|
|
self.corner_snap_btn.setCheckable(True)
|
|
self.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap"))
|
|
|
|
self.options = {
|
|
"snap-x": 0.1,
|
|
"snap-y": 0.1,
|
|
"snap_max": 0.05,
|
|
"grid_snap": False,
|
|
"corner_snap": False,
|
|
}
|
|
|
|
self.grid_gap_x_entry.setText(str(self.options["snap-x"]))
|
|
self.grid_gap_y_entry.setText(str(self.options["snap-y"]))
|
|
self.snap_max_dist_entry.setText(str(self.options["snap_max"]))
|
|
|
|
self.rtree_index = rtindex.Index()
|
|
|
|
def entry2option(option, entry):
|
|
self.options[option] = float(entry.text())
|
|
|
|
self.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
|
|
self.grid_gap_x_entry.editingFinished.connect(lambda: entry2option("snap-x", self.grid_gap_x_entry))
|
|
self.grid_gap_y_entry.setValidator(QtGui.QDoubleValidator())
|
|
self.grid_gap_y_entry.editingFinished.connect(lambda: entry2option("snap-y", self.grid_gap_y_entry))
|
|
self.snap_max_dist_entry.setValidator(QtGui.QDoubleValidator())
|
|
self.snap_max_dist_entry.editingFinished.connect(lambda: entry2option("snap_max", self.snap_max_dist_entry))
|
|
|
|
def activate(self):
|
|
pass
|
|
|
|
def connect_canvas_event_handlers(self):
|
|
## Canvas events
|
|
self.cid_canvas_click = self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
|
|
self.cid_canvas_move = self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move)
|
|
self.cid_canvas_key = self.canvas.mpl_connect('key_press_event', self.on_canvas_key)
|
|
self.cid_canvas_key_release = self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release)
|
|
|
|
def disconnect_canvas_event_handlers(self):
|
|
self.canvas.mpl_disconnect(self.cid_canvas_click)
|
|
self.canvas.mpl_disconnect(self.cid_canvas_move)
|
|
self.canvas.mpl_disconnect(self.cid_canvas_key)
|
|
self.canvas.mpl_disconnect(self.cid_canvas_key_release)
|
|
|
|
def add_shape(self, shape):
|
|
"""
|
|
Adds a shape to the shape storage.
|
|
|
|
:param shape: Shape to be added.
|
|
:type shape: DrawToolShape
|
|
:return: None
|
|
"""
|
|
|
|
# List of DrawToolShape?
|
|
if isinstance(shape, list):
|
|
for subshape in shape:
|
|
self.add_shape(subshape)
|
|
return
|
|
|
|
assert isinstance(shape, DrawToolShape), \
|
|
"Expected a DrawToolShape, got %s" % type(shape)
|
|
|
|
assert shape.geo is not None, \
|
|
"Shape object has empty geometry (None)"
|
|
|
|
assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or \
|
|
not isinstance(shape.geo, list), \
|
|
"Shape objects has empty geometry ([])"
|
|
|
|
if isinstance(shape, DrawToolUtilityShape):
|
|
self.utility.append(shape)
|
|
else:
|
|
self.storage.insert(shape)
|
|
|
|
def deactivate(self):
|
|
self.disconnect_canvas_event_handlers()
|
|
self.clear()
|
|
self.drawing_toolbar.setDisabled(True)
|
|
self.snap_toolbar.setDisabled(True) # TODO: Combine and move into tool
|
|
|
|
def delete_utility_geometry(self):
|
|
#for_deletion = [shape for shape in self.shape_buffer if shape.utility]
|
|
#for_deletion = [shape for shape in self.storage.get_objects() if shape.utility]
|
|
for_deletion = [shape for shape in self.utility]
|
|
for shape in for_deletion:
|
|
self.delete_shape(shape)
|
|
|
|
def cutpath(self):
|
|
selected = self.get_selected()
|
|
tools = selected[1:]
|
|
toolgeo = cascaded_union([shp.geo for shp in tools])
|
|
|
|
target = selected[0]
|
|
if type(target.geo) == Polygon:
|
|
for ring in poly2rings(target.geo):
|
|
self.add_shape(DrawToolShape(ring.difference(toolgeo)))
|
|
self.delete_shape(target)
|
|
elif type(target.geo) == LineString or type(target.geo) == LinearRing:
|
|
self.add_shape(DrawToolShape(target.geo.difference(toolgeo)))
|
|
self.delete_shape(target)
|
|
else:
|
|
self.app.log.warning("Not implemented.")
|
|
|
|
self.replot()
|
|
|
|
def toolbar_tool_toggle(self, key):
|
|
self.options[key] = self.sender().isChecked()
|
|
|
|
def clear(self):
|
|
self.active_tool = None
|
|
#self.shape_buffer = []
|
|
self.selected = []
|
|
self.storage = FlatCAMDraw.make_storage()
|
|
self.replot()
|
|
|
|
def edit_fcgeometry(self, fcgeometry):
|
|
"""
|
|
Imports the geometry from the given FlatCAM Geometry object
|
|
into the editor.
|
|
|
|
:param fcgeometry: FlatCAMGeometry
|
|
:return: None
|
|
"""
|
|
assert isinstance(fcgeometry, Geometry), \
|
|
"Expected a Geometry, got %s" % type(fcgeometry)
|
|
|
|
self.deactivate()
|
|
|
|
self.connect_canvas_event_handlers()
|
|
self.select_tool("select")
|
|
|
|
# Link shapes into editor.
|
|
for shape in fcgeometry.flatten():
|
|
if shape is not None: # TODO: Make flatten never create a None
|
|
self.add_shape(DrawToolShape(shape))
|
|
|
|
self.replot()
|
|
self.drawing_toolbar.setDisabled(False)
|
|
self.snap_toolbar.setDisabled(False)
|
|
|
|
def on_buffer_tool(self):
|
|
buff_tool = BufferSelectionTool(self.app, self)
|
|
buff_tool.run()
|
|
|
|
def on_paint_tool(self):
|
|
paint_tool = PaintOptionsTool(self.app, self)
|
|
paint_tool.run()
|
|
|
|
def on_tool_select(self, tool):
|
|
"""
|
|
Behavior of the toolbar. Tool initialization.
|
|
|
|
:rtype : None
|
|
"""
|
|
self.app.log.debug("on_tool_select('%s')" % tool)
|
|
|
|
# This is to make the group behave as radio group
|
|
if tool in self.tools:
|
|
if self.tools[tool]["button"].isChecked():
|
|
self.app.log.debug("%s is checked." % tool)
|
|
for t in self.tools:
|
|
if t != tool:
|
|
self.tools[t]["button"].setChecked(False)
|
|
|
|
self.active_tool = self.tools[tool]["constructor"](self)
|
|
self.app.info(self.active_tool.start_msg)
|
|
else:
|
|
self.app.log.debug("%s is NOT checked." % tool)
|
|
for t in self.tools:
|
|
self.tools[t]["button"].setChecked(False)
|
|
self.active_tool = None
|
|
|
|
def on_canvas_click(self, event):
|
|
"""
|
|
event.x and .y have canvas coordinates
|
|
event.xdaya and .ydata have plot coordinates
|
|
|
|
:param event: Event object dispatched by Matplotlib
|
|
:return: None
|
|
"""
|
|
# Selection with left mouse button
|
|
if self.active_tool is not None and event.button is 1:
|
|
# Dispatch event to active_tool
|
|
msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
|
|
self.app.info(msg)
|
|
|
|
# If it is a shape generating tool
|
|
if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
|
|
self.on_shape_complete()
|
|
return
|
|
|
|
if isinstance(self.active_tool, FCSelect):
|
|
self.app.log.debug("Replotting after click.")
|
|
self.replot()
|
|
else:
|
|
self.app.log.debug("No active tool to respond to click!")
|
|
|
|
def on_canvas_move(self, event):
|
|
"""
|
|
event.x and .y have canvas coordinates
|
|
event.xdaya and .ydata have plot coordinates
|
|
|
|
:param event: Event object dispatched by Matplotlib
|
|
:return:
|
|
"""
|
|
self.on_canvas_move_effective(event)
|
|
return None
|
|
|
|
# self.move_timer.stop()
|
|
#
|
|
# if self.active_tool is None:
|
|
# return
|
|
#
|
|
# # Make a function to avoid late evaluation
|
|
# def make_callback():
|
|
# def f():
|
|
# self.on_canvas_move_effective(event)
|
|
# return f
|
|
# callback = make_callback()
|
|
#
|
|
# self.move_timer.timeout.connect(callback)
|
|
# self.move_timer.start(500) # Stops if aready running
|
|
|
|
def on_canvas_move_effective(self, event):
|
|
"""
|
|
Is called after timeout on timer set in on_canvas_move.
|
|
|
|
For details on animating on MPL see:
|
|
http://wiki.scipy.org/Cookbook/Matplotlib/Animations
|
|
|
|
event.x and .y have canvas coordinates
|
|
event.xdaya and .ydata have plot coordinates
|
|
|
|
:param event: Event object dispatched by Matplotlib
|
|
:return: None
|
|
"""
|
|
|
|
try:
|
|
x = float(event.xdata)
|
|
y = float(event.ydata)
|
|
except TypeError:
|
|
return
|
|
|
|
if self.active_tool is None:
|
|
return
|
|
|
|
### Snap coordinates
|
|
x, y = self.snap(x, y)
|
|
|
|
### Utility geometry (animated)
|
|
self.canvas.canvas.restore_region(self.canvas.background)
|
|
geo = self.active_tool.utility_geometry(data=(x, y))
|
|
|
|
if isinstance(geo, DrawToolShape) and geo.geo is not None:
|
|
|
|
# Remove any previous utility shape
|
|
self.delete_utility_geometry()
|
|
|
|
# Add the new utility shape
|
|
self.add_shape(geo)
|
|
|
|
# Efficient plotting for fast animation
|
|
|
|
#self.canvas.canvas.restore_region(self.canvas.background)
|
|
elements = self.plot_shape(geometry=geo.geo,
|
|
linespec="b--",
|
|
linewidth=1,
|
|
animated=True)
|
|
for el in elements:
|
|
self.axes.draw_artist(el)
|
|
#self.canvas.canvas.blit(self.axes.bbox)
|
|
|
|
# Pointer (snapped)
|
|
elements = self.axes.plot(x, y, 'bo', animated=True)
|
|
for el in elements:
|
|
self.axes.draw_artist(el)
|
|
|
|
self.canvas.canvas.blit(self.axes.bbox)
|
|
|
|
def on_canvas_key(self, event):
|
|
"""
|
|
event.key has the key.
|
|
|
|
:param event:
|
|
:return:
|
|
"""
|
|
self.key = event.key
|
|
|
|
### Finish the current action. Use with tools that do not
|
|
### complete automatically, like a polygon or path.
|
|
if event.key == ' ':
|
|
if isinstance(self.active_tool, FCShapeTool):
|
|
self.active_tool.click(self.snap(event.xdata, event.ydata))
|
|
self.active_tool.make()
|
|
if self.active_tool.complete:
|
|
self.on_shape_complete()
|
|
self.app.info("Done.")
|
|
return
|
|
|
|
### Abort the current action
|
|
if event.key == 'escape':
|
|
# TODO: ...?
|
|
#self.on_tool_select("select")
|
|
self.app.info("Cancelled.")
|
|
|
|
self.delete_utility_geometry()
|
|
|
|
self.replot()
|
|
# self.select_btn.setChecked(True)
|
|
# self.on_tool_select('select')
|
|
self.select_tool('select')
|
|
return
|
|
|
|
### Delete selected object
|
|
if event.key == '-':
|
|
self.delete_selected()
|
|
self.replot()
|
|
|
|
### Move
|
|
if event.key == 'm':
|
|
self.move_btn.setChecked(True)
|
|
self.on_tool_select('move')
|
|
self.active_tool.set_origin(self.snap(event.xdata, event.ydata))
|
|
self.app.info("Click on target point.")
|
|
|
|
### Copy
|
|
if event.key == 'c':
|
|
self.copy_btn.setChecked(True)
|
|
self.on_tool_select('copy')
|
|
self.active_tool.set_origin(self.snap(event.xdata, event.ydata))
|
|
self.app.info("Click on target point.")
|
|
|
|
### Snap
|
|
if event.key == 'g':
|
|
self.grid_snap_btn.trigger()
|
|
if event.key == 'k':
|
|
self.corner_snap_btn.trigger()
|
|
|
|
### Buffer
|
|
if event.key == 'b':
|
|
self.on_buffer_tool()
|
|
|
|
### Propagate to tool
|
|
response = None
|
|
if self.active_tool is not None:
|
|
response = self.active_tool.on_key(event.key)
|
|
if response is not None:
|
|
self.app.info(response)
|
|
|
|
def on_canvas_key_release(self, event):
|
|
self.key = None
|
|
|
|
def on_delete_btn(self):
|
|
self.delete_selected()
|
|
self.replot()
|
|
|
|
def get_selected(self):
|
|
"""
|
|
Returns list of shapes that are selected in the editor.
|
|
|
|
:return: List of shapes.
|
|
"""
|
|
#return [shape for shape in self.shape_buffer if shape["selected"]]
|
|
return self.selected
|
|
|
|
def delete_selected(self):
|
|
tempref = [s for s in self.selected]
|
|
for shape in tempref:
|
|
self.delete_shape(shape)
|
|
|
|
self.selected = []
|
|
|
|
def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
|
|
"""
|
|
Plots a geometric object or list of objects without rendering. Plotted objects
|
|
are returned as a list. This allows for efficient/animated rendering.
|
|
|
|
:param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such)
|
|
:param linespec: Matplotlib linespec string.
|
|
:param linewidth: Width of lines in # of pixels.
|
|
:param animated: If geometry is to be animated. (See MPL plot())
|
|
:return: List of plotted elements.
|
|
"""
|
|
plot_elements = []
|
|
|
|
if geometry is None:
|
|
geometry = self.active_tool.geometry
|
|
|
|
try:
|
|
for geo in geometry:
|
|
plot_elements += self.plot_shape(geometry=geo,
|
|
linespec=linespec,
|
|
linewidth=linewidth,
|
|
animated=animated)
|
|
|
|
## Non-iterable
|
|
except TypeError:
|
|
|
|
## DrawToolShape
|
|
if isinstance(geometry, DrawToolShape):
|
|
plot_elements += self.plot_shape(geometry=geometry.geo,
|
|
linespec=linespec,
|
|
linewidth=linewidth,
|
|
animated=animated)
|
|
|
|
## Polygon: Dscend into exterior and each interior.
|
|
if type(geometry) == Polygon:
|
|
plot_elements += self.plot_shape(geometry=geometry.exterior,
|
|
linespec=linespec,
|
|
linewidth=linewidth,
|
|
animated=animated)
|
|
plot_elements += self.plot_shape(geometry=geometry.interiors,
|
|
linespec=linespec,
|
|
linewidth=linewidth,
|
|
animated=animated)
|
|
|
|
if type(geometry) == LineString or type(geometry) == LinearRing:
|
|
x, y = geometry.coords.xy
|
|
element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
|
|
plot_elements.append(element)
|
|
|
|
if type(geometry) == Point:
|
|
x, y = geometry.coords.xy
|
|
element, = self.axes.plot(x, y, 'bo', linewidth=linewidth, animated=animated)
|
|
plot_elements.append(element)
|
|
|
|
return plot_elements
|
|
|
|
def plot_all(self):
|
|
"""
|
|
Plots all shapes in the editor.
|
|
Clears the axes, plots, and call self.canvas.auto_adjust_axes.
|
|
|
|
:return: None
|
|
:rtype: None
|
|
"""
|
|
self.app.log.debug("plot_all()")
|
|
self.axes.cla()
|
|
|
|
for shape in self.storage.get_objects():
|
|
if shape.geo is None: # TODO: This shouldn't have happened
|
|
continue
|
|
|
|
if shape in self.selected:
|
|
self.plot_shape(geometry=shape.geo, linespec='k-', linewidth=2)
|
|
continue
|
|
|
|
self.plot_shape(geometry=shape.geo)
|
|
|
|
for shape in self.utility:
|
|
self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1)
|
|
continue
|
|
|
|
self.canvas.auto_adjust_axes()
|
|
|
|
def on_shape_complete(self):
|
|
self.app.log.debug("on_shape_complete()")
|
|
|
|
# Add shape
|
|
self.add_shape(self.active_tool.geometry)
|
|
|
|
# Remove any utility shapes
|
|
self.delete_utility_geometry()
|
|
|
|
# Replot and reset tool.
|
|
self.replot()
|
|
self.active_tool = type(self.active_tool)(self)
|
|
|
|
def delete_shape(self, shape):
|
|
|
|
if shape in self.utility:
|
|
self.utility.remove(shape)
|
|
return
|
|
|
|
self.storage.remove(shape)
|
|
|
|
if shape in self.selected:
|
|
self.selected.remove(shape)
|
|
|
|
def replot(self):
|
|
self.axes = self.canvas.new_axes("draw")
|
|
self.plot_all()
|
|
|
|
@staticmethod
|
|
def make_storage():
|
|
|
|
## Shape storage.
|
|
storage = FlatCAMRTreeStorage()
|
|
storage.get_points = DrawToolShape.get_pts
|
|
|
|
return storage
|
|
|
|
def select_tool(self, toolname):
|
|
"""
|
|
Selects a drawing tool. Impacts the object and GUI.
|
|
|
|
:param toolname: Name of the tool.
|
|
:return: None
|
|
"""
|
|
self.tools[toolname]["button"].setChecked(True)
|
|
self.on_tool_select(toolname)
|
|
|
|
def set_selected(self, shape):
|
|
|
|
# Remove and add to the end.
|
|
if shape in self.selected:
|
|
self.selected.remove(shape)
|
|
|
|
self.selected.append(shape)
|
|
|
|
def set_unselected(self, shape):
|
|
if shape in self.selected:
|
|
self.selected.remove(shape)
|
|
|
|
def snap(self, x, y):
|
|
"""
|
|
Adjusts coordinates to snap settings.
|
|
|
|
:param x: Input coordinate X
|
|
:param y: Input coordinate Y
|
|
:return: Snapped (x, y)
|
|
"""
|
|
|
|
snap_x, snap_y = (x, y)
|
|
snap_distance = Inf
|
|
|
|
### Object (corner?) snap
|
|
### No need for the objects, just the coordinates
|
|
### in the index.
|
|
if self.options["corner_snap"]:
|
|
try:
|
|
nearest_pt, shape = self.storage.nearest((x, y))
|
|
|
|
nearest_pt_distance = distance((x, y), nearest_pt)
|
|
if nearest_pt_distance <= self.options["snap_max"]:
|
|
snap_distance = nearest_pt_distance
|
|
snap_x, snap_y = nearest_pt
|
|
except (StopIteration, AssertionError):
|
|
pass
|
|
|
|
### Grid snap
|
|
if self.options["grid_snap"]:
|
|
if self.options["snap-x"] != 0:
|
|
snap_x_ = round(x / self.options["snap-x"]) * self.options['snap-x']
|
|
else:
|
|
snap_x_ = x
|
|
|
|
if self.options["snap-y"] != 0:
|
|
snap_y_ = round(y / self.options["snap-y"]) * self.options['snap-y']
|
|
else:
|
|
snap_y_ = y
|
|
nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))
|
|
if nearest_grid_distance < snap_distance:
|
|
snap_x, snap_y = (snap_x_, snap_y_)
|
|
|
|
return snap_x, snap_y
|
|
|
|
def update_fcgeometry(self, fcgeometry):
|
|
"""
|
|
Transfers the drawing tool shape buffer to the selected geometry
|
|
object. The geometry already in the object are removed.
|
|
|
|
:param fcgeometry: FlatCAMGeometry
|
|
:return: None
|
|
"""
|
|
fcgeometry.solid_geometry = []
|
|
#for shape in self.shape_buffer:
|
|
for shape in self.storage.get_objects():
|
|
fcgeometry.solid_geometry.append(shape.geo)
|
|
|
|
def union(self):
|
|
"""
|
|
Makes union of selected polygons. Original polygons
|
|
are deleted.
|
|
|
|
:return: None.
|
|
"""
|
|
|
|
results = cascaded_union([t.geo for t in self.get_selected()])
|
|
|
|
# Delete originals.
|
|
for_deletion = [s for s in self.get_selected()]
|
|
for shape in for_deletion:
|
|
self.delete_shape(shape)
|
|
|
|
# Selected geometry is now gone!
|
|
self.selected = []
|
|
|
|
self.add_shape(DrawToolShape(results))
|
|
|
|
self.replot()
|
|
|
|
def intersection(self):
|
|
"""
|
|
Makes intersectino of selected polygons. Original polygons are deleted.
|
|
|
|
:return: None
|
|
"""
|
|
|
|
shapes = self.get_selected()
|
|
|
|
results = shapes[0].geo
|
|
|
|
for shape in shapes[1:]:
|
|
results = results.intersection(shape.geo)
|
|
|
|
# Delete originals.
|
|
for_deletion = [s for s in self.get_selected()]
|
|
for shape in for_deletion:
|
|
self.delete_shape(shape)
|
|
|
|
# Selected geometry is now gone!
|
|
self.selected = []
|
|
|
|
self.add_shape(DrawToolShape(results))
|
|
|
|
self.replot()
|
|
|
|
def subtract(self):
|
|
selected = self.get_selected()
|
|
tools = selected[1:]
|
|
toolgeo = cascaded_union([shp.geo for shp in tools])
|
|
result = selected[0].geo.difference(toolgeo)
|
|
|
|
self.delete_shape(selected[0])
|
|
self.add_shape(DrawToolShape(result))
|
|
|
|
self.replot()
|
|
|
|
def buffer(self, buf_distance):
|
|
selected = self.get_selected()
|
|
|
|
if len(selected) == 0:
|
|
self.app.inform.emit("[warning] Nothing selected for buffering.")
|
|
return
|
|
|
|
if not isinstance(buf_distance, float):
|
|
self.app.inform.emit("[warning] Invalid distance for buffering.")
|
|
return
|
|
|
|
pre_buffer = cascaded_union([t.geo for t in selected])
|
|
results = pre_buffer.buffer(buf_distance)
|
|
self.add_shape(DrawToolShape(results))
|
|
|
|
self.replot()
|
|
|
|
def paint(self, tooldia, overlap, margin, method):
|
|
selected = self.get_selected()
|
|
|
|
if len(selected) == 0:
|
|
self.app.inform.emit("[warning] Nothing selected for painting.")
|
|
return
|
|
|
|
for param in [tooldia, overlap, margin]:
|
|
if not isinstance(param, float):
|
|
param_name = [k for k, v in locals().iteritems() if v is param][0]
|
|
self.app.inform.emit("[warning] Invalid value for {}".format())
|
|
|
|
# Todo: Check for valid method.
|
|
|
|
# Todo: This is the 3rd implementation on painting polys... try to consolidate
|
|
|
|
results = []
|
|
|
|
def recurse(geo):
|
|
try:
|
|
for subg in geo:
|
|
for subsubg in recurse(subg):
|
|
yield subsubg
|
|
except TypeError:
|
|
if isinstance(geo, Polygon):
|
|
yield geo
|
|
|
|
raise StopIteration
|
|
|
|
for geo in selected:
|
|
|
|
local_results = []
|
|
for poly in recurse(geo.geo):
|
|
|
|
if method == "seed":
|
|
# Type(cp) == FlatCAMRTreeStorage | None
|
|
cp = Geometry.clear_polygon2(poly.buffer(-margin),
|
|
tooldia, overlap=overlap)
|
|
|
|
else:
|
|
# Type(cp) == FlatCAMRTreeStorage | None
|
|
cp = Geometry.clear_polygon(poly.buffer(-margin),
|
|
tooldia, overlap=overlap)
|
|
|
|
if cp is not None:
|
|
local_results += list(cp.get_objects())
|
|
|
|
results.append(cascaded_union(local_results))
|
|
|
|
# This is a dirty patch:
|
|
for r in results:
|
|
self.add_shape(DrawToolShape(r))
|
|
|
|
self.replot()
|
|
|
|
|
|
def distance(pt1, pt2):
|
|
return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
|
|
|
|
|
|
def mag(vec):
|
|
return sqrt(vec[0] ** 2 + vec[1] ** 2)
|
|
|
|
|
|
def poly2rings(poly):
|
|
return [poly.exterior] + [interior for interior in poly.interiors] |