| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993 |
- # ##########################################################
- # FlatCAM: 2D Post-processing for Manufacturing #
- # http://flatcam.org #
- # Author: Juan Pablo Caram (c) #
- # Date: 2/5/2014 #
- # MIT Licence #
- # ##########################################################
- # ##########################################################
- # File Modified (major mod): Marius Adrian Stanciu #
- # Date: 11/4/2019 #
- # ##########################################################
- # ##########################################################
- # File Modified : Marcos Dumay de Medeiros #
- # Modifications under GPLv3 #
- # ##########################################################
- from PyQt5 import QtCore
- from shapely.geometry import Polygon, Point, LineString
- from shapely.ops import unary_union
- from appGUI.VisPyVisuals import ShapeCollection
- from appTool import AppTool
- from copy import deepcopy
- import collections
- import numpy as np
- # from voronoi import Voronoi
- # from voronoi import Polygon as voronoi_polygon
- import gettext
- import appTranslation as fcTranslate
- import builtins
- fcTranslate.apply_language('strings')
- if '_' not in builtins.__dict__:
- _ = gettext.gettext
- class GracefulException(Exception):
- """
- Graceful Exception raised when the user is requesting to cancel the current threaded task
- """
- def __init__(self):
- super().__init__()
- def __str__(self):
- return '\n\n%s' % _("The user requested a graceful exit of the current task.")
- class LoudDict(dict):
- """
- A Dictionary with a callback for item changes.
- """
- def __init__(self, *args, **kwargs):
- dict.__init__(self, *args, **kwargs)
- self.callback = lambda x: None
- def __setitem__(self, key, value):
- """
- Overridden __setitem__ method. Will emit 'changed(QString)' if the item was changed, with key as parameter.
- """
- if key in self and self.__getitem__(key) == value:
- return
- dict.__setitem__(self, key, value)
- self.callback(key)
- def update(self, *args, **kwargs):
- if len(args) > 1:
- raise TypeError("update expected at most 1 arguments, got %d" % len(args))
- other = dict(*args, **kwargs)
- for key in other:
- self[key] = other[key]
- def set_change_callback(self, callback):
- """
- Assigns a function as callback on item change. The callback
- will receive the key of the object that was changed.
- :param callback: Function to call on item change.
- :type callback: func
- :return: None
- """
- self.callback = callback
- # Fix for Python3.10
- MutableSequence = None
- try:
- MutableSequence = collections.MutableSequence
- except AttributeError:
- MutableSequence = collections.abc.MutableSequence
- class LoudUniqueList(list, MutableSequence):
- """
- A List with a callback for item changes, callback which returns the index where the items are added/modified.
- A List that will allow adding only items that are not in the list.
- """
- def __init__(self, arg=None):
- super().__init__()
- self.callback = lambda x: None
- if arg is not None:
- if isinstance(arg, list):
- self.extend(arg)
- else:
- self.extend([arg])
- def insert(self, i, v):
- if v in self:
- raise ValueError("One of the added items is already in the list.")
- self.callback(i)
- return super().insert(i, v)
- def append(self, v):
- if v in self:
- raise ValueError("One of the added items is already in the list.")
- le = len(self)
- self.callback(le)
- return super().append(v)
- def extend(self, t):
- for v in t:
- if v in self:
- raise ValueError("One of the added items is already in the list.")
- le = len(self)
- self.callback(le)
- return super().extend(t)
- def __add__(self, t): # This is for something like `LoudUniqueList([1, 2, 3]) + list([4, 5, 6])`...
- for v in t:
- if v in self:
- raise ValueError("One of the added items is already in the list.")
- le = len(self)
- self.callback(le)
- return super().__add__(t)
- def __iadd__(self, t): # This is for something like `l = LoudUniqueList(); l += [1, 2, 3]`
- for v in t:
- if v in self:
- raise ValueError("One of the added items is already in the list.")
- le = len(self)
- self.callback(le)
- return super().__iadd__(t)
- def __setitem__(self, i, v):
- try:
- for v1 in v:
- if v1 in self:
- raise ValueError("One of the modified items is already in the list.")
- except TypeError:
- if v in self:
- raise ValueError("One of the modified items is already in the list.")
- if v is not None:
- self.callback(i)
- return super().__setitem__(i, v)
- def set_callback(self, callback):
- """
- Assigns a function as callback on item change. The callback
- will receive the index of the object that was changed.
- :param callback: Function to call on item change.
- :type callback: func
- :return: None
- """
- self.callback = callback
- class FCSignal:
- """
- Taken from here: https://blog.abstractfactory.io/dynamic-signals-in-pyqt/
- """
- def __init__(self):
- self.__subscribers = []
- def emit(self, *args, **kwargs):
- for subs in self.__subscribers:
- subs(*args, **kwargs)
- def connect(self, func):
- self.__subscribers.append(func)
- def disconnect(self, func):
- try:
- self.__subscribers.remove(func)
- except ValueError:
- print('Warning: function %s not removed '
- 'from signal %s' % (func, self))
- def color_variant(hex_color, bright_factor=1):
- """
- Takes a color in HEX format #FF00FF and produces a lighter or darker variant
- :param hex_color: color to change
- :type hex_color: str
- :param bright_factor: factor to change the color brightness [0 ... 1]
- :type bright_factor: float
- :return: Modified color
- :rtype: str
- """
- if len(hex_color) != 7:
- print("Color is %s, but needs to be in #FF00FF format. Returning original color." % hex_color)
- return hex_color
- if bright_factor > 1.0:
- bright_factor = 1.0
- if bright_factor < 0.0:
- bright_factor = 0.0
- rgb_hex = [hex_color[x:x + 2] for x in [1, 3, 5]]
- new_rgb = []
- for hex_value in rgb_hex:
- # adjust each color channel and turn it into a INT suitable as argument for hex()
- mod_color = round(int(hex_value, 16) * bright_factor)
- # make sure that each color channel has two digits without the 0x prefix
- mod_color_hex = str(hex(mod_color)[2:]).zfill(2)
- new_rgb.append(mod_color_hex)
- return "#" + "".join([i for i in new_rgb])
- class ExclusionAreas(QtCore.QObject):
- """
- Functionality for adding Exclusion Areas for the Excellon and Geometry FlatCAM Objects
- """
- e_shape_modified = QtCore.pyqtSignal()
- def __init__(self, app):
- super().__init__()
- self.app = app
- self.app.log.debug("+ Adding Exclusion Areas")
- # Storage for shapes, storage that can be used by FlatCAm tools for utility geometry
- # VisPy visuals
- if self.app.is_legacy is False:
- try:
- self.exclusion_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
- except AttributeError:
- self.exclusion_shapes = None
- else:
- from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
- self.exclusion_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name="exclusion")
- # Event signals disconnect id holders
- self.mr = None
- self.mm = None
- self.kp = None
- # variables to be used in area exclusion
- self.cursor_pos = (0, 0)
- self.first_click = False
- self.points = []
- self.poly_drawn = False
- '''
- Here we store the exclusion shapes and some other information's
- Each list element is a dictionary with the format:
-
- {
- "obj_type": string ("excellon" or "geometry") <- self.obj_type
- "shape": Shapely polygon
- "strategy": string ("over" or "around") <- self.strategy_button
- "overz": float <- self.over_z_button
- }
- '''
- self.exclusion_areas_storage = []
- self.mouse_is_dragging = False
- self.solid_geometry = []
- self.obj_type = None
- self.shape_type_button = None
- self.over_z_button = None
- self.strategy_button = None
- self.cnc_button = None
- def on_add_area_click(self, shape_button, overz_button, strategy_radio, cnc_button, solid_geo, obj_type):
- """
- :param shape_button: a FCButton that has the value for the shape
- :param overz_button: a FCDoubleSpinner that holds the Over Z value
- :param strategy_radio: a RadioSet button with the strategy_button value
- :param cnc_button: a FCButton in Object UI that when clicked the CNCJob is created
- We have a reference here so we can change the color signifying that exclusion areas are
- available.
- :param solid_geo: reference to the object solid geometry for which we add exclusion areas
- :param obj_type: Type of FlatCAM object that called this method. String: "excellon" or "geometry"
- :type obj_type: str
- :return: None
- """
- self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
- self.app.call_source = 'geometry'
- self.shape_type_button = shape_button
- self.over_z_button = overz_button
- self.strategy_button = strategy_radio
- self.cnc_button = cnc_button
- self.solid_geometry = solid_geo
- self.obj_type = obj_type
- if self.app.is_legacy is False:
- self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
- self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
- self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
- else:
- self.app.plotcanvas.graph_event_disconnect(self.app.mp)
- self.app.plotcanvas.graph_event_disconnect(self.app.mm)
- self.app.plotcanvas.graph_event_disconnect(self.app.mr)
- self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
- self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
- # self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
- # To be called after clicking on the plot.
- def on_mouse_release(self, event):
- """
- Called on mouse click release.
- :param event: Mouse event
- :type event:
- :return: None
- :rtype:
- """
- if self.app.is_legacy is False:
- event_pos = event.pos
- # event_is_dragging = event.is_dragging
- right_button = 2
- else:
- event_pos = (event.xdata, event.ydata)
- # event_is_dragging = self.app.plotcanvas.is_dragging
- right_button = 3
- event_pos = self.app.plotcanvas.translate_coords(event_pos)
- if self.app.grid_status():
- curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
- else:
- curr_pos = (event_pos[0], event_pos[1])
- x1, y1 = curr_pos[0], curr_pos[1]
- # shape_type_button = self.ui.area_shape_radio.get_value()
- # do clear area only for left mouse clicks
- if event.button == 1:
- if self.shape_type_button.get_value() == "square":
- if self.first_click is False:
- self.first_click = True
- self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the area."))
- self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
- if self.app.grid_status():
- self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
- else:
- self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
- self.app.delete_selection_shape()
- x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
- pt1 = (x0, y0)
- pt2 = (x1, y0)
- pt3 = (x1, y1)
- pt4 = (x0, y1)
- new_rectangle = Polygon([pt1, pt2, pt3, pt4])
- # {
- # "obj_type": string("excellon" or "geometry") < - self.obj_type
- # "shape": Shapely polygon
- # "strategy_button": string("over" or "around") < - self.strategy_button
- # "overz": float < - self.over_z_button
- # }
- new_el = {
- "obj_type": self.obj_type,
- "shape": new_rectangle,
- "strategy": self.strategy_button.get_value(),
- "overz": self.over_z_button.get_value()
- }
- self.exclusion_areas_storage.append(new_el)
- if self.obj_type == 'excellon':
- color = "#FF7400"
- face_color = "#FF7400BF"
- else:
- color = "#098a8f"
- face_color = "#FF7400BF"
- # add a temporary shape on canvas
- AppTool.draw_tool_selection_shape(
- self, old_coords=(x0, y0), coords=(x1, y1),
- color=color,
- face_color=face_color,
- shapes_storage=self.exclusion_shapes)
- self.first_click = False
- return
- else:
- self.points.append((x1, y1))
- if len(self.points) > 1:
- self.poly_drawn = True
- self.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
- return ""
- elif event.button == right_button and self.mouse_is_dragging is False:
- shape_type = self.shape_type_button.get_value()
- if shape_type == "square":
- self.first_click = False
- else:
- # if we finish to add a polygon
- if self.poly_drawn is True:
- try:
- # try to add the point where we last clicked if it is not already in the self.points
- last_pt = (x1, y1)
- if last_pt != self.points[-1]:
- self.points.append(last_pt)
- except IndexError:
- pass
- # we need to add a Polygon and a Polygon can be made only from at least 3 points
- if len(self.points) > 2:
- AppTool.delete_moving_selection_shape(self)
- pol = Polygon(self.points)
- # do not add invalid polygons even if they are drawn by utility geometry
- if pol.is_valid:
- """
- {
- "obj_type": string("excellon" or "geometry") < - self.obj_type
- "shape": Shapely polygon
- "strategy": string("over" or "around") < - self.strategy_button
- "overz": float < - self.over_z_button
- }
- """
- new_el = {
- "obj_type": self.obj_type,
- "shape": pol,
- "strategy": self.strategy_button.get_value(),
- "overz": self.over_z_button.get_value()
- }
- self.exclusion_areas_storage.append(new_el)
- if self.obj_type == 'excellon':
- color = "#FF7400"
- face_color = "#FF7400BF"
- else:
- color = "#098a8f"
- face_color = "#FF7400BF"
- AppTool.draw_selection_shape_polygon(
- self, points=self.points,
- color=color,
- face_color=face_color,
- shapes_storage=self.exclusion_shapes)
- self.app.inform.emit(
- _("Zone added. Click to start adding next zone or right click to finish."))
- self.points = []
- self.poly_drawn = False
- return
- # AppTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
- if self.app.is_legacy is False:
- self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
- self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
- # self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
- else:
- self.app.plotcanvas.graph_event_disconnect(self.mr)
- self.app.plotcanvas.graph_event_disconnect(self.mm)
- # self.app.plotcanvas.graph_event_disconnect(self.kp)
- self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
- self.app.on_mouse_click_over_plot)
- self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
- self.app.on_mouse_move_over_plot)
- self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
- self.app.on_mouse_click_release_over_plot)
- self.app.call_source = 'app'
- if len(self.exclusion_areas_storage) == 0:
- return
- # since the exclusion areas should apply to all objects in the app collection, this check is limited to
- # only the current object therefore it will not guarantee success
- self.app.inform.emit("%s" % _("Exclusion areas added. Checking overlap with the object geometry ..."))
- for el in self.exclusion_areas_storage:
- if el["shape"].intersects(unary_union(self.solid_geometry)):
- self.on_clear_area_click()
- self.app.inform.emit(
- "[ERROR_NOTCL] %s" % _("Failed. Exclusion areas intersects the object geometry ..."))
- return
- self.app.inform.emit("[success] %s" % _("Exclusion areas added."))
- self.cnc_button.setStyleSheet("""
- QPushButton
- {
- font-weight: bold;
- color: orange;
- }
- """)
- self.cnc_button.setToolTip(
- '%s %s' % (_("Generate the CNC Job object."), _("With Exclusion areas."))
- )
- self.e_shape_modified.emit()
- def area_disconnect(self):
- """
- Will do the cleanup. Will disconnect the mouse events for the custom handlers in this class and initialize
- certain class attributes.
- :return: None
- :rtype:
- """
- if self.app.is_legacy is False:
- self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
- self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
- else:
- self.app.plotcanvas.graph_event_disconnect(self.mr)
- self.app.plotcanvas.graph_event_disconnect(self.mm)
- self.app.plotcanvas.graph_event_disconnect(self.kp)
- self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
- self.app.on_mouse_click_over_plot)
- self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
- self.app.on_mouse_move_over_plot)
- self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
- self.app.on_mouse_click_release_over_plot)
- self.points = []
- self.poly_drawn = False
- self.exclusion_areas_storage = []
- AppTool.delete_moving_selection_shape(self)
- # AppTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
- self.app.call_source = "app"
- self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
- def on_mouse_move(self, event):
- """
- Called on mouse move
- :param event: mouse event
- :type event:
- :return: None
- :rtype:
- """
- shape_type = self.shape_type_button.get_value()
- if self.app.is_legacy is False:
- event_pos = event.pos
- event_is_dragging = event.is_dragging
- # right_button = 2
- else:
- event_pos = (event.xdata, event.ydata)
- event_is_dragging = self.app.plotcanvas.is_dragging
- # right_button = 3
- curr_pos = self.app.plotcanvas.translate_coords(event_pos)
- # detect mouse dragging motion
- if event_is_dragging is True:
- self.mouse_is_dragging = True
- else:
- self.mouse_is_dragging = False
- # update the cursor position
- if self.app.grid_status():
- # Update cursor
- curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
- self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
- symbol='++', edge_color=self.app.cursor_color_3D,
- edge_width=self.app.defaults["global_cursor_width"],
- size=self.app.defaults["global_cursor_size"])
- # update the positions on status bar
- if self.cursor_pos is None:
- self.cursor_pos = (0, 0)
- self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
- self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
- self.app.ui.position_label.setText(" <b>X</b>: %.4f "
- "<b>Y</b>: %.4f " % (curr_pos[0], curr_pos[1]))
- self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f <b>Dy</b>: "
- "%.4f " % (self.app.dx, self.app.dy))
- units = self.app.defaults["units"].lower()
- self.app.plotcanvas.text_hud.text = \
- 'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX: \t{:<.4f} [{:s}]\nY: \t{:<.4f} [{:s}]'.format(
- self.app.dx, units, self.app.dy, units, curr_pos[0], units, curr_pos[1], units)
- if self.obj_type == 'excellon':
- color = "#FF7400"
- face_color = "#FF7400BF"
- else:
- color = "#098a8f"
- face_color = "#FF7400BF"
- # draw the utility geometry
- if shape_type == "square":
- if self.first_click:
- self.app.delete_selection_shape()
- self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
- color=color,
- face_color=face_color,
- coords=(curr_pos[0], curr_pos[1]))
- else:
- AppTool.delete_moving_selection_shape(self)
- AppTool.draw_moving_selection_shape_poly(
- self, points=self.points,
- color=color,
- face_color=face_color,
- data=(curr_pos[0], curr_pos[1]))
- def on_clear_area_click(self):
- """
- Slot for clicking the button for Deleting all the Exclusion areas.
- :return: None
- :rtype:
- """
- self.clear_shapes()
- # restore the default StyleSheet
- self.cnc_button.setStyleSheet("")
- # update the StyleSheet
- self.cnc_button.setStyleSheet("""
- QPushButton
- {
- font-weight: bold;
- }
- """)
- self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
- def clear_shapes(self):
- """
- Will delete all the Exclusion areas; will delete on canvas any possible selection box for the Exclusion areas.
- :return: None
- :rtype:
- """
- if self.exclusion_areas_storage:
- self.app.inform.emit('%s' % _("All exclusion zones deleted."))
- self.exclusion_areas_storage.clear()
- AppTool.delete_moving_selection_shape(self)
- self.app.delete_selection_shape()
- AppTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
- def delete_sel_shapes(self, idxs):
- """
- :param idxs: list of indexes in self.exclusion_areas_storage list to be deleted
- :type idxs: list
- :return: None
- """
- # delete all plotted shapes
- AppTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
- # delete shapes
- for idx in sorted(idxs, reverse=True):
- del self.exclusion_areas_storage[idx]
- # re-add what's left after deletion in first step
- if self.obj_type == 'excellon':
- color = "#FF7400"
- face_color = "#FF7400BF"
- else:
- color = "#098a8f"
- face_color = "#FF7400BF"
- face_alpha = 0.3
- color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
- for geo_el in self.exclusion_areas_storage:
- if isinstance(geo_el['shape'], Polygon):
- self.exclusion_shapes.add(
- geo_el['shape'], color=color, face_color=color_t, update=True, layer=0, tolerance=None)
- if self.app.is_legacy is True:
- self.exclusion_shapes.redraw()
- # if there are still some exclusion areas in the storage
- if self.exclusion_areas_storage:
- self.app.inform.emit('[success] %s' % _("Selected exclusion zones deleted."))
- else:
- # restore the default StyleSheet
- self.cnc_button.setStyleSheet("")
- # update the StyleSheet
- self.cnc_button.setStyleSheet("""
- QPushButton
- {
- font-weight: bold;
- }
- """)
- self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
- # there are no more exclusion areas in the storage, all have been selected and deleted
- self.app.inform.emit('%s' % _("All exclusion zones deleted."))
- def travel_coordinates(self, start_point, end_point, tooldia):
- """
- WIll create a path the go around the exclusion areas on the shortest path when travelling (at a Z above the
- material).
- :param start_point: X,Y coordinates for the start point of the travel line
- :type start_point: tuple
- :param end_point: X,Y coordinates for the destination point of the travel line
- :type end_point: tuple
- :param tooldia: THe tool diameter used and which generates the travel lines
- :type tooldia float
- :return: A list of x,y tuples that describe the avoiding path
- :rtype: list
- """
- ret_list = []
- # Travel lines: rapids. Should not pass through Exclusion areas
- travel_line = LineString([start_point, end_point])
- origin_point = Point(start_point)
- buffered_storage = []
- # add a little something to the half diameter, to make sure that we really don't enter in the exclusion zones
- buffered_distance = (tooldia / 2.0) + (0.1 if self.app.defaults['units'] == 'MM' else 0.00393701)
- for area in self.exclusion_areas_storage:
- new_area = deepcopy(area)
- new_area['shape'] = area['shape'].buffer(buffered_distance, join_style=2)
- buffered_storage.append(new_area)
- # sort the Exclusion areas from the closest to the start_point to the farthest
- tmp = []
- for area in buffered_storage:
- dist = Point(start_point).distance(area['shape'])
- tmp.append((dist, area))
- tmp.sort(key=lambda k: k[0])
- sorted_area_storage = [k[1] for k in tmp]
- # process the ordered exclusion areas list
- for area in sorted_area_storage:
- outline = area['shape'].exterior
- if travel_line.intersects(outline):
- intersection_pts = travel_line.intersection(outline)
- if isinstance(intersection_pts, Point):
- # it's just a touch, continue
- continue
- entry_pt = nearest_point(origin_point, intersection_pts)
- exit_pt = farthest_point(origin_point, intersection_pts)
- if area['strategy'] == 'around':
- full_vertex_points = [Point(x) for x in list(outline.coords)]
- # the last coordinate in outline, a LinearRing, is the closing one
- # therefore a duplicate of the first one; discard it
- vertex_points = full_vertex_points[:-1]
- # dist_from_entry = [(entry_pt.distance(vt), vertex_points.index(vt)) for vt in vertex_points]
- # closest_point_entry = nsmallest(1, dist_from_entry, key=lambda x: x[0])
- # start_idx = closest_point_entry[0][1]
- #
- # dist_from_exit = [(exit_pt.distance(vt), vertex_points.index(vt)) for vt in vertex_points]
- # closest_point_exit = nsmallest(1, dist_from_exit, key=lambda x: x[0])
- # end_idx = closest_point_exit[0][1]
- # pts_line_entry = None
- # pts_line_exit = None
- # for i in range(len(full_vertex_points)):
- # try:
- # line = LineString(
- # [
- # (full_vertex_points[i].x, full_vertex_points[i].y),
- # (full_vertex_points[i + 1].x, full_vertex_points[i + 1].y)
- # ]
- # )
- # except IndexError:
- # continue
- #
- # if entry_pt.within(line) or entry_pt.equals(Point(line.coords[0])) or \
- # entry_pt.equals(Point(line.coords[1])):
- # pts_line_entry = [Point(x) for x in line.coords]
- #
- # if exit_pt.within(line) or exit_pt.equals(Point(line.coords[0])) or \
- # exit_pt.equals(Point(line.coords[1])):
- # pts_line_exit = [Point(x) for x in line.coords]
- #
- # closest_point_entry = nearest_point(entry_pt, pts_line_entry)
- # start_idx = vertex_points.index(closest_point_entry)
- #
- # closest_point_exit = nearest_point(exit_pt, pts_line_exit)
- # end_idx = vertex_points.index(closest_point_exit)
- # find all vertexes for which a line from start_point does not cross the Exclusion area polygon
- # the same for end_point
- # we don't need closest points for which the path leads to crosses of the Exclusion area
- close_start_points = []
- close_end_points = []
- for i in range(len(vertex_points)):
- try:
- start_line = LineString(
- [
- start_point,
- (vertex_points[i].x, vertex_points[i].y)
- ]
- )
- end_line = LineString(
- [
- end_point,
- (vertex_points[i].x, vertex_points[i].y)
- ]
- )
- except IndexError:
- continue
- if not start_line.crosses(area['shape']):
- close_start_points.append(vertex_points[i])
- if not end_line.crosses(area['shape']):
- close_end_points.append(vertex_points[i])
- closest_point_entry = nearest_point(entry_pt, close_start_points)
- closest_point_exit = nearest_point(exit_pt, close_end_points)
- start_idx = vertex_points.index(closest_point_entry)
- end_idx = vertex_points.index(closest_point_exit)
- # calculate possible paths: one clockwise the other counterclockwise on the exterior of the
- # exclusion area outline (Polygon.exterior)
- vp_len = len(vertex_points)
- if end_idx > start_idx:
- path_1 = vertex_points[start_idx:(end_idx + 1)]
- path_2 = [vertex_points[start_idx]]
- idx = start_idx
- for __ in range(vp_len):
- idx = idx - 1 if idx > 0 else (vp_len - 1)
- path_2.append(vertex_points[idx])
- if idx == end_idx:
- break
- else:
- path_1 = vertex_points[end_idx:(start_idx + 1)]
- path_2 = [vertex_points[end_idx]]
- idx = end_idx
- for __ in range(vp_len):
- idx = idx - 1 if idx > 0 else (vp_len - 1)
- path_2.append(vertex_points[idx])
- if idx == start_idx:
- break
- path_1.reverse()
- path_2.reverse()
- # choose the one with the lesser length
- length_path_1 = 0
- for i in range(len(path_1)):
- try:
- length_path_1 += path_1[i].distance(path_1[i + 1])
- except IndexError:
- pass
- length_path_2 = 0
- for i in range(len(path_2)):
- try:
- length_path_2 += path_2[i].distance(path_2[i + 1])
- except IndexError:
- pass
- path = path_1 if length_path_1 < length_path_2 else path_2
- # transform the list of Points into a list of Points coordinates
- path_coords = [[None, (p.x, p.y)] for p in path]
- ret_list += path_coords
- else:
- path_coords = [[float(area['overz']), (entry_pt.x, entry_pt.y)], [None, (exit_pt.x, exit_pt.y)]]
- ret_list += path_coords
- # create a new LineString to test again for possible other Exclusion zones
- last_pt_in_path = path_coords[-1][1]
- travel_line = LineString([last_pt_in_path, end_point])
- ret_list.append([None, end_point])
- return ret_list
- def farthest_point(origin, points_list):
- """
- Calculate the farthest Point in a list from another Point
- :param origin: Reference Point
- :type origin: Point
- :param points_list: List of Points or a MultiPoint
- :type points_list: list
- :return: Farthest Point
- :rtype: Point
- """
- old_dist = 0
- fartherst_pt = None
- for pt in points_list:
- dist = abs(origin.distance(pt))
- if dist >= old_dist:
- fartherst_pt = pt
- old_dist = dist
- return fartherst_pt
- # def voronoi_diagram(geom, envelope, edges=False):
- # """
- #
- # :param geom: a collection of Shapely Points from which to build the Voronoi diagram
- # :type geom: MultiPoint
- # :param envelope: a bounding box to constrain the diagram (Shapely Polygon)
- # :type envelope: Polygon
- # :param edges: If False, return regions as polygons. Else, return only
- # edges e.g. LineStrings.
- # :type edges: bool, False
- # :return:
- # :rtype:
- # """
- #
- # if not isinstance(geom, MultiPoint):
- # return False
- #
- # coords = list(envelope.exterior.coords)
- # v_poly = voronoi_polygon(coords)
- #
- # vp = Voronoi(v_poly)
- #
- # points = []
- # for pt in geom:
- # points.append((pt.x, pt.y))
- # vp.create_diagram(points=points, vis_steps=False, verbose=False, vis_result=False, vis_tree=False)
- #
- # if edges is True:
- # return vp.edges
- # else:
- # voronoi_polygons = []
- # for pt in vp.points:
- # try:
- # poly_coords = list(pt.get_coordinates())
- # new_poly_coords = []
- # for coord in poly_coords:
- # new_poly_coords.append((coord.x, coord.y))
- #
- # voronoi_polygons.append(Polygon(new_poly_coords))
- # except Exception:
- # print(traceback.format_exc())
- #
- # return voronoi_polygons
- def nearest_point(origin, points_list):
- """
- Calculate the nearest Point in a list from another Point
- :param origin: Reference Point
- :type origin: Point
- :param points_list: List of Points or a MultiPoint
- :type points_list: list
- :return: Nearest Point
- :rtype: Point
- """
- old_dist = np.inf
- nearest_pt = None
- for pt in points_list:
- dist = abs(origin.distance(pt))
- if dist <= old_dist:
- nearest_pt = pt
- old_dist = dist
- return nearest_pt
|