def generate_toolpath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None): path = [] quit_requested = False model = pycam.Geometry.Model.get_combined_model(models) # Transfer the grid (a generator) into a list of lists and count the # items. lines = [] # usually there is only one layer - but an xy-grid consists of two for layer in motion_grid: for line in layer: lines.append(line) num_of_lines = len(lines) progress_counter = ProgressCounter(len(lines), draw_callback) current_line = 0 args = [] for one_grid_line in lines: # simplify the data (useful for remote processing) xy_coords = [(pos[0], pos[1]) for pos in one_grid_line] args.append((xy_coords, minz, maxz, model, cutter)) for points in run_in_parallel(_process_one_grid_line, args, callback=progress_counter.update): if draw_callback and draw_callback( text="DropCutter: processing line %d/%d" % (current_line + 1, num_of_lines)): # cancel requested quit_requested = True break for point in points: if point is None: # exceeded maxz - the cutter has to skip this point path.append(MoveSafety()) else: path.append(MoveStraight(point)) # The progress counter may return True, if cancel was requested. if draw_callback and draw_callback(tool_position=point, toolpath=path): quit_requested = True break # add a move to safety height after each line of moves path.append(MoveSafety()) progress_counter.increment() # update progress current_line += 1 if quit_requested: break return path
def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None): quit_requested = False model = pycam.Geometry.Model.get_combined_model(models) # Transfer the grid (a generator) into a list of lists and count the # items. lines = [] # usually there is only one layer - but an xy-grid consists of two for layer in motion_grid: for line in layer: lines.append(line) num_of_lines = len(lines) progress_counter = ProgressCounter(len(lines), draw_callback) current_line = 0 self.pa.new_direction(0) args = [] for one_grid_line in lines: # simplify the data (useful for remote processing) xy_coords = [(pos.x, pos.y) for pos in one_grid_line] args.append((xy_coords, minz, maxz, model, cutter, self.physics)) for points in run_in_parallel(_process_one_grid_line, args, callback=progress_counter.update): self.pa.new_scanline() if draw_callback and draw_callback(text="DropCutter: processing " \ + "line %d/%d" % (current_line + 1, num_of_lines)): # cancel requested quit_requested = True break for point in points: if point is None: # exceeded maxz - the cutter has to skip this point self.pa.end_scanline() self.pa.new_scanline() continue self.pa.append(point) # "draw_callback" returns true, if the user requested to quit # via the GUI. # The progress counter may return True, if cancel was requested. if draw_callback and draw_callback(tool_position=point, toolpath=self.pa.paths): quit_requested = True break progress_counter.increment() self.pa.end_scanline() # update progress current_line += 1 if quit_requested: break self.pa.end_direction() self.pa.finish() return self.pa.paths
def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None): path = [] quit_requested = False model = pycam.Geometry.Model.get_combined_model(models) # Transfer the grid (a generator) into a list of lists and count the # items. lines = [] # usually there is only one layer - but an xy-grid consists of two for layer in motion_grid: for line in layer: lines.append(line) num_of_lines = len(lines) progress_counter = ProgressCounter(len(lines), draw_callback) current_line = 0 args = [] for one_grid_line in lines: # simplify the data (useful for remote processing) xy_coords = [(pos[0], pos[1]) for pos in one_grid_line] args.append((xy_coords, minz, maxz, model, cutter, self.physics)) for points in run_in_parallel(_process_one_grid_line, args, callback=progress_counter.update): if draw_callback and draw_callback(text="DropCutter: processing " \ + "line %d/%d" % (current_line + 1, num_of_lines)): # cancel requested quit_requested = True break for point in points: if point is None: # exceeded maxz - the cutter has to skip this point path.append((MOVE_SAFETY, None)) else: path.append((MOVE_STRAIGHT, point)) # The progress counter may return True, if cancel was requested. if draw_callback and draw_callback(tool_position=point, toolpath=path): quit_requested = True break # add a move to safety height after each line of moves path.append((MOVE_SAFETY, None)) progress_counter.increment() # update progress current_line += 1 if quit_requested: break return path
def GenerateToolPath(self, motion_grid, draw_callback=None): # calculate the number of steps # Transfer the grid (a generator) into a list of lists and count the # items. grid = [] num_of_grid_positions = 0 for layer in motion_grid: lines = [] for line in layer: # convert the generator to a list lines.append(list(line)) num_of_grid_positions += len(lines) grid.append(lines) num_of_layers = len(grid) progress_counter = ProgressCounter(num_of_grid_positions, draw_callback) current_layer = 0 for layer_grid in grid: # update the progress bar and check, if we should cancel the process if draw_callback and draw_callback(text="PushCutter: processing" \ + " layer %d/%d" % (current_layer + 1, num_of_layers)): # cancel immediately break self.pa.new_direction(0) self.GenerateToolPathSlice(layer_grid, draw_callback, progress_counter) self.pa.end_direction() self.pa.finish() current_layer += 1 if self._use_polygon_extractor and (len(self.models) > 1): other_models = self.models[1:] # TODO: this is complicated and hacky :( # we don't use parallelism or ODE (for the sake of simplicity) final_pa = pycam.PathProcessors.SimpleCutter.SimpleCutter( reverse=self.pa.reverse) for path in self.pa.paths: final_pa.new_scanline() pairs = [] for index in range(len(path.points) - 1): pairs.append((path.points[index], path.points[index + 1])) for p1, p2 in pairs: free_points = get_free_paths_triangles( other_models, self.cutter, p1, p2) for point in free_points: final_pa.append(point) final_pa.end_scanline() final_pa.finish() return final_pa.paths else: return self.pa.paths
def generate_toolpath(self, cutter, models, minx, maxx, miny, maxy, minz, maxz, dz, draw_callback=None): # reset the list of processed triangles self._processed_triangles = [] # calculate the number of steps # Sometimes there is a floating point accuracy issue: make sure # that only one layer is drawn, if maxz and minz are almost the same. if abs(maxz - minz) < epsilon: diff_z = 0 else: diff_z = abs(maxz - minz) num_of_layers = 1 + ceil(diff_z / dz) z_step = diff_z / max(1, (num_of_layers - 1)) # only the first model is used for the contour-follow algorithm # TODO: should we combine all models? num_of_triangles = len(models[0].triangles(minx=minx, miny=miny, maxx=maxx, maxy=maxy)) progress_counter = ProgressCounter( 2 * num_of_layers * num_of_triangles, draw_callback) current_layer = 0 z_steps = [(maxz - i * z_step) for i in range(num_of_layers)] # collision handling function for z in z_steps: # update the progress bar and check, if we should cancel the process if draw_callback: if draw_callback( text=("ContourFollow: processing layer %d/%d" % (current_layer + 1, num_of_layers))): # cancel immediately break self.pa.new_direction(0) self.generate_toolpath_slice(cutter, models[0], minx, maxx, miny, maxy, z, draw_callback, progress_counter, num_of_triangles) self.pa.end_direction() self.pa.finish() current_layer += 1 return self.pa.paths
def _get_progress_callback(self, update_callback): if update_callback: return ProgressCounter(self.get_children_count(), update_callback=update_callback).increment else: return None
def GenerateToolPath(self, minz, maxz, horiz_step, dz, draw_callback=None): quit_requested = False # calculate the number of steps num_of_layers = 1 + ceil(abs(maxz - minz) / dz) if num_of_layers > 1: z_step = abs(maxz - minz) / (num_of_layers - 1) z_steps = [(maxz - i * z_step) for i in range(num_of_layers)] # The top layer is treated as the current surface - thus it does not # require engraving. z_steps = z_steps[1:] else: z_steps = [minz] num_of_layers = len(z_steps) current_layer = 0 num_of_lines = self.contour_model.get_num_of_lines() progress_counter = ProgressCounter(len(z_steps) * num_of_lines, draw_callback) if draw_callback: draw_callback(text="Engrave: optimizing polygon order") # Sort the polygons according to their directions (first inside, then # outside. This reduces the problem of break-away pieces. inner_polys = [] outer_polys = [] for poly in self.contour_model.get_polygons(): if poly.get_area() <= 0: inner_polys.append(poly) else: outer_polys.append(poly) inner_sorter = PolygonSorter(inner_polys, callback=draw_callback) outer_sorter = PolygonSorter(outer_polys, callback=draw_callback) line_groups = inner_sorter.get_polygons() + outer_sorter.get_polygons() if self.clockwise: for line_group in line_groups: line_group.reverse_direction() # push slices for all layers above ground if maxz == minz: # only one layer - use PushCutter instead of DropCutter # put "last_z" clearly above the model plane last_z = maxz + 1 push_steps = z_steps drop_steps = [] else: # multiple layers last_z = maxz push_steps = z_steps[:-1] drop_steps = [z_steps[-1]] for z in push_steps: # update the progress bar and check, if we should cancel the process if draw_callback and draw_callback( text="Engrave: processing" + " layer %d/%d" % (current_layer + 1, num_of_layers) ): # cancel immediately break for line_group in line_groups: for line in line_group.get_lines(): self.GenerateToolPathLinePush(self.pa_push, line, z, last_z, draw_callback=draw_callback) if progress_counter.increment(): # cancel requested quit_requested = True # finish the current path self.pa_push.finish() break self.pa_push.finish() # break the outer loop if requested if quit_requested: break current_layer += 1 last_z = z if quit_requested: return self.pa_push.paths for z in drop_steps: if draw_callback: draw_callback(text="Engrave: processing layer %d/%d" % (current_layer + 1, num_of_layers)) # process the final layer with a drop cutter for line_group in line_groups: self.pa_drop.new_direction(0) self.pa_drop.new_scanline() for line in line_group.get_lines(): self.GenerateToolPathLineDrop( self.pa_drop, line, z, maxz, horiz_step, last_z, draw_callback=draw_callback ) if progress_counter.increment(): # quit requested quit_requested = True break self.pa_drop.end_scanline() self.pa_drop.end_direction() # break the outer loop if requested if quit_requested: break current_layer += 1 last_z = z self.pa_drop.finish() return self.pa_push.paths + self.pa_drop.paths
def generate_toolpath(model, tool_settings=None, bounds=None, direction="x", path_generator="DropCutter", path_postprocessor="ZigZagCutter", material_allowance=0, overlap_percent=0, step_down=0, engrave_offset=0, milling_style="ignore", pocketing_type="none", support_model=None, calculation_backend=None, callback=None): """ abstract interface for generating a toolpath @type model: pycam.Geometry.Model.Model @value model: a model contains surface triangles or a contour @type tool_settings: dict @value tool_settings: contains at least the following keys (depending on the tool type): "shape": any of possible cutter shape (see "pycam.Cutters") "tool_radius": main radius of the tools "torus_radius": (only for ToroidalCutter) second toroidal radius @type bounds_low: tuple(float) | list(float) @value bounds_low: the lower processing boundary (used for the center of the tool) (order: minx, miny, minz) @type bounds_high: tuple(float) | list(float) @value bounds_high: the lower processing boundary (used for the center of the tool) (order: maxx, maxy, maxz) @type direction: str @value direction: any member of the DIRECTIONS set (e.g. "x", "y" or "xy") @type path_generator: str @value path_generator: any member of the PATH_GENERATORS set @type path_postprocessor: str @value path_postprocessor: any member of the PATH_POSTPROCESSORS set @type material_allowance: float @value material_allowance: the minimum distance between the tool and the model @type overlap_percent: int @value overlap_percent: the overlap between two adjacent tool paths (0..100) given in percent @type step_down: float @value step_down: maximum height of each layer (for PushCutter) @type engrave_offset: float @value engrave_offset: toolpath distance to the contour model @type calculation_backend: str | None @value calculation_backend: any member of the CALCULATION_BACKENDS set The default is the triangular collision detection. @rtype: pycam.Toolpath.Toolpath | str @return: the resulting toolpath object or an error string in case of invalid arguments """ log.debug("Starting toolpath generation") step_down = number(step_down) engrave_offset = number(engrave_offset) if bounds is None: # no bounds were given - we use the boundaries of the model bounds = pycam.Toolpath.Bounds(pycam.Toolpath.Bounds.TYPE_CUSTOM, (model.minx, model.miny, model.minz), (model.maxx, model.maxy, model.maxz)) bounds_low, bounds_high = bounds.get_absolute_limits() minx, miny, minz = [number(value) for value in bounds_low] maxx, maxy, maxz = [number(value) for value in bounds_high] # trimesh model or contour model? if isinstance(model, pycam.Geometry.Model.ContourModel): # contour model trimesh_models = [] contour_model = model else: # trimesh model trimesh_models = [model] contour_model = None # Due to some weirdness the height of the drill must be bigger than the # object's size. Otherwise some collisions are not detected. cutter_height = 4 * abs(maxz - minz) cutter = pycam.Cutters.get_tool_from_settings(tool_settings, cutter_height) if isinstance(cutter, basestring): return cutter if not path_generator in ("EngraveCutter", "ContourFollow"): # material allowance is not available for these two strategies cutter.set_required_distance(material_allowance) # create the grid model if requested if support_model: trimesh_models.append(support_model) # Adapt the contour_model to the engraving offset. This offset is # considered to be part of the material_allowance. if contour_model and (engrave_offset != 0): if not callback is None: callback(text="Preparing contour model with offset ...") contour_model = contour_model.get_offset_model(engrave_offset, callback=callback) if contour_model: return "Failed to calculate offset polygons" if not callback is None: # reset percentage counter after the contour model calculation callback(percent=0) if callback(text="Checking contour model with offset for " \ + "collisions ..."): # quit requested return None progress_callback = ProgressCounter( len(contour_model.get_polygons()), callback).increment else: progress_callback = None result = contour_model.check_for_collisions(callback=progress_callback) if result is None: return None elif result: warning = "The contour model contains colliding line groups. " + \ "This can cause problems with an engraving offset.\n" + \ "A collision was detected at (%.2f, %.2f, %.2f)." % \ (result.x, result.y, result.z) log.warning(warning) else: # no collisions and no user interruption pass # check the pocketing type if contour_model and (pocketing_type != "none"): if not callback is None: callback(text="Generating pocketing polygons ...") pocketing_offset = cutter.radius * 1.8 # TODO: this is an arbitrary limit to avoid infinite loops pocketing_limit = 1000 base_polygons = [] other_polygons = [] if pocketing_type == "holes": # fill polygons with negative area for poly in contour_model.get_polygons(): if poly.is_closed and not poly.is_outer(): base_polygons.append(poly) else: other_polygons.append(poly) elif pocketing_type == "enclosed": # fill polygons with positive area pocketing_offset *= -1 for poly in contour_model.get_polygons(): if poly.is_closed and poly.is_outer(): base_polygons.append(poly) else: other_polygons.append(poly) else: return "Unknown pocketing type given (not one of 'none', " + \ "'holes', 'enclosed'): %s" % str(pocketing_type) # For now we use only the polygons that do not surround eny other # polygons. Sorry - the pocketing is currently very simple ... base_filtered_polygons = [] for candidate in base_polygons: if callback and callback(): return "Interrupted" for other in other_polygons: if candidate.is_polygon_inside(other): break else: base_filtered_polygons.append(candidate) # start the pocketing for all remaining polygons pocket_polygons = [] for base_polygon in base_filtered_polygons: current_queue = [base_polygon] next_queue = [] pocket_depth = 0 while current_queue and (pocket_depth < pocketing_limit): if callback and callback(): return "Interrupted" for poly in current_queue: result = poly.get_offset_polygons(pocketing_offset) pocket_polygons.extend(result) next_queue.extend(result) pocket_depth += 1 current_queue = next_queue next_queue = [] # use a copy instead of the original contour_model = contour_model.get_copy() for pocket in pocket_polygons: contour_model.append(pocket) # limit the contour model to the bounding box if contour_model: # use minz/maxz of the contour model (in other words: ignore z) contour_model = contour_model.get_cropped_model( minx, maxx, miny, maxy, contour_model.minz, contour_model.maxz) if contour_model: return "No part of the contour model is within the bounding box." physics = _get_physics(trimesh_models, cutter, calculation_backend) if isinstance(physics, basestring): return physics generator = _get_pathgenerator_instance(trimesh_models, contour_model, cutter, path_generator, path_postprocessor, physics, milling_style) if isinstance(generator, basestring): return generator overlap = overlap_percent / 100.0 if (overlap < 0) or (overlap >= 1): return "Invalid overlap value (%f): should be greater or equal 0 " \ + "and lower than 1" # factor "2" since we are based on radius instead of diameter line_stepping = 2 * number(tool_settings["tool_radius"]) * (1 - overlap) if path_generator == "PushCutter": step_width = None else: # the step_width is only used for the DropCutter step_width = tool_settings["tool_radius"] / 4 if path_generator == "DropCutter": layer_distance = None else: layer_distance = step_down direction_dict = { "x": pycam.Toolpath.MotionGrid.GRID_DIRECTION_X, "y": pycam.Toolpath.MotionGrid.GRID_DIRECTION_Y, "xy": pycam.Toolpath.MotionGrid.GRID_DIRECTION_XY } milling_style_grid = { "ignore": pycam.Toolpath.MotionGrid.MILLING_STYLE_IGNORE, "conventional": pycam.Toolpath.MotionGrid.MILLING_STYLE_CONVENTIONAL, "climb": pycam.Toolpath.MotionGrid.MILLING_STYLE_CLIMB } if path_generator in ("DropCutter", "PushCutter"): motion_grid = pycam.Toolpath.MotionGrid.get_fixed_grid( (bounds_low, bounds_high), layer_distance, line_stepping, step_width=step_width, grid_direction=direction_dict[direction], milling_style=milling_style_grid[milling_style]) if path_generator == "DropCutter": toolpath = generator.GenerateToolPath(motion_grid, minz, maxz, callback) else: toolpath = generator.GenerateToolPath(motion_grid, callback) elif path_generator == "EngraveCutter": if step_down > 0: dz = step_down else: dz = maxz - minz toolpath = generator.GenerateToolPath(minz, maxz, step_width, dz, callback) elif path_generator == "ContourFollow": if step_down > 0: dz = step_down else: dz = maxz - minz if dz <= 0: dz = 1 toolpath = generator.GenerateToolPath(minx, maxx, miny, maxy, minz, maxz, dz, callback) else: return "Invalid path generator (%s): not one of %s" \ % (path_generator, PATH_GENERATORS) return toolpath
def GenerateToolPath(self, minz, maxz, horiz_step, dz, draw_callback=None): quit_requested = False # calculate the number of steps num_of_layers = 1 + ceil(abs(maxz - minz) / dz) if num_of_layers > 1: z_step = abs(maxz - minz) / (num_of_layers - 1) z_steps = [(maxz - i * z_step) for i in range(num_of_layers)] # The top layer is treated as the current surface - thus it does not # require engraving. z_steps = z_steps[1:] else: z_steps = [minz] num_of_layers = len(z_steps) current_layer = 0 num_of_lines = self.contour_model.get_num_of_lines() progress_counter = ProgressCounter(len(z_steps) * num_of_lines, draw_callback) if draw_callback: draw_callback(text="Engrave: optimizing polygon order") # Sort the polygons according to their directions (first inside, then # outside. This reduces the problem of break-away pieces. inner_polys = [] outer_polys = [] for poly in self.contour_model.get_polygons(): if poly.get_area() <= 0: inner_polys.append(poly) else: outer_polys.append(poly) inner_sorter = PolygonSorter(inner_polys, callback=draw_callback) outer_sorter = PolygonSorter(outer_polys, callback=draw_callback) line_groups = inner_sorter.get_polygons() + outer_sorter.get_polygons() if self.clockwise: for line_group in line_groups: line_group.reverse_direction() # push slices for all layers above ground if maxz == minz: # only one layer - use PushCutter instead of DropCutter # put "last_z" clearly above the model plane last_z = maxz + 1 push_steps = z_steps drop_steps = [] else: # multiple layers last_z = maxz push_steps = z_steps[:-1] drop_steps = [z_steps[-1]] for z in push_steps: # update the progress bar and check, if we should cancel the process if draw_callback and draw_callback(text="Engrave: processing" \ + " layer %d/%d" % (current_layer + 1, num_of_layers)): # cancel immediately break for line_group in line_groups: for line in line_group.get_lines(): self.GenerateToolPathLinePush(self.pa_push, line, z, last_z, draw_callback=draw_callback) if progress_counter.increment(): # cancel requested quit_requested = True # finish the current path self.pa_push.finish() break self.pa_push.finish() # break the outer loop if requested if quit_requested: break current_layer += 1 last_z = z if quit_requested: return self.pa_push.paths for z in drop_steps: if draw_callback: draw_callback(text="Engrave: processing layer %d/%d" \ % (current_layer + 1, num_of_layers)) # process the final layer with a drop cutter for line_group in line_groups: self.pa_drop.new_direction(0) self.pa_drop.new_scanline() for line in line_group.get_lines(): self.GenerateToolPathLineDrop(self.pa_drop, line, z, maxz, horiz_step, last_z, draw_callback=draw_callback) if progress_counter.increment(): # quit requested quit_requested = True break self.pa_drop.end_scanline() self.pa_drop.end_direction() # break the outer loop if requested if quit_requested: break current_layer += 1 last_z = z self.pa_drop.finish() return self.pa_push.paths + self.pa_drop.paths
def generate_toolpath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None): # Transfer the grid (a generator) into a list of lists and count the items. grid = [] num_of_grid_positions = 0 for layer in motion_grid: lines = [] for line in layer: # convert the generator to a list lines.append(list(line)) num_of_grid_positions += len(lines) grid.append(lines) num_of_layers = len(grid) progress_counter = ProgressCounter(num_of_grid_positions, draw_callback) current_layer = 0 if self.waterlines: self.pa = pycam.PathProcessors.ContourCutter.ContourCutter() else: path = [] for layer_grid in grid: # update the progress bar and check, if we should cancel the process if draw_callback and draw_callback( text=("PushCutter: processing layer %d/%d" % (current_layer + 1, num_of_layers))): # cancel immediately break if self.waterlines: self.pa.new_direction(0) result = self.generate_toolpath_slice(cutter, models, layer_grid, draw_callback, progress_counter) if self.waterlines: self.pa.end_direction() self.pa.finish() else: path.extend(result) current_layer += 1 if self.waterlines: # TODO: this is complicated and hacky :( # we don't use parallelism (for the sake of simplicity) result = [] # turn the waterline points into cutting segments for path in self.pa.paths: pairs = [] for index in range(len(path.points) - 1): pairs.append((path.points[index], path.points[index + 1])) if len(models) > 1: # We assume that the first model is used for the waterline and all # other models are obstacles (e.g. a support grid). other_models = models[1:] for p1, p2 in pairs: free_points = get_free_paths_triangles( other_models, cutter, p1, p2) for index in range(len(free_points) // 2): result.append(MoveStraight(free_points[2 * index])) result.append( MoveStraight(free_points[2 * index + 1])) result.append(MoveSafety()) else: for p1, p2 in pairs: result.append(MoveStraight(p1)) result.append(MoveStraight(p2)) result.append(MoveSafety()) return result else: return path