Commit 83a69d40 authored by sumpfralle's avatar sumpfralle

added a separate class for handling toolpath settings

 * this will ease the repeatition of previous toolpath generations
added meta data to the STL and GCode exporters


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@394 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent aef44e26
......@@ -47,7 +47,7 @@ def get_tool_from_settings(tool_settings, height=None):
@return: a tool object or an error string
"""
cuttername = tool_settings["shape"]
radius = tool_settings["radius"]
radius = tool_settings["tool_radius"]
if cuttername == "SphericalCutter":
return SphericalCutter(radius, height=height)
elif cuttername == "CylindricalCutter":
......
......@@ -26,10 +26,11 @@ import os
class STLExporter:
def __init__(self, model, name="model", created_by="pycam", linesep=None):
def __init__(self, model, name="model", created_by="pycam", linesep=None, comment=None):
self.model = model
self.name = name
self.created_by = created_by
self.comment = comment
if linesep is None:
self.linesep = os.linesep
else:
......@@ -46,6 +47,9 @@ class STLExporter:
def get_output_lines(self):
date = datetime.date.today().isoformat()
yield """solid "%s"; Produced by %s, %s""" % (self.name, self.created_by, date)
if self.comment:
for line in self.comment.split(self.linesep):
yield(";%s" % line)
for tr in self.model.triangles():
norm = tr.normal().normalize()
yield "facet normal %f %f %f" % (norm.x, norm.y, norm.z)
......
......@@ -22,6 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
from gcode import gcode
import os
# simplistic GCode exporter
# does each run, and moves the tool to the safetyheight in between
......@@ -30,7 +31,7 @@ class SimpleGCodeExporter:
def __init__(self, destination, unit, startx, starty, startz, feedrate,
speed, safety_height=None, tool_id=1, finish_program=False,
max_skip_safety_distance=None):
max_skip_safety_distance=None, comment=None):
self._last_path_point = None
self._max_skip_safety_distance = max_skip_safety_distance
if isinstance(destination, basestring):
......@@ -43,6 +44,8 @@ class SimpleGCodeExporter:
self.destination = destination
# don't close the stream if we did not open it on our own
self._close_stream_on_exit = False
if comment:
self.add_comment(comment)
if unit == "mm":
self.destination.write("G21\n")
else:
......@@ -69,6 +72,10 @@ class SimpleGCodeExporter:
distance = new_point.sub(self._last_path_point).norm()
return distance <= self._max_skip_safety_distance
def add_comment(self, comment):
for line in comment.split(os.linesep):
self.destination.write(";%s\n" % line)
def AddPath(self, path):
gc = self.gcode
point = path.points[0]
......
......@@ -32,6 +32,7 @@ import pycam.Toolpath.Generator
import pycam.Toolpath
import pycam.Geometry.utils as utils
from pycam.Gui.OpenGLTools import ModelViewWindowGL
from pycam import VERSION
import pycam.Physics.ode_physics
# this requires ODE - we import it later, if necessary
#import pycam.Simulation.ODEBlocks
......@@ -39,6 +40,7 @@ import gtk
import ConfigParser
import math
import time
import datetime
import re
import os
import sys
......@@ -112,6 +114,8 @@ class ProjectGui:
"along": 0,
"around": 1}
META_DATA_PREFIX = "PYCAM-META-DATA:"
def __init__(self, master=None, no_dialog=False):
""" TODO: remove "master" above when the Tk interface is abandoned"""
self.settings = pycam.Gui.Settings.Settings()
......@@ -1057,7 +1061,7 @@ class ProjectGui:
return
try:
fi = open(filename, "w")
pycam.Exporters.STLExporter.STLExporter(self.model).write(fi)
pycam.Exporters.STLExporter.STLExporter(self.model, comment=self.get_meta_data()).write(fi)
fi.close()
except IOError, err_msg:
if not no_dialog and not self.no_dialog:
......@@ -1503,9 +1507,13 @@ 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.tool_settings["radius"],
tp.tool_id, tp.material_allowance, tp.speed,
tp.feedrate, get_time_string(tp.get_machine_time()))
toolpath_settings = tp.get_toolpath_settings()
tool = toolpath_settings.get_tool_settings()
process = toolpath_settings.get_process_settings()
items = (index, tp.name, tp.visible, tool["tool_radius"],
tool["id"], process["material_allowance"],
tool["speed"], tool["feedrate"],
get_time_string(tp.get_machine_time()))
model.append(items)
if not new_index is None:
self._treeview_set_active_index(self.toolpath_table, new_index)
......@@ -1577,7 +1585,8 @@ class ProjectGui:
toolpath = self.toolpath[toolpath_index]
paths = toolpath.get_path()
# set the current cutter
self.cutter = pycam.Cutters.get_tool_from_settings(toolpath.tool_settings)
self.cutter = pycam.Cutters.get_tool_from_settings(
toolpath.get_tool_settings())
# calculate steps
detail_level = self.gui.get_object("SimulationDetailsValue").get_value()
grid_size = 100 * pow(2, detail_level - 1)
......@@ -1586,7 +1595,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.tool_settings, toolpath.bounding_box,
toolpath.get_tool_settings(), toolpath.get_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")
......@@ -1666,10 +1675,47 @@ class ProjectGui:
self.update_progress_bar("Generating collision model")
if self.settings.get("enable_ode"):
calculation_backend = "ODE"
# turn the toolpath settings into a dict
toolpath_settings = self.get_toolpath_settings(tool_settings, process_settings)
if toolpath_settings is None:
# behave as if "cancel" was requested
return True
self.cutter = toolpath_settings.get_tool()
# run the toolpath generation
self.update_progress_bar("Starting the toolpath generation")
toolpath = pycam.Toolpath.Generator.generate_toolpath_from_settings(
self.model, toolpath_settings, callback=draw_callback)
print "Time elapsed: %f" % (time.time() - start_time)
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:
print >>sys.stderr, message
# we were not successful (similar to a "cancel" request)
return False
else:
calculation_backend = None
# 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:
self.toolpath[-1].visible = False
# add the new toolpath
description = "%s / %s" % (tool_settings["name"],
process_settings["name"])
# the tool id numbering should start with 1 instead of zero
self.toolpath.add_toolpath(toolpath, description, toolpath_settings)
self.update_toolpath_table()
self.update_view()
# return "False" if the action was cancelled
return not self._progress_cancel_requested
def get_toolpath_settings(self, tool_settings, process_settings):
toolpath_settings = pycam.Gui.Settings.ToolpathSettings()
# this offset allows to cut a model with a minimal boundary box correctly
offset = tool_settings["tool_radius"] / 2.0
......@@ -1693,79 +1739,46 @@ 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)
toolpath_settings.set_bounds(minx, maxx, miny, maxy, minz, maxz)
# check if the boundary limits are valid
if (minx > maxx) or (miny > maxy) or (minz > maxz):
# don't generate a toolpath if the area is too small (e.g. due to the tool size)
if not self.no_dialog:
show_error_dialog(self.window, "Processing boundaries are too small for this tool size.")
return True
return None
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"],
}
self.cutter = pycam.Cutters.get_tool_from_settings(tool_dict)
tool_id = self.tool_list.index(tool_settings) + 1
toolpath_settings.set_tool(tool_id, tool_settings["shape"],
tool_settings["tool_radius"], tool_settings["torus_radius"],
tool_settings["speed"], tool_settings["feedrate"])
# get the support grid options
if self.gui.get_object("SupportGridEnable").get_active():
support_grid_distance = self.settings.get("support_grid_distance")
support_grid_thickness = self.settings.get("support_grid_thickness")
support_grid_height = self.settings.get("support_grid_height")
else:
support_grid_distance = None
support_grid_thickness = None
support_grid_height = None
# run the toolpath generation
toolpath = pycam.Toolpath.Generator.generate_toolpath(self.model,
tool_settings=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"],
support_grid_distance=support_grid_distance,
support_grid_thickness=support_grid_thickness,
support_grid_height=support_grid_height,
calculation_backend=calculation_backend, callback=draw_callback)
toolpath_settings.set_support_grid(
self.settings.get("support_grid_distance"),
self.settings.get("support_grid_thickness"),
self.settings.get("support_grid_height"))
print "Time elapsed: %f" % (time.time() - start_time)
# calculation backend: ODE / None
if self.settings.get("enable_ode"):
toolpath_settings.set_calculation_backend("ODE")
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:
print >>sys.stderr, message
# we were not successful (similar to a "cancel" request)
return False
else:
# 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:
self.toolpath[-1].visible = False
# add the new toolpath
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
self.toolpath.add_toolpath(toolpath,
description, tool_dict, tool_id,
tool_settings["speed"],
tool_settings["feedrate"],
# unit size
toolpath_settings.set_unit_size(self.settings.get("unit"))
# process settings
toolpath_settings.set_process_settings(
process_settings["path_generator"],
process_settings["path_postprocessor"],
process_settings["path_direction"],
process_settings["material_allowance"],
process_settings["safety_height"],
self.settings.get("unit"),
minx, miny, process_settings["safety_height"],
bounds)
self.update_toolpath_table()
self.update_view()
# return "False" if the action was cancelled
return not self._progress_cancel_requested
process_settings["overlap_percent"] / 100.0,
process_settings["step_down"])
return toolpath_settings
def get_filename_via_dialog(self, title, mode_load=False, type_filter=None):
# we open a dialog
......@@ -1878,12 +1891,20 @@ class ProjectGui:
is_last_loop = True
else:
is_last_loop = False
start_pos = tp.get_start_position()
settings = tp.get_toolpath_settings()
process = settings.get_process_settings()
tool = settings.get_tool_settings()
meta_data = []
meta_data.append(self.get_meta_data())
meta_data.append(tp.get_meta_data())
pycam.Exporters.SimpleGCodeExporter.ExportPathList(destination,
tp.toolpath, tp.unit,
tp.start_x, tp.start_y, tp.start_z,
tp.feedrate, tp.speed, safety_height=tp.safety_height, tool_id=tp.tool_id,
finish_program=is_last_loop,
max_skip_safety_distance=2*tp.tool_settings["radius"])
tp.get_path(), settings.get_unit_size(), start_pos.x,
start_pos.y, start_pos.z, tool["feedrate"],
tool["speed"], safety_height=process["safety_height"],
tool_id=tool["id"], finish_program=is_last_loop,
max_skip_safety_distance=2*tool["tool_radius"],
comment=os.linesep.join(meta_data))
destination.close()
if self.no_dialog:
print "GCode file successfully written: %s" % str(filename)
......@@ -1891,6 +1912,15 @@ class ProjectGui:
if not no_dialog and not self.no_dialog:
show_error_dialog(self.window, "Failed to save toolpath file")
def get_meta_data(self):
filename = "Filename: %s" % str(self.last_model_file)
timestamp = "Timestamp: %s" % str(datetime.datetime.now())
version = "Version: %s" % VERSION
result = []
for text in (filename, timestamp, version):
result.append("%s %s" % (self.META_DATA_PREFIX, text))
return os.linesep.join(result)
def mainloop(self):
# run the mainloop only if a GUI was requested
if not self.no_dialog:
......
......@@ -20,6 +20,7 @@ You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
import pycam.Cutters
import ConfigParser
import StringIO
import sys
......@@ -388,3 +389,157 @@ process: 3
result.append("")
return os.linesep.join(result)
class ToolpathSettings:
SECTIONS = {
"Bounds": {
"minx": float,
"maxx": float,
"miny": float,
"maxy": float,
"minz": float,
"maxz": float,
},
"Tool": {
"shape": str,
"tool_radius": float,
"torus_radius": float,
"speed": float,
"feedrate": float,
},
"SupportGrid": {
"distance": float,
"thickness": float,
"height": float,
},
"General": {
"calculation_backend": str,
},
"ProcessSettings": {
"generator": str,
"postprocessor": str,
"path_direction": str,
"material_allowance": float,
"safety_height": float,
"overlap": float,
"step_down": float,
},
}
META_MARKER_START = "PYCAM_TOOLPATH_SETTINGS: START"
META_MARKER_END = "PYCAM_TOOLPATH_SETTINGS: END"
def __init__(self):
self.general = {}
self.bounds = {}
self.tool_settings = {}
self.support_grid = {}
self.process_settings = {}
def set_bounds(self, minx, maxx, miny, maxy, minz, maxz):
self.bounds = {
"minx": minx,
"maxx": maxx,
"miny": miny,
"maxy": maxy,
"minz": minz,
"maxz": maxz,
}
def get_bounds(self):
return self.bounds
def set_tool(self, index, shape, tool_radius, torus_radius=None, speed=0.0, feedrate=0.0):
self.tool_settings = {"id": index,
"shape": shape,
"tool_radius": tool_radius,
"torus_radius": torus_radius,
"speed": speed,
"feedrate": feedrate,
}
def get_tool(self):
return pycam.Cutters.get_tool_from_settings(self.tool_settings)
def get_tool_settings(self):
return self.tool_settings
def set_support_grid(self, distance, thickness, height):
self.support_grid["distance"] = distance
self.support_grid["thickness"] = thickness
self.support_grid["height"] = height
def get_support_grid(self):
if self.support_grid:
return self.support_grid
else:
return {"distance": None, "thickness": None, "height": None}
def set_calculation_backend(self, backend=None):
self.general["calculation_backend"] = None
def get_calculation_backend(self):
if self.general.has_key("calculation_backend"):
return self.general["calculation_backend"]
else:
return None
def set_unit_size(self, unit_size):
self.general["unit_size"] = unit_size
def get_unit_size(self):
if self.general.has_key("unit_size"):
return self.general["unit_size"]
else:
return "mm"
def set_process_settings(self, generator, postprocessor, path_direction,
material_allowance=0.0, safety_height=0.0, overlap=0.0,
step_down=1.0):
self.process_settings = {
"generator": generator,
"postprocessor": postprocessor,
"path_direction": path_direction,
"material_allowance": material_allowance,
"safety_height": safety_height,
"overlap": overlap,
"step_down": step_down,
}
def get_process_settings(self):
return self.process_settings
def parse(self, text):
text_stream = StringIO.StringIO(text)
config = ConfigParser.SafeConfigParser()
config.readfp(text_stream)
for config_dict, section in ((self.bounds, "Bounds"),
(self.tool_settings, "Tool"),
(self.support_grid, "SupportGrid"),
(self.process_settings, "ProcessSettings")):
for key, value_type in self.SECTIONS[section].items():
raw_value = config.get(section, key, None)
if not raw_value is None:
try:
value = value_type(raw_value)
config_dict[key] = value
except ValueError:
print >>sys.stderr, "Ignored invalid setting (%s -> %s): %s" % (section, key, value_raw)
def get_string(self):
result = []
for config_dict, section in ((self.bounds, "Bounds"),
(self.tool_settings, "Tool"),
(self.support_grid, "SupportGrid"),
(self.process_settings, "ProcessSettings")):
result.append("[%s]" % section)
for key, value_type in self.SECTIONS[section].items():
if config_dict.has_key(key):
value = config_dict[key]
if type(value) == value_type:
result.append("%s = %s" % (key, value))
# add one empty line after each section
result.append("")
return os.linesep.join(result)
......@@ -32,6 +32,22 @@ PATH_GENERATORS = frozenset(("DropCutter", "PushCutter", "EngraveCutter"))
PATH_POSTPROCESSORS = frozenset(("ContourCutter", "PathAccumulator", "PolygonCutter", "SimpleCutter", "ZigZagCutter"))
CALCULATION_BACKENDS = frozenset((None, "ODE"))
def generate_toolpath_from_settings(model, tp_settings, callback=None):
process = tp_settings.get_process_settings()
grid = tp_settings.get_support_grid()
backend = tp_settings.get_calculation_backend()
bounds = []
bounds_dict = tp_settings.get_bounds()
for key in ("minx", "maxx", "miny", "maxy", "minz", "maxz"):
bounds.append(bounds_dict[key])
return generate_toolpath(model, tp_settings.get_tool_settings(),
bounds, process["path_direction"], process["generator"],
process["postprocessor"], process["material_allowance"],
process["safety_height"], process["overlap"],
process["step_down"], grid["distance"], grid["thickness"],
grid["height"], backend, callback)
def generate_toolpath(model, tool_settings=None,
bounds=None, direction="x", path_generator="DropCutter",
path_postprocessor="ZigZagCutter", material_allowance=0.0,
......@@ -46,7 +62,7 @@ def generate_toolpath(model, tool_settings=None,
@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
"tool_radius": main radius of the tools
"torus_radius": (only for ToroidalCutter) second toroidal radius
@type bounds: tuple(float) | list(float)
@value bounds: the processing boundary (relative to the center of the tool)
......@@ -126,7 +142,7 @@ def generate_toolpath(model, tool_settings=None,
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)
effective_toolradius = tool_settings["tool_radius"] * (1.0 - overlap)
if path_generator == "DropCutter":
if direction == "x":
direction_param = 0
......
......@@ -23,32 +23,24 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
__all__ = ["ToolPathList", "ToolPath", "Generator"]
from pycam.Geometry.Point import Point
import pycam.Gui.Settings
import random
import os
class ToolPathList(list):
def add_toolpath(self, toolpath, name, tool_settings, *args):
self.append(ToolPath(toolpath, name, tool_settings, *args))
def add_toolpath(self, toolpath, name, tool_settings):
self.append(ToolPath(toolpath, name, tool_settings))
class ToolPath:
def __init__(self, toolpath, name, tool_settings, tool_id, speed,
feedrate, material_allowance, safety_height, unit, start_x,
start_y, start_z, bounding_box):
def __init__(self, toolpath, name, toolpath_settings):
self.toolpath = toolpath
self.name = name
self.toolpath_settings = toolpath_settings
self.visible = True
self.tool_id = tool_id
self.tool_settings = tool_settings
self.speed = speed
self.feedrate = feedrate
self.material_allowance = material_allowance
self.safety_height = safety_height
self.unit = unit
self.start_x = start_x
self.start_y = start_y
self.start_z = start_z
self.bounding_box = bounding_box
self.color = None
# generate random color
self.set_color()
......@@ -56,6 +48,32 @@ class ToolPath:
def get_path(self):
return self.toolpath
def get_start_position(self):
safety_height = self.toolpath_settings.get_process_settings()["safety_height"]
for path in self.toolpath:
if path.points:
p = path.points[0]
return Point(p.x, p.y, safety_height)
else:
return Point(0, 0, safety_height)
def get_bounding_box(self):
box = self.toolpath_settings.get_bounding_box()
return (box["minx"], box["maxx"], box["miny"], box["maxy"], box["minz"],
box["maxz"])
def get_tool_settings(self):
return self.toolpath_settings.get_tool_settings()
def get_toolpath_settings(self):
return self.toolpath_settings
def get_meta_data(self):
meta = self.toolpath_settings.get_string()
start_marker = self.toolpath_settings.META_MARKER_START
end_marker = self.toolpath_settings.META_MARKER_END
return os.linesep.join((start_marker, meta, end_marker))
def set_color(self, color=None):
if color is None:
self.color = (random.random(), random.random(), random.random())
......@@ -74,22 +92,24 @@ class ToolPath:
"""
if start_position is None:
start_position = Point(0, 0, 0)
feedrate = self.toolpath_settings.get_tool_settings()["feedrate"]
def move(new_pos):
move.result_time += new_pos.sub(move.current_position).norm() / self.feedrate
move.result_time += new_pos.sub(move.current_position).norm() / feedrate
move.current_position = new_pos
move.current_position = start_position
move.result_time = 0
# move to safey height at the starting position
move(Point(start_position.x, start_position.y, self.safety_height))
safety_height = self.toolpath_settings.get_process_settings()["safety_height"]
move(Point(start_position.x, start_position.y, safety_height))
for path in self.get_path():
# go to safety height (horizontally from the previous x/y location)
if len(path.points) > 0:
move(Point(path.points[0].x, path.points[0].y, self.safety_height))
move(Point(path.points[0].x, path.points[0].y, safety_height))
# go through all points of the path
for point in path.points:
move(point)
# go to safety height (vertically up from the current x/y location)
if len(path.points) > 0:
move(Point(path.points[-1].x, path.points[-1].y, self.safety_height))
move(Point(path.points[-1].x, path.points[-1].y, safety_height))
return move.result_time
......@@ -22,3 +22,5 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
__all__=["Cutters","Exporters","Geometry","Gui","Importers","PathGenerators","PathProcessors","Utils"]
VERSION = "0.2.5"
......@@ -2,6 +2,7 @@
- in "pycam/Gui/gtk-interface/pycam-project.ui" ("version" in "GtkAboutDialog")
- in "Changelog"
- in "setup.py"
- in "pycam/__init__.py"
- update the release date and the list of changes in "Changelog"
- commit the changes
......
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