274 lines
12 KiB
Python
274 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2018 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 Part
|
|
import Path
|
|
import PathScripts.PathEngraveBase as PathEngraveBase
|
|
import PathScripts.PathGeom as PathGeom
|
|
import PathScripts.PathLog as PathLog
|
|
import PathScripts.PathOp as PathOp
|
|
import math
|
|
|
|
from PySide import QtCore
|
|
|
|
if True:
|
|
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
|
|
PathLog.trackModule(PathLog.thisModule())
|
|
else:
|
|
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
|
|
|
# Qt tanslation handling
|
|
def translate(context, text, disambig=None):
|
|
return QtCore.QCoreApplication.translate(context, text, disambig)
|
|
|
|
def orientWire(w, forward=True):
|
|
'''orientWire(w, forward=True) ... orients given wire in a specific direction.
|
|
If forward = True (the default) the wire is oriented clockwise, looking down the negative Z axis.
|
|
If forward = False the wire is oriented counter clockwise.
|
|
If forward = None the orientation is determined by the order in which the edges appear in the wire.'''
|
|
# first, we must ensure all edges are oriented the same way
|
|
# one would thing this is the way it should be, but it turns out it isn't
|
|
# on top of that, when creating a face the axis of the face seems to depend
|
|
# the axis of any included arcs, and not in the order of the edges
|
|
e0 = w.Edges[0]
|
|
# well, even the very first edge could be misoriented, so let's try and connect it to the second
|
|
if 1 < len(w.Edges):
|
|
last = e0.valueAt(e0.LastParameter)
|
|
e1 = w.Edges[1]
|
|
if not PathGeom.pointsCoincide(last, e1.valueAt(e1.FirstParameter)) and not PathGeom.pointsCoincide(last, e1.valueAt(e1.LastParameter)):
|
|
e0 = PathGeom.flipEdge(e0)
|
|
|
|
edges = [e0]
|
|
last = e0.valueAt(e0.LastParameter)
|
|
for e in w.Edges[1:]:
|
|
edge = e if PathGeom.pointsCoincide(last, e.valueAt(e.FirstParameter)) else PathGeom.flipEdge(e)
|
|
edges.append(edge)
|
|
last = edge.valueAt(edge.LastParameter)
|
|
wire = Part.Wire(edges)
|
|
if forward is not None:
|
|
# now that we have a wire where all edges are oriented in the same way which
|
|
# also matches their order - we can create a face and get it's axis to determine
|
|
# the orientation of the wire - which is all we need here
|
|
face = Part.Face(wire)
|
|
cw = 0 < face.Surface.Axis.z
|
|
if forward != cw:
|
|
PathLog.track('orientWire - needs flipping')
|
|
return PathGeom.flipWire(wire)
|
|
PathLog.track('orientWire - ok')
|
|
return wire
|
|
|
|
def isCircleAt(edge, center):
|
|
if Circel == type(edge.Curve) or ArcOfCircle == type(edge.Curve):
|
|
return PathGeom.pointsCoincide(edge.Curve.Center, center)
|
|
return False
|
|
|
|
def offsetWire(wire, base, offset, forward):
|
|
'''offsetWire(wire, base, offset, forward) ... offsets the wire away from base and orients the wire accordingly.
|
|
The function tries to avoid most of the pitfalls of Part.makeOffset2D which is possible because all offsetting
|
|
happens in the XY plane.
|
|
'''
|
|
PathLog.track('offsetWire')
|
|
|
|
if 1 == len(wire.Edges):
|
|
edge = wire.Edges[0]
|
|
curve = edge.Curve
|
|
if Part.Circle == type(curve) and wire.isClosed():
|
|
# it's a full circle and there are some problems with that, see
|
|
# http://www.freecadweb.org/wiki/Part%20Offset2D
|
|
# it's easy to construct them manually though
|
|
z = -1 if forward else 1
|
|
edge = Part.makeCircle(curve.Radius + offset, curve.Center, FreeCAD.Vector(0, 0, z))
|
|
if base.isInside(edge.Vertexes[0].Point, offset/2, True):
|
|
if offset > curve.Radius or PathGeom.isRoughly(offset, curve.Radius):
|
|
# offsetting a hole by its own radius (or more) makes the hole vanish
|
|
return None
|
|
edge = Part.makeCircle(curve.Radius - offset, curve.Center, FreeCAD.Vector(0, 0, -z))
|
|
w = Part.Wire([edge])
|
|
return w
|
|
if Part.Line == type(curve) or Part.LineSegment == type(curve):
|
|
# offsetting a single edge doesn't work because there is an infinite
|
|
# possible planes into which the edge could be offset
|
|
# luckily, the plane here must be the XY-plane ...
|
|
n = (edge.Vertexes[1].Point - edge.Vertexes[0].Point).cross(FreeCAD.Vector(0, 0, 1))
|
|
o = n.normalize() * offset
|
|
edge.translate(o)
|
|
if base.isInside(edge.valueAt((edge.FirstParameter + edge.LastParameter)/2), offset/2, True):
|
|
edge.translate(-2 * o)
|
|
w = Part.Wire([edge])
|
|
return orientWire(w, forward)
|
|
# if we get to this point the assumption is that makeOffset2D can deal with the edge
|
|
pass
|
|
|
|
w = wire.makeOffset2D(offset)
|
|
|
|
if wire.isClosed():
|
|
if not base.isInside(w.Edges[0].Vertexes[0].Point, offset/2, True):
|
|
PathLog.track('closed - outside')
|
|
return orientWire(w, forward)
|
|
PathLog.track('closed - inside')
|
|
try:
|
|
w = wire.makeOffset2D(-offset)
|
|
except:
|
|
# most likely offsetting didn't work because the wire is a hole
|
|
# and the offset is too big - making the hole vanish
|
|
return None
|
|
return orientWire(w, forward)
|
|
|
|
# An edge is considered to be inside of shape if the mid point is inside
|
|
# Of the remaining edges we take the longest wire to be the engraving side
|
|
# Looking for a circle with the start vertex as center marks and end
|
|
# starting from there follow the edges until a circle with the end vertex as center is found
|
|
# if the traversed edges include any oof the remainig from above, all those edges are remaining
|
|
# this is to also include edges which might partially be inside shape
|
|
# if they need to be discarded, split, that should happen in a post process
|
|
# Depending on the Axis of the circle, and which side remains we know if the wire needs to be flipped
|
|
|
|
# find edges that are not inside the shape
|
|
def isInside(edge):
|
|
if shape.Shape.isInside(edge.Vertexes[0].Point, offset/2, True) and shape.Shape.isInside(edge.Vertexes[-1].Point, offset/2, True):
|
|
return True
|
|
return False
|
|
outside = [e for e in edges if not isInside(e)]
|
|
# discard all edges that are not part of the longest wire
|
|
longestWire = None
|
|
for w in [Part.Wire(el) for el in Part.sortEdges(outside)]:
|
|
if not longestWire or longestWire.Length < w.Length:
|
|
longestWire = w
|
|
|
|
# find the start and end point
|
|
start = wire.Vertexes[0].Point
|
|
end = wire.Vertexes[-1].Point
|
|
|
|
collectLeft = False
|
|
collectRight = False
|
|
leftSideEdges = []
|
|
rightSideEdges = []
|
|
|
|
for e in (w.Edges + w.Edges):
|
|
if isCircleAt(e, start):
|
|
if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)):
|
|
if not collectLeft and leftSideEdges:
|
|
break
|
|
collectLeft = True
|
|
collectRight = False
|
|
else:
|
|
if not collectRight and rightSideEdges:
|
|
break
|
|
collectLeft = False
|
|
collectRight = True
|
|
elif isCircleAt(e, end):
|
|
if PathGeom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)):
|
|
if not collectRight and rightSideEdges:
|
|
break
|
|
collectLeft = False
|
|
collectRight = True
|
|
else:
|
|
if not collectLeft and leftSideEdges:
|
|
break
|
|
collectLeft = True
|
|
collectRight = False
|
|
elif collectLeft:
|
|
leftSideEdges.append(e)
|
|
elif collectRight:
|
|
rightSideEdges.append(e)
|
|
|
|
edges = leftSideEdges
|
|
for e in longestWire.Edges:
|
|
for e0 in rightSideEdges:
|
|
if PathGeom.edgesMatch(e, e0):
|
|
if forward:
|
|
edges = [PathGeom.flipEdge(edge) for edge in rightSideEdges]
|
|
return Part.Wire(edges)
|
|
|
|
if not forward:
|
|
edges = [PathGeom.flipEdge(edge) for edge in rightSideEdges]
|
|
return Part.Wire(edges)
|
|
|
|
class ObjectChamfer(PathEngraveBase.ObjectOp):
|
|
'''Proxy class for Chamfer operation.'''
|
|
|
|
def opFeatures(self, obj):
|
|
return PathOp.FeatureTool | PathOp.FeatureHeights | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces
|
|
|
|
def initOperation(self, obj):
|
|
obj.addProperty("App::PropertyDistance", "Width", "Chamfer", QtCore.QT_TRANSLATE_NOOP("PathChamfer", "The desired width of the chamfer"))
|
|
obj.addProperty("App::PropertyDistance", "ExtraDepth", "Chamfer", QtCore.QT_TRANSLATE_NOOP("PathChamfer", "The additional depth of the tool path"))
|
|
obj.addProperty("App::PropertyEnumeration", "Join", "Chamfer", QtCore.QT_TRANSLATE_NOOP("PathChamfer", "How to join chamfer segments"))
|
|
obj.Join = ['Round', 'Miter']
|
|
|
|
def opExecute(self, obj):
|
|
angle = self.tool.CuttingEdgeAngle
|
|
if 0 == angle:
|
|
angle = 180
|
|
tan = math.tan(math.radians(angle/2))
|
|
|
|
toolDepth = 0 if 0 == tan else obj.Width.Value / tan
|
|
extraDepth = obj.ExtraDepth.Value
|
|
depth = toolDepth + extraDepth
|
|
toolOffset = self.tool.FlatRadius
|
|
extraOffset = self.tool.Diameter/2 - obj.Width.Value if 180 == angle else obj.ExtraDepth.Value / tan
|
|
offset = toolOffset + extraOffset
|
|
|
|
self.basewires = []
|
|
self.adjusted_basewires = []
|
|
wires = []
|
|
for base, subs in obj.Base:
|
|
edges = []
|
|
basewires = []
|
|
for f in subs:
|
|
sub = base.Shape.getElement(f)
|
|
if type(sub) == Part.Edge:
|
|
edges.append(sub)
|
|
elif sub.Wires:
|
|
basewires.extend(sub.Wires)
|
|
else:
|
|
basewires.append(Part.Wire(sub.Edges))
|
|
self.edges = edges
|
|
for edgelist in Part.sortEdges(edges):
|
|
basewires.append(Part.Wire(edgelist))
|
|
|
|
self.basewires.extend(basewires)
|
|
|
|
for w in self.adjustWirePlacement(obj, base, basewires):
|
|
self.adjusted_basewires.append(w)
|
|
wire = offsetWire(w, base.Shape, offset, True)
|
|
if wire:
|
|
wires.append(wire)
|
|
|
|
self.wires = wires
|
|
self.buildpathocc(obj, wires, [depth], True)
|
|
|
|
def opSetDefaultValues(self, obj):
|
|
obj.Width = '1 mm'
|
|
obj.ExtraDepth = '0.1 mm'
|
|
obj.Join = 'Round'
|
|
|
|
def Create(name):
|
|
'''Create(name) ... Creates and returns a Chamfer operation.'''
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
|
proxy = ObjectChamfer(obj)
|
|
return obj
|
|
|