Commit 6a841011 authored by sumpfralle's avatar sumpfralle

added a new implementation of SimpleGCodeExporter

added new gcode settings:
 * start/stop spindle (M3/M5)
 * path mode (G61/G61.1/G64)
moved gcode settings to the general program preferences


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@633 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent 0e561ace
This diff is collapsed.
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2010 Lars Kruse <devel@sumpfralle.de>
Copyright 2008-2009 Lode Leroy
This file is part of PyCAM.
PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
PyCAM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
from pycam.Exporters.gcode import gcode
import os
DEFAULT_HEADER = ("G40 (disable tool radius compensation)",
"G49 (disable_tool_length_compensation)",
"G80 (cancel_modal_motion)",
"G54 (select_coordinate_system_1)",
"G90 (use_absolute_coordinates)")
PATH_MODES = {"exact_path": 0, "exact_stop": 1, "continuous": 2}
class GCodeGenerator:
def __init__(self, destination, metric_units=True, safety_height=0.0,
toggle_spindle_status=False, max_skip_safety_distance=None,
header=None, comment=None):
if isinstance(destination, basestring):
# open the file
self.destination = file(destination,"w")
self._close_stream_on_exit = True
else:
# assume that "destination" is something like a StringIO instance
# or an open file
self.destination = destination
# don't close the stream if we did not open it on our own
self._close_stream_on_exit = False
self.safety_height = safety_height
self.gcode = gcode(safetyheight=self.safety_height)
self.max_skip_safety_distance = max_skip_safety_distance
self.toggle_spindle_status = toggle_spindle_status
self.comment = comment
self._last_path_point = None
self._finished = False
if comment:
self.add_comment(comment)
if header is None:
self.append(DEFAULT_HEADER)
else:
self.append(header)
if metric_units:
self.append("G21 (metric)")
else:
self.append("G20 (imperial)")
def set_speed(self, feedrate=None, spindle_speed=None):
if not feedrate is None:
self.append("F%.4f" % feedrate)
if not spindle_speed is None:
self.append("S%.4f" % spindle_speed)
def set_path_mode(self, mode, motion_tolerance=None,
naive_cam_tolerance=None):
result = ""
if mode == PATH_MODES["exact_path"]:
result = "G61 (exact path mode)"
elif mode == PATH_MODES["exact_stop"]:
result = "G61.1 (exact stop mode)"
elif mode == PATH_MODES["continuous"]:
if motion_tolerance is None:
result = "G64 (continuous mode with maximum speed)"
elif naive_cam_tolerance is None:
result = "G64 P%f (continuous mode with tolerance)" \
% motion_tolerance
else:
result = ("G64 P%f Q%f (continuous mode with tolerance and " \
+ "cleanup") % (motion_tolerance, naive_cam_tolerance)
else:
raise ValueError("GCodeGenerator: invalid path mode (%s)" \
% str(mode))
self.append(result)
def add_path_list(self, paths, tool_id=None, max_skip_safety_distance=None,
comment=None):
if max_skip_safety_distance is None:
max_skip_safety_distance = self.max_skip_safety_distance
if not comment is None:
self.add_comment(comment)
if not tool_id is None:
# Move straight up to safety height (avoiding any collisions on the
# way to the tool changer).
self.append(self.gcode.safety())
self.append("T%d M6" % tool_id)
if self.toggle_spindle_status:
self.append("M3 (start spindle)")
self.append(self.gcode.delay(2))
# move straight up to safety height
self.append(self.gcode.safety())
for path in paths:
self.add_path(path)
# go back to safety height
self.append(self.gcode.safety())
if self.toggle_spindle_status:
self.append("M5 (stop spindle)")
def _check_distance_for_skipping_safety_height(self, new_point,
max_skip_safety_distance):
if (self._last_path_point is None) \
or (max_skip_safety_distance is None):
return False
distance = new_point.sub(self._last_path_point).norm
return distance <= max_skip_safety_distance
def add_path(self, path, max_skip_safety_distance=None):
if not path:
return
point = path.points[0]
# first move to the safety height if the distance to the last point
# does not exceed the given maximum
if not self._check_distance_for_skipping_safety_height(point,
max_skip_safety_distance):
# move to safety height at the end of the previous path
if not self._last_path_point is None:
self.append(self.gcode.safety())
# move to safety height for the start of the current path
self.append(self.gcode.rapid(point.x, point.y,
self.safety_height))
for point in path.points:
self.append(self.gcode.cut(point.x, point.y, point.z))
self._last_path_point = point
def finish(self):
self.append(self.gcode.safety())
self.append("M2 (end program)")
self._finished = True
def add_comment(self, comment):
if isinstance(comment, basestring):
lines = comment.split(os.linesep)
else:
lines = comment
for line in lines:
self.append(";%s" % line)
def append(self, command):
if self._finished:
raise TypeError("GCodeGenerator: can't add further commands to a " \
+ "finished GCodeGenerator instance: %s" % str(command))
if isinstance(command, basestring):
command = [command]
for line in command:
self.destination.write(line + os.linesep)
......@@ -17,16 +17,19 @@
class gcode:
lastx = lasty = lastz = lasta = lastgcode = None
lastfeed = None
def __init__(self, homeheight=1.5, safetyheight=None, tool_id=1):
def __init__(self, safetyheight=1.0, tool_id=1):
self.tool_id = tool_id
if safetyheight is None:
safetyheight = max(homeheight, 0.04)
self.homeheight = homeheight
self.safetyheight = safetyheight
self.lastz = self.safetyheight
self.lastfeed = None
self.lastx = None
self.lasty = None
self.lastz = None
self.lasta = None
self.lastgcode = None
self.lastz = None
def delay(self, seconds):
return "G04 P%d" % seconds
def begin(self):
"""
......@@ -37,12 +40,9 @@ class gcode:
G90: use absolute coordinates instead of axis increments
G00 Z?: no x/y positioning - just go up to safety height
"""
return "G40 G49 G54 G80 G90\n" \
+ "G04 P3 T%d M6\n" % self.tool_id \
+ "G00 Z%.4f\n" % self.safetyheight
def end(self):
return self.safety() + "\n" + "M2\n"
return ("G40 G49 G54 G80 G90",
"G04 P3 T%d M6" % self.tool_id,
"G00 Z%.4f" % self.safetyheight)
def exactpath(self):
return "G61"
......@@ -79,7 +79,11 @@ class gcode:
if (gcode == "G01") and feed and (feed != self.lastfeed):
feedstring = " F%.4f" % (feed)
self.lastfeed = feed
return gcodestring + feedstring + xstring + ystring + zstring + astring
positionstring = xstring + ystring + zstring + astring
if len(positionstring) > 0:
return gcodestring + feedstring + positionstring
else:
return ""
def cut(self, x = None, y = None, z = None, a = None, feed=None):
if x == None: x = self.lastx
......@@ -88,9 +92,6 @@ class gcode:
if a == None: a = self.lasta
return self.rapid(x, y, z, a, gcode="G01", feed=feed)
def home(self):
return self.rapid(z=self.homeheight)
def safety(self):
return self.rapid(z=self.safetyheight)
......@@ -22,7 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
import pycam.Exporters.SimpleGCodeExporter
import pycam.Exporters.GCodeExporter
import pycam.Exporters.EMCToolExporter
import pycam.Gui.Settings
import pycam.Cutters
......@@ -94,6 +94,11 @@ PREFERENCES_DEFAULTS = {
"view_polygon": True,
"simulation_details_level": 3,
"drill_progress_max_fps": 2,
"gcode_safety_height": 25.0,
"gcode_path_mode": 0,
"gcode_motion_tolerance": 0,
"gcode_naive_tolerance": 0,
"gcode_start_stop_spindle": True,
"external_program_inkscape": "",
"external_program_pstoedit": "",
}
......@@ -507,9 +512,8 @@ class ProjectGui:
self.gui.get_object(objname).connect("toggled", self.update_process_controls)
if objname != "SettingEnableODE":
self.gui.get_object(objname).connect("toggled", self.handle_process_settings_change)
for objname in ("SafetyHeightControl", "OverlapPercentControl",
"MaterialAllowanceControl", "MaxStepDownControl",
"EngraveOffsetControl"):
for objname in ("OverlapPercentControl", "MaterialAllowanceControl",
"MaxStepDownControl", "EngraveOffsetControl"):
self.gui.get_object(objname).connect("value-changed", self.handle_process_settings_change)
self.gui.get_object("ProcessSettingName").connect("changed", self.handle_process_settings_change)
# get/set functions for the current tool/process/bounds/task
......@@ -622,6 +626,23 @@ class ProjectGui:
obj = self.gui.get_object(objname)
self._task_property_signals.append((obj,
obj.connect("changed", self._handle_task_setting_change)))
# gcode settings
gcode_safety_height = self.gui.get_object("SafetyHeightControl")
self.settings.add_item("gcode_safety_height",
gcode_safety_height.get_value, gcode_safety_height.set_value)
gcode_path_mode = self.gui.get_object("GCodeCornerStyleControl")
self.settings.add_item("gcode_path_mode", gcode_path_mode.get_active,
gcode_path_mode.set_active)
gcode_path_mode.connect("changed", self.update_gcode_controls)
gcode_motion_tolerance = self.gui.get_object("GCodeCornerStyleMotionTolerance")
self.settings.add_item("gcode_motion_tolerance",
gcode_motion_tolerance.get_value, gcode_motion_tolerance.set_value)
gcode_naive_tolerance = self.gui.get_object("GCodeCornerStyleCAMTolerance")
self.settings.add_item("gcode_naive_tolerance",
gcode_naive_tolerance.get_value, gcode_naive_tolerance.set_value)
gcode_start_stop_spindle = self.gui.get_object("GCodeStartStopSpindle")
self.settings.add_item("gcode_start_stop_spindle",
gcode_start_stop_spindle.get_active, gcode_start_stop_spindle.set_active)
# configure locations of external programs
for auto_control_name, location_control_name, browse_button, key in (
("ExternalProgramInkscapeAuto",
......@@ -686,6 +707,11 @@ class ProjectGui:
self.update_unit_labels()
self.update_support_grid_controls()
self.update_scale_controls()
self.update_gcode_controls()
def update_gcode_controls(self, widget=None):
path_mode = self.settings.get("gcode_path_mode")
self.gui.get_object("GCodeToleranceTable").set_sensitive(path_mode == 3)
def progress_activity_guard(func):
def progress_activity_guard_wrapper(self, *args, **kwargs):
......@@ -1632,8 +1658,8 @@ class ProjectGui:
if self.gui.get_object("UnitChangeProcesses").get_active():
# scale the process settings
for process in self.process_list:
for key in ("safety_height", "material_allowance",
"step_down", "engrave_offset"):
for key in ("material_allowance", "step_down",
"engrave_offset"):
process[key] *= factor
if self.gui.get_object("UnitChangeBounds").get_active():
# scale the boundaries and keep their center
......@@ -2193,8 +2219,7 @@ class ProjectGui:
if self.gui.get_object(name).get_active():
return name
settings["path_postprocessor"] = get_path_postprocessor()
for objname, key in (("SafetyHeightControl", "safety_height"),
("OverlapPercentControl", "overlap_percent"),
for objname, key in (("OverlapPercentControl", "overlap_percent"),
("MaterialAllowanceControl", "material_allowance"),
("MaxStepDownControl", "step_down"),
("EngraveOffsetControl", "engrave_offset")):
......@@ -2217,8 +2242,7 @@ class ProjectGui:
def set_path_postprocessor(value):
self.gui.get_object(value).set_active(True)
set_path_postprocessor(settings["path_postprocessor"])
for objname, key in (("SafetyHeightControl", "safety_height"),
("OverlapPercentControl", "overlap_percent"),
for objname, key in (("OverlapPercentControl", "overlap_percent"),
("MaterialAllowanceControl", "material_allowance"),
("MaxStepDownControl", "step_down"),
("EngraveOffsetControl", "engrave_offset")):
......@@ -2349,7 +2373,8 @@ class ProjectGui:
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()))
get_time_string(tp.get_machine_time(
safety_height=self.settings.get("gcode_safety_height"))))
model.append(items)
if not new_index is None:
self._treeview_set_active_index(self.toolpath_table, new_index)
......@@ -2648,7 +2673,6 @@ class ProjectGui:
process_settings["path_postprocessor"],
process_settings["path_direction"],
process_settings["material_allowance"],
process_settings["safety_height"],
process_settings["overlap_percent"] / 100.0,
process_settings["step_down"],
process_settings["engrave_offset"])
......@@ -2754,31 +2778,43 @@ class ProjectGui:
# no filename given -> exit
if not filename:
return
if self.settings.get("gcode_safety_height") < self.settings.get("maxz"):
log.warn(("Safety height (%.4f) is below the top of the model " \
+ "(%.4f) - this can cause collisions of the tool with " \
+ "the material.") % (self.settings.get(
"gcode_safety_height"), self.settings.get("maxz")))
try:
destination = open(filename, "w")
index = 0
for index in range(len(self.toolpath)):
tp = self.toolpath[index]
# check if this is the last loop iteration
# only the last toolpath of the list should contain the "M2"
# ("end program") G-code
if index + 1 == len(self.toolpath):
is_last_loop = True
else:
is_last_loop = False
generator = pycam.Exporters.GCodeExporter.GCodeGenerator(
destination,
metric_units=(self.settings.get("unit") == "mm"),
safety_height=self.settings.get("gcode_safety_height"),
toggle_spindle_status=self.settings.get("gcode_start_stop_spindle"),
comment=self.get_meta_data())
path_mode = self.settings.get("gcode_path_mode")
PATH_MODES = pycam.Exporters.GCodeExporter.PATH_MODES
if path_mode == 0:
generator.set_path_mode(PATH_MODES["exact_path"])
elif path_mode == 1:
generator.set_path_mode(PATH_MODES["exact_stop"])
elif path_mode == 2:
generator.set_path_mode(PATH_MODES["continuous"])
else:
naive_tolerance = self.settings.get("gcode_naive_tolerance")
if naive_tolerance == 0:
naive_tolerance = None
generator.set_path_mode(PATH_MODES["continuous"],
self.settings.get("gcode_motion_tolerance"),
naive_tolerance)
for tp in self.toolpath:
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.get_path(), settings.get_unit_size(),
tool["feedrate"], tool["speed"],
safety_height=process["safety_height"],
tool_id=tool["id"], finish_program=is_last_loop,
generator.set_speed(tool["feedrate"], tool["speed"])
generator.add_path_list(tp.get_path(), tool_id=tool["id"],
max_skip_safety_distance=2*tool["tool_radius"],
comment=os.linesep.join(meta_data))
comment=tp.get_meta_data())
generator.finish()
destination.close()
log.info("GCode file successfully written: %s" % str(filename))
except IOError, err_msg:
......
......@@ -131,7 +131,6 @@ speed: 1000
[ProcessDefault]
name: Remove material
path_direction: x
safety_height: 5
engrave_offset: 0.0
path_generator: PushCutter
path_postprocessor: PolygonCutter
......@@ -181,7 +180,6 @@ tool_radius: 0.5
[ProcessDefault]
path_direction: x
safety_height: 25
engrave_offset: 0.0
[Process0]
......@@ -272,7 +270,6 @@ process: 3
"path_direction": str,
"path_generator": str,
"path_postprocessor": str,
"safety_height": float,
"material_allowance": float,
"overlap_percent": int,
"step_down": float,
......@@ -294,7 +291,7 @@ process: 3
"tool": ("name", "shape", "tool_radius", "torus_radius", "feedrate",
"speed"),
"process": ("name", "path_generator", "path_postprocessor",
"path_direction", "safety_height", "material_allowance",
"path_direction", "material_allowance",
"overlap_percent", "step_down", "engrave_offset"),
"bounds": ("name", "type", "x_low", "x_high", "y_low",
"y_high", "z_low", "z_high"),
......@@ -592,7 +589,6 @@ class ToolpathSettings:
"postprocessor": str,
"path_direction": str,
"material_allowance": float,
"safety_height": float,
"overlap": float,
"step_down": float,
"engrave_offset": float,
......@@ -703,14 +699,13 @@ class ToolpathSettings:
return "mm"
def set_process_settings(self, generator, postprocessor, path_direction,
material_allowance=0.0, safety_height=0.0, overlap=0.0,
material_allowance=0.0, overlap=0.0,
step_down=1.0, engrave_offset=0.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,
"engrave_offset": engrave_offset,
......
......@@ -63,13 +63,11 @@ class Dimension:
class DropCutter:
def __init__(self, cutter, model, path_processor, physics=None,
safety_height=INFINITE):
def __init__(self, cutter, model, path_processor, physics=None):
self.cutter = cutter
self.model = model
self.pa = path_processor
self.physics = physics
self.safety_height = safety_height
# remember if we already reported an invalid boundary
self._boundary_warning_already_shown = False
......@@ -130,7 +128,7 @@ class DropCutter:
for p in points:
self.pa.append(p)
else:
p = Point(x, y, self.safety_height)
p = Point(x, y, self.model.maxz)
self.pa.append(p)
if not self._boundary_warning_already_shown:
log.warn("DropCutter: exceed the height of the " \
......
......@@ -48,8 +48,8 @@ def generate_toolpath_from_settings(model, tp_settings, callback=None):
return generate_toolpath(model, tp_settings.get_tool_settings(),
bounds_low, bounds_high, process["path_direction"],
process["generator"], process["postprocessor"],
process["material_allowance"], process["safety_height"],
process["overlap"], process["step_down"], process["engrave_offset"],
process["material_allowance"], process["overlap"],
process["step_down"], process["engrave_offset"],
grid["type"], grid["distance_x"], grid["distance_y"],
grid["thickness"], grid["height"], grid["offset_x"],
grid["offset_y"], grid["adjustments_x"], grid["adjustments_y"],
......@@ -59,8 +59,8 @@ def generate_toolpath_from_settings(model, tp_settings, callback=None):
def generate_toolpath(model, tool_settings=None,
bounds_low=None, bounds_high=None, direction="x",
path_generator="DropCutter", path_postprocessor="ZigZagCutter",
material_allowance=0, safety_height=None, overlap=0, step_down=0,
engrave_offset=0, support_grid_type=None, support_grid_distance_x=None,
material_allowance=0, overlap=0, step_down=0, engrave_offset=0,
support_grid_type=None, support_grid_distance_x=None,
support_grid_distance_y=None, support_grid_thickness=None,
support_grid_height=None, support_grid_offset_x=None,
support_grid_offset_y=None, support_grid_adjustments_x=None,
......@@ -122,7 +122,6 @@ def generate_toolpath(model, tool_settings=None,
"""
overlap = number(overlap)
step_down = number(step_down)
safety_height = number(safety_height)
engrave_offset = number(engrave_offset)
if bounds_low is None:
# no bounds were given - we use the boundaries of the model
......@@ -252,16 +251,12 @@ def generate_toolpath(model, tool_settings=None,
if isinstance(physics, basestring):
return physics
generator = _get_pathgenerator_instance(trimesh_model, contour_model,
cutter, path_generator, path_postprocessor, safety_height, physics)
cutter, path_generator, path_postprocessor, 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"
if safety_height < maxz:
return ("Safety height (%.4f) is within the bounding box height " \
+ "(%.4f) - this can cause collisions of the tool with " \
+ "the material.") % (safety_height, maxz)
# factor "2" since we are based on radius instead of diameter
stepping = 2 * number(tool_settings["tool_radius"]) * (1 - overlap)
if path_generator == "DropCutter":
......@@ -303,7 +298,7 @@ def generate_toolpath(model, tool_settings=None,
return toolpath
def _get_pathgenerator_instance(trimesh_model, contour_model, cutter,
pathgenerator, pathprocessor, safety_height, physics):
pathgenerator, pathprocessor, physics):
if pathgenerator == "DropCutter":
if pathprocessor == "ZigZagCutter":
processor = pycam.PathProcessors.PathAccumulator(zigzag=True)
......@@ -314,7 +309,7 @@ def _get_pathgenerator_instance(trimesh_model, contour_model, cutter,
+ "'ZigZagCutter' or 'PathAccumulator' are allowed") \
% str(pathprocessor)
return DropCutter.DropCutter(cutter, trimesh_model, processor,
physics=physics, safety_height=safety_height)
physics=physics)
elif pathgenerator == "PushCutter":
if pathprocessor == "PathAccumulator":
processor = pycam.PathProcessors.PathAccumulator()
......
......@@ -101,7 +101,7 @@ class Toolpath(object):
else:
self.color = color
def get_machine_time(self, start_position=None):
def get_machine_time(self, start_position=None, safety_height=0.0):
""" calculate an estimation of the time required for processing the
toolpath with the machine
......@@ -122,7 +122,7 @@ class Toolpath(object):
result["time"] += new_pos.sub(result["position"]).norm / feedrate
result["position"] = new_pos
# move to safey height at the starting position
safety_height = number(settings.get_process_settings()["safety_height"])
safety_height = number(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)
......
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