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
* added toolpath cropping
* 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
* more efficient tool moves for Engraving
* Model handling:
* added support for simple multi-layered 2D models
* added 2D projection of 3D models
......@@ -35,9 +36,12 @@ Version 0.5 - UNRELEASED
* added support for single-line fonts text (based on fonts from QCAD)
* support non-local file handling (e.g. http, ...)
* added support for more DXF features:
* 2D: "LWPOLYLINE", "POLYLINE", "CIRCLE", "ARC", "TEXT", "MTEXT"
* 3D: "3DFACE"
* 2D: LWPOLYLINE, POLYLINE, CIRCLE, ARC, TEXT, MTEXT
* 3D: 3DFACE
* 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
* disabled parallel processing for Windows standalone executable
......
......@@ -39,6 +39,174 @@ except ImportError:
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):
def __init__(self, plane=None):
......@@ -271,6 +439,12 @@ class Polygon(TransformableContainer):
return self.get_area() > 0
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:
if not self.is_point_inside(point):
return False
......@@ -286,6 +460,8 @@ class Polygon(TransformableContainer):
""" 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).
"""
if not self.is_closed:
return False
# 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,
self.minz, self.maxz):
......
......@@ -25,6 +25,7 @@ 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.Polygon import PolygonSorter
from pycam.Geometry.utils import ceil
from pycam.PathGenerators import get_max_height_dynamic, get_free_paths_ode, \
get_free_paths_triangles
......@@ -47,10 +48,8 @@ class EngraveCutter:
self.combined_model += model
else:
self.combined_model = []
if clockwise:
self.contour_model = contour_model.get_reversed()
else:
self.contour_model = contour_model
self.clockwise = clockwise
self.contour_model = contour_model
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.
......@@ -77,7 +76,24 @@ class EngraveCutter:
progress_counter = ProgressCounter(len(z_steps) * num_of_lines,
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
if maxz == minz:
# only one layer - use PushCutter instead of DropCutter
......@@ -94,7 +110,7 @@ class EngraveCutter:
for z in push_steps:
# update the progress bar and check, if we should cancel the process
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
break
for line_group in line_groups:
......@@ -116,40 +132,13 @@ class EngraveCutter:
if quit_requested:
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:
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
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_scanline()
for line in line_group.get_lines():
......@@ -164,6 +153,7 @@ class EngraveCutter:
# break the outer loop if requested
if quit_requested:
break
current_layer += 1
last_z = z
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