Commit 572a93cc authored by sumpfralle's avatar sumpfralle

added support for multiple trimesh models to all path generators

* this allows a better handling of the support grid (especially for the ContourFollow strategy)
fixed an old bug regarding the "get_free_path_triangles" function: it returned a free path even if the start and end points were completely within the model


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@770 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent ff46ead3
...@@ -189,9 +189,9 @@ class CollisionPaths: ...@@ -189,9 +189,9 @@ class CollisionPaths:
class ContourFollow: class ContourFollow:
def __init__(self, cutter, model, path_processor, physics=None): def __init__(self, cutter, models, path_processor, physics=None):
self.cutter = cutter self.cutter = cutter
self.model = model self.models = models
self.pa = path_processor self.pa = path_processor
self._up_vector = Vector(0, 0, 1) self._up_vector = Vector(0, 0, 1)
self.physics = physics self.physics = physics
...@@ -199,8 +199,11 @@ class ContourFollow: ...@@ -199,8 +199,11 @@ class ContourFollow:
if self.physics: if self.physics:
accuracy = 20 accuracy = 20
max_depth = 16 max_depth = 16
model_dim = max(abs(self.model.maxx - self.model.minx), maxx = max([m.maxx for m in self.models])
abs(self.model.maxy - self.model.miny)) minx = max([m.minx for m in self.models])
maxy = max([m.maxy for m in self.models])
miny = max([m.miny for m in self.models])
model_dim = max(abs(maxx - minx), abs(maxy - miny))
depth = math.log(accuracy * model_dim / self.cutter.radius) / math.log(2) depth = math.log(accuracy * model_dim / self.cutter.radius) / math.log(2)
self._physics_maxdepth = min(max_depth, max(ceil(depth_x), 4)) self._physics_maxdepth = min(max_depth, max(ceil(depth_x), 4))
...@@ -209,7 +212,7 @@ class ContourFollow: ...@@ -209,7 +212,7 @@ class ContourFollow:
return get_free_paths_ode(self.physics, p1, p2, return get_free_paths_ode(self.physics, p1, p2,
depth=self._physics_maxdepth) depth=self._physics_maxdepth)
else: else:
return get_free_paths_triangles(self.model, self.cutter, p1, p2) return get_free_paths_triangles(self.models, self.cutter, p1, p2)
def GenerateToolPath(self, minx, maxx, miny, maxy, minz, maxz, dz, def GenerateToolPath(self, minx, maxx, miny, maxy, minz, maxz, dz,
draw_callback=None): draw_callback=None):
...@@ -225,7 +228,8 @@ class ContourFollow: ...@@ -225,7 +228,8 @@ class ContourFollow:
num_of_layers = 1 + ceil(diff_z / dz) num_of_layers = 1 + ceil(diff_z / dz)
z_step = diff_z / max(1, (num_of_layers - 1)) z_step = diff_z / max(1, (num_of_layers - 1))
num_of_triangles = len(self.model.triangles(minx=minx, miny=miny, maxx=maxx, maxy=maxy)) # only the first model is used for the contour-follow algorithm
num_of_triangles = len(self.models[0].triangles(minx=minx, miny=miny, maxx=maxx, maxy=maxy))
progress_counter = ProgressCounter(2 * num_of_layers * num_of_triangles, progress_counter = ProgressCounter(2 * num_of_layers * num_of_triangles,
draw_callback) draw_callback)
...@@ -242,16 +246,18 @@ class ContourFollow: ...@@ -242,16 +246,18 @@ class ContourFollow:
break break
self.pa.new_direction(0) self.pa.new_direction(0)
self.GenerateToolPathSlice(minx, maxx, miny, maxy, z, self.GenerateToolPathSlice(minx, maxx, miny, maxy, z,
draw_callback, progress_counter) draw_callback, progress_counter, num_of_triangles)
self.pa.end_direction() self.pa.end_direction()
self.pa.finish() self.pa.finish()
current_layer += 1 current_layer += 1
return self.pa.paths return self.pa.paths
def GenerateToolPathSlice(self, minx, maxx, miny, maxy, z, def GenerateToolPathSlice(self, minx, maxx, miny, maxy, z,
draw_callback=None, progress_counter=None): draw_callback=None, progress_counter=None, num_of_triangles=None):
shifted_lines = self.get_potential_contour_lines(minx, maxx, miny, maxy, shifted_lines = self.get_potential_contour_lines(minx, maxx, miny, maxy,
z, progress_counter=progress_counter) z, progress_counter=progress_counter)
if num_of_triangles is None:
num_of_triangles = len(shifted_lines)
last_position = None last_position = None
self.pa.new_scanline() self.pa.new_scanline()
for line in shifted_lines: for line in shifted_lines:
...@@ -275,19 +281,21 @@ class ContourFollow: ...@@ -275,19 +281,21 @@ class ContourFollow:
break break
# the progress counter jumps up by the number of non directly processed triangles # the progress counter jumps up by the number of non directly processed triangles
if not progress_counter is None: if not progress_counter is None:
progress_counter.increment(len(self.model.triangles()) - len(shifted_lines)) progress_counter.increment(num_of_triangles - len(shifted_lines))
self.pa.end_scanline() self.pa.end_scanline()
return self.pa.paths return self.pa.paths
def get_potential_contour_lines(self, minx, maxx, miny, maxy, z, def get_potential_contour_lines(self, minx, maxx, miny, maxy, z,
progress_counter=None): progress_counter=None):
# use only the first model for the contour
follow_model = self.models[0]
plane = Plane(Point(0, 0, z), self._up_vector) plane = Plane(Point(0, 0, z), self._up_vector)
lines = [] lines = []
waterline_triangles = CollisionPaths() waterline_triangles = CollisionPaths()
projected_waterlines = [] projected_waterlines = []
triangles = self.model.triangles(minx=minx, miny=miny, maxx=maxx, triangles = follow_model.triangles(minx=minx, miny=miny, maxx=maxx,
maxy=maxy) maxy=maxy)
args = [(self.model, self.cutter, self._up_vector, t, z) args = [(follow_model, self.cutter, self._up_vector, t, z)
for t in triangles if not id(t) in self._processed_triangles] for t in triangles if not id(t) in self._processed_triangles]
results_iter = run_in_parallel(_process_one_triangle, args, results_iter = run_in_parallel(_process_one_triangle, args,
unordered=True) unordered=True)
...@@ -458,7 +466,7 @@ def get_collision_waterline_of_triangle(model, cutter, up_vector, triangle, z): ...@@ -458,7 +466,7 @@ def get_collision_waterline_of_triangle(model, cutter, up_vector, triangle, z):
start = edge.p1.add(edge_dir.mul(factor)) start = edge.p1.add(edge_dir.mul(factor))
# We need to use the triangle collision algorithm here - because we # We need to use the triangle collision algorithm here - because we
# need the point of collision in the triangle. # need the point of collision in the triangle.
collisions = get_free_paths_triangles(model, cutter, start, collisions = get_free_paths_triangles([model], cutter, start,
start.add(direction), return_triangles=True) start.add(direction), return_triangles=True)
for index, coll in enumerate(collisions): for index, coll in enumerate(collisions):
if (index % 2 == 0) and (not coll[1] is None) \ if (index % 2 == 0) and (not coll[1] is None) \
......
...@@ -86,9 +86,12 @@ class Dimension: ...@@ -86,9 +86,12 @@ class Dimension:
class DropCutter: class DropCutter:
def __init__(self, cutter, model, path_processor, physics=None): def __init__(self, cutter, models, path_processor, physics=None):
self.cutter = cutter self.cutter = cutter
self.model = model # combine the models (if there is more than one)
self.model = models[0]
for model in models[1:]:
self.model += model
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
......
...@@ -34,10 +34,17 @@ log = pycam.Utils.log.get_logger() ...@@ -34,10 +34,17 @@ log = pycam.Utils.log.get_logger()
class EngraveCutter: class EngraveCutter:
def __init__(self, cutter, model, contour_model, path_processor, def __init__(self, cutter, trimesh_models, contour_model, path_processor,
physics=None, safety_height=INFINITE): physics=None, safety_height=INFINITE):
self.cutter = cutter self.cutter = cutter
self.model = model self.models = trimesh_models
# combine the models (if there is more than one)
if self.models:
self.combined_model = self.models[0]
for model in self.models[1:]:
self.combined_model += model
else:
self.combined_models = []
self.contour_model = contour_model 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.
...@@ -153,12 +160,13 @@ class EngraveCutter: ...@@ -153,12 +160,13 @@ class EngraveCutter:
p2 = Point(line.p2.x, line.p2.y, z) p2 = Point(line.p2.x, line.p2.y, z)
# no model -> no possible obstacles # no model -> no possible obstacles
# model is completely below z (e.g. support bridges) -> no obstacles # model is completely below z (e.g. support bridges) -> no obstacles
if not self.model or (self.model.maxz < z): relevant_models = [m for m in self.models if m.maxz >= z]
if not relevant_models:
points = [p1, p2] points = [p1, p2]
elif self.physics: elif self.physics:
points = get_free_paths_ode(self.physics, p1, p2) points = get_free_paths_ode(self.physics, p1, p2)
else: else:
points = get_free_paths_triangles(self.model, self.cutter, p1, p2) points = get_free_paths_triangles(relevant_models, self.cutter, p1, p2)
if points: if points:
for p in points: for p in points:
pa.append(p) pa.append(p)
...@@ -185,13 +193,13 @@ class EngraveCutter: ...@@ -185,13 +193,13 @@ class EngraveCutter:
last_position = None last_position = None
for x, y in step_coords: for x, y in step_coords:
if not self.model: if not self.combined_model:
# no obstacle -> minimum height # no obstacle -> minimum height
points = [Point(x, y, minz)] points = [Point(x, y, minz)]
elif self.physics: elif self.physics:
points = get_max_height_ode(self.physics, x, y, minz, maxz) points = get_max_height_ode(self.physics, x, y, minz, maxz)
else: else:
points = get_max_height_triangles(self.model, self.cutter, points = get_max_height_triangles(self.combined_model, self.cutter,
x, y, minz, maxz, last_pos=last_position) x, y, minz, maxz, last_pos=last_position)
if points: if points:
......
...@@ -31,19 +31,19 @@ import math ...@@ -31,19 +31,19 @@ import math
# 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_line((p1, p2, depth, model, cutter, physics)): def _process_one_line((p1, p2, depth, models, cutter, physics)):
if physics: if physics:
points = get_free_paths_ode(physics, p1, p2, depth=depth) points = get_free_paths_ode(physics, p1, p2, depth=depth)
else: else:
points = get_free_paths_triangles(model, cutter, p1, p2) points = get_free_paths_triangles(models, cutter, p1, p2)
return points return points
class PushCutter: class PushCutter:
def __init__(self, cutter, model, path_processor, physics=None): def __init__(self, cutter, models, path_processor, physics=None):
self.cutter = cutter self.cutter = cutter
self.model = model self.models = models
self.pa = path_processor self.pa = path_processor
self.physics = physics self.physics = physics
...@@ -100,7 +100,7 @@ class PushCutter: ...@@ -100,7 +100,7 @@ class PushCutter:
args = [] args = []
for line in layer_grid: for line in layer_grid:
p1, p2 = line p1, p2 = line
args.append((p1, p2, depth, self.model, self.cutter, self.physics)) args.append((p1, p2, depth, self.models, self.cutter, self.physics))
# ODE does not work with multi-threading # ODE does not work with multi-threading
disable_multiprocessing = not self.physics is None disable_multiprocessing = not self.physics is None
......
...@@ -41,7 +41,27 @@ class Hit: ...@@ -41,7 +41,27 @@ class Hit:
return "%s - %s - %s - %s" % (self.d, self.cl, self.dir, self.cp) return "%s - %s - %s - %s" % (self.d, self.cl, self.dir, self.cp)
def get_free_paths_triangles(model, cutter, p1, p2, return_triangles=False): def get_free_paths_triangles(models, cutter, p1, p2, return_triangles=False):
if len(models) == 1:
# only one model is left - just continue
model = models[0]
else:
# multiple models were given - process them in layers
result = get_free_paths_triangles(models[:1], cutter, p1, p2,
return_triangles)
# group the result into pairs of two points (start/end)
point_pairs = []
while result:
pair1 = result.pop(0)
pair2 = result.pop(0)
point_pairs.append((pair1, pair2))
all_results = []
for pair in point_pairs:
one_result = get_free_paths_triangles(models[1:], cutter, pair[0],
pair[1], return_triangles)
all_results.extend(one_result)
return all_results
backward = p1.sub(p2).normalized() backward = p1.sub(p2).normalized()
forward = p2.sub(p1).normalized() forward = p2.sub(p1).normalized()
xyz_dist = p2.sub(p1).norm xyz_dist = p2.sub(p1).norm
...@@ -90,8 +110,20 @@ def get_free_paths_triangles(model, cutter, p1, p2, return_triangles=False): ...@@ -90,8 +110,20 @@ def get_free_paths_triangles(model, cutter, p1, p2, return_triangles=False):
points.append((p2, None, None)) points.append((p2, None, None))
if len(points) == 0: if len(points) == 0:
points.append((p1, None, None)) # check if the path is completely free or if we are inside of the model
points.append((p2, None, None)) inside_counter = 0
for h in hits:
if -epsilon <= h.d:
# we reached the outer limit of the model
break
if h.dir == forward:
inside_counter += 1
else:
inside_counter -= 1
if inside_counter <= 0:
# we are not inside of the model
points.append((p1, None, None))
points.append((p2, None, None))
if return_triangles: if return_triangles:
return points return points
......
...@@ -137,14 +137,13 @@ def generate_toolpath(model, tool_settings=None, ...@@ -137,14 +137,13 @@ def generate_toolpath(model, tool_settings=None,
# trimesh model or contour model? # trimesh model or contour model?
if isinstance(model, pycam.Geometry.Model.Model): if isinstance(model, pycam.Geometry.Model.Model):
# trimesh model # trimesh model
trimesh_model = model trimesh_models = [model]
contour_model = None contour_model = None
else: else:
# contour model # contour model
trimesh_model = pycam.Geometry.Model.Model() trimesh_models = []
contour_model = model contour_model = model
# create the grid model if requested # create the grid model if requested
trimesh_models = [trimesh_model]
if (support_grid_type == "grid") \ if (support_grid_type == "grid") \
and (((not support_grid_distance_x is None) \ and (((not support_grid_distance_x is None) \
or (not support_grid_distance_y is None)) \ or (not support_grid_distance_y is None)) \
...@@ -194,7 +193,7 @@ def generate_toolpath(model, tool_settings=None, ...@@ -194,7 +193,7 @@ def generate_toolpath(model, tool_settings=None,
if not contour_model is None: if not contour_model is None:
model = contour_model model = contour_model
else: else:
model = trimesh_model model = trimesh_models[0]
support_grid_model = pycam.Toolpath.SupportGrid.get_support_distributed( support_grid_model = pycam.Toolpath.SupportGrid.get_support_distributed(
model, minz, support_grid_average_distance, model, minz, support_grid_average_distance,
support_grid_minimum_bridges, support_grid_thickness, support_grid_minimum_bridges, support_grid_thickness,
...@@ -242,7 +241,7 @@ def generate_toolpath(model, tool_settings=None, ...@@ -242,7 +241,7 @@ def generate_toolpath(model, tool_settings=None,
return "No part of the contour model is within the bounding box." return "No part of the contour model is within the bounding box."
# Due to some weirdness the height of the drill must be bigger than the # Due to some weirdness the height of the drill must be bigger than the
# object's size. Otherwise some collisions are not detected. # object's size. Otherwise some collisions are not detected.
cutter_height = 4 * (maxy - miny) cutter_height = 4 * abs(maxz - minz)
cutter = pycam.Cutters.get_tool_from_settings(tool_settings, cutter_height) cutter = pycam.Cutters.get_tool_from_settings(tool_settings, cutter_height)
if isinstance(cutter, basestring): if isinstance(cutter, basestring):
return cutter return cutter
...@@ -252,10 +251,7 @@ def generate_toolpath(model, tool_settings=None, ...@@ -252,10 +251,7 @@ def generate_toolpath(model, tool_settings=None,
physics = _get_physics(trimesh_models, cutter, calculation_backend) physics = _get_physics(trimesh_models, cutter, calculation_backend)
if isinstance(physics, basestring): if isinstance(physics, basestring):
return physics return physics
combined_models = trimesh_models[0] generator = _get_pathgenerator_instance(trimesh_models, contour_model,
for next_model in trimesh_models[1:]:
combined_models += next_model
generator = _get_pathgenerator_instance(combined_models, contour_model,
cutter, path_generator, path_postprocessor, milling_style, physics) cutter, path_generator, path_postprocessor, milling_style, physics)
if isinstance(generator, basestring): if isinstance(generator, basestring):
return generator return generator
...@@ -309,9 +305,9 @@ def generate_toolpath(model, tool_settings=None, ...@@ -309,9 +305,9 @@ def generate_toolpath(model, tool_settings=None,
% (path_generator, PATH_GENERATORS) % (path_generator, PATH_GENERATORS)
return toolpath return toolpath
def _get_pathgenerator_instance(trimesh_model, contour_model, cutter, def _get_pathgenerator_instance(trimesh_models, contour_model, cutter,
pathgenerator, pathprocessor, reverse, physics): pathgenerator, pathprocessor, reverse, physics):
if pathgenerator != "EngraveCutter" and not trimesh_model.triangles(): if pathgenerator != "EngraveCutter" and contour_model:
return ("The only available toolpath strategy for 2D contour models " \ return ("The only available toolpath strategy for 2D contour models " \
+ "is 'Engraving'.") + "is 'Engraving'.")
if pathgenerator == "DropCutter": if pathgenerator == "DropCutter":
...@@ -324,7 +320,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter, ...@@ -324,7 +320,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter,
return ("Invalid postprocessor (%s) for 'DropCutter': only " \ return ("Invalid postprocessor (%s) for 'DropCutter': only " \
+ "'ZigZagCutter' or 'PathAccumulator' are allowed") \ + "'ZigZagCutter' or 'PathAccumulator' are allowed") \
% str(pathprocessor) % str(pathprocessor)
return DropCutter.DropCutter(cutter, trimesh_model, processor, return DropCutter.DropCutter(cutter, trimesh_models, processor,
physics=physics) physics=physics)
elif pathgenerator == "PushCutter": elif pathgenerator == "PushCutter":
if pathprocessor == "PathAccumulator": if pathprocessor == "PathAccumulator":
...@@ -340,7 +336,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter, ...@@ -340,7 +336,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter,
else: else:
return ("Invalid postprocessor (%s) for 'PushCutter' - it should " \ return ("Invalid postprocessor (%s) for 'PushCutter' - it should " \
+ "be one of these: %s") % (processor, PATH_POSTPROCESSORS) + "be one of these: %s") % (processor, PATH_POSTPROCESSORS)
return PushCutter.PushCutter(cutter, trimesh_model, processor, return PushCutter.PushCutter(cutter, trimesh_models, processor,
physics=physics) physics=physics)
elif pathgenerator == "EngraveCutter": elif pathgenerator == "EngraveCutter":
if pathprocessor == "SimpleCutter": if pathprocessor == "SimpleCutter":
...@@ -351,7 +347,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter, ...@@ -351,7 +347,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter,
if not contour_model: if not contour_model:
return "The 'Engraving' toolpath strategy requires a 2D contour " \ return "The 'Engraving' toolpath strategy requires a 2D contour " \
+ "model (e.g. from a DXF or SVG file)." + "model (e.g. from a DXF or SVG file)."
return EngraveCutter.EngraveCutter(cutter, trimesh_model, return EngraveCutter.EngraveCutter(cutter, trimesh_models,
contour_model, processor, physics=physics) contour_model, processor, physics=physics)
elif pathgenerator == "ContourFollow": elif pathgenerator == "ContourFollow":
if pathprocessor == "SimpleCutter": if pathprocessor == "SimpleCutter":
...@@ -359,7 +355,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter, ...@@ -359,7 +355,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter,
else: else:
return ("Invalid postprocessor (%s) for 'ContourFollow' - it " \ return ("Invalid postprocessor (%s) for 'ContourFollow' - it " \
+ "should be: SimpleCutter") % str(processor) + "should be: SimpleCutter") % str(processor)
return ContourFollow.ContourFollow(cutter, trimesh_model, processor, return ContourFollow.ContourFollow(cutter, trimesh_models, processor,
physics=physics) physics=physics)
else: else:
return "Invalid path generator (%s): not one of %s" \ return "Invalid path generator (%s): not one of %s" \
......
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