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 @@ ...@@ -37,6 +37,14 @@
</row> </row>
</data> </data>
</object> </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"> <object class="GtkWindow" id="window1">
<child> <child>
<object class="GtkVPaned" id="ProcessBox"> <object class="GtkVPaned" id="ProcessBox">
...@@ -343,7 +351,7 @@ ...@@ -343,7 +351,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkSpinButton" id="OverlapPercentControl"> <object class="GtkSpinButton" id="OverlapPercent">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property> <property name="invisible_char">&#x25CF;</property>
...@@ -370,7 +378,7 @@ ...@@ -370,7 +378,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkSpinButton" id="MaterialAllowanceControl"> <object class="GtkSpinButton" id="MaterialAllowance">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property> <property name="invisible_char">&#x25CF;</property>
...@@ -387,7 +395,7 @@ ...@@ -387,7 +395,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkSpinButton" id="MaxStepDownControl"> <object class="GtkSpinButton" id="MaxStepDown">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property> <property name="invisible_char">&#x25CF;</property>
...@@ -430,7 +438,7 @@ ...@@ -430,7 +438,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkSpinButton" id="EngraveOffsetControl"> <object class="GtkSpinButton" id="EngraveOffset">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">&#x25CF;</property> <property name="invisible_char">&#x25CF;</property>
...@@ -697,12 +705,4 @@ ...@@ -697,12 +705,4 @@
</object> </object>
</child> </child>
</object> </object>
<object class="GtkListStore" id="ProcessList">
<columns>
<!-- column-name ref -->
<column type="gulong"/>
<!-- column-name name -->
<column type="gchararray"/>
</columns>
</object>
</interface> </interface>
...@@ -552,13 +552,14 @@ ...@@ -552,13 +552,14 @@
</child> </child>
</object> </object>
<packing> <packing>
<property name="expand">False</property>
<property name="position">4</property> <property name="position">4</property>
</packing> </packing>
</child> </child>
</object> </object>
<packing> <packing>
<property name="resize">True</property> <property name="resize">False</property>
<property name="shrink">True</property> <property name="shrink">False</property>
</packing> </packing>
</child> </child>
</object> </object>
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
<interface> <interface>
<!-- interface-requires gtk+ 2.12 --> <!-- interface-requires gtk+ 2.12 -->
<!-- interface-naming-policy project-wide --> <!-- interface-naming-policy project-wide -->
<object class="GtkAction" id="ExportGCodeVisible"> <object class="GtkAction" id="ExportGCodeSelected">
<property name="label">Export _visible Toolpaths ...</property> <property name="label">Export _selected Toolpaths ...</property>
<property name="short_label">Export _visible Toolpaths ...</property> <property name="short_label">Export _selected Toolpaths ...</property>
<property name="tooltip">Write all visible toolpaths to a file.</property> <property name="tooltip">Write all selected toolpaths to a file.</property>
</object> </object>
<object class="GtkAction" id="ExportGCodeAll"> <object class="GtkAction" id="ExportGCodeAll">
<property name="label">_Export all Toolpaths ...</property> <property name="label">_Export all Toolpaths ...</property>
...@@ -13,53 +13,29 @@ ...@@ -13,53 +13,29 @@
<property name="tooltip">Write all toolpaths to a file.</property> <property name="tooltip">Write all toolpaths to a file.</property>
<property name="stock_id">gtk-execute</property> <property name="stock_id">gtk-execute</property>
</object> </object>
<object class="GtkListStore" id="ToolPathListModel"> <object class="GtkListStore" id="ToolpathListModel">
<columns> <columns>
<!-- column-name index --> <!-- column-name ref -->
<column type="guint"/> <column type="gulong"/>
<!-- column-name name --> <!-- column-name name -->
<column type="gchararray"/> <column type="gchararray"/>
<!-- column-name visible --> <!-- column-name visible -->
<column type="gboolean"/> <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> </columns>
<data> <data>
<row> <row>
<col id="0">0</col> <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="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>
<row> <row>
<col id="0">0</col> <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="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> </row>
</data> </data>
</object> </object>
<object class="GtkVBox" id="ToolpathsTab"> <object class="GtkVBox" id="ToolpathsBox">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="spacing">3</property> <property name="spacing">3</property>
<child> <child>
...@@ -84,28 +60,30 @@ ...@@ -84,28 +60,30 @@
<property name="vscrollbar_policy">automatic</property> <property name="vscrollbar_policy">automatic</property>
<property name="shadow_type">etched-in</property> <property name="shadow_type">etched-in</property>
<child> <child>
<object class="GtkTreeView" id="ToolPathTable"> <object class="GtkTreeView" id="ToolpathTable">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">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="headers_clickable">False</property>
<property name="search_column">0</property> <property name="search_column">0</property>
<child> <child>
<object class="GtkTreeViewColumn" id="Visibility"> <object class="GtkTreeViewColumn" id="ToolpathVisibleColumn">
<property name="title">Visible</property> <property name="title">Visible</property>
<child> <child>
<object class="GtkCellRendererToggle" id="toolpath_visible"/> <object class="GtkCellRendererPixbuf" id="ToolpathVisibleSymbol">
<property name="stock_size">2</property>
</object>
<attributes> <attributes>
<attribute name="active">2</attribute> <attribute name="cell-background">3</attribute>
</attributes> </attributes>
</child> </child>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkTreeViewColumn" id="Operation"> <object class="GtkTreeViewColumn" id="ToolpathNameColumn">
<property name="title">Operation</property> <property name="title">Operation</property>
<child> <child>
<object class="GtkCellRendererText" id="name"/> <object class="GtkCellRendererText" id="ToolpathNameCell"/>
<attributes> <attributes>
<attribute name="text">1</attribute> <attribute name="text">1</attribute>
</attributes> </attributes>
...@@ -113,59 +91,10 @@ ...@@ -113,59 +91,10 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkTreeViewColumn" id="Machine Time"> <object class="GtkTreeViewColumn" id="ToolpathTimeColumn">
<property name="title">Machine Time</property> <property name="title">Machine Time</property>
<child> <child>
<object class="GtkCellRendererText" id="machine_time"/> <object class="GtkCellRendererText" id="ToolpathTimeCell"/>
<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>
</child> </child>
</object> </object>
</child> </child>
...@@ -182,7 +111,7 @@ ...@@ -182,7 +111,7 @@
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="layout_style">center</property> <property name="layout_style">center</property>
<child> <child>
<object class="GtkButton" id="toolpath_delete"> <object class="GtkButton" id="ToolpathDelete">
<property name="label">gtk-delete</property> <property name="label">gtk-delete</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
...@@ -196,8 +125,8 @@ ...@@ -196,8 +125,8 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkButton" id="toolpath_up"> <object class="GtkButton" id="ToolpathDeleteAll">
<property name="label">gtk-go-up</property> <property name="label">gtk-clear</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
...@@ -210,8 +139,8 @@ ...@@ -210,8 +139,8 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkButton" id="toolpath_down"> <object class="GtkButton" id="ToolpathMoveUp">
<property name="label">gtk-go-down</property> <property name="label">gtk-go-up</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
...@@ -223,6 +152,20 @@ ...@@ -223,6 +152,20 @@
<property name="position">2</property> <property name="position">2</property>
</packing> </packing>
</child> </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> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
......
...@@ -47,6 +47,9 @@ class Line(TransformableContainer): ...@@ -47,6 +47,9 @@ class Line(TransformableContainer):
self.p2 = p2 self.p2 = p2
self.reset_cache() self.reset_cache()
def copy(self):
return self.__class__(self.p1.copy(), self.p2.copy())
@property @property
def vector(self): def vector(self):
if self._vector is None: if self._vector is None:
......
...@@ -69,6 +69,14 @@ def get_combined_bounds(models): ...@@ -69,6 +69,14 @@ def get_combined_bounds(models):
high[2] = model.maxz high[2] = model.maxz
return low, high 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): class BaseModel(TransformableContainer):
id = 0 id = 0
...@@ -90,11 +98,9 @@ class BaseModel(TransformableContainer): ...@@ -90,11 +98,9 @@ class BaseModel(TransformableContainer):
def __add__(self, other_model): def __add__(self, other_model):
""" combine two models """ """ combine two models """
result = self.__class__() result = self.copy()
for item in self.next():
result.append(item)
for item in other_model.next(): for item in other_model.next():
result.append(item) result.append(item.copy())
return result return result
def __len__(self): def __len__(self):
...@@ -307,6 +313,12 @@ class Model(BaseModel): ...@@ -307,6 +313,12 @@ class Model(BaseModel):
""" """
return len(self._triangles) 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 @property
def uuid(self): def uuid(self):
if (self.__uuid is None) or self._dirty: if (self.__uuid is None) or self._dirty:
...@@ -421,7 +433,7 @@ class ContourModel(BaseModel): ...@@ -421,7 +433,7 @@ class ContourModel(BaseModel):
self.name = "contourmodel%d" % self.id self.name = "contourmodel%d" % self.id
if plane is None: if plane is None:
# the default plane points upwards along the z axis # 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._plane = plane
self._line_groups = [] self._line_groups = []
self._item_groups.append(self._line_groups) self._item_groups.append(self._line_groups)
...@@ -438,6 +450,12 @@ class ContourModel(BaseModel): ...@@ -438,6 +450,12 @@ class ContourModel(BaseModel):
""" """
return len(self._line_groups) 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): def reset_cache(self):
super(ContourModel, self).reset_cache() super(ContourModel, self).reset_cache()
# reset the offset model cache # reset the offset model cache
......
...@@ -52,6 +52,9 @@ class Plane(TransformableContainer): ...@@ -52,6 +52,9 @@ class Plane(TransformableContainer):
else: else:
return cmp(str(self), str(other)) return cmp(str(self), str(other))
def copy(self):
return self.__class__(self.p.copy(), self.n.copy())
def next(self): def next(self):
yield self.p yield self.p
yield self.n yield self.n
......
...@@ -50,6 +50,9 @@ class Point(object): ...@@ -50,6 +50,9 @@ class Point(object):
self._normsq = self.dot(self) self._normsq = self.dot(self)
return self._normsq return self._normsq
def copy(self):
return self.__class__(float(self.x), float(self.y), float(self.z))
def __repr__(self): def __repr__(self):
return "Point%d<%g,%g,%g>" % (self.id, self.x, self.y, self.z) return "Point%d<%g,%g,%g>" % (self.id, self.x, self.y, self.z)
......
...@@ -230,6 +230,9 @@ class Polygon(TransformableContainer): ...@@ -230,6 +230,9 @@ class Polygon(TransformableContainer):
self._area_cache = None self._area_cache = None
self._cached_offset_polygons = {} self._cached_offset_polygons = {}
def copy(self):
return self.__class__(plane=self.plane.copy())
def append(self, line): def append(self, line):
if not self.is_connectable(line): if not self.is_connectable(line):
raise ValueError("This line does not fit to the polygon") raise ValueError("This line does not fit to the polygon")
......
...@@ -91,6 +91,10 @@ class Triangle(TransformableContainer): ...@@ -91,6 +91,10 @@ class Triangle(TransformableContainer):
def __repr__(self): def __repr__(self):
return "Triangle%d<%s,%s,%s>" % (self.id, self.p1, self.p2, self.p3) 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): def next(self):
yield self.p1 yield self.p1
yield self.p2 yield self.p2
......
...@@ -22,8 +22,6 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -22,8 +22,6 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
from pycam.Geometry.Point import Point from pycam.Geometry.Point import Point
from pycam.Geometry.utils import sqrt from pycam.Geometry.utils import sqrt
import pycam.Geometry.Model
import pycam.Utils.log
# careful import # careful import
try: try:
...@@ -32,32 +30,9 @@ try: ...@@ -32,32 +30,9 @@ try:
except (ImportError, RuntimeError): except (ImportError, RuntimeError):
pass pass
import gtk
import math 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(func):
def keep_gl_mode_wrapper(*args, **kwargs): def keep_gl_mode_wrapper(*args, **kwargs):
prev_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE) prev_mode = GL.glGetIntegerv(GL.GL_MATRIX_MODE)
...@@ -143,20 +118,6 @@ def draw_complete_model_view(settings): ...@@ -143,20 +118,6 @@ def draw_complete_model_view(settings):
settings.get("color_toolpath_return"), settings.get("color_toolpath_return"),
show_directions=settings.get("show_directions"), show_directions=settings.get("show_directions"),
lighting=settings.get("view_light")) 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 # draw the drill
if settings.get("show_drill"): if settings.get("show_drill"):
cutter = settings.get("cutter") cutter = settings.get("cutter")
...@@ -179,37 +140,3 @@ def draw_complete_model_view(settings): ...@@ -179,37 +140,3 @@ def draw_complete_model_view(settings):
show_directions=settings.get("show_directions"), show_directions=settings.get("show_directions"),
lighting=settings.get("view_light")) 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/>. ...@@ -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.Exporters.EMCToolExporter
import pycam.Gui.Settings import pycam.Gui.Settings
import pycam.Cutters import pycam.Cutters
...@@ -58,7 +57,6 @@ import pickle ...@@ -58,7 +57,6 @@ import pickle
import time import time
import logging import logging
import datetime import datetime
import traceback
import random import random
import math import math
import re import re
...@@ -147,11 +145,6 @@ GTK_COLOR_MAX = 65535.0 ...@@ -147,11 +145,6 @@ GTK_COLOR_MAX = 65535.0
log = pycam.Utils.log.get_logger() 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): def get_filters_from_list(filter_list):
result = [] result = []
for one_filter in filter_list: for one_filter in filter_list:
...@@ -867,7 +860,7 @@ class ProjectGui(object): ...@@ -867,7 +860,7 @@ class ProjectGui(object):
except Exception: except Exception:
# Catch possible exceptions (except system-exit ones) and # Catch possible exceptions (except system-exit ones) and
# report them. # report them.
report_exception() log.error(pycam.Utils.get_exception_report())
result = None result = None
self.gui_is_active = False self.gui_is_active = False
while self._batch_queue: while self._batch_queue:
...@@ -1576,152 +1569,6 @@ class ProjectGui(object): ...@@ -1576,152 +1569,6 @@ class ProjectGui(object):
self.add_to_recent_file_list(filename) self.add_to_recent_file_list(filename)
self.update_save_actions() 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, def get_filename_via_dialog(self, title, mode_load=False, type_filter=None,
filename_templates=None, filename_extension=None, parent=None): filename_templates=None, filename_extension=None, parent=None):
if parent is None: if parent is None:
...@@ -1837,101 +1684,6 @@ class ProjectGui(object): ...@@ -1837,101 +1684,6 @@ class ProjectGui(object):
if uri.is_local(): if uri.is_local():
self.last_dirname = os.path.dirname(uri.get_local_path()) 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): def get_meta_data(self):
filename = "Filename: %s" % str(self.last_model_uri) filename = "Filename: %s" % str(self.last_model_uri)
timestamp = "Timestamp: %s" % str(datetime.datetime.now()) timestamp = "Timestamp: %s" % str(datetime.datetime.now())
......
...@@ -195,9 +195,7 @@ class CollisionPaths(object): ...@@ -195,9 +195,7 @@ class CollisionPaths(object):
class ContourFollow(object): class ContourFollow(object):
def __init__(self, cutter, models, path_processor, physics=None): def __init__(self, path_processor, physics=None):
self.cutter = cutter
self.models = models
self.pa = path_processor self.pa = path_processor
self._up_vector = Vector(0, 0, 1) self._up_vector = Vector(0, 0, 1)
self.physics = physics self.physics = physics
...@@ -205,6 +203,7 @@ class ContourFollow(object): ...@@ -205,6 +203,7 @@ class ContourFollow(object):
if self.physics: if self.physics:
accuracy = 20 accuracy = 20
max_depth = 16 max_depth = 16
# TODO: migrate to new interface
maxx = max([m.maxx for m in self.models]) maxx = max([m.maxx for m in self.models])
minx = max([m.minx for m in self.models]) minx = max([m.minx for m in self.models])
maxy = max([m.maxy for m in self.models]) maxy = max([m.maxy for m in self.models])
...@@ -214,15 +213,15 @@ class ContourFollow(object): ...@@ -214,15 +213,15 @@ class ContourFollow(object):
math.log(2) math.log(2)
self._physics_maxdepth = min(max_depth, max(ceil(depth), 4)) 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: if self.physics:
return get_free_paths_ode(self.physics, p1, p2, return get_free_paths_ode(self.physics, p1, p2,
depth=self._physics_maxdepth) depth=self._physics_maxdepth)
else: else:
return get_free_paths_triangles(self.models, self.cutter, p1, p2) return get_free_paths_triangles(models, cutter, p1, p2)
def GenerateToolPath(self, minx, maxx, miny, maxy, minz, maxz, dz, def GenerateToolPath(self, cutter, models, minx, maxx, miny, maxy, minz,
draw_callback=None): maxz, dz, draw_callback=None):
# reset the list of processed triangles # reset the list of processed triangles
self._processed_triangles = [] self._processed_triangles = []
# calculate the number of steps # calculate the number of steps
...@@ -236,7 +235,8 @@ class ContourFollow(object): ...@@ -236,7 +235,8 @@ class ContourFollow(object):
z_step = diff_z / max(1, (num_of_layers - 1)) z_step = diff_z / max(1, (num_of_layers - 1))
# only the first model is used for the contour-follow algorithm # 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)) maxx=maxx, maxy=maxy))
progress_counter = ProgressCounter(2 * num_of_layers * num_of_triangles, progress_counter = ProgressCounter(2 * num_of_layers * num_of_triangles,
draw_callback) draw_callback)
...@@ -254,16 +254,16 @@ class ContourFollow(object): ...@@ -254,16 +254,16 @@ class ContourFollow(object):
# cancel immediately # cancel immediately
break break
self.pa.new_direction(0) 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) draw_callback, progress_counter, num_of_triangles)
self.pa.end_direction() self.pa.end_direction()
self.pa.finish() self.pa.finish()
current_layer += 1 current_layer += 1
return self.pa.paths return self.pa.paths
def GenerateToolPathSlice(self, minx, maxx, miny, maxy, z, def GenerateToolPathSlice(self, cutter, model, minx, maxx, miny, maxy, z,
draw_callback=None, progress_counter=None, num_of_triangles=None): draw_callback=None, progress_counter=None, num_of_triangles=None):
shifted_lines = self.get_potential_contour_lines(minx, maxx, miny, maxy, shifted_lines = self.get_potential_contour_lines(cutter, model, minx, maxx, miny, maxy,
z, progress_counter=progress_counter) z, progress_counter=progress_counter)
if num_of_triangles is None: if num_of_triangles is None:
num_of_triangles = len(shifted_lines) num_of_triangles = len(shifted_lines)
...@@ -296,14 +296,14 @@ class ContourFollow(object): ...@@ -296,14 +296,14 @@ class ContourFollow(object):
self.pa.end_scanline() self.pa.end_scanline()
return self.pa.paths return self.pa.paths
def get_potential_contour_lines(self, minx, maxx, miny, maxy, z, def get_potential_contour_lines(self, cutter, model, minx, maxx, miny, maxy, z,
progress_counter=None): progress_counter=None):
# use only the first model for the contour # use only the first model for the contour
follow_model = self.models[0] follow_model = model
waterline_triangles = CollisionPaths() waterline_triangles = CollisionPaths()
triangles = follow_model.triangles(minx=minx, miny=miny, maxx=maxx, triangles = follow_model.triangles(minx=minx, miny=miny, maxx=maxx,
maxy=maxy) 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] for t in triangles if not id(t) in self._processed_triangles]
results_iter = run_in_parallel(_process_one_triangle, args, results_iter = run_in_parallel(_process_one_triangle, args,
unordered=True, callback=progress_counter.update) unordered=True, callback=progress_counter.update)
......
...@@ -24,6 +24,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -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.PathGenerators import get_max_height_dynamic
from pycam.Utils import ProgressCounter from pycam.Utils import ProgressCounter
from pycam.Utils.threading import run_in_parallel from pycam.Utils.threading import run_in_parallel
import pycam.Geometry.Model
import pycam.Utils.log import pycam.Utils.log
log = pycam.Utils.log.get_logger() log = pycam.Utils.log.get_logger()
...@@ -39,51 +40,17 @@ def _process_one_grid_line((positions, minz, maxz, model, cutter, physics)): ...@@ -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) 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): class DropCutter(object):
def __init__(self, cutter, models, path_processor, physics=None): def __init__(self, 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
self.pa = path_processor self.pa = path_processor
self.physics = physics 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 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 # Transfer the grid (a generator) into a list of lists and count the
# items. # items.
...@@ -103,7 +70,7 @@ class DropCutter(object): ...@@ -103,7 +70,7 @@ class DropCutter(object):
for one_grid_line in lines: for one_grid_line in lines:
# simplify the data (useful for remote processing) # simplify the data (useful for remote processing)
xy_coords = [(pos.x, pos.y) for pos in one_grid_line] 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)) self.physics))
for points in run_in_parallel(_process_one_grid_line, args, for points in run_in_parallel(_process_one_grid_line, args,
callback=progress_counter.update): callback=progress_counter.update):
......
...@@ -25,7 +25,6 @@ import pycam.PathProcessors.PathAccumulator ...@@ -25,7 +25,6 @@ import pycam.PathProcessors.PathAccumulator
from pycam.Geometry.Point import Point, Vector from pycam.Geometry.Point import Point, Vector
from pycam.Geometry.Line import Line from pycam.Geometry.Line import Line
from pycam.Geometry.Plane import Plane from pycam.Geometry.Plane import Plane
from pycam.Geometry.Polygon import PolygonSorter
from pycam.Geometry.utils import ceil from pycam.Geometry.utils import ceil
from pycam.PathGenerators import get_max_height_dynamic, get_free_paths_ode, \ from pycam.PathGenerators import get_max_height_dynamic, get_free_paths_ode, \
get_free_paths_triangles get_free_paths_triangles
...@@ -37,19 +36,7 @@ log = pycam.Utils.log.get_logger() ...@@ -37,19 +36,7 @@ log = pycam.Utils.log.get_logger()
class EngraveCutter(object): class EngraveCutter(object):
def __init__(self, cutter, trimesh_models, contour_model, path_processor, def __init__(self, path_processor, physics=None):
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
self.pa_push = path_processor self.pa_push = path_processor
# We use a separated path processor for the last "drop" layer. # We use a separated path processor for the last "drop" layer.
# This path processor does not need to be configurable. # This path processor does not need to be configurable.
...@@ -57,199 +44,44 @@ class EngraveCutter(object): ...@@ -57,199 +44,44 @@ class EngraveCutter(object):
reverse=self.pa_push.reverse) reverse=self.pa_push.reverse)
self.physics = physics 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 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 model = pycam.Geometry.Model.get_combined_model(models)
num_of_lines = self.contour_model.get_num_of_lines()
progress_counter = ProgressCounter(len(z_steps) * num_of_lines,
draw_callback)
if draw_callback: if draw_callback:
draw_callback(text="Engrave: optimizing polygon order") 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. num_of_layers = len(motion_grid)
inner_polys = []
outer_polys = [] push_layers = motion_grid[:-1]
for poly in self.contour_model.get_polygons(): push_generator = pycam.PathGenerators.PushCutter(self.pa_push,
if poly.get_area() <= 0: physics=self.physics)
inner_polys.append(poly) current_layer = 0
else: for push_layer in push_layers:
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:
# update the progress bar and check, if we should cancel the process # update the progress bar and check, if we should cancel the process
if draw_callback and draw_callback(text="Engrave: processing" \ if draw_callback and draw_callback(text="Engrave: processing" \
+ " layer %d/%d" % (current_layer + 1, num_of_layers)): + " layer %d/%d" % (current_layer + 1, num_of_layers)):
# cancel immediately # cancel immediately
quit_requested = True
break break
for line_group in line_groups: # no callback: otherwise the status text gets lost
for line in line_group.get_lines(): push_generator.GenerateToolpath(cutter, [model], push_layer)
self.GenerateToolPathLinePush(self.pa_push, line, z, last_z, if draw_callback and draw_callback():
draw_callback=draw_callback) # cancel requested
if progress_counter.increment(): quit_requested = True
# 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:
break break
current_layer += 1 current_layer += 1
last_z = z
if quit_requested: if quit_requested:
return self.pa_push.paths return self.pa_push.paths
for z in drop_steps: drop_generator = pycam.PathGenerators.PushCutter(self.pa_drop)
if draw_callback: if draw_callback:
draw_callback(text="Engrave: processing layer %d/%d" \ draw_callback(text="Engrave: processing layer" + \
% (current_layer + 1, num_of_layers)) "%d/%d" % (current_layer + 1, num_of_layers))
# process the final layer with a drop cutter push_generator.GenerateToolpath(cutter, [model], push_layer,
for line_group in line_groups: minz=None, maxz=None)
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()
return self.pa_push.paths + self.pa_drop.paths 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)): ...@@ -45,21 +45,17 @@ def _process_one_line((p1, p2, depth, models, cutter, physics)):
class PushCutter(object): class PushCutter(object):
def __init__(self, cutter, models, path_processor, physics=None): def __init__(self, path_processor, physics=None):
if physics is None: if physics is None:
log.debug("Starting PushCutter (without ODE)") log.debug("Starting PushCutter (without ODE)")
else: else:
log.debug("Starting PushCutter (with ODE)") log.debug("Starting PushCutter (with ODE)")
self.cutter = cutter
self.models = models
self.pa = path_processor self.pa = path_processor
self.physics = physics self.physics = physics
# check if we use a PolygonExtractor # check if we use a PolygonExtractor
self._use_polygon_extractor = hasattr(self.pa, "pe") self._use_polygon_extractor = hasattr(self.pa, "pe")
def GenerateToolPath(self, motion_grid, draw_callback=None): def GenerateToolPath(self, cutter, models, motion_grid, minz=None, maxz=None, draw_callback=None):
# calculate the number of steps
# Transfer the grid (a generator) into a list of lists and count the # Transfer the grid (a generator) into a list of lists and count the
# items. # items.
grid = [] grid = []
...@@ -85,15 +81,15 @@ class PushCutter(object): ...@@ -85,15 +81,15 @@ class PushCutter(object):
break break
self.pa.new_direction(0) self.pa.new_direction(0)
self.GenerateToolPathSlice(layer_grid, draw_callback, self.GenerateToolPathSlice(cutter, models, layer_grid, draw_callback,
progress_counter) progress_counter)
self.pa.end_direction() self.pa.end_direction()
self.pa.finish() self.pa.finish()
current_layer += 1 current_layer += 1
if self._use_polygon_extractor and (len(self.models) > 1): if self._use_polygon_extractor and (len(models) > 1):
other_models = self.models[1:] other_models = models[1:]
# TODO: this is complicated and hacky :( # TODO: this is complicated and hacky :(
# we don't use parallelism or ODE (for the sake of simplicity) # we don't use parallelism or ODE (for the sake of simplicity)
final_pa = pycam.PathProcessors.SimpleCutter.SimpleCutter( final_pa = pycam.PathProcessors.SimpleCutter.SimpleCutter(
...@@ -105,7 +101,7 @@ class PushCutter(object): ...@@ -105,7 +101,7 @@ class PushCutter(object):
pairs.append((path.points[index], path.points[index + 1])) pairs.append((path.points[index], path.points[index + 1]))
for p1, p2 in pairs: for p1, p2 in pairs:
free_points = get_free_paths_triangles(other_models, free_points = get_free_paths_triangles(other_models,
self.cutter, p1, p2) cutter, p1, p2)
for point in free_points: for point in free_points:
final_pa.append(point) final_pa.append(point)
final_pa.end_scanline() final_pa.end_scanline()
...@@ -114,7 +110,7 @@ class PushCutter(object): ...@@ -114,7 +110,7 @@ class PushCutter(object):
else: else:
return self.pa.paths 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): progress_counter=None):
""" only dx or (exclusive!) dy may be bigger than zero """ only dx or (exclusive!) dy may be bigger than zero
""" """
...@@ -131,14 +127,14 @@ class PushCutter(object): ...@@ -131,14 +127,14 @@ class PushCutter(object):
# the ContourCutter pathprocessor does not work with combined models # the ContourCutter pathprocessor does not work with combined models
if self._use_polygon_extractor: if self._use_polygon_extractor:
models = self.models[:1] models = models[:1]
else: else:
models = self.models models = models
args = [] args = []
for line in layer_grid: for line in layer_grid:
p1, p2 = line 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, for points in run_in_parallel(_process_one_line, args,
callback=progress_counter.update): callback=progress_counter.update):
......
...@@ -25,6 +25,10 @@ import pycam.Plugins ...@@ -25,6 +25,10 @@ import pycam.Plugins
import pycam.Toolpath import pycam.Toolpath
_RELATIVE_UNIT = ("%", "mm")
_BOUNDARY_MODES = ("inside", "along", "around")
class Bounds(pycam.Plugins.ListPluginBase): class Bounds(pycam.Plugins.ListPluginBase):
UI_FILE = "bounds.ui" UI_FILE = "bounds.ui"
...@@ -32,12 +36,10 @@ class Bounds(pycam.Plugins.ListPluginBase): ...@@ -32,12 +36,10 @@ class Bounds(pycam.Plugins.ListPluginBase):
COLUMN_REF, COLUMN_NAME = range(2) COLUMN_REF, COLUMN_NAME = range(2)
LIST_ATTRIBUTE_MAP = {"ref": COLUMN_REF, "name": COLUMN_NAME} LIST_ATTRIBUTE_MAP = {"ref": COLUMN_REF, "name": COLUMN_NAME}
BOUNDARY_MODES = ("inside", "along", "around")
# mapping of boundary types and GUI control elements # mapping of boundary types and GUI control elements
BOUNDARY_TYPES = { BOUNDARY_TYPES = {
pycam.Toolpath.Bounds.TYPE_RELATIVE_MARGIN: "TypeRelativeMargin", pycam.Toolpath.Bounds.TYPE_RELATIVE_MARGIN: "TypeRelativeMargin",
pycam.Toolpath.Bounds.TYPE_CUSTOM: "TypeCustom"} pycam.Toolpath.Bounds.TYPE_CUSTOM: "TypeCustom"}
RELATIVE_UNIT = ("%", "mm")
CONTROL_BUTTONS = ("TypeRelativeMargin", "TypeCustom", CONTROL_BUTTONS = ("TypeRelativeMargin", "TypeCustom",
"ToolLimit", "RelativeUnit", "BoundaryLowX", "ToolLimit", "RelativeUnit", "BoundaryLowX",
"BoundaryLowY", "BoundaryLowZ", "BoundaryHighX", "BoundaryLowY", "BoundaryLowZ", "BoundaryHighX",
...@@ -184,34 +186,6 @@ class Bounds(pycam.Plugins.ListPluginBase): ...@@ -184,34 +186,6 @@ class Bounds(pycam.Plugins.ListPluginBase):
for not_found in remaining: for not_found in remaining:
models.remove(not_found) 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): def _render_model_name(self, column, cell, model, m_iter):
path = model.get_path(m_iter) path = model.get_path(m_iter)
all_models = self.core.get("models") all_models = self.core.get("models")
...@@ -386,7 +360,7 @@ class Bounds(pycam.Plugins.ListPluginBase): ...@@ -386,7 +360,7 @@ class Bounds(pycam.Plugins.ListPluginBase):
self.core.emit_event("bounds-changed") self.core.emit_event("bounds-changed")
def _is_percent(self): 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): def _update_controls(self):
bounds = self.get_selected() bounds = self.get_selected()
...@@ -425,7 +399,22 @@ class Bounds(pycam.Plugins.ListPluginBase): ...@@ -425,7 +399,22 @@ class Bounds(pycam.Plugins.ListPluginBase):
current_bounds_index = self.get_selected(index=True) current_bounds_index = self.get_selected(index=True)
if current_bounds_index is None: if current_bounds_index is None:
current_bounds_index = 0 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, "BoundaryLowX": 0,
"BoundaryLowY": 0, "BoundaryLowY": 0,
"BoundaryLowZ": 0, "BoundaryLowZ": 0,
...@@ -434,16 +423,36 @@ class Bounds(pycam.Plugins.ListPluginBase): ...@@ -434,16 +423,36 @@ class Bounds(pycam.Plugins.ListPluginBase):
"BoundaryHighZ": 0, "BoundaryHighZ": 0,
"TypeRelativeMargin": True, "TypeRelativeMargin": True,
"TypeCustom": False, "TypeCustom": False,
"RelativeUnit": self.RELATIVE_UNIT.index("%"), "RelativeUnit": _RELATIVE_UNIT.index("%"),
"ToolLimit": self.BOUNDARY_MODES.index("along"), "ToolLimit": _BOUNDARY_MODES.index("along"),
"Models": [], "Models": [],
} })
self.append(new_bounds)
self.select(new_bounds)
def _edit_bounds_name(self, cell, path, new_text): def get_absolute_limits(self):
path = int(path) default = (None, None, None), (None, None, None)
if (new_text != self._treemodel[path][self.COLUMN_NAME]) and \ get_low_value = lambda axis: self["BoundaryLow%s" % "XYZ"[axis]]
new_text: get_high_value = lambda axis: self["BoundaryHigh%s" % "XYZ"[axis]]
self._treemodel[path][self.COLUMN_NAME] = new_text 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): ...@@ -43,7 +43,7 @@ class OpenGLViewBounds(pycam.Plugins.PluginBase):
bounds = self.core.get("bounds").get_selected() bounds = self.core.get("bounds").get_selected()
if not bounds: if not bounds:
return 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: if None in low or None in high:
return return
minx, miny, minz = low[0], low[1], low[2] minx, miny, minz = low[0], low[1], low[2]
......
...@@ -26,7 +26,7 @@ import pycam.Plugins ...@@ -26,7 +26,7 @@ import pycam.Plugins
GTK_COLOR_MAX = 65535.0 GTK_COLOR_MAX = 65535.0
class OpenGLWindow(pycam.Plugins.PluginBase): class OpenGLViewModel(pycam.Plugins.PluginBase):
DEPENDS = ["OpenGLWindow", "Models"] 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): ...@@ -30,8 +30,8 @@ class Processes(pycam.Plugins.ListPluginBase):
LIST_ATTRIBUTE_MAP = {"ref": COLUMN_REF, "name": COLUMN_NAME} LIST_ATTRIBUTE_MAP = {"ref": COLUMN_REF, "name": COLUMN_NAME}
CONTROL_BUTTONS = ("PushRemoveStrategy", "ContourPolygonStrategy", CONTROL_BUTTONS = ("PushRemoveStrategy", "ContourPolygonStrategy",
"ContourFollowStrategy", "SurfaceStrategy", "EngraveStrategy", "ContourFollowStrategy", "SurfaceStrategy", "EngraveStrategy",
"OverlapPercentControl", "MaterialAllowanceControl", "OverlapPercent", "MaterialAllowance",
"MaxStepDownControl", "EngraveOffsetControl", "MaxStepDown", "EngraveOffset",
"PocketingControl", "GridDirectionX", "GridDirectionY", "PocketingControl", "GridDirectionX", "GridDirectionY",
"GridDirectionXY", "MillingStyleConventional", "MillingStyleClimb", "GridDirectionXY", "MillingStyleConventional", "MillingStyleClimb",
"MillingStyleIgnore") "MillingStyleIgnore")
...@@ -128,19 +128,19 @@ class Processes(pycam.Plugins.ListPluginBase): ...@@ -128,19 +128,19 @@ class Processes(pycam.Plugins.ListPluginBase):
strategy = key strategy = key
break break
if strategy == "PushRemoveStrategy": if strategy == "PushRemoveStrategy":
text = "Slice %g%s %d%%" % (data["MaxStepDownControl"], text = "Slice %g%s %d%%" % (data["MaxStepDown"],
self.core.get("unit"), data["OverlapPercentControl"]) self.core.get("unit"), data["OverlapPercent"])
elif strategy == "ContourPolygonStrategy": elif strategy == "ContourPolygonStrategy":
text = "Contour (polygon) %g%s" % (data["MaxStepDownControl"], text = "Contour (polygon) %g%s" % (data["MaxStepDown"],
self.core.get("unit")) self.core.get("unit"))
elif strategy == "ContourFollowStrategy": elif strategy == "ContourFollowStrategy":
text = "Contour (follow) %g%s" % (data["MaxStepDownControl"], text = "Contour (follow) %g%s" % (data["MaxStepDown"],
self.core.get("unit")) self.core.get("unit"))
elif strategy == "SurfaceStrategy": elif strategy == "SurfaceStrategy":
text = "Surface %d%%" % data["OverlapPercentControl"] text = "Surface %d%%" % data["OverlapPercent"]
else: else:
# EngraveStrategy # EngraveStrategy
text = "Engrave %g%s" % (data["EngraveOffsetControl"], text = "Engrave %g%s" % (data["EngraveOffset"],
self.core.get("unit")) self.core.get("unit"))
cell.set_property("text", text) cell.set_property("text", text)
...@@ -206,10 +206,10 @@ class Processes(pycam.Plugins.ListPluginBase): ...@@ -206,10 +206,10 @@ class Processes(pycam.Plugins.ListPluginBase):
"ContourFollowStrategy": False, "ContourFollowStrategy": False,
"SurfaceStrategy": False, "SurfaceStrategy": False,
"EngraveStrategy": False, "EngraveStrategy": False,
"OverlapPercentControl": 10, "OverlapPercent": 10,
"MaterialAllowanceControl": 0, "MaterialAllowance": 0,
"MaxStepDownControl": 1, "MaxStepDown": 1,
"EngraveOffsetControl": 0, "EngraveOffset": 0,
"PocketingControl": self.POCKETING_TYPES.index("none"), "PocketingControl": self.POCKETING_TYPES.index("none"),
"GridDirectionX": True, "GridDirectionX": True,
"GridDirectionY": False, "GridDirectionY": False,
...@@ -250,26 +250,26 @@ class Processes(pycam.Plugins.ListPluginBase): ...@@ -250,26 +250,26 @@ class Processes(pycam.Plugins.ListPluginBase):
return False return False
all_controls = ("GridDirectionX", "GridDirectionY", "GridDirectionXY", all_controls = ("GridDirectionX", "GridDirectionY", "GridDirectionXY",
"MillingStyleConventional", "MillingStyleClimb", "MillingStyleConventional", "MillingStyleClimb",
"MillingStyleIgnore", "MaxStepDownControl", "MillingStyleIgnore", "MaxStepDown",
"MaterialAllowanceControl", "OverlapPercentControl", "MaterialAllowance", "OverlapPercent",
"EngraveOffsetControl", "PocketingControl") "EngraveOffset", "PocketingControl")
active_controls = { active_controls = {
"PushRemoveStrategy": ("GridDirectionX", "GridDirectionY", "PushRemoveStrategy": ("GridDirectionX", "GridDirectionY",
"GridDirectionXY", "MillingStyleConventional", "GridDirectionXY", "MillingStyleConventional",
"MillingStyleClimb", "MillingStyleIgnore", "MillingStyleClimb", "MillingStyleIgnore",
"MaxStepDownControl", "MaterialAllowanceControl", "MaxStepDown", "MaterialAllowance",
"OverlapPercentControl"), "OverlapPercent"),
# TODO: direction y and xy currently don't work for ContourPolygonStrategy # TODO: direction y and xy currently don't work for ContourPolygonStrategy
"ContourPolygonStrategy": ("GridDirectionX", "ContourPolygonStrategy": ("GridDirectionX",
"MillingStyleIgnore", "MaxStepDownControl", "MillingStyleIgnore", "MaxStepDown",
"MaterialAllowanceControl", "OverlapPercentControl"), "MaterialAllowance", "OverlapPercent"),
"ContourFollowStrategy": ("MillingStyleConventional", "ContourFollowStrategy": ("MillingStyleConventional",
"MillingStyleClimb", "MaxStepDownControl"), "MillingStyleClimb", "MaxStepDown"),
"SurfaceStrategy": ("GridDirectionX", "GridDirectionY", "SurfaceStrategy": ("GridDirectionX", "GridDirectionY",
"GridDirectionXY", "MillingStyleConventional", "GridDirectionXY", "MillingStyleConventional",
"MillingStyleClimb", "MillingStyleIgnore", "MillingStyleClimb", "MillingStyleIgnore",
"MaterialAllowanceControl", "OverlapPercentControl"), "MaterialAllowance", "OverlapPercent"),
"EngraveStrategy": ("MaxStepDownControl", "EngraveOffsetControl", "EngraveStrategy": ("MaxStepDown", "EngraveOffset",
"MillingStyleConventional", "MillingStyleClimb", "MillingStyleConventional", "MillingStyleClimb",
"PocketingControl"), "PocketingControl"),
} }
......
...@@ -20,8 +20,21 @@ You should have received a copy of the GNU General Public License ...@@ -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/>. along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
""" """
import time
import pycam.Plugins 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): class Tasks(pycam.Plugins.ListPluginBase):
...@@ -29,7 +42,7 @@ class Tasks(pycam.Plugins.ListPluginBase): ...@@ -29,7 +42,7 @@ class Tasks(pycam.Plugins.ListPluginBase):
UI_FILE = "tasks.ui" UI_FILE = "tasks.ui"
COLUMN_REF, COLUMN_NAME = range(2) COLUMN_REF, COLUMN_NAME = range(2)
LIST_ATTRIBUTE_MAP = {"id": COLUMN_REF, "name": COLUMN_NAME} LIST_ATTRIBUTE_MAP = {"id": COLUMN_REF, "name": COLUMN_NAME}
DEPENDS = ["Models", "Tools", "Processes", "Bounds"] DEPENDS = ["Models", "Tools", "Processes", "Bounds", "Toolpaths"]
def setup(self): def setup(self):
if self.gui: if self.gui:
...@@ -46,6 +59,7 @@ class Tasks(pycam.Plugins.ListPluginBase): ...@@ -46,6 +59,7 @@ class Tasks(pycam.Plugins.ListPluginBase):
self.gui.get_object(obj_name)) self.gui.get_object(obj_name))
self.gui.get_object("TaskNew").connect("clicked", self.gui.get_object("TaskNew").connect("clicked",
self._task_new) self._task_new)
# handle table events
self.core.register_event("task-selection-changed", self.core.register_event("task-selection-changed",
self._switch_task) self._switch_task)
self.gui.get_object("TaskNameCell").connect("edited", self.gui.get_object("TaskNameCell").connect("edited",
...@@ -57,6 +71,12 @@ class Tasks(pycam.Plugins.ListPluginBase): ...@@ -57,6 +71,12 @@ class Tasks(pycam.Plugins.ListPluginBase):
selection.set_mode(self._gtk.SELECTION_MULTIPLE) selection.set_mode(self._gtk.SELECTION_MULTIPLE)
self._treemodel = self.gui.get_object("TaskList") self._treemodel = self.gui.get_object("TaskList")
self._treemodel.clear() 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(): def update_model():
if not hasattr(self, "_model_cache"): if not hasattr(self, "_model_cache"):
self._model_cache = {} self._model_cache = {}
...@@ -76,6 +96,8 @@ class Tasks(pycam.Plugins.ListPluginBase): ...@@ -76,6 +96,8 @@ class Tasks(pycam.Plugins.ListPluginBase):
handler = obj.connect("value-changed", handler = obj.connect("value-changed",
lambda widget: self.core.emit_event("task-changed")) lambda widget: self.core.emit_event("task-changed"))
self._detail_handlers.append((obj, handler)) 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"): for obj_name in ("Models", "ToolSelector", "ProcessSelector", "BoundsSelector"):
obj = self.gui.get_object(obj_name) obj = self.gui.get_object(obj_name)
obj.get_model().clear() obj.get_model().clear()
...@@ -214,30 +236,148 @@ class Tasks(pycam.Plugins.ListPluginBase): ...@@ -214,30 +236,148 @@ class Tasks(pycam.Plugins.ListPluginBase):
self.append(new_task) self.append(new_task)
self.select(new_task) self.select(new_task)
def process_one_task(self, task_index): def generate_toolpaths(self, tasks):
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)
progress = self.core.get("progress") progress = self.core.get("progress")
progress.set_multiple(len(enabled_tasks), "Toolpath") progress.set_multiple(len(tasks), "Toolpath")
for task in enabled_tasks: for task in tasks:
if not self.generate_toolpath(task["tool"], task["process"], if not self.generate_toolpath(task, progress=progress):
task["bounds"], progress=progress):
# break out of the loop, if cancel was requested # break out of the loop, if cancel was requested
break break
progress.update_multiple() progress.update_multiple()
progress.finish() 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 ...@@ -26,14 +26,72 @@ import pycam.Plugins
class Toolpaths(pycam.Plugins.ListPluginBase): class Toolpaths(pycam.Plugins.ListPluginBase):
UI_FILE = "toolpaths.ui" 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): def setup(self):
""" self.last_toolpath_file = None
("ExportGCodeAll", self.save_toolpath, False, "<Control><Shift>e"), if self.gui:
("ExportGCodeVisible", self.save_toolpath, True, None), import gtk
# store the original content (for adding the number of current toolpaths in "update_toolpath_table") self.tp_box = self.gui.get_object("ToolpathsBox")
self._original_toolpath_tab_label = self.gui.get_object("ToolpathsTabLabel").get_text() 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) self.core.add_item("toolpaths", lambda: self)
return True return True
...@@ -41,24 +99,48 @@ class Toolpaths(pycam.Plugins.ListPluginBase): ...@@ -41,24 +99,48 @@ class Toolpaths(pycam.Plugins.ListPluginBase):
self.core.set("toolpaths", None) self.core.set("toolpaths", None)
return True return True
def _update_toolpath_related_controls(self): def get_selected(self):
# show or hide the "toolpath" tab return self._get_selected(self._modelview, force_list=True)
toolpath_tab = self.gui.get_object("ToolpathsTab")
if not self.toolpath: def get_visible(self):
toolpath_tab.hide() 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: else:
self.gui.get_object("ToolpathsTabLabel").set_text( self.tp_box.show()
"%s (%d)" % (self._original_toolpath_tab_label, len(self.toolpath)))
toolpath_tab.show()
# enable/disable the export menu item # enable/disable the export menu item
self.gui.get_object("ExportGCodeAll").set_sensitive(len(self.toolpath) > 0) self.gui.get_object("ExportGCodeAll").set_sensitive(len(toolpaths) > 0)
toolpaths_are_visible = any([tp.visible for tp in self.toolpath]) selected_toolpaths = self.get_selected()
self.gui.get_object("ExportGCodeVisible").set_sensitive( self.gui.get_object("ExportGCodeSelected").set_sensitive(
toolpaths_are_visible) len(selected_toolpaths) > 0)
self.gui.get_object("ExportVisibleToolpathsButton").set_sensitive(
toolpaths_are_visible)
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): def get_time_string(minutes):
if minutes > 180: if minutes > 180:
return "%d hours" % int(round(minutes / 60)) return "%d hours" % int(round(minutes / 60))
...@@ -66,15 +148,19 @@ class Toolpaths(pycam.Plugins.ListPluginBase): ...@@ -66,15 +148,19 @@ class Toolpaths(pycam.Plugins.ListPluginBase):
return "%d minutes" % int(round(minutes)) return "%d minutes" % int(round(minutes))
else: else:
return "%d seconds" % int(round(minutes * 60)) 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 # reset the model data and the selection
if new_index is None: if new_index is None:
# keep the old selection - this may return "None" if nothing is selected # keep the old selection - this may return "None" if nothing is selected
new_index = self._treeview_get_active_index(self.toolpath_table, self.toolpath) new_index = self._treeview_get_active_index(self.toolpath_table, self.toolpath)
if not skip_model_update: if not skip_model_update:
# update the TreeModel data # update the TreeModel data
model = self.gui.get_object("ToolPathListModel") self._treemodel.clear()
model.clear()
# columns: name, visible, drill_size, drill_id, allowance, speed, feedrate # columns: name, visible, drill_size, drill_id, allowance, speed, feedrate
for index in range(len(self.toolpath)): for index in range(len(self.toolpath)):
tp = self.toolpath[index] tp = self.toolpath[index]
...@@ -85,11 +171,101 @@ class Toolpaths(pycam.Plugins.ListPluginBase): ...@@ -85,11 +171,101 @@ class Toolpaths(pycam.Plugins.ListPluginBase):
tool["id"], process["material_allowance"], tool["id"], process["material_allowance"],
tool["speed"], tool["feedrate"], tool["speed"], tool["feedrate"],
get_time_string(tp.get_machine_time( get_time_string(tp.get_machine_time(
self.settings.get("gcode_safety_height")))) self.core.get("gcode_safety_height"))))
model.append(items) self._treemodel.append(items)
if not new_index is None: if not new_index is None:
self._treeview_set_active_index(self.toolpath_table, new_index) self._treeview_set_active_index(self.toolpath_table, new_index)
# enable/disable the modification buttons # enable/disable the modification buttons
self.gui.get_object("toolpath_simulate").set_sensitive(not new_index is None) 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) 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): ...@@ -87,10 +87,12 @@ class PluginBase(object):
except ImportError: except ImportError:
return return
actiongroup = gtk.ActionGroup(groupname) actiongroup = gtk.ActionGroup(groupname)
key, mod = gtk.accelerator_parse(accel_string)
accel_path = "<pycam>/%s" % accel_name accel_path = "<pycam>/%s" % accel_name
action.set_accel_path(accel_path) 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) actiongroup.add_action(action)
self.core.get("gtk-uimanager").insert_action_group(actiongroup, pos=-1) 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/>. ...@@ -22,6 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
from pycam.Geometry.Point import Point from pycam.Geometry.Point import Point
from pycam.Geometry.utils import epsilon from pycam.Geometry.utils import epsilon
from pycam.Geometry.Polygon import PolygonSorter
import math import math
...@@ -156,7 +157,7 @@ def get_fixed_grid_layer(minx, maxx, miny, maxy, z, line_distance, ...@@ -156,7 +157,7 @@ def get_fixed_grid_layer(minx, maxx, miny, maxy, z, line_distance,
return result, end_position return result, end_position
return get_lines(start, end, 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, grid_direction=GRID_DIRECTION_X, milling_style=MILLING_STYLE_IGNORE,
start_position=START_Z): start_position=START_Z):
""" Calculate the grid positions for toolpath moves """ Calculate the grid positions for toolpath moves
...@@ -172,6 +173,7 @@ def get_fixed_grid(bounds, layer_distance, line_distance, step_width=None, ...@@ -172,6 +173,7 @@ def get_fixed_grid(bounds, layer_distance, line_distance, step_width=None,
reverse=bool(start_position & START_Z)) reverse=bool(start_position & START_Z))
def get_layers_with_direction(layers): def get_layers_with_direction(layers):
for layer in 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: if grid_direction != GRID_DIRECTION_Y:
yield (layer, GRID_DIRECTION_X) yield (layer, GRID_DIRECTION_X)
if grid_direction != 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, ...@@ -183,3 +185,106 @@ def get_fixed_grid(bounds, layer_distance, line_distance, step_width=None,
start_position=start_position) start_position=start_position)
yield result 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/>. ...@@ -22,7 +22,7 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
__all__ = ["iterators", "polynomials", "ProgressCounter", "threading", __all__ = ["iterators", "polynomials", "ProgressCounter", "threading",
"get_platform", "URIHandler", "PLATFORM_WINDOWS", "PLATFORM_MACOS", "get_platform", "URIHandler", "PLATFORM_WINDOWS", "PLATFORM_MACOS",
"PLATFORM_LINUX", "PLATFORM_UNKNOWN"] "PLATFORM_LINUX", "PLATFORM_UNKNOWN", "get_exception_report"]
import sys import sys
import os import os
...@@ -30,6 +30,7 @@ import re ...@@ -30,6 +30,7 @@ import re
import socket import socket
import urllib import urllib
import urlparse import urlparse
import traceback
# this is imported below on demand # this is imported below on demand
#import win32com #import win32com
#import win32api #import win32api
...@@ -39,6 +40,7 @@ PLATFORM_WINDOWS = 1 ...@@ -39,6 +40,7 @@ PLATFORM_WINDOWS = 1
PLATFORM_MACOS = 2 PLATFORM_MACOS = 2
PLATFORM_UNKNOWN = 3 PLATFORM_UNKNOWN = 3
# setproctitle is (optionally) imported # setproctitle is (optionally) imported
try: try:
from setproctitle import setproctitle from setproctitle import setproctitle
...@@ -191,6 +193,11 @@ def get_all_ips(): ...@@ -191,6 +193,11 @@ def get_all_ips():
filtered_result.sort(cmp=sort_ip_by_relevance) filtered_result.sort(cmp=sort_ip_by_relevance)
return filtered_result 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): 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