Commit 8a797963 authored by sumpfralle's avatar sumpfralle

improved tool move sorting for Engraving (reduce machine time, inner->outer)


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@1033 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent bf75adaf
...@@ -6,6 +6,7 @@ Version 0.5 - UNRELEASED ...@@ -6,6 +6,7 @@ Version 0.5 - UNRELEASED
* added toolpath cropping * added toolpath cropping
* added toolpath grid pattern: clone a single toolpath in rows and columns * added toolpath grid pattern: clone a single toolpath in rows and columns
* unify DropCutter behaviour for models that are higher than the defined bounding box * unify DropCutter behaviour for models that are higher than the defined bounding box
* more efficient tool moves for Engraving
* Model handling: * Model handling:
* added support for simple multi-layered 2D models * added support for simple multi-layered 2D models
* added 2D projection of 3D models * added 2D projection of 3D models
...@@ -35,9 +36,12 @@ Version 0.5 - UNRELEASED ...@@ -35,9 +36,12 @@ Version 0.5 - UNRELEASED
* added support for single-line fonts text (based on fonts from QCAD) * added support for single-line fonts text (based on fonts from QCAD)
* support non-local file handling (e.g. http, ...) * support non-local file handling (e.g. http, ...)
* added support for more DXF features: * added support for more DXF features:
* 2D: "LWPOLYLINE", "POLYLINE", "CIRCLE", "ARC", "TEXT", "MTEXT" * 2D: LWPOLYLINE, POLYLINE, CIRCLE, ARC, TEXT, MTEXT
* 3D: "3DFACE" * 3D: 3DFACE
* added support for EPS/PS contour files * added support for EPS/PS contour files
* noteworthy changes:
* bounding box for 2D models must be _above_ the model (before: below or above)
* simulation mode: the stock material removal simulation was removed
Version 0.4.0.1 - 2010-10-24 Version 0.4.0.1 - 2010-10-24
* disabled parallel processing for Windows standalone executable * disabled parallel processing for Windows standalone executable
......
...@@ -39,6 +39,174 @@ except ImportError: ...@@ -39,6 +39,174 @@ except ImportError:
log = pycam.Utils.log.get_logger() log = pycam.Utils.log.get_logger()
class PolygonInTree(object):
""" This class is a wrapper around Polygon objects that is used for sorting.
"""
next_id = 0
def __init__(self, polygon):
self.id = PolygonInTree.next_id
PolygonInTree.next_id += 1
self.start = polygon.get_points()[0]
self.end = polygon.get_points()[-1]
self.polygon = polygon
self.area = polygon.get_area()
self.children = []
def __eq__(self, other):
return self.id == other.id
def __cmp__(self, other):
return cmp(self.area, other.area)
def insert_if_child(self, other):
if self.polygon.is_polygon_inside(other.polygon):
self.children.append(other)
def remove_child(self, other):
try:
self.children.remove(other)
except ValueError:
pass
def get_cost(self, other):
return other.start.sub(self.end).norm
class PolygonPositionSorter(object):
""" sort PolygonInTree objects for a minimized way length.
The sorter takes care that no polygons are processed before their children
(inside polygons).
"""
def __init__(self, polygons):
self.polygons = []
for poly in polygons:
self._append(poly)
self.optimize_order()
self.branches = []
for poly in self.polygons:
self.branches.append([poly])
def _append(self, poly):
if self.polygons:
min_cost = poly.get_cost(self.polygons[0])
min_index = -1
for index in range(len(self.polygons)):
prev_item = self.polygons[index]
cost = prev_item.get_cost(poly)
try:
next_item = self.polygons[index + 1]
except IndexError:
pass
else:
cost += poly.get_cost(next_item)
cost -= prev_item.get_cost(next_item)
if cost < min_cost:
min_cost = cost
min_index = index
self.polygons.insert(min_index + 1, poly)
else:
self.polygons.append(poly)
def append(self, poly):
min_cost = None
min_branch = None
for branch_index in range(len(self.branches) - 1, -1, -1):
this_branch = self.branches[branch_index]
cost = this_branch[-1].get_cost(poly)
try:
next_branch = self.branches[branch_index + 1]
except IndexError:
pass
else:
cost += poly.get_cost(next_branch[0])
cost -= this_branch[-1].get_cost(next_branch[0])
if (min_cost is None) or (cost < min_cost):
min_cost = cost
min_branch = this_branch
for child in poly.children:
if child in this_branch:
break
else:
continue
break
if min_branch:
min_branch.append(poly)
def optimize_order(self):
""" re-insert all items until their order stabilizes """
finished = False
counter_left = len(self.polygons)
while not finished and (counter_left > 0):
finished = True
for index in range(len(self.polygons)):
item = self.polygons.pop(index)
self._append(item)
if self.polygons[index] != item:
finished = False
counter_left -= 1
def get_polygons(self):
result = []
for branch in self.branches:
result.extend(branch)
return result
class PolygonSorter(object):
""" sort Plygon instances according to the following rules:
* inner polygons first (with no inside polygons)
* inner polygons with inside polygons that are already processed
* outer polygons (with no polygons inside that are not yet processed)
* remaining outer polygons
The order of polygons is slightly optimized (minimizing the way length).
"""
def __init__(self, polygons, callback=None):
self.polygons = []
self.sorter = None
self.callback = callback
for poly in polygons:
self._append(poly)
self.optimize_order()
def _append(self, polygon):
new_item = PolygonInTree(polygon)
for item in self.polygons:
item.insert_if_child(new_item)
new_item.insert_if_child(item)
self.polygons.append(new_item)
def optimize_order(self):
self.polygons.sort()
remaining_polygons = list(self.polygons)
done_polygons = []
while remaining_polygons:
if self.callback:
self.callback()
usable_polys = []
for poly in remaining_polygons:
for child in poly.children:
if not child in done_polygons:
break
else:
usable_polys.append(poly)
for poly in usable_polys:
remaining_polygons.remove(poly)
if self.sorter is None:
self.sorter = PolygonPositionSorter(usable_polys)
else:
for poly in usable_polys:
self.sorter.append(poly)
done_polygons.extend(usable_polys)
def get_polygons(self):
return [poly.polygon for poly in self.sorter.get_polygons()]
class Polygon(TransformableContainer): class Polygon(TransformableContainer):
def __init__(self, plane=None): def __init__(self, plane=None):
...@@ -271,6 +439,12 @@ class Polygon(TransformableContainer): ...@@ -271,6 +439,12 @@ class Polygon(TransformableContainer):
return self.get_area() > 0 return self.get_area() > 0
def is_polygon_inside(self, polygon): def is_polygon_inside(self, polygon):
if not self.is_closed:
return False
if (self.minx > polygon.maxx) or (self.maxx < polygon.minx) or \
(self.miny > polygon.maxy) or (self.maxy < polygon.miny) or \
(self.minz > polygon.maxz) or (self.maxz < polygon.minz):
return False
for point in polygon._points: for point in polygon._points:
if not self.is_point_inside(point): if not self.is_point_inside(point):
return False return False
...@@ -286,6 +460,8 @@ class Polygon(TransformableContainer): ...@@ -286,6 +460,8 @@ class Polygon(TransformableContainer):
""" Test if a given point is inside of the polygon. """ Test if a given point is inside of the polygon.
The result is True if the point is on a line (or very close to it). The result is True if the point is on a line (or very close to it).
""" """
if not self.is_closed:
return False
# First: check if the point is within the boundary of the polygon. # First: check if the point is within the boundary of the polygon.
if not p.is_inside(self.minx, self.maxx, self.miny, self.maxy, if not p.is_inside(self.minx, self.maxx, self.miny, self.maxy,
self.minz, self.maxz): self.minz, self.maxz):
......
...@@ -25,6 +25,7 @@ import pycam.PathProcessors.PathAccumulator ...@@ -25,6 +25,7 @@ import pycam.PathProcessors.PathAccumulator
from pycam.Geometry.Point import Point, Vector 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.Polygon import PolygonSorter
from pycam.Geometry.utils import ceil from pycam.Geometry.utils import ceil
from pycam.PathGenerators import get_max_height_dynamic, get_free_paths_ode, \ from pycam.PathGenerators import get_max_height_dynamic, get_free_paths_ode, \
get_free_paths_triangles get_free_paths_triangles
...@@ -47,10 +48,8 @@ class EngraveCutter: ...@@ -47,10 +48,8 @@ class EngraveCutter:
self.combined_model += model self.combined_model += model
else: else:
self.combined_model = [] self.combined_model = []
if clockwise: self.clockwise = clockwise
self.contour_model = contour_model.get_reversed() self.contour_model = contour_model
else:
self.contour_model = contour_model
self.pa_push = path_processor self.pa_push = path_processor
# We use a separated path processor for the last "drop" layer. # We use a separated path processor for the last "drop" layer.
# This path processor does not need to be configurable. # This path processor does not need to be configurable.
...@@ -77,7 +76,24 @@ class EngraveCutter: ...@@ -77,7 +76,24 @@ class EngraveCutter:
progress_counter = ProgressCounter(len(z_steps) * num_of_lines, progress_counter = ProgressCounter(len(z_steps) * num_of_lines,
draw_callback) draw_callback)
line_groups = self.contour_model.get_polygons() if draw_callback:
draw_callback(text="Engrave: optimizing polygon order")
# Sort the polygons according to their directions (first inside, then
# outside. This reduces the problem of break-away pieces.
inner_polys = []
outer_polys = []
for poly in self.contour_model.get_polygons():
if poly.get_area() <= 0:
inner_polys.append(poly)
else:
outer_polys.append(poly)
inner_sorter = PolygonSorter(inner_polys, callback=draw_callback)
outer_sorter = PolygonSorter(outer_polys, callback=draw_callback)
line_groups = inner_sorter.get_polygons() + outer_sorter.get_polygons()
if self.clockwise:
for line_group in line_groups:
line_group.reverse_direction()
# push slices for all layers above ground # push slices for all layers above ground
if maxz == minz: if maxz == minz:
# only one layer - use PushCutter instead of DropCutter # only one layer - use PushCutter instead of DropCutter
...@@ -94,7 +110,7 @@ class EngraveCutter: ...@@ -94,7 +110,7 @@ class EngraveCutter:
for z in push_steps: for z in push_steps:
# 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" \
+ " layer %d/%d" % (current_layer, num_of_layers)): + " layer %d/%d" % (current_layer + 1, num_of_layers)):
# cancel immediately # cancel immediately
break break
for line_group in line_groups: for line_group in line_groups:
...@@ -116,40 +132,13 @@ class EngraveCutter: ...@@ -116,40 +132,13 @@ class EngraveCutter:
if quit_requested: if quit_requested:
return self.pa_push.paths return self.pa_push.paths
if draw_callback:
draw_callback(text="Engrave: processing layer %d/%d" \
% (current_layer + 1, num_of_layers))
# Sort the polygons according to their directions (first inside, then
# outside.
# This reduces the problem of break-away pieces.
# We do the sorting just before the final layer (breakage does not
# happen before).
def polygon_priority(poly1, poly2):
""" polygon priority comparison: first holes and open polygons, then
outlines (roughly sorted by ascending area size)
TODO: ordering according to the locations and groups of polygons
would be even better.
"""
area1 = poly1.get_area()
area2 = poly2.get_area()
if (area1 <= 0) and (area2 > 0):
return -1
elif (area2 <= 0) and (area1 > 0):
return 1
else:
# do a "relaxed" sorting by size
if abs(area1) < 2 * abs(area2):
return -1
elif abs(area2) < 2 * abs(area1):
return 1
else:
return 0
line_groups.sort(cmp=polygon_priority)
for z in drop_steps: for z in drop_steps:
if draw_callback:
draw_callback(text="Engrave: processing layer %d/%d" \
% (current_layer + 1, num_of_layers))
# process the final layer with a drop cutter # process the final layer with a drop cutter
for line_group in self.contour_model.get_polygons(): for line_group in line_groups:
self.pa_drop.new_direction(0) self.pa_drop.new_direction(0)
self.pa_drop.new_scanline() self.pa_drop.new_scanline()
for line in line_group.get_lines(): for line in line_group.get_lines():
...@@ -164,6 +153,7 @@ class EngraveCutter: ...@@ -164,6 +153,7 @@ class EngraveCutter:
# break the outer loop if requested # break the outer loop if requested
if quit_requested: if quit_requested:
break break
current_layer += 1
last_z = z last_z = z
self.pa_drop.finish() self.pa_drop.finish()
......
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