Split Contour op into generic PathArea part and contour specifics.

This commit is contained in:
Markus Lampert
2017-08-01 13:24:30 -07:00
committed by Yorik van Havre
parent bb3e69fdd9
commit 42f0d71d4a
4 changed files with 320 additions and 651 deletions

View File

@@ -18,6 +18,7 @@ INSTALL(
SET(PathScripts_SRCS
PathCommands.py
PathScripts/PathAreaOp.py
PathScripts/PathArray.py
PathScripts/PathComment.py
PathScripts/PathCompoundExtended.py

View File

@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2017 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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

View File

@@ -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

View File

@@ -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):