diff --git a/CHANGELOG.md b/CHANGELOG.md index a0a11a43..2c9d283e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,15 @@ CHANGELOG for FlatCAM Evo beta ================================================= +18.04.2022 + +- in Geometry Editor, in Copy Tool added the 2D copy-as-array feature therefore finishing this editor plugin upgrade + 17.04.2022 - in Geometry Editor, in Copy Tool - work in progress (adding utility geometry for the array mode) - in Geometry Editor, in Copy Tool - linear array utility geometry is working -- in Geometry Editor, COpy Tool, finished the copy-as-array feature except the 2D array type which was not implemented yet +- in Geometry Editor, Copy Tool, finished the copy-as-array feature except the 2D array type which was not implemented yet 16.04.2022 diff --git a/appEditors/AppGeoEditor.py b/appEditors/AppGeoEditor.py index e1792871..94189b45 100644 --- a/appEditors/AppGeoEditor.py +++ b/appEditors/AppGeoEditor.py @@ -2281,82 +2281,157 @@ class FCCopy(FCShapeTool): return self.util_geo def array_util_geometry(self, pos, static=None): - axis = self.copy_tool.ui.axis_radio.get_value() # X, Y or A array_type = self.copy_tool.ui.array_type_radio.get_value() # 'linear', '2D', 'circular' - array_size = int(self.copy_tool.ui.array_size_entry.get_value()) - pitch = float(self.copy_tool.ui.pitch_entry.get_value()) - linear_angle = float(self.copy_tool.ui.linear_angle_spinner.get_value()) if array_type == 'linear': # 'Linear' - if pos[0] is None and pos[1] is None: - dx = self.draw_app.x - dy = self.draw_app.y - else: - dx = pos[0] - dy = pos[1] - - geo_list = [] - self.points = [(dx, dy)] - - for item in range(array_size): - if axis == 'X': - new_pos = ((dx + (pitch * item)), dy) - elif axis == 'Y': - new_pos = (dx, (dy + (pitch * item))) - else: # 'A' - x_adj = pitch * math.cos(math.radians(linear_angle)) - y_adj = pitch * math.sin(math.radians(linear_angle)) - new_pos = ((dx + (x_adj * item)), (dy + (y_adj * item))) - - for g in self.draw_app.get_selected(): - if static is None or static is False: - geo_list.append(translate(g.geo, xoff=new_pos[0], yoff=new_pos[1])) - else: - geo_list.append(g.geo) - - return DrawToolUtilityShape(geo_list) + return self.linear_geo(pos, static) elif array_type == '2D': - pass + return self.dd_geo(pos) elif array_type == 'circular': # 'Circular' - if pos[0] is None and pos[1] is None: - cdx = self.draw_app.x - cdy = self.draw_app.y + return self.circular_geo(pos) + + def linear_geo(self, pos, static): + axis = self.copy_tool.ui.axis_radio.get_value() # X, Y or A + pitch = float(self.copy_tool.ui.pitch_entry.get_value()) + linear_angle = float(self.copy_tool.ui.linear_angle_spinner.get_value()) + array_size = int(self.copy_tool.ui.array_size_entry.get_value()) + + if pos[0] is None and pos[1] is None: + dx = self.draw_app.x + dy = self.draw_app.y + else: + dx = pos[0] + dy = pos[1] + + geo_list = [] + self.points = [(dx, dy)] + + for item in range(array_size): + if axis == 'X': + new_pos = ((dx + (pitch * item)), dy) + elif axis == 'Y': + new_pos = (dx, (dy + (pitch * item))) + else: # 'A' + x_adj = pitch * math.cos(math.radians(linear_angle)) + y_adj = pitch * math.sin(math.radians(linear_angle)) + new_pos = ((dx + (x_adj * item)), (dy + (y_adj * item))) + + for g in self.draw_app.get_selected(): + if static is None or static is False: + geo_list.append(translate(g.geo, xoff=new_pos[0], yoff=new_pos[1])) + else: + geo_list.append(g.geo) + + return DrawToolUtilityShape(geo_list) + + def dd_geo(self, pos): + trans_geo = [] + array_2d_type = self.copy_tool.ui.placement_radio.get_value() + + rows = self.copy_tool.ui.rows.get_value() + columns = self.copy_tool.ui.columns.get_value() + + spacing_rows = self.copy_tool.ui.spacing_rows.get_value() + spacing_columns = self.copy_tool.ui.spacing_columns.get_value() + + off_x = self.copy_tool.ui.offsetx_entry.get_value() + off_y = self.copy_tool.ui.offsety_entry.get_value() + + geo_source = [s.geo for s in self.draw_app.get_selected()] + + def geo_bounds(geo: (BaseGeometry, list)): + minx = np.Inf + miny = np.Inf + maxx = -np.Inf + maxy = -np.Inf + + if type(geo) == list: + for shp in geo: + minx_, miny_, maxx_, maxy_ = geo_bounds(shp) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy else: - cdx = pos[0] + self.origin[0] - cdy = pos[1] + self.origin[1] + # it's an object, return its bounds + return geo.bounds - utility_list = [] + xmin, ymin, xmax, ymax = geo_bounds(geo_source) + currentx = pos[0] + currenty = pos[1] + + def translate_recursion(geom): + if type(geom) == list: + geoms = [] + for local_geom in geom: + res_geo = translate_recursion(local_geom) + try: + geoms += res_geo + except TypeError: + geoms.append(res_geo) + return geoms + else: + return translate(geom, xoff=currentx, yoff=currenty) + + for row in range(rows): + currentx = pos[0] + + for col in range(columns): + trans_geo += translate_recursion(geo_source) + if array_2d_type == 's': # 'spacing' + currentx += (xmax - xmin + spacing_columns) + else: # 'offset' + currentx = pos[0] + off_x * (col + 1) # because 'col' starts from 0 we increment by 1 + + if array_2d_type == 's': # 'spacing' + currenty += (ymax - ymin + spacing_rows) + else: # 'offset; + currenty = pos[1] + off_y * (row + 1) # because 'row' starts from 0 we increment by 1 + + return DrawToolUtilityShape(trans_geo) + + def circular_geo(self, pos): + if pos[0] is None and pos[1] is None: + cdx = self.draw_app.x + cdy = self.draw_app.y + else: + cdx = pos[0] + self.origin[0] + cdy = pos[1] + self.origin[1] + + utility_list = [] + + try: + radius = distance((cdx, cdy), self.origin) + except Exception: + radius = 0 + + if radius == 0: + self.draw_app.delete_utility_geometry() + + if len(self.points) >= 1 and radius > 0: try: - radius = distance((cdx, cdy), self.origin) - except Exception: - radius = 0 + if cdx < self.origin[0]: + radius = -radius - if radius == 0: - self.draw_app.delete_utility_geometry() + # draw the temp geometry + initial_angle = math.asin((cdy - self.origin[1]) / radius) + temp_circular_geo = self.circular_util_shape(radius, initial_angle) - if len(self.points) >= 1 and radius > 0: - try: - if cdx < self.origin[0]: - radius = -radius + temp_points = [ + (self.origin[0], self.origin[1]), + (self.origin[0] + pos[0], self.origin[1] + pos[1]) + ] + temp_line = LineString(temp_points) - # draw the temp geometry - initial_angle = math.asin((cdy - self.origin[1]) / radius) - temp_circular_geo = self.circular_util_shape(radius, initial_angle) + for geo_shape in temp_circular_geo: + utility_list.append(geo_shape.geo) + utility_list.append(temp_line) - temp_points = [ - (self.origin[0], self.origin[1]), - (self.origin[0]+pos[0], self.origin[1]+pos[1]) - ] - temp_line = LineString(temp_points) - - for geo_shape in temp_circular_geo: - utility_list.append(geo_shape.geo) - utility_list.append(temp_line) - - return DrawToolUtilityShape(utility_list) - except Exception as e: - log.error("DrillArray.utility_geometry -- circular -> %s" % str(e)) + return DrawToolUtilityShape(utility_list) + except Exception as e: + log.error("DrillArray.utility_geometry -- circular -> %s" % str(e)) def circular_util_shape(self, radius, ini_angle): direction = self.copy_tool.ui.array_dir_radio.get_value() # CW or CCW diff --git a/appEditors/geo_plugins/GeoCopyPlugin.py b/appEditors/geo_plugins/GeoCopyPlugin.py index 1aa1ab9a..fb71cd2f 100644 --- a/appEditors/geo_plugins/GeoCopyPlugin.py +++ b/appEditors/geo_plugins/GeoCopyPlugin.py @@ -82,6 +82,14 @@ class CopyEditorTool(AppTool): self.ui.array_dir_radio.set_value('CW') self.ui.placement_radio.set_value('s') + self.ui.on_placement_radio(self.ui.placement_radio.get_value()) + + self.ui.spacing_rows.set_value(0) + self.ui.spacing_columns.set_value(0) + self.ui.rows.set_value(1) + self.ui.columns.set_value(1) + self.ui.offsetx_entry.set_value(0) + self.ui.offsety_entry.set_value(0) def on_tab_close(self): self.disconnect_signals() @@ -158,10 +166,10 @@ class CopyEditorUI: # Type of Array self.mode_label = FCLabel('%s:' % _("Mode")) self.mode_label.setToolTip( - _("Normal copy or special (array of copies)") + _("Single copy or special (array of copies)") ) self.mode_radio = RadioSet([ - {'label': _('Normal'), 'value': 'n'}, + {'label': _('Single'), 'value': 'n'}, {'label': _('Array'), 'value': 'a'} ]) @@ -185,7 +193,7 @@ class CopyEditorUI: self.array_size_label.setToolTip(_("Specify how many items to be in the array.")) self.array_size_entry = FCSpinner(policy=False) - self.array_size_entry.set_range(1, 10000) + self.array_size_entry.set_range(1, 100000) self.array_grid.addWidget(self.array_size_label, 2, 0) self.array_grid.addWidget(self.array_size_entry, 2, 1) @@ -282,11 +290,11 @@ class CopyEditorUI: self.two_dim_array_frame.setLayout(self.dd_grid) # 2D placement - self.place_label = FCLabel('%s:' % _('Direction')) + self.place_label = FCLabel('%s:' % _('Placement')) self.place_label.setToolTip( _("Placement of array items:\n" - "- 'Spacing' - define space between rows and columns \n" - "- 'Offset' - each row (and column) will be placed at a multiple of a value, from origin") + "'Spacing' - define space between rows and columns \n" + "'Offset' - each row (and column) will be placed at a multiple of a value, from origin") ) self.placement_radio = RadioSet([ @@ -297,6 +305,102 @@ class CopyEditorUI: self.dd_grid.addWidget(self.place_label, 0, 0) self.dd_grid.addWidget(self.placement_radio, 0, 1) + # Rows + self.rows = FCSpinner(callback=self.confirmation_message_int) + self.rows.set_range(0, 10000) + + self.rows_label = FCLabel('%s:' % _("Rows")) + self.rows_label.setToolTip( + _("Number of rows") + ) + self.dd_grid.addWidget(self.rows_label, 2, 0) + self.dd_grid.addWidget(self.rows, 2, 1) + + # Columns + self.columns = FCSpinner(callback=self.confirmation_message_int) + self.columns.set_range(0, 10000) + + self.columns_label = FCLabel('%s:' % _("Columns")) + self.columns_label.setToolTip( + _("Number of columns") + ) + self.dd_grid.addWidget(self.columns_label, 4, 0) + self.dd_grid.addWidget(self.columns, 4, 1) + + # ------------------------------------------------ + # ############## Spacing Frame ################# + # ------------------------------------------------ + self.spacing_frame = QtWidgets.QFrame() + self.spacing_frame.setContentsMargins(0, 0, 0, 0) + self.dd_grid.addWidget(self.spacing_frame, 6, 0, 1, 2) + + self.s_grid = GLay(v_spacing=5, h_spacing=3) + self.s_grid.setContentsMargins(0, 0, 0, 0) + self.spacing_frame.setLayout(self.s_grid) + + # Spacing Rows + self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message) + self.spacing_rows.set_range(0, 9999) + self.spacing_rows.set_precision(4) + + self.spacing_rows_label = FCLabel('%s:' % _("Spacing rows")) + self.spacing_rows_label.setToolTip( + _("Spacing between rows.\n" + "In current units.") + ) + self.s_grid.addWidget(self.spacing_rows_label, 0, 0) + self.s_grid.addWidget(self.spacing_rows, 0, 1) + + # Spacing Columns + self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message) + self.spacing_columns.set_range(0, 9999) + self.spacing_columns.set_precision(4) + + self.spacing_columns_label = FCLabel('%s:' % _("Spacing cols")) + self.spacing_columns_label.setToolTip( + _("Spacing between columns.\n" + "In current units.") + ) + self.s_grid.addWidget(self.spacing_columns_label, 2, 0) + self.s_grid.addWidget(self.spacing_columns, 2, 1) + + # ------------------------------------------------ + # ############## Offset Frame ################## + # ------------------------------------------------ + self.offset_frame = QtWidgets.QFrame() + self.offset_frame.setContentsMargins(0, 0, 0, 0) + self.dd_grid.addWidget(self.offset_frame, 8, 0, 1, 2) + + self.o_grid = GLay(v_spacing=5, h_spacing=3) + self.o_grid.setContentsMargins(0, 0, 0, 0) + self.offset_frame.setLayout(self.o_grid) + + # Offset X Value + self.offsetx_label = FCLabel('%s X:' % _("Offset")) + self.offsetx_label.setToolTip( + _("'Offset' - each row (and column) will be placed at a multiple of a value, from origin") + ) + + self.offsetx_entry = FCDoubleSpinner(policy=False) + self.offsetx_entry.set_precision(self.decimals) + self.offsetx_entry.set_range(0.0000, 10000.0000) + + self.o_grid.addWidget(self.offsetx_label, 0, 0) + self.o_grid.addWidget(self.offsetx_entry, 0, 1) + + # Offset Y Value + self.offsety_label = FCLabel('%s Y:' % _("Offset")) + self.offsety_label.setToolTip( + _("'Offset' - each row (and column) will be placed at a multiple of a value, from origin") + ) + + self.offsety_entry = FCDoubleSpinner(policy=False) + self.offsety_entry.set_precision(self.decimals) + self.offsety_entry.set_range(0.0000, 10000.0000) + + self.o_grid.addWidget(self.offsety_label, 2, 0) + self.o_grid.addWidget(self.offsety_entry, 2, 1) + # ############################################################################################################# # ############################ CIRCULAR Array ################################################################# # ############################################################################################################# @@ -339,7 +443,7 @@ class CopyEditorUI: self.layout.addWidget(self.add_button) GLay.set_common_column_size([ - grid0, self.array_grid, self.lin_grid, self.dd_grid, self.circ_grid + grid0, self.array_grid, self.lin_grid, self.dd_grid, self.circ_grid, self.s_grid, self.o_grid ], 0) self.layout.addStretch(1) @@ -348,6 +452,24 @@ class CopyEditorUI: self.mode_radio.activated_custom.connect(self.on_copy_mode) self.array_type_radio.activated_custom.connect(self.on_array_type_radio) self.axis_radio.activated_custom.connect(self.on_linear_angle_radio) + self.placement_radio.activated_custom.connect(self.on_placement_radio) + + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) + + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) def on_copy_mode(self, val): if val == 'n': @@ -357,21 +479,58 @@ class CopyEditorUI: self.array_frame.show() def on_array_type_radio(self, val): - if val == 'linear': - self.array_circular_frame.hide() - self.array_linear_frame.show() - self.two_dim_array_frame.hide() - self.app.inform.emit(_("Click to place ...")) - elif val == '2D': + if val == '2D': self.array_circular_frame.hide() self.array_linear_frame.hide() self.two_dim_array_frame.show() + if self.placement_radio.get_value() == 's': + self.spacing_frame.show() + self.offset_frame.hide() + else: + self.spacing_frame.hide() + self.offset_frame.show() + + self.array_size_entry.setDisabled(True) + self.on_rows_cols_value_changed() + + self.rows.valueChanged.connect(self.on_rows_cols_value_changed) + self.columns.valueChanged.connect(self.on_rows_cols_value_changed) + self.app.inform.emit(_("Click to place ...")) else: - self.array_circular_frame.show() - self.array_linear_frame.hide() - self.two_dim_array_frame.hide() - self.app.inform.emit(_("Click on the circular array Center position")) + if val == 'linear': + self.array_circular_frame.hide() + self.array_linear_frame.show() + self.two_dim_array_frame.hide() + self.spacing_frame.hide() + self.offset_frame.hide() + + self.app.inform.emit(_("Click to place ...")) + else: # 'circular' + self.array_circular_frame.show() + self.array_linear_frame.hide() + self.two_dim_array_frame.hide() + self.spacing_frame.hide() + self.offset_frame.hide() + + self.app.inform.emit(_("Click on the circular array Center position")) + + self.array_size_entry.setDisabled(False) + try: + self.rows.valueChanged.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.columns.valueChanged.disconnect() + except (TypeError, AttributeError): + pass + + def on_rows_cols_value_changed(self): + new_size = self.rows.get_value() * self.columns.get_value() + if new_size == 0: + new_size = 1 + self.array_size_entry.set_value(new_size) def on_linear_angle_radio(self, val): if val == 'A': @@ -380,3 +539,11 @@ class CopyEditorUI: else: self.linear_angle_spinner.hide() self.linear_angle_label.hide() + + def on_placement_radio(self, val): + if val == 's': + self.spacing_frame.show() + self.offset_frame.hide() + else: + self.spacing_frame.hide() + self.offset_frame.show() diff --git a/appEditors/geo_plugins/GeoTransformationPlugin.py b/appEditors/geo_plugins/GeoTransformationPlugin.py index bebadce3..7a609183 100644 --- a/appEditors/geo_plugins/GeoTransformationPlugin.py +++ b/appEditors/geo_plugins/GeoTransformationPlugin.py @@ -588,7 +588,7 @@ class TransformEditorTool(AppTool): maxy = max(maxy, maxy_) return minx, miny, maxx, maxy except TypeError: - # it's an object, return it's bounds + # it's an object, return its bounds return lst.bounds() return bounds_rec(shapelist) diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py index dc39ae36..1bd9d30a 100644 --- a/appGUI/MainGUI.py +++ b/appGUI/MainGUI.py @@ -3505,10 +3505,14 @@ class MainGUI(QtWidgets.QMainWindow): self.app.geo_editor.active_tool.rect_tool.length != 0.0 and \ self.app.geo_editor.active_tool.rect_tool.width != 0.0: pass - elif self.app.geo_editor.active_tool.name in ['move', 'copy'] and \ + elif self.app.geo_editor.active_tool.name == 'move' and \ self.app.geo_editor.active_tool.move_tool.length != 0.0 and \ self.app.geo_editor.active_tool.move_tool.width != 0.0: pass + elif self.app.geo_editor.active_tool.name == 'copy' and \ + self.app.geo_editor.active_tool.copy_tool.length != 0.0 and \ + self.app.geo_editor.active_tool.copy_tool.width != 0.0: + pass else: self.app.geo_editor.active_tool.click( self.app.geo_editor.snap(self.app.geo_editor.x, self.app.geo_editor.y)) diff --git a/appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py b/appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py index 15c7f28e..125a3ba0 100644 --- a/appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py +++ b/appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py @@ -46,7 +46,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): self.spacing_columns_label = FCLabel('%s:' % _("Spacing cols")) self.spacing_columns_label.setToolTip( - _("Spacing between columns of the desired panel.\n" + _("Spacing between columns.\n" "In current units.") ) param_grid.addWidget(self.spacing_columns_label, 0, 0) @@ -60,7 +60,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): self.spacing_rows_label = FCLabel('%s:' % _("Spacing rows")) self.spacing_rows_label.setToolTip( - _("Spacing between rows of the desired panel.\n" + _("Spacing between rows.\n" "In current units.") ) param_grid.addWidget(self.spacing_rows_label, 2, 0) @@ -73,7 +73,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): self.columns_label = FCLabel('%s:' % _("Columns")) self.columns_label.setToolTip( - _("Number of columns of the desired panel") + _("Number of columns") ) param_grid.addWidget(self.columns_label, 4, 0) param_grid.addWidget(self.pcolumns, 4, 1) @@ -85,7 +85,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): self.rows_label = FCLabel('%s:' % _("Rows")) self.rows_label.setToolTip( - _("Number of rows of the desired panel") + _("Number of rows") ) param_grid.addWidget(self.rows_label, 6, 0) param_grid.addWidget(self.prows, 6, 1) diff --git a/appPlugins/ToolPanelize.py b/appPlugins/ToolPanelize.py index d83aa42f..6936cf4d 100644 --- a/appPlugins/ToolPanelize.py +++ b/appPlugins/ToolPanelize.py @@ -1287,7 +1287,7 @@ class PanelizeUI: self.spacing_columns_label = FCLabel('%s:' % _("Spacing cols")) self.spacing_columns_label.setToolTip( - _("Spacing between columns of the desired panel.\n" + _("Spacing between columns.\n" "In current units.") ) grid2.addWidget(self.spacing_columns_label, 0, 0) @@ -1300,7 +1300,7 @@ class PanelizeUI: self.spacing_rows_label = FCLabel('%s:' % _("Spacing rows")) self.spacing_rows_label.setToolTip( - _("Spacing between rows of the desired panel.\n" + _("Spacing between rows.\n" "In current units.") ) grid2.addWidget(self.spacing_rows_label, 2, 0) @@ -1312,7 +1312,7 @@ class PanelizeUI: self.columns_label = FCLabel('%s:' % _("Columns")) self.columns_label.setToolTip( - _("Number of columns of the desired panel") + _("Number of columns") ) grid2.addWidget(self.columns_label, 4, 0) grid2.addWidget(self.columns, 4, 1) @@ -1323,7 +1323,7 @@ class PanelizeUI: self.rows_label = FCLabel('%s:' % _("Rows")) self.rows_label.setToolTip( - _("Number of rows of the desired panel") + _("Number of rows") ) grid2.addWidget(self.rows_label, 6, 0) grid2.addWidget(self.rows, 6, 1)