Commit c6f0edc9 authored by sumpfralle's avatar sumpfralle

moved the toolpath generation code to a separate module

renamed configuration setting "overlap" to "overlap_percent"


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@326 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent 409a2478
Version 0.2.5 - NOT RELEASED
* changed name of configuration setting "overlap" to "overlap_percent"
(you may need to change this name in your custom config files)
Version 0.2.4 - 2010-04-12
* added a simple simulation mode for visualizing the material penetration of a toolpath
* join tangential moves (removes the inner points in a colinear set of adjacent path points)
......
......@@ -9,15 +9,17 @@ class BaseCutter:
id = 0
vertical = Point(0,0,-1)
def __init__(self, radius, location=None, height=10):
def __init__(self, radius, location=None, height=None):
if location is None:
location = Point(0, 0, 0)
self.location = location
if height is None:
height = 10
self.height = height
self.id = BaseCutter.id
BaseCutter.id += 1
self.radius = radius
self.radiussq = radius*radius
self.height = height
self.required_distance = 0
self.distance_radius = self.radius
self.distance_radiussq = self.distance_radius * self.distance_radius
......
......@@ -5,3 +5,34 @@ from BaseCutter import BaseCutter
from SphericalCutter import SphericalCutter
from CylindricalCutter import CylindricalCutter
from ToroidalCutter import ToroidalCutter
def get_tool_from_settings(tool_settings, height=None):
""" get the tool specified by the relevant settings
The settings must include:
- "shape": one of "SphericalCutter", "CylindricalCutter" and
"ToroidalCutter"
- "radius": the tool radius
The following settings are optional or shape specific:
- "torus_radius": necessary for ToroidalCutter
@type tool_settings: dict
@value tool_settings: contains the attributes of the tool
@type height: float
@value height: the height of the tool
@rtype: BaseCutter | basestring
@return: a tool object or an error string
"""
cuttername = tool_settings["shape"]
radius = tool_settings["radius"]
if cuttername == "SphericalCutter":
return SphericalCutter(radius, height=height)
elif cuttername == "CylindricalCutter":
return CylindricalCutter(radius, height=height)
elif cuttername == "ToroidalCutter":
toroid = tool_settings["torus_radius"]
return ToroidalCutter(radius, toroid, height=height)
else:
return "Invalid cutter shape: '%s' is not one of %s" % (cuttername, TOOL_SHAPES)
......@@ -7,8 +7,7 @@ import pycam.Exporters.EMCToolExporter
import pycam.Gui.Settings
import pycam.Gui.common as GuiCommon
import pycam.Cutters
import pycam.PathGenerators
import pycam.PathProcessors
import pycam.Toolpath.Generator
import pycam.Toolpath
import pycam.Geometry.utils as utils
from pycam.Gui.OpenGLTools import ModelViewWindowGL
......@@ -152,7 +151,6 @@ class ProjectGui:
# set defaults
self.model = None
self.toolpath = pycam.Toolpath.ToolPathList()
self._physics_cache = None
self.cutter = None
self.process_list = []
self.tool_list = []
......@@ -402,14 +400,6 @@ class ProjectGui:
self.view3d.glsetup()
self.view3d.paint()
def get_physics(self, cutter):
if self.settings.get("enable_ode"):
self._physics_cache = pycam.Physics.ode_physics.generate_physics(self.model,
cutter, self._physics_cache)
else:
self._physics_cache = None
return self._physics_cache
def update_save_actions(self):
self.gui.get_object("SaveTaskSettings").set_sensitive(not self.last_task_settings_file is None)
self.gui.get_object("SaveModel").set_sensitive(not self.last_model_file is None)
......@@ -447,7 +437,7 @@ class ProjectGui:
lines.append(tool_desc)
lines.append("Speed: %d/minute / Feedrate: %d%s/minute" % (tool["speed"], tool["feedrate"], unit))
lines.append("Path: %s / %s" % (process["path_generator"], process["path_postprocessor"]))
lines.append("Overlap: %d%%" % process["overlap"])
lines.append("Overlap: %d%%" % process["overlap_percent"])
lines.append("Material allowance: %.2f%s" % (process["material_allowance"], unit))
if process["path_generator"] == "PushCutter":
lines.append("Maximum step down: %.2f%s" % (process["step_down"], unit))
......@@ -1174,7 +1164,7 @@ class ProjectGui:
return name
settings["path_postprocessor"] = get_path_postprocessor()
for objname, key in (("SafetyHeightControl", "safety_height"),
("OverlapPercentControl", "overlap"),
("OverlapPercentControl", "overlap_percent"),
("MaterialAllowanceControl", "material_allowance"),
("MaxStepDownControl", "step_down")):
settings[key] = self.gui.get_object(objname).get_value()
......@@ -1197,7 +1187,7 @@ class ProjectGui:
self.gui.get_object(value).set_active(True)
set_path_postprocessor(settings["path_postprocessor"])
for objname, key in (("SafetyHeightControl", "safety_height"),
("OverlapPercentControl", "overlap"),
("OverlapPercentControl", "overlap_percent"),
("MaterialAllowanceControl", "material_allowance"),
("MaxStepDownControl", "step_down")):
self.gui.get_object(objname).set_value(settings[key])
......@@ -1314,8 +1304,8 @@ class ProjectGui:
# columns: name, visible, drill_size, drill_id, allowance, speed, feedrate
for index in range(len(self.toolpath)):
tp = self.toolpath[index]
items = (index, tp.name, tp.visible, tp.drill_size,
tp.drill_id, tp.material_allowance, tp.speed, tp.feedrate)
items = (index, tp.name, tp.visible, tp.tool_settings["radius"],
tp.tool_id, tp.material_allowance, tp.speed, tp.feedrate)
model.append(items)
if not new_index is None:
self._treeview_set_active_index(self.toolpath_table, new_index)
......@@ -1346,58 +1336,6 @@ class ProjectGui:
if not settings.write_to_file(filename, self.tool_list, self.process_list, self.task_list) and not no_dialog and not self.no_dialog:
show_error_dialog(self.window, "Failed to save settings file")
def get_tool_instance(self, tool_settings):
cutter_height = self.settings.get("maxz") - self.settings.get("minz")
if self.model:
cutter_height = max(cutter_height, self.model.maxz - self.model.minz)
# Due to some weirdness the height of the drill must be bigger than the object's size.
# Otherwise some collisions are not detected.
cutter_height *= 4
cuttername = tool_settings["shape"]
radius = tool_settings["tool_radius"]
if cuttername == "SphericalCutter":
cutter = pycam.Cutters.SphericalCutter(radius, height=cutter_height)
elif cuttername == "CylindricalCutter":
cutter = pycam.Cutters.CylindricalCutter(radius, height=cutter_height)
elif cuttername == "ToroidalCutter":
toroid = tool_settings["torus_radius"]
cutter = pycam.Cutters.ToroidalCutter(radius, toroid, height=cutter_height)
else:
pass
return cutter
def get_pathgenerator_instance(self, cutter, process_settings):
pathgenerator = process_settings["path_generator"]
pathprocessor = process_settings["path_postprocessor"]
cutter.set_required_distance(process_settings["material_allowance"])
physics = self.get_physics(cutter)
if pathgenerator == "DropCutter":
if pathprocessor == "ZigZagCutter":
processor = pycam.PathProcessors.PathAccumulator(zigzag=True)
else:
processor = None
result = pycam.PathGenerators.DropCutter(cutter,
self.model, processor, physics=physics,
safety_height=self.settings.get("safety_height"))
elif pathgenerator == "PushCutter":
if pathprocessor == "PathAccumulator":
processor = pycam.PathProcessors.PathAccumulator()
elif pathprocessor == "SimpleCutter":
processor = pycam.PathProcessors.SimpleCutter()
elif pathprocessor == "ZigZagCutter":
processor = pycam.PathProcessors.ZigZagCutter()
elif pathprocessor == "PolygonCutter":
processor = pycam.PathProcessors.PolygonCutter()
elif pathprocessor == "ContourCutter":
processor = pycam.PathProcessors.ContourCutter()
else:
processor = None
result = pycam.PathGenerators.PushCutter(cutter,
self.model, processor, physics=physics)
else:
result = None
return result
def toggle_progress_bar(self, status):
if status:
self.task_pane.set_sensitive(False)
......@@ -1435,6 +1373,8 @@ class ProjectGui:
else:
toolpath = self.toolpath[toolpath_index]
paths = toolpath.get_path()
# set the current cutter
self.cutter = pycam.Cutters.get_tool_from_settings(toolpath.tool_settings)
# calculate steps
detail_level = self.gui.get_object("SimulationDetailsValue").get_value()
grid_size = 100 * pow(2, detail_level - 1)
......@@ -1443,7 +1383,7 @@ class ProjectGui:
x_steps = int(math.sqrt(grid_size) * proportion)
y_steps = int(math.sqrt(grid_size) / proportion)
simulation_backend = pycam.Simulation.ODEBlocks.ODEBlocks(
toolpath.drill, toolpath.bounding_box,
toolpath.tool_settings, toolpath.bounding_box,
x_steps=x_steps, y_steps=y_steps)
self.settings.set("simulation_object", simulation_backend)
# disable the simulation widget (avoids confusion regarding "cancel")
......@@ -1516,10 +1456,13 @@ class ProjectGui:
callback = None
draw_callback = UpdateView(callback,
max_fps=self.settings.get("drill_progress_max_fps")).update
direction = process_settings["path_direction"]
self.update_progress_bar("Generating collision model")
self.cutter = self.get_tool_instance(tool_settings)
if self.settings.get("enable_ode"):
calculation_backend = "ODE"
else:
calculation_backend = None
# this offset allows to cut a model with a minimal boundary box correctly
offset = tool_settings["tool_radius"] / 2.0
......@@ -1543,6 +1486,7 @@ class ProjectGui:
maxy = float(self.settings.get("maxy"))+offset
minz = float(self.settings.get("minz"))
maxz = float(self.settings.get("maxz"))
bounds = (minx, maxx, miny, maxy, minz, maxz)
# check if the boundary limits are valid
if (minx > maxx) or (miny > maxy) or (minz > maxz):
......@@ -1551,46 +1495,36 @@ class ProjectGui:
show_error_dialog(self.window, "Processing boundaries are too small for this tool size.")
return True
effective_toolradius = tool_settings["tool_radius"] * (1.0 - process_settings["overlap"] / 100.0)
x_shift = effective_toolradius
y_shift = effective_toolradius
self.update_progress_bar("Starting the toolpath generation")
# put the tool settings together
tool_dict = {"shape": tool_settings["shape"],
"radius": tool_settings["tool_radius"],
"torus_radius": tool_settings["torus_radius"],
}
# run the toolpath generation
toolpath = pycam.Toolpath.Generator.generate_toolpath(self.model,
tool_dict, bounds=bounds,
direction=process_settings["path_direction"],
path_generator=process_settings["path_generator"],
path_postprocessor=process_settings["path_postprocessor"],
material_allowance=process_settings["material_allowance"],
safety_height=process_settings["safety_height"],
overlap=process_settings["overlap_percent"] / 100.0,
step_down=process_settings["step_down"],
calculation_backend=calculation_backend, callback=draw_callback)
pathgenerator = self.get_pathgenerator_instance(self.cutter, process_settings)
pathgenerator_name = process_settings["path_generator"]
if pathgenerator_name == "DropCutter":
dx = x_shift
dy = y_shift
if direction == "x":
toolpath = pathgenerator.GenerateToolPath(minx, maxx, miny, maxy, minz, maxz, dx, dy, 0, draw_callback)
elif direction == "y":
toolpath = pathgenerator.GenerateToolPath(minx, maxx, miny, maxy, minz, maxz, dy, dx, 1, draw_callback)
print "Time elapsed: %f" % (time.time() - start_time)
elif pathgenerator_name == "PushCutter":
if process_settings["path_postprocessor"] == "ContourCutter":
dx = x_shift
else:
dx = utils.INFINITE
dy = y_shift
if process_settings["step_down"] > 0:
dz = process_settings["step_down"]
if isinstance(toolpath, basestring):
# an error occoured - "toolpath" contains the error message
message = "Failed to generate toolpath: %s" % toolpath
if not self.no_dialog:
show_error_dialog(self.window, message)
else:
dz = utils.INFINITE
if direction == "x":
toolpath = pathgenerator.GenerateToolPath(minx, maxx, miny, maxy, minz, maxz, 0, dy, dz, draw_callback)
elif direction == "y":
toolpath = pathgenerator.GenerateToolPath(minx, maxx, miny, maxy, minz, maxz, dy, 0, dz, draw_callback)
elif direction == "xy":
toolpath = pathgenerator.GenerateToolPath(minx, maxx, miny, maxy, minz, maxz, dy, dy, dz, draw_callback)
print "Time elapsed: %f" % (time.time() - start_time)
# calculate the z offset for the starting position
# TODO: fix these hard-coded offsets; maybe use the safety height instead?
if self.settings.get("unit") == 'mm':
start_offset = 7.0
print >>sys.stderr, message
# we were not successful (similar to a "cancel" request)
return False
else:
start_offset = 0.25
# hide the previous toolpath if it is the only visible one (automatic mode)
if (len([True for path in self.toolpath if path.visible]) == 1) \
and self.toolpath[-1].visible:
......@@ -1599,18 +1533,15 @@ class ProjectGui:
description = "%s / %s" % (tool_settings["name"], process_settings["name"])
# the tool id numbering should start with 1 instead of zero
tool_id = self.tool_list.index(tool_settings) + 1
bounding_box = (self.settings.get("minx"), self.settings.get("maxx"),
self.settings.get("miny"), self.settings.get("maxy"),
self.settings.get("minz"), self.settings.get("maxz"))
self.toolpath.add_toolpath(toolpath,
description, self.cutter, tool_id,
description, tool_dict, tool_id,
tool_settings["speed"],
tool_settings["feedrate"],
process_settings["material_allowance"],
process_settings["safety_height"],
self.settings.get("unit"),
minx, miny, maxz + start_offset,
bounding_box)
minx, miny, process_settings["safety_height"],
bounds)
self.update_toolpath_table()
self.update_view()
# return "False" if the action was cancelled
......@@ -1727,7 +1658,7 @@ class ProjectGui:
pycam.Exporters.SimpleGCodeExporter.ExportPathList(destination,
tp.toolpath, tp.unit,
tp.start_x, tp.start_y, tp.start_z,
tp.feedrate, tp.speed, tp.safety_height, tp.drill_id,
tp.feedrate, tp.speed, tp.safety_height, tp.tool_id,
finish_program=is_last_loop)
destination.close()
if self.no_dialog:
......
......@@ -120,7 +120,7 @@ path_generator: PushCutter
path_postprocessor: PolygonCutter
material_allowance: 0.5
step_down: 0.8
overlap: 0
overlap_percent: 0
[Process1]
name: Semi-finish
......@@ -128,7 +128,7 @@ path_generator: PushCutter
path_postprocessor: ContourCutter
material_allowance: 0.2
step_down: 0.5
overlap: 20
overlap_percent: 20
[Process2]
name: Finish
......@@ -136,7 +136,7 @@ path_generator: DropCutter
path_postprocessor: ZigZagCutter
material_allowance: 0.0
step_down: 1.0
overlap: 60
overlap_percent: 60
[TaskDefault]
enabled: 1
......@@ -166,7 +166,7 @@ process: 2
"path_postprocessor": str,
"safety_height": float,
"material_allowance": float,
"overlap": int,
"overlap_percent": int,
"step_down": float,
"tool": object,
"process": object,
......@@ -182,7 +182,7 @@ process: 2
CATEGORY_KEYS = {
"tool": ("name", "shape", "tool_radius", "torus_radius", "feedrate", "speed"),
"process": ("name", "path_generator", "path_postprocessor", "path_direction",
"safety_height", "material_allowance", "overlap", "step_down"),
"safety_height", "material_allowance", "overlap_percent", "step_down"),
"task": ("tool", "process", "enabled"),
}
......
import pycam.Cutters
from pycam.Geometry.Point import Point
import ode
try:
......@@ -8,8 +9,8 @@ except:
class ODEBlocks:
def __init__(self, cutter, (minx, maxx, miny, maxy, minz, maxz), x_steps=None, y_steps=None):
self.cutter = cutter
def __init__(self, tool_settings, (minx, maxx, miny, maxy, minz, maxz), x_steps=None, y_steps=None):
self.cutter = pycam.Cutters.get_tool_from_settings(tool_settings)
# we don't want to use the "material allowance" distance
self.cutter.set_required_distance(0)
dimx = maxx - minx
......
import pycam.PathGenerators
import pycam.PathProcessors
import pycam.Cutters
import sys
DIRECTIONS = frozenset(("x", "y", "xy"))
PATH_GENERATORS = frozenset(("DropCutter", "PushCutter"))
PATH_POSTPROCESSORS = frozenset(("ContourCutter", "PathAccumulator", "PolygonCutter", "SimpleCutter", "ZigZagCutter"))
CALCULATION_BACKENDS = frozenset((None, "ODE"))
def generate_toolpath(model, tool_settings=None, bounds=None, direction="x",
path_generator="DropCutter", path_postprocessor="ZigZagCutter",
material_allowance=0.0, safety_height=None, overlap=0.0,
step_down=0.0, calculation_backend=None, callback=None):
""" abstract interface for generating a toolpath
@type model: pycam.Geometry.Model.Model
@value model: a model contains the surface triangles
@type direction: str
@value direction: any member of the DIRECTIONS set (e.g. "x", "y" or "xy")
@type bounds: tuple(float) | list(float)
@value bounds: the processing boundary (relative to the center of the tool)
(order: minx, maxx, miny, maxy, minz, maxz)
@type tool_settings: dict
@value tool_settings: contains at least the following keys (depending on
the tool type):
"shape": any of possible cutter shape (see "pycam.Cutters")
"radius": main radius of the tools
"torus_radius": (only for ToroidalCutter) second toroidal radius
@type path_generator: str
@value path_generator: any member of the PATH_GENERATORS set
@type path_postprocessor: str
@value path_postprocessor: any member of the PATH_POSTPROCESSORS set
@type material_allowance: float
@value material_allowance: the minimum distance between the tool and the model
@type overlap: float
@value overlap: the overlap between two adjacent tool paths (0 <= overlap < 1)
@type calculation_backend: str | None
@value calculation_backend: any member of the CALCULATION_BACKENDS set
The default is the triangular collision detection.
@rtype: pycam.Toolpath.ToolPath | str
@return: the resulting toolpath object or an error string in case of invalid
arguments
"""
if bounds is None:
# no bounds were given - we use the boundaries of the model
minx, maxx = model.minx, model.maxx
miny, maxy = model.miny, model.maxy
minz, maxz = model.minz, model.maxz
else:
minx, maxx, miny, maxy, minz, maxz = bounds
# Due to some weirdness the height of the drill must be bigger than the object's size.
# Otherwise some collisions are not detected.
cutter_height = 4 * (maxy - miny)
cutter = pycam.Cutters.get_tool_from_settings(tool_settings, cutter_height)
if isinstance(cutter, basestring):
return cutter
cutter.set_required_distance(material_allowance)
physics = _get_physics(cutter, calculation_backend)
if isinstance(physics, basestring):
return physics
generator = _get_pathgenerator_instance(model, cutter, path_generator, path_postprocessor, material_allowance, safety_height, physics)
if isinstance(generator, basestring):
return generator
if (overlap < 0) or (overlap >= 1):
return "Invalid overlap value (%f): should be greater or equal 0 and lower than 1"
effective_toolradius = tool_settings["radius"] * (1.0 - overlap)
if path_generator == "DropCutter":
if direction == "x":
direction_param = 0
elif direction == "y":
direction_param = 1
else:
return "Invalid direction value (%s): not one of %s" % (direction, DIRECTIONS)
toolpath = generator.GenerateToolPath(minx, maxx, miny, maxy, minz, maxz,
effective_toolradius, effective_toolradius, direction_param, callback)
else:
if step_down > 0:
dz = step_down
else:
dz = maxz - minz
if direction == "x":
dx, dy = 0, effective_toolradius
elif direction == "y":
dx, dy = effective_toolradius, 0
elif direction == "xy":
dx, dy = effective_toolradius, effective_toolradius
else:
return "Invalid direction (%s): not one of %s" % (direction, DIRECTIONS)
toolpath = generator.GenerateToolPath(minx, maxx, miny, maxy, minz, maxz, dx, dy, dz, callback)
return toolpath
def _get_pathgenerator_instance(model, cutter, pathgenerator, pathprocessor,
material_allowance, safety_height, physics):
if pathgenerator == "DropCutter":
if pathprocessor == "ZigZagCutter":
processor = pycam.PathProcessors.PathAccumulator(zigzag=True)
elif pathprocessor == "PathAccumulator":
processor = pycam.PathProcessors.PathAccumulator()
else:
return "Invalid postprocessor (%s) for 'DropCutter': only 'ZigZagCutter' or 'PathAccumulator' are allowed" % str(pathprocessor)
return pycam.PathGenerators.DropCutter(cutter,
model, processor, physics=physics,
safety_height=safety_height)
elif pathgenerator == "PushCutter":
if pathprocessor == "PathAccumulator":
processor = pycam.PathProcessors.PathAccumulator()
elif pathprocessor == "SimpleCutter":
processor = pycam.PathProcessors.SimpleCutter()
elif pathprocessor == "ZigZagCutter":
processor = pycam.PathProcessors.ZigZagCutter()
elif pathprocessor == "PolygonCutter":
processor = pycam.PathProcessors.PolygonCutter()
elif pathprocessor == "ContourCutter":
processor = pycam.PathProcessors.ContourCutter()
else:
return "Invalid postprocessor (%s) for 'PushCutter' - it should be one of these: %s" % (processor, PATH_POSTPROCESSORS)
return pycam.PathGenerators.PushCutter(cutter,
model, processor, physics=physics)
else:
return "Invalid path generator (%s): not one of %s" % (pathgenerator, PATH_GENERATORS)
def _get_physics(cutter, calculation_backend):
if calculation_backend is None:
# triangular collision detection does not need any physical model
return None
elif calculation_backend == "ODE":
import pycam.Physics.ode_physics
return pycam.Physics.ode_physics.generate_physics(model, cutter)
else:
return "Invalid calculation backend (%s): not one of %s" % (calculation_backend, CALCULATION_BACKENDS)
__all__ = ["ToolPathList", "ToolPath", "Generator"]
import random
class ToolPathList(list):
def add_toolpath(self, toolpath, name, cutter, *args):
self.append(ToolPath(toolpath, name, cutter, *args))
def add_toolpath(self, toolpath, name, tool_settings, *args):
self.append(ToolPath(toolpath, name, tool_settings, *args))
class ToolPath:
def __init__(self, toolpath, name, cutter, drill_id, speed, feedrate,
material_allowance, safety_height, unit, start_x, start_y, start_z, bounding_box):
def __init__(self, toolpath, name, tool_settings, tool_id, speed,
feedrate, material_allowance, safety_height, unit, start_x,
start_y, start_z, bounding_box):
self.toolpath = toolpath
self.name = name
self.visible = True
self.drill_id = drill_id
self.drill = cutter
self.drill_size = cutter.radius
self.tool_id = tool_id
self.tool_settings = tool_settings
self.speed = speed
self.feedrate = feedrate
self.material_allowance = material_allowance
......
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