Commit 3fd4780d authored by sumpfralle's avatar sumpfralle

implemented adaptive DropCutter positioning

 * see a blog post of Anders Wallin for more details: http://www.anderswallin.net/2010/10/adaptive-sampling-drop-cutter/
simplified the "max_height" detection


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@815 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent d9d40db7
Version 0.4.1 - UNRELEASED Version 0.4.1 - UNRELEASED
* added support for EPS/PS contour files * added support for EPS/PS contour files
* added adaptive positioning for DropCutter strategy (improves precision)
Version 0.4.0.1 - 2010-10-24
* disabled parallel processing for Windows standalone executable
(a real fix will follow later)
Version 0.4 - 2010-10-19 Version 0.4 - 2010-10-19
* use all available CPU cores for parallel processing * use all available CPU cores for parallel processing
......
...@@ -23,7 +23,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -23,7 +23,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
from pycam.Geometry.Point import Point from pycam.Geometry.Point import Point
from pycam.Geometry.utils import INFINITE, ceil from pycam.Geometry.utils import INFINITE, ceil
from pycam.PathGenerators import get_max_height_triangles, get_max_height_ode from pycam.PathGenerators import get_max_height_dynamic
from pycam.Utils import ProgressCounter from pycam.Utils import ProgressCounter
from pycam.Utils.threading import run_in_parallel from pycam.Utils.threading import run_in_parallel
import pycam.Utils.log import pycam.Utils.log
...@@ -33,24 +33,12 @@ log = pycam.Utils.log.get_logger() ...@@ -33,24 +33,12 @@ log = pycam.Utils.log.get_logger()
# We need to use a global function here - otherwise it does not work with # We need to use a global function here - otherwise it does not work with
# the multiprocessing Pool. # the multiprocessing Pool.
def _process_one_grid_line((positions, minz, maxz, model, cutter, def _process_one_grid_line((positions, minz, maxz, model, cutter, physics)):
physics, safety_height)): """ This function assumes, that the positions are next to each other.
# for now only used for triangular collision detection Otherwise the dynamic over-sampling (in get_max_height_dynamic) is
last_position = None pointless.
points = [] """
height_exceeded = False return get_max_height_dynamic(model, cutter, positions, minz, maxz, physics)
for x, y in positions:
if physics:
result = get_max_height_ode(physics, x, y, minz, maxz)
else:
result = get_max_height_triangles(model, cutter, x, y, minz, maxz,
last_pos=last_position)
if result:
points.extend(result)
else:
points.append(Point(x, y, safety_height))
height_exceeded = True
return points, height_exceeded
class Dimension: class Dimension:
...@@ -95,76 +83,58 @@ class DropCutter: ...@@ -95,76 +83,58 @@ class DropCutter:
self.pa = path_processor self.pa = path_processor
self.physics = physics self.physics = physics
# remember if we already reported an invalid boundary # remember if we already reported an invalid boundary
self._boundary_warning_already_shown = False
def GenerateToolPath(self, motion_grid, minz, maxz, draw_callback=None): def GenerateToolPath(self, motion_grid, minz, maxz, draw_callback=None):
quit_requested = False quit_requested = False
# 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.
num_of_grid_positions = 0
lines = [] lines = []
# there should be only one layer for DropCutter # there should be only one layer for DropCutter
for layer in motion_grid: for layer in motion_grid:
for line in layer: for line in layer:
lines.append(list(line)) lines.append(line)
num_of_grid_positions += len(lines[-1])
# ignore any other layers # ignore any other layers
break break
num_of_lines = len(lines) num_of_lines = len(lines)
progress_counter = ProgressCounter(num_of_grid_positions, draw_callback) progress_counter = ProgressCounter(len(lines), draw_callback)
current_line = 0 current_line = 0
self.pa.new_direction(0) self.pa.new_direction(0)
self._boundary_warning_already_shown = False
args = [] args = []
for one_grid_line in lines: for one_grid_line in lines:
# simplify the data (useful for remote processing) # simplify the data (useful for remote processing)
xy_coords = [(pos.x, pos.y) for pos in one_grid_line] xy_coords = [(pos.x, pos.y) for pos in one_grid_line]
args.append((xy_coords, minz, maxz, self.model, self.cutter, args.append((xy_coords, minz, maxz, self.model, self.cutter,
self.physics, self.model.maxz)) self.physics))
# ODE does not work with multi-threading # ODE does not work with multi-threading (TODO: check this)
disable_multiprocessing = not self.physics is None disable_multiprocessing = not self.physics is None
for points, height_exceeded in run_in_parallel(_process_one_grid_line, for points in run_in_parallel(_process_one_grid_line,
args, disable_multiprocessing=disable_multiprocessing): args, disable_multiprocessing=disable_multiprocessing):
if height_exceeded and not self._boundary_warning_already_shown:
log.warn("DropCutter: exceed the height of the " \
+ "boundary box: using a safe height instead." \
+ " This warning is reported only once.")
self._boundary_warning_already_shown = True
self.pa.new_scanline() 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" % (current_line + 1, num_of_lines)): + "line %d/%d" % (current_line + 1, num_of_lines)):
# cancel requested # cancel requested
quit_requested = True quit_requested = True
break break
for p in points: for p in points:
self.pa.append(p) self.pa.append(p)
# "draw_callback" returns true, if the user requested to quit # "draw_callback" returns true, if the user requested to quit
# via the GUI. # 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=p, if draw_callback and draw_callback(tool_position=p,
toolpath=self.pa.paths)) \ toolpath=self.pa.paths):
or (progress_counter.increment()):
quit_requested = True quit_requested = True
break break
progress_counter.increment()
self.pa.end_scanline() 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() self.pa.end_direction()
self.pa.finish() self.pa.finish()
return self.pa.paths return self.pa.paths
...@@ -23,9 +23,10 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -23,9 +23,10 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
import pycam.PathProcessors.PathAccumulator import pycam.PathProcessors.PathAccumulator
from pycam.Geometry.Point import Point from pycam.Geometry.Point import Point
from pycam.Geometry.utils import INFINITE, ceil from pycam.Geometry.utils import ceil
from pycam.PathGenerators import get_max_height_triangles, get_max_height_ode, \ from pycam.PathGenerators import get_max_height_dynamic
get_free_paths_ode, get_free_paths_triangles from pycam.PathGenerators import get_max_height_dynamic, get_free_paths_ode, \
get_free_paths_triangles
from pycam.Utils import ProgressCounter from pycam.Utils import ProgressCounter
import pycam.Utils.log import pycam.Utils.log
...@@ -35,7 +36,7 @@ log = pycam.Utils.log.get_logger() ...@@ -35,7 +36,7 @@ log = pycam.Utils.log.get_logger()
class EngraveCutter: class EngraveCutter:
def __init__(self, cutter, trimesh_models, contour_model, path_processor, def __init__(self, cutter, trimesh_models, contour_model, path_processor,
physics=None, safety_height=INFINITE): physics=None):
self.cutter = cutter self.cutter = cutter
self.models = trimesh_models self.models = trimesh_models
# combine the models (if there is more than one) # combine the models (if there is more than one)
...@@ -51,8 +52,6 @@ class EngraveCutter: ...@@ -51,8 +52,6 @@ class EngraveCutter:
# This path processor does not need to be configurable. # This path processor does not need to be configurable.
self.pa_drop = pycam.PathProcessors.PathAccumulator() self.pa_drop = pycam.PathProcessors.PathAccumulator()
self.physics = physics self.physics = physics
self.safety_height = safety_height
self._boundary_warning_already_shown = False
def GenerateToolPath(self, minz, maxz, horiz_step, dz, draw_callback=None): def GenerateToolPath(self, minz, maxz, horiz_step, dz, draw_callback=None):
quit_requested = False quit_requested = False
...@@ -178,46 +177,30 @@ class EngraveCutter: ...@@ -178,46 +177,30 @@ class EngraveCutter:
draw_callback=None): draw_callback=None):
pa.new_direction(0) pa.new_direction(0)
pa.new_scanline() pa.new_scanline()
p1 = Point(line.p1.x, line.p1.y, minz) if not self.combined_model:
p2 = Point(line.p2.x, line.p2.y, minz) # no obstacle -> minimum height
distance = line.len points = [Point(line.p1.x, line.p1.y, minz),
# we want to have at least five steps each Point(line.p2.x, line.p2.y, minz)]
num_of_steps = max(5, 1 + ceil(distance / horiz_step)) else:
# steps may be negative p1 = Point(line.p1.x, line.p1.y, minz)
x_step = (p2.x - p1.x) / (num_of_steps - 1) p2 = Point(line.p2.x, line.p2.y, minz)
y_step = (p2.y - p1.y) / (num_of_steps - 1) distance = line.len
x_steps = [(p1.x + i * x_step) for i in range(num_of_steps)] # we want to have at least five steps each
y_steps = [(p1.y + i * y_step) for i in range(num_of_steps)] num_of_steps = max(5, 1 + ceil(distance / horiz_step))
step_coords = zip(x_steps, y_steps) # steps may be negative
x_step = (p2.x - p1.x) / (num_of_steps - 1)
last_position = None y_step = (p2.y - p1.y) / (num_of_steps - 1)
x_steps = [(p1.x + i * x_step) for i in range(num_of_steps)]
for x, y in step_coords: y_steps = [(p1.y + i * y_step) for i in range(num_of_steps)]
if not self.combined_model: step_coords = zip(x_steps, y_steps)
# no obstacle -> minimum height points = get_max_height_dynamic(self.combined_model, self.cutter,
points = [Point(x, y, minz)] step_coords, minz, maxz, self.physics)
elif self.physics: for p in points:
points = get_max_height_ode(self.physics, x, y, minz, maxz) pa.append(p)
else: # "draw_callback" returns true, if the user requested quitting via
points = get_max_height_triangles(self.combined_model, self.cutter, # the GUI.
x, y, minz, maxz, last_pos=last_position) if draw_callback and points:
draw_callback(tool_position=points[-1], toolpath=pa.paths)
if points:
for p in points:
pa.append(p)
else:
p = Point(x, y, self.safety_height)
pa.append(p)
if not self._boundary_warning_already_shown:
log.warn("EngraveCutter: exceed the height " \
+ "of the boundary box: using a safe height " \
+ "instead. This warning is reported only once.")
self._boundary_warning_already_shown = True
# "draw_callback" returns true, if the user requested quitting via
# the GUI.
if draw_callback \
and draw_callback(tool_position=p, toolpath=pa.paths):
break
pa.end_scanline() pa.end_scanline()
pa.end_direction() pa.end_direction()
...@@ -215,30 +215,14 @@ def get_max_height_ode(physics, x, y, minz, maxz): ...@@ -215,30 +215,14 @@ def get_max_height_ode(physics, x, y, minz, maxz):
safe_z = current_z safe_z = current_z
trips -= 1 trips -= 1
if safe_z is None: if safe_z is None:
# no safe position was found - let's check the upper bound # return maxz as the collision height
physics.set_drill_position((x, y, maxz)) return Point(x, y, maxz)
if physics.check_collision():
# the object fills the whole range of z0..z1 -> no safe height
pass
else:
# at least the upper bound is collision free
safe_z = maxz
if safe_z is None:
return []
else: else:
return [Point(x, y, safe_z)] return Point(x, y, safe_z)
def get_max_height_triangles(model, cutter, x, y, minz, maxz, last_pos=None): def get_max_height_triangles(model, cutter, x, y, minz, maxz):
result = []
if last_pos is None:
last_pos = {}
for key in ("triangle", "cut"):
if not key in last_pos:
last_pos[key] = None
p = Point(x, y, maxz) p = Point(x, y, maxz)
height_max = None height_max = None
cut_max = None
triangle_max = None
box_x_min = cutter.get_minx(p) box_x_min = cutter.get_minx(p)
box_x_max = cutter.get_maxx(p) box_x_max = cutter.get_maxx(p)
box_y_min = cutter.get_miny(p) box_y_min = cutter.get_miny(p)
...@@ -251,61 +235,61 @@ def get_max_height_triangles(model, cutter, x, y, minz, maxz, last_pos=None): ...@@ -251,61 +235,61 @@ def get_max_height_triangles(model, cutter, x, y, minz, maxz, last_pos=None):
cut = cutter.drop(t, start=p) cut = cutter.drop(t, start=p)
if cut and ((height_max is None) or (cut.z > height_max)): if cut and ((height_max is None) or (cut.z > height_max)):
height_max = cut.z height_max = cut.z
cut_max = cut
triangle_max = t
# don't do a complete boundary check for the height # don't do a complete boundary check for the height
# this avoids zero-cuts for models that exceed the bounding box height # this avoids zero-cuts for models that exceed the bounding box height
if not cut_max or cut_max.z < minz + epsilon: if (height_max is None) or (height_max < minz + epsilon):
cut_max = Point(x, y, minz) height_max = minz
if last_pos["cut"] and \ # check if we need more points in between (for better accuracy)
((triangle_max and not last_pos["triangle"]) \ return Point(x, y, height_max)
or (last_pos["triangle"] and not triangle_max)):
if minz - epsilon <= last_pos["cut"].z <= maxz + epsilon:
result.append(Point(last_pos["cut"].x, last_pos["cut"].y,
cut_max.z))
else:
result.append(Point(cut_max.x, cut_max.y, last_pos["cut"].z))
elif (triangle_max and last_pos["triangle"] and last_pos["cut"] and \
cut_max) and (triangle_max != last_pos["triangle"]):
# TODO: check if this path is ever in use (e.g. "intersect_lines" is not
# defined)
nl = range(3)
nl[0] = -getattr(last_pos["triangle"].normal, order[0])
nl[2] = last_pos["triangle"].normal.z
nm = range(3)
nm[0] = -getattr(triangle_max.normal, order[0])
nm[2] = triangle_max.normal.z
last = range(3)
last[0] = getattr(last_pos["cut"], order[0])
last[2] = last_pos["cut"].z
mx = range(3)
mx[0] = getattr(cut_max, order[0])
mx[2] = cut_max.z
c = range(3)
(c[0], c[2]) = intersect_lines(last[0], last[2], nl[0], nl[2], mx[0],
mx[2], nm[0], nm[2])
if c[0] and last[0] < c[0] < mx[0] and (c[2] > last[2] or c[2] > mx[2]):
c[1] = getattr(last_pos["cut"], order[1])
if (c[2] < minz - 10) or (c[2] > maxz + 10):
print "^", "%sl=%s" % (order[0], last[0]), \
", %sl=%s" % ("z", last[2]), \
", n%sl=%s" % (order[0], nl[0]), \
", n%sl=%s" % ("z", nl[2]), \
", %s=%s" % (order[0].upper(), c[0]), \
", %s=%s" % ("z".upper(), c[2]), \
", %sm=%s" % (order[0], mx[0]), \
", %sm=%s" % ("z", mx[2]), \
", n%sm=%s" % (order[0], nm[0]), \
", n%sm=%s" % ("z", nm[2])
else: def _check_deviance_of_adjacent_points(p1, p2, p3, min_distance):
if order[0] == "x": straight = p3.sub(p1)
result.append(Point(c[0], c[1], c[2])) added = p2.sub(p1).norm + p3.sub(p2).norm
else: # compare only the x/y distance of p1 and p3 with min_distance
result.append(Point(c[1], c[0], c[2])) if straight.x ** 2 + straight.y ** 2 < min_distance ** 2:
result.append(cut_max) # the points are too close together
return True
else:
# allow 0.1% deviance - this is an angle of around 2 degrees
return (added / straight.norm) < 1.001
last_pos["cut"] = cut_max def get_max_height_dynamic(model, cutter, positions, minz, maxz, physics=None):
last_pos["triangle"] = triangle_max max_depth = 8
# the points don't need to get closer than 1/1000 of the cutter radius
min_distance = cutter.distance_radius / 1000
result = []
if physics:
get_max_height = lambda x, y: get_max_height_ode(physics, x, y, minz,
maxz)
else:
get_max_height = lambda x, y: get_max_height_triangles(model, cutter,
x, y, minz, maxz)
# add one point between all existing points
for index in range(len(positions)):
p = positions[index]
result.append(get_max_height(p[0], p[1]))
# Check if three consecutive points are "flat".
# Add additional points if necessary.
index = 0
depth_count = 0
while index < len(result) - 2:
p1 = result[index]
p2 = result[index + 1]
p3 = result[index + 2]
if not _check_deviance_of_adjacent_points(p1, p2, p3, min_distance) \
and (depth_count < max_depth):
# distribute the new point two before the middle and one after
if depth_count % 3 != 2:
# insert between the 1st and 2nd point
middle = ((p1.x + p2.x) / 2, (p1.y + p2.y) / 2)
result.insert(index + 1, get_max_height(middle[0], middle[1]))
else:
# insert between the 2nd and 3rd point
middle = ((p2.x + p3.x) / 2, (p2.y + p3.y) / 2)
result.insert(index + 2, get_max_height(middle[0], middle[1]))
depth_count += 1
else:
index += 1
depth_count = 0
return result return result
...@@ -265,8 +265,8 @@ def generate_toolpath(model, tool_settings=None, ...@@ -265,8 +265,8 @@ def generate_toolpath(model, tool_settings=None,
if path_generator == "PushCutter": if path_generator == "PushCutter":
step_width = None step_width = None
else: else:
# TODO: the step_width should be configurable # the step_width is only used for the DropCutter
step_width = tool_settings["tool_radius"] / 10.0 step_width = tool_settings["tool_radius"] / 4
if path_generator == "DropCutter": if path_generator == "DropCutter":
layer_distance = None layer_distance = None
else: else:
......
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