From 419330ee9303151d15aab622916cb96f4ed4c65e Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 1 Aug 2022 12:27:14 +0300 Subject: [PATCH] - fixed some bugs in Geometry Editor in regards of Buffer Tool - fixed some issues in the Cutout Plugin by adding more checks - fixed issues when loading files by dragging in the UI (caused by recent code refactoring) --- CHANGELOG.md | 6 + appCommon/RegisterFileKeywords.py | 5 + appEditors/AppGeoEditor.py | 6 +- appEditors/geo_plugins/GeoBufferPlugin.py | 14 +- appGUI/MainGUI.py | 16 +-- appObjects/ObjectCollection.py | 14 +- appParsers/ParseDXF.py | 12 +- appParsers/ParseDXF_Spline.py | 166 +--------------------- appPlugins/ToolCutOut.py | 27 +++- 9 files changed, 72 insertions(+), 194 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8641eba..8b8a06fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ CHANGELOG for FlatCAM Evo beta ================================================= +1.08.2022 + +- fixed some bugs in Geometry Editor in regards of Buffer Tool +- fixed some issues in the Cutout Plugin by adding more checks +- fixed issues when loading files by dragging in the UI (caused by recent code refactoring) + 26.05.2022 - upgraded the UI of Region sub-tool in the Gerber Editor diff --git a/appCommon/RegisterFileKeywords.py b/appCommon/RegisterFileKeywords.py index 1e2396cc..5f6269d6 100644 --- a/appCommon/RegisterFileKeywords.py +++ b/appCommon/RegisterFileKeywords.py @@ -319,6 +319,11 @@ class RegisterFK(QtCore.QObject): self.exc_list = extensions.exc_list self.grb_list = extensions.grb_list self.gcode_list = extensions.gcode_list + self.svg_list = extensions.svg_list + self.dxf_list = extensions.dxf_list + self.pdf_list = extensions.pdf_list + self.prj_list = extensions.prj_list + self.conf_list = extensions.conf_list autocomplete_kw_list = self.options['util_autocomplete_keywords'].replace(' ', '').split(',') self.myKeywords = self.tcl_commands_list + autocomplete_kw_list + self.tcl_keywords diff --git a/appEditors/AppGeoEditor.py b/appEditors/AppGeoEditor.py index 99e75e16..11cfa5d9 100644 --- a/appEditors/AppGeoEditor.py +++ b/appEditors/AppGeoEditor.py @@ -2847,7 +2847,7 @@ class FCBuffer(FCShapeTool): # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT) join_style = self.buff_tool.ui.buffer_corner_cb.currentIndex() + 1 - ret_val = self.draw_app.buffer(buffer_distance, join_style) + ret_val = self.buff_tool.buffer(buffer_distance, join_style) self.deactivate() if ret_val == 'fail': @@ -2873,7 +2873,7 @@ class FCBuffer(FCShapeTool): # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT) join_style = self.buff_tool.ui.buffer_corner_cb.currentIndex() + 1 - ret_val = self.draw_app.buffer_int(buffer_distance, join_style) + ret_val = self.buff_tool.buffer_int(buffer_distance, join_style) self.deactivate() if ret_val == 'fail': @@ -2899,7 +2899,7 @@ class FCBuffer(FCShapeTool): # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT) join_style = self.buff_tool.ui.buffer_corner_cb.currentIndex() + 1 - ret_val = self.draw_app.buffer_ext(buffer_distance, join_style) + ret_val = self.buff_tool.buffer_ext(buffer_distance, join_style) # self.app.ui.notebook.setTabText(2, _("Tools")) # self.draw_app.app.ui.splitter.setSizes([0, 1]) diff --git a/appEditors/geo_plugins/GeoBufferPlugin.py b/appEditors/geo_plugins/GeoBufferPlugin.py index 435c6871..72e1b847 100644 --- a/appEditors/geo_plugins/GeoBufferPlugin.py +++ b/appEditors/geo_plugins/GeoBufferPlugin.py @@ -200,7 +200,7 @@ class BufferSelectionTool(AppToolEditor): geo_editor.build_ui_sig.emit() geo_editor.app.inform.emit('[success] %s' % _("Done.")) - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + self.app.worker_task.emit({'fcn': work_task, 'params': [self.draw_app]}) def buffer_int(self, buf_distance, join_style): def work_task(geo_editor): @@ -238,6 +238,8 @@ class BufferSelectionTool(AppToolEditor): for line in t.geo: if line.is_ring: b_geo = Polygon(line) + else: + b_geo = line results.append(b_geo.buffer( -buf_distance + 1e-10, resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), @@ -246,6 +248,8 @@ class BufferSelectionTool(AppToolEditor): elif t.geo.geom_type in ['LineString', 'LinearRing']: if t.geo.is_ring: b_geo = Polygon(t.geo) + else: + b_geo = t.geo results.append(b_geo.buffer( -buf_distance + 1e-10, resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), @@ -266,7 +270,7 @@ class BufferSelectionTool(AppToolEditor): geo_editor.build_ui_sig.emit() geo_editor.app.inform.emit('[success] %s' % _("Done.")) - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + self.app.worker_task.emit({'fcn': work_task, 'params': [self.draw_app]}) def buffer_ext(self, buf_distance, join_style): def work_task(geo_editor): @@ -306,6 +310,8 @@ class BufferSelectionTool(AppToolEditor): for line in t.geo: if line.is_ring: b_geo = Polygon(line) + else: + b_geo = line results.append(b_geo.buffer( buf_distance - 1e-10, resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), @@ -314,6 +320,8 @@ class BufferSelectionTool(AppToolEditor): elif t.geo.geom_type in ['LineString', 'LinearRing']: if t.geo.is_ring: b_geo = Polygon(t.geo) + else: + b_geo = t.geo results.append(b_geo.buffer( buf_distance - 1e-10, resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), @@ -334,7 +342,7 @@ class BufferSelectionTool(AppToolEditor): geo_editor.build_ui_sig.emit() geo_editor.app.inform.emit('[success] %s' % _("Done.")) - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + self.app.worker_task.emit({'fcn': work_task, 'params': [self.draw_app]}) def hide_tool(self): self.ui.buffer_tool_frame.hide() diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py index 3d43c108..262fc021 100644 --- a/appGUI/MainGUI.py +++ b/appGUI/MainGUI.py @@ -4522,44 +4522,44 @@ class MainGUI(QtWidgets.QMainWindow): else: extension = self.filename.lower().rpartition('.')[-1] - if extension in self.app.grb_list: + if extension in self.app.regFK.grb_list: self.app.worker_task.emit({'fcn': self.app.f_handlers.open_gerber, 'params': [self.filename]}) else: event.ignore() - if extension in self.app.exc_list: + if extension in self.app.regFK.exc_list: self.app.worker_task.emit({'fcn': self.app.f_handlers.open_excellon, 'params': [self.filename]}) else: event.ignore() - if extension in self.app.gcode_list: + if extension in self.app.regFK.gcode_list: self.app.worker_task.emit({'fcn': self.app.f_handlers.open_gcode, 'params': [self.filename]}) else: event.ignore() - if extension in self.app.svg_list: + if extension in self.app.regFK.svg_list: object_type = 'geometry' self.app.worker_task.emit({'fcn': self.app.f_handlers.import_svg, 'params': [self.filename, object_type, None]}) - if extension in self.app.dxf_list: + if extension in self.app.regFK.dxf_list: object_type = 'geometry' self.app.worker_task.emit({'fcn': self.app.f_handlers.import_dxf, 'params': [self.filename, object_type, None]}) - if extension in self.app.pdf_list: + if extension in self.app.regFK.pdf_list: self.app.pdf_tool.periodic_check(1000) self.app.worker_task.emit({'fcn': self.app.pdf_tool.open_pdf, 'params': [self.filename]}) - if extension in self.app.prj_list: + if extension in self.app.regFK.prj_list: # self.app.open_project() is not Thread Safe self.app.f_handlers.open_project(self.filename) - if extension in self.app.conf_list: + if extension in self.app.regFK.conf_list: self.app.f_handlers.open_config_file(self.filename) else: event.ignore() diff --git a/appObjects/ObjectCollection.py b/appObjects/ObjectCollection.py index 51b44282..88216cb4 100644 --- a/appObjects/ObjectCollection.py +++ b/appObjects/ObjectCollection.py @@ -154,35 +154,35 @@ class EventSensitiveListView(QtWidgets.QTreeView): if self.filename == "": self.app.inform.emit(_("Cancelled.")) else: - if self.filename.lower().rpartition('.')[-1] in self.app.grb_list: + if self.filename.lower().rpartition('.')[-1] in self.app.regFK.grb_list: self.app.worker_task.emit({'fcn': self.app.f_handlers.open_gerber, 'params': [self.filename]}) else: event.ignore() - if self.filename.lower().rpartition('.')[-1] in self.app.exc_list: + if self.filename.lower().rpartition('.')[-1] in self.app.regFK.exc_list: self.app.worker_task.emit({'fcn': self.app.f_handlers.open_excellon, 'params': [self.filename]}) else: event.ignore() - if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list: + if self.filename.lower().rpartition('.')[-1] in self.app.regFK.gcode_list: self.app.worker_task.emit({'fcn': self.app.f_handlers.open_gcode, 'params': [self.filename]}) else: event.ignore() - if self.filename.lower().rpartition('.')[-1] in self.app.svg_list: + if self.filename.lower().rpartition('.')[-1] in self.app.regFK.svg_list: object_type = 'geometry' self.app.worker_task.emit({'fcn': self.app.f_handlers.import_svg, 'params': [self.filename, object_type, None]}) - if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list: + if self.filename.lower().rpartition('.')[-1] in self.app.regFK.dxf_list: object_type = 'geometry' self.app.worker_task.emit({'fcn': self.app.f_handlers.import_dxf, 'params': [self.filename, object_type, None]}) - if self.filename.lower().rpartition('.')[-1] in self.app.prj_list: + if self.filename.lower().rpartition('.')[-1] in self.app.regFK.prj_list: # self.app.open_project() is not Thread Safe self.app.f_handlers.open_project(self.filename) else: @@ -558,7 +558,7 @@ class ObjectCollection(QtCore.QAbstractItemModel): try: self.app.regFK.remove_keyword(old_name, update=False) self.app.regFK.prepend_keyword(new_name) - self.app.shell._edit.set_model_data(self.app.myKeywords) + self.app.shell._edit.set_model_data(self.app.regFK.myKeywords) except Exception as e: self.app.log.error( "setData() --> Could not remove the old object name from auto-completer model list. %s" % diff --git a/appParsers/ParseDXF.py b/appParsers/ParseDXF.py index 576e1d02..fb6e7639 100644 --- a/appParsers/ParseDXF.py +++ b/appParsers/ParseDXF.py @@ -11,7 +11,11 @@ from shapely.affinity import rotate from ezdxf.math import Vec3 as ezdxf_vector from appParsers.ParseFont import * -from appParsers.ParseDXF_Spline import * +from appParsers.ParseDXF_Spline import spline2Polyline, normalize_2 +from appParsers.ParseDXF_Spline import Vector as DxfVector + +import math + import logging log = logging.getLogger('base2') @@ -176,7 +180,7 @@ def dxfellipse2shapely(ellipse, ellipse_segments=100): ratio = ellipse.dxf.ratio points_list = [] - major_axis = Vector(list(major_axis)) + major_axis = DxfVector(list(major_axis)) major_x = major_axis[0] major_y = major_axis[1] @@ -191,7 +195,7 @@ def dxfellipse2shapely(ellipse, ellipse_segments=100): for step in range(line_seg + 1): if direction == 'CW': major_dim = normalize_2(major_axis) - minor_dim = normalize_2(Vector([ratio * k for k in major_axis])) + minor_dim = normalize_2(DxfVector([ratio * k for k in major_axis])) vx = (major_dim[0] + major_dim[1]) * math.cos(angle) vy = (minor_dim[0] - minor_dim[1]) * math.sin(angle) x = center[0] + major_x * vx - major_y * vy @@ -199,7 +203,7 @@ def dxfellipse2shapely(ellipse, ellipse_segments=100): angle += step_angle else: major_dim = normalize_2(major_axis) - minor_dim = (Vector([ratio * k for k in major_dim])) + minor_dim = (DxfVector([ratio * k for k in major_dim])) vx = (major_dim[0] + major_dim[1]) * math.cos(angle) vy = (minor_dim[0] + minor_dim[1]) * math.sin(angle) x = center[0] + major_x * vx + major_y * vy diff --git a/appParsers/ParseDXF_Spline.py b/appParsers/ParseDXF_Spline.py index 57d481ba..3d1a2b88 100644 --- a/appParsers/ParseDXF_Spline.py +++ b/appParsers/ParseDXF_Spline.py @@ -102,7 +102,7 @@ def spline2Polyline(xyz, degree, closed, segments, knots): # equal to the order at the ends. # c = order of the basis function # n = the number of defining polygon vertices -# n+2 = index of x[] for the first occurence of the maximum knot vector value +# n+2 = index of x[] for the first occurrence of the maximum knot vector value # n+order = maximum value of the knot vector -- $n + c$ # x[] = array containing the knot vector # ------------------------------------------------------------------------------ @@ -659,167 +659,3 @@ class Vector(list): """@return the transverse component (R in cylindrical coordinate system).""" return math.sqrt(self.perp2()) - - # ---------------------------------------------------------------------- - # Return a random 3D vector - # ---------------------------------------------------------------------- - # @staticmethod - # def random(): - # cosTheta = 2.0 * random.random() - 1.0 - # sinTheta = math.sqrt(1.0 - cosTheta ** 2) - # phi = 2.0 * math.pi * random.random() - # return Vector(math.cos(phi) * sinTheta, math.sin(phi) * sinTheta, cosTheta) - -# #=============================================================================== -# # Cardinal cubic spline class -# #=============================================================================== -# class CardinalSpline: -# def __init__(self, A=0.5): -# # The default matrix is the Catmull-Rom spline -# # which is equal to Cardinal matrix -# # for A = 0.5 -# # -# # Note: Vasilis -# # The A parameter should be the fraction in t where -# # the second derivative is zero -# self.setMatrix(A) -# -# #----------------------------------------------------------------------- -# # Set the matrix according to Cardinal -# #----------------------------------------------------------------------- -# def setMatrix(self, A=0.5): -# self.M = [] -# self.M.append([ -A, 2.-A, A-2., A ]) -# self.M.append([2.*A, A-3., 3.-2.*A, -A ]) -# self.M.append([ -A, 0., A, 0.]) -# self.M.append([ 0., 1., 0, 0.]) -# -# #----------------------------------------------------------------------- -# # Evaluate Cardinal spline at position t -# # @param P list or tuple with 4 points y positions -# # @param t [0..1] fraction of interval from points 1..2 -# # @param k index of starting 4 elements in P -# # @return spline evaluation -# #----------------------------------------------------------------------- -# def __call__(self, P, t, k=1): -# T = [t*t*t, t*t, t, 1.0] -# R = [0.0]*4 -# for i in range(4): -# for j in range(4): -# R[i] += T[j] * self.M[j][i] -# y = 0.0 -# for i in range(4): -# y += R[i]*P[k+i-1] -# -# return y -# -# #----------------------------------------------------------------------- -# # Return the coefficients of a 3rd degree polynomial -# # f(x) = a t^3 + b t^2 + c t + d -# # @return [a, b, c, d] -# #----------------------------------------------------------------------- -# def coefficients(self, P, k=1): -# C = [0.0]*4 -# for i in range(4): -# for j in range(4): -# C[i] += self.M[i][j] * P[k+j-1] -# return C -# -# #----------------------------------------------------------------------- -# # Evaluate the value of the spline using the coefficients -# #----------------------------------------------------------------------- -# def evaluate(self, C, t): -# return ((C[0]*t + C[1])*t + C[2])*t + C[3] -# -# #=============================================================================== -# # Cubic spline ensuring that the first and second derivative are continuous -# # adapted from Penelope Manual Appending B.1 -# # It requires all the points (xi,yi) and the assumption on how to deal -# # with the second derivative on the extremities -# # Option 1: assume zero as second derivative on both ends -# # Option 2: assume the same as the next or previous one -# #=============================================================================== -# class CubicSpline: -# def __init__(self, X, Y): -# self.X = X -# self.Y = Y -# self.n = len(X) -# -# # Option #1 -# s1 = 0.0 # zero based = s0 -# sN = 0.0 # zero based = sN-1 -# -# # Construct the tri-diagonal matrix -# A = [] -# B = [0.0] * (self.n-2) -# for i in range(self.n-2): -# A.append([0.0] * (self.n-2)) -# -# for i in range(1,self.n-1): -# hi = self.h(i) -# Hi = 2.0*(self.h(i-1) + hi) -# j = i-1 -# A[j][j] = Hi -# if i+1> s <<" -# # pprint(self.s) -# -# #----------------------------------------------------------------------- -# def h(self, i): -# return self.X[i+1] - self.X[i] -# -# #----------------------------------------------------------------------- -# def d(self, i): -# return (self.Y[i+1] - self.Y[i]) / (self.X[i+1] - self.X[i]) -# -# #----------------------------------------------------------------------- -# def coefficients(self, i): -# """return coefficients of cubic spline for interval i a*x**3+b*x**2+c*x+d""" -# hi = self.h(i) -# si = self.s[i] -# si1 = self.s[i+1] -# xi = self.X[i] -# xi1 = self.X[i+1] -# fi = self.Y[i] -# fi1 = self.Y[i+1] -# -# a = 1./(6.*hi)*(si*xi1**3 - si1*xi**3 + 6.*(fi*xi1 - fi1*xi)) + hi/6.*(si1*xi - si*xi1) -# b = 1./(2.*hi)*(si1*xi**2 - si*xi1**2 + 2*(fi1 - fi)) + hi/6.*(si - si1) -# c = 1./(2.*hi)*(si*xi1 - si1*xi) -# d = 1./(6.*hi)*(si1-si) -# -# return [d,c,b,a] -# -# #----------------------------------------------------------------------- -# def __call__(self, i, x): -# C = self.coefficients(i) -# return ((C[0]*x + C[1])*x + C[2])*x + C[3] -# -# #----------------------------------------------------------------------- -# # @return evaluation of cubic spline at x using coefficients C -# #----------------------------------------------------------------------- -# def evaluate(self, C, x): -# return ((C[0]*x + C[1])*x + C[2])*x + C[3] -# -# #----------------------------------------------------------------------- -# # Return evaluated derivative at x using coefficients C -# #----------------------------------------------------------------------- -# def derivative(self, C, x): -# a = 3.0*C[0] # derivative coefficients -# b = 2.0*C[1] # ... for sampling with rejection -# c = C[2] -# return (3.0*C[0]*x + 2.0*C[1])*x + C[2] -# diff --git a/appPlugins/ToolCutOut.py b/appPlugins/ToolCutOut.py index 02076c12..330052c8 100644 --- a/appPlugins/ToolCutOut.py +++ b/appPlugins/ToolCutOut.py @@ -756,8 +756,13 @@ class CutOut(AppTool): self.app.log.debug("Cutout.on_freeform_cutout() -> Empty geometry.") self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) return 'fail' + try: + solid_geo, rest_geo = self.any_cutout_handler(geo, abs(cut_dia), gaps, gapsize, margin) + except Exception as err: + self.app.log.error("Cutout.on_freeform_cutout() -> %s" % str(err)) + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) + return - solid_geo, rest_geo = self.any_cutout_handler(geo, abs(cut_dia), gaps, gapsize, margin) if gap_type == 1 and thin_entry != 0: # "Thin gaps" gaps_solid_geo = rest_geo else: @@ -779,9 +784,21 @@ class CutOut(AppTool): geo_buf = geom_struct.buffer(0.0000001) geo_ext = geo_buf.exterior buff_geo_ext = geo_ext.buffer(-margin) - geom_struct = unary_union(buff_geo_ext.interiors) + if isinstance(buff_geo_ext, MultiPolygon): + self.app.log.debug( + "Cutout.on_freeform_cutout() -> The source geometry cannot be used.") + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) + unary_union(buff_geo_ext) + else: + geom_struct = unary_union(buff_geo_ext.interiors) + + try: + c_geo, r_geo = self.any_cutout_handler(geom_struct, abs(cut_dia), gaps, gapsize, margin) + except Exception as err: + self.app.log.error("Cutout.on_freeform_cutout() -> %s" % str(err)) + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) + continue - c_geo, r_geo = self.any_cutout_handler(geom_struct, abs(cut_dia), gaps, gapsize, margin) solid_geo += c_geo if gap_type == 1 and thin_entry != 0: # "Thin gaps" gaps_solid_geo += r_geo @@ -2178,6 +2195,8 @@ class CutOut(AppTool): work_geo = obj.geoms if isinstance(obj, (MultiPolygon, MultiLineString)) else obj for k in work_geo: + if k.is_empty or not k.is_valid: + continue minx_, miny_, maxx_, maxy_ = bounds_rec(k) minx = min(minx, minx_) miny = min(miny, miny_) @@ -2185,7 +2204,7 @@ class CutOut(AppTool): maxy = max(maxy, maxy_) return minx, miny, maxx, maxy except TypeError: - # it's a Shapely object, return it's bounds + # it's a Shapely object, return its bounds if obj: return obj.bounds