# -*- coding: utf-8 -*- """ $Id$ Copyright 2010 Lars Kruse <devel@sumpfralle.de> This file is part of PyCAM. PyCAM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. PyCAM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with PyCAM. If not, see <http://www.gnu.org/licenses/>. """ __all__ = ["simplify_toolpath", "ToolpathList", "Toolpath", "Generator"] from pycam.Geometry.Point import Point from pycam.Geometry.Path import Path from pycam.Geometry.Line import Line from pycam.Geometry.utils import number, epsilon import pycam.Utils.log import random import os log = pycam.Utils.log.get_logger() def _check_colinearity(p1, p2, p3): v1 = p2.sub(p1).normalized() v2 = p3.sub(p2).normalized() # compare if the normalized distances between p1-p2 and p2-p3 are equal return v1 == v2 def simplify_toolpath(path): """ remove multiple points in a line from a toolpath If A, B, C and D are on a straight line, then B and C will be removed. This reduces memory consumption and avoids a severe slow-down of the machine when moving along very small steps. The toolpath is simplified _in_place_. @value path: a single separate segment of a toolpath @type path: pycam.Geometry.Path.Path """ index = 1 points = path.points while index < len(points) - 1: if _check_colinearity(points[index-1], points[index], points[index+1]): points.pop(index) # don't increase the counter - otherwise we skip one point else: index += 1 class Toolpath(object): def __init__(self, paths, parameters=None): self.paths = paths if parameters: self.parameters = parameters else: self.parameters = {} self._max_safe_distance = 2 * parameters.get("tool_radius", 0) self._feedrate = parameters.get("tool_feedrate", 300) def get_params(self): return dict(self.parameters) def copy(self): new_paths = [] for path in self.paths: new_path = Path() for point in path.points: new_path.append(point) new_paths.append(new_path) return Toolpath(new_paths, parameters=self.get_params()) def _get_limit_generic(self, attr, func): path_min = [] for path in self.paths: if path.points: path_min.append(func([getattr(p, attr) for p in path.points])) return func(path_min) @property def minx(self): return self._get_limit_generic("x", min) @property def maxx(self): return self._get_limit_generic("x", max) @property def miny(self): return self._get_limit_generic("y", min) @property def maxy(self): return self._get_limit_generic("y", max) @property def minz(self): return self._get_limit_generic("z", min) @property def maxz(self): return self._get_limit_generic("z", max) def get_meta_data(self): meta = self.toolpath_settings.get_string() start_marker = self.toolpath_settings.META_MARKER_START end_marker = self.toolpath_settings.META_MARKER_END return os.linesep.join((start_marker, meta, end_marker)) def get_moves(self, safety_height, max_movement=None): class MoveContainer(object): def __init__(self, max_movement): self.max_movement = max_movement self.moved_distance = 0 self.moves = [] self.last_pos = None if max_movement is None: self.append = self.append_without_movement_limit else: self.append = self.append_with_movement_limit def append_with_movement_limit(self, new_position, rapid): if self.last_pos is None: # first move with unknown start position - ignore it self.moves.append((new_position, rapid)) self.last_pos = new_position return True else: distance = new_position.sub(self.last_pos).norm if self.moved_distance + distance > self.max_movement: partial = (self.max_movement - self.moved_distance) / \ distance partial_dest = self.last_pos.add(new_position.sub( self.last_pos).mul(partial)) self.moves.append((partial_dest, rapid)) self.last_pos = partial_dest # we are finished return False else: self.moves.append((new_position, rapid)) self.moved_distance += distance self.last_pos = new_position return True def append_without_movement_limit(self, new_position, rapid): self.moves.append((new_position, rapid)) return True p_last = None result = MoveContainer(max_movement) for path in self.paths: if not path: # ignore empty paths continue p_next = path.points[0] if p_last is None: p_last = Point(p_next.x, p_next.y, safety_height) if not result.append(p_last, True): return result.moves if ((abs(p_last.x - p_next.x) > epsilon) \ or (abs(p_last.y - p_next.y) > epsilon)): # Draw the connection between the last and the next path. # Respect the safety height. if (abs(p_last.z - p_next.z) > epsilon) \ or (p_last.sub(p_next).norm > \ self._max_safe_distance + epsilon): # The distance between these two points is too far. # This condition helps to prevent moves up/down for # adjacent lines. safety_last = Point(p_last.x, p_last.y, safety_height) safety_next = Point(p_next.x, p_next.y, safety_height) if not result.append(safety_last, True): return result.moves if not result.append(safety_next, True): return result.moves for p in path.points: if not result.append(p, False): return result.moves p_last = path.points[-1] if not p_last is None: p_last_safety = Point(p_last.x, p_last.y, safety_height) result.append(p_last_safety, True) return result.moves def get_machine_time(self, safety_height=0.0): """ calculate an estimation of the time required for processing the toolpath with the machine @value safety_height: the safety height configured for this toolpath @type safety_height: float @rtype: float @returns: the machine time used for processing the toolpath in minutes """ result = 0 safety_height = number(safety_height) current_position = None # go through all points of the path for new_pos, rapid in self.get_moves(safety_height): if not current_position is None: result += new_pos.sub(current_position).norm / self._feedrate current_position = new_pos return result def get_machine_movement_distance(self, safety_height=0.0): result = 0 safety_height = number(safety_height) current_position = None # go through all points of the path for new_pos, rapid in self.get_moves(safety_height): if not current_position is None: result += new_pos.sub(current_position).norm current_position = new_pos return result def get_cropped_copy(self, polygons, callback=None): # create a deep copy of the current toolpath tp = self.copy() tp.crop(polygons, callback=callback) return tp def crop(self, polygons, callback=None): # collect all existing toolpath lines open_lines = [] for path in self.paths: if path: for index in range(len(path.points) - 1): open_lines.append(Line(path.points[index], path.points[index + 1])) # go through all polygons and add "inner" lines (or parts thereof) to # the final list of remaining lines inner_lines = [] for polygon in polygons: new_open_lines = [] for line in open_lines: if callback and callback(): return inner, outer = polygon.split_line(line) inner_lines.extend(inner) new_open_lines.extend(outer) open_lines = new_open_lines # turn all "inner_lines" into toolpath moves new_paths = [] current_path = Path() if inner_lines: line = inner_lines.pop(0) current_path.append(line.p1) current_path.append(line.p2) while inner_lines: if callback and callback(): return end = current_path.points[-1] # look for the next connected point for line in inner_lines: if line.p1 == end: inner_lines.remove(line) current_path.append(line.p2) break else: new_paths.append(current_path) current_path = Path() line = inner_lines.pop(0) current_path.append(line.p1) current_path.append(line.p2) if current_path.points: new_paths.append(current_path) self.paths = new_paths class Bounds(object): TYPE_RELATIVE_MARGIN = 0 TYPE_FIXED_MARGIN = 1 TYPE_CUSTOM = 2 def __init__(self, bounds_type=None, bounds_low=None, bounds_high=None, reference=None): """ create a new Bounds instance @value bounds_type: any of TYPE_RELATIVE_MARGIN | TYPE_FIXED_MARGIN | TYPE_CUSTOM @type bounds_type: int @value bounds_low: the lower margin of the boundary compared to the reference object (for TYPE_RELATIVE_MARGIN | TYPE_FIXED_MARGIN) or the specific boundary values (for TYPE_CUSTOM). Only the lower values of the three axes (x, y and z) are given. @type bounds_low: (tuple|list) of float @value bounds_high: see 'bounds_low' @type bounds_high: (tuple|list) of float @value reference: optional default reference Bounds instance @type reference: Bounds """ self.name = "No name" # set type self.bounds_type = None if bounds_type is None: self.set_type(Bounds.TYPE_CUSTOM) else: self.set_type(bounds_type) # store the bounds values self.bounds_low = None self.bounds_high = None if bounds_low is None: bounds_low = [0, 0, 0] if bounds_high is None: bounds_high = [0, 0, 0] self.set_bounds(bounds_low, bounds_high) self.reference = reference def __repr__(self): bounds_type_labels = ("relative", "fixed", "custom") return "Bounds(%s, %s, %s)" % (bounds_type_labels[self.bounds_type], self.bounds_low, self.bounds_high) def is_valid(self): for index in range(3): if self.bounds_low[index] > self.bounds_high[index]: return False else: return True def set_reference(self, reference): self.reference = reference def set_name(self, name): self.name = name def get_name(self): return self.name def get_type(self): return self.bounds_type def set_type(self, bounds_type): # complain if an unknown bounds_type value was given if not bounds_type in (Bounds.TYPE_RELATIVE_MARGIN, Bounds.TYPE_FIXED_MARGIN, Bounds.TYPE_CUSTOM): raise ValueError, "failed to create an instance of " \ + "pycam.Toolpath.Bounds due to an invalid value of " \ + "'bounds_type': %s" % repr(bounds_type) else: self.bounds_type = bounds_type def get_referenced_bounds(self, reference): return Bounds(bounds_type=self.bounds_type, bounds_low=self.bounds_low, bounds_high=self.bounds_high, reference=reference) def get_bounds(self): return self.bounds_low[:], self.bounds_high[:] def set_bounds(self, low=None, high=None): if not low is None: if len(low) != 3: raise ValueError, "lower bounds should be supplied as a " \ + "tuple/list of 3 items - but %d were given" % len(low) else: self.bounds_low = [number(value) for value in low] if not high is None: if len(high) != 3: raise ValueError, "upper bounds should be supplied as a " \ + "tuple/list of 3 items - but %d were given" \ % len(high) else: self.bounds_high = [number(value) for value in high] def get_absolute_limits(self, reference=None): """ calculate the current absolute limits of the Bounds instance @value reference: a reference object described by a tuple (or list) of three item. These three values describe only the lower boundary of this object (for the x, y and z axes). Each item must be a float value. This argument is ignored for the boundary type "TYPE_CUSTOM". @type reference: (tuple|list) of float @returns: a tuple of two lists containg the low and high limits @rvalue: tuple(list) """ # use the default reference if none was given if reference is None: reference = self.reference # check if a reference is given (if necessary) if self.bounds_type \ in (Bounds.TYPE_RELATIVE_MARGIN, Bounds.TYPE_FIXED_MARGIN): if reference is None: raise ValueError, "any non-custom boundary definition " \ + "requires a reference object for caluclating " \ + "absolute limits" else: ref_low, ref_high = reference.get_absolute_limits() low = [None] * 3 high = [None] * 3 # calculate the absolute limits if self.bounds_type == Bounds.TYPE_RELATIVE_MARGIN: for index in range(3): dim_width = ref_high[index] - ref_low[index] low[index] = ref_low[index] \ - self.bounds_low[index] * dim_width high[index] = ref_high[index] \ + self.bounds_high[index] * dim_width elif self.bounds_type == Bounds.TYPE_FIXED_MARGIN: for index in range(3): low[index] = ref_low[index] - self.bounds_low[index] high[index] = ref_high[index] + self.bounds_high[index] elif self.bounds_type == Bounds.TYPE_CUSTOM: for index in range(3): low[index] = number(self.bounds_low[index]) high[index] = number(self.bounds_high[index]) else: # this should not happen raise NotImplementedError, "the function 'get_absolute_limits' is" \ + " currently not implemented for the bounds_type " \ + "'%s'" % str(self.bounds_type) return low, high def adjust_bounds_to_absolute_limits(self, limits_low, limits_high, reference=None): """ change the current bounds settings according to some absolute values This does not change the type of this bounds instance (e.g. relative). @value limits_low: a tuple describing the new lower absolute boundary @type limits_low: (tuple|list) of float @value limits_high: a tuple describing the new lower absolute boundary @type limits_high: (tuple|list) of float @value reference: a reference object described by a tuple (or list) of three item. These three values describe only the lower boundary of this object (for the x, y and z axes). Each item must be a float value. This argument is ignored for the boundary type "TYPE_CUSTOM". @type reference: (tuple|list) of float """ # use the default reference if none was given if reference is None: reference = self.reference # check if a reference is given (if necessary) if self.bounds_type \ in (Bounds.TYPE_RELATIVE_MARGIN, Bounds.TYPE_FIXED_MARGIN): if reference is None: raise ValueError, "any non-custom boundary definition " \ + "requires an a reference object for caluclating " \ + "absolute limits" else: ref_low, ref_high = reference.get_absolute_limits() # calculate the new settings if self.bounds_type == Bounds.TYPE_RELATIVE_MARGIN: for index in range(3): dim_width = ref_high[index] - ref_low[index] if dim_width == 0: # We always loose relative margins if the specific dimension # is zero. There is no way to avoid this. message = "Non-zero %s boundary lost during conversion " \ + "to relative margins due to zero size " \ + "dimension '%s'." % "xyz"[index] # Display warning messages, if we can't reach the requested # absolute dimension. if ref_low[index] != limits_low[index]: log.info(message % "lower") if ref_high[index] != limits_high[index]: log.info(message % "upper") self.bounds_low[index] = 0 self.bounds_high[index] = 0 else: self.bounds_low[index] = \ (ref_low[index] - limits_low[index]) / dim_width self.bounds_high[index] = \ (limits_high[index] - ref_high[index]) / dim_width elif self.bounds_type == Bounds.TYPE_FIXED_MARGIN: for index in range(3): self.bounds_low[index] = ref_low[index] - limits_low[index] self.bounds_high[index] = limits_high[index] - ref_high[index] elif self.bounds_type == Bounds.TYPE_CUSTOM: for index in range(3): self.bounds_low[index] = limits_low[index] self.bounds_high[index] = limits_high[index] else: # this should not happen raise NotImplementedError, "the function " \ + "'adjust_bounds_to_absolute_limits' is currently not " \ + "implemented for the bounds_type '%s'" \ % str(self.bounds_type)