diff --git a/FlatCAMApp.py b/FlatCAMApp.py index d3441e20..2f709a89 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -748,7 +748,8 @@ class App(QtCore.QObject): "tools_solderpaste_speedfwd": 20, "tools_solderpaste_dwellfwd": 1, "tools_solderpaste_speedrev": 10, - "tools_solderpaste_dwellrev": 1 + "tools_solderpaste_dwellrev": 1, + "tools_solderpaste_pp": '' }) ############################### diff --git a/README.md b/README.md index 1b54dd6e..c1978e9d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ CAD program, and create G-Code for Isolation routing. - added the functions for GCode View and GCode Save in Tool SolderPaste - some work in the Gcode generation function in Tool SolderPaste - added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool. -- ToolSoderPaste tools (nozzles) now have each it's own settings +- ToolSolderPaste tools (nozzles) now have each it's own settings +- creating the camlib functions for the ToolSolderPaste gcode generation functions 20.02.2019 diff --git a/camlib.py b/camlib.py index 59df4410..028c9833 100644 --- a/camlib.py +++ b/camlib.py @@ -5026,7 +5026,7 @@ class CNCjob(Geometry): :param depthpercut: Maximum depth in each pass. :param extracut: Adds (or not) an extra cut at the end of each path overlapping the first point in path to ensure complete copper removal - :return: None + :return: GCode - string """ log.debug("Generate_from_multitool_geometry()") @@ -5191,6 +5191,99 @@ class CNCjob(Geometry): return self.gcode + def generate_gcode_from_solderpaste_geo(self, **kwargs): + """ + Algorithm to generate from multitool Geometry. + + Algorithm description: + ---------------------- + Uses RTree to find the nearest path to follow. + + :return: Gcode string + """ + + log.debug("Generate_from_solderpaste_geometry()") + + ## Index first and last points in paths + # What points to index. + def get_pts(o): + return [o.coords[0], o.coords[-1]] + + self.gcode = "" + + if not kwargs: + log.debug("camlib.generate_from_solderpaste_geo() --> No tool in the solderpaste geometry.") + self.app.inform.emit("[ERROR_NOTCL] There is no tool data in the SolderPaste geometry.") + + + # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter + # given under the name 'toolC' + + self.postdata['toolC'] = kwargs['tooldia'] + + # Initial G-Code + pp_solderpaste_name = kwargs['data']['tools_solderpaste_pp'] if kwargs['data']['tools_solderpaste_pp'] else \ + self.app.defaults['tools_solderpaste_pp'] + p = self.app.postprocessors[pp_solderpaste_name] + + self.gcode = self.doformat(p.start_code) + + ## Flatten the geometry. Only linear elements (no polygons) remain. + flat_geometry = self.flatten(kwargs['solid_geometry'], pathonly=True) + log.debug("%d paths" % len(flat_geometry)) + + # Create the indexed storage. + storage = FlatCAMRTreeStorage() + storage.get_points = get_pts + + # Store the geometry + log.debug("Indexing geometry before generating G-Code...") + for shape in flat_geometry: + if shape is not None: + storage.insert(shape) + + # kwargs length will tell actually the number of tools used so if we have more than one tools then + # we have toolchange event + if len(kwargs) > 1: + self.gcode += self.doformat(p.toolchange_code) + else: + self.gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height + + ## Iterate over geometry paths getting the nearest each time. + log.debug("Starting SolderPaste G-Code...") + path_count = 0 + current_pt = (0, 0) + + pt, geo = storage.nearest(current_pt) + + try: + while True: + path_count += 1 + + # Remove before modifying, otherwise deletion will fail. + storage.remove(geo) + + # If last point in geometry is the nearest but prefer the first one if last point == first point + # then reverse coordinates. + if pt != geo.coords[0] and pt == geo.coords[-1]: + geo.coords = list(geo.coords)[::-1] + + self.gcode += self.create_soldepaste_gcode(geo, p=p) + current_pt = geo.coords[-1] + pt, geo = storage.nearest(current_pt) # Next + + except StopIteration: # Nothing found in storage. + pass + + log.debug("Finishing SolderPste G-Code... %s paths traced." % path_count) + + # Finish + self.gcode += self.doformat(p.lift_code) + self.gcode += self.doformat(p.end_code) + + return self.gcode + + def generate_from_geometry_2(self, geometry, append=True, tooldia=None, offset=0.0, tolerance=0, z_cut=1.0, z_move=2.0, @@ -5443,13 +5536,51 @@ class CNCjob(Geometry): return self.gcode + def create_soldepaste_gcode(self, geometry, p): + gcode = '' + path = self.segment(geometry.coords) + + if type(geometry) == LineString or type(geometry) == LinearRing: + # Move fast to 1st point + gcode += self.doformat(p.rapid_code) # Move to first point + + # Move down to cutting depth + gcode += self.doformat(p.feedrate_z_code) + gcode += self.doformat(p.down_z_start_code) + gcode += self.doformat(p.spindle_on_fwd_code) # Start dispensing + gcode += self.doformat(p.feedrate_xy_code) + + # Cutting... + for pt in path[1:]: + gcode += self.doformat(p.linear_code) # Linear motion to point + + # Up to travelling height. + gcode += self.doformat(p.spindle_off_code) # Stop dispensing + gcode += self.doformat(p.spindle_on_rev_code) + gcode += self.doformat(p.down_z_stop_code) + gcode += self.doformat(p.spindle_off_code) + gcode += self.doformat(p.lift_code) + elif type(geometry) == Point: + gcode += self.doformat(p.linear_code) # Move to first point + + gcode += self.doformat(p.feedrate_z_code) + gcode += self.doformat(p.down_z_start_code) + gcode += self.doformat(p.spindle_on_fwd_code) # Start dispensing + # TODO A dwell time for dispensing? + gcode += self.doformat(p.spindle_off_code) # Stop dispensing + gcode += self.doformat(p.spindle_on_rev_code) + gcode += self.doformat(p.down_z_stop_code) + gcode += self.doformat(p.spindle_off_code) + gcode += self.doformat(p.lift_code) + return gcode + def create_gcode_single_pass(self, geometry, extracut, tolerance): # G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time. gcode_single_pass = '' if type(geometry) == LineString or type(geometry) == LinearRing: if extracut is False: - gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, ) + gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance) else: if geometry.is_ring: gcode_single_pass = self.linear2gcode_extra(geometry, tolerance=tolerance) diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 757e4e11..5a0cebe3 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -109,20 +109,10 @@ class ToolSolderPaste(FlatCAMTool): "Generate solder paste dispensing geometry." ) - step1_lbl = QtWidgets.QLabel("STEP 1:") - step1_lbl.setToolTip( - "First step is to select a number of nozzle tools for usage\n" - "and then create a solder paste dispensing geometry out of an\n" - "Solder Paste Mask Gerber file." - ) - grid0.addWidget(self.addtool_btn, 0, 0) # grid2.addWidget(self.copytool_btn, 0, 1) grid0.addWidget(self.deltool_btn, 0, 2) - grid0.addWidget(step1_lbl, 2, 0) - grid0.addWidget(self.soldergeo_btn, 2, 2) - ## Form Layout geo_form_layout = QtWidgets.QFormLayout() self.layout.addLayout(geo_form_layout) @@ -259,16 +249,6 @@ class ToolSolderPaste(FlatCAMTool): "on PCB pads." ) - step2_lbl = QtWidgets.QLabel("STEP 2:") - step2_lbl.setToolTip( - "Second step is to select a solder paste dispensing geometry,\n" - "set the CAM parameters and then generate a CNCJob object which\n" - "will pe painted on canvas in blue color." - ) - - grid1.addWidget(step2_lbl, 0, 0) - grid1.addWidget(self.solder_gcode_btn, 0, 2) - ## Form Layout cnc_form_layout = QtWidgets.QFormLayout() self.gcode_box.addLayout(cnc_form_layout) @@ -300,6 +280,25 @@ class ToolSolderPaste(FlatCAMTool): grid2 = QtWidgets.QGridLayout() self.save_gcode_box.addLayout(grid2) + step1_lbl = QtWidgets.QLabel("STEP 1:") + step1_lbl.setToolTip( + "First step is to select a number of nozzle tools for usage\n" + "and then create a solder paste dispensing geometry out of an\n" + "Solder Paste Mask Gerber file." + ) + grid2.addWidget(step1_lbl, 0, 0) + grid2.addWidget(self.soldergeo_btn, 0, 2) + + step2_lbl = QtWidgets.QLabel("STEP 2:") + step2_lbl.setToolTip( + "Second step is to select a solder paste dispensing geometry,\n" + "set the CAM parameters and then generate a CNCJob object which\n" + "will pe painted on canvas in blue color." + ) + + grid2.addWidget(step2_lbl, 1, 0) + grid2.addWidget(self.solder_gcode_btn, 1, 2) + self.solder_gcode_view_btn = QtWidgets.QPushButton("View GCode") self.solder_gcode_view_btn.setToolTip( "View the generated GCode for Solder Paste dispensing\n" @@ -318,9 +317,9 @@ class ToolSolderPaste(FlatCAMTool): "a solder paste dispensing geometry, and then view/save it's GCode." ) - grid2.addWidget(step3_lbl, 0, 0) - grid2.addWidget(self.solder_gcode_view_btn, 0, 2) - grid2.addWidget(self.solder_gcode_save_btn, 1, 2) + grid2.addWidget(step3_lbl, 2, 0) + grid2.addWidget(self.solder_gcode_view_btn, 2, 2) + grid2.addWidget(self.solder_gcode_save_btn, 3, 2) self.layout.addStretch() @@ -853,6 +852,15 @@ class ToolSolderPaste(FlatCAMTool): tooluid = int(uid) break + geo_obj.tools[tooluid] = {} + geo_obj.tools[tooluid]['tooldia'] = tool + geo_obj.tools[tooluid]['data'] = self.tools[tooluid]['data'] + geo_obj.tools[tooluid]['solid_geometry'] = [] + geo_obj.tools[tooluid]['offset'] = 'Path' + geo_obj.tools[tooluid]['offset_value'] = 0.0 + geo_obj.tools[tooluid]['type'] = 'SolderPaste' + geo_obj.tools[tooluid]['tool_type'] = 'Dispenser Nozzle' + for g in work_geo: if type(g) == MultiPolygon: for poly in g: @@ -860,16 +868,8 @@ class ToolSolderPaste(FlatCAMTool): if geom != 'fail': try: geo_obj.tools[tooluid]['solid_geometry'].append(geom) - except KeyError: - geo_obj.tools[tooluid] = {} - geo_obj.tools[tooluid]['solid_geometry'] = [] - geo_obj.tools[tooluid]['solid_geometry'].append(geom) - geo_obj.tools[tooluid]['tooldia'] = tool - geo_obj.tools[tooluid]['offset'] = 'Path' - geo_obj.tools[tooluid]['offset_value'] = 0.0 - geo_obj.tools[tooluid]['type'] = ' ' - geo_obj.tools[tooluid]['tool_type'] = ' ' - geo_obj.tools[tooluid]['data'] = {} + except Exception as e: + log.debug('ToolSoderPaste.on_create_geo() --> %s' % str(e)) else: rest_geo.append(poly) elif type(g) == Polygon: @@ -877,16 +877,8 @@ class ToolSolderPaste(FlatCAMTool): if geom != 'fail': try: geo_obj.tools[tooluid]['solid_geometry'].append(geom) - except KeyError: - geo_obj.tools[tooluid] = {} - geo_obj.tools[tooluid]['solid_geometry'] = [] - geo_obj.tools[tooluid]['solid_geometry'].append(geom) - geo_obj.tools[tooluid]['tooldia'] = tool - geo_obj.tools[tooluid]['offset'] = 'Path' - geo_obj.tools[tooluid]['offset_value'] = 0.0 - geo_obj.tools[tooluid]['type'] = ' ' - geo_obj.tools[tooluid]['tool_type'] = ' ' - geo_obj.tools[tooluid]['data'] = {} + except Exception as e: + log.debug('ToolSoderPaste.on_create_geo() --> %s' % str(e)) else: rest_geo.append(g) @@ -1004,23 +996,11 @@ class ToolSolderPaste(FlatCAMTool): def on_create_gcode(self, use_thread=True): """ - Creates a multi-tool CNCJob out of this Geometry object. - The actual work is done by the target FlatCAMCNCjob object's - `generate_from_geometry_2()` method. + Creates a multi-tool CNCJob out of this Geometry object. + :return: None + """ - :param z_cut: Cut depth (negative) - :param z_move: Hight of the tool when travelling (not cutting) - :param feedrate: Feed rate while cutting on X - Y plane - :param feedrate_z: Feed rate while cutting on Z plane - :param feedrate_rapid: Feed rate while moving with rapids - :param tooldia: Tool diameter - :param outname: Name of the new object - :param spindlespeed: Spindle speed (RPM) - :param ppname_g Name of the postprocessor - :return: None - """ - - name = self.obj_combo.currentText() + name = self.geo_obj_combo.currentText() obj = self.app.collection.get_by_name(name) if obj.special_group != 'solder_paste_tool': @@ -1031,7 +1011,8 @@ class ToolSolderPaste(FlatCAMTool): multitool_gcode = '' # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia - outname = "%s_%s" % (name, 'cnc_solderpaste') + originar_name = obj.options['name'].rpartition('_')[0] + outname = "%s_%s" % (originar_name, '_cnc_solderpaste') try: xmin = obj.options['xmin'] @@ -1053,9 +1034,7 @@ class ToolSolderPaste(FlatCAMTool): assert isinstance(job_obj, FlatCAMCNCjob), \ "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) - # count the tools - tool_cnt = 0 - dia_cnc_dict = {} + tool_cnc_dict = {} # this turn on the FlatCAMCNCJob plot for multiple tools job_obj.multitool = True @@ -1067,146 +1046,52 @@ class ToolSolderPaste(FlatCAMTool): job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax - - # try: - # job_obj.feedrate_probe = float(self.options["feedrate_probe"]) - # except ValueError: - # # try to convert comma to decimal point. if it's still not working error message and return - # try: - # job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) - # except ValueError: - # self.app.inform.emit( - # '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' - # 'or self.options["feedrate_probe"]') - - # make sure that trying to make a CNCJob from an empty file is not creating an app crash a = 0 - for tooluid_key in self.tools: - if self.tools[tooluid_key]['solid_geometry'] is None: + for tooluid_key in obj.tools: + if obj.tools[tooluid_key]['solid_geometry'] is None: a += 1 - if a == len(self.tools): + if a == len(obj.tools): self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') return 'fail' - for tooluid_key in self.tools: - tool_cnt += 1 + for tooluid_key, tooluid_value in obj.tools.items(): app_obj.progress.emit(20) # find the tool_dia associated with the tooluid_key - tool_dia = self.sel_tools[tooluid_key]['tooldia'] - tool_solid_geometry = self.tools[tooluid_key]['solid_geometry'] - - for diadict_key, diadict_value in self.sel_tools[tooluid_key].items(): - if diadict_key == 'tooldia': - tooldia_val = float('%.4f' % float(diadict_value)) - dia_cnc_dict.update({ - diadict_key: tooldia_val - }) - if diadict_key == 'offset': - dia_cnc_dict.update({ - diadict_key: '' - }) - - if diadict_key == 'type': - dia_cnc_dict.update({ - diadict_key: '' - }) - - if diadict_key == 'tool_type': - dia_cnc_dict.update({ - diadict_key: '' - }) - - if diadict_key == 'data': - for data_key, data_value in diadict_value.items(): - if data_key == "multidepth": - multidepth = data_value - if data_key == "depthperpass": - depthpercut = data_value - - if data_key == "extracut": - extracut = data_value - if data_key == "startz": - startz = data_value - if data_key == "endz": - endz = data_value - - if data_key == "toolchangez": - toolchangez = data_value - if data_key == "toolchangexy": - toolchangexy = data_value - if data_key == "toolchange": - toolchange = data_value - - if data_key == "cutz": - z_cut = data_value - if data_key == "travelz": - z_move = data_value - - if data_key == "feedrate": - feedrate = data_value - if data_key == "feedrate_z": - feedrate_z = data_value - if data_key == "feedrate_rapid": - feedrate_rapid = data_value - - if data_key == "ppname_g": - pp_geometry_name = data_value - - if data_key == "spindlespeed": - spindlespeed = data_value - if data_key == "dwell": - dwell = data_value - if data_key == "dwelltime": - dwelltime = data_value - - datadict = copy.deepcopy(diadict_value) - dia_cnc_dict.update({ - diadict_key: datadict - }) + tool_dia = tooluid_value['tooldia'] + tool_cnc_dict = deepcopy(tooluid_value) job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] # Propagate options - job_obj.options["tooldia"] = tooldia_val - job_obj.options['type'] = 'Geometry' - job_obj.options['tool_dia'] = tooldia_val + job_obj.options["tooldia"] = tool_dia + job_obj.options['tool_dia'] = tool_dia - app_obj.progress.emit(40) - - res = job_obj.generate_from_multitool_geometry( - tool_solid_geometry, tooldia=tooldia_val, offset=0.0, - tolerance=0.0005, z_cut=z_cut, z_move=z_move, - feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, - spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime, - multidepth=multidepth, depthpercut=depthpercut, - extracut=extracut, startz=startz, endz=endz, - toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, - pp_geometry_name=pp_geometry_name, - tool_no=tool_cnt) + ### CREATE GCODE ### + res = job_obj.generate_gcode_from_solderpaste_geo(**tool_cnc_dict) if res == 'fail': log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") return 'fail' else: - dia_cnc_dict['gcode'] = res + tool_cnc_dict['gcode'] = res - dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() + ### PARSE GCODE ### + tool_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() # TODO this serve for bounding box creation only; should be optimized - dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']]) + tool_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in tool_cnc_dict['gcode_parsed']]) # tell gcode_parse from which point to start drawing the lines depending on what kind of # object is the source of gcode job_obj.toolchange_xy_type = "geometry" - app_obj.progress.emit(80) job_obj.cnc_tools.update({ - tooluid_key: copy.deepcopy(dia_cnc_dict) + tooluid_key: copy.deepcopy(tool_cnc_dict) }) - dia_cnc_dict.clear() + tool_cnc_dict.clear() if use_thread: # To be run in separate thread