| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284 |
- from PyQt4 import QtGui, QtCore, Qt
- import FlatCAMApp
- from camlib import *
- 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
- class DrawToolShape(object):
- @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):
- 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):
- 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
- 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) > 1:
- temp_points = [x for x in self.points]
- temp_points.append(data)
- return DrawToolUtilityShape(LineString(temp_points))
- return None
- 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):
- _, closest_shape = self.storage.nearest(point)
- 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)
- 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.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 = QtGui.QLineEdit()
- 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 = QtGui.QLineEdit()
- 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 = QtGui.QLineEdit()
- 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)
- ### Event handlers ###
- ## Canvas events
- self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
- self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move)
- self.canvas.mpl_connect('key_press_event', self.on_canvas_key)
- self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release)
- self.union_btn.triggered.connect(self.union)
- 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 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)
- assert shape.geo is not None
- assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list)
- if isinstance(shape, DrawToolUtilityShape):
- self.utility.append(shape)
- else:
- self.storage.insert(shape)
- def deactivate(self):
- 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)
- self.clear()
- # 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_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
- """
- if self.active_tool is not None:
- # 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()
- 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')
- 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))
- ### 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))
- ### Snap
- if event.key == 'g':
- self.grid_snap_btn.trigger()
- if event.key == 'k':
- self.corner_snap_btn.trigger()
- ### Propagate to tool
- 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):
- # for shape in self.get_selected():
- # self.shape_buffer.remove(shape)
- # self.app.info("Shape deleted.")
- tempref = [s for s in self.selected]
- for shape in tempref:
- #self.shape_buffer.remove(shape)
- 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:
- # _ = iter(geometry)
- # iterable_geometry = geometry
- # except TypeError:
- # iterable_geometry = [geometry]
- ## Iterable: Descend into each element.
- 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)
- # x, y = geo.exterior.coords.xy
- # element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
- # plot_elements.append(element)
- # for ints in geo.interiors:
- # x, y = ints.coords.xy
- # element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
- # plot_elements.append(element)
- # continue
- 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)
- # continue
- # if type(geo) == MultiPolygon:
- # for poly in geo:
- # x, y = poly.exterior.coords.xy
- # element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
- # plot_elements.append(element)
- # for ints in poly.interiors:
- # x, y = ints.coords.xy
- # element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
- # plot_elements.append(element)
- # continue
- if type(geometry) == Point:
- x, y = geometry.coords.xy
- element, = self.axes.plot(x, y, 'bo', linewidth=linewidth, animated=animated)
- plot_elements.append(element)
- # continue
- return plot_elements
- # self.canvas.auto_adjust_axes()
- def plot_all(self):
- self.app.log.debug("plot_all()")
- self.axes.cla()
- #for shape in self.shape_buffer:
- 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 add2index(self, id, geo):
- """
- Adds every coordinate of geo to the rtree index
- under the given id.
- :param id: Index of data in list being indexed.
- :param geo: Some Shapely.geom kind
- :return: None
- """
- if isinstance(geo, DrawToolShape):
- self.add2index(id, geo.geo)
- else:
- # List or Iterable Shapely type
- try:
- for subgeo in geo:
- self.add2index(id, subgeo)
- # Not iteable...
- except TypeError:
- try:
- for pt in geo.coords:
- self.rtree_index.add(id, pt)
- except NotImplementedError:
- # It's a polygon?
- for pt in geo.exterior.coords:
- self.rtree_index.add(id, pt)
- def remove_from_index(self, id, geo):
- """
- :param id: Index id
- :param geo: Geometry to remove from index.
- :return: None
- """
- # DrawToolShape
- if isinstance(geo, DrawToolShape):
- self.remove_from_index(id, geo.geo)
- else:
- # List or Iterable Shapely type
- try:
- for subgeo in geo:
- self.remove_from_index(id, subgeo)
- # Not iteable...
- except TypeError:
- try:
- for pt in geo.coords:
- self.rtree_index.delete(id, pt)
- except NotImplementedError:
- # It's a polygon?
- for pt in geo.exterior.coords:
- self.rtree_index.delete(id, pt)
- def on_shape_complete(self):
- self.app.log.debug("on_shape_complete()")
- # For some reason plotting just the last created figure does not
- # work. The figure is not shown. Calling replot does the trick
- # which generates a new axes object.
- #self.plot_shape()
- #self.canvas.auto_adjust_axes()
- self.add_shape(self.active_tool.geometry)
- # Remove any utility shapes
- self.delete_utility_geometry()
- self.replot()
- self.active_tool = type(self.active_tool)(self)
- def delete_shape(self, shape):
- # try:
- # # Remove from index list
- # shp_idx = self.main_index.index(shape)
- # self.main_index[shp_idx] = None
- #
- # # Remove from rtree index
- # self.remove_from_index(shp_idx, shape)
- # except ValueError:
- # pass
- #
- # if shape in self.shape_buffer:
- # self.shape_buffer.remove(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.canvas.clear()
- 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 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 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 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]
|