From 42f0d71d4af18a3bb6ab6e5d6904541aa521e814 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Tue, 1 Aug 2017 13:24:30 -0700 Subject: [PATCH] Split Contour op into generic PathArea part and contour specifics. --- src/Mod/Path/CMakeLists.txt | 1 + src/Mod/Path/PathScripts/PathAreaOp.py | 257 +++++++++++++ src/Mod/Path/PathScripts/PathAreaUtils.py | 431 ---------------------- src/Mod/Path/PathScripts/PathContour.py | 282 ++++---------- 4 files changed, 320 insertions(+), 651 deletions(-) create mode 100644 src/Mod/Path/PathScripts/PathAreaOp.py delete mode 100644 src/Mod/Path/PathScripts/PathAreaUtils.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 7b895527d9..645f12aac8 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -18,6 +18,7 @@ INSTALL( SET(PathScripts_SRCS PathCommands.py + PathScripts/PathAreaOp.py PathScripts/PathArray.py PathScripts/PathComment.py PathScripts/PathCompoundExtended.py diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py new file mode 100644 index 0000000000..9697dd4cc0 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program 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 Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import Path + +from PathScripts.PathUtils import depth_params +from PathScripts.PathUtils import makeWorkplane +from PathScripts.PathUtils import waiting_effects +from PySide import QtCore + +__title__ = "Base class for PathArea based operations." +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule() + +# Qt tanslation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +class ObjectOp(object): + FeatureTool = 0x01 + FeatureDepths = 0x02 + FeatureHeights = 0x04 + FeatureStartPoint = 0x08 + + def __init__(self, obj): + PathLog.track() + + obj.addProperty("App::PropertyBool", "Active", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Make False, to prevent operation from generating code")) + obj.addProperty("App::PropertyString", "Comment", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "An optional comment for this Contour")) + + if self.FeatureTool & self.opFeatures(obj): + obj.addProperty("App::PropertyLink", "ToolController", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool controller that will be used to calculate the path")) + + if self.FeatureDepths & self.opFeatures(obj): + obj.addProperty("App::PropertyDistance", "StepDown", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Incremental Step Down of Tool")) + obj.addProperty("App::PropertyDistance", "StartDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Starting Depth of Tool- first cut depth in Z")) + obj.addProperty("App::PropertyDistance", "FinalDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Final Depth of Tool- lowest value in Z")) + + if self.FeatureHeights & self.opFeatures(obj): + obj.addProperty("App::PropertyDistance", "ClearanceHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "The height needed to clear clamps and obstructions")) + obj.addProperty("App::PropertyDistance", "SafeHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid Safety Height between locations.")) + + if self.FeatureStartPoint & self.opFeatures(obj): + obj.addProperty("App::PropertyVector", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")) + obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "make True, if specifying a Start Point")) + + # Debugging + obj.addProperty("App::PropertyString", "AreaParams", "Path") + obj.setEditorMode('AreaParams', 2) # hide + obj.addProperty("App::PropertyString", "PathParams", "Path") + obj.setEditorMode('PathParams', 2) # hide + obj.addProperty("Part::PropertyPartShape", "removalshape", "Path") + obj.setEditorMode('removalshape', 2) # hide + + self.initOperation(obj) + obj.Proxy = self + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def opFeatures(self, obj): + return self.FeatureTool | self.FeatureDepths | self.FeatureHeights | self.FeatureStartPoint + + def onChanged(self, obj, prop): + if prop in ['AreaParams', 'PathParams', 'removalshape']: + obj.setEditorMode(prop, 2) + self.opOnChanged(obj, prop) + + def setDefaultValues(self, obj): + PathUtils.addToJob(obj) + + obj.Active = True + + if self.FeatureTool & self.opFeatures(obj): + obj.ToolController = PathUtils.findToolController(obj) + + if self.FeatureDepths & self.opFeatures(obj): + try: + shape = self.opShapeForDepths(obj) + except: + shape = None + + if shape: + bb = shape.BoundBox + obj.StartDepth = bb.ZMax + obj.FinalDepth = bb.ZMin + obj.StepDown = 1.0 + else: + obj.StartDepth = 1.0 + obj.FinalDepth = 0.0 + obj.StepDown = 1.0 + + if self.FeatureHeights & self.opFeatures(obj): + try: + shape = self.opShapeForDepths(obj) + except: + shape = None + + if shape: + bb = shape.BoundBox + obj.ClearanceHeight = bb.ZMax + 5.0 + obj.SafeHeight = bb.ZMax + 3.0 + else: + obj.ClearanceHeight = 10.0 + obj.SafeHeight = 8.0 + + if self.FeatureStartPoint & self.opFeatures(obj): + obj.UseStartPoint = False + + self.opSetDefaultValues(obj) + + @waiting_effects + def _buildPathArea(self, obj, baseobject, start=None, getsim=False): + PathLog.track() + area = Path.Area() + area.setPlane(makeWorkplane(baseobject)) + area.add(baseobject) + + areaParams = {'Fill': 0, 'Coplanar': 2} + + areaParams = self.opAreaParams(obj) + + heights = [i for i in self.depthparams] + PathLog.debug('depths: {}'.format(heights)) + area.setParams(**areaParams) + obj.AreaParams = str(area.getParams()) + + PathLog.debug("Area with params: {}".format(area.getParams())) + + sections = area.makeSections(mode=0, project=True, heights=heights) + shapelist = [sec.getShape() for sec in sections] + + pathParams = self.opPathParams(obj) + pathParams['shapes'] = shapelist + pathParams['feedrate'] = self.horizFeed + pathParams['feedrate_v'] = self.vertFeed + pathParams['verbose'] = True + pathParams['resume_height'] = obj.StepDown.Value + pathParams['retraction'] = obj.ClearanceHeight.Value + pathParams['return_end'] = True + + if self.endVector is not None: + pathParams['start'] = self.endVector + elif obj.UseStartPoint: + pathParams['start'] = obj.StartPoint + + obj.PathParams = str({key: value for key, value in pathParams.items() if key != 'shapes'}) + + (pp, end_vector) = Path.fromShapes(**pathParams) + PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) + self.endVector = end_vector + + simobj = None + if getsim: + areaParams['Thicken'] = True + areaParams['ToolRadius'] = self.radius - self.radius * .005 + area.setParams(**areaParams) + sec = area.makeSections(mode=0, project=False, heights=heights)[-1].getShape() + simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax)) + + return pp, simobj + + def execute(self, obj, getsim=False): + PathLog.track() + self.endVector = None + + if not obj.Active: + path = Path.Path("(inactive operation)") + obj.Path = path + if obj.ViewObject: + obj.ViewObject.Visibility = False + return + + self.depthparams = depth_params( + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=obj.StartDepth.Value, + step_down=obj.StepDown.Value, + z_finish_step=0.0, + final_depth=obj.FinalDepth.Value, + user_depths=None) + + toolLoad = obj.ToolController + if toolLoad is None or toolLoad.ToolNumber == 0: + + FreeCAD.Console.PrintError("No Tool Controller is selected. We need a tool to build a Path.") + return + else: + self.vertFeed = toolLoad.VertFeed.Value + self.horizFeed = toolLoad.HorizFeed.Value + self.vertRapid = toolLoad.VertRapid.Value + self.horizRapid = toolLoad.HorizRapid.Value + tool = toolLoad.Proxy.getTool(toolLoad) + if not tool or tool.Diameter == 0: + FreeCAD.Console.PrintError("No Tool found or diameter is zero. We need a tool to build a Path.") + return + else: + self.radius = tool.Diameter/2 + + commandlist = [] + commandlist.append(Path.Command("(" + obj.Label + ")")) + + shape = self.opShape(obj, commandlist) + + if self.FeatureStartPoint and obj.UseStartPoint: + start = obj.StartPoint + else: + start = FreeCAD.Vector() + + try: + (pp, sim) = self._buildPathArea(obj, shape, start, getsim) + commandlist.extend(pp.Commands) + except Exception as e: + FreeCAD.Console.PrintError(e) + FreeCAD.Console.PrintError("Something unexpected happened. Check project and tool config.") + sim = None + + + # Let's finish by rapid to clearance...just for safety + commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) + + PathLog.track() + path = Path.Path(commandlist) + obj.Path = path + return sim + diff --git a/src/Mod/Path/PathScripts/PathAreaUtils.py b/src/Mod/Path/PathScripts/PathAreaUtils.py deleted file mode 100644 index e78c6188a6..0000000000 --- a/src/Mod/Path/PathScripts/PathAreaUtils.py +++ /dev/null @@ -1,431 +0,0 @@ -import area -from nc.nc import * -import PathScripts.nc.iso -import math -import PathKurveUtils - -# some globals, to save passing variables as parameters too much -area_for_feed_possible = None -tool_radius_for_pocket = None - -def cut_curve(curve, need_rapid, p, rapid_safety_space, current_start_depth, final_depth): - prev_p = p - first = True - #comment("cut_curve:14 rss:" + str(rapid_safety_space) + " current start depth :" + str(current_start_depth) + " final depth :" + str(final_depth) + " need rapid: " + str(need_rapid)) - for vertex in curve.getVertices(): - if need_rapid and first: - # rapid across - rapid(vertex.p.x, vertex.p.y) - ##rapid down - rapid(z = current_start_depth + rapid_safety_space) - #feed down - feed(z = final_depth) - first = False - else: - if vertex.type == 1: - arc_ccw(vertex.p.x, vertex.p.y, i = vertex.c.x, j = vertex.c.y) - elif vertex.type == -1: - arc_cw(vertex.p.x, vertex.p.y, i = vertex.c.x, j = vertex.c.y) - else: - feed(vertex.p.x, vertex.p.y) - prev_p = vertex.p - return prev_p - -def area_distance(a, old_area): - best_dist = None - - for curve in a.getCurves(): - for vertex in curve.getVertices(): - c = old_area.NearestPoint(vertex.p) - d = c.dist(vertex.p) - if best_dist == None or d < best_dist: - best_dist = d - - for curve in old_area.getCurves(): - for vertex in curve.getVertices(): - c = a.NearestPoint(vertex.p) - d = c.dist(vertex.p) - if best_dist == None or d < best_dist: - best_dist = d - - return best_dist - -def make_obround(p0, p1, radius): - dir = p1 - p0 - d = dir.length() - dir.normalize() - right = area.Point(dir.y, -dir.x) - obround = area.Area() - c = area.Curve() - vt0 = p0 + right * radius - vt1 = p1 + right * radius - vt2 = p1 - right * radius - vt3 = p0 - right * radius - c.append(area.Vertex(0, vt0, area.Point(0, 0))) - c.append(area.Vertex(0, vt1, area.Point(0, 0))) - c.append(area.Vertex(1, vt2, p1)) - c.append(area.Vertex(0, vt3, area.Point(0, 0))) - c.append(area.Vertex(1, vt0, p0)) - obround.append(c) - return obround - -def feed_possible(p0, p1): - if p0 == p1: - return True - obround = make_obround(p0, p1, tool_radius_for_pocket) - a = area.Area(area_for_feed_possible) - obround.Subtract(a) - if obround.num_curves() > 0: - return False - return True - -def cut_curvelist1(curve_list, rapid_safety_space, current_start_depth, depth, clearance_height, keep_tool_down_if_poss): - p = area.Point(0, 0) - first = True - for curve in curve_list: - need_rapid = True - if first == False: - s = curve.FirstVertex().p - if keep_tool_down_if_poss == True: - # see if we can feed across - if feed_possible(p, s): - need_rapid = False - elif s.x == p.x and s.y == p.y: - need_rapid = False - if need_rapid: - rapid(z = clearance_height) - p = cut_curve(curve, need_rapid, p, rapid_safety_space, current_start_depth, depth) - first = False - - rapid(z = clearance_height) - -def cut_curvelist2(curve_list, rapid_safety_space, current_start_depth, depth, clearance_height, keep_tool_down_if_poss,start_point): - p = area.Point(0, 0) - start_x,start_y=start_point - first = True - for curve in curve_list: - need_rapid = True - if first == True: - direction = "On";radius = 0.0;offset_extra = 0.0; roll_radius = 0.0;roll_on = 0.0; roll_off = 0.0; rapid_safety_space; step_down = math.fabs(depth);extend_at_start = 0.0;extend_at_end = 0.0 - PathKurveUtils.make_smaller( curve, start = area.Point(start_x,start_y)) - PathKurveUtils.profile(curve, direction, radius , offset_extra, roll_radius, roll_on, roll_off, rapid_safety_space , clearance_height, current_start_depth, step_down , depth, extend_at_start, extend_at_end) - else: - s = curve.FirstVertex().p - if keep_tool_down_if_poss == True: - - # see if we can feed across - if feed_possible(p, s): - need_rapid = False - elif s.x == p.x and s.y == p.y: - need_rapid = False - - cut_curve(curve, need_rapid, p, rapid_safety_space, current_start_depth, depth) - first = False #change to True if you want to rapid back to start side before zigging again with unidirectional set - rapid(z = clearance_height) - -def recur(arealist, a1, stepover, from_center): - # this makes arealist by recursively offsetting a1 inwards - - if a1.num_curves() == 0: - return - - if from_center: - arealist.insert(0, a1) - else: - arealist.append(a1) - - a_offset = area.Area(a1) - a_offset.Offset(stepover) - - # split curves into new areas - if area.holes_linked(): - for curve in a_offset.getCurves(): - a2 = area.Area() - a2.append(curve) - recur(arealist, a2, stepover, from_center) - - else: - # split curves into new areas - a_offset.Reorder() - a2 = None - - for curve in a_offset.getCurves(): - if curve.IsClockwise(): - if a2 != None: - a2.append(curve) - else: - if a2 != None: - recur(arealist, a2, stepover, from_center) - a2 = area.Area() - a2.append(curve) - - if a2 != None: - recur(arealist, a2, stepover, from_center) - -def get_curve_list(arealist, reverse_curves = False): - curve_list = list() - for a in arealist: - for curve in a.getCurves(): - if reverse_curves == True: - curve.Reverse() - curve_list.append(curve) - return curve_list - -curve_list_for_zigs = [] -rightward_for_zigs = True -sin_angle_for_zigs = 0.0 -cos_angle_for_zigs = 1.0 -sin_minus_angle_for_zigs = 0.0 -cos_minus_angle_for_zigs = 1.0 -one_over_units = 1.0 - -def make_zig_curve(curve, y0, y, zig_unidirectional): - if rightward_for_zigs: - curve.Reverse() - - # find a high point to start looking from - high_point = None - for vertex in curve.getVertices(): - if high_point == None: - high_point = vertex.p - elif vertex.p.y > high_point.y: - # use this as the new high point - high_point = vertex.p - elif math.fabs(vertex.p.y - high_point.y) < 0.002 * one_over_units: - # equal high point - if rightward_for_zigs: - # use the furthest left point - if vertex.p.x < high_point.x: - high_point = vertex.p - else: - # use the furthest right point - if vertex.p.x > high_point.x: - high_point = vertex.p - - zig = area.Curve() - - high_point_found = False - zig_started = False - zag_found = False - - for i in range(0, 2): # process the curve twice because we don't know where it will start - prev_p = None - for vertex in curve.getVertices(): - if zag_found: break - if prev_p != None: - if zig_started: - zig.append(unrotated_vertex(vertex)) - if math.fabs(vertex.p.y - y) < 0.002 * one_over_units: - zag_found = True - break - elif high_point_found: - if math.fabs(vertex.p.y - y0) < 0.002 * one_over_units: - if zig_started: - zig.append(unrotated_vertex(vertex)) - elif math.fabs(prev_p.y - y0) < 0.002 * one_over_units and vertex.type == 0: - zig.append(area.Vertex(0, unrotated_point(prev_p), area.Point(0, 0))) - zig.append(unrotated_vertex(vertex)) - zig_started = True - elif vertex.p.x == high_point.x and vertex.p.y == high_point.y: - high_point_found = True - prev_p = vertex.p - - if zig_started: - - if zig_unidirectional == True: - # remove the last bit of zig - if math.fabs(zig.LastVertex().p.y - y) < 0.002 * one_over_units: - vertices = zig.getVertices() - while len(vertices) > 0: - v = vertices[len(vertices)-1] - if math.fabs(v.p.y - y0) < 0.002 * one_over_units: - break - else: - vertices.pop() - zig = area.Curve() - for v in vertices: - zig.append(v) - - curve_list_for_zigs.append(zig) - -def make_zig(a, y0, y, zig_unidirectional): - for curve in a.getCurves(): - make_zig_curve(curve, y0, y, zig_unidirectional) - -reorder_zig_list_list = [] - -def add_reorder_zig(curve): - global reorder_zig_list_list - - # look in existing lists - s = curve.FirstVertex().p - for curve_list in reorder_zig_list_list: - last_curve = curve_list[len(curve_list) - 1] - e = last_curve.LastVertex().p - if math.fabs(s.x - e.x) < 0.002 * one_over_units and math.fabs(s.y - e.y) < 0.002 * one_over_units: - curve_list.append(curve) - return - - # else add a new list - curve_list = [] - curve_list.append(curve) - reorder_zig_list_list.append(curve_list) - -def reorder_zigs(): - global curve_list_for_zigs - global reorder_zig_list_list - reorder_zig_list_list = [] - for curve in curve_list_for_zigs: - add_reorder_zig(curve) - - curve_list_for_zigs = [] - for curve_list in reorder_zig_list_list: - for curve in curve_list: - curve_list_for_zigs.append(curve) - -def rotated_point(p): - return area.Point(p.x * cos_angle_for_zigs - p.y * sin_angle_for_zigs, p.x * sin_angle_for_zigs + p.y * cos_angle_for_zigs) - -def unrotated_point(p): - return area.Point(p.x * cos_minus_angle_for_zigs - p.y * sin_minus_angle_for_zigs, p.x * sin_minus_angle_for_zigs + p.y * cos_minus_angle_for_zigs) - -def rotated_vertex(v): - if v.type: - return area.Vertex(v.type, rotated_point(v.p), rotated_point(v.c)) - return area.Vertex(v.type, rotated_point(v.p), area.Point(0, 0)) - -def unrotated_vertex(v): - if v.type: - return area.Vertex(v.type, unrotated_point(v.p), unrotated_point(v.c)) - return area.Vertex(v.type, unrotated_point(v.p), area.Point(0, 0)) - -def rotated_area(a): - an = area.Area() - for curve in a.getCurves(): - curve_new = area.Curve() - for v in curve.getVertices(): - curve_new.append(rotated_vertex(v)) - an.append(curve_new) - return an - -def zigzag(a, stepover, zig_unidirectional): - if a.num_curves() == 0: - return - - global rightward_for_zigs - global curve_list_for_zigs - global sin_angle_for_zigs - global cos_angle_for_zigs - global sin_minus_angle_for_zigs - global cos_minus_angle_for_zigs - global one_over_units - - one_over_units = 1 / area.get_units() - - a = rotated_area(a) - - b = area.Box() - a.GetBox(b) - - x0 = b.MinX() - 1.0 - x1 = b.MaxX() + 1.0 - - height = b.MaxY() - b.MinY() - num_steps = int(height / stepover + 1) - y = b.MinY() + 0.1 * one_over_units - null_point = area.Point(0, 0) - rightward_for_zigs = True - curve_list_for_zigs = [] - - for i in range(0, num_steps): - y0 = y - y = y + stepover - p0 = area.Point(x0, y0) - p1 = area.Point(x0, y) - p2 = area.Point(x1, y) - p3 = area.Point(x1, y0) - c = area.Curve() - c.append(area.Vertex(0, p0, null_point, 0)) - c.append(area.Vertex(0, p1, null_point, 0)) - c.append(area.Vertex(0, p2, null_point, 1)) - c.append(area.Vertex(0, p3, null_point, 0)) - c.append(area.Vertex(0, p0, null_point, 1)) - a2 = area.Area() - a2.append(c) - a2.Intersect(a) - make_zig(a2, y0, y, zig_unidirectional) - if zig_unidirectional == False: - rightward_for_zigs = (rightward_for_zigs == False) - - reorder_zigs() - -def pocket(a,tool_radius, extra_offset, stepover, depthparams, from_center, keep_tool_down_if_poss, use_zig_zag, zig_angle, zig_unidirectional = False,start_point=None, cut_mode = 'conventional'): - global tool_radius_for_pocket - global area_for_feed_possible - #if len(a.getCurves()) > 1: - # for crv in a.getCurves(): - # ar = area.Area() - # ar.append(crv) - # pocket(ar, tool_radius, extra_offset, rapid_safety_space, start_depth, final_depth, stepover, stepdown, clearance_height, from_center, keep_tool_down_if_poss, use_zig_zag, zig_angle, zig_unidirectional) - # return - - tool_radius_for_pocket = tool_radius - - if keep_tool_down_if_poss: - area_for_feed_possible = area.Area(a) - area_for_feed_possible.Offset(extra_offset - 0.01) - - use_internal_function = False #(area.holes_linked() == False) # use internal function, if area module is the Clipper library - - if use_internal_function: - curve_list = a.MakePocketToolpath(tool_radius, extra_offset, stepover, from_center, use_zig_zag, zig_angle) - - else: - global sin_angle_for_zigs - global cos_angle_for_zigs - global sin_minus_angle_for_zigs - global cos_minus_angle_for_zigs - radians_angle = zig_angle * math.pi / 180 - sin_angle_for_zigs = math.sin(-radians_angle) - cos_angle_for_zigs = math.cos(-radians_angle) - sin_minus_angle_for_zigs = math.sin(radians_angle) - cos_minus_angle_for_zigs = math.cos(radians_angle) - - arealist = list() - - a_offset = area.Area(a) - current_offset = tool_radius + extra_offset - a_offset.Offset(current_offset) - - do_recursive = True - - if use_zig_zag: - zigzag(a_offset, stepover, zig_unidirectional) - curve_list = curve_list_for_zigs - else: - if do_recursive: - recur(arealist, a_offset, stepover, from_center) - else: - while(a_offset.num_curves() > 0): - if from_center: - arealist.insert(0, a_offset) - else: - arealist.append(a_offset) - current_offset = current_offset + stepover - a_offset = area.Area(a) - a_offset.Offset(current_offset) - curve_list = get_curve_list(arealist, cut_mode == 'climb') - - depths = depthparams.get_depths() - current_start_depth = depthparams.start_depth - if start_point==None: - for depth in depths: - cut_curvelist1(curve_list, depthparams.rapid_safety_space, current_start_depth, depth, depthparams.clearance_height, keep_tool_down_if_poss) - current_start_depth = depth - - else: - for depth in depths: - cut_curvelist2(curve_list, depthparams.rapid_safety_space, current_start_depth, depth, depthparams.clearance_height, keep_tool_down_if_poss, start_point) - current_start_depth = depth - - diff --git a/src/Mod/Path/PathScripts/PathContour.py b/src/Mod/Path/PathScripts/PathContour.py index c315b6202f..082ce20e8c 100644 --- a/src/Mod/Path/PathScripts/PathContour.py +++ b/src/Mod/Path/PathScripts/PathContour.py @@ -23,16 +23,19 @@ # *************************************************************************** from __future__ import print_function -import FreeCAD -import Path -import PathScripts.PathLog as PathLog -from PySide import QtCore, QtGui -from PathScripts import PathUtils + import ArchPanel +import FreeCAD import Part -from PathScripts.PathUtils import waiting_effects -from PathScripts.PathUtils import makeWorkplane +import Path +import PathScripts.PathAreaOp as PathAreaOp +import PathScripts.PathLog as PathLog + +from PathScripts import PathUtils from PathScripts.PathUtils import depth_params +from PathScripts.PathUtils import makeWorkplane +from PathScripts.PathUtils import waiting_effects +from PySide import QtCore, QtGui FreeCAD.setLogLevel('Path.Area', 0) @@ -57,26 +60,10 @@ __url__ = "http://www.freecadweb.org" """Path Contour object and FreeCAD command""" -class ObjectContour: +class ObjectContour(PathAreaOp.ObjectOp): - def __init__(self, obj): + def initOperation(self, obj): PathLog.track() - obj.addProperty("App::PropertyBool", "Active", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "Make False, to prevent operation from generating code")) - obj.addProperty("App::PropertyString", "Comment", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "An optional comment for this Contour")) - - # Tool Properties - obj.addProperty("App::PropertyLink", "ToolController", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool controller that will be used to calculate the path")) - - # Depth Properties - obj.addProperty("App::PropertyDistance", "ClearanceHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "The height needed to clear clamps and obstructions")) - obj.addProperty("App::PropertyDistance", "SafeHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid Safety Height between locations.")) - obj.addProperty("App::PropertyDistance", "StepDown", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Incremental Step Down of Tool")) - obj.addProperty("App::PropertyDistance", "StartDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Starting Depth of Tool- first cut depth in Z")) - obj.addProperty("App::PropertyDistance", "FinalDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("App::Property", "Final Depth of Tool- lowest value in Z")) - - # Start Point Properties - obj.addProperty("App::PropertyVector", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "The start point of this path")) - obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("App::Property", "make True, if specifying a Start Point")) # Contour Properties obj.addProperty("App::PropertyEnumeration", "Direction", "Contour", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part ClockWise CW or CounterClockWise CCW")) @@ -90,172 +77,44 @@ class ObjectContour: obj.addProperty("App::PropertyFloat", "MiterLimit", "Contour", QtCore.QT_TRANSLATE_NOOP("App::Property", "Maximum distance before a miter join is truncated")) obj.setEditorMode('MiterLimit', 2) - # Debug Parameters - obj.addProperty("App::PropertyString", "AreaParams", "Path") - obj.setEditorMode('AreaParams', 2) # hide - obj.addProperty("App::PropertyString", "PathParams", "Path") - obj.setEditorMode('PathParams', 2) # hide - obj.addProperty("Part::PropertyPartShape", "removalshape", "Path") - obj.setEditorMode('removalshape', 2) # hide - if FreeCAD.GuiUp: _ViewProviderContour(obj.ViewObject) - obj.Proxy = self self.endVector = None - def onChanged(self, obj, prop): + def opOnChanged(self, obj, prop): PathLog.track('prop: {} state: {}'.format(prop, obj.State)) - if prop in ['AreaParams', 'PathParams', 'removalshape']: - obj.setEditorMode(prop, 2) - obj.setEditorMode('MiterLimit', 2) if obj.JoinType == 'Miter': obj.setEditorMode('MiterLimit', 0) - def __getstate__(self): - PathLog.track() + def opShapeForDepths(self, obj): + job = PathUtils.findParentJob(obj) + if job and job.Base: + PathLog.info("job=%s base=%s shape=%s" % (job, job.Base, job.Base.Shape)) + return job.Base.Shape + PathLog.warning("No job object found (%s), or job has no Base." % job) return None - def __setstate__(self, state): - PathLog.track(state) - return None + def opSetDefaultValues(self, obj): + obj.Direction = "CW" + obj.UseComp = True + obj.OffsetExtra = 0.0 + obj.JoinType = "Round" + obj.MiterLimit = 0.1 - def setDepths(self, obj): - PathLog.track() - parentJob = PathUtils.findParentJob(obj) - if parentJob is None: - return - baseobject = parentJob.Base - if baseobject is None: - return - - try: - bb = baseobject.Shape.BoundBox # parent boundbox - obj.StartDepth = bb.ZMax - obj.ClearanceHeight = bb.ZMax + 5.0 - obj.SafeHeight = bb.ZMax + 3.0 - obj.FinalDepth = bb.ZMin - - except: - obj.StartDepth = 5.0 - obj.ClearanceHeight = 10.0 - obj.SafeHeight = 8.0 - - @waiting_effects - def _buildPathArea(self, obj, baseobject, start=None, getsim=False): - PathLog.track() - profile = Path.Area() - profile.setPlane(makeWorkplane(baseobject)) - profile.add(baseobject) - - profileparams = {'Fill': 0, - 'Coplanar': 2} - - if obj.UseComp is False: - profileparams['Offset'] = 0.0 - else: - profileparams['Offset'] = self.radius+obj.OffsetExtra.Value - - jointype = ['Round', 'Square', 'Miter'] - profileparams['JoinType'] = jointype.index(obj.JoinType) - - if obj.JoinType == 'Miter': - profileparams['MiterLimit'] = obj.MiterLimit - - heights = [i for i in self.depthparams] - PathLog.debug('depths: {}'.format(heights)) - profile.setParams(**profileparams) - obj.AreaParams = str(profile.getParams()) - - PathLog.debug("Contour with params: {}".format(profile.getParams())) - sections = profile.makeSections(mode=0, project=True, heights=heights) - shapelist = [sec.getShape() for sec in sections] - - params = {'shapes': shapelist, - 'feedrate': self.horizFeed, - 'feedrate_v': self.vertFeed, - 'verbose': True, - 'resume_height': obj.StepDown.Value, - 'retraction': obj.ClearanceHeight.Value, - 'return_end': True} - - if obj.Direction == 'CCW': - params['orientation'] = 0 - else: - params['orientation'] = 1 - - if self.endVector is not None: - params['start'] = self.endVector - elif obj.UseStartPoint: - params['start'] = obj.StartPoint - - obj.PathParams = str({key: value for key, value in params.items() if key != 'shapes'}) - - (pp, end_vector) = Path.fromShapes(**params) - PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) - self.endVector = end_vector - - simobj = None - if getsim: - profileparams['Thicken'] = True - profileparams['ToolRadius'] = self.radius - self.radius * .005 - profile.setParams(**profileparams) - sec = profile.makeSections(mode=0, project=False, heights=heights)[-1].getShape() - simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax)) - - return pp, simobj - - def execute(self, obj, getsim=False): - PathLog.track() - self.endVector = None - - if not obj.Active: - path = Path.Path("(inactive operation)") - obj.Path = path - obj.ViewObject.Visibility = False - return - - commandlist = [] - toolLoad = obj.ToolController - - self.depthparams = depth_params( - clearance_height=obj.ClearanceHeight.Value, - safe_height=obj.SafeHeight.Value, - start_depth=obj.StartDepth.Value, - step_down=obj.StepDown.Value, - z_finish_step=0.0, - final_depth=obj.FinalDepth.Value, - user_depths=None) - - if toolLoad is None or toolLoad.ToolNumber == 0: - - FreeCAD.Console.PrintError("No Tool Controller is selected. We need a tool to build a Path.") - return - else: - self.vertFeed = toolLoad.VertFeed.Value - self.horizFeed = toolLoad.HorizFeed.Value - self.vertRapid = toolLoad.VertRapid.Value - self.horizRapid = toolLoad.HorizRapid.Value - tool = toolLoad.Proxy.getTool(toolLoad) - if not tool or tool.Diameter == 0: - FreeCAD.Console.PrintError("No Tool found or diameter is zero. We need a tool to build a Path.") - return - else: - self.radius = tool.Diameter/2 - - commandlist.append(Path.Command("(" + obj.Label + ")")) + def opShape(self, obj, commandlist): if obj.UseComp: commandlist.append(Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")")) else: commandlist.append(Path.Command("(Uncompensated Tool Path)")) - parentJob = PathUtils.findParentJob(obj) + job = PathUtils.findParentJob(obj) - if parentJob is None: + if job is None: return - baseobject = parentJob.Base + baseobject = job.Base if baseobject is None: return @@ -268,31 +127,33 @@ class ObjectContour: for shape in shapes: f = Part.makeFace([shape], 'Part::FaceMakerSimple') thickness = baseobject.Group[0].Source.Thickness - contourshape = f.extrude(FreeCAD.Vector(0, 0, thickness)) - try: - (pp, sim) = self._buildPathArea(obj, contourshape, start=obj.StartPoint, getsim=getsim) - commandlist.extend(pp.Commands) - except Exception as e: - FreeCAD.Console.PrintError(e) - FreeCAD.Console.PrintError("Something unexpected happened. Unable to generate a contour path. Check project and tool config.") + return f.extrude(FreeCAD.Vector(0, 0, thickness)) if hasattr(baseobject, "Shape") and not isPanel: - env = PathUtils.getEnvelope(partshape=baseobject.Shape, subshape=None, depthparams=self.depthparams) - try: - (pp, sim) = self._buildPathArea(obj, env, start=obj.StartPoint, getsim=getsim) - commandlist.extend(pp.Commands) - except Exception as e: - FreeCAD.Console.PrintError(e) - FreeCAD.Console.PrintError("Something unexpected happened. Unable to generate a contour path. Check project and tool config.") + return PathUtils.getEnvelope(partshape=baseobject.Shape, subshape=None, depthparams=self.depthparams) - # Let's finish by rapid to clearance...just for safety - commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) + def opAreaParams(self, obj): + params = {'Fill': 0, 'Coplanar': 2} - PathLog.track() - path = Path.Path(commandlist) - obj.Path = path - return sim + if obj.UseComp is False: + params['Offset'] = 0.0 + else: + params['Offset'] = self.radius+obj.OffsetExtra.Value + jointype = ['Round', 'Square', 'Miter'] + params['JoinType'] = jointype.index(obj.JoinType) + + if obj.JoinType == 'Miter': + params['MiterLimit'] = obj.MiterLimit + return params + + def opPathParams(self, obj): + params = {} + if obj.Direction == 'CCW': + params['orientation'] = 0 + else: + params['orientation'] = 1 + return params class _ViewProviderContour: @@ -349,6 +210,16 @@ class _CommandSetStartPoint: def Activated(self): FreeCADGui.Snapper.getPoint(callback=self.setpoint) +def Create(name): + FreeCAD.ActiveDocument.openTransaction(translate("Path", "Create a Contour")) + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + proxy = ObjectContour(obj) + proxy.setDefaultValues(obj) + + obj.ViewObject.Proxy.deleteOnReject = True + + FreeCAD.ActiveDocument.commitTransaction() + obj.ViewObject.startEditing() class CommandPathContour: def GetResources(self): @@ -365,36 +236,7 @@ class CommandPathContour: return False def Activated(self): - ztop = 10.0 - zbottom = 0.0 - - FreeCAD.ActiveDocument.openTransaction(translate("Path", "Create a Contour")) - FreeCADGui.addModule("PathScripts.PathContour") - FreeCADGui.doCommand('obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", "Contour")') - FreeCADGui.doCommand('PathScripts.PathContour.ObjectContour(obj)') - FreeCADGui.doCommand('obj.ViewObject.Proxy.deleteOnReject = True') - - FreeCADGui.doCommand('obj.Active = True') - - FreeCADGui.doCommand('obj.ClearanceHeight = ' + str(ztop + 10.0)) - FreeCADGui.doCommand('obj.StepDown = 1.0') - FreeCADGui.doCommand('obj.StartDepth= ' + str(ztop)) - FreeCADGui.doCommand('obj.FinalDepth=' + str(zbottom)) - - FreeCADGui.doCommand('obj.SafeHeight = ' + str(ztop + 2.0)) - FreeCADGui.doCommand('obj.OffsetExtra = 0.0') - FreeCADGui.doCommand('obj.Direction = "CW"') - FreeCADGui.doCommand('obj.UseComp = True') - FreeCADGui.doCommand('obj.JoinType = "Round"') - FreeCADGui.doCommand('obj.MiterLimit =' + str(0.1)) - - FreeCADGui.doCommand('PathScripts.PathUtils.addToJob(obj)') - FreeCADGui.doCommand('PathScripts.PathContour.ObjectContour.setDepths(obj.Proxy, obj)') - FreeCADGui.doCommand('obj.ToolController = PathScripts.PathUtils.findToolController(obj)') - - FreeCAD.ActiveDocument.commitTransaction() - FreeCADGui.doCommand('obj.ViewObject.startEditing()') - + return Create("Contour") class TaskPanel: def __init__(self, obj, deleteOnReject):