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
from pycam.Utils.threading import run_in_parallel
import pycam.Geometry.Model
import pycam.Utils.log
from pycam.Toolpath import MOVE_STRAIGHT, MOVE_SAFETY
log = pycam.Utils.log.get_logger()
......@@ -43,60 +43,55 @@ def _process_one_grid_line((positions, minz, maxz, model, cutter, physics)):
class DropCutter(object):
def __init__(self, path_processor, physics=None):
self.pa = path_processor
def __init__(self, physics=None):
self.physics = physics
def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None):
path = []
quit_requested = False
model = pycam.Geometry.Model.get_combined_model(models)
# Transfer the grid (a generator) into a list of lists and count the
# items.
# usually there is only one layer - but an xy-grid consists of two
lines = []
# usually there is only one layer - but an xy-grid consists of two
for layer in motion_grid:
lines.extend(layer)
for line in layer:
lines.append(line)
num_of_lines = len(lines)
progress_counter = ProgressCounter(len(lines), draw_callback)
current_line = 0
self.pa.new_direction(0)
args = []
for one_grid_line in lines:
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,
callback=progress_counter.update):
self.pa.new_scanline()
if draw_callback and draw_callback(text="DropCutter: processing line %d/%d"
% (current_line + 1, num_of_lines)):
if draw_callback and draw_callback(text="DropCutter: processing " \
+ "line %d/%d" % (current_line + 1, num_of_lines)):
# cancel requested
quit_requested = True
break
for point in points:
if point is None:
# exceeded maxz - the cutter has to skip this point
self.pa.end_scanline()
self.pa.new_scanline()
path.append((MOVE_SAFETY))
continue
self.pa.append(point)
# "draw_callback" returns true, if the user requested to quit
# via the GUI.
path.append((MOVE_STRAIGHT, point))
# The progress counter may return True, if cancel was requested.
if draw_callback and draw_callback(tool_position=point,
toolpath=self.pa.paths):
toolpath=path):
quit_requested = True
break
progress_counter.increment()
self.pa.end_scanline()
# update progress
current_line += 1
if quit_requested:
break
self.pa.end_direction()
self.pa.finish()
return self.pa.paths
return path
......@@ -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/>.
"""
import pycam.PathProcessors.PathAccumulator
from pycam.Geometry.Point import Point, Vector
from pycam.Geometry.Line import Line
from pycam.Geometry.Plane import Plane
from pycam.Geometry.utils import ceil
......@@ -35,12 +35,7 @@ log = pycam.Utils.log.get_logger()
class EngraveCutter(object):
def __init__(self, path_processor, 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)
def __init__(self, physics=None):
self.physics = physics
def GenerateToolPath(self, cutter, models, motion_grid, minz=None,
......@@ -58,8 +53,9 @@ class EngraveCutter(object):
push_layers = motion_grid[:-1]
push_generator = pycam.PathGenerators.PushCutter.PushCutter(
self.pa_push, physics=self.physics)
physics=self.physics)
current_layer = 0
push_moves = []
for push_layer in push_layers:
# update the progress bar and check, if we should cancel the process
if draw_callback and draw_callback(text="Engrave: processing " \
......@@ -68,7 +64,8 @@ class EngraveCutter(object):
quit_requested = True
break
# 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():
# cancel requested
quit_requested = True
......@@ -76,15 +73,15 @@ class EngraveCutter(object):
current_layer += 1
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)
drop_layers = motion_grid[-1:]
if draw_callback:
draw_callback(text="Engrave: processing layer " + \
"%d/%d" % (current_layer + 1, num_of_layers))
drop_generator.GenerateToolPath(cutter, [model], drop_layers,
minz=minz, maxz=maxz, draw_callback=draw_callback)
return self.pa_push.paths + self.pa_drop.paths
drop_moves = drop_generator.GenerateToolPath(cutter, [model],
drop_layers, minz=minz, maxz=maxz, draw_callback=draw_callback)
return push_moves + drop_moves
......@@ -29,6 +29,7 @@ from pycam.Utils import ProgressCounter
from pycam.Geometry.PointUtils import *
import pycam.Utils.log
import math
from pycam.Toolpath import MOVE_STRAIGHT, MOVE_SAFETY
log = pycam.Utils.log.get_logger()
......@@ -46,15 +47,13 @@ def _process_one_line((p1, p2, depth, models, cutter, physics)):
class PushCutter(object):
def __init__(self, path_processor, physics=None):
def __init__(self, waterlines=False, physics=None):
if physics is None:
log.debug("Starting PushCutter (without ODE)")
else:
log.debug("Starting PushCutter (with ODE)")
self.pa = path_processor
self.physics = physics
# check if we use a PolygonExtractor
self._use_polygon_extractor = hasattr(self.pa, "polygon_extractor")
self.waterlines = waterlines
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
......@@ -74,6 +73,10 @@ class PushCutter(object):
progress_counter = ProgressCounter(num_of_grid_positions, draw_callback)
current_layer = 0
if self.waterlines:
self.pa = pycam.PathProcessors.ContourCutter.ContourCutter()
else:
path = []
for layer_grid in grid:
# update the progress bar and check, if we should cancel the process
if draw_callback and draw_callback(text="PushCutter: processing" \
......@@ -81,45 +84,58 @@ class PushCutter(object):
# cancel immediately
break
self.pa.new_direction(0)
self.GenerateToolPathSlice(cutter, models, layer_grid, draw_callback,
progress_counter)
self.pa.end_direction()
self.pa.finish()
if self.waterlines:
self.pa.new_direction(0)
result = self.GenerateToolPathSlice(cutter, models, layer_grid,
draw_callback, progress_counter)
if self.waterlines:
self.pa.end_direction()
self.pa.finish()
else:
path.extend(result)
current_layer += 1
if self._use_polygon_extractor and (len(models) > 1):
other_models = models[1:]
if self.waterlines:
# TODO: this is complicated and hacky :(
# we don't use parallelism or ODE (for the sake of simplicity)
final_pa = pycam.PathProcessors.SimpleCutter.SimpleCutter(
reverse=self.pa.reverse)
result = []
# turn the waterline points into cutting segments
for path in self.pa.paths:
final_pa.new_scanline()
pairs = []
for index in range(len(path.points) - 1):
pairs.append((path.points[index], path.points[index + 1]))
for p1, p2 in pairs:
free_points = get_free_paths_triangles(other_models,
cutter, p1, p2)
for point in free_points:
final_pa.append(point)
final_pa.end_scanline()
final_pa.finish()
return final_pa.paths
if len(models) > 1:
# We assume that the first model is used for the waterline and all
# other models are obstacles (e.g. a support grid).
other_models = models[1:]
for p1, p2 in pairs:
free_points = get_free_paths_triangles(other_models,
cutter, p1, p2)
for index in range(len(free_points) / 2):
result.append((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:
return self.pa.paths
return path
def GenerateToolPathSlice(self, cutter, models, layer_grid, draw_callback=None,
progress_counter=None):
path = []
# settings for calculation of depth
accuracy = 20
max_depth = 20
min_depth = 4
# the ContourCutter pathprocessor does not work with combined models
if self._use_polygon_extractor:
if self.waterlines:
models = models[:1]
else:
models = models
......@@ -137,14 +153,27 @@ class PushCutter(object):
for points in run_in_parallel(_process_one_line, args,
callback=progress_counter.update):
if points:
self.pa.new_scanline()
for point in points:
self.pa.append(point)
if draw_callback:
draw_callback(tool_position=points[-1], toolpath=self.pa.paths)
self.pa.end_scanline()
if self.waterlines:
self.pa.new_scanline()
for point in points:
self.pa.append(point)
else:
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
if progress_counter and progress_counter.increment():
# quit requested
break
if not self.waterlines:
return path
......@@ -22,17 +22,16 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
import pycam.PathProcessors
from pycam.Geometry.PolygonExtractor import PolygonExtractor
from pycam.Geometry.PointUtils import *
from pycam.Geometry.Point import Point
from pycam.Toolpath import simplify_toolpath
class ContourCutter(pycam.PathProcessors.BasePathProcessor):
def __init__(self, reverse=False):
def __init__(self):
super(ContourCutter, self).__init__()
self.curr_path = None
self.scanline = None
self.polygon_extractor = None
self.points = []
self.reverse = reverse
self.__forward = (1, 1, 0)
def append(self, point):
......@@ -74,8 +73,6 @@ class ContourCutter(pycam.PathProcessors.BasePathProcessor):
path.append(path.points[0])
simplify_toolpath(path)
if paths:
if self.reverse:
paths.reverse()
self.paths.extend(paths)
self.sort_layered()
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
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
__all__ = ["PathAccumulator", "SimpleCutter", "ZigZagCutter", "PolygonCutter",
"ContourCutter", "BasePathProcessor"]
__all__ = ["PolygonCutter", "ContourCutter", "BasePathProcessor"]
class BasePathProcessor(object):
......
......@@ -22,6 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
import pycam.Plugins
import pycam.Gui.OpenGLTools
from pycam.Toolpath import MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID
class OpenGLViewToolpath(pycam.Plugins.PluginBase):
......@@ -75,10 +76,12 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase):
def draw_toolpaths(self):
if self._is_visible():
for toolpath in self.core.get("toolpaths").get_visible():
moves = toolpath.get_moves_for_opengl(self.core.get("gcode_safety_height"))
self._draw_toolpath_moves2(moves)
#moves = toolpath.get_moves(self.core.get("gcode_safety_height"))
#self._draw_toolpath_moves(moves)
# TODO: enable the VBO code for speedup!
#moves = toolpath.get_moves_for_opengl(self.core.get("gcode_safety_height"))
#self._draw_toolpath_moves2(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):
GL = self._GL
......@@ -118,7 +121,10 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase):
last_position = None
last_rapid = None
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:
GL.glEnd()
if rapid:
......@@ -141,3 +147,4 @@ class OpenGLViewToolpath(pycam.Plugins.PluginBase):
p1 = moves[index][0]
p2 = moves[index + 1][0]
pycam.Gui.OpenGLTools.draw_direction_cone(p1, p2)
......@@ -23,7 +23,6 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
import pycam.Plugins
import pycam.PathGenerators.PushCutter
import pycam.PathProcessors.PathAccumulator
import pycam.Toolpath.MotionGrid
from pycam.Toolpath.MotionGrid import START_X, START_Y, START_Z
......@@ -56,8 +55,7 @@ class ProcessStrategySlicing(pycam.Plugins.PluginBase):
tool=tool, models=environment["collision_models"])
line_distance = 2 * tool_params["radius"] * \
(1.0 - process["parameters"]["overlap"])
path_generator = pycam.PathGenerators.PushCutter.PushCutter(
pycam.PathProcessors.SimpleCutter.SimpleCutter())
path_generator = pycam.PathGenerators.PushCutter.PushCutter(waterlines=False)
path_pattern = process["parameters"]["path_pattern"]
path_get_func = self.core.get("get_parameter_sets")(
"path_pattern")[path_pattern["name"]]["func"]
......@@ -95,8 +93,7 @@ class ProcessStrategyContour(pycam.Plugins.PluginBase):
tool=tool, models=environment["collision_models"])
line_distance = 2 * tool_params["radius"] * \
(1.0 - process["parameters"]["overlap"])
path_generator = pycam.PathGenerators.PushCutter.PushCutter(
pycam.PathProcessors.ContourCutter.ContourCutter())
path_generator = pycam.PathGenerators.PushCutter.PushCutter(waterlines=True)
# TODO: milling_style currently refers to the grid lines - not to the waterlines
motion_grid = pycam.Toolpath.MotionGrid.get_fixed_grid(
(low, high), process["parameters"]["step_down"],
......@@ -132,8 +129,7 @@ class ProcessStrategySurfacing(pycam.Plugins.PluginBase):
tool=tool, models=environment["collision_models"])
line_distance = 2 * tool_params["radius"] * \
(1.0 - process["parameters"]["overlap"])
path_generator = pycam.PathGenerators.DropCutter.DropCutter(
pycam.PathProcessors.PathAccumulator.PathAccumulator())
path_generator = pycam.PathGenerators.DropCutter.DropCutter()
path_pattern = process["parameters"]["path_pattern"]
path_get_func = self.core.get("get_parameter_sets")(
"path_pattern")[path_pattern["name"]]["func"]
......@@ -171,8 +167,7 @@ class ProcessStrategyEngraving(pycam.Plugins.PluginBase):
tool_params = tool["parameters"]
low, high = environment["bounds"].get_absolute_limits(
tool=tool, models=environment["collision_models"])
path_generator = pycam.PathGenerators.EngraveCutter.EngraveCutter(
pycam.PathProcessors.SimpleCutter.SimpleCutter())
path_generator = pycam.PathGenerators.EngraveCutter.EngraveCutter()
models = [m.model for m in process["parameters"]["trace_models"]]
if not models:
self.log.error("No trace models given: you need to assign a " + \
......
......@@ -219,7 +219,8 @@ class ToolpathExport(pycam.Plugins.PluginBase):
spindle_speed = params.get("spindle_speed", 1000)
generator.set_speed(feedrate, spindle_speed)
# 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="")
generator.finish()
destination.close()
......
......@@ -98,9 +98,8 @@ class ToolpathGrid(pycam.Plugins.PluginBase):
for x in range(x_count):
for y in range(y_count):
shift = (x * (x_space + x_dim), y * (y_space + y_dim), 0, 'v')
for path in toolpath.paths:
new_path = pycam.Geometry.Path.Path()
new_path.points = [padd(shift, p) for p in path.points]
for index in len(toolpath.paths):
new_path = [shift.add(p) for p in toolpath.paths[index]]
new_paths.append(new_path)
if not self.gui.get_object("KeepOriginal").get_active():
toolpath.paths = new_paths
......
......@@ -98,7 +98,7 @@ class ToolpathSimulation(pycam.Plugins.PluginBase):
self._progress.set_upper(self._toolpath.get_machine_time(
safety_height=self._safety_height))
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)
self._feedrate = self._toolpath.get_params().get("tool_feedrate",
300)
......@@ -203,11 +203,11 @@ class ToolpathSimulation(pycam.Plugins.PluginBase):
if progress.update(text=progress_text, percent=progress_value_percent):
# break if the user pressed the "cancel" button
break
for index in range(len(path.points)):
self.cutter.moveto(path.points[index])
for index in range(len(path)):
self.cutter.moveto(path[index])
if index != 0:
start = path.points[index - 1]
end = path.points[index]
start = path[index - 1]
end = path[index]
if start != end:
simulation_backend.process_cutter_movement(start, end)
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/>.
from pycam.PathGenerators import DropCutter, PushCutter, EngraveCutter, \
ContourFollow
from pycam.Geometry.utils import number
from pycam.PathProcessors import PathAccumulator, SimpleCutter, ZigZagCutter, \
PolygonCutter, ContourCutter
from pycam.PathProcessors import PolygonCutter, ContourCutter
from pycam.Cutters.CylindricalCutter import CylindricalCutter
import pycam.Cutters
import pycam.Toolpath.SupportGrid
......
......@@ -30,12 +30,18 @@ from pycam.Geometry.PointUtils import *
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
import math
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):
v1 = pnormalized(psub(p2, p1))
v2 = pnormalized(psub(p3, p2))
......@@ -65,20 +71,23 @@ def simplify_toolpath(path):
class Toolpath(object):
def __init__(self, paths, parameters=None):
self.paths = paths
def __init__(self, path, parameters=None):
self.path = path
if not 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.opengl_safety_height = None
self._minx = None
self._maxx = None
self._miny = None
self._maxy = None
self._minz = None
self._maxz = None
self.clear_cache()
def clear_cache(self):
self.opengl_safety_height = None
......@@ -94,19 +103,17 @@ class Toolpath(object):
def copy(self):
new_paths = []
for path in self.paths:
for path in self.path:
new_path = Path()
for point in path.points:
for point in path:
new_path.append(point)
new_paths.append(new_path)
return Toolpath(new_paths, parameters=self.get_params())
def _get_limit_generic(self, idx, func):
path_min = []
for path in self.paths:
if path.points:
path_min.append(func([ p[idx] for p in path.points]))
return func(path_min)
values = [p[x] for move_type, p in self.path
if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID)]
return func(values)
@property
def minx(self):
......@@ -151,6 +158,7 @@ class Toolpath(object):
return os.linesep.join((start_marker, meta, end_marker))
def get_moves(self, safety_height, max_movement=None):
self._update_safety_height(safety_height)
class MoveContainer(object):
def __init__(self, max_movement):
self.max_movement = max_movement
......@@ -187,11 +195,11 @@ class Toolpath(object):
return True
p_last = None
result = MoveContainer(max_movement)
for path in self.paths:
for path in self.path:
if not path:
# ignore empty paths
continue
p_next = path.points[0]
p_next = path[0]
if p_last is None:
p_last = (p_next[0], p_next[1], safety_height)
if not result.append(p_last, True):
......@@ -345,26 +353,36 @@ class Toolpath(object):
@rtype: float
@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
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
self._update_safety_height(safety_height)
# 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))
current_position = new_pos
for move_type, args in self.get_basic_moves():
if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID):
if not current_position is None:
result += pnorm(psub(args, current_position))
current_position = args
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):
# create a deep copy of the current toolpath
......@@ -375,11 +393,9 @@ class Toolpath(object):
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]))
# TODO: migrate "crop" to the new toolpath structure
for index in range(len(path) - 1):
open_lines.append(Line(path[index], path[index + 1]))
# go through all polygons and add "inner" lines (or parts thereof) to
# the final list of remaining lines
inner_lines = []
......@@ -402,7 +418,7 @@ class Toolpath(object):
while inner_lines:
if callback and callback():
return
end = current_path.points[-1]
end = current_path[-1]
# look for the next connected point
for line in inner_lines:
if line.p1 == end:
......@@ -415,10 +431,9 @@ class Toolpath(object):
line = inner_lines.pop(0)
current_path.append(line.p1)
current_path.append(line.p2)
if current_path.points:
if current_path:
new_paths.append(current_path)
self.paths = new_paths
self.clear_cache()
self.path = new_path
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