fd6298d542
Path: Refactor `Extensions` Gui code into independent module. Move the `Extensions` Gui code to independent module so access to other operations will be easier. Path: Add `Extensions` feature to Adaptive operation Path: Isolate Adaptive GUI elements in preparation of Adaptive unit tests Path: Implement `PathLog` debug module Path: Implement `translate()` for multi-language message support Path: Fix `StockType`check bug Path: Relocate `getCutRegionWires()` to `FeatureExtensions` module Path: Add `Extensions` property checks on document restored Path: Improve `Extend Outline` feature implementation Path: Initialize a waterline type extension Path: Add enable/disable extensions feature. It is quite possible that many complex faces exist that have large quantities of both simple and complex edges. For this reason, a manual push button to enable Extensions is useful so the users machine is not bogged down with extra or unnecessary computing time. Extensions are not necessary at all times. This commit also includes an edge count threshold that will disable the Extensions feature temporarily upon initial loading of the Task Panel. The manual enable button will do just that. Path: Add enable extensions warning label Path: Shorten enable/disable Extensions button message Path: Remove run-time added Task Panel elements - this QButton and QLabel were moved to UI panel directly. Path: Add include/ignore Edges button Path: Improve extension preview rendering Path: Fixes for `useOutline` modification and updates Path: Add internal feature to cache calculated extensions for reuse Path: Add `SetupProperties()` function and connect to GUI command Path: Add `Avoid Face` extension to ignore base face. This feature allows for some simple access to the exterior of a selected face without clearing the face itself. This will allow for an exterior clearing operation in a simple manner. Path: Fix bug restricting extensions on internal closed-wires
725 lines
30 KiB
Python
725 lines
30 KiB
Python
# -*- coding: utf-8 -*-
|
|
# ***************************************************************************
|
|
# * Copyright (c) 2018 Kresimir Tusek <kresimir.tusek@gmail.com> *
|
|
# * Copyright (c) 2019-2021 Schildkroet *
|
|
# * *
|
|
# * This file is part of the FreeCAD CAx development system. *
|
|
# * *
|
|
# * This library is free software; you can redistribute it and/or *
|
|
# * modify it under the terms of the GNU Library General Public *
|
|
# * License as published by the Free Software Foundation; either *
|
|
# * version 2 of the License, or (at your option) any later version. *
|
|
# * *
|
|
# * This library 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 library; see the file COPYING.LIB. If not, *
|
|
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
|
|
# * Suite 330, Boston, MA 02111-1307, USA *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
import PathScripts.PathOp as PathOp
|
|
import PathScripts.PathUtils as PathUtils
|
|
import PathScripts.PathLog as PathLog
|
|
import PathScripts.PathGeom as PathGeom
|
|
import Path
|
|
import FreeCAD
|
|
import time
|
|
import json
|
|
import math
|
|
import area
|
|
|
|
from PySide import QtCore
|
|
|
|
# lazily loaded modules
|
|
from lazy_loader.lazy_loader import LazyLoader
|
|
Part = LazyLoader('Part', globals(), 'Part')
|
|
# TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw')
|
|
FeatureExtensions = LazyLoader('PathScripts.PathFeatureExtensions',
|
|
globals(),
|
|
'PathScripts.PathFeatureExtensions')
|
|
|
|
if FreeCAD.GuiUp:
|
|
from pivy import coin
|
|
import FreeCADGui
|
|
|
|
__doc__ = "Class and implementation of the Adaptive path operation."
|
|
|
|
|
|
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
|
# PathLog.trackModule(PathLog.thisModule())
|
|
|
|
|
|
# Qt translation handling
|
|
def translate(context, text, disambig=None):
|
|
return QtCore.QCoreApplication.translate(context, text, disambig)
|
|
|
|
|
|
def convertTo2d(pathArray):
|
|
output = []
|
|
for path in pathArray:
|
|
pth2 = []
|
|
for edge in path:
|
|
for pt in edge:
|
|
pth2.append([pt[0], pt[1]])
|
|
output.append(pth2)
|
|
return output
|
|
|
|
|
|
sceneGraph = None
|
|
scenePathNodes = [] # for scene cleanup aftewards
|
|
topZ = 10
|
|
|
|
|
|
def sceneDrawPath(path, color=(0, 0, 1)):
|
|
coPoint = coin.SoCoordinate3()
|
|
|
|
pts = []
|
|
for pt in path:
|
|
pts.append([pt[0], pt[1], topZ])
|
|
|
|
coPoint.point.setValues(0, len(pts), pts)
|
|
ma = coin.SoBaseColor()
|
|
ma.rgb = color
|
|
li = coin.SoLineSet()
|
|
li.numVertices.setValue(len(pts))
|
|
pathNode = coin.SoSeparator()
|
|
pathNode.addChild(coPoint)
|
|
pathNode.addChild(ma)
|
|
pathNode.addChild(li)
|
|
sceneGraph.addChild(pathNode)
|
|
scenePathNodes.append(pathNode) # for scene cleanup afterwards
|
|
|
|
|
|
def sceneClean():
|
|
for n in scenePathNodes:
|
|
sceneGraph.removeChild(n)
|
|
|
|
del scenePathNodes[:]
|
|
|
|
|
|
def discretize(edge, flipDirection=False):
|
|
pts = edge.discretize(Deflection=0.0001)
|
|
if flipDirection:
|
|
pts.reverse()
|
|
|
|
return pts
|
|
|
|
|
|
def CalcHelixConePoint(height, cur_z, radius, angle):
|
|
x = ((height - cur_z) / height) * radius * math.cos(math.radians(angle)*cur_z)
|
|
y = ((height - cur_z) / height) * radius * math.sin(math.radians(angle)*cur_z)
|
|
z = cur_z
|
|
|
|
return {'X': x, 'Y': y, 'Z': z}
|
|
|
|
|
|
def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
|
# pylint: disable=unused-argument
|
|
if len(adaptiveResults) == 0 or len(adaptiveResults[0]["AdaptivePaths"]) == 0:
|
|
return
|
|
|
|
# minLiftDistance = op.tool.Diameter
|
|
helixRadius = 0
|
|
for region in adaptiveResults:
|
|
p1 = region["HelixCenterPoint"]
|
|
p2 = region["StartPoint"]
|
|
r = math.sqrt((p1[0]-p2[0]) * (p1[0]-p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1]))
|
|
if r > helixRadius:
|
|
helixRadius = r
|
|
|
|
stepDown = obj.StepDown.Value
|
|
passStartDepth = obj.StartDepth.Value
|
|
|
|
if stepDown < 0.1:
|
|
stepDown = 0.1
|
|
|
|
length = 2*math.pi * helixRadius
|
|
|
|
if float(obj.HelixAngle) < 1:
|
|
obj.HelixAngle = 1
|
|
if float(obj.HelixAngle) > 89:
|
|
obj.HelixAngle = 89
|
|
|
|
if float(obj.HelixConeAngle) < 0:
|
|
obj.HelixConeAngle = 0
|
|
|
|
helixAngleRad = math.pi * float(obj.HelixAngle) / 180.0
|
|
depthPerOneCircle = length * math.tan(helixAngleRad)
|
|
# print("Helix circle depth: {}".format(depthPerOneCircle))
|
|
|
|
stepUp = obj.LiftDistance.Value
|
|
if stepUp < 0:
|
|
stepUp = 0
|
|
|
|
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
|
if finish_step > stepDown:
|
|
finish_step = stepDown
|
|
|
|
depth_params = PathUtils.depth_params(
|
|
clearance_height=obj.ClearanceHeight.Value,
|
|
safe_height=obj.SafeHeight.Value,
|
|
start_depth=obj.StartDepth.Value,
|
|
step_down=stepDown,
|
|
z_finish_step=finish_step,
|
|
final_depth=obj.FinalDepth.Value,
|
|
user_depths=None)
|
|
|
|
# ml: this is dangerous because it'll hide all unused variables hence forward
|
|
# however, I don't know what lx and ly signify so I'll leave them for now
|
|
# pylint: disable=unused-variable
|
|
# lx = adaptiveResults[0]["HelixCenterPoint"][0]
|
|
# ly = adaptiveResults[0]["HelixCenterPoint"][1]
|
|
lz = passStartDepth
|
|
step = 0
|
|
|
|
for passEndDepth in depth_params.data:
|
|
step = step + 1
|
|
|
|
for region in adaptiveResults:
|
|
startAngle = math.atan2(region["StartPoint"][1] - region["HelixCenterPoint"][1], region["StartPoint"][0] - region["HelixCenterPoint"][0])
|
|
|
|
# lx = region["HelixCenterPoint"][0]
|
|
# ly = region["HelixCenterPoint"][1]
|
|
|
|
passDepth = (passStartDepth - passEndDepth)
|
|
|
|
p1 = region["HelixCenterPoint"]
|
|
p2 = region["StartPoint"]
|
|
helixRadius = math.sqrt((p1[0]-p2[0]) * (p1[0]-p2[0]) + (p1[1]-p2[1]) * (p1[1]-p2[1]))
|
|
|
|
# Helix ramp
|
|
if helixRadius > 0.01:
|
|
r = helixRadius - 0.01
|
|
|
|
maxfi = passDepth / depthPerOneCircle * 2 * math.pi
|
|
fi = 0
|
|
offsetFi = -maxfi + startAngle-math.pi/16
|
|
|
|
helixStart = [region["HelixCenterPoint"][0] + r * math.cos(offsetFi), region["HelixCenterPoint"][1] + r * math.sin(offsetFi)]
|
|
|
|
op.commandlist.append(Path.Command("(Helix to depth: %f)" % passEndDepth))
|
|
|
|
if obj.UseHelixArcs is False:
|
|
# rapid move to start point
|
|
op.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
|
|
op.commandlist.append(Path.Command("G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.ClearanceHeight.Value}))
|
|
|
|
# rapid move to safe height
|
|
op.commandlist.append(Path.Command("G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.SafeHeight.Value}))
|
|
|
|
# move to start depth
|
|
op.commandlist.append(Path.Command("G1", {"X": helixStart[0], "Y": helixStart[1], "Z": passStartDepth, "F": op.vertFeed}))
|
|
|
|
if obj.HelixConeAngle == 0:
|
|
while fi < maxfi:
|
|
x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi)
|
|
y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi)
|
|
z = passStartDepth - fi / maxfi * (passStartDepth - passEndDepth)
|
|
op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "Z": z, "F": op.vertFeed}))
|
|
# lx = x
|
|
# ly = y
|
|
fi = fi + math.pi / 16
|
|
|
|
# one more circle at target depth to make sure center is cleared
|
|
maxfi = maxfi + 2*math.pi
|
|
while fi < maxfi:
|
|
x = region["HelixCenterPoint"][0] + r * math.cos(fi+offsetFi)
|
|
y = region["HelixCenterPoint"][1] + r * math.sin(fi+offsetFi)
|
|
z = passEndDepth
|
|
op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "Z": z, "F": op.horizFeed}))
|
|
# lx = x
|
|
# ly = y
|
|
fi = fi + math.pi/16
|
|
|
|
else:
|
|
# Cone
|
|
_HelixAngle = 360 - (float(obj.HelixAngle) * 4)
|
|
|
|
if obj.HelixConeAngle > 6:
|
|
obj.HelixConeAngle = 6
|
|
|
|
helixRadius *= 0.9
|
|
|
|
# Calculate everything
|
|
helix_height = passStartDepth - passEndDepth
|
|
r_extra = helix_height * math.tan(math.radians(obj.HelixConeAngle))
|
|
HelixTopRadius = helixRadius + r_extra
|
|
helix_full_height = HelixTopRadius * (math.cos(math.radians(obj.HelixConeAngle)) / math.sin(math.radians(obj.HelixConeAngle)))
|
|
|
|
# Start height
|
|
z = passStartDepth
|
|
i = 0
|
|
|
|
# Default step down
|
|
z_step = 0.05
|
|
|
|
# Bigger angle, smaller step down
|
|
if _HelixAngle > 120:
|
|
z_step = 0.025
|
|
if _HelixAngle > 240:
|
|
z_step = 0.015
|
|
|
|
p = None
|
|
# Calculate conical helix
|
|
while(z >= passEndDepth):
|
|
if z < passEndDepth:
|
|
z = passEndDepth
|
|
|
|
p = CalcHelixConePoint(helix_full_height, i, HelixTopRadius, _HelixAngle)
|
|
op.commandlist.append(Path.Command("G1", {"X": p['X'] + region["HelixCenterPoint"][0], "Y": p['Y'] + region["HelixCenterPoint"][1], "Z": z, "F": op.vertFeed}))
|
|
z = z - z_step
|
|
i = i + z_step
|
|
|
|
# Calculate some stuff for arcs at bottom
|
|
p['X'] = p['X'] + region["HelixCenterPoint"][0]
|
|
p['Y'] = p['Y'] + region["HelixCenterPoint"][1]
|
|
x_m = region["HelixCenterPoint"][0] - p['X'] + region["HelixCenterPoint"][0]
|
|
y_m = region["HelixCenterPoint"][1] - p['Y'] + region["HelixCenterPoint"][1]
|
|
i_off = (x_m - p['X']) / 2
|
|
j_off = (y_m - p['Y']) / 2
|
|
|
|
# One more circle at target depth to make sure center is cleared
|
|
op.commandlist.append(Path.Command("G3", {"X": x_m, "Y": y_m, "Z": passEndDepth, "I": i_off, "J": j_off, "F": op.horizFeed}))
|
|
op.commandlist.append(Path.Command("G3", {"X": p['X'], "Y": p['Y'], "Z": passEndDepth, "I": -i_off, "J": -j_off, "F": op.horizFeed}))
|
|
|
|
else:
|
|
# Use arcs for helix - no conical shape support
|
|
helixStart = [region["HelixCenterPoint"][0] + r, region["HelixCenterPoint"][1]]
|
|
|
|
# rapid move to start point
|
|
op.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
|
|
op.commandlist.append(Path.Command("G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.ClearanceHeight.Value}))
|
|
|
|
# rapid move to safe height
|
|
op.commandlist.append(Path.Command("G0", {"X": helixStart[0], "Y": helixStart[1], "Z": obj.SafeHeight.Value}))
|
|
|
|
# move to start depth
|
|
op.commandlist.append(Path.Command("G1", {"X": helixStart[0], "Y": helixStart[1], "Z": passStartDepth, "F": op.vertFeed}))
|
|
|
|
x = region["HelixCenterPoint"][0] + r
|
|
y = region["HelixCenterPoint"][1]
|
|
|
|
curDep = passStartDepth
|
|
while curDep > (passEndDepth + depthPerOneCircle):
|
|
op.commandlist.append(Path.Command("G2", {"X": x - (2*r), "Y": y, "Z": curDep - (depthPerOneCircle/2), "I": -r, "F": op.vertFeed}))
|
|
op.commandlist.append(Path.Command("G2", {"X": x, "Y": y, "Z": curDep - depthPerOneCircle, "I": r, "F": op.vertFeed}))
|
|
curDep = curDep - depthPerOneCircle
|
|
|
|
lastStep = curDep - passEndDepth
|
|
if lastStep > (depthPerOneCircle/2):
|
|
op.commandlist.append(Path.Command("G2", {"X": x - (2*r), "Y": y, "Z": curDep - (lastStep/2), "I": -r, "F": op.vertFeed}))
|
|
op.commandlist.append(Path.Command("G2", {"X": x, "Y": y, "Z": passEndDepth, "I": r, "F": op.vertFeed}))
|
|
else:
|
|
op.commandlist.append(Path.Command("G2", {"X": x - (2*r), "Y": y, "Z": passEndDepth, "I": -r, "F": op.vertFeed}))
|
|
op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "Z": passEndDepth, "F": op.vertFeed}))
|
|
|
|
# one more circle at target depth to make sure center is cleared
|
|
op.commandlist.append(Path.Command("G2", {"X": x - (2*r), "Y": y, "Z": passEndDepth, "I": -r, "F": op.horizFeed}))
|
|
op.commandlist.append(Path.Command("G2", {"X": x, "Y": y, "Z": passEndDepth, "I": r, "F": op.horizFeed}))
|
|
# lx = x
|
|
# ly = y
|
|
|
|
else: # no helix entry
|
|
# rapid move to clearance height
|
|
op.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value}))
|
|
op.commandlist.append(Path.Command("G0", {"X": region["StartPoint"][0], "Y": region["StartPoint"][1], "Z": obj.ClearanceHeight.Value}))
|
|
# straight plunge to target depth
|
|
op.commandlist.append(Path.Command("G1", {"X": region["StartPoint"][0], "Y": region["StartPoint"][1], "Z": passEndDepth, "F": op.vertFeed}))
|
|
|
|
lz = passEndDepth
|
|
z = obj.ClearanceHeight.Value
|
|
op.commandlist.append(Path.Command("(Adaptive - depth: %f)" % passEndDepth))
|
|
|
|
# add adaptive paths
|
|
for pth in region["AdaptivePaths"]:
|
|
motionType = pth[0] # [0] contains motion type
|
|
|
|
for pt in pth[1]: # [1] contains list of points
|
|
x = pt[0]
|
|
y = pt[1]
|
|
|
|
# dist = math.sqrt((x-lx)*(x-lx) + (y-ly)*(y-ly))
|
|
|
|
if motionType == area.AdaptiveMotionType.Cutting:
|
|
z = passEndDepth
|
|
if z != lz:
|
|
op.commandlist.append(Path.Command("G1", {"Z": z, "F": op.vertFeed}))
|
|
|
|
op.commandlist.append(Path.Command("G1", {"X": x, "Y": y, "F": op.horizFeed}))
|
|
|
|
elif motionType == area.AdaptiveMotionType.LinkClear:
|
|
z = passEndDepth + stepUp
|
|
if z != lz:
|
|
op.commandlist.append(Path.Command("G0", {"Z": z}))
|
|
|
|
op.commandlist.append(Path.Command("G0", {"X": x, "Y": y}))
|
|
|
|
elif motionType == area.AdaptiveMotionType.LinkNotClear:
|
|
z = obj.ClearanceHeight.Value
|
|
if z != lz:
|
|
op.commandlist.append(Path.Command("G0", {"Z": z}))
|
|
|
|
op.commandlist.append(Path.Command("G0", {"X": x, "Y": y}))
|
|
|
|
# elif motionType == area.AdaptiveMotionType.LinkClearAtPrevPass:
|
|
# if lx!=x or ly!=y:
|
|
# op.commandlist.append(Path.Command("G0", { "X": lx, "Y":ly, "Z":passStartDepth+stepUp}))
|
|
# op.commandlist.append(Path.Command("G0", { "X": x, "Y":y, "Z":passStartDepth+stepUp}))
|
|
|
|
# lx = x
|
|
# ly = y
|
|
lz = z
|
|
|
|
# return to safe height in this Z pass
|
|
z = obj.ClearanceHeight.Value
|
|
if z != lz:
|
|
op.commandlist.append(Path.Command("G0", {"Z": z}))
|
|
|
|
lz = z
|
|
|
|
passStartDepth = passEndDepth
|
|
|
|
# return to safe height in this Z pass
|
|
z = obj.ClearanceHeight.Value
|
|
if z != lz:
|
|
op.commandlist.append(Path.Command("G0", {"Z": z}))
|
|
|
|
lz = z
|
|
|
|
z = obj.ClearanceHeight.Value
|
|
if z != lz:
|
|
op.commandlist.append(Path.Command("G0", {"Z": z}))
|
|
|
|
lz = z
|
|
|
|
|
|
def Execute(op, obj):
|
|
# pylint: disable=global-statement
|
|
global sceneGraph
|
|
global topZ
|
|
|
|
if FreeCAD.GuiUp:
|
|
sceneGraph = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
|
|
|
|
PathLog.info("*** Adaptive toolpath processing started...\n")
|
|
|
|
# hide old toolpaths during recalculation
|
|
obj.Path = Path.Path("(Calculating...)")
|
|
|
|
if FreeCAD.GuiUp:
|
|
#store old visibility state
|
|
job = op.getJob(obj)
|
|
oldObjVisibility = obj.ViewObject.Visibility
|
|
oldJobVisibility = job.ViewObject.Visibility
|
|
|
|
obj.ViewObject.Visibility = False
|
|
job.ViewObject.Visibility = False
|
|
|
|
FreeCADGui.updateGui()
|
|
|
|
try:
|
|
helixDiameter = obj.HelixDiameterLimit.Value
|
|
topZ = op.stock.Shape.BoundBox.ZMax
|
|
obj.Stopped = False
|
|
obj.StopProcessing = False
|
|
if obj.Tolerance < 0.001:
|
|
obj.Tolerance = 0.001
|
|
|
|
# Get list of working edges for adaptive algorithm
|
|
pathArray = op.pathArray
|
|
if not pathArray:
|
|
PathLog.error("No wire data returned.")
|
|
return
|
|
|
|
path2d = convertTo2d(pathArray)
|
|
|
|
stockPaths = []
|
|
if hasattr(op.stock, "StockType") and op.stock.StockType == "CreateCylinder":
|
|
stockPaths.append([discretize(op.stock.Shape.Edges[0])])
|
|
|
|
else:
|
|
stockBB = op.stock.Shape.BoundBox
|
|
v = []
|
|
v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMin, 0))
|
|
v.append(FreeCAD.Vector(stockBB.XMax, stockBB.YMin, 0))
|
|
v.append(FreeCAD.Vector(stockBB.XMax, stockBB.YMax, 0))
|
|
v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMax, 0))
|
|
v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMin, 0))
|
|
stockPaths.append([v])
|
|
|
|
stockPath2d = convertTo2d(stockPaths)
|
|
|
|
opType = area.AdaptiveOperationType.ClearingInside
|
|
if obj.OperationType == "Clearing":
|
|
if obj.Side == "Outside":
|
|
opType = area.AdaptiveOperationType.ClearingOutside
|
|
|
|
else:
|
|
opType = area.AdaptiveOperationType.ClearingInside
|
|
|
|
else: # profiling
|
|
if obj.Side == "Outside":
|
|
opType = area.AdaptiveOperationType.ProfilingOutside
|
|
|
|
else:
|
|
opType = area.AdaptiveOperationType.ProfilingInside
|
|
|
|
keepToolDownRatio = 3.0
|
|
if hasattr(obj, 'KeepToolDownRatio'):
|
|
keepToolDownRatio = float(obj.KeepToolDownRatio)
|
|
|
|
# put here all properties that influence calculation of adaptive base paths,
|
|
|
|
inputStateObject = {
|
|
"tool": float(op.tool.Diameter),
|
|
"tolerance": float(obj.Tolerance),
|
|
"geometry": path2d,
|
|
"stockGeometry": stockPath2d,
|
|
"stepover": float(obj.StepOver),
|
|
"effectiveHelixDiameter": float(helixDiameter),
|
|
"operationType": obj.OperationType,
|
|
"side": obj.Side,
|
|
"forceInsideOut": obj.ForceInsideOut,
|
|
"finishingProfile": obj.FinishingProfile,
|
|
"keepToolDownRatio": keepToolDownRatio,
|
|
"stockToLeave": float(obj.StockToLeave)
|
|
}
|
|
|
|
inputStateChanged = False
|
|
adaptiveResults = None
|
|
|
|
if obj.AdaptiveOutputState is not None and obj.AdaptiveOutputState != "":
|
|
adaptiveResults = obj.AdaptiveOutputState
|
|
|
|
if json.dumps(obj.AdaptiveInputState) != json.dumps(inputStateObject):
|
|
inputStateChanged = True
|
|
adaptiveResults = None
|
|
|
|
# progress callback fn, if return true it will stop processing
|
|
def progressFn(tpaths):
|
|
if FreeCAD.GuiUp:
|
|
for path in tpaths: #path[0] contains the MotionType, #path[1] contains list of points
|
|
if path[0] == area.AdaptiveMotionType.Cutting:
|
|
sceneDrawPath(path[1],(0,0,1))
|
|
|
|
else:
|
|
sceneDrawPath(path[1],(1,0,1))
|
|
|
|
FreeCADGui.updateGui()
|
|
|
|
return obj.StopProcessing
|
|
|
|
start = time.time()
|
|
|
|
if inputStateChanged or adaptiveResults is None:
|
|
a2d = area.Adaptive2d()
|
|
a2d.stepOverFactor = 0.01 * obj.StepOver
|
|
a2d.toolDiameter = float(op.tool.Diameter)
|
|
a2d.helixRampDiameter = helixDiameter
|
|
a2d.keepToolDownDistRatio = keepToolDownRatio
|
|
a2d.stockToLeave = float(obj.StockToLeave)
|
|
a2d.tolerance = float(obj.Tolerance)
|
|
a2d.forceInsideOut = obj.ForceInsideOut
|
|
a2d.finishingProfile = obj.FinishingProfile
|
|
a2d.opType = opType
|
|
|
|
# EXECUTE
|
|
results = a2d.Execute(stockPath2d, path2d, progressFn)
|
|
|
|
# need to convert results to python object to be JSON serializable
|
|
adaptiveResults = []
|
|
for result in results:
|
|
adaptiveResults.append({
|
|
"HelixCenterPoint": result.HelixCenterPoint,
|
|
"StartPoint": result.StartPoint,
|
|
"AdaptivePaths": result.AdaptivePaths,
|
|
"ReturnMotionType": result.ReturnMotionType})
|
|
|
|
# GENERATE
|
|
GenerateGCode(op, obj, adaptiveResults, helixDiameter)
|
|
|
|
if not obj.StopProcessing:
|
|
PathLog.info("*** Done. Elapsed time: %f sec\n\n" % (time.time()-start))
|
|
obj.AdaptiveOutputState = adaptiveResults
|
|
obj.AdaptiveInputState = inputStateObject
|
|
|
|
else:
|
|
PathLog.info("*** Processing cancelled (after: %f sec).\n\n" % (time.time()-start))
|
|
|
|
finally:
|
|
if FreeCAD.GuiUp:
|
|
obj.ViewObject.Visibility = oldObjVisibility
|
|
job.ViewObject.Visibility = oldJobVisibility
|
|
sceneClean()
|
|
|
|
|
|
def _get_working_edges(op, obj):
|
|
"""_get_working_edges(op, obj)...
|
|
Compile all working edges from the Base Geometry selection (obj.Base)
|
|
for the current operation.
|
|
Additional modifications to selected region(face), such as extensions,
|
|
should be placed within this function.
|
|
"""
|
|
regions = list()
|
|
all_regions = list()
|
|
edge_list = list()
|
|
avoidFeatures = list()
|
|
|
|
# Get extensions and identify faces to avoid
|
|
extensions = FeatureExtensions.getExtensions(obj)
|
|
for e in extensions:
|
|
if e.avoid:
|
|
avoidFeatures.append(e.feature)
|
|
|
|
# Get faces selected by user
|
|
for base, subs in obj.Base:
|
|
for sub in subs:
|
|
if sub not in avoidFeatures:
|
|
if obj.UseOutline:
|
|
face = base.Shape.getElement(sub)
|
|
# get outline with wire_A method used in PocketShape, but it does not play nicely later
|
|
# wire_A = TechDraw.findShapeOutline(face, 1, FreeCAD.Vector(0.0, 0.0, 1.0))
|
|
wire_B = face.Wires[0]
|
|
shape = Part.Face(wire_B)
|
|
else:
|
|
shape = base.Shape.getElement(sub)
|
|
regions.append(shape)
|
|
# Efor
|
|
|
|
# Return Extend Outline extension, OR regular edge extension
|
|
all_regions = regions
|
|
# Apply regular Extensions
|
|
op.exts = [] # pylint: disable=attribute-defined-outside-init
|
|
for ext in extensions:
|
|
if not ext.avoid:
|
|
wire = ext.getWire()
|
|
if wire:
|
|
for f in ext.getExtensionFaces(wire):
|
|
op.exts.append(f)
|
|
all_regions.append(f)
|
|
|
|
# Second face-combining method attempted
|
|
horizontal = PathGeom.combineHorizontalFaces(all_regions)
|
|
for f in horizontal:
|
|
for w in f.Wires:
|
|
for e in w.Edges:
|
|
edge_list.append([discretize(e)])
|
|
|
|
return edge_list
|
|
|
|
|
|
class PathAdaptive(PathOp.ObjectOp):
|
|
def opFeatures(self, obj):
|
|
'''opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation.
|
|
The default implementation returns "FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint"
|
|
Should be overwritten by subclasses.'''
|
|
return PathOp.FeatureTool | PathOp.FeatureBaseEdges | PathOp.FeatureDepths \
|
|
| PathOp.FeatureFinishDepth | PathOp.FeatureStepDown | PathOp.FeatureHeights \
|
|
| PathOp.FeatureBaseGeometry | PathOp.FeatureCoolant | PathOp.FeatureLocations
|
|
|
|
def initOperation(self, obj):
|
|
'''initOperation(obj) ... implement to create additional properties.
|
|
Should be overwritten by subclasses.'''
|
|
obj.addProperty("App::PropertyEnumeration", "Side", "Adaptive", "Side of selected faces that tool should cut")
|
|
obj.Side = ['Outside', 'Inside'] # side of profile that cutter is on in relation to direction of profile
|
|
|
|
obj.addProperty("App::PropertyEnumeration", "OperationType", "Adaptive", "Type of adaptive operation")
|
|
obj.OperationType = ['Clearing', 'Profiling'] # side of profile that cutter is on in relation to direction of profile
|
|
|
|
obj.addProperty("App::PropertyFloat", "Tolerance", "Adaptive", "Influences accuracy and performance")
|
|
obj.addProperty("App::PropertyPercent", "StepOver", "Adaptive", "Percent of cutter diameter to step over on each pass")
|
|
obj.addProperty("App::PropertyDistance", "LiftDistance", "Adaptive", "Lift distance for rapid moves")
|
|
obj.addProperty("App::PropertyDistance", "KeepToolDownRatio", "Adaptive", "Max length of keep tool down path compared to direct distance between points")
|
|
obj.addProperty("App::PropertyDistance", "StockToLeave", "Adaptive", "How much stock to leave (i.e. for finishing operation)")
|
|
# obj.addProperty("App::PropertyBool", "ProcessHoles", "Adaptive","Process holes as well as the face outline")
|
|
|
|
obj.addProperty("App::PropertyBool", "ForceInsideOut", "Adaptive", "Force plunging into material inside and clearing towards the edges")
|
|
obj.addProperty("App::PropertyBool", "FinishingProfile", "Adaptive", "To take a finishing profile path at the end")
|
|
obj.addProperty("App::PropertyBool", "Stopped",
|
|
"Adaptive", "Stop processing")
|
|
obj.setEditorMode('Stopped', 2) # hide this property
|
|
|
|
obj.addProperty("App::PropertyBool", "StopProcessing",
|
|
"Adaptive", "Stop processing")
|
|
obj.setEditorMode('StopProcessing', 2) # hide this property
|
|
|
|
obj.addProperty("App::PropertyBool", "UseHelixArcs", "Adaptive", "Use Arcs (G2) for helix ramp")
|
|
|
|
obj.addProperty("App::PropertyPythonObject", "AdaptiveInputState",
|
|
"Adaptive", "Internal input state")
|
|
obj.addProperty("App::PropertyPythonObject", "AdaptiveOutputState",
|
|
"Adaptive", "Internal output state")
|
|
obj.setEditorMode('AdaptiveInputState', 2) # hide this property
|
|
obj.setEditorMode('AdaptiveOutputState', 2) # hide this property
|
|
obj.addProperty("App::PropertyAngle", "HelixAngle", "Adaptive", "Helix ramp entry angle (degrees)")
|
|
obj.addProperty("App::PropertyAngle", "HelixConeAngle", "Adaptive", "Helix cone angle (degrees)")
|
|
obj.addProperty("App::PropertyLength", "HelixDiameterLimit", "Adaptive", "Limit helix entry diameter, if limit larger than tool diameter or 0, tool diameter is used")
|
|
|
|
obj.addProperty("App::PropertyBool", "UseOutline", "Adaptive", "Uses the outline of the base geometry.")
|
|
|
|
FeatureExtensions.initialize_properties(obj)
|
|
|
|
def opSetDefaultValues(self, obj, job):
|
|
obj.Side = "Inside"
|
|
obj.OperationType = "Clearing"
|
|
obj.Tolerance = 0.1
|
|
obj.StepOver = 20
|
|
obj.LiftDistance = 0
|
|
# obj.ProcessHoles = True
|
|
obj.ForceInsideOut = False
|
|
obj.FinishingProfile = True
|
|
obj.Stopped = False
|
|
obj.StopProcessing = False
|
|
obj.HelixAngle = 5
|
|
obj.HelixConeAngle = 0
|
|
obj.HelixDiameterLimit = 0.0
|
|
obj.AdaptiveInputState = ""
|
|
obj.AdaptiveOutputState = ""
|
|
obj.StockToLeave = 0
|
|
obj.KeepToolDownRatio = 3.0
|
|
obj.UseHelixArcs = False
|
|
obj.UseOutline = False
|
|
FeatureExtensions.set_default_property_values(obj, job)
|
|
|
|
def opExecute(self, obj):
|
|
'''opExecute(obj) ... called whenever the receiver needs to be recalculated.
|
|
See documentation of execute() for a list of base functionality provided.
|
|
Should be overwritten by subclasses.'''
|
|
|
|
self.pathArray = _get_working_edges(self, obj)
|
|
Execute(self, obj)
|
|
|
|
def opOnDocumentRestored(self, obj):
|
|
if not hasattr(obj, 'HelixConeAngle'):
|
|
obj.addProperty("App::PropertyAngle", "HelixConeAngle", "Adaptive", "Helix cone angle (degrees)")
|
|
|
|
if not hasattr(obj, "UseOutline"):
|
|
obj.addProperty("App::PropertyBool",
|
|
"UseOutline",
|
|
"Adaptive",
|
|
"Uses the outline of the base geometry.")
|
|
FeatureExtensions.initialize_properties(obj)
|
|
|
|
|
|
def SetupProperties():
|
|
setup = ["Side", "OperationType", "Tolerance", "StepOver",
|
|
"LiftDistance", "KeepToolDownRatio", "StockToLeave",
|
|
"ForceInsideOut", "FinishingProfile", "Stopped",
|
|
"StopProcessing", "UseHelixArcs", "AdaptiveInputState",
|
|
"AdaptiveOutputState", "HelixAngle", "HelixConeAngle",
|
|
"HelixDiameterLimit", "UseOutline"]
|
|
return setup
|
|
|
|
|
|
def Create(name, obj=None):
|
|
'''Create(name) ... Creates and returns a Adaptive operation.'''
|
|
if obj is None:
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
|
obj.Proxy = PathAdaptive(obj, name)
|
|
return obj
|