Commit 9b5a670d authored by sumpfralle's avatar sumpfralle

added cropping of toolpath (reduces machine time)


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@920 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent 6fb115db
...@@ -8,6 +8,7 @@ Version 0.4.1 - UNRELEASED ...@@ -8,6 +8,7 @@ Version 0.4.1 - UNRELEASED
* changed "simulation mode" for visualizing the machine moves * changed "simulation mode" for visualizing the machine moves
* added a very simple "pocketing" mode for 2D models * added a very simple "pocketing" mode for 2D models
* added 2D projection of 3D models * added 2D projection of 3D models
* added toolpath cropping
* added support for DXF feature "LWPOLYLINE" * added support for DXF feature "LWPOLYLINE"
* added a configuration setting for automatically loading a custom task settings file on startup * added a configuration setting for automatically loading a custom task settings file on startup
* added a simple "undo" feature for reversing model manipulations * added a simple "undo" feature for reversing model manipulations
......
...@@ -4282,19 +4282,34 @@ Usually you will want to use the cutter radius here to cut around the outline.</ ...@@ -4282,19 +4282,34 @@ Usually you will want to use the cutter radius here to cut around the outline.</
<property name="spacing">2</property> <property name="spacing">2</property>
<child> <child>
<object class="GtkButton" id="toolpath_simulate"> <object class="GtkButton" id="toolpath_simulate">
<property name="label" translatable="yes">Simulation <property name="label" translatable="yes">Simulate</property>
(experimental)</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>
<property name="tooltip_text" translatable="yes">Show a simple simulation of the generated toolpath.</property> <property name="tooltip_text" translatable="yes">Show a simple simulation of the generated toolpath.</property>
<property name="image">ShowToolPathSimulation</property> <property name="image">ShowToolPathSimulation</property>
<property name="xalign">0</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkButton" id="toolpath_crop">
<property name="label" translatable="yes">Crop</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Crop the selected toolpath to the current 2D model (or alternatively the 2D projection of the current 3D model) with a positive offset of the tool diameter. </property>
<property name="image">CropToolpath</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="position">1</property>
</packing>
</child>
<child> <child>
<object class="GtkButton" id="toolpath_delete"> <object class="GtkButton" id="toolpath_delete">
<property name="label">gtk-delete</property> <property name="label">gtk-delete</property>
...@@ -4306,7 +4321,7 @@ Usually you will want to use the cutter radius here to cut around the outline.</ ...@@ -4306,7 +4321,7 @@ Usually you will want to use the cutter radius here to cut around the outline.</
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="position">1</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
<child> <child>
...@@ -4320,7 +4335,7 @@ Usually you will want to use the cutter radius here to cut around the outline.</ ...@@ -4320,7 +4335,7 @@ Usually you will want to use the cutter radius here to cut around the outline.</
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="position">2</property> <property name="position">3</property>
</packing> </packing>
</child> </child>
<child> <child>
...@@ -4334,7 +4349,7 @@ Usually you will want to use the cutter radius here to cut around the outline.</ ...@@ -4334,7 +4349,7 @@ Usually you will want to use the cutter radius here to cut around the outline.</
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="position">3</property> <property name="position">4</property>
</packing> </packing>
</child> </child>
</object> </object>
...@@ -8019,4 +8034,8 @@ upon interesting bugs and weird results.</property> ...@@ -8019,4 +8034,8 @@ upon interesting bugs and weird results.</property>
<property name="upper">100</property> <property name="upper">100</property>
<property name="step_increment">1</property> <property name="step_increment">1</property>
</object> </object>
<object class="GtkImage" id="CropToolpath">
<property name="visible">True</property>
<property name="stock">gtk-zoom-fit</property>
</object>
</interface> </interface>
...@@ -1067,3 +1067,30 @@ class Polygon(TransformableContainer): ...@@ -1067,3 +1067,30 @@ class Polygon(TransformableContainer):
result.append(poly) result.append(poly)
return result return result
def split_line(self, line):
outer = []
inner = []
# project the line onto the polygon's plane
proj_line = self.plane.get_line_projection(line)
intersections = []
for pline in self.get_lines():
cp, d = proj_line.get_intersection(pline)
if cp:
intersections.append((cp, d))
# sort the intersections
intersections.sort(key=lambda (cp, d): d)
intersections.insert(0, (proj_line.p1, 0))
intersections.append((proj_line.p2, 1))
get_original_point = lambda d: line.p1.add(line.vector.mul(d))
for index in range(len(intersections) - 1):
p1, d1 = intersections[index]
p2, d2 = intersections[index + 1]
if p1 != p2:
middle = p1.add(p2).div(2)
new_line = Line(get_original_point(d1), get_original_point(d2))
if self.is_point_inside(middle):
inner.append(new_line)
else:
outer.append(new_line)
return (inner, outer)
...@@ -829,6 +829,7 @@ class ProjectGui: ...@@ -829,6 +829,7 @@ class ProjectGui:
self.gui.get_object("toolpath_down").connect("clicked", self.toolpath_table_event, "move_down") self.gui.get_object("toolpath_down").connect("clicked", self.toolpath_table_event, "move_down")
self.gui.get_object("toolpath_delete").connect("clicked", self.toolpath_table_event, "delete") self.gui.get_object("toolpath_delete").connect("clicked", self.toolpath_table_event, "delete")
self.gui.get_object("toolpath_simulate").connect("clicked", self.toolpath_table_event, "simulate") self.gui.get_object("toolpath_simulate").connect("clicked", self.toolpath_table_event, "simulate")
self.gui.get_object("toolpath_crop").connect("clicked", self.toolpath_table_event, "crop")
self.gui.get_object("ExitSimulationButton").connect("clicked", self.finish_toolpath_simulation) self.gui.get_object("ExitSimulationButton").connect("clicked", self.finish_toolpath_simulation)
speed_factor_widget = self.gui.get_object("SimulationSpeedFactor") speed_factor_widget = self.gui.get_object("SimulationSpeedFactor")
self.settings.add_item("simulation_speed_factor", self.settings.add_item("simulation_speed_factor",
...@@ -1094,8 +1095,8 @@ class ProjectGui: ...@@ -1094,8 +1095,8 @@ class ProjectGui:
# for now we only store the model # for now we only store the model
if not self.model: if not self.model:
return return
log.debug("Storing the current state of the model for undo")
self._undo_states.append(pickle.dumps(self.model)) self._undo_states.append(pickle.dumps(self.model))
log.debug("Stored the current state of the model for undo")
while len(self._undo_states) > MAX_UNDO_STATES: while len(self._undo_states) > MAX_UNDO_STATES:
self._undo_states.pop(0) self._undo_states.pop(0)
self.gui.get_object("UndoButton").set_sensitive(True) self.gui.get_object("UndoButton").set_sensitive(True)
...@@ -1103,13 +1104,13 @@ class ProjectGui: ...@@ -1103,13 +1104,13 @@ class ProjectGui:
def _restore_undo_state(self, widget=None, event=None): def _restore_undo_state(self, widget=None, event=None):
if len(self._undo_states) > 0: if len(self._undo_states) > 0:
latest = StringIO.StringIO(self._undo_states.pop(-1)) latest = StringIO.StringIO(self._undo_states.pop(-1))
log.info("Restoring the previous state of the model")
self.model = pickle.Unpickler(latest).load() self.model = pickle.Unpickler(latest).load()
self.gui.get_object("UndoButton").set_sensitive( self.gui.get_object("UndoButton").set_sensitive(
len(self._undo_states) > 0) len(self._undo_states) > 0)
log.info("Restored the previous state of the model")
self._update_all_model_attributes() self._update_all_model_attributes()
else: else:
log.info("No previous undo state available - ignoring request") log.info("No previous undo state available - request ignored")
def show_help(self, widget=None, page="Main_Page"): def show_help(self, widget=None, page="Main_Page"):
if not page.startswith("http"): if not page.startswith("http"):
...@@ -2536,23 +2537,27 @@ class ProjectGui: ...@@ -2536,23 +2537,27 @@ class ProjectGui:
self.model.shift(new_x - old_x, new_y - old_y, new_z - old_z, self.model.shift(new_x - old_x, new_y - old_y, new_z - old_z,
callback=self.update_progress_bar) callback=self.update_progress_bar)
@progress_activity_guard
@gui_activity_guard def _get_projection_plane(self):
def projection_2d(self, widget=None):
self.update_progress_bar("Calculating 2D projection")
# determine projection plane # determine projection plane
if (self.model.maxz < 0) or (self.model.minz > 0): if (self.model.maxz < 0) or (self.model.minz > 0):
# completely above or below zero # completely above or below zero
plane_z = self.model.minz plane_z = self.model.minz
else: else:
plane_z = 0 plane_z = 0
plane = Plane(Point(0, 0, plane_z), Vector(0, 0, 1)) return Plane(Point(0, 0, plane_z), Vector(0, 0, 1))
log.info("Projecting 3D model at level z=%g" % plane_z)
@progress_activity_guard
@gui_activity_guard
def projection_2d(self, widget=None):
self.update_progress_bar("Calculating 2D projection")
plane = self._get_projection_plane()
log.info("Projecting 3D model at level z=%g" % plane.p.z)
projection = self.model.get_waterline_contour(plane) projection = self.model.get_waterline_contour(plane)
if projection.get_num_of_lines() > 0: if projection.get_num_of_lines() > 0:
self.load_model(projection) self.load_model(projection)
else: else:
log.warn("The 2D projection at z=%g is empty. Aborted." % plane_z) log.warn("The 2D projection at z=%g is empty. Aborted." % plane.p.z)
@progress_activity_guard @progress_activity_guard
@gui_activity_guard @gui_activity_guard
...@@ -3089,12 +3094,36 @@ class ProjectGui: ...@@ -3089,12 +3094,36 @@ class ProjectGui:
index = self._treeview_get_active_index(self.toolpath_table, self.toolpath) index = self._treeview_get_active_index(self.toolpath_table, self.toolpath)
if not index is None: if not index is None:
self.show_toolpath_simulation(self.toolpath[index]) self.show_toolpath_simulation(self.toolpath[index])
self._treeview_button_event(self.toolpath_table, self.toolpath, action, self.update_toolpath_table) elif action == "crop":
index = self._treeview_get_active_index(self.toolpath_table, self.toolpath)
if not index is None:
self.crop_toolpath(self.toolpath[index])
# process the default operations (move, delete)
self._treeview_button_event(self.toolpath_table, self.toolpath, action,
self.update_toolpath_table)
# do some post-processing ... # do some post-processing ...
if action == "delete": if action in ("delete", "crop"):
# hide the deleted toolpath immediately # hide the deleted toolpath immediately
self.update_view() self.update_view()
@progress_activity_guard
def crop_toolpath(self, tp):
if hasattr(self.model, "get_polygons"):
contour = self.model
elif hasattr(self.model, "get_waterline_contour"):
plane = self._get_projection_plane()
self.update_progress_bar("Calculating the 2D projection")
contour = self.model.get_waterline_contour(plane)
else:
log.warn(("The current model (%s) does not support " \
+ "projections") % str(type(self.model)))
return
self.update_progress_bar("Applying the tool diameter offset")
offset_model = contour.get_offset_model(
2 * tp.get_tool_settings()["tool_radius"])
self.update_progress_bar("Cropping the toolpath")
tp.crop(offset_model.get_polygons(), callback=self.update_progress_bar)
def update_toolpath_table(self, new_index=None, skip_model_update=False): def update_toolpath_table(self, new_index=None, skip_model_update=False):
def get_time_string(minutes): def get_time_string(minutes):
if minutes > 180: if minutes > 180:
...@@ -3142,6 +3171,7 @@ class ProjectGui: ...@@ -3142,6 +3171,7 @@ class ProjectGui:
self.gui.get_object("toolpath_delete").set_sensitive(not new_index is None) self.gui.get_object("toolpath_delete").set_sensitive(not new_index is None)
self.gui.get_object("toolpath_down").set_sensitive((not new_index is None) and (new_index + 1 < len(self.toolpath))) self.gui.get_object("toolpath_down").set_sensitive((not new_index is None) and (new_index + 1 < len(self.toolpath)))
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("toolpath_crop").set_sensitive(not new_index is None)
@gui_activity_guard @gui_activity_guard
def save_task_settings_file(self, widget=None, filename=None): def save_task_settings_file(self, widget=None, filename=None):
......
...@@ -23,6 +23,8 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>. ...@@ -23,6 +23,8 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
__all__ = ["simplify_toolpath", "ToolpathList", "Toolpath", "Generator"] __all__ = ["simplify_toolpath", "ToolpathList", "Toolpath", "Generator"]
from pycam.Geometry.Point import Point from pycam.Geometry.Point import Point
from pycam.Geometry.Path import Path
from pycam.Geometry.Line import Line
from pycam.Geometry.utils import number, epsilon from pycam.Geometry.utils import number, epsilon
import pycam.Utils.log import pycam.Utils.log
import random import random
...@@ -205,6 +207,52 @@ class Toolpath(object): ...@@ -205,6 +207,52 @@ class Toolpath(object):
current_position = new_pos current_position = new_pos
return result return result
def crop(self, polygons, callback=None):
# collect all existing toolpath lines
open_lines = []
for path in self.toolpath:
if path:
for index in range(len(path.points) - 1):
open_lines.append(Line(path.points[index], path.points[index + 1]))
# go through all polygons and add "inner" lines (or parts thereof) to
# the final list of remaining lines
inner_lines = []
for polygon in polygons:
new_open_lines = []
for line in open_lines:
if callback and callback():
return
inner, outer = polygon.split_line(line)
inner_lines.extend(inner)
new_open_lines.extend(outer)
open_lines = new_open_lines
# turn all "inner_lines" into toolpath movements
new_paths = []
current_path = Path()
if inner_lines:
line = inner_lines.pop(0)
current_path.append(line.p1)
current_path.append(line.p2)
while inner_lines:
if callback and callback():
return
end = current_path.points[-1]
# look for the next connected point
for line in inner_lines:
if line.p1 == end:
inner_lines.remove(line)
current_path.append(line.p2)
break
else:
new_paths.append(current_path)
current_path = Path()
line = inner_lines.pop(0)
current_path.append(line.p1)
current_path.append(line.p2)
if current_path.points:
new_paths.append(current_path)
self.toolpath = new_paths
class Bounds: class Bounds:
......
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