Commit a19f544c authored by sumpfralle's avatar sumpfralle

mirgrated the path generators to a more uniform interface

added a motion-grid generator for engraving
separated the toolpath visualization


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@1125 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent 16acdad6
......@@ -37,6 +37,14 @@
</row>
</data>
</object>
<object class="GtkListStore" id="ProcessList">
<columns>
<!-- column-name ref -->
<column type="gulong"/>
<!-- column-name name -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkWindow" id="window1">
<child>
<object class="GtkVPaned" id="ProcessBox">
......@@ -343,7 +351,7 @@
</packing>
</child>
<child>
<object class="GtkSpinButton" id="OverlapPercentControl">
<object class="GtkSpinButton" id="OverlapPercent">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
......@@ -370,7 +378,7 @@
</packing>
</child>
<child>
<object class="GtkSpinButton" id="MaterialAllowanceControl">
<object class="GtkSpinButton" id="MaterialAllowance">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
......@@ -387,7 +395,7 @@
</packing>
</child>
<child>
<object class="GtkSpinButton" id="MaxStepDownControl">
<object class="GtkSpinButton" id="MaxStepDown">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
......@@ -430,7 +438,7 @@
</packing>
</child>
<child>
<object class="GtkSpinButton" id="EngraveOffsetControl">
<object class="GtkSpinButton" id="EngraveOffset">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property>
......@@ -697,12 +705,4 @@
</object>
</child>
</object>
<object class="GtkListStore" id="ProcessList">
<columns>
<!-- column-name ref -->
<column type="gulong"/>
<!-- column-name name -->
<column type="gchararray"/>
</columns>
</object>
</interface>
......@@ -552,13 +552,14 @@
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
<property name="resize">False</property>
<property name="shrink">False</property>
</packing>
</child>
</object>
......
......@@ -2,10 +2,10 @@
<interface>
<!-- interface-requires gtk+ 2.12 -->
<!-- interface-naming-policy project-wide -->
<object class="GtkAction" id="ExportGCodeVisible">
<property name="label">Export _visible Toolpaths ...</property>
<property name="short_label">Export _visible Toolpaths ...</property>
<property name="tooltip">Write all visible toolpaths to a file.</property>
<object class="GtkAction" id="ExportGCodeSelected">
<property name="label">Export _selected Toolpaths ...</property>
<property name="short_label">Export _selected Toolpaths ...</property>
<property name="tooltip">Write all selected toolpaths to a file.</property>
</object>
<object class="GtkAction" id="ExportGCodeAll">
<property name="label">_Export all Toolpaths ...</property>
......@@ -13,53 +13,29 @@
<property name="tooltip">Write all toolpaths to a file.</property>
<property name="stock_id">gtk-execute</property>
</object>
<object class="GtkListStore" id="ToolPathListModel">
<object class="GtkListStore" id="ToolpathListModel">
<columns>
<!-- column-name index -->
<column type="guint"/>
<!-- column-name ref -->
<column type="gulong"/>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name visible -->
<column type="gboolean"/>
<!-- column-name drill_size -->
<column type="gfloat"/>
<!-- column-name drill_id -->
<column type="guint"/>
<!-- column-name allowance -->
<column type="gfloat"/>
<!-- column-name speed -->
<column type="gfloat"/>
<!-- column-name feedrate -->
<column type="gfloat"/>
<!-- column-name machine_time -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0">0</col>
<col id="1" translatable="yes">Rough</col>
<col id="1" translatable="yes">#1</col>
<col id="2">True</col>
<col id="3">12</col>
<col id="4">0</col>
<col id="5">0</col>
<col id="6">0</col>
<col id="7">0</col>
<col id="8">0</col>
</row>
<row>
<col id="0">0</col>
<col id="1" translatable="yes">test</col>
<col id="1" translatable="yes">#2</col>
<col id="2">False</col>
<col id="3">0</col>
<col id="4">0</col>
<col id="5">0</col>
<col id="6">0</col>
<col id="7">0</col>
<col id="8">0</col>
</row>
</data>
</object>
<object class="GtkVBox" id="ToolpathsTab">
<object class="GtkVBox" id="ToolpathsBox">
<property name="orientation">vertical</property>
<property name="spacing">3</property>
<child>
......@@ -84,28 +60,30 @@
<property name="vscrollbar_policy">automatic</property>
<property name="shadow_type">etched-in</property>
<child>
<object class="GtkTreeView" id="ToolPathTable">
<object class="GtkTreeView" id="ToolpathTable">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">ToolPathListModel</property>
<property name="model">ToolpathListModel</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<child>
<object class="GtkTreeViewColumn" id="Visibility">
<object class="GtkTreeViewColumn" id="ToolpathVisibleColumn">
<property name="title">Visible</property>
<child>
<object class="GtkCellRendererToggle" id="toolpath_visible"/>
<object class="GtkCellRendererPixbuf" id="ToolpathVisibleSymbol">
<property name="stock_size">2</property>
</object>
<attributes>
<attribute name="active">2</attribute>
<attribute name="cell-background">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="Operation">
<object class="GtkTreeViewColumn" id="ToolpathNameColumn">
<property name="title">Operation</property>
<child>
<object class="GtkCellRendererText" id="name"/>
<object class="GtkCellRendererText" id="ToolpathNameCell"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
......@@ -113,59 +91,10 @@
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="Machine Time">
<object class="GtkTreeViewColumn" id="ToolpathTimeColumn">
<property name="title">Machine Time</property>
<child>
<object class="GtkCellRendererText" id="machine_time"/>
<attributes>
<attribute name="text">8</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="Drill">
<property name="title">Drill</property>
<child>
<object class="GtkCellRendererText" id="drill_id"/>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="drill_size"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="Speed">
<property name="visible">False</property>
<property name="title">Speed</property>
<child>
<object class="GtkCellRendererText" id="speed-speed"/>
<attributes>
<attribute name="text">6</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="speed-feedrate"/>
<attributes>
<attribute name="text">7</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="Material Allowance">
<property name="title">Material Allowance</property>
<child>
<object class="GtkCellRendererText" id="allowance"/>
<attributes>
<attribute name="text">5</attribute>
</attributes>
<object class="GtkCellRendererText" id="ToolpathTimeCell"/>
</child>
</object>
</child>
......@@ -182,7 +111,7 @@
<property name="orientation">vertical</property>
<property name="layout_style">center</property>
<child>
<object class="GtkButton" id="toolpath_delete">
<object class="GtkButton" id="ToolpathDelete">
<property name="label">gtk-delete</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
......@@ -196,8 +125,8 @@
</packing>
</child>
<child>
<object class="GtkButton" id="toolpath_up">
<property name="label">gtk-go-up</property>
<object class="GtkButton" id="ToolpathDeleteAll">
<property name="label">gtk-clear</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
......@@ -210,8 +139,8 @@
</packing>
</child>
<child>
<object class="GtkButton" id="toolpath_down">
<property name="label">gtk-go-down</property>
<object class="GtkButton" id="ToolpathMoveUp">
<property name="label">gtk-go-up</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
......@@ -223,6 +152,20 @@
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ToolpathMoveDown">
<property name="label">gtk-go-down</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
......
......@@ -47,6 +47,9 @@ class Line(TransformableContainer):
self.p2 = p2
self.reset_cache()
def copy(self):
return self.__class__(self.p1.copy(), self.p2.copy())
@property
def vector(self):
if self._vector is None:
......
......@@ -69,6 +69,14 @@ def get_combined_bounds(models):
high[2] = model.maxz
return low, high
def get_combined_model(models):
if not models:
return None
result = models.pop(0).copy()
while models:
result += models.pop(0)
return result
class BaseModel(TransformableContainer):
id = 0
......@@ -90,11 +98,9 @@ class BaseModel(TransformableContainer):
def __add__(self, other_model):
""" combine two models """
result = self.__class__()
for item in self.next():
result.append(item)
result = self.copy()
for item in other_model.next():
result.append(item)
result.append(item.copy())
return result
def __len__(self):
......@@ -307,6 +313,12 @@ class Model(BaseModel):
"""
return len(self._triangles)
def copy(self):
result = self.__class__(use_kdtree=self._use_kdtree)
for triangle in self.triangles():
result.append(triangle.copy())
return result
@property
def uuid(self):
if (self.__uuid is None) or self._dirty:
......@@ -421,7 +433,7 @@ class ContourModel(BaseModel):
self.name = "contourmodel%d" % self.id
if plane is None:
# the default plane points upwards along the z axis
plane = Plane(Point(0, 0, 0), Point(0, 0, 1))
plane = Plane(Point(0, 0, 0), Vector(0, 0, 1))
self._plane = plane
self._line_groups = []
self._item_groups.append(self._line_groups)
......@@ -438,6 +450,12 @@ class ContourModel(BaseModel):
"""
return len(self._line_groups)
def copy(self):
result = self.__class__(plane=self._plane.copy())
for polygon in self.get_polygons():
result.append(polygon.copy())
return result
def reset_cache(self):
super(ContourModel, self).reset_cache()
# reset the offset model cache
......
......@@ -52,6 +52,9 @@ class Plane(TransformableContainer):
else:
return cmp(str(self), str(other))
def copy(self):
return self.__class__(self.p.copy(), self.n.copy())
def next(self):
yield self.p
yield self.n
......
......@@ -50,6 +50,9 @@ class Point(object):
self._normsq = self.dot(self)
return self._normsq
def copy(self):
return self.__class__(float(self.x), float(self.y), float(self.z))
def __repr__(self):
return "Point%d<%g,%g,%g>" % (self.id, self.x, self.y, self.z)
......
......@@ -230,6 +230,9 @@ class Polygon(TransformableContainer):
self._area_cache = None
self._cached_offset_polygons = {}
def copy(self):
return self.__class__(plane=self.plane.copy())
def append(self, line):
if not self.is_connectable(line):
raise ValueError("This line does not fit to the polygon")
......
......@@ -91,6 +91,10 @@ class Triangle(TransformableContainer):
def __repr__(self):
return "Triangle%d<%s,%s,%s>" % (self.id, self.p1, self.p2, self.p3)
def copy(self):
return self.__class__(self.p1.copy(), self.p2.copy(), self.p3.copy(),
self.n.copy())
def next(self):
yield self.p1
yield self.p2
......
......@@ -22,8 +22,6 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
from pycam.Geometry.Point import Point
from pycam.Geometry.utils import sqrt
import pycam.Geometry.Model
import pycam.Utils.log
# careful import
try:
......@@ -32,32 +30,9 @@ try:
except (ImportError, RuntimeError):
pass
import gtk
import math
log = pycam.Utils.log.get_logger()
def connect_button_handlers(signal, original_button, derived_button):
""" Join two buttons (probably "toggle" buttons) to keep their values
synchronized.
"""
def derived_handler(widget, original_button=original_button):
original_button.set_active(not original_button.get_active())
derived_handler_id = derived_button.connect_object_after(
signal, derived_handler, derived_button)
def original_handler(original_button, derived_button=derived_button,
derived_handler_id=derived_handler_id):
derived_button.handler_block(derived_handler_id)
# prevent any recursive handler-triggering
if derived_button.get_active() != original_button.get_active():
derived_button.set_active(not derived_button.get_active())
derived_button.handler_unblock(derived_handler_id)
original_button.connect_object_after(signal, original_handler,
original_button)
def keep_gl_mode(func):
def keep_gl_mode_wrapper(*args, **kwargs):
prev_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE)
......@@ -143,20 +118,6 @@ def draw_complete_model_view(settings):
settings.get("color_toolpath_return"),
show_directions=settings.get("show_directions"),
lighting=settings.get("view_light"))
# draw the toolpath
# don't do it, if a new toolpath is just being calculated
safety_height = settings.get("gcode_safety_height")
if settings.get("toolpath") and settings.get("show_toolpath") \
and not settings.get("toolpath_in_progress") \
and not (settings.get("show_simulation") \
and settings.get("simulation_toolpath_moves")):
for toolpath_obj in settings.get("toolpath"):
if toolpath_obj.visible:
draw_toolpath(toolpath_obj.get_moves(safety_height),
settings.get("color_toolpath_cut"),
settings.get("color_toolpath_return"),
show_directions=settings.get("show_directions"),
lighting=settings.get("view_light"))
# draw the drill
if settings.get("show_drill"):
cutter = settings.get("cutter")
......@@ -179,37 +140,3 @@ def draw_complete_model_view(settings):
show_directions=settings.get("show_directions"),
lighting=settings.get("view_light"))
@keep_gl_mode
@keep_matrix
def draw_toolpath(moves, color_cut, color_rapid, show_directions=False, lighting=True):
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glLoadIdentity()
last_position = None
last_rapid = None
if lighting:
GL.glDisable(GL.GL_LIGHTING)
GL.glBegin(GL.GL_LINE_STRIP)
for position, rapid in moves:
if last_rapid != rapid:
GL.glEnd()
if rapid:
GL.glColor4f(*color_rapid)
else:
GL.glColor4f(*color_cut)
# we need to wait until the color change is active
GL.glFinish()
GL.glBegin(GL.GL_LINE_STRIP)
if not last_position is None:
GL.glVertex3f(last_position.x, last_position.y, last_position.z)
last_rapid = rapid
GL.glVertex3f(position.x, position.y, position.z)
last_position = position
GL.glEnd()
if lighting:
GL.glEnable(GL.GL_LIGHTING)
if show_directions:
for index in range(len(moves) - 1):
p1 = moves[index][0]
p2 = moves[index + 1][0]
draw_direction_cone(p1, p2)
......@@ -22,7 +22,6 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
from pycam.Exporters.GCodeExporter import PATH_MODES, GCodeGenerator
import pycam.Exporters.EMCToolExporter
import pycam.Gui.Settings
import pycam.Cutters
......@@ -58,7 +57,6 @@ import pickle
import time
import logging
import datetime
import traceback
import random
import math
import re
......@@ -147,11 +145,6 @@ GTK_COLOR_MAX = 65535.0
log = pycam.Utils.log.get_logger()
def report_exception():
log.error("An unexpected exception occoured: please send the " \
+ "text below to the developers of PyCAM. Thanks a lot!" \
+ os.linesep + traceback.format_exc())
def get_filters_from_list(filter_list):
result = []
for one_filter in filter_list:
......@@ -867,7 +860,7 @@ class ProjectGui(object):
except Exception:
# Catch possible exceptions (except system-exit ones) and
# report them.
report_exception()
log.error(pycam.Utils.get_exception_report())
result = None
self.gui_is_active = False
while self._batch_queue:
......@@ -1576,152 +1569,6 @@ class ProjectGui(object):
self.add_to_recent_file_list(filename)
self.update_save_actions()
def generate_toolpath(self, tool_settings, process_settings, bounds,
progress=None):
start_time = time.time()
if progress:
use_multi_progress = True
else:
use_multi_progress = False
progress = self.settings.get("progress")
progress.update(text="Preparing toolpath generation")
parent = self
class UpdateView(object):
def __init__(self, func, max_fps=1):
self.last_update = time.time()
self.max_fps = max_fps
self.func = func
def update(self, text=None, percent=None, tool_position=None,
toolpath=None):
if parent.settings.get("show_drill_progress"):
if not tool_position is None:
parent.cutter.moveto(tool_position)
if not toolpath is None:
parent.settings.set("toolpath_in_progress", toolpath)
current_time = time.time()
if (current_time - self.last_update) > 1.0/self.max_fps:
self.last_update = current_time
if self.func:
self.func()
# break the loop if someone clicked the "cancel" button
return progress.update(text=text, percent=percent)
draw_callback = UpdateView(
lambda: self.settings.emit_event("visual-item-updated"),
max_fps=self.settings.get("drill_progress_max_fps")).update
progress.update(text="Generating collision model")
# turn the toolpath settings into a dict
toolpath_settings = self.get_toolpath_settings(tool_settings,
process_settings, bounds)
if toolpath_settings is None:
# behave as if "cancel" was requested
if not use_multi_progress:
progress.finish()
return True
self.cutter = toolpath_settings.get_tool()
# TODO: find the right model
model = self.settings.get("models")[0]
# run the toolpath generation
progress.update(text="Starting the toolpath generation")
try:
toolpath = pycam.Toolpath.Generator.generate_toolpath_from_settings(
model, toolpath_settings, callback=draw_callback)
except Exception:
# catch all non-system-exiting exceptions
report_exception()
if not use_multi_progress:
progress.finish()
return False
log.info("Toolpath generation time: %f" % (time.time() - start_time))
# don't show the new toolpath anymore
self.settings.set("toolpath_in_progress", None)
if toolpath is None:
# user interruption
# return "False" if the action was cancelled
result = not progress.update()
elif isinstance(toolpath, basestring):
# an error occoured - "toolpath" contains the error message
log.error("Failed to generate toolpath: %s" % toolpath)
# we were not successful (similar to a "cancel" request)
result = 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
self.toolpath.add_toolpath(toolpath, description, toolpath_settings)
self.update_toolpath_table()
# return "False" if the action was cancelled
cancelled = progress.update()
result = not cancelled
if not use_multi_progress:
progress.finish()
return result
def get_toolpath_settings(self, tool_settings, process_settings, bounds):
toolpath_settings = pycam.Gui.Settings.ToolpathSettings()
# TODO: find the right model
model = self.settings.get("models")[0]
bounds.set_reference(model.get_bounds())
# check if the boundary limits are valid
if not bounds.is_valid():
# don't generate a toolpath if the area is too small (e.g. due to the tool size)
log.error("Processing boundaries are too small for this tool size.")
return None
toolpath_settings.set_bounds(bounds)
# put the tool settings together
tools = self.settings.get("tools")
tool_id = tools.get_attr("id")
toolpath_settings.set_tool(tool_id, tool_settings["shape"],
tool_settings["tool_radius"], tool_settings["torus_radius"],
tool_settings["speed"], tool_settings["feedrate"])
support_model = self.settings.get("current_support_model")
if support_model:
toolpath_settings.set_support_model(support_model)
# calculation backend: ODE / None
if self.settings.get("enable_ode"):
toolpath_settings.set_calculation_backend("ODE")
# unit size
toolpath_settings.set_unit_size(self.settings.get("unit"))
STRATEGY_GENERATORS = {
"PushRemoveStrategy": ("PushCutter", "SimpleCutter"),
"ContourPolygonStrategy": ("PushCutter", "ContourCutter"),
"ContourFollowStrategy": ("ContourFollow", "SimpleCutter"),
"SurfaceStrategy": ("DropCutter", "PathAccumulator"),
"EngraveStrategy": ("EngraveCutter", "SimpleCutter")}
generator, postprocessor = STRATEGY_GENERATORS[
process_settings["path_strategy"]]
# process settings
toolpath_settings.set_process_settings(
generator, postprocessor, process_settings["path_direction"],
process_settings["material_allowance"],
process_settings["overlap_percent"],
process_settings["step_down"],
process_settings["engrave_offset"],
process_settings["milling_style"],
process_settings["pocketing_type"])
return toolpath_settings
def get_filename_via_dialog(self, title, mode_load=False, type_filter=None,
filename_templates=None, filename_extension=None, parent=None):
if parent is None:
......@@ -1837,101 +1684,6 @@ class ProjectGui(object):
if uri.is_local():
self.last_dirname = os.path.dirname(uri.get_local_path())
@gui_activity_guard
def save_toolpath(self, widget=None, only_visible=False):
if not self.toolpath:
return
if callable(widget):
widget = widget()
if isinstance(widget, basestring):
filename = widget
else:
# we open a dialog
if self.settings.get("gcode_filename_extension"):
filename_extension = self.settings.get("gcode_filename_extension")
else:
filename_extension = None
filename = self.get_filename_via_dialog("Save toolpath to ...",
mode_load=False, type_filter=FILTER_GCODE,
filename_templates=(self.last_toolpath_file, self.last_model_uri),
filename_extension=filename_extension)
if filename:
self.last_toolpath_file = filename
self.update_save_actions()
# 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:
if only_visible:
export_toolpaths = [tp for tp in self.toolpath if tp.visible]
else:
export_toolpaths = self.toolpath
destination = open(filename, "w")
safety_height = self.settings.get("gcode_safety_height")
meta_data = self.get_meta_data()
machine_time = 0
# calculate the machine time and store it in the GCode header
for toolpath in export_toolpaths:
machine_time += toolpath.get_machine_time(safety_height)
all_info = meta_data + os.linesep \
+ "Estimated machine time: %.0f minutes" % machine_time
minimum_steps = [self.settings.get("gcode_minimum_step_x"),
self.settings.get("gcode_minimum_step_y"),
self.settings.get("gcode_minimum_step_z")]
if self.settings.get("touch_off_position_type") == "absolute":
pos_x = self.settings.get("touch_off_position_x")
pos_y = self.settings.get("touch_off_position_y")
pos_z = self.settings.get("touch_off_position_z")
touch_off_pos = Point(pos_x, pos_y, pos_z)
else:
touch_off_pos = None
generator = GCodeGenerator(destination,
metric_units=(self.settings.get("unit") == "mm"),
safety_height=safety_height,
toggle_spindle_status=self.settings.get("gcode_start_stop_spindle"),
spindle_delay=self.settings.get("gcode_spindle_delay"),
comment=all_info, minimum_steps=minimum_steps,
touch_off_on_startup=self.settings.get("touch_off_on_startup"),
touch_off_on_tool_change=self.settings.get("touch_off_on_tool_change"),
touch_off_position=touch_off_pos,
touch_off_rapid_move=self.settings.get("touch_off_rapid_move"),
touch_off_slow_move=self.settings.get("touch_off_slow_move"),
touch_off_slow_feedrate=self.settings.get("touch_off_slow_feedrate"),
touch_off_height=self.settings.get("touch_off_height"),
touch_off_pause_execution=self.settings.get("touch_off_pause_execution"))
path_mode = self.settings.get("gcode_path_mode")
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 toolpath in export_toolpaths:
settings = toolpath.get_toolpath_settings()
tool = settings.get_tool_settings()
generator.set_speed(tool["feedrate"], tool["speed"])
generator.add_moves(toolpath.get_moves(safety_height),
tool_id=tool["id"], comment=toolpath.get_meta_data())
generator.finish()
destination.close()
log.info("GCode file successfully written: %s" % str(filename))
except IOError, err_msg:
log.error("Failed to save toolpath file: %s" % err_msg)
else:
self.add_to_recent_file_list(filename)
def get_meta_data(self):
filename = "Filename: %s" % str(self.last_model_uri)
timestamp = "Timestamp: %s" % str(datetime.datetime.now())
......
......@@ -195,9 +195,7 @@ class CollisionPaths(object):
class ContourFollow(object):
def __init__(self, cutter, models, path_processor, physics=None):
self.cutter = cutter
self.models = models
def __init__(self, path_processor, physics=None):
self.pa = path_processor
self._up_vector = Vector(0, 0, 1)
self.physics = physics
......@@ -205,6 +203,7 @@ class ContourFollow(object):
if self.physics:
accuracy = 20
max_depth = 16
# TODO: migrate to new interface
maxx = max([m.maxx for m in self.models])
minx = max([m.minx for m in self.models])
maxy = max([m.maxy for m in self.models])
......@@ -214,15 +213,15 @@ class ContourFollow(object):
math.log(2)
self._physics_maxdepth = min(max_depth, max(ceil(depth), 4))
def _get_free_paths(self, p1, p2):
def _get_free_paths(self, cutter, models, p1, p2):
if self.physics:
return get_free_paths_ode(self.physics, p1, p2,
depth=self._physics_maxdepth)
else:
return get_free_paths_triangles(self.models, self.cutter, p1, p2)
return get_free_paths_triangles(models, cutter, p1, p2)
def GenerateToolPath(self, minx, maxx, miny, maxy, minz, maxz, dz,
draw_callback=None):
def GenerateToolPath(self, cutter, models, minx, maxx, miny, maxy, minz,
maxz, dz, draw_callback=None):
# reset the list of processed triangles
self._processed_triangles = []
# calculate the number of steps
......@@ -236,7 +235,8 @@ class ContourFollow(object):
z_step = diff_z / max(1, (num_of_layers - 1))
# only the first model is used for the contour-follow algorithm
num_of_triangles = len(self.models[0].triangles(minx=minx, miny=miny,
# TODO: should we combine all models?
num_of_triangles = len(models[0].triangles(minx=minx, miny=miny,
maxx=maxx, maxy=maxy))
progress_counter = ProgressCounter(2 * num_of_layers * num_of_triangles,
draw_callback)
......@@ -254,16 +254,16 @@ class ContourFollow(object):
# cancel immediately
break
self.pa.new_direction(0)
self.GenerateToolPathSlice(minx, maxx, miny, maxy, z,
self.GenerateToolPathSlice(cutter, models[0], minx, maxx, miny, maxy, z,
draw_callback, progress_counter, num_of_triangles)
self.pa.end_direction()
self.pa.finish()
current_layer += 1
return self.pa.paths
def GenerateToolPathSlice(self, minx, maxx, miny, maxy, z,
def GenerateToolPathSlice(self, cutter, model, minx, maxx, miny, maxy, z,
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(cutter, model, minx, maxx, miny, maxy,
z, progress_counter=progress_counter)
if num_of_triangles is None:
num_of_triangles = len(shifted_lines)
......@@ -296,14 +296,14 @@ class ContourFollow(object):
self.pa.end_scanline()
return self.pa.paths
def get_potential_contour_lines(self, minx, maxx, miny, maxy, z,
def get_potential_contour_lines(self, cutter, model, minx, maxx, miny, maxy, z,
progress_counter=None):
# use only the first model for the contour
follow_model = self.models[0]
follow_model = model
waterline_triangles = CollisionPaths()
triangles = follow_model.triangles(minx=minx, miny=miny, maxx=maxx,
maxy=maxy)
args = [(follow_model, self.cutter, self._up_vector, t, z)
args = [(follow_model, cutter, self._up_vector, t, z)
for t in triangles if not id(t) in self._processed_triangles]
results_iter = run_in_parallel(_process_one_triangle, args,
unordered=True, callback=progress_counter.update)
......
......@@ -24,6 +24,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
from pycam.PathGenerators import get_max_height_dynamic
from pycam.Utils import ProgressCounter
from pycam.Utils.threading import run_in_parallel
import pycam.Geometry.Model
import pycam.Utils.log
log = pycam.Utils.log.get_logger()
......@@ -39,51 +40,17 @@ def _process_one_grid_line((positions, minz, maxz, model, cutter, physics)):
return get_max_height_dynamic(model, cutter, positions, minz, maxz, physics)
class Dimension(object):
def __init__(self, start, end):
self.start = float(start)
self.end = float(end)
self.min = float(min(start, end))
self.max = float(max(start, end))
self.downward = start > end
self.value = 0.0
def check_bounds(self, value=None, tolerance=None):
if value is None:
value = self.value
if tolerance is None:
return (value >= self.min) and (value <= self.max)
else:
return (value > self.min - tolerance) \
and (value < self.max + tolerance)
def shift(self, distance):
if self.downward:
self.value -= distance
else:
self.value += distance
def set(self, value):
self.value = float(value)
def get(self):
return self.value
class DropCutter(object):
def __init__(self, cutter, models, path_processor, physics=None):
self.cutter = cutter
# combine the models (if there is more than one)
self.model = models[0]
for model in models[1:]:
self.model += model
def __init__(self, path_processor, physics=None):
self.pa = path_processor
self.physics = physics
# remember if we already reported an invalid boundary
def GenerateToolPath(self, motion_grid, minz, maxz, draw_callback=None):
def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None):
quit_requested = False
model = pycam.Geometry.Model.get_combined_model(models)
if not model:
return
# Transfer the grid (a generator) into a list of lists and count the
# items.
......@@ -103,7 +70,7 @@ class DropCutter(object):
for one_grid_line in lines:
# simplify the data (useful for remote processing)
xy_coords = [(pos.x, pos.y) for pos in one_grid_line]
args.append((xy_coords, minz, maxz, self.model, self.cutter,
args.append((xy_coords, minz, maxz, model, cutter,
self.physics))
for points in run_in_parallel(_process_one_grid_line, args,
callback=progress_counter.update):
......
......@@ -25,7 +25,6 @@ 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
......@@ -37,19 +36,7 @@ log = pycam.Utils.log.get_logger()
class EngraveCutter(object):
def __init__(self, cutter, trimesh_models, contour_model, path_processor,
clockwise=False, physics=None):
self.cutter = cutter
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_model = []
self.clockwise = clockwise
self.contour_model = contour_model
def __init__(self, path_processor, physics=None):
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.
......@@ -57,199 +44,44 @@ class EngraveCutter(object):
reverse=self.pa_push.reverse)
self.physics = physics
def GenerateToolPath(self, minz, maxz, horiz_step, dz, draw_callback=None):
def GenerateToolPath(self, cutter, models, motion_grid, minz=None,
maxz=None, draw_callback=None):
quit_requested = False
# calculate the number of steps
num_of_layers = 1 + ceil(abs(maxz - minz) / dz)
if num_of_layers > 1:
z_step = abs(maxz - minz) / (num_of_layers - 1)
z_steps = [(maxz - i * z_step) for i in range(num_of_layers)]
# The top layer is treated as the current surface - thus it does not
# require engraving.
z_steps = z_steps[1:]
else:
z_steps = [minz]
num_of_layers = len(z_steps)
current_layer = 0
num_of_lines = self.contour_model.get_num_of_lines()
progress_counter = ProgressCounter(len(z_steps) * num_of_lines,
draw_callback)
model = pycam.Geometry.Model.get_combined_model(models)
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
# put "last_z" clearly above the model plane
last_z = maxz + 1
push_steps = z_steps
drop_steps = []
else:
# multiple layers
last_z = maxz
push_steps = z_steps[:-1]
drop_steps = [z_steps[-1]]
for z in push_steps:
num_of_layers = len(motion_grid)
push_layers = motion_grid[:-1]
push_generator = pycam.PathGenerators.PushCutter(self.pa_push,
physics=self.physics)
current_layer = 0
for push_layer in push_layers:
# 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 + 1, num_of_layers)):
# cancel immediately
quit_requested = True
break
for line_group in line_groups:
for line in line_group.get_lines():
self.GenerateToolPathLinePush(self.pa_push, line, z, last_z,
draw_callback=draw_callback)
if progress_counter.increment():
# cancel requested
quit_requested = True
# finish the current path
self.pa_push.finish()
break
self.pa_push.finish()
# break the outer loop if requested
if quit_requested:
# no callback: otherwise the status text gets lost
push_generator.GenerateToolpath(cutter, [model], push_layer)
if draw_callback and draw_callback():
# cancel requested
quit_requested = True
break
current_layer += 1
last_z = z
if quit_requested:
return self.pa_push.paths
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 line_groups:
self.pa_drop.new_direction(0)
self.pa_drop.new_scanline()
for line in line_group.get_lines():
self.GenerateToolPathLineDrop(self.pa_drop, line, z, maxz,
horiz_step, last_z, draw_callback=draw_callback)
if progress_counter.increment():
# quit requested
quit_requested = True
break
self.pa_drop.end_scanline()
self.pa_drop.end_direction()
# break the outer loop if requested
if quit_requested:
break
current_layer += 1
last_z = z
self.pa_drop.finish()
drop_generator = pycam.PathGenerators.PushCutter(self.pa_drop)
if draw_callback:
draw_callback(text="Engrave: processing layer" + \
"%d/%d" % (current_layer + 1, num_of_layers))
push_generator.GenerateToolpath(cutter, [model], push_layer,
minz=None, maxz=None)
return self.pa_push.paths + self.pa_drop.paths
def GenerateToolPathLinePush(self, pa, line, z, previous_z,
draw_callback=None):
if previous_z <= line.minz:
# the line is completely above the previous level
pass
elif line.minz < z < line.maxz:
# Split the line at the point at z level and do the calculation
# for both point pairs.
factor = (z - line.p1.z) / (line.p2.z - line.p1.z)
plane_point = line.p1.add(line.vector.mul(factor))
self.GenerateToolPathLinePush(pa, Line(line.p1, plane_point), z,
previous_z, draw_callback=draw_callback)
self.GenerateToolPathLinePush(pa, Line(plane_point, line.p2), z,
previous_z, draw_callback=draw_callback)
elif line.minz < previous_z < line.maxz:
plane = Plane(Point(0, 0, previous_z), Vector(0, 0, 1))
cp = plane.intersect_point(line.dir, line.p1)[0]
# we can be sure that there is an intersection
if line.p1.z > previous_z:
p1, p2 = cp, line.p2
else:
p1, p2 = line.p1, cp
self.GenerateToolPathLinePush(pa, Line(p1, p2), z, previous_z,
draw_callback=draw_callback)
else:
if line.maxz <= z:
# the line is completely below z
p1 = Point(line.p1.x, line.p1.y, z)
p2 = Point(line.p2.x, line.p2.y, z)
elif line.minz >= z:
p1 = line.p1
p2 = line.p2
else:
log.warn("Unexpected condition EC_GTPLP: %s / %s / %s / %s" % \
(line.p1, line.p2, z, previous_z))
return
# no model -> no possible obstacles
# model is completely below z (e.g. support bridges) -> no obstacles
relevant_models = [m for m in self.models if m.maxz >= z]
if not relevant_models:
points = [p1, p2]
elif self.physics:
points = get_free_paths_ode(self.physics, p1, p2)
else:
points = get_free_paths_triangles(relevant_models, self.cutter,
p1, p2)
if points:
for point in points:
pa.append(point)
if draw_callback:
draw_callback(tool_position=points[-1], toolpath=pa.paths)
def GenerateToolPathLineDrop(self, pa, line, minz, maxz, horiz_step,
previous_z, draw_callback=None):
if line.minz >= previous_z:
# the line is not below maxz -> nothing to be done
return
pa.new_direction(0)
pa.new_scanline()
if not self.combined_model:
# no obstacle -> minimum height
# TODO: this "max(..)" is not correct for inclined lines
points = [Point(line.p1.x, line.p1.y, max(minz, line.p1.z)),
Point(line.p2.x, line.p2.y, max(minz, line.p2.z))]
else:
# TODO: this "max(..)" is not correct for inclined lines.
p1 = Point(line.p1.x, line.p1.y, max(minz, line.p1.z))
p2 = Point(line.p2.x, line.p2.y, max(minz, line.p2.z))
distance = line.len
# we want to have at least five steps each
num_of_steps = max(5, 1 + ceil(distance / horiz_step))
# steps may be negative
x_step = (p2.x - p1.x) / (num_of_steps - 1)
y_step = (p2.y - p1.y) / (num_of_steps - 1)
x_steps = [(p1.x + i * x_step) for i in range(num_of_steps)]
y_steps = [(p1.y + i * y_step) for i in range(num_of_steps)]
step_coords = zip(x_steps, y_steps)
# TODO: this "min(..)" is not correct for inclided lines. This
# should be fixed in "get_max_height".
points = get_max_height_dynamic(self.combined_model, self.cutter,
step_coords, min(p1.z, p2.z), maxz, self.physics)
for point in points:
if point is None:
# exceeded maxz - the cutter has to skip this point
pa.end_scanline()
pa.new_scanline()
continue
pa.append(point)
if draw_callback and points:
draw_callback(tool_position=points[-1], toolpath=pa.paths)
pa.end_scanline()
pa.end_direction()
......@@ -45,21 +45,17 @@ def _process_one_line((p1, p2, depth, models, cutter, physics)):
class PushCutter(object):
def __init__(self, cutter, models, path_processor, physics=None):
def __init__(self, path_processor, physics=None):
if physics is None:
log.debug("Starting PushCutter (without ODE)")
else:
log.debug("Starting PushCutter (with ODE)")
self.cutter = cutter
self.models = models
self.pa = path_processor
self.physics = physics
# check if we use a PolygonExtractor
self._use_polygon_extractor = hasattr(self.pa, "pe")
def GenerateToolPath(self, motion_grid, draw_callback=None):
# calculate the number of steps
def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None):
# Transfer the grid (a generator) into a list of lists and count the
# items.
grid = []
......@@ -85,15 +81,15 @@ class PushCutter(object):
break
self.pa.new_direction(0)
self.GenerateToolPathSlice(layer_grid, draw_callback,
self.GenerateToolPathSlice(cutter, models, layer_grid, draw_callback,
progress_counter)
self.pa.end_direction()
self.pa.finish()
current_layer += 1
if self._use_polygon_extractor and (len(self.models) > 1):
other_models = self.models[1:]
if self._use_polygon_extractor and (len(models) > 1):
other_models = models[1:]
# TODO: this is complicated and hacky :(
# we don't use parallelism or ODE (for the sake of simplicity)
final_pa = pycam.PathProcessors.SimpleCutter.SimpleCutter(
......@@ -105,7 +101,7 @@ class PushCutter(object):
pairs.append((path.points[index], path.points[index + 1]))
for p1, p2 in pairs:
free_points = get_free_paths_triangles(other_models,
self.cutter, p1, p2)
cutter, p1, p2)
for point in free_points:
final_pa.append(point)
final_pa.end_scanline()
......@@ -114,7 +110,7 @@ class PushCutter(object):
else:
return self.pa.paths
def GenerateToolPathSlice(self, layer_grid, draw_callback=None,
def GenerateToolPathSlice(self, cutter, models, layer_grid, draw_callback=None,
progress_counter=None):
""" only dx or (exclusive!) dy may be bigger than zero
"""
......@@ -131,14 +127,14 @@ class PushCutter(object):
# the ContourCutter pathprocessor does not work with combined models
if self._use_polygon_extractor:
models = self.models[:1]
models = models[:1]
else:
models = self.models
models = models
args = []
for line in layer_grid:
p1, p2 = line
args.append((p1, p2, depth, models, self.cutter, self.physics))
args.append((p1, p2, depth, models, cutter, self.physics))
for points in run_in_parallel(_process_one_line, args,
callback=progress_counter.update):
......
......@@ -25,6 +25,10 @@ import pycam.Plugins
import pycam.Toolpath
_RELATIVE_UNIT = ("%", "mm")
_BOUNDARY_MODES = ("inside", "along", "around")
class Bounds(pycam.Plugins.ListPluginBase):
UI_FILE = "bounds.ui"
......@@ -32,12 +36,10 @@ class Bounds(pycam.Plugins.ListPluginBase):
COLUMN_REF, COLUMN_NAME = range(2)
LIST_ATTRIBUTE_MAP = {"ref": COLUMN_REF, "name": COLUMN_NAME}
BOUNDARY_MODES = ("inside", "along", "around")
# mapping of boundary types and GUI control elements
BOUNDARY_TYPES = {
pycam.Toolpath.Bounds.TYPE_RELATIVE_MARGIN: "TypeRelativeMargin",
pycam.Toolpath.Bounds.TYPE_CUSTOM: "TypeCustom"}
RELATIVE_UNIT = ("%", "mm")
CONTROL_BUTTONS = ("TypeRelativeMargin", "TypeCustom",
"ToolLimit", "RelativeUnit", "BoundaryLowX",
"BoundaryLowY", "BoundaryLowZ", "BoundaryHighX",
......@@ -184,34 +186,6 @@ class Bounds(pycam.Plugins.ListPluginBase):
for not_found in remaining:
models.remove(not_found)
def get_bounds_limit(self, bounds):
default = (None, None, None), (None, None, None)
get_low_value = lambda axis: bounds["BoundaryLow%s" % "XYZ"[axis]]
get_high_value = lambda axis: bounds["BoundaryHigh%s" % "XYZ"[axis]]
if bounds["TypeRelativeMargin"]:
low_model, high_model = pycam.Geometry.Model.get_combined_bounds(
bounds["Models"])
if None in low_model or None in high_model:
# zero-sized models -> no action
return default
is_percent = self.RELATIVE_UNIT[bounds["RelativeUnit"]] == "%"
low, high = [], []
if is_percent:
for axis in range(3):
dim = high_model[axis] - low_model[axis]
low.append(low_model[axis] - (get_low_value(axis) / 100.0 * dim))
high.append(high_model[axis] + (get_high_value(axis) / 100.0 * dim))
else:
for axis in range(3):
low.append(low_model[axis] - get_low_value(axis))
high.append(high_model[axis] + get_high_value(axis))
else:
low, high = [], []
for axis in range(3):
low.append(get_low_value(axis))
high.append(get_high_value(axis))
return low, high
def _render_model_name(self, column, cell, model, m_iter):
path = model.get_path(m_iter)
all_models = self.core.get("models")
......@@ -386,7 +360,7 @@ class Bounds(pycam.Plugins.ListPluginBase):
self.core.emit_event("bounds-changed")
def _is_percent(self):
return self.RELATIVE_UNIT[self.gui.get_object("RelativeUnit").get_active()] == "%"
return _RELATIVE_UNIT[self.gui.get_object("RelativeUnit").get_active()] == "%"
def _update_controls(self):
bounds = self.get_selected()
......@@ -425,7 +399,22 @@ class Bounds(pycam.Plugins.ListPluginBase):
current_bounds_index = self.get_selected(index=True)
if current_bounds_index is None:
current_bounds_index = 0
new_bounds = {
new_bounds = BoundsDict()
self.append(new_bounds)
self.select(new_bounds)
def _edit_bounds_name(self, cell, path, new_text):
path = int(path)
if (new_text != self._treemodel[path][self.COLUMN_NAME]) and \
new_text:
self._treemodel[path][self.COLUMN_NAME] = new_text
class BoundsDict(dict):
def __init__(self, *args, **kwargs):
super(BoundsDict, self).__init__(*args, **kwargs)
self.update({
"BoundaryLowX": 0,
"BoundaryLowY": 0,
"BoundaryLowZ": 0,
......@@ -434,16 +423,36 @@ class Bounds(pycam.Plugins.ListPluginBase):
"BoundaryHighZ": 0,
"TypeRelativeMargin": True,
"TypeCustom": False,
"RelativeUnit": self.RELATIVE_UNIT.index("%"),
"ToolLimit": self.BOUNDARY_MODES.index("along"),
"RelativeUnit": _RELATIVE_UNIT.index("%"),
"ToolLimit": _BOUNDARY_MODES.index("along"),
"Models": [],
}
self.append(new_bounds)
self.select(new_bounds)
})
def _edit_bounds_name(self, cell, path, new_text):
path = int(path)
if (new_text != self._treemodel[path][self.COLUMN_NAME]) and \
new_text:
self._treemodel[path][self.COLUMN_NAME] = new_text
def get_absolute_limits(self):
default = (None, None, None), (None, None, None)
get_low_value = lambda axis: self["BoundaryLow%s" % "XYZ"[axis]]
get_high_value = lambda axis: self["BoundaryHigh%s" % "XYZ"[axis]]
if self["TypeRelativeMargin"]:
low_model, high_model = pycam.Geometry.Model.get_combined_bounds(
self["Models"])
if None in low_model or None in high_model:
# zero-sized models -> no action
return default
is_percent = _RELATIVE_UNIT[self["RelativeUnit"]] == "%"
low, high = [], []
if is_percent:
for axis in range(3):
dim = high_model[axis] - low_model[axis]
low.append(low_model[axis] - (get_low_value(axis) / 100.0 * dim))
high.append(high_model[axis] + (get_high_value(axis) / 100.0 * dim))
else:
for axis in range(3):
low.append(low_model[axis] - get_low_value(axis))
high.append(high_model[axis] + get_high_value(axis))
else:
low, high = [], []
for axis in range(3):
low.append(get_low_value(axis))
high.append(get_high_value(axis))
return low, high
......@@ -43,7 +43,7 @@ class OpenGLViewBounds(pycam.Plugins.PluginBase):
bounds = self.core.get("bounds").get_selected()
if not bounds:
return
low, high = self.core.get("bounds").get_bounds_limit(bounds)
low, high = bounds.get_absolute_limits()
if None in low or None in high:
return
minx, miny, minz = low[0], low[1], low[2]
......
......@@ -26,7 +26,7 @@ import pycam.Plugins
GTK_COLOR_MAX = 65535.0
class OpenGLWindow(pycam.Plugins.PluginBase):
class OpenGLViewModel(pycam.Plugins.PluginBase):
DEPENDS = ["OpenGLWindow", "Models"]
......
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2011 Lars Kruse <devel@sumpfralle.de>
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/>.
"""
import pycam.Plugins
class OpenGLViewToolpath(pycam.Plugins.PluginBase):
DEPENDS = ["OpenGLWindow", "Toolpaths"]
def setup(self):
import OpenGL.GL
self._GL = OpenGL.GL
self.core.register_event("visualize-items", self.draw_toolpath)
return True
def teardown(self):
self.core.unregister_event("visualize-items", self.draw_toolpath)
return True
def draw_toolpath(self):
if self.core.get("show_toolpath") \
and not self.core.get("toolpath_in_progress") \
and not (self.core.get("show_simulation") \
and self.core.get("simulation_toolpath_moves")):
GL = self._GL
for toolpath in self.core.get("toolpaths").get_visible():
color_rapid = self.core.get("color_toolpath_return")
color_cut = self.core.get("color_toolpath_cut")
show_directions = self.core.get("show_directions")
lighting = self.core.get("view_light")
moves = toolpath.get_moves(self.core.get("gcode_safety_height"))
GL.glMatrixMode(GL.GL_MODELVIEW)
GL.glLoadIdentity()
last_position = None
last_rapid = None
if lighting:
GL.glDisable(GL.GL_LIGHTING)
GL.glBegin(GL.GL_LINE_STRIP)
for position, rapid in moves:
if last_rapid != rapid:
GL.glEnd()
if rapid:
GL.glColor4f(*color_rapid)
else:
GL.glColor4f(*color_cut)
# we need to wait until the color change is active
GL.glFinish()
GL.glBegin(GL.GL_LINE_STRIP)
if not last_position is None:
GL.glVertex3f(last_position.x, last_position.y, last_position.z)
last_rapid = rapid
GL.glVertex3f(position.x, position.y, position.z)
last_position = position
GL.glEnd()
if lighting:
GL.glEnable(GL.GL_LIGHTING)
if show_directions:
for index in range(len(moves) - 1):
p1 = moves[index][0]
p2 = moves[index + 1][0]
draw_direction_cone(p1, p2)
......@@ -30,8 +30,8 @@ class Processes(pycam.Plugins.ListPluginBase):
LIST_ATTRIBUTE_MAP = {"ref": COLUMN_REF, "name": COLUMN_NAME}
CONTROL_BUTTONS = ("PushRemoveStrategy", "ContourPolygonStrategy",
"ContourFollowStrategy", "SurfaceStrategy", "EngraveStrategy",
"OverlapPercentControl", "MaterialAllowanceControl",
"MaxStepDownControl", "EngraveOffsetControl",
"OverlapPercent", "MaterialAllowance",
"MaxStepDown", "EngraveOffset",
"PocketingControl", "GridDirectionX", "GridDirectionY",
"GridDirectionXY", "MillingStyleConventional", "MillingStyleClimb",
"MillingStyleIgnore")
......@@ -128,19 +128,19 @@ class Processes(pycam.Plugins.ListPluginBase):
strategy = key
break
if strategy == "PushRemoveStrategy":
text = "Slice %g%s %d%%" % (data["MaxStepDownControl"],
self.core.get("unit"), data["OverlapPercentControl"])
text = "Slice %g%s %d%%" % (data["MaxStepDown"],
self.core.get("unit"), data["OverlapPercent"])
elif strategy == "ContourPolygonStrategy":
text = "Contour (polygon) %g%s" % (data["MaxStepDownControl"],
text = "Contour (polygon) %g%s" % (data["MaxStepDown"],
self.core.get("unit"))
elif strategy == "ContourFollowStrategy":
text = "Contour (follow) %g%s" % (data["MaxStepDownControl"],
text = "Contour (follow) %g%s" % (data["MaxStepDown"],
self.core.get("unit"))
elif strategy == "SurfaceStrategy":
text = "Surface %d%%" % data["OverlapPercentControl"]
text = "Surface %d%%" % data["OverlapPercent"]
else:
# EngraveStrategy
text = "Engrave %g%s" % (data["EngraveOffsetControl"],
text = "Engrave %g%s" % (data["EngraveOffset"],
self.core.get("unit"))
cell.set_property("text", text)
......@@ -206,10 +206,10 @@ class Processes(pycam.Plugins.ListPluginBase):
"ContourFollowStrategy": False,
"SurfaceStrategy": False,
"EngraveStrategy": False,
"OverlapPercentControl": 10,
"MaterialAllowanceControl": 0,
"MaxStepDownControl": 1,
"EngraveOffsetControl": 0,
"OverlapPercent": 10,
"MaterialAllowance": 0,
"MaxStepDown": 1,
"EngraveOffset": 0,
"PocketingControl": self.POCKETING_TYPES.index("none"),
"GridDirectionX": True,
"GridDirectionY": False,
......@@ -250,26 +250,26 @@ class Processes(pycam.Plugins.ListPluginBase):
return False
all_controls = ("GridDirectionX", "GridDirectionY", "GridDirectionXY",
"MillingStyleConventional", "MillingStyleClimb",
"MillingStyleIgnore", "MaxStepDownControl",
"MaterialAllowanceControl", "OverlapPercentControl",
"EngraveOffsetControl", "PocketingControl")
"MillingStyleIgnore", "MaxStepDown",
"MaterialAllowance", "OverlapPercent",
"EngraveOffset", "PocketingControl")
active_controls = {
"PushRemoveStrategy": ("GridDirectionX", "GridDirectionY",
"GridDirectionXY", "MillingStyleConventional",
"MillingStyleClimb", "MillingStyleIgnore",
"MaxStepDownControl", "MaterialAllowanceControl",
"OverlapPercentControl"),
"MaxStepDown", "MaterialAllowance",
"OverlapPercent"),
# TODO: direction y and xy currently don't work for ContourPolygonStrategy
"ContourPolygonStrategy": ("GridDirectionX",
"MillingStyleIgnore", "MaxStepDownControl",
"MaterialAllowanceControl", "OverlapPercentControl"),
"MillingStyleIgnore", "MaxStepDown",
"MaterialAllowance", "OverlapPercent"),
"ContourFollowStrategy": ("MillingStyleConventional",
"MillingStyleClimb", "MaxStepDownControl"),
"MillingStyleClimb", "MaxStepDown"),
"SurfaceStrategy": ("GridDirectionX", "GridDirectionY",
"GridDirectionXY", "MillingStyleConventional",
"MillingStyleClimb", "MillingStyleIgnore",
"MaterialAllowanceControl", "OverlapPercentControl"),
"EngraveStrategy": ("MaxStepDownControl", "EngraveOffsetControl",
"MaterialAllowance", "OverlapPercent"),
"EngraveStrategy": ("MaxStepDown", "EngraveOffset",
"MillingStyleConventional", "MillingStyleClimb",
"PocketingControl"),
}
......
......@@ -20,8 +20,21 @@ You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
import time
import pycam.Plugins
import pycam.Utils
from pycam.Exporters.GCodeExporter import GCodeGenerator
from pycam.Toolpath.MotionGrid import MILLING_STYLE_IGNORE, \
MILLING_STYLE_CONVENTIONAL, MILLING_STYLE_CLIMB, GRID_DIRECTION_X, \
GRID_DIRECTION_Y, GRID_DIRECTION_XY, get_lines_grid, get_fixed_grid
import pycam.PathProcessors.SimpleCutter
import pycam.PathGenerators.PushCutter
import pycam.PathProcessors.ContourCutter
import pycam.PathGenerators.ContourFollow
import pycam.PathProcessors.PathAccumulator
import pycam.PathGenerators.DropCutter
import pycam.PathGenerators.EngraveCutter
class Tasks(pycam.Plugins.ListPluginBase):
......@@ -29,7 +42,7 @@ class Tasks(pycam.Plugins.ListPluginBase):
UI_FILE = "tasks.ui"
COLUMN_REF, COLUMN_NAME = range(2)
LIST_ATTRIBUTE_MAP = {"id": COLUMN_REF, "name": COLUMN_NAME}
DEPENDS = ["Models", "Tools", "Processes", "Bounds"]
DEPENDS = ["Models", "Tools", "Processes", "Bounds", "Toolpaths"]
def setup(self):
if self.gui:
......@@ -46,6 +59,7 @@ class Tasks(pycam.Plugins.ListPluginBase):
self.gui.get_object(obj_name))
self.gui.get_object("TaskNew").connect("clicked",
self._task_new)
# handle table events
self.core.register_event("task-selection-changed",
self._switch_task)
self.gui.get_object("TaskNameCell").connect("edited",
......@@ -57,6 +71,12 @@ class Tasks(pycam.Plugins.ListPluginBase):
selection.set_mode(self._gtk.SELECTION_MULTIPLE)
self._treemodel = self.gui.get_object("TaskList")
self._treemodel.clear()
# generate toolpaths
self.gui.get_object("GenerateToolPathButton").connect("clicked",
self._generate_selected_toolpaths)
self.gui.get_object("GenerateAllToolPathsButton").connect("clicked",
self._generate_all_toolpaths)
# manage the treemodel
def update_model():
if not hasattr(self, "_model_cache"):
self._model_cache = {}
......@@ -76,6 +96,8 @@ class Tasks(pycam.Plugins.ListPluginBase):
handler = obj.connect("value-changed",
lambda widget: self.core.emit_event("task-changed"))
self._detail_handlers.append((obj, handler))
self.gui.get_object("Models").get_selection().set_mode(
self._gtk.SELECTION_MULTIPLE)
for obj_name in ("Models", "ToolSelector", "ProcessSelector", "BoundsSelector"):
obj = self.gui.get_object(obj_name)
obj.get_model().clear()
......@@ -214,30 +236,148 @@ class Tasks(pycam.Plugins.ListPluginBase):
self.append(new_task)
self.select(new_task)
def process_one_task(self, task_index):
try:
task = self.task_list[task_index]
except IndexError:
# this should only happen, if we were called in batch mode (command line)
log.warn("The given task ID (%d) does not exist. Valid values are: %s." % (task_index, range(len(self.task_list))))
return
self.generate_toolpath(task["tool"], task["process"], task["bounds"])
def process_multiple_tasks(self, task_list=None):
if task_list is None:
task_list = self.task_list[:]
enabled_tasks = []
for index in range(len(task_list)):
task = task_list[index]
if task["enabled"]:
enabled_tasks.append(task)
def generate_toolpaths(self, tasks):
progress = self.core.get("progress")
progress.set_multiple(len(enabled_tasks), "Toolpath")
for task in enabled_tasks:
if not self.generate_toolpath(task["tool"], task["process"],
task["bounds"], progress=progress):
progress.set_multiple(len(tasks), "Toolpath")
for task in tasks:
if not self.generate_toolpath(task, progress=progress):
# break out of the loop, if cancel was requested
break
progress.update_multiple()
progress.finish()
def _generate_selected_toolpaths(self, widget=None):
tasks = self.get_selected()
self.generate_toolpaths(self.get_selected())
def _generate_all_toolpaths(self, widget=None):
self.generate_toolpaths(self)
def _get_path_generator(self, process):
if process["PushRemoveStrategy"]:
processor = pycam.PathProcessors.SimpleCutter.SimpleCutter
generator = pycam.PathGenerators.PushCutter.PushCutter
elif process["ContourPolygonStrategy"]:
processor = pycam.PathProcessors.ContourCutter.ContourCutter
generator = pycam.PathGenerators.PushCutter.PushCutter
elif process["ContourFollowStrategy"]:
processor = pycam.PathProcessors.SimpleCutter.SimpleCutter
generator = pycam.PathGenerators.ContourFollow.ContourFollow
elif process["SurfaceStrategy"]:
processor = pycam.PathProcessors.PathAccumulator.PathAccumulator
generator = pycam.PathGenerators.DropCutter.DropCutter
elif process["EngraveStrategy"]:
processor = pycam.PathProcessors.SimpleCutter.SimpleCutter
generator = pycam.PathGenerators.EngraveCutter.EngraveCutter
else:
self.log.error("Unknown path strategy: %s" % str(process))
return
# TODO: "physics" should be set, as well
return generator(processor(), physics=None)
def _get_motion_grid(self, tool, process, bounds, models):
step_width = float(tool.radius) / 4.0
milling_style_map = {
"MillingStyleConventional": MILLING_STYLE_CONVENTIONAL,
"MillingStyleClimb": MILLING_STYLE_CLIMB,
"MillingStyleIgnore": MILLING_STYLE_IGNORE,
}
for key in milling_style_map:
if process[key]:
milling_style = milling_style_map[key]
break
grid_direction_map = {"GridDirectionX": GRID_DIRECTION_X,
"GridDirectionY": GRID_DIRECTION_Y,
"GridDirectionXY": GRID_DIRECTION_XY,
}
for key in grid_direction_map:
if process[key]:
grid_direction = grid_direction_map[key]
break
line_distance = 2 * float(tool.radius) * \
(1 - 0.01 * float(process["OverlapPercent"]))
# TODO: handle offset and pocketing
if process["EngraveStrategy"]:
return get_lines_grid(models, bounds, process["MaxStepDown"],
step_width=step_width, milling_style=milling_style)
else:
return get_fixed_grid(bounds, process["MaxStepDown"],
line_distance=line_distance, grid_direction=grid_direction,
milling_style=milling_style)
def generate_toolpath(self, task, progress=None):
models = task["models"]
tool = task["tool"]
process = task["process"]
bounds = task["bounds"]
path_generator = self._get_path_generator(process)
motion_grid = self._get_motion_grid(tool, process, bounds, models)
start_time = time.time()
if progress:
use_multi_progress = True
else:
use_multi_progress = False
progress = self.core.get("progress")
progress.update(text="Preparing toolpath generation")
parent = self
class UpdateView(object):
def __init__(self, func, max_fps=1):
self.last_update = time.time()
self.max_fps = max_fps
self.func = func
def update(self, text=None, percent=None, tool_position=None,
toolpath=None):
if parent.core.get("show_drill_progress"):
if not tool_position is None:
parent.cutter.moveto(tool_position)
if not toolpath is None:
parent.core.set("toolpath_in_progress", toolpath)
current_time = time.time()
if (current_time - self.last_update) > 1.0/self.max_fps:
self.last_update = current_time
if self.func:
self.func()
# break the loop if someone clicked the "cancel" button
return progress.update(text=text, percent=percent)
draw_callback = UpdateView(
lambda: self.core.emit_event("visual-item-updated"),
max_fps=self.core.get("drill_progress_max_fps")).update
progress.update(text="Generating collision model")
# run the toolpath generation
progress.update(text="Starting the toolpath generation")
low, high = bounds.get_absolute_limits()
try:
toolpath = path_generator.GenerateToolPath(tool,
models, motion_grid, minz=low[2], maxz=high[2],
draw_callback=draw_callback)
except Exception:
# catch all non-system-exiting exceptions
self.log.error(pycam.Utils.get_exception_report())
if not use_multi_progress:
progress.finish()
return False
self.log.info("Toolpath generation time: %f" % (time.time() - start_time))
# don't show the new toolpath anymore
self.core.set("toolpath_in_progress", None)
if toolpath is None:
# user interruption
# return "False" if the action was cancelled
result = not progress.update()
elif isinstance(toolpath, basestring):
# an error occoured - "toolpath" contains the error message
self.log.error("Failed to generate toolpath: %s" % toolpath)
# we were not successful (similar to a "cancel" request)
result = False
else:
# TODO: create a real toolpath object
self.core.get("toolpaths").append(toolpath)
# return "False" if the action was cancelled
result = not progress.update()
if not use_multi_progress:
progress.finish()
return result
......@@ -26,14 +26,72 @@ import pycam.Plugins
class Toolpaths(pycam.Plugins.ListPluginBase):
UI_FILE = "toolpaths.ui"
COLUMN_REF, COLUMN_NAME, COLUMN_VISIBLE = range(3)
LIST_ATTRIBUTE_MAP = {"name": COLUMN_NAME, "visible": COLUMN_VISIBLE}
ICONS = {"visible": "visible.svg", "hidden": "visible_off.svg"}
def setup(self):
"""
("ExportGCodeAll", self.save_toolpath, False, "<Control><Shift>e"),
("ExportGCodeVisible", self.save_toolpath, True, None),
# store the original content (for adding the number of current toolpaths in "update_toolpath_table")
self._original_toolpath_tab_label = self.gui.get_object("ToolpathsTabLabel").get_text()
"""
self.last_toolpath_file = None
if self.gui:
import gtk
self.tp_box = self.gui.get_object("ToolpathsBox")
self.tp_box.unparent()
self.core.register_ui("main", "Toolpaths", self.tp_box, weight=50)
self._modelview = self.gui.get_object("ToolpathTable")
self._treemodel = self.gui.get_object("ToolpathListModel")
self._treemodel.clear()
for action, obj_name in ((self.ACTION_UP, "ToolpathMoveUp"),
(self.ACTION_DOWN, "ToolpathMoveDown"),
(self.ACTION_DELETE, "ToolpathDelete"),
(self.ACTION_CLEAR, "ToolpathDeleteAll")):
self.register_list_action_button(action, self._modelview,
self.gui.get_object(obj_name))
# handle table changes
self._modelview.connect("row-activated",
self._list_action_toggle_custom, self.COLUMN_VISIBLE)
self.gui.get_object("ToolpathVisibleColumn").set_cell_data_func(
self.gui.get_object("ToolpathVisibleSymbol"),
self._visualize_machine_time)
self.gui.get_object("ToolpathNameCell").connect("edited",
self._edit_toolpath_name)
self.gui.get_object("ToolpathTimeColumn").set_cell_data_func(
self.gui.get_object("ToolpathTimeCell"),
self._visualize_machine_time)
# handle selection changes
selection = self._modelview.get_selection()
selection.connect("changed",
lambda widget, event: self.core.emit_event(event),
"toolpath-selection-changed")
selection.set_mode(gtk.SELECTION_MULTIPLE)
# configure "export" actions
export_all = self.gui.get_object("ExportGCodeAll")
self.register_gtk_accelerator("toolpaths", export_all,
"<Control><Shift>e", "ExportGCodeAll")
export_all.connect("activate", self.save_toolpath, False)
export_visible = self.gui.get_object("ExportGCodeSelected")
self.register_gtk_accelerator("toolpaths", export_visible,
None, "ExportGCodeSelected")
export_visible.connect("activate", self.save_toolpath, True)
# model handling
def update_model():
print "UPDATE"
if not hasattr(self, "_model_cache"):
self._model_cache = {}
cache = self._model_cache
for row in self._treemodel:
cache[row[self.COLUMN_REF]] = list(row)
self._treemodel.clear()
for index, item in enumerate(self):
if id(item) in cache:
self._treemodel.append(cache[id(item)])
else:
self._treemodel.append((id(item),
"Toolpath #%d" % index, True))
self.core.emit_event("toolpath-list-changed")
self.register_model_update(update_model)
self.core.register_event("toolpath-list-changed",
self._update_widgets)
self._update_widgets()
self.core.add_item("toolpaths", lambda: self)
return True
......@@ -41,24 +99,48 @@ class Toolpaths(pycam.Plugins.ListPluginBase):
self.core.set("toolpaths", None)
return True
def _update_toolpath_related_controls(self):
# show or hide the "toolpath" tab
toolpath_tab = self.gui.get_object("ToolpathsTab")
if not self.toolpath:
toolpath_tab.hide()
def get_selected(self):
return self._get_selected(self._modelview, force_list=True)
def get_visible(self):
return [self[index] for index, item in enumerate(self._treemodel)
if item[self.COLUMN_VISIBLE]]
def _update_widgets(self):
toolpaths = self
if not toolpaths:
self.tp_box.hide()
else:
self.gui.get_object("ToolpathsTabLabel").set_text(
"%s (%d)" % (self._original_toolpath_tab_label, len(self.toolpath)))
toolpath_tab.show()
self.tp_box.show()
# enable/disable the export menu item
self.gui.get_object("ExportGCodeAll").set_sensitive(len(self.toolpath) > 0)
toolpaths_are_visible = any([tp.visible for tp in self.toolpath])
self.gui.get_object("ExportGCodeVisible").set_sensitive(
toolpaths_are_visible)
self.gui.get_object("ExportVisibleToolpathsButton").set_sensitive(
toolpaths_are_visible)
self.gui.get_object("ExportGCodeAll").set_sensitive(len(toolpaths) > 0)
selected_toolpaths = self.get_selected()
self.gui.get_object("ExportGCodeSelected").set_sensitive(
len(selected_toolpaths) > 0)
def _update_toolpath_table(self, new_index=None, skip_model_update=False):
def _list_action_toggle_custom(self, treeview, path, clicked_column,
force_column=None):
if force_column is None:
column = self._modelview.get_columns().index(clicked_column)
else:
column = force_column
self._list_action_toggle(clicked_column, str(path[0]), column)
def _list_action_toggle(self, widget, path, column):
path = int(path)
model = self._treemodel
model[path][column] = not model[path][column]
self.core.emit_event("visual-item-updated")
def _edit_toolpath_name(self, cell, path, new_text):
path = int(path)
if (new_text != self._treemodel[path][self.COLUMN_NAME]) and \
new_text:
self._treemodel[path][self.COLUMN_NAME] = new_text
def _visualize_machine_time(self, column, cell, model, m_iter):
path = model.get_path(m_iter)
toolpath = self[path[0]]
def get_time_string(minutes):
if minutes > 180:
return "%d hours" % int(round(minutes / 60))
......@@ -66,15 +148,19 @@ class Toolpaths(pycam.Plugins.ListPluginBase):
return "%d minutes" % int(round(minutes))
else:
return "%d seconds" % int(round(minutes * 60))
self.update_toolpath_related_controls()
text = get_time_string(toolpath.get_machine_time(
self.core.get("gcode_safety_height")))
cell.set_property("text", text)
def _update_toolpath_table(self, new_index=None, skip_model_update=False):
self._update_widgets()
# reset the model data and the selection
if new_index is None:
# keep the old selection - this may return "None" if nothing is selected
new_index = self._treeview_get_active_index(self.toolpath_table, self.toolpath)
if not skip_model_update:
# update the TreeModel data
model = self.gui.get_object("ToolPathListModel")
model.clear()
self._treemodel.clear()
# columns: name, visible, drill_size, drill_id, allowance, speed, feedrate
for index in range(len(self.toolpath)):
tp = self.toolpath[index]
......@@ -85,11 +171,101 @@ class Toolpaths(pycam.Plugins.ListPluginBase):
tool["id"], process["material_allowance"],
tool["speed"], tool["feedrate"],
get_time_string(tp.get_machine_time(
self.settings.get("gcode_safety_height"))))
model.append(items)
self.core.get("gcode_safety_height"))))
self._treemodel.append(items)
if not new_index is None:
self._treeview_set_active_index(self.toolpath_table, new_index)
# enable/disable the modification buttons
self.gui.get_object("toolpath_simulate").set_sensitive(not new_index is None)
self.gui.get_object("ToolpathGrid").set_sensitive(not new_index is None)
def save_toolpath(self, widget=None, only_visible=False):
if only_visible:
toolpaths = self.get_selected()
else:
toolpaths = self
if not toolpaths:
return
if callable(widget):
widget = widget()
if isinstance(widget, basestring):
filename = widget
else:
# we open a dialog
if self.core.get("gcode_filename_extension"):
filename_extension = self.core.get("gcode_filename_extension")
else:
filename_extension = None
# TODO: separate this away from Gui/Project.py
filename = self.get_filename_via_dialog("Save toolpath to ...",
mode_load=False, type_filter=FILTER_GCODE,
filename_templates=(self.last_toolpath_file, self.last_model_uri),
filename_extension=filename_extension)
if filename:
self.last_toolpath_file = filename
self._update_widgets()
# no filename given -> exit
if not filename:
return
try:
destination = open(filename, "w")
safety_height = self.core.get("gcode_safety_height")
meta_data = self.get_meta_data()
machine_time = 0
# calculate the machine time and store it in the GCode header
for toolpath in toolpaths:
machine_time += toolpath.get_machine_time(safety_height)
all_info = meta_data + os.linesep \
+ "Estimated machine time: %.0f minutes" % machine_time
minimum_steps = [self.core.get("gcode_minimum_step_x"),
self.core.get("gcode_minimum_step_y"),
self.core.get("gcode_minimum_step_z")]
if self.core.get("touch_off_position_type") == "absolute":
pos_x = self.core.get("touch_off_position_x")
pos_y = self.core.get("touch_off_position_y")
pos_z = self.core.get("touch_off_position_z")
touch_off_pos = Point(pos_x, pos_y, pos_z)
else:
touch_off_pos = None
generator = GCodeGenerator(destination,
metric_units=(self.core.get("unit") == "mm"),
safety_height=safety_height,
toggle_spindle_status=self.core.get("gcode_start_stop_spindle"),
spindle_delay=self.core.get("gcode_spindle_delay"),
comment=all_info, minimum_steps=minimum_steps,
touch_off_on_startup=self.core.get("touch_off_on_startup"),
touch_off_on_tool_change=self.core.get("touch_off_on_tool_change"),
touch_off_position=touch_off_pos,
touch_off_rapid_move=self.core.get("touch_off_rapid_move"),
touch_off_slow_move=self.core.get("touch_off_slow_move"),
touch_off_slow_feedrate=self.core.get("touch_off_slow_feedrate"),
touch_off_height=self.core.get("touch_off_height"),
touch_off_pause_execution=self.core.get("touch_off_pause_execution"))
path_mode = self.core.get("gcode_path_mode")
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.core.get("gcode_naive_tolerance")
if naive_tolerance == 0:
naive_tolerance = None
generator.set_path_mode(PATH_MODES["continuous"],
self.core.get("gcode_motion_tolerance"),
naive_tolerance)
for toolpath in toolpaths:
settings = toolpath.get_toolpath_settings()
tool = settings.get_tool_settings()
generator.set_speed(tool["feedrate"], tool["speed"])
generator.add_moves(toolpath.get_moves(safety_height),
tool_id=tool["id"], comment=toolpath.get_meta_data())
generator.finish()
destination.close()
log.info("GCode file successfully written: %s" % str(filename))
except IOError, err_msg:
log.error("Failed to save toolpath file: %s" % err_msg)
else:
self.add_to_recent_file_list(filename)
......@@ -87,10 +87,12 @@ class PluginBase(object):
except ImportError:
return
actiongroup = gtk.ActionGroup(groupname)
key, mod = gtk.accelerator_parse(accel_string)
accel_path = "<pycam>/%s" % accel_name
action.set_accel_path(accel_path)
gtk.accel_map_change_entry(accel_path, key, mod, True)
# it is a bit pointless, but we allow an empty accel_string anyway ...
if accel_string:
key, mod = gtk.accelerator_parse(accel_string)
gtk.accel_map_change_entry(accel_path, key, mod, True)
actiongroup.add_action(action)
self.core.get("gtk-uimanager").insert_action_group(actiongroup, pos=-1)
......
......@@ -22,6 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
from pycam.Geometry.Point import Point
from pycam.Geometry.utils import epsilon
from pycam.Geometry.Polygon import PolygonSorter
import math
......@@ -156,7 +157,7 @@ def get_fixed_grid_layer(minx, maxx, miny, maxy, z, line_distance,
return result, end_position
return get_lines(start, end, end_position)
def get_fixed_grid(bounds, layer_distance, line_distance, step_width=None,
def get_fixed_grid(bounds, layer_distance, line_distance=None, step_width=None,
grid_direction=GRID_DIRECTION_X, milling_style=MILLING_STYLE_IGNORE,
start_position=START_Z):
""" Calculate the grid positions for toolpath moves
......@@ -172,6 +173,7 @@ def get_fixed_grid(bounds, layer_distance, line_distance, step_width=None,
reverse=bool(start_position & START_Z))
def get_layers_with_direction(layers):
for layer in layers:
# this will produce a nice xy-grid, as well as simple x and y grids
if grid_direction != GRID_DIRECTION_Y:
yield (layer, GRID_DIRECTION_X)
if grid_direction != GRID_DIRECTION_X:
......@@ -183,3 +185,106 @@ def get_fixed_grid(bounds, layer_distance, line_distance, step_width=None,
start_position=start_position)
yield result
def get_lines_layer(lines, z, last_z=None, step_width=None,
milling_style=MILLING_STYLE_CONVENTIONAL):
get_proj_point = lambda proj_point: Point(proj_point.x, proj_point.y, z)
projected_lines = []
for line in lines:
if (not last_z is None) and (last_z < line.minz):
# the line was processed before
continue
elif line.minz < z < line.maxz:
# Split the line at the point at z level and do the calculation
# for both point pairs.
factor = (z - line.p1.z) / (line.p2.z - line.p1.z)
plane_point = line.p1.add(line.vector.mul(factor))
if line.p1.z < z:
p1 = get_proj_point(line.p1)
p2 = line.p2
else:
p1 = line.p1
p2 = get_proj_point(line.p2)
projected_lines.append(Line(p1, plane_point))
yield Line(plane_point, p2)
elif line.minz < last_z < line.maxz:
plane = Plane(Point(0, 0, last_z), Vector(0, 0, 1))
cp = plane.intersect_point(line.dir, line.p1)[0]
# we can be sure that there is an intersection
if line.p1.z > last_z:
p1, p2 = cp, line.p2
else:
p1, p2 = line.p1, cp
projected_lines.append(Line(p1, p2))
else:
if line.maxz <= z:
# the line is completely below z
projected_lines.append(Line(get_proj_point(line.p1),
get_proj_point(line.p2)))
elif line.minz >= z:
projected_lines.append(line)
else:
log.warn("Unexpected condition 'get_lines_layer': " + \
"%s / %s / %s / %s" % (line.p1, line.p2, z, last_z))
# process all projected lines
for line in projected_lines:
if step_width is None:
yield line.p1
yield line.p2
else:
if isiterable(step_width):
steps = step_width
else:
steps = floatrange(0.0, line.len, inc=step_width)
for step in steps:
yield line.p1.add(line.dir.mul(step))
def _get_sorted_polygons(models, callback=None):
# Sort the polygons according to their directions (first inside, then
# outside. This reduces the problem of break-away pieces.
inner_polys = []
outer_polys = []
for model in models:
for poly in model.get_polygons():
if poly.get_area() <= 0:
inner_polys.append(poly)
else:
outer_polys.append(poly)
inner_sorter = PolygonSorter(inner_polys, callback=callback)
outer_sorter = PolygonSorter(outer_polys, callback=callback)
return inner_sorter.get_polygons() + outer_sorter.get_polygons()
def get_lines_grid(models, bounds, layer_distance, line_distance=None,
step_width=None, grid_direction=None,
milling_style=MILLING_STYLE_CONVENTIONAL,
start_position=None, callback=None):
lines = []
for polygon in _get_sorted_polygons(models, callback=callback):
if polygon.is_closed and \
(milling_style == MILLING_STYLE_CONVENTIONAL):
polygon = polygon.copy()
polygon.reverse()
for line in polygon.get_lines():
lines.append(line)
low, high = bounds.get_absolute_limits()
# the lower limit is never below the model
low_limit_lines = min([line.minz for line in lines])
low[2] = max(low[2], low_limit_lines)
if isiterable(layer_distance):
layers = layer_distance
elif layer_distance is None:
# only one layer
layers = [low[2]]
else:
layers = floatrange(low[2], high[2], inc=layer_distance,
reverse=bool(start_position & START_Z))
last_z = None
if layers:
# the upper layers are used for PushCutter operations
for z in layers[:-1]:
yield get_lines_layer(lines, z, last_z=last_z,
step_width=None, milling_style=milling_style)
last_z = z
# the last layer is used for a DropCutter operation
yield get_lines_layer(lines, layers[-1], last_z=last_z,
step_width=step_width, milling_style=milling_style)
......@@ -22,7 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
__all__ = ["iterators", "polynomials", "ProgressCounter", "threading",
"get_platform", "URIHandler", "PLATFORM_WINDOWS", "PLATFORM_MACOS",
"PLATFORM_LINUX", "PLATFORM_UNKNOWN"]
"PLATFORM_LINUX", "PLATFORM_UNKNOWN", "get_exception_report"]
import sys
import os
......@@ -30,6 +30,7 @@ import re
import socket
import urllib
import urlparse
import traceback
# this is imported below on demand
#import win32com
#import win32api
......@@ -39,6 +40,7 @@ PLATFORM_WINDOWS = 1
PLATFORM_MACOS = 2
PLATFORM_UNKNOWN = 3
# setproctitle is (optionally) imported
try:
from setproctitle import setproctitle
......@@ -191,6 +193,11 @@ def get_all_ips():
filtered_result.sort(cmp=sort_ip_by_relevance)
return filtered_result
def get_exception_report():
return "An unexpected exception occoured: please send the " \
+ "text below to the developers of PyCAM. Thanks a lot!" \
+ os.linesep + traceback.format_exc()
class ProgressCounter(object):
......
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