Path: Vcarve - Added threshold property to remove unwanted segments

code cleanup & debug
This commit is contained in:
sliptonic
2019-06-11 21:59:54 -05:00
parent d9d4387b1d
commit f63d0fadcb
4 changed files with 291 additions and 161 deletions

View File

@@ -49,6 +49,23 @@ class MESHGate(PathBaseGate):
class VCARVEGate:
def allow(self, doc, obj, sub):
try:
shape = obj.Shape
except Exception: # pylint: disable=broad-except
return False
if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0:
return True
if shape.ShapeType == 'Edge':
return True
if sub:
subShape = shape.getElement(sub)
if subShape.ShapeType == 'Edge':
return True
return False
class ENGRAVEGate(PathBaseGate):

View File

@@ -22,7 +22,6 @@
# * *
# ***************************************************************************
import ArchPanel
import FreeCAD
import Part
import Path
@@ -30,9 +29,9 @@ import PathScripts.PathEngraveBase as PathEngraveBase
import PathScripts.PathLog as PathLog
import PathScripts.PathOp as PathOp
import PathScripts.PathUtils as PathUtils
import PathScripts.PathGeom as PathGeom
import traceback
import time
import PathScripts.PathGeom as pg
from PathScripts.PathOpTools import orientWire
import math
@@ -46,148 +45,264 @@ if False:
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
# Qt tanslation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class ObjectVcarve(PathEngraveBase.ObjectOp):
'''Proxy class for Vcarve operation.'''
def opFeatures(self, obj):
'''opFeatures(obj) ... return all standard features and edges based geomtries'''
return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureBaseFaces;
return PathOp.FeatureTool | PathOp.FeatureHeights | PathOp.FeatureBaseFaces
def setupAdditionalProperties(self, obj):
if not hasattr(obj, 'BaseShapes'):
obj.addProperty("App::PropertyLinkList", "BaseShapes", "Path", QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Additional base objects to be engraved"))
obj.setEditorMode('BaseShapes', 2) # hide
obj.setEditorMode('BaseShapes', 2) # hide
if not hasattr(obj, 'BaseObject'):
obj.addProperty("App::PropertyLink", "BaseObject", "Path", QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Additional base objects to be engraved"))
obj.setEditorMode('BaseObject', 2) # hide
obj.setEditorMode('BaseObject', 2) # hide
def initOperation(self, obj):
'''initOperation(obj) ... create vcarve specific properties.'''
obj.addProperty("App::PropertyFloat", "Discretize", "Path", QtCore.QT_TRANSLATE_NOOP("PathVcarve", "The deflection value for discretizing arcs"))
obj.addProperty("App::PropertyFloat", "Threshold", "Path", QtCore.QT_TRANSLATE_NOOP("PathVcarve", "cutoff threshold for removing extraneous segments (0-1.0). default=0.8. Larger numbers remove less."))
obj.Threshold = 0.8
obj.Discretize = 0.01
self.setupAdditionalProperties(obj)
def opOnDocumentRestored(self, obj):
# upgrade ...
self.setupAdditionalProperties(obj)
def buildPathMedial(self, obj, Faces, zDepths, unitcircle):
def buildPathMedial(self, obj, Faces):
'''constructs a medial axis path using openvoronoi'''
import openvoronoi as ovd
#import openvoronoi as ovd
def insert_wire_points(vd, wire):
pts=[]
for p in wire.Vertexes:
pts.append( ovd.Point( p.X, p.Y ) )
print('p1 = FreeCAD.Vector(X:{} Y:{}'.format(p.X, p.Y))
id_list = []
print("inserting ",len(pts)," point-sites:")
for p in pts:
id_list.append( vd.addVertexSite( p ) )
return id_list
# def insert_wire_points(vd, wire):
# pts = []
# for p in wire.Vertexes:
# pts.append(ovd.Point(p.X, p.Y))
# PathLog.debug('ovd.Point( {} ,{} )'.format(p.X, p.Y))
# id_list = []
# PathLog.debug("inserting {} openvoronoi point-sites".format(len(pts)))
# for p in pts:
# id_list.append(vd.addVertexSite(p))
# return id_list
def insert_wire_segments(vd,id_list):
print('insert_polygon-segments')
print('inserting {} segments'.format(len(id_list)))
for n in range(len(id_list)):
n_nxt = n+1
if n==(len(id_list)-1):
n_nxt=0
vd.addLineSite( id_list[n], id_list[n_nxt])
# def insert_wire_segments(vd, id_list):
# PathLog.debug('inserting {} segments into the voronoi diagram'.format(len(id_list)))
# for n in range(len(id_list)):
# n_nxt = n + 1
# if n == (len(id_list) - 1):
# n_nxt = 0
# vd.addLineSite(id_list[n], id_list[n_nxt])
def insert_many_wires(vd, wires):
# print('inserting {} wires'.format(len(obj.Wires)))
polygon_ids =[]
t_before = time.time()
for idx, wire in enumerate(wires):
print('discretize: {}'.format(obj.Discretize))
pointList = wire.discretize(Deflection=obj.Discretize)
segwire = Part.Wire([Part.makeLine(p[0],p[1]) for p in zip(pointList, pointList[1:] )])
#polygon_ids = []
#t_before = time.time()
for wire in wires:
PathLog.debug('discretize value: {}'.format(obj.Discretize))
pts = wire.discretize(QuasiDeflection=obj.Discretize)
ptv = [FreeCAD.Vector(p[0], p[1]) for p in pts]
ptv.append(ptv[0])
if idx == 0:
segwire = orientWire(segwire, forward=False)
else:
segwire = orientWire(segwire, forward=True)
for i in range(len(pts)):
vd.addSegment(ptv[i], ptv[i+1])
poly_id = insert_wire_points(vd,segwire)
polygon_ids.append(poly_id)
t_after = time.time()
pt_time = t_after-t_before
# segwire = Part.Wire([Part.makeLine(p[0], p[1]) for p in zip(pointList, pointList[1:])])
t_before = time.time()
for ids in polygon_ids:
insert_wire_segments(vd,ids)
t_after = time.time()
seg_time = t_after-t_before
return [pt_time, seg_time]
# if idx == 0:
# segwire = orientWire(segwire, forward=False)
# else:
# segwire = orientWire(segwire, forward=True)
# poly_id = insert_wire_points(vd, segwire)
# polygon_ids.append(poly_id)
# t_after = time.time()
# pt_time = t_after - t_before
# t_before = time.time()
# for ids in polygon_ids:
# insert_wire_segments(vd, ids)
# t_after = time.time()
# seg_time = t_after - t_before
# return [pt_time, seg_time]
def calculate_depth(MIC):
# given a maximum inscribed circle (MIC) and tool angle,
# return depth of cut.
maxdepth = obj.ToolController.Tool.CuttingEdgeHeight
toolangle = obj.ToolController.Tool.CuttingEdgeAngle
return MIC / math.tan(math.radians(toolangle/2))
d = MIC / math.tan(math.radians(toolangle / 2))
return d if d <= maxdepth else maxdepth
def buildMedial(vd):
safeheight = obj.SafeHeight.Value
# def buildMedial(vd):
# safeheight = obj.SafeHeight.Value
# path = []
# maw = ovd.MedialAxisWalk(vd.getGraph())
# toolpath = maw.walk()
# for chain in toolpath:
# path.append(Path.Command("G0 Z{}".format(safeheight)))
# p = chain[0][0][0]
# z = -(chain[0][0][1])
# path.append(Path.Command("G0 X{} Y{} Z{}".format(p.x, p.y, safeheight)))
# for step in chain:
# for point in step:
# p = point[0]
# z = calculate_depth(-(point[1]))
# path.append(Path.Command("G1 X{} Y{} Z{} F{}".format(p.x, p.y, z, obj.ToolController.HorizFeed.Value)))
# path.append(Path.Command("G0 Z{}".format(safeheight))) return path pathlist = []
def getEdges(vd, color=[0]):
if type(color) == int:
color = [color]
geomList = []
for e in vd.Edges:
if e.Color not in color:
continue
# geom = e.toGeom(8)
if e.toGeom(8) is None:
continue
p1 = e.Vertices[0].toGeom(calculate_depth(0-e.getDistances()[0]))
p2 = e.Vertices[-1].toGeom(calculate_depth(0-e.getDistances()[-1]))
geomList.append(Part.LineSegment(p1, p2))
# if individualEdges:
# name = "e%04d" % i
# Part.show(Part.Edge(geom), name)
#geomList.append(Part.Edge(geom))
if geomList:
return geomList
def areConnected(seg1, seg2):
'''
Checks if two segments share an endpoint.
returns a new linesegment if connected or original seg1 if not
'''
l1 = [seg1.StartPoint, seg1.EndPoint]
l2 = [seg2.StartPoint, seg2.EndPoint]
l3 = [v1 for v1 in l1 for v2 in l2 if PathGeom.pointsCoincide (v1, v2, error=0.01)]
# for v1 in l1:
# for v2 in l2:
# if PathGeom.pointsCoincide(v1, v2):
# l3.append(v1)
#l3 = [value for value in l1 if value in l2]
print('l1: {} l2: {} l3: {}'.format(l1,l2,l3))
if len(l3) == 0: # no connection
print('no connection')
return seg1
elif len(l3) == 1: # extend chain
print('one vert')
p1 = l1[0] if l1[0] == l3[0] else l1[1]
p2 = l2[0] if l2[0] == l3[0] else l2[1]
return Part.LineSegment(p1, p2)
else: # loop
print('loop')
return None
def chains(seglist):
'''
iterates through segements and builds a list of chains
'''
chains = []
while len(seglist) > 0:
cur_seg = seglist.pop(0)
cur_chain = [cur_seg]
remaining = []
tempseg = cur_seg # tempseg is a linesegment from first vertex to last in curchain
for i, seg in enumerate(seglist):
ac = areConnected(tempseg, seg)
if ac != tempseg:
cur_chain.append(seg)
if ac is None:
remaining.extend(seglist[i+1:])
break
else:
tempseg = ac
#print("c: {}".format(cur_chain))
chains.append(cur_chain)
seglist = remaining
return chains
def cutWire(w):
path = []
maw = ovd.MedialAxisWalk( vd.getGraph() )
toolpath = maw.walk()
for chain in toolpath:
path.append(Path.Command("G0 Z{}".format(safeheight)))
p = chain[0][0][0]
z = -(chain[0][0][1])
path.append(Path.Command("G0 X{} Y{} Z{}".format(p.x, p.y, safeheight)))
for step in chain:
for point in step:
p = point[0]
z = calculate_depth(-(point[1]))
path.append(Path.Command("G1 X{} Y{} Z{} F{}".format(p.x, p.y, z, obj.ToolController.HorizFeed.Value)))
path.append(Path.Command("G0 Z{}".format(safeheight)))
p = w.Vertexes[0]
path.append(Path.Command("G0 Z{}".format(obj.SafeHeight.Value)))
path.append(Path.Command("G0 X{} Y{} Z{}".format(p.X, p.Y, obj.SafeHeight.Value)))
# print('\/ \/ \/')
# print(p.Point)
c = Path.Command("G1 X{} Y{} Z{} F{}".format(p.X, p.Y, p.Z, obj.ToolController.HorizFeed.Value))
# print(c)
# print('/\ /\ /\ ')
path.append(c)
#path.append(Path.Command("G1 X{} Y{} Z{} F{}".format(p.X, p.Y, p.Z, obj.ToolController.HorizFeed.Value)))
for vert in w.Vertexes[1:]:
path.append(Path.Command("G1 X{} Y{} Z{} F{}".format(vert.X, vert.Y, vert.Z, obj.ToolController.HorizFeed.Value)))
path.append(Path.Command("G0 Z{}".format(obj.SafeHeight.Value)))
return path
pathlist = []
bins = 120 # int bins = number of bins for grid-search (affects performance, should not affect correctness)
pathlist.append(Path.Command("(starting)"))
for f in Faces:
#unitcircle = f.BoundBox.DiagonalLength/2
print('unitcircle: {}'.format(unitcircle))
vd = ovd.VoronoiDiagram(200, bins)
vd.set_silent(True) # suppress Warnings!
wires = f.Wires
insert_many_wires(vd, wires)
pi = ovd.PolygonInterior( True )
vd.filter_graph(pi)
ma = ovd.MedialAxis()
vd.filter_graph(ma)
pathlist.extend(buildMedial( vd )) # the actual cutting g-code
vd = Path.Voronoi()
insert_many_wires(vd, f.Wires)
vd.construct()
# vd.colorExterior(1)
# vd.colorTwins(2)
for e in vd.Edges:
e.Color = 0 if e.isPrimary() else 5
vd.colorExterior(1)
vd.colorExterior(4, lambda v: not f.isInside(v.toGeom(), 0.01, True)) # should derive tolerance from geometry
vd.colorColinear(3)
vd.colorTwins(2)
edgelist = getEdges(vd)
# for e in edgelist:
# Part.show(e.toShape())
# for e in [e_ for e_ in vd.Edges if e_.Color == 2]:
# print(e.getDistances())
# p1 = e.Vertices[0].toGeom(calculate_depth(0-e.getDistances()[0]))
# p2 = e.Vertices[-1].toGeom(calculate_depth(0-e.getDistances()[-1]))
# edgelist.append(Part.makeLine(p1, p2))
# vlist = []
# for v, r in zip(e.Vertices, e.getDistances()):
# p = v.toGeom()
# p.z = calculate_depth(r)
# vlist.append(p)
# l = Part.makeLine(vlist[0], vlist[-1])
# edgelist.append(l)
# for s in Part.sortEdges(edgelist):
# pathlist.extend(cutWire(Part.Wire(s)))
for chain in chains(edgelist):
print('chain length {}'.format(len(chain)))
print(chain)
Part.show(Part.Wire([e.toShape() for e in chain]))
#pathlist.extend(sortedWires) # the actual cutting g-code
self.commandlist = pathlist
def opExecute(self, obj):
'''opExecute(obj) ... process engraving operation'''
PathLog.track()
# Openvoronoi must be installed
try:
import openvoronoi as ovd
except:
FreeCAD.Console.PrintError(
translate("Path_Vcarve", "This operation requires OpenVoronoi to be installed.") + "\n")
return
job = PathUtils.findParentJob(obj)
jobshapes = []
zValues = self.getZValues(obj)
if obj.ToolController.Tool.ToolType != 'Engraver':
FreeCAD.Console.PrintError(
@@ -199,62 +314,23 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
translate("Path_Vcarve", "Engraver Cutting Edge Angle must be < 180 degrees.") + "\n")
return
try:
if len(self.model) == 1 and self.model[0].isDerivedFrom('Sketcher::SketchObject') or \
if obj.Base:
PathLog.track()
for base in obj.Base:
faces = []
for sub in base[1]:
shape = getattr(base[0].Shape, sub)
if isinstance(shape, Part.Face):
faces.append(shape)
modelshape = Part.makeCompound(faces)
elif len(self.model) == 1 and self.model[0].isDerivedFrom('Sketcher::SketchObject') or \
self.model[0].isDerivedFrom('Part::Part2DObject'):
PathLog.track()
# self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
# we only consider the outer wire if this is a Face
modelshape = self.model[0].Shape
modelshape.tessellate(0.01)
self.buildPathMedial(obj, modelshape.Faces, zValues, modelshape.BoundBox.DiagonalLength/2)
# self.wires = wires
# elif obj.Base:
# PathLog.track()
# wires = []
# for base, subs in obj.Base:
# edges = []
# basewires = []
# for feature in subs:
# sub = base.Shape.getElement(feature)
# if type(sub) == Part.Edge:
# edges.append(sub)
# elif sub.Wires:
# basewires.extend(sub.Wires)
# else:
# basewires.append(Part.Wire(sub.Edges))
# for edgelist in Part.sortEdges(edges):
# basewires.append(Part.Wire(edgelist))
# wires.extend(basewires)
# self.buildpathocc(obj, wires, zValues)
# self.wires = wires
# elif not obj.BaseShapes:
# PathLog.track()
# if not obj.Base and not obj.BaseShapes:
# for base in self.model:
# PathLog.track(base.Label)
# if base.isDerivedFrom('Part::Part2DObject'):
# jobshapes.append(base)
# if not jobshapes:
# raise ValueError(translate('PathVcarve', "Unknown baseobject type for engraving (%s)") % (obj.Base))
# if obj.BaseShapes or jobshapes:
# PathLog.track()
# wires = []
# for shape in obj.BaseShapes + jobshapes:
# PathLog.track(shape.Label)
# shapeWires = shape.Shape.Wires
# self.buildpathocc(obj, shapeWires, zValues)
# wires.extend(shapeWires)
# self.wires = wires
# # the last command is a move to clearance, which is automatically added by PathOp
# if self.commandlist:
# self.commandlist.pop()
self.buildPathMedial(obj, modelshape.Faces)
except Exception as e:
PathLog.error(e)
@@ -266,13 +342,14 @@ class ObjectVcarve(PathEngraveBase.ObjectOp):
job = PathUtils.findParentJob(obj)
self.opSetDefaultValues(obj, job)
def SetupProperties():
return [ "Discretize" ]
def Create(name, obj = None):
def SetupProperties():
return ["Discretize"]
def Create(name, obj=None):
'''Create(name) ... Creates and returns a Vcarve operation.'''
if obj is None:
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
proxy = ObjectVcarve(obj, name)
return obj

View File

@@ -27,7 +27,6 @@ import FreeCADGui
import PathScripts.PathVcarve as PathVcarve
import PathScripts.PathLog as PathLog
import PathScripts.PathOpGui as PathOpGui
import PathScripts.PathSelection as PathSelection
import PathScripts.PathUtils as PathUtils
from PySide import QtCore, QtGui
@@ -43,9 +42,11 @@ if False:
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
'''Enhanced base geometry page to also allow special base objects.'''
@@ -59,10 +60,10 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
job = PathUtils.findParentJob(self.obj)
base = job.Proxy.resourceClone(job, sel.Object)
if not base:
PathLog.notice((translate("Path", "%s is not a Base Model object of the job %s")+"\n") % (sel.Object.Label, job.Label))
PathLog.notice((translate("Path", "%s is not a Base Model object of the job %s") + "\n") % (sel.Object.Label, job.Label))
continue
if base in shapes:
PathLog.notice((translate("Path", "Base shape %s already in the list")+"\n") % (sel.Object.Label))
PathLog.notice((translate("Path", "Base shape %s already in the list") + "\n") % (sel.Object.Label))
continue
if base.isDerivedFrom('Part::Part2DObject'):
if sel.HasSubObjects:
@@ -74,7 +75,7 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
self.obj.Proxy.addBase(self.obj, base, sub)
else:
# when adding an entire shape to BaseShapes we can take its sub shapes out of Base
self.obj.Base = [(p,el) for p,el in self.obj.Base if p != base]
self.obj.Base = [(p, el) for p, el in self.obj.Base if p != base]
shapes.append(base)
self.obj.BaseShapes = shapes
added = True
@@ -108,6 +109,7 @@ class TaskPanelBaseGeometryPage(PathOpGui.TaskPanelBaseGeometryPage):
self.obj.BaseShapes = shapes
return self.super().updateBase()
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
'''Page controller class for the Vcarve operation.'''
@@ -119,17 +121,21 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
'''getFields(obj) ... transfers values from UI to obj's proprties'''
if obj.Discretize != self.form.discretize.value():
obj.Discretize = self.form.discretize.value()
if obj.Threshold != self.form.threshold.value():
obj.Threshold = self.form.threshold.value()
self.updateToolController(obj, self.form.toolController)
def setFields(self, obj):
'''setFields(obj) ... transfers obj's property values to UI'''
self.form.discretize.setValue(obj.Discretize)
self.form.threshold.setValue(obj.Threshold)
self.setupToolController(obj, self.form.toolController)
def getSignalsForUpdate(self, obj):
'''getSignalsForUpdate(obj) ... return list of signals for updating obj'''
signals = []
signals.append(self.form.discretize.editingFinished)
signals.append(self.form.threshold.editingFinished)
signals.append(self.form.toolController.currentIndexChanged)
return signals
@@ -137,11 +143,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
'''taskPanelBaseGeometryPage(obj, features) ... return page for adding base geometries.'''
return TaskPanelBaseGeometryPage(obj, features)
Command = PathOpGui.SetupOperation('Vcarve',
PathVcarve.Create,
TaskPanelOpPage,
'Path-Vcarve',
QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Vcarve"),
Command = PathOpGui.SetupOperation('Vcarve', PathVcarve.Create, TaskPanelOpPage,
'Path-Vcarve', QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Vcarve"),
QtCore.QT_TRANSLATE_NOOP("PathVcarve", "Creates a medial line engraving path"),
PathVcarve.SetupProperties)