Commit 8aceb43d authored by Lars Kruse's avatar Lars Kruse

replaced toolpath handling with a more flexible approach

Toolpaths ("moves") are now created as simple lists containing a tuple
of two values:
- a type identifier (numeric constant) - e.g. "move", "move rapid", "safety move"
- arguments (can be a tuple, as well)

This structure allows the use of "filters" for GCode generation.
A simple example:
A "laser" GCode generator will turn all "safety move" items into
"laser power on" and "laser power off" respectively.
A milling machine GCode generator will turn all "safety move" items into
rapid moves up, then sideways moves and then slow moves down to the next
destination.

Currently the following filters are implemented:
- skip a safety move if a short sideways move does no harm (e.g. zigzag mode)
- replace safety moves with the proper combination of normal and rapid moves
- a simple example filter for machine settings (feedrate, metric system, ...)
Tool change and touch off, as well as the missing startup settings are still
open.
parent 45749e8b
...@@ -26,7 +26,7 @@ from pycam.Utils import ProgressCounter ...@@ -26,7 +26,7 @@ from pycam.Utils import ProgressCounter
from pycam.Utils.threading import run_in_parallel from pycam.Utils.threading import run_in_parallel
import pycam.Geometry.Model import pycam.Geometry.Model
import pycam.Utils.log import pycam.Utils.log
from pycam.Toolpath import MOVE_STRAIGHT, MOVE_SAFETY
log = pycam.Utils.log.get_logger() log = pycam.Utils.log.get_logger()
...@@ -43,60 +43,55 @@ def _process_one_grid_line((positions, minz, maxz, model, cutter, physics)): ...@@ -43,60 +43,55 @@ def _process_one_grid_line((positions, minz, maxz, model, cutter, physics)):
class DropCutter(object): class DropCutter(object):
def __init__(self, path_processor, physics=None): def __init__(self, physics=None):
self.pa = path_processor
self.physics = physics self.physics = physics
def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None): def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None):
path = []
quit_requested = False quit_requested = False
model = pycam.Geometry.Model.get_combined_model(models) model = pycam.Geometry.Model.get_combined_model(models)
# Transfer the grid (a generator) into a list of lists and count the # Transfer the grid (a generator) into a list of lists and count the
# items. # items.
# usually there is only one layer - but an xy-grid consists of two
lines = [] lines = []
# usually there is only one layer - but an xy-grid consists of two
for layer in motion_grid: for layer in motion_grid:
lines.extend(layer) for line in layer:
lines.append(line)
num_of_lines = len(lines) num_of_lines = len(lines)
progress_counter = ProgressCounter(len(lines), draw_callback) progress_counter = ProgressCounter(len(lines), draw_callback)
current_line = 0 current_line = 0
self.pa.new_direction(0)
args = [] args = []
for one_grid_line in lines: for one_grid_line in lines:
args.append(([(x,y) for x,y,z in one_grid_line], minz, maxz, model, cutter, self.physics)) # 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, for points in run_in_parallel(_process_one_grid_line, args,
callback=progress_counter.update): callback=progress_counter.update):
self.pa.new_scanline() if draw_callback and draw_callback(text="DropCutter: processing " \
if draw_callback and draw_callback(text="DropCutter: processing line %d/%d" + "line %d/%d" % (current_line + 1, num_of_lines)):
% (current_line + 1, num_of_lines)):
# cancel requested # cancel requested
quit_requested = True quit_requested = True
break break
for point in points: for point in points:
if point is None: if point is None:
# exceeded maxz - the cutter has to skip this point # exceeded maxz - the cutter has to skip this point
self.pa.end_scanline() path.append((MOVE_SAFETY))
self.pa.new_scanline()
continue continue
self.pa.append(point) path.append((MOVE_STRAIGHT, point))
# "draw_callback" returns true, if the user requested to quit
# via the GUI.
# The progress counter may return True, if cancel was requested. # The progress counter may return True, if cancel was requested.
if draw_callback and draw_callback(tool_position=point, if draw_callback and draw_callback(tool_position=point,
toolpath=self.pa.paths): toolpath=path):
quit_requested = True quit_requested = True
break break
progress_counter.increment() progress_counter.increment()
self.pa.end_scanline()
# update progress # update progress
current_line += 1 current_line += 1
if quit_requested: if quit_requested:
break break
self.pa.end_direction() return path
self.pa.finish()
return self.pa.paths
...@@ -21,7 +21,7 @@ You should have received a copy of the GNU General Public License ...@@ -21,7 +21,7 @@ You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>. along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
""" """
import pycam.PathProcessors.PathAccumulator from pycam.Geometry.Point import Point, Vector
from pycam.Geometry.Line import Line from pycam.Geometry.Line import Line
from pycam.Geometry.Plane import Plane from pycam.Geometry.Plane import Plane
from pycam.Geometry.utils import ceil from pycam.Geometry.utils import ceil
...@@ -35,12 +35,7 @@ log = pycam.Utils.log.get_logger() ...@@ -35,12 +35,7 @@ log = pycam.Utils.log.get_logger()
class EngraveCutter(object): class EngraveCutter(object):
def __init__(self, path_processor, physics=None): def __init__(self, physics=None):
self.pa_push = path_processor
# We use a separated path processor for the last "drop" layer.
# This path processor does not need to be configurable.
self.pa_drop = pycam.PathProcessors.PathAccumulator.PathAccumulator(
reverse=self.pa_push.reverse)
self.physics = physics self.physics = physics
def GenerateToolPath(self, cutter, models, motion_grid, minz=None, def GenerateToolPath(self, cutter, models, motion_grid, minz=None,
...@@ -58,8 +53,9 @@ class EngraveCutter(object): ...@@ -58,8 +53,9 @@ class EngraveCutter(object):
push_layers = motion_grid[:-1] push_layers = motion_grid[:-1]
push_generator = pycam.PathGenerators.PushCutter.PushCutter( push_generator = pycam.PathGenerators.PushCutter.PushCutter(
self.pa_push, physics=self.physics) physics=self.physics)
current_layer = 0 current_layer = 0
push_moves = []
for push_layer in push_layers: for push_layer in push_layers:
# update the progress bar and check, if we should cancel the process # update the progress bar and check, if we should cancel the process
if draw_callback and draw_callback(text="Engrave: processing " \ if draw_callback and draw_callback(text="Engrave: processing " \
...@@ -68,7 +64,8 @@ class EngraveCutter(object): ...@@ -68,7 +64,8 @@ class EngraveCutter(object):
quit_requested = True quit_requested = True
break break
# no callback: otherwise the status text gets lost # no callback: otherwise the status text gets lost
push_generator.GenerateToolPath(cutter, [model], [push_layer]) push_moves.extend(push_generator.GenerateToolPath(cutter, [model],
[push_layer]))
if draw_callback and draw_callback(): if draw_callback and draw_callback():
# cancel requested # cancel requested
quit_requested = True quit_requested = True
...@@ -76,15 +73,15 @@ class EngraveCutter(object): ...@@ -76,15 +73,15 @@ class EngraveCutter(object):
current_layer += 1 current_layer += 1
if quit_requested: if quit_requested:
return self.pa_push.paths return push_moves
drop_generator = pycam.PathGenerators.DropCutter.DropCutter(self.pa_drop, drop_generator = pycam.PathGenerators.DropCutter.DropCutter(
physics=self.physics) physics=self.physics)
drop_layers = motion_grid[-1:] drop_layers = motion_grid[-1:]
if draw_callback: if draw_callback:
draw_callback(text="Engrave: processing layer " + \ draw_callback(text="Engrave: processing layer " + \
"%d/%d" % (current_layer + 1, num_of_layers)) "%d/%d" % (current_layer + 1, num_of_layers))
drop_generator.GenerateToolPath(cutter, [model], drop_layers, drop_moves = drop_generator.GenerateToolPath(cutter, [model],
minz=minz, maxz=maxz, draw_callback=draw_callback) drop_layers, minz=minz, maxz=maxz, draw_callback=draw_callback)
return self.pa_push.paths + self.pa_drop.paths return push_moves + drop_moves
...@@ -29,6 +29,7 @@ from pycam.Utils import ProgressCounter ...@@ -29,6 +29,7 @@ from pycam.Utils import ProgressCounter
from pycam.Geometry.PointUtils import * from pycam.Geometry.PointUtils import *
import pycam.Utils.log import pycam.Utils.log
import math import math
from pycam.Toolpath import MOVE_STRAIGHT, MOVE_SAFETY
log = pycam.Utils.log.get_logger() log = pycam.Utils.log.get_logger()
...@@ -46,15 +47,13 @@ def _process_one_line((p1, p2, depth, models, cutter, physics)): ...@@ -46,15 +47,13 @@ def _process_one_line((p1, p2, depth, models, cutter, physics)):
class PushCutter(object): class PushCutter(object):
def __init__(self, path_processor, physics=None): def __init__(self, waterlines=False, physics=None):
if physics is None: if physics is None:
log.debug("Starting PushCutter (without ODE)") log.debug("Starting PushCutter (without ODE)")
else: else:
log.debug("Starting PushCutter (with ODE)") log.debug("Starting PushCutter (with ODE)")
self.pa = path_processor
self.physics = physics self.physics = physics
# check if we use a PolygonExtractor self.waterlines = waterlines
self._use_polygon_extractor = hasattr(self.pa, "polygon_extractor")
def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None): def GenerateToolPath(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 # Transfer the grid (a generator) into a list of lists and count the
...@@ -74,6 +73,10 @@ class PushCutter(object): ...@@ -74,6 +73,10 @@ class PushCutter(object):
progress_counter = ProgressCounter(num_of_grid_positions, draw_callback) progress_counter = ProgressCounter(num_of_grid_positions, draw_callback)
current_layer = 0 current_layer = 0
if self.waterlines:
self.pa = pycam.PathProcessors.ContourCutter.ContourCutter()
else:
path = []
for layer_grid in grid: for layer_grid in grid:
# update the progress bar and check, if we should cancel the process # update the progress bar and check, if we should cancel the process
if draw_callback and draw_callback(text="PushCutter: processing" \ if draw_callback and draw_callback(text="PushCutter: processing" \
...@@ -81,45 +84,58 @@ class PushCutter(object): ...@@ -81,45 +84,58 @@ class PushCutter(object):
# cancel immediately # cancel immediately
break break
self.pa.new_direction(0) if self.waterlines:
self.GenerateToolPathSlice(cutter, models, layer_grid, draw_callback, self.pa.new_direction(0)
progress_counter) result = self.GenerateToolPathSlice(cutter, models, layer_grid,
self.pa.end_direction() draw_callback, progress_counter)
self.pa.finish() if self.waterlines:
self.pa.end_direction()
self.pa.finish()
else:
path.extend(result)
current_layer += 1 current_layer += 1
if self._use_polygon_extractor and (len(models) > 1): if self.waterlines:
other_models = models[1:]
# TODO: this is complicated and hacky :( # TODO: this is complicated and hacky :(
# we don't use parallelism or ODE (for the sake of simplicity) # we don't use parallelism or ODE (for the sake of simplicity)
final_pa = pycam.PathProcessors.SimpleCutter.SimpleCutter( result = []
reverse=self.pa.reverse) # turn the waterline points into cutting segments
for path in self.pa.paths: for path in self.pa.paths:
final_pa.new_scanline()
pairs = [] pairs = []
for index in range(len(path.points) - 1): for index in range(len(path.points) - 1):
pairs.append((path.points[index], path.points[index + 1])) pairs.append((path.points[index], path.points[index + 1]))
for p1, p2 in pairs: if len(models) > 1:
free_points = get_free_paths_triangles(other_models, # We assume that the first model is used for the waterline and all
cutter, p1, p2) # other models are obstacles (e.g. a support grid).
for point in free_points: other_models = models[1:]
final_pa.append(point) for p1, p2 in pairs:
final_pa.end_scanline() free_points = get_free_paths_triangles(other_models,
final_pa.finish() cutter, p1, p2)
return final_pa.paths for index in range(len(free_points) / 2):
result.append((MOVE_STRAIGHT, free_points[2 * index]))
result.append((MOVE_STRAIGHT, free_points[2 * index + 1]))
result.append((MOVE_SAFETY, None))
else:
for p1, p2 in pairs:
result.append((MOVE_STRAIGHT, p1))
result.append((MOVE_STRAIGHT, p2))
result.append((MOVE_SAFETY, None))
return result
else: else:
return self.pa.paths return path
def GenerateToolPathSlice(self, cutter, models, layer_grid, draw_callback=None, def GenerateToolPathSlice(self, cutter, models, layer_grid, draw_callback=None,
progress_counter=None): progress_counter=None):
path = []
# settings for calculation of depth # settings for calculation of depth
accuracy = 20 accuracy = 20
max_depth = 20 max_depth = 20
min_depth = 4 min_depth = 4
# the ContourCutter pathprocessor does not work with combined models # the ContourCutter pathprocessor does not work with combined models
if self._use_polygon_extractor: if self.waterlines:
models = models[:1] models = models[:1]
else: else:
models = models models = models
...@@ -137,14 +153,27 @@ class PushCutter(object): ...@@ -137,14 +153,27 @@ class PushCutter(object):
for points in run_in_parallel(_process_one_line, args, for points in run_in_parallel(_process_one_line, args,
callback=progress_counter.update): callback=progress_counter.update):
if points: if points:
self.pa.new_scanline() if self.waterlines:
for point in points: self.pa.new_scanline()
self.pa.append(point) for point in points:
if draw_callback: self.pa.append(point)
draw_callback(tool_position=points[-1], toolpath=self.pa.paths) else:
self.pa.end_scanline() for index in range(len(points) / 2):
path.append((MOVE_STRAIGHT, points[2 * index]))
path.append((MOVE_STRAIGHT, points[2 * index + 1]))
path.append((MOVE_SAFETY, None))
if self.waterlines:
if draw_callback:
draw_callback(tool_position=points[-1])
self.pa.end_scanline()
else:
if draw_callback:
draw_callback(tool_position=points[-1], toolpath=path)
# update the progress counter # update the progress counter
if progress_counter and progress_counter.increment(): if progress_counter and progress_counter.increment():
# quit requested # quit requested
break break
if not self.waterlines:
return path
...@@ -22,17 +22,16 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -22,17 +22,16 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
import pycam.PathProcessors import pycam.PathProcessors
from pycam.Geometry.PolygonExtractor import PolygonExtractor from pycam.Geometry.PolygonExtractor import PolygonExtractor
from pycam.Geometry.PointUtils import * from pycam.Geometry.Point import Point
from pycam.Toolpath import simplify_toolpath from pycam.Toolpath import simplify_toolpath
class ContourCutter(pycam.PathProcessors.BasePathProcessor): class ContourCutter(pycam.PathProcessors.BasePathProcessor):
def __init__(self, reverse=False): def __init__(self):
super(ContourCutter, self).__init__() super(ContourCutter, self).__init__()
self.curr_path = None self.curr_path = None
self.scanline = None self.scanline = None
self.polygon_extractor = None self.polygon_extractor = None
self.points = [] self.points = []
self.reverse = reverse
self.__forward = (1, 1, 0) self.__forward = (1, 1, 0)
def append(self, point): def append(self, point):
...@@ -74,8 +73,6 @@ class ContourCutter(pycam.PathProcessors.BasePathProcessor): ...@@ -74,8 +73,6 @@ class ContourCutter(pycam.PathProcessors.BasePathProcessor):
path.append(path.points[0]) path.append(path.points[0])
simplify_toolpath(path) simplify_toolpath(path)
if paths: if paths:
if self.reverse:
paths.reverse()
self.paths.extend(paths) self.paths.extend(paths)
self.sort_layered() self.sort_layered()
self.polygon_extractor = None self.polygon_extractor = None
......
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2010 Lars Kruse <devel@sumpfralle.de>
Copyright 2008 Lode Leroy
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/>.
"""
import pycam.PathProcessors
from pycam.Toolpath import simplify_toolpath
from pycam.Geometry.Path import Path
class PathAccumulator(pycam.PathProcessors.BasePathProcessor):
def __init__(self, zigzag=False, reverse=False):
super(PathAccumulator, self).__init__()
self.curr_path = None
self.zigzag = zigzag
self.scanline = None
self.reverse = reverse
def append(self, point):
if self.curr_path == None:
self.curr_path = Path()
if self.reverse:
self.curr_path.insert(0, point)
else:
self.curr_path.append(point)
def new_direction(self, direction):
self.scanline = 0
def new_scanline(self):
self.scanline += 1
if self.curr_path:
print "ERROR: curr_path expected to be empty"
self.curr_path = None
def end_scanline(self):
if self.curr_path:
if self.zigzag and (self.scanline % 2 == 0):
self.curr_path.reverse()
simplify_toolpath(self.curr_path)
if self.reverse:
self.paths.insert(0, self.curr_path)
else:
self.paths.append(self.curr_path)
self.curr_path = None
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2008 Lode Leroy
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/>.
"""
import pycam.PathProcessors
from pycam.Geometry.Path import Path
from pycam.Toolpath import simplify_toolpath
class SimpleCutter(pycam.PathProcessors.BasePathProcessor):
def __init__(self, reverse=False):
super(SimpleCutter, self).__init__()
self.curr_path = None
self.reverse = reverse
def append(self, point):
curr_path = None
if self.curr_path == None:
curr_path = Path()
self.curr_path = curr_path
else:
curr_path = self.curr_path
self.curr_path = None
curr_path.append(point)
if self.curr_path == None:
simplify_toolpath(curr_path)
if self.reverse:
curr_path.reverse()
self.paths.insert(0, curr_path)
else:
self.paths.append(curr_path)
def new_scanline(self):
if self.curr_path:
print "ERROR: curr_path expected to be empty"
self.curr_path = None
def end_scanline(self):
if self.curr_path:
print "ERROR: curr_path expected to be empty"
self.curr_path = None
def finish(self):
self.sort_layered()
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2008 Lode Leroy
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/>.
"""
import pycam.PathProcessors
from pycam.Geometry.Path import Path
from pycam.Toolpath import simplify_toolpath
class ZigZagCutter(pycam.PathProcessors.BasePathProcessor):
def __init__(self, reverse=False):
super(ZigZagCutter, self).__init__()
self.curr_path = None
self.scanline = None
self.curr_scanline = None
self.reverse = reverse
def append(self, point):
curr_path = None
if self.curr_path == None:
curr_path = Path()
self.curr_path = curr_path
else:
curr_path = self.curr_path
self.curr_path = None
curr_path.append(point)
if self.curr_path == None:
if (self.scanline % 2) == 0:
self.curr_scanline.append(curr_path)
else:
curr_path.reverse()
self.curr_scanline.insert(0, curr_path)
def new_direction(self, direction):
self.scanline = 0
def new_scanline(self):
self.scanline += 1
self.curr_scanline = []
def end_scanline(self):
for path in self.curr_scanline:
simplify_toolpath(path)
if self.reverse:
path.reverse()
self.paths.append(path)
self.curr_scanline = None
...@@ -20,8 +20,7 @@ You should have received a copy of the GNU General Public License ...@@ -20,8 +20,7 @@ You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>. along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
""" """
__all__ = ["PathAccumulator", "SimpleCutter", "ZigZagCutter", "PolygonCutter", __all__ = ["PolygonCutter", "ContourCutter", "BasePathProcessor"]
"ContourCutter", "BasePathProcessor"]
class BasePathProcessor(object): class BasePathProcessor(object):
......
...@@ -22,6 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -22,6 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
import pycam.Plugins import pycam.Plugins
import pycam.Gui.OpenGLTools import pycam.Gui.OpenGLTools
from pycam.Toolpath import MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID
class OpenGLViewToolpath(pycam.Plugins.PluginBase): class OpenGLViewToolpath(pycam.Plugins.PluginBase):
...@@ -75,10 +76,12 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase): ...@@ -75,10 +76,12 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase):
def draw_toolpaths(self): def draw_toolpaths(self):
if self._is_visible(): if self._is_visible():
for toolpath in self.core.get("toolpaths").get_visible(): for toolpath in self.core.get("toolpaths").get_visible():
moves = toolpath.get_moves_for_opengl(self.core.get("gcode_safety_height")) # TODO: enable the VBO code for speedup!
self._draw_toolpath_moves2(moves) #moves = toolpath.get_moves_for_opengl(self.core.get("gcode_safety_height"))
#moves = toolpath.get_moves(self.core.get("gcode_safety_height")) #self._draw_toolpath_moves2(moves)
#self._draw_toolpath_moves(moves) toolpath._update_safety_height(self.core.get("gcode_safety_height"))
moves = toolpath.get_basic_moves()
self._draw_toolpath_moves(moves)
def _draw_toolpath_moves2(self, paths): def _draw_toolpath_moves2(self, paths):
GL = self._GL GL = self._GL
...@@ -118,7 +121,10 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase): ...@@ -118,7 +121,10 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase):
last_position = None last_position = None
last_rapid = None last_rapid = None
GL.glBegin(GL.GL_LINE_STRIP) GL.glBegin(GL.GL_LINE_STRIP)
for position, rapid in moves: for move_type, position in moves:
if not move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID):
continue
rapid = move_type == MOVE_STRAIGHT_RAPID
if last_rapid != rapid: if last_rapid != rapid:
GL.glEnd() GL.glEnd()
if rapid: if rapid:
...@@ -141,3 +147,4 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase): ...@@ -141,3 +147,4 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase):
p1 = moves[index][0] p1 = moves[index][0]
p2 = moves[index + 1][0] p2 = moves[index + 1][0]
pycam.Gui.OpenGLTools.draw_direction_cone(p1, p2) pycam.Gui.OpenGLTools.draw_direction_cone(p1, p2)
...@@ -23,7 +23,6 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -23,7 +23,6 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
import pycam.Plugins import pycam.Plugins
import pycam.PathGenerators.PushCutter import pycam.PathGenerators.PushCutter
import pycam.PathProcessors.PathAccumulator
import pycam.Toolpath.MotionGrid import pycam.Toolpath.MotionGrid
from pycam.Toolpath.MotionGrid import START_X, START_Y, START_Z from pycam.Toolpath.MotionGrid import START_X, START_Y, START_Z
...@@ -56,8 +55,7 @@ class ProcessStrategySlicing(pycam.Plugins.PluginBase): ...@@ -56,8 +55,7 @@ class ProcessStrategySlicing(pycam.Plugins.PluginBase):
tool=tool, models=environment["collision_models"]) tool=tool, models=environment["collision_models"])
line_distance = 2 * tool_params["radius"] * \ line_distance = 2 * tool_params["radius"] * \
(1.0 - process["parameters"]["overlap"]) (1.0 - process["parameters"]["overlap"])
path_generator = pycam.PathGenerators.PushCutter.PushCutter( path_generator = pycam.PathGenerators.PushCutter.PushCutter(waterlines=False)
pycam.PathProcessors.SimpleCutter.SimpleCutter())
path_pattern = process["parameters"]["path_pattern"] path_pattern = process["parameters"]["path_pattern"]
path_get_func = self.core.get("get_parameter_sets")( path_get_func = self.core.get("get_parameter_sets")(
"path_pattern")[path_pattern["name"]]["func"] "path_pattern")[path_pattern["name"]]["func"]
...@@ -95,8 +93,7 @@ class ProcessStrategyContour(pycam.Plugins.PluginBase): ...@@ -95,8 +93,7 @@ class ProcessStrategyContour(pycam.Plugins.PluginBase):
tool=tool, models=environment["collision_models"]) tool=tool, models=environment["collision_models"])
line_distance = 2 * tool_params["radius"] * \ line_distance = 2 * tool_params["radius"] * \
(1.0 - process["parameters"]["overlap"]) (1.0 - process["parameters"]["overlap"])
path_generator = pycam.PathGenerators.PushCutter.PushCutter( path_generator = pycam.PathGenerators.PushCutter.PushCutter(waterlines=True)
pycam.PathProcessors.ContourCutter.ContourCutter())
# TODO: milling_style currently refers to the grid lines - not to the waterlines # TODO: milling_style currently refers to the grid lines - not to the waterlines
motion_grid = pycam.Toolpath.MotionGrid.get_fixed_grid( motion_grid = pycam.Toolpath.MotionGrid.get_fixed_grid(
(low, high), process["parameters"]["step_down"], (low, high), process["parameters"]["step_down"],
...@@ -132,8 +129,7 @@ class ProcessStrategySurfacing(pycam.Plugins.PluginBase): ...@@ -132,8 +129,7 @@ class ProcessStrategySurfacing(pycam.Plugins.PluginBase):
tool=tool, models=environment["collision_models"]) tool=tool, models=environment["collision_models"])
line_distance = 2 * tool_params["radius"] * \ line_distance = 2 * tool_params["radius"] * \
(1.0 - process["parameters"]["overlap"]) (1.0 - process["parameters"]["overlap"])
path_generator = pycam.PathGenerators.DropCutter.DropCutter( path_generator = pycam.PathGenerators.DropCutter.DropCutter()
pycam.PathProcessors.PathAccumulator.PathAccumulator())
path_pattern = process["parameters"]["path_pattern"] path_pattern = process["parameters"]["path_pattern"]
path_get_func = self.core.get("get_parameter_sets")( path_get_func = self.core.get("get_parameter_sets")(
"path_pattern")[path_pattern["name"]]["func"] "path_pattern")[path_pattern["name"]]["func"]
...@@ -171,8 +167,7 @@ class ProcessStrategyEngraving(pycam.Plugins.PluginBase): ...@@ -171,8 +167,7 @@ class ProcessStrategyEngraving(pycam.Plugins.PluginBase):
tool_params = tool["parameters"] tool_params = tool["parameters"]
low, high = environment["bounds"].get_absolute_limits( low, high = environment["bounds"].get_absolute_limits(
tool=tool, models=environment["collision_models"]) tool=tool, models=environment["collision_models"])
path_generator = pycam.PathGenerators.EngraveCutter.EngraveCutter( path_generator = pycam.PathGenerators.EngraveCutter.EngraveCutter()
pycam.PathProcessors.SimpleCutter.SimpleCutter())
models = [m.model for m in process["parameters"]["trace_models"]] models = [m.model for m in process["parameters"]["trace_models"]]
if not models: if not models:
self.log.error("No trace models given: you need to assign a " + \ self.log.error("No trace models given: you need to assign a " + \
......
...@@ -219,7 +219,8 @@ class ToolpathExport(pycam.Plugins.PluginBase): ...@@ -219,7 +219,8 @@ class ToolpathExport(pycam.Plugins.PluginBase):
spindle_speed = params.get("spindle_speed", 1000) spindle_speed = params.get("spindle_speed", 1000)
generator.set_speed(feedrate, spindle_speed) generator.set_speed(feedrate, spindle_speed)
# TODO: implement toolpath.get_meta_data() # TODO: implement toolpath.get_meta_data()
generator.add_moves(toolpath.get_moves(safety_height), toolpath._update_safety_height(safety_height)
generator.add_moves(toolpath.get_basic_moves(),
tool_id=tool_id, comment="") tool_id=tool_id, comment="")
generator.finish() generator.finish()
destination.close() destination.close()
......
...@@ -98,9 +98,8 @@ class ToolpathGrid(pycam.Plugins.PluginBase): ...@@ -98,9 +98,8 @@ class ToolpathGrid(pycam.Plugins.PluginBase):
for x in range(x_count): for x in range(x_count):
for y in range(y_count): for y in range(y_count):
shift = (x * (x_space + x_dim), y * (y_space + y_dim), 0, 'v') shift = (x * (x_space + x_dim), y * (y_space + y_dim), 0, 'v')
for path in toolpath.paths: for index in len(toolpath.paths):
new_path = pycam.Geometry.Path.Path() new_path = [shift.add(p) for p in toolpath.paths[index]]
new_path.points = [padd(shift, p) for p in path.points]
new_paths.append(new_path) new_paths.append(new_path)
if not self.gui.get_object("KeepOriginal").get_active(): if not self.gui.get_object("KeepOriginal").get_active():
toolpath.paths = new_paths toolpath.paths = new_paths
......
...@@ -98,7 +98,7 @@ class ToolpathSimulation(pycam.Plugins.PluginBase): ...@@ -98,7 +98,7 @@ class ToolpathSimulation(pycam.Plugins.PluginBase):
self._progress.set_upper(self._toolpath.get_machine_time( self._progress.set_upper(self._toolpath.get_machine_time(
safety_height=self._safety_height)) safety_height=self._safety_height))
self._progress.set_value(0) self._progress.set_value(0)
self._distance = self._toolpath.get_machine_movement_distance( self._distance = self._toolpath.get_machine_move_distance(
safety_height=self._safety_height) safety_height=self._safety_height)
self._feedrate = self._toolpath.get_params().get("tool_feedrate", self._feedrate = self._toolpath.get_params().get("tool_feedrate",
300) 300)
...@@ -203,11 +203,11 @@ class ToolpathSimulation(pycam.Plugins.PluginBase): ...@@ -203,11 +203,11 @@ class ToolpathSimulation(pycam.Plugins.PluginBase):
if progress.update(text=progress_text, percent=progress_value_percent): if progress.update(text=progress_text, percent=progress_value_percent):
# break if the user pressed the "cancel" button # break if the user pressed the "cancel" button
break break
for index in range(len(path.points)): for index in range(len(path)):
self.cutter.moveto(path.points[index]) self.cutter.moveto(path[index])
if index != 0: if index != 0:
start = path.points[index - 1] start = path[index - 1]
end = path.points[index] end = path[index]
if start != end: if start != end:
simulation_backend.process_cutter_movement(start, end) simulation_backend.process_cutter_movement(start, end)
self.update_view() self.update_view()
......
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2012 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/>.
"""
from pycam.Toolpath import MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID, MOVE_SAFETY, MACHINE_SETTING
from pycam.Geometry.Point import Point
class BaseFilter(object):
def __ror__(self, toolpath):
return self.filter_toolpath(toolpath)
def filter_toolpath(self, toolpath):
raise NotImplementedError("The filter class %s failed to " + \
"implement the 'filter_toolpath' method" % str(type(self)))
class SafetyHeightFilter(BaseFilter):
def __init__(self, safety_height):
self.safety_height = safety_height
def filter_toolpath(self, toolpath):
last_pos = None
new_path = []
for move_type, args in toolpath:
if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID):
if not last_pos:
# there was a safety move (or no move at all) before
# -> move sideways
safe_pos = Point(args.x, args.y, self.safety_height)
new_path.append((MOVE_STRAIGHT_RAPID, safe_pos))
last_pos = args
new_path.append((move_type, args))
elif move_type == MOVE_SAFETY:
if last_pos:
# safety move -> move straight up to safety height
next_pos = Point(last_pos.x, last_pos.y, self.safety_height)
new_path.append((MOVE_STRAIGHT_RAPID, next_pos))
last_pos = None
else:
# this looks like a duplicate safety move -> ignore
pass
else:
# unknown move -> keep it
new_path.append((move_type, args))
return new_path
class TinySidewaysMovesFilter(BaseFilter):
def __init__(self, tolerance):
self.tolerance = tolerance
def filter_toolpath(self, toolpath):
new_path = []
last_pos = None
in_safety = False
for move_type, args in toolpath:
if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID):
if in_safety and last_pos:
# check if the last position was very close
if (last_pos.sub(args).norm < self.tolerance) and \
(last_pos.x == args.x) and (last_pos.y == args.y):
# within tolerance -> remove previous safety move
new_path.pop(-1)
in_safety = False
last_pos = args
elif move_type == MOVE_SAFETY:
in_safety = True
else:
pass
new_path.append((move_type, args))
return new_path
class MachineSetting(BaseFilter):
def __init__(self, key, value):
self.key = key
self.value = value
def filter_toolpath(self, toolpath):
return [(MACHINE_SETTING, (self.key, self.value))] + toolpath
...@@ -23,8 +23,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -23,8 +23,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
from pycam.PathGenerators import DropCutter, PushCutter, EngraveCutter, \ from pycam.PathGenerators import DropCutter, PushCutter, EngraveCutter, \
ContourFollow ContourFollow
from pycam.Geometry.utils import number from pycam.Geometry.utils import number
from pycam.PathProcessors import PathAccumulator, SimpleCutter, ZigZagCutter, \ from pycam.PathProcessors import PolygonCutter, ContourCutter
PolygonCutter, ContourCutter
from pycam.Cutters.CylindricalCutter import CylindricalCutter from pycam.Cutters.CylindricalCutter import CylindricalCutter
import pycam.Cutters import pycam.Cutters
import pycam.Toolpath.SupportGrid import pycam.Toolpath.SupportGrid
......
...@@ -30,12 +30,18 @@ from pycam.Geometry.PointUtils import * ...@@ -30,12 +30,18 @@ from pycam.Geometry.PointUtils import *
from pycam.Geometry.Path import Path from pycam.Geometry.Path import Path
from pycam.Geometry.Line import Line from pycam.Geometry.Line import Line
from pycam.Geometry.utils import number, epsilon from pycam.Geometry.utils import number, epsilon
import pycam.Utils.log
import random import random
import os import os
import math import math
from itertools import groupby from itertools import groupby
MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID, MOVE_ARC, MOVE_SAFETY, TOOL_CHANGE, \
MACHINE_SETTING = range(6)
def _check_colinearity(p1, p2, p3): def _check_colinearity(p1, p2, p3):
v1 = pnormalized(psub(p2, p1)) v1 = pnormalized(psub(p2, p1))
v2 = pnormalized(psub(p3, p2)) v2 = pnormalized(psub(p3, p2))
...@@ -65,20 +71,23 @@ def simplify_toolpath(path): ...@@ -65,20 +71,23 @@ def simplify_toolpath(path):
class Toolpath(object): class Toolpath(object):
def __init__(self, paths, parameters=None): def __init__(self, path, parameters=None):
self.paths = paths self.path = path
if not parameters: if not parameters:
parameters = {} parameters = {}
self.parameters = parameters self.parameters = parameters
self._max_safe_distance = 2 * parameters.get("tool_radius", 0) # TODO: remove this hidden import (currently necessary to avoid dependency loop)
from pycam.Toolpath.Filters import TinySidewaysMovesFilter, MachineSetting, \
SafetyHeightFilter
self.filters = []
self.filters.append(MachineSetting("metric", True))
self.filters.append(MachineSetting("feedrate",
parameters.get("tool_feedrate", 300)))
self.filters.append(TinySidewaysMovesFilter(
2 * parameters.get("tool_radius", 0)))
self.filters.append(SafetyHeightFilter(20))
self._feedrate = parameters.get("tool_feedrate", 300) self._feedrate = parameters.get("tool_feedrate", 300)
self.opengl_safety_height = None self.clear_cache()
self._minx = None
self._maxx = None
self._miny = None
self._maxy = None
self._minz = None
self._maxz = None
def clear_cache(self): def clear_cache(self):
self.opengl_safety_height = None self.opengl_safety_height = None
...@@ -94,19 +103,17 @@ class Toolpath(object): ...@@ -94,19 +103,17 @@ class Toolpath(object):
def copy(self): def copy(self):
new_paths = [] new_paths = []
for path in self.paths: for path in self.path:
new_path = Path() new_path = Path()
for point in path.points: for point in path:
new_path.append(point) new_path.append(point)
new_paths.append(new_path) new_paths.append(new_path)
return Toolpath(new_paths, parameters=self.get_params()) return Toolpath(new_paths, parameters=self.get_params())
def _get_limit_generic(self, idx, func): def _get_limit_generic(self, idx, func):
path_min = [] values = [p[x] for move_type, p in self.path
for path in self.paths: if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID)]
if path.points: return func(values)
path_min.append(func([ p[idx] for p in path.points]))
return func(path_min)
@property @property
def minx(self): def minx(self):
...@@ -151,6 +158,7 @@ class Toolpath(object): ...@@ -151,6 +158,7 @@ class Toolpath(object):
return os.linesep.join((start_marker, meta, end_marker)) return os.linesep.join((start_marker, meta, end_marker))
def get_moves(self, safety_height, max_movement=None): def get_moves(self, safety_height, max_movement=None):
self._update_safety_height(safety_height)
class MoveContainer(object): class MoveContainer(object):
def __init__(self, max_movement): def __init__(self, max_movement):
self.max_movement = max_movement self.max_movement = max_movement
...@@ -187,11 +195,11 @@ class Toolpath(object): ...@@ -187,11 +195,11 @@ class Toolpath(object):
return True return True
p_last = None p_last = None
result = MoveContainer(max_movement) result = MoveContainer(max_movement)
for path in self.paths: for path in self.path:
if not path: if not path:
# ignore empty paths # ignore empty paths
continue continue
p_next = path.points[0] p_next = path[0]
if p_last is None: if p_last is None:
p_last = (p_next[0], p_next[1], safety_height) p_last = (p_next[0], p_next[1], safety_height)
if not result.append(p_last, True): if not result.append(p_last, True):
...@@ -345,26 +353,36 @@ class Toolpath(object): ...@@ -345,26 +353,36 @@ class Toolpath(object):
@rtype: float @rtype: float
@returns: the machine time used for processing the toolpath in minutes @returns: the machine time used for processing the toolpath in minutes
""" """
return self.get_machine_move_distance(safety_height) / self._feedrate
def _update_safety_height(self, safety_height):
# TODO: remove this ugly hack!
from pycam.Toolpath.Filters import SafetyHeightFilter
for index in range(len(self.filters)):
if isinstance(self.filters[index], SafetyHeightFilter) and \
(self.filters[index].safety_height != safety_height):
self.filters[index] = SafetyHeightFilter(safety_height)
self.get_basic_moves(reset_cache=True)
break
def get_machine_move_distance(self, safety_height):
result = 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 += pnorm(psub(new_pos, current_position)) / 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 current_position = None
self._update_safety_height(safety_height)
# go through all points of the path # go through all points of the path
for new_pos, rapid in self.get_moves(safety_height): for move_type, args in self.get_basic_moves():
if not current_position is None: if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID):
result += pnorm(psub(new_pos, current_position)) if not current_position is None:
current_position = new_pos result += pnorm(psub(args, current_position))
current_position = args
return result return result
def get_basic_moves(self, reset_cache=False):
if reset_cache or not hasattr(self, "_cache_basic_moves"):
result = list(self.path)
for move_filter in self.filters:
result |= move_filter
self._cache_basic_moves = result
return self._cache_basic_moves
def get_cropped_copy(self, polygons, callback=None): def get_cropped_copy(self, polygons, callback=None):
# create a deep copy of the current toolpath # create a deep copy of the current toolpath
...@@ -375,11 +393,9 @@ class Toolpath(object): ...@@ -375,11 +393,9 @@ class Toolpath(object):
def crop(self, polygons, callback=None): def crop(self, polygons, callback=None):
# collect all existing toolpath lines # collect all existing toolpath lines
open_lines = [] open_lines = []
for path in self.paths: # TODO: migrate "crop" to the new toolpath structure
if path: for index in range(len(path) - 1):
for index in range(len(path.points) - 1): open_lines.append(Line(path[index], path[index + 1]))
open_lines.append(Line(path.points[index],
path.points[index + 1]))
# go through all polygons and add "inner" lines (or parts thereof) to # go through all polygons and add "inner" lines (or parts thereof) to
# the final list of remaining lines # the final list of remaining lines
inner_lines = [] inner_lines = []
...@@ -402,7 +418,7 @@ class Toolpath(object): ...@@ -402,7 +418,7 @@ class Toolpath(object):
while inner_lines: while inner_lines:
if callback and callback(): if callback and callback():
return return
end = current_path.points[-1] end = current_path[-1]
# look for the next connected point # look for the next connected point
for line in inner_lines: for line in inner_lines:
if line.p1 == end: if line.p1 == end:
...@@ -415,10 +431,9 @@ class Toolpath(object): ...@@ -415,10 +431,9 @@ class Toolpath(object):
line = inner_lines.pop(0) line = inner_lines.pop(0)
current_path.append(line.p1) current_path.append(line.p1)
current_path.append(line.p2) current_path.append(line.p2)
if current_path.points: if current_path:
new_paths.append(current_path) new_paths.append(current_path)
self.paths = new_paths self.path = new_path
self.clear_cache()
class Bounds(object): class Bounds(object):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment