def draw_axes(settings): GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() # GL.glTranslatef(0, 0, -2) size_x = abs(settings.get("maxx")) size_y = abs(settings.get("maxy")) size_z = abs(settings.get("maxz")) size = number(1.7) * max(size_x, size_y, size_z) # the divider is just based on playing with numbers scale = size / number(1500.0) string_distance = number(1.1) * size GL.glBegin(GL.GL_LINES) GL.glColor3f(1, 0, 0) GL.glVertex3f(0, 0, 0) GL.glVertex3f(size, 0, 0) GL.glEnd() draw_string(string_distance, 0, 0, "xy", "X", scale=scale) GL.glBegin(GL.GL_LINES) GL.glColor3f(0, 1, 0) GL.glVertex3f(0, 0, 0) GL.glVertex3f(0, size, 0) GL.glEnd() draw_string(0, string_distance, 0, "yz", "Y", scale=scale) GL.glBegin(GL.GL_LINES) GL.glColor3f(0, 0, 1) GL.glVertex3f(0, 0, 0) GL.glVertex3f(0, 0, size) GL.glEnd() draw_string(0, 0, string_distance, "xz", "Z", scale=scale)
def get_rotation_matrix_axis_angle(rot_axis, rot_angle, use_radians=True): """ calculate rotation matrix for a normalized vector and an angle see http://mathworld.wolfram.com/RotationMatrix.html @type rot_axis: tuple(float) @value rot_axis: the vector describes the rotation axis. Its length should be 1.0 (normalized). @type rot_angle: float @value rot_angle: rotation angle (radiant) @rtype: tuple(tuple(float)) @return: the roation matrix (3x3) """ if not use_radians: rot_angle *= math.pi / 180 sin = number(math.sin(rot_angle)) cos = number(math.cos(rot_angle)) return ((cos + rot_axis[0] * rot_axis[0] * (1 - cos), rot_axis[0] * rot_axis[1] * (1 - cos) - rot_axis[2] * sin, rot_axis[0] * rot_axis[2] * (1 - cos) + rot_axis[1] * sin), (rot_axis[1] * rot_axis[0] * (1 - cos) + rot_axis[2] * sin, cos + rot_axis[1] * rot_axis[1] * (1 - cos), rot_axis[1] * rot_axis[2] * (1 - cos) - rot_axis[0] * sin), (rot_axis[2] * rot_axis[0] * (1 - cos) - rot_axis[1] * sin, rot_axis[2] * rot_axis[1] * (1 - cos) + rot_axis[0] * sin, cos + rot_axis[2] * rot_axis[2] * (1 - cos)))
def draw_axes(settings): GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() #GL.glTranslatef(0, 0, -2) size_x = abs(settings.get("maxx")) size_y = abs(settings.get("maxy")) size_z = abs(settings.get("maxz")) size = number(1.7) * max(size_x, size_y, size_z) # the divider is just based on playing with numbers scale = size / number(1500.0) string_distance = number(1.1) * size GL.glBegin(GL.GL_LINES) GL.glColor3f(1, 0, 0) GL.glVertex3f(0, 0, 0) GL.glVertex3f(size, 0, 0) GL.glEnd() draw_string(string_distance, 0, 0, 'xy', "X", scale=scale) GL.glBegin(GL.GL_LINES) GL.glColor3f(0, 1, 0) GL.glVertex3f(0, 0, 0) GL.glVertex3f(0, size, 0) GL.glEnd() draw_string(0, string_distance, 0, 'yz', "Y", scale=scale) GL.glBegin(GL.GL_LINES) GL.glColor3f(0, 0, 1) GL.glVertex3f(0, 0, 0) GL.glVertex3f(0, 0, size) GL.glEnd() draw_string(0, 0, string_distance, 'xz', "Z", scale=scale)
def move_camera_by_screen(self, x_move, y_move, max_model_shift): """ move the camera acoording to a mouse movement @type x_move: int @value x_move: movement of the mouse along the x axis @type y_move: int @value y_move: movement of the mouse along the y axis @type max_model_shift: float @value max_model_shift: maximum shifting of the model view (e.g. for x_move == screen width) """ factors_x, factors_y = self._get_axes_vectors() width, height = self._get_screen_dimensions() # relation of x/y movement to the respective screen dimension win_x_rel = (-2 * x_move) / float(width) / math.sin(self.view["fovy"]) win_y_rel = (-2 * y_move) / float(height) / math.sin(self.view["fovy"]) # This code is completely arbitrarily based on trial-and-error for # finding a nice movement speed for all distances. # Anyone with a better approach should just fix this. distance_vector = self.get("distance") distance = float(sqrt(sum([dim**2 for dim in distance_vector]))) win_x_rel *= math.cos(win_x_rel / distance)**20 win_y_rel *= math.cos(win_y_rel / distance)**20 # update the model position that should be centered on the screen old_center = self.view["center"] new_center = [] for i in range(3): new_center.append(old_center[i] \ + max_model_shift * (number(win_x_rel) * factors_x[i] \ + number(win_y_rel) * factors_y[i])) self.view["center"] = tuple(new_center)
def move_camera_by_screen(self, x_move, y_move, max_model_shift): """ move the camera acoording to a mouse movement @type x_move: int @value x_move: movement of the mouse along the x axis @type y_move: int @value y_move: movement of the mouse along the y axis @type max_model_shift: float @value max_model_shift: maximum shifting of the model view (e.g. for x_move == screen width) """ factors_x, factors_y = self._get_axes_vectors() width, height = self._get_screen_dimensions() # relation of x/y movement to the respective screen dimension win_x_rel = (-2 * x_move) / float(width) / math.sin(self.view["fovy"]) win_y_rel = (-2 * y_move) / float(height) / math.sin(self.view["fovy"]) # This code is completely arbitrarily based on trial-and-error for # finding a nice movement speed for all distances. # Anyone with a better approach should just fix this. distance_vector = self.get("distance") distance = float(sqrt(sum([dim ** 2 for dim in distance_vector]))) win_x_rel *= math.cos(win_x_rel / distance) ** 20 win_y_rel *= math.cos(win_y_rel / distance) ** 20 # update the model position that should be centered on the screen old_center = self.view["center"] new_center = [] for i in range(3): new_center.append(old_center[i] \ + max_model_shift * (number(win_x_rel) * factors_x[i] \ + number(win_y_rel) * factors_y[i])) self.view["center"] = tuple(new_center)
def __init__(self, x, y, z): self.id = Point.id Point.id += 1 self.x = number(x) self.y = number(y) self.z = number(z) self.reset_cache()
def get_rotation_matrix_axis_angle(rot_axis, rot_angle, use_radians=True): """ calculate rotation matrix for a normalized vector and an angle see http://mathworld.wolfram.com/RotationMatrix.html @type rot_axis: tuple(float) @value rot_axis: the vector describes the rotation axis. Its length should be 1.0 (normalized). @type rot_angle: float @value rot_angle: rotation angle (radiant) @rtype: tuple(tuple(float)) @return: the roation matrix (3x3) """ if not use_radians: rot_angle *= math.pi / 180 sin = number(math.sin(rot_angle)) cos = number(math.cos(rot_angle)) return ((cos + rot_axis[0]*rot_axis[0]*(1-cos), rot_axis[0]*rot_axis[1]*(1-cos) - rot_axis[2]*sin, rot_axis[0]*rot_axis[2]*(1-cos) + rot_axis[1]*sin), (rot_axis[1]*rot_axis[0]*(1-cos) + rot_axis[2]*sin, cos + rot_axis[1]*rot_axis[1]*(1-cos), rot_axis[1]*rot_axis[2]*(1-cos) - rot_axis[0]*sin), (rot_axis[2]*rot_axis[0]*(1-cos) - rot_axis[1]*sin, rot_axis[2]*rot_axis[1]*(1-cos) + rot_axis[0]*sin, cos + rot_axis[2]*rot_axis[2]*(1-cos)))
def _get_axes_vectors(self): """calculate the model vectors along the screen's x and y axes""" # The "up" vector defines, in what proportion each axis of the model is # in line with the screen's y axis. v_up = self.view["up"] factors_y = (number(v_up[0]), number(v_up[1]), number(v_up[2])) # Calculate the proportion of each model axis according to the x axis of # the screen. distv = self.view["distance"] distv = pnormalized((distv[0], distv[1], distv[2])) factors_x = pnormalized(pcross(distv, (v_up[0], v_up[1], v_up[2]))) return (factors_x, factors_y)
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 __init__(self, radius, location=None, height=None): super(BaseCutter, self).__init__() if location is None: location = (0, 0, 0) if height is None: height = 10 radius = number(radius) self.height = number(height) self.radius = radius self.radiussq = radius**2 self.required_distance = 0 self.distance_radius = self.radius self.distance_radiussq = self.distance_radius**2 self.shape = {} self.location = location self.moveto(self.location) self.uuid = None self.update_uuid()
def __init__(self, radius, location=None, height=None): super(BaseCutter, self).__init__() if location is None: location = Point(0, 0, 0) if height is None: height = 10 radius = number(radius) self.height = number(height) self.radius = radius self.radiussq = radius ** 2 self.required_distance = 0 self.distance_radius = self.radius self.distance_radiussq = self.distance_radius ** 2 self.shape = {} self.location = location self.moveto(self.location) self.uuid = None self.update_uuid()
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_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 feedrate = self.toolpath_settings.get_tool_settings()["feedrate"] feedrate = number(feedrate) 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 / feedrate current_position = new_pos return result
def multiply_vector_matrix(v, m): """ Multiply a 3d vector with a 3x3 matrix. The result is a 3d vector. @type v: tuple(float) | list(float) @value v: a 3d vector as tuple or list containing three floats @type m: tuple(tuple(float)) | list(list(float)) @value m: a 3x3 list/tuple of floats @rtype: tuple(float) @return: a tuple of 3 floats as the matrix product """ if len(m) == 9: m = [number(value) for value in m] m = ((m[0], m[1], m[2]), (m[3], m[4], m[5]), (m[6], m[7], m[8])) else: new_m = [] for column in m: new_m.append([number(value) for value in column]) v = [number(value) for value in v] return (v[0] * m[0][0] + v[1] * m[0][1] + v[2] * m[0][2], v[0] * m[1][0] + v[1] * m[1][1] + v[2] * m[1][2], v[0] * m[2][0] + v[1] * m[2][1] + v[2] * m[2][2])
def get_support_grid(minx, maxx, miny, maxy, z_plane, dist_x, dist_y, thickness, height, offset_x=0.0, offset_y=0.0, adjustments_x=None, adjustments_y=None): lines_x, lines_y = get_support_grid_locations(minx, maxx, miny, maxy, dist_x, dist_y, offset_x, offset_y, adjustments_x, adjustments_y) # create all x grid lines grid_model = Model() # convert all inputs to "number" thickness = number(thickness) height = number(height) # helper variables thick_half = thickness / 2 length_extension = max(thickness, height) for line_x in lines_x: # we make the grid slightly longer (by thickness) than necessary grid_model += _add_aligned_cuboid_to_model(line_x - thick_half, line_x + thick_half, miny - length_extension, maxy + length_extension, z_plane, z_plane + height) for line_y in lines_y: # we make the grid slightly longer (by thickness) than necessary grid_model += _add_aligned_cuboid_to_model(minx - length_extension, maxx + length_extension, line_y - thick_half, line_y + thick_half, z_plane, z_plane + height) return grid_model
def set_drill(self, shape, position): #geom = ode.GeomTransform(self._space) #geom.setOffset(position) #geom.setGeom(shape) #shape.setOffset(position) self._space.add(shape) # sadly PyODE forgets to update the "space" attribute that we need in # the cutters' "extend" functions shape.space = self._space self._add_geom(shape, position, append=False) self._drill_offset = [number(value) for value in position] self._drill = shape self.reset_drill() self._dirty = True
def __init__(self, radius, minorradius, **kwargs): minorradius = number(minorradius) self.minorradius = minorradius # we need "minorradius" for "moveto" - thus set it before parent's init BaseCutter.__init__(self, radius, **kwargs) self.majorradius = self.radius - minorradius self.axis = (0, 0, 1) self.majorradiussq = self.majorradius**2 self.minorradiussq = self.minorradius**2 self.distance_majorradius = self.majorradius \ + self.get_required_distance() self.distance_minorradius = self.minorradius \ + self.get_required_distance() self.distance_majorradiussq = self.distance_majorradius**2 self.distance_minorradiussq = self.distance_minorradius**2
def __init__(self, radius, minorradius, **kwargs): minorradius = number(minorradius) self.minorradius = minorradius # we need "minorradius" for "moveto" - thus set it before parent's init BaseCutter.__init__(self, radius, **kwargs) self.majorradius = self.radius - minorradius self.axis = (0, 0, 1) self.majorradiussq = self.majorradius ** 2 self.minorradiussq = self.minorradius ** 2 self.distance_majorradius = self.majorradius \ + self.get_required_distance() self.distance_minorradius = self.minorradius \ + self.get_required_distance() self.distance_majorradiussq = self.distance_majorradius ** 2 self.distance_minorradiussq = self.distance_minorradius ** 2
def auto_adjust_distance(self): s = self.core v = self.view # adjust the distance to get a view of the whole object low_high = zip(*self._get_low_high_dims()) if (None, None) in low_high: return max_dim = max([high - low for low, high in low_high]) distv = pnormalized((v["distance"][0], v["distance"][1], v["distance"][2])) # The multiplier "1.25" is based on experiments. 1.414 (sqrt(2)) should # be roughly sufficient for showing the diagonal of any model. distv = pmul(distv, (max_dim * 1.25) / number(math.sin(v["fovy"] / 2))) self.view["distance"] = distv # Adjust the "far" distance for the camera to make sure, that huge # models (e.g. x=1000) are still visible. self.view["zfar"] = 100 * max_dim
def auto_adjust_distance(self): s = self.settings v = self.view # adjust the distance to get a view of the whole object dimx = s.get("maxx") - s.get("minx") dimy = s.get("maxy") - s.get("miny") dimz = s.get("maxz") - s.get("minz") max_dim = max(max(dimx, dimy), dimz) distv = Point(v["distance"][0], v["distance"][1], v["distance"][2]).normalized() # The multiplier "1.25" is based on experiments. 1.414 (sqrt(2)) should # be roughly sufficient for showing the diagonal of any model. distv = distv.mul((max_dim * 1.25) / number(math.sin(v["fovy"] / 2))) self.view["distance"] = (distv.x, distv.y, distv.z) # Adjust the "far" distance for the camera to make sure, that huge # models (e.g. x=1000) are still visible. self.view["zfar"] = 100 * max_dim
def auto_adjust_distance(self): s = self.core v = self.view # adjust the distance to get a view of the whole object low_high = zip(*self._get_low_high_dims()) if (None, None) in low_high: return max_dim = max([high - low for low, high in low_high]) distv = pnormalized( (v["distance"][0], v["distance"][1], v["distance"][2])) # The multiplier "1.25" is based on experiments. 1.414 (sqrt(2)) should # be roughly sufficient for showing the diagonal of any model. distv = pmul(distv, (max_dim * 1.25) / number(math.sin(v["fovy"] / 2))) self.view["distance"] = distv # Adjust the "far" distance for the camera to make sure, that huge # models (e.g. x=1000) are still visible. self.view["zfar"] = 100 * max_dim
def get_support_grid_locations(minx, maxx, miny, maxy, dist_x, dist_y, offset_x=0.0, offset_y=0.0, adjustments_x=None, adjustments_y=None): def get_lines(center, dist, min_value, max_value): """ generate a list of positions starting from the middle going up and and down """ if dist > 0: lines = [center] current = center while current - dist > min_value: current -= dist lines.insert(0, current) current = center while current + dist < max_value: current += dist lines.append(current) else: lines = [] # remove lines that are out of range (e.g. due to a huge offset) lines = [line for line in lines if min_value < line < max_value] return lines # convert all inputs to the type defined in "number" dist_x = number(dist_x) dist_y = number(dist_y) offset_x = number(offset_x) offset_y = number(offset_y) center_x = (maxx + minx) / 2 + offset_x center_y = (maxy + miny) / 2 + offset_y lines_x = get_lines(center_x, dist_x, minx, maxx) lines_y = get_lines(center_y, dist_y, miny, maxy) if adjustments_x: for index in range(min(len(lines_x), len(adjustments_x))): lines_x[index] += number(adjustments_x[index]) if adjustments_y: for index in range(min(len(lines_y), len(adjustments_y))): lines_y[index] += number(adjustments_y[index]) return lines_x, lines_y
def pdiv(a, c): c = number(c) return (a[0] / c, a[1] / c, a[2] / c)
def mul(self, c): c = number(c) return Point(self.x * c, self.y * c, self.z * c)
def scale_distance(self, scale): if scale != 0: scale = number(scale) dist = self.view["distance"] self.view["distance"] = (scale * dist[0], scale * dist[1], scale * dist[2])
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_grid_type=None, support_grid_distance_x=None, support_grid_distance_y=None, support_grid_thickness=None, support_grid_height=None, support_grid_offset_x=None, support_grid_offset_y=None, support_grid_adjustments_x=None, support_grid_adjustments_y=None, support_grid_average_distance=None, support_grid_minimum_bridges=None, support_grid_length=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 support_grid_distance_x: float @value support_grid_distance_x: distance between support grid lines along x @type support_grid_distance_y: float @value support_grid_distance_y: distance between support grid lines along y @type support_grid_thickness: float @value support_grid_thickness: thickness of the support grid @type support_grid_height: float @value support_grid_height: height of the support grid @type support_grid_offset_x: float @value support_grid_offset_x: shift the support grid by this value along x @type support_grid_offset_y: float @value support_grid_offset_y: shift the support grid by this value along y @type support_grid_adjustments_x: list(float) @value support_grid_adjustments_x: manual adjustment of each x-grid bar @type support_grid_adjustments_y: list(float) @value support_grid_adjustments_y: manual adjustment of each y-grid bar @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_grid_type == "grid") \ and (((not support_grid_distance_x is None) \ or (not support_grid_distance_y is None)) \ and (not support_grid_thickness is None)): # grid height defaults to the thickness if support_grid_height is None: support_grid_height = support_grid_thickness if (support_grid_distance_x < 0) or (support_grid_distance_y < 0): return "The distance of the support grid must be a positive value" if not ((support_grid_distance_x > 0) or (support_grid_distance_y > 0)): return "Both distance values for the support grid may not be " \ + "zero at the same time" if support_grid_thickness <= 0: return "The thickness of the support grid must be a positive value" if support_grid_height <= 0: return "The height of the support grid must be a positive value" if not callback is None: callback(text="Preparing support grid model ...") support_grid_model = pycam.Toolpath.SupportGrid.get_support_grid( minx, maxx, miny, maxy, minz, support_grid_distance_x, support_grid_distance_y, support_grid_thickness, support_grid_height, offset_x=support_grid_offset_x, offset_y=support_grid_offset_y, adjustments_x=support_grid_adjustments_x, adjustments_y=support_grid_adjustments_y) trimesh_models.append(support_grid_model) elif (support_grid_type in ("distributed_edges", "distributed_corners")) \ and (not support_grid_average_distance is None) \ and (not support_grid_thickness is None) \ and (not support_grid_length is None): if support_grid_height is None: support_grid_height = support_grid_thickness if support_grid_minimum_bridges is None: support_grid_minimum_bridges = 2 if support_grid_average_distance <= 0: return "The average support grid distance must be a positive value" if support_grid_minimum_bridges <= 0: return "The minimum number of bridged per polygon must be a " \ + "positive value" if support_grid_thickness <= 0: return "The thickness of the support grid must be a positive value" if support_grid_height <= 0: return "The height of the support grid must be a positive value" if not callback is None: callback(text="Preparing support grid model ...") # check which model to choose if contour_model: model = contour_model else: model = trimesh_models[0] start_at_corners = (support_grid_type == "distributed_corners") support_grid_model = pycam.Toolpath.SupportGrid.get_support_distributed( model, minz, support_grid_average_distance, support_grid_minimum_bridges, support_grid_thickness, support_grid_height, support_grid_length, bounds, start_at_corners=start_at_corners) trimesh_models.append(support_grid_model) elif (not support_grid_type) or (support_grid_type == "none"): pass else: return "Invalid support grid type selected: %s" % support_grid_type # 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 not 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 not 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, 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) elif path_generator == "Contour2dCutter": # JULIEN toolpath = generator.GenerateToolPath(callback) else: return "Invalid path generator (%s): not one of %s" \ % (path_generator, PATH_GENERATORS) return toolpath
def pmul(a, c): c = number(c) return (a[0] * c, a[1] * c, a[2] * c)
def div(self, c): c = number(c) return Point(self.x / c, self.y / c, self.z / c)
def set_required_distance(self, value): if value >= 0: self.required_distance = number(value) self.distance_radius = self.radius + self.get_required_distance() self.distance_radiussq = self.distance_radius * self.distance_radius self.update_uuid()
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 __init__(self, x, y, z): super(Point, self).__init__() self.x = number(x) self.y = number(y) self.z = number(z) self.reset_cache()