Files
create/src/Mod/Path/PathScripts/PathDressupDogbone.py
2018-11-04 14:50:04 -03:00

1083 lines
44 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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 *
# * *
# ***************************************************************************
from __future__ import print_function
import DraftGeomUtils
import FreeCAD
import FreeCADGui
import math
import Part
import Path
import PathScripts.PathDressup as PathDressup
import PathScripts.PathGeom as PathGeom
import PathScripts.PathLog as PathLog
import PathScripts.PathUtil as PathUtil
import PathScripts.PathUtils as PathUtils
from PySide import QtCore, QtGui
"""Dogbone Dressup object and FreeCAD command"""
LOG_MODULE = PathLog.thisModule()
if False:
PathLog.setLevel(PathLog.Level.DEBUG, LOG_MODULE)
PathLog.setLevel(PathLog.Level.DEBUG, LOG_MODULE)
else:
PathLog.setLevel(PathLog.Level.NOTICE, LOG_MODULE)
# Qt tanslation handling
def translate(context, text, disambig=None):
return QtCore.QCoreApplication.translate(context, text, disambig)
movecommands = ['G0', 'G00', 'G1', 'G01', 'G2', 'G02', 'G3', 'G03']
movestraight = ['G1', 'G01']
movecw = ['G2', 'G02']
moveccw = ['G3', 'G03']
movearc = movecw + moveccw
def debugMarker(vector, label, color=None, radius=0.5):
if PathLog.getLevel(LOG_MODULE) == PathLog.Level.DEBUG:
obj = FreeCAD.ActiveDocument.addObject("Part::Sphere", label)
obj.Label = label
obj.Radius = radius
obj.Placement = FreeCAD.Placement(vector, FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0))
if color:
obj.ViewObject.ShapeColor = color
def debugCircle(vector, r, label, color=None):
if PathLog.getLevel(LOG_MODULE) == PathLog.Level.DEBUG:
obj = FreeCAD.ActiveDocument.addObject("Part::Cylinder", label)
obj.Label = label
obj.Radius = r
obj.Height = 1
obj.Placement = FreeCAD.Placement(vector, FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0))
obj.ViewObject.Transparency = 90
if color:
obj.ViewObject.ShapeColor = color
def addAngle(a1, a2):
a = a1 + a2
while a <= -math.pi:
a += 2*math.pi
while a > math.pi:
a -= 2*math.pi
return a
def anglesAreParallel(a1, a2):
an1 = addAngle(a1, 0)
an2 = addAngle(a2, 0)
if an1 == an2:
return True
if an1 == addAngle(an2, math.pi):
return True
return False
def getAngle(v):
a = v.getAngle(FreeCAD.Vector(1, 0, 0))
if v.y < 0:
return -a
return a
def pointFromCommand(cmd, pt, X='X', Y='Y', Z='Z'):
x = cmd.Parameters.get(X, pt.x)
y = cmd.Parameters.get(Y, pt.y)
z = cmd.Parameters.get(Z, pt.z)
return FreeCAD.Vector(x, y, z)
def edgesForCommands(cmds, startPt):
edges = []
lastPt = startPt
for cmd in cmds:
if cmd.Name in movecommands:
pt = pointFromCommand(cmd, lastPt)
if cmd.Name in movestraight:
edges.append(Part.Edge(Part.LineSegment(lastPt, pt)))
elif cmd.Name in movearc:
center = lastPt + pointFromCommand(cmd, FreeCAD.Vector(0, 0, 0), 'I', 'J', 'K')
A = lastPt - center
B = pt - center
d = -B.x * A.y + B.y * A.x
if d == 0:
# we're dealing with half a circle here
angle = getAngle(A) + math.pi/2
if cmd.Name in movecw:
angle -= math.pi
else:
C = A + B
angle = getAngle(C)
R = (lastPt - center).Length
ptm = center + FreeCAD.Vector(math.cos(angle), math.sin(angle), 0) * R
edges.append(Part.Edge(Part.Arc(lastPt, ptm, pt)))
lastPt = pt
return edges
class Style:
Dogbone = 'Dogbone'
Tbone_H = 'T-bone horizontal'
Tbone_V = 'T-bone vertical'
Tbone_L = 'T-bone long edge'
Tbone_S = 'T-bone short edge'
All = [Dogbone, Tbone_H, Tbone_V, Tbone_L, Tbone_S]
class Side:
Left = 'Left'
Right = 'Right'
All = [Left, Right]
@classmethod
def oppositeOf(cls, side):
if side == cls.Left:
return cls.Right
if side == cls.Right:
return cls.Left
return None
class Incision:
Fixed = 'fixed'
Adaptive = 'adaptive'
Custom = 'custom'
All = [Adaptive, Fixed, Custom]
class Smooth:
Neither = 0
In = 1
Out = 2
InAndOut = In | Out
# Chord
# A class to represent the start and end point of a path command. If the underlying
# Command is a rotate command the receiver does represent a chord in the geometric
# sense of the word. If the underlying command is a straight move then the receiver
# represents the actual move.
# This implementation really only deals with paths in the XY plane. Z is assumed to
# be constant in all calculated results.
# Instances of Chord are generally considered immutable and all movement member
# functions return new instances.
class Chord (object):
def __init__(self, start=None, end=None):
if not start:
start = FreeCAD.Vector()
if not end:
end = FreeCAD.Vector()
self.Start = start
self.End = end
def __str__(self):
return "Chord([%g, %g, %g] -> [%g, %g, %g])" % (self.Start.x, self.Start.y, self.Start.z, self.End.x, self.End.y, self.End.z)
def moveTo(self, newEnd):
return Chord(self.End, newEnd)
def moveToParameters(self, params):
x = params.get('X', self.End.x)
y = params.get('Y', self.End.y)
z = params.get('Z', self.End.z)
return self.moveTo(FreeCAD.Vector(x, y, z))
def moveBy(self, x, y, z):
return self.moveTo(self.End + FreeCAD.Vector(x, y, z))
def move(self, distance, angle):
dx = distance * math.cos(angle)
dy = distance * math.sin(angle)
return self.moveBy(dx, dy, 0)
def asVector(self):
return self.End - self.Start
def asLine(self):
return Part.LineSegment(self.Start, self.End)
def asEdge(self):
return Part.Edge(self.asLine())
def getLength(self):
return self.asVector().Length
def getDirectionOfVector(self, B):
A = self.asVector()
# if the 2 vectors are identical, they head in the same direction
if PathGeom.pointsCoincide(A, B):
return 'Straight'
d = -A.x*B.y + A.y*B.x
if d < 0:
return Side.Left
if d > 0:
return Side.Right
# at this point the only direction left is backwards
return 'Back'
def getDirectionOf(self, chordOrVector):
if type(chordOrVector) is Chord:
return self.getDirectionOfVector(chordOrVector.asVector())
return self.getDirectionOfVector(chordOrVector)
def getAngleOfVector(self, ref):
angle = self.asVector().getAngle(ref)
# unfortunately they never figure out the sign :(
# positive angles go up, so when the reference vector is left
# then the receiver must go down
if self.getDirectionOfVector(ref) == Side.Left:
return -angle
return angle
def getAngle(self, refChordOrVector):
if type(refChordOrVector) is Chord:
return self.getAngleOfVector(refChordOrVector.asVector())
return self.getAngleOfVector(refChordOrVector)
def getAngleXY(self):
return self.getAngle(FreeCAD.Vector(1, 0, 0))
def commandParams(self, f):
params = {"X": self.End.x, "Y": self.End.y, "Z": self.End.z}
if f:
params['F'] = f
return params
def g1Command(self, f):
return Path.Command("G1", self.commandParams(f))
def arcCommand(self, cmd, center, f):
params = self.commandParams(f)
d = center - self.Start
params['I'] = d.x
params['J'] = d.y
params['K'] = 0
return Path.Command(cmd, params)
def g2Command(self, center, f):
return self.arcCommand("G2", center, f)
def g3Command(self, center, f):
return self.arcCommand("G3", center, f)
def isAPlungeMove(self):
return not PathGeom.isRoughly(self.End.z, self.Start.z)
def foldsBackOrTurns(self, chord, side):
dir = chord.getDirectionOf(self)
PathLog.info(" - direction = %s/%s" % (dir, side))
return dir == 'Back' or dir == side
def connectsTo(self, chord):
return PathGeom.pointsCoincide(self.End, chord.Start)
class Bone:
def __init__(self, boneId, obj, lastCommand, inChord, outChord, smooth, F):
self.obj = obj
self.boneId = boneId
self.lastCommand = lastCommand
self.inChord = inChord
self.outChord = outChord
self.smooth = smooth
self.smooth = Smooth.Neither
self.F = F
def angle(self):
if not hasattr(self, 'cAngle'):
baseAngle = self.inChord.getAngleXY()
turnAngle = self.outChord.getAngle(self.inChord)
angle = addAngle(baseAngle, (turnAngle - math.pi)/2)
if self.obj.Side == Side.Left:
angle = addAngle(angle, math.pi)
self.tAngle = turnAngle
self.cAngle = angle
return self.cAngle
def distance(self, toolRadius):
if not hasattr(self, 'cDist'):
self.angle() # make sure the angles are initialized
self.cDist = toolRadius / math.cos(self.tAngle/2)
return self.cDist
def corner(self, toolRadius):
if not hasattr(self, 'cPt'):
self.cPt = self.inChord.move(self.distance(toolRadius), self.angle()).End
return self.cPt
def location(self):
return (self.inChord.End.x, self.inChord.End.y)
def adaptiveLength(self, boneAngle, toolRadius):
angle = self.angle()
distance = self.distance(toolRadius)
# there is something weird happening if the boneAngle came from a horizontal/vertical t-bone
# for some reason pi/2 is not equal to pi/2
if math.fabs(angle - boneAngle) < 0.00001:
# moving directly towards the corner
PathLog.debug("adaptive - on target: %.2f - %.2f" % (distance, toolRadius))
return distance - toolRadius
PathLog.debug("adaptive - angles: corner=%.2f bone=%.2f diff=%.12f" % (angle/math.pi, boneAngle/math.pi, angle - boneAngle))
# The bones root and end point form a triangle with the intersection of the tool path
# with the toolRadius circle around the bone end point.
# In case the math looks questionable, look for "triangle ssa"
# c = distance
# b = self.toolRadius
# beta = fabs(boneAngle - angle)
beta = math.fabs(addAngle(boneAngle, -angle))
D = (distance / toolRadius) * math.sin(beta)
if D > 1: # no intersection
PathLog.debug("adaptive - no intersection - no bone")
return 0
gamma = math.asin(D)
alpha = math.pi - beta - gamma
length = toolRadius * math.sin(alpha) / math.sin(beta)
if D < 1 and toolRadius < distance: # there exists a second solution
beta2 = beta
gamma2 = math.pi - gamma
alpha2 = math.pi - beta2 - gamma2
length2 = toolRadius * math.sin(alpha2) / math.sin(beta2)
length = min(length, length2)
PathLog.debug("adaptive corner=%.2f * %.2f˚ -> bone=%.2f * %.2f˚" % (distance, angle, length, boneAngle))
return length
class ObjectDressup:
def __init__(self, obj, base):
# Tool Properties
obj.addProperty("App::PropertyLink", "Base", "Base", QtCore.QT_TRANSLATE_NOOP("Path_DressupDogbone", "The base path to modify"))
obj.addProperty("App::PropertyEnumeration", "Side", "Dressup", QtCore.QT_TRANSLATE_NOOP("Path_DressupDogbone", "The side of path to insert bones"))
obj.Side = [Side.Left, Side.Right]
obj.Side = Side.Right
obj.addProperty("App::PropertyEnumeration", "Style", "Dressup", QtCore.QT_TRANSLATE_NOOP("Path_DressupDogbone", "The style of bones"))
obj.Style = Style.All
obj.Style = Style.Dogbone
obj.addProperty("App::PropertyIntegerList", "BoneBlacklist", "Dressup", QtCore.QT_TRANSLATE_NOOP("Path_DressupDogbone", "Bones that aren't dressed up"))
obj.BoneBlacklist = []
obj.setEditorMode('BoneBlacklist', 2) # hide this one
obj.addProperty("App::PropertyEnumeration", "Incision", "Dressup", QtCore.QT_TRANSLATE_NOOP("Path_DressupDogbone", "The algorithm to determine the bone length"))
obj.Incision = Incision.All
obj.Incision = Incision.Adaptive
obj.addProperty("App::PropertyFloat", "Custom", "Dressup", QtCore.QT_TRANSLATE_NOOP("Path_DressupDogbone", "Dressup length if Incision == custom"))
obj.Custom = 0.0
obj.Proxy = self
obj.Base = base
def onDocumentRestored(self, obj):
obj.setEditorMode('BoneBlacklist', 2) # hide this one
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def theOtherSideOf(self, side):
if side == Side.Left:
return Side.Right
return Side.Left
# Answer true if a dogbone could be on either end of the chord, given its command
def canAttachDogbone(self, cmd, chord):
return cmd.Name in movestraight and not chord.isAPlungeMove()
def shouldInsertDogbone(self, obj, inChord, outChord):
return outChord.foldsBackOrTurns(inChord, self.theOtherSideOf(obj.Side))
def findPivotIntersection(self, pivot, pivotEdge, edge, refPt, d, color):
PathLog.track("(%.2f, %.2f)^%.2f - [(%.2f, %.2f), (%.2f, %.2f)]" % (pivotEdge.Curve.Center.x, pivotEdge.Curve.Center.y, pivotEdge.Curve.Radius, edge.Vertexes[0].Point.x, edge.Vertexes[0].Point.y, edge.Vertexes[1].Point.x, edge.Vertexes[1].Point.y))
ppt = None
pptDistance = 0
for pt in DraftGeomUtils.findIntersection(edge, pivotEdge, dts=False):
# debugMarker(pt, "pti.%d-%s.in" % (self.boneId, d), color, 0.2)
distance = (pt - refPt).Length
PathLog.debug(" --> (%.2f, %.2f): %.2f" % (pt.x, pt.y, distance))
if not ppt or pptDistance < distance:
ppt = pt
pptDistance = distance
if not ppt:
tangent = DraftGeomUtils.findDistance(pivot, edge)
if tangent:
PathLog.debug("Taking tangent as intersect %s" % tangent)
ppt = pivot + tangent
else:
PathLog.debug("Taking chord start as intersect %s" % edge.Vertexes[0].Point)
ppt = edge.Vertexes[0].Point
# debugMarker(ppt, "ptt.%d-%s.in" % (self.boneId, d), color, 0.2)
PathLog.debug(" --> (%.2f, %.2f)" % (ppt.x, ppt.y))
return ppt
def pointIsOnEdge(self, point, edge):
param = edge.Curve.parameter(point)
return edge.FirstParameter <= param <= edge.LastParameter
def smoothChordCommands(self, bone, inChord, outChord, edge, wire, corner, smooth, color=None):
if smooth == 0:
PathLog.info(" No smoothing requested")
return [bone.lastCommand, outChord.g1Command(bone.F)]
d = 'in'
refPoint = inChord.Start
if smooth == Smooth.Out:
d = 'out'
refPoint = outChord.End
if DraftGeomUtils.areColinear(inChord.asEdge(), outChord.asEdge()):
PathLog.info(" straight edge %s" % d)
return [outChord.g1Command(bone.F)]
pivot = None
pivotDistance = 0
PathLog.info("smooth: (%.2f, %.2f)-(%.2f, %.2f)" % (edge.Vertexes[0].Point.x, edge.Vertexes[0].Point.y, edge.Vertexes[1].Point.x, edge.Vertexes[1].Point.y))
for e in wire.Edges:
self.dbg.append(e)
if type(e.Curve) == Part.LineSegment or type(e.Curve) == Part.Line:
PathLog.debug(" (%.2f, %.2f)-(%.2f, %.2f)" % (e.Vertexes[0].Point.x, e.Vertexes[0].Point.y, e.Vertexes[1].Point.x, e.Vertexes[1].Point.y))
else:
PathLog.debug(" (%.2f, %.2f)^%.2f" % (e.Curve.Center.x, e.Curve.Center.y, e.Curve.Radius))
for pt in DraftGeomUtils.findIntersection(edge, e, True, findAll=True):
if not PathGeom.pointsCoincide(pt, corner) and self.pointIsOnEdge(pt, e):
# debugMarker(pt, "candidate-%d-%s" % (self.boneId, d), color, 0.05)
PathLog.debug(" -> candidate")
distance = (pt - refPoint).Length
if not pivot or pivotDistance > distance:
pivot = pt
pivotDistance = distance
else:
PathLog.debug(" -> corner intersect")
if pivot:
# debugCircle(pivot, self.toolRadius, "pivot.%d-%s" % (self.boneId, d), color)
pivotEdge = Part.Edge(Part.Circle(pivot, FreeCAD.Vector(0, 0, 1), self.toolRadius))
t1 = self.findPivotIntersection(pivot, pivotEdge, inChord.asEdge(), inChord.End, d, color)
t2 = self.findPivotIntersection(pivot, pivotEdge, outChord.asEdge(), inChord.End, d, color)
commands = []
if not PathGeom.pointsCoincide(t1, inChord.Start):
PathLog.debug(" add lead in")
commands.append(Chord(inChord.Start, t1).g1Command(bone.F))
if bone.obj.Side == Side.Left:
PathLog.debug(" add g3 command")
commands.append(Chord(t1, t2).g3Command(pivot, bone.F))
else:
PathLog.debug(" add g2 command center=(%.2f, %.2f) -> from (%2f, %.2f) to (%.2f, %.2f" % (pivot.x, pivot.y, t1.x, t1.y, t2.x, t2.y))
commands.append(Chord(t1, t2).g2Command(pivot, bone.F))
if not PathGeom.pointsCoincide(t2, outChord.End):
PathLog.debug(" add lead out")
commands.append(Chord(t2, outChord.End).g1Command(bone.F))
# debugMarker(pivot, "pivot.%d-%s" % (self.boneId, d), color, 0.2)
# debugMarker(t1, "pivot.%d-%s.in" % (self.boneId, d), color, 0.1)
# debugMarker(t2, "pivot.%d-%s.out" % (self.boneId, d), color, 0.1)
return commands
PathLog.info(" no pivot found - straight command")
return [inChord.g1Command(bone.F), outChord.g1Command(bone.F)]
def inOutBoneCommands(self, bone, boneAngle, fixedLength):
corner = bone.corner(self.toolRadius)
bone.tip = bone.inChord.End # in case there is no bone
PathLog.debug("corner = (%.2f, %.2f)" % (corner.x, corner.y))
# debugMarker(corner, 'corner', (1., 0., 1.), self.toolRadius)
length = fixedLength
if bone.obj.Incision == Incision.Custom:
length = bone.obj.Custom
if bone.obj.Incision == Incision.Adaptive:
length = bone.adaptiveLength(boneAngle, self.toolRadius)
if length == 0:
PathLog.info("no bone after all ..")
return [bone.lastCommand, bone.outChord.g1Command(bone.F)]
boneInChord = bone.inChord.move(length, boneAngle)
boneOutChord = boneInChord.moveTo(bone.outChord.Start)
# debugCircle(boneInChord.Start, self.toolRadius, 'boneStart')
# debugCircle(boneInChord.End, self.toolRadius, 'boneEnd')
bone.tip = boneInChord.End
if bone.smooth == 0:
return [bone.lastCommand, boneInChord.g1Command(bone.F), boneOutChord.g1Command(bone.F), bone.outChord.g1Command(bone.F)]
# reconstruct the corner and convert to an edge
offset = corner - bone.inChord.End
iChord = Chord(bone.inChord.Start + offset, bone.inChord.End + offset)
oChord = Chord(bone.outChord.Start + offset, bone.outChord.End + offset)
iLine = iChord.asLine()
oLine = oChord.asLine()
cornerShape = Part.Shape([iLine, oLine])
# construct a shape representing the cut made by the bone
vt0 = FreeCAD.Vector(0, self.toolRadius, 0)
vt1 = FreeCAD.Vector(length, self.toolRadius, 0)
vb0 = FreeCAD.Vector(0, -self.toolRadius, 0)
vb1 = FreeCAD.Vector(length, -self.toolRadius, 0)
vm2 = FreeCAD.Vector(length + self.toolRadius, 0, 0)
boneBot = Part.LineSegment(vb1, vb0)
boneLid = Part.LineSegment(vb0, vt0)
boneTop = Part.LineSegment(vt0, vt1)
# what we actually want is an Arc - but findIntersect only returns the coincident if one exists
# which really sucks because that's the one we're probably not interested in ....
boneArc = Part.Arc(vt1, vm2, vb1)
# boneArc = Part.Circle(FreeCAD.Vector(length, 0, 0), FreeCAD.Vector(0,0,1), self.toolRadius)
boneWire = Part.Shape([boneTop, boneArc, boneBot, boneLid])
boneWire.rotate(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, 1), boneAngle * 180 / math.pi)
boneWire.translate(bone.inChord.End)
self.boneShapes = [cornerShape, boneWire]
bone.inCommands = self.smoothChordCommands(bone, bone.inChord, boneInChord, Part.Edge(iLine), boneWire, corner, bone.smooth & Smooth.In, (1., 0., 0.))
bone.outCommands = self.smoothChordCommands(bone, boneOutChord, bone.outChord, Part.Edge(oLine), boneWire, corner, bone.smooth & Smooth.Out, (0., 1., 0.))
return bone.inCommands + bone.outCommands
def dogbone(self, bone):
boneAngle = bone.angle()
length = self.toolRadius * 0.41422 # 0.41422 = 2/sqrt(2) - 1 + (a tiny bit)
return self.inOutBoneCommands(bone, boneAngle, length)
def tboneHorizontal(self, bone):
angle = bone.angle()
boneAngle = 0
if PathGeom.isRoughly(angle, math.pi) or math.fabs(angle) > math.pi/2:
boneAngle = -math.pi
return self.inOutBoneCommands(bone, boneAngle, self.toolRadius)
def tboneVertical(self, bone):
angle = bone.angle()
boneAngle = math.pi/2
if PathGeom.isRoughly(angle, math.pi) or angle < 0:
boneAngle = -boneAngle
return self.inOutBoneCommands(bone, boneAngle, self.toolRadius)
def tboneEdgeCommands(self, bone, onIn):
if onIn:
boneAngle = bone.inChord.getAngleXY()
else:
boneAngle = bone.outChord.getAngleXY()
if Side.Right == bone.outChord.getDirectionOf(bone.inChord):
boneAngle = boneAngle - math.pi/2
else:
boneAngle = boneAngle + math.pi/2
onInString = 'out'
if onIn:
onInString = 'in'
PathLog.debug("tboneEdge boneAngle[%s]=%.2f (in=%.2f, out=%.2f)" % (onInString, boneAngle/math.pi, bone.inChord.getAngleXY()/math.pi, bone.outChord.getAngleXY()/math.pi))
return self.inOutBoneCommands(bone, boneAngle, self.toolRadius)
def tboneLongEdge(self, bone):
inChordIsLonger = bone.inChord.getLength() > bone.outChord.getLength()
return self.tboneEdgeCommands(bone, inChordIsLonger)
def tboneShortEdge(self, bone):
inChordIsShorter = bone.inChord.getLength() < bone.outChord.getLength()
return self.tboneEdgeCommands(bone, inChordIsShorter)
def boneIsBlacklisted(self, bone):
blacklisted = False
parentConsumed = False
if bone.boneId in bone.obj.BoneBlacklist:
blacklisted = True
elif bone.location() in self.locationBlacklist:
bone.obj.BoneBlacklist.append(bone.boneId)
blacklisted = True
elif hasattr(bone.obj.Base, 'BoneBlacklist'):
parentConsumed = bone.boneId not in bone.obj.Base.BoneBlacklist
blacklisted = parentConsumed
if blacklisted:
self.locationBlacklist.add(bone.location())
return (blacklisted, parentConsumed)
# Generate commands necessary to execute the dogbone
def boneCommands(self, bone, enabled):
if enabled:
if bone.obj.Style == Style.Dogbone:
return self.dogbone(bone)
if bone.obj.Style == Style.Tbone_H:
return self.tboneHorizontal(bone)
if bone.obj.Style == Style.Tbone_V:
return self.tboneVertical(bone)
if bone.obj.Style == Style.Tbone_L:
return self.tboneLongEdge(bone)
if bone.obj.Style == Style.Tbone_S:
return self.tboneShortEdge(bone)
else:
return [bone.lastCommand, bone.outChord.g1Command(bone.F)]
def insertBone(self, bone):
PathLog.debug(">----------------------------------- %d --------------------------------------" % bone.boneId)
self.boneShapes = []
blacklisted, inaccessible = self.boneIsBlacklisted(bone)
enabled = not blacklisted
self.bones.append((bone.boneId, bone.location(), enabled, inaccessible))
self.boneId = bone.boneId
if False and PathLog.getLevel(LOG_MODULE) == PathLog.Level.DEBUG and bone.boneId > 2:
commands = self.boneCommands(bone, False)
else:
commands = self.boneCommands(bone, enabled)
bone.commands = commands
self.shapes[bone.boneId] = self.boneShapes
PathLog.debug("<----------------------------------- %d --------------------------------------" % bone.boneId)
return commands
def removePathCrossing(self, commands, bone1, bone2):
commands.append(bone2.lastCommand)
bones = bone2.commands
if True and hasattr(bone1, "outCommands") and hasattr(bone2, "inCommands"):
inEdges = edgesForCommands(bone1.outCommands, bone1.tip)
outEdges = edgesForCommands(bone2.inCommands, bone2.inChord.Start)
for i in range(len(inEdges)):
e1 = inEdges[i]
for j in range(len(outEdges)-1, -1, -1):
e2 = outEdges[j]
cutoff = DraftGeomUtils.findIntersection(e1, e2)
for pt in cutoff:
# debugCircle(e1.Curve.Center, e1.Curve.Radius, "bone.%d-1" % (self.boneId), (1.,0.,0.))
# debugCircle(e2.Curve.Center, e2.Curve.Radius, "bone.%d-2" % (self.boneId), (0.,1.,0.))
if PathGeom.pointsCoincide(pt, e1.valueAt(e1.LastParameter)) or PathGeom.pointsCoincide(pt, e2.valueAt(e2.FirstParameter)):
continue
# debugMarker(pt, "it", (0.0, 1.0, 1.0))
# 1. remove all redundant commands
commands = commands[:-(len(inEdges) - i)]
# 2., correct where c1 ends
c1 = bone1.outCommands[i]
c1Params = c1.Parameters
c1Params.update({'X': pt.x, 'Y': pt.y, 'Z': pt.z})
c1 = Path.Command(c1.Name, c1Params)
commands.append(c1)
# 3. change where c2 starts, this depends on the command itself
c2 = bone2.inCommands[j]
if c2.Name in movearc:
center = e2.Curve.Center
offset = center - pt
c2Params = c2.Parameters
c2Params.update({'I': offset.x, 'J': offset.y, 'K': offset.z})
c2 = Path.Command(c2.Name, c2Params)
bones = [c2]
bones.extend(bone2.commands[j+1:])
else:
bones = bone2.commands[j:]
# there can only be the one ...
return commands, bones
return commands, bones
def execute(self, obj, forReal=True):
if not obj.Base:
return
if forReal and not obj.Base.isDerivedFrom("Path::Feature"):
return
if not obj.Base.Path:
return
if not obj.Base.Path.Commands:
return
self.setup(obj, False)
commands = [] # the dressed commands
lastChord = Chord() # the last chord
lastCommand = None # the command that generated the last chord
lastBone = None # track last bone for optimizations
oddsAndEnds = [] # track chords that are connected to plunges - in case they form a loop
boneId = 1
self.bones = []
self.locationBlacklist = set()
# boneIserted = False
for (i, thisCommand) in enumerate(obj.Base.Path.Commands):
# if i > 14:
# if lastCommand:
# commands.append(lastCommand)
# lastCommand = None
# commands.append(thisCommand)
# continue
PathLog.info("%3d: %s" % (i, thisCommand))
if thisCommand.Name in movecommands:
thisChord = lastChord.moveToParameters(thisCommand.Parameters)
thisIsACandidate = self.canAttachDogbone(thisCommand, thisChord)
if thisIsACandidate and lastCommand and self.shouldInsertDogbone(obj, lastChord, thisChord):
PathLog.info(" Found bone corner")
bone = Bone(boneId, obj, lastCommand, lastChord, thisChord, Smooth.InAndOut, thisCommand.Parameters.get('F'))
bones = self.insertBone(bone)
boneId += 1
if lastBone:
PathLog.info(" removing potential path crossing")
# debugMarker(thisChord.Start, "it", (1.0, 0.0, 1.0))
commands, bones = self.removePathCrossing(commands, lastBone, bone)
commands.extend(bones[:-1])
lastCommand = bones[-1]
lastBone = bone
elif lastCommand and thisChord.isAPlungeMove():
PathLog.info(" Looking for connection in odds and ends")
haveNewLastCommand = False
for chord in (chord for chord in oddsAndEnds if lastChord.connectsTo(chord)):
if self.shouldInsertDogbone(obj, lastChord, chord):
PathLog.info(" and there is one")
bone = Bone(boneId, obj, lastCommand, lastChord, chord, Smooth.In, lastCommand.Parameters.get('F'))
bones = self.insertBone(bone)
boneId += 1
if lastBone:
PathLog.info(" removing potential path crossing")
# debugMarker(chord.Start, "it", (0.0, 1.0, 1.0))
commands, bones = self.removePathCrossing(commands, lastBone, bone)
commands.extend(bones[:-1])
lastCommand = bones[-1]
haveNewLastCommand = True
if not haveNewLastCommand:
commands.append(lastCommand)
lastCommand = None
commands.append(thisCommand)
lastBone = None
elif thisIsACandidate:
PathLog.info(" is a candidate, keeping for later")
if lastCommand:
commands.append(lastCommand)
lastCommand = thisCommand
lastBone = None
else:
PathLog.info(" nope")
if lastCommand:
commands.append(lastCommand)
lastCommand = None
commands.append(thisCommand)
lastBone = None
if lastChord.isAPlungeMove() and thisIsACandidate:
PathLog.info(" adding to odds and ends")
oddsAndEnds.append(thisChord)
lastChord = thisChord
else:
PathLog.info(" Clean slate")
if lastCommand:
commands.append(lastCommand)
lastCommand = None
commands.append(thisCommand)
lastBone = None
# for cmd in commands:
# PathLog.debug("cmd = '%s'" % cmd)
path = Path.Path(commands)
obj.Path = path
def setup(self, obj, initial):
PathLog.info("Here we go ... ")
if initial:
if hasattr(obj.Base, "BoneBlacklist"):
# dressing up a bone dressup
obj.Side = obj.Base.Side
else:
PathLog.info("Default side = right")
# otherwise dogbones are opposite of the base path's side
side = Side.Right
if hasattr(obj.Base, 'Side') and obj.Base.Side == 'Inside':
PathLog.info("inside -> side = left")
side = Side.Left
else:
PathLog.info("not inside -> side stays right")
if hasattr(obj.Base, 'Direction') and obj.Base.Direction == 'CCW':
PathLog.info("CCW -> switch sides")
side = Side.oppositeOf(side)
else:
PathLog.info("CW -> stay on side")
obj.Side = side
self.toolRadius = 5
tc = PathDressup.toolController(obj.Base)
if tc is None or tc.ToolNumber == 0:
self.toolRadius = 5
else:
tool = tc.Proxy.getTool(tc) # PathUtils.getTool(obj, tc.ToolNumber)
if not tool or tool.Diameter == 0:
self.toolRadius = 5
else:
self.toolRadius = tool.Diameter / 2
self.shapes = {}
self.dbg = []
def boneStateList(self, obj):
state = {}
# If the receiver was loaded from file, then it never generated the bone list.
if not hasattr(self, 'bones'):
self.execute(obj)
for (id, loc, enabled, inaccessible) in self.bones:
item = state.get(loc)
if item:
item[2].append(id)
else:
state[loc] = (enabled, inaccessible, [id])
return state
class TaskPanel:
DataIds = QtCore.Qt.ItemDataRole.UserRole
DataKey = QtCore.Qt.ItemDataRole.UserRole + 1
def __init__(self, obj):
self.obj = obj
self.form = FreeCADGui.PySideUic.loadUi(":/panels/DogboneEdit.ui")
FreeCAD.ActiveDocument.openTransaction(translate("Path_DressupDogbone", "Edit Dogbone Dress-up"))
def reject(self):
FreeCAD.ActiveDocument.abortTransaction()
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.recompute()
FreeCADGui.Selection.removeObserver(self.s)
def accept(self):
self.getFields()
FreeCAD.ActiveDocument.commitTransaction()
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.recompute()
FreeCADGui.Selection.removeObserver(self.s)
FreeCAD.ActiveDocument.recompute()
def getFields(self):
self.obj.Style = str(self.form.styleCombo.currentText())
self.obj.Side = str(self.form.sideCombo.currentText())
self.obj.Incision = str(self.form.incisionCombo.currentText())
self.obj.Custom = self.form.custom.value()
blacklist = []
for i in range(0, self.form.bones.count()):
item = self.form.bones.item(i)
if item.checkState() == QtCore.Qt.CheckState.Unchecked:
blacklist.extend(item.data(self.DataIds))
self.obj.BoneBlacklist = sorted(blacklist)
self.obj.Proxy.execute(self.obj)
def updateBoneList(self):
itemList = []
for loc, (enabled, inaccessible, ids) in PathUtil.keyValueIter(self.obj.Proxy.boneStateList(self.obj)):
lbl = '(%.2f, %.2f): %s' % (loc[0], loc[1], ','.join(str(id) for id in ids))
item = QtGui.QListWidgetItem(lbl)
if enabled:
item.setCheckState(QtCore.Qt.CheckState.Checked)
else:
item.setCheckState(QtCore.Qt.CheckState.Unchecked)
flags = QtCore.Qt.ItemFlag.ItemIsSelectable
if not inaccessible:
flags |= QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable
item.setFlags(flags)
item.setData(self.DataIds, ids)
item.setData(self.DataKey, ids[0])
itemList.append(item)
self.form.bones.clear()
for item in sorted(itemList, key=lambda item: item.data(self.DataKey)):
self.form.bones.addItem(item)
def updateUI(self):
customSelected = self.obj.Incision == Incision.Custom
self.form.custom.setEnabled(customSelected)
self.form.customLabel.setEnabled(customSelected)
self.updateBoneList()
if PathLog.getLevel(LOG_MODULE) == PathLog.Level.DEBUG:
for obj in FreeCAD.ActiveDocument.Objects:
if obj.Name.startswith('Shape'):
FreeCAD.ActiveDocument.removeObject(obj.Name)
print('object name %s' % self.obj.Name)
if hasattr(self.obj.Proxy, "shapes"):
PathLog.info("showing shapes attribute")
for shapes in self.obj.Proxy.shapes.itervalues():
for shape in shapes:
Part.show(shape)
else:
PathLog.info("no shapes attribute found")
def updateModel(self):
self.getFields()
self.updateUI()
FreeCAD.ActiveDocument.recompute()
def setupCombo(self, combo, text, items):
if items and len(items) > 0:
for i in range(combo.count(), -1, -1):
combo.removeItem(i)
combo.addItems(items)
index = combo.findText(text, QtCore.Qt.MatchFixedString)
if index >= 0:
combo.setCurrentIndex(index)
def setFields(self):
self.setupCombo(self.form.styleCombo, self.obj.Style, Style.All)
self.setupCombo(self.form.sideCombo, self.obj.Side, Side.All)
self.setupCombo(self.form.incisionCombo, self.obj.Incision, Incision.All)
self.form.custom.setMinimum(0.0)
self.form.custom.setDecimals(3)
self.form.custom.setValue(self.obj.Custom)
self.updateUI()
def open(self):
self.s = SelObserver()
# install the function mode resident
FreeCADGui.Selection.addObserver(self.s)
def setupUi(self):
self.setFields()
# now that the form is filled, setup the signal handlers
self.form.styleCombo.currentIndexChanged.connect(self.updateModel)
self.form.sideCombo.currentIndexChanged.connect(self.updateModel)
self.form.incisionCombo.currentIndexChanged.connect(self.updateModel)
self.form.custom.valueChanged.connect(self.updateModel)
self.form.bones.itemChanged.connect(self.updateModel)
class SelObserver:
def __init__(self):
import PathScripts.PathSelection as PST
PST.eselect()
def __del__(self):
import PathScripts.PathSelection as PST
PST.clear()
def addSelection(self, doc, obj, sub, pnt):
FreeCADGui.doCommand('Gui.Selection.addSelection(FreeCAD.ActiveDocument.' + obj + ')')
FreeCADGui.updateGui()
class ViewProviderDressup:
def __init__(self, vobj):
vobj.Proxy = self
def attach(self, vobj):
self.obj = vobj.Object
if self.obj and self.obj.Base:
for i in self.obj.Base.InList:
if hasattr(i, "Group"):
group = i.Group
for g in group:
if g.Name == self.obj.Base.Name:
group.remove(g)
i.Group = group
# FreeCADGui.ActiveDocument.getObject(obj.Base.Name).Visibility = False
return
def claimChildren(self):
return [self.obj.Base]
def setEdit(self, vobj, mode=0):
FreeCADGui.Control.closeDialog()
panel = TaskPanel(vobj.Object)
FreeCADGui.Control.showDialog(panel)
panel.setupUi()
return True
def __getstate__(self):
return None
def __setstate__(self, state):
return None
def onDelete(self, arg1=None, arg2=None):
'''this makes sure that the base operation is added back to the project and visible'''
FreeCADGui.ActiveDocument.getObject(arg1.Object.Base.Name).Visibility = True
job = PathUtils.findParentJob(arg1.Object)
job.Proxy.addOperation(arg1.Object.Base, arg1.Object)
arg1.Object.Base = None
return True
def Create(base, name='DogboneDressup'):
'''
Create(obj, name='DogboneDressup') ... dresses the given PathProfile/PathContour object with dogbones.
'''
obj = FreeCAD.ActiveDocument.addObject('Path::FeaturePython', 'DogboneDressup')
dbo = ObjectDressup(obj, base)
job = PathUtils.findParentJob(base)
job.Proxy.addOperation(obj, base)
if FreeCAD.GuiUp:
ViewProviderDressup(obj.ViewObject)
obj.Base.ViewObject.Visibility = False
dbo.setup(obj, True)
return obj
class CommandDressupDogbone:
def GetResources(self):
return {'Pixmap': 'Path-Dressup',
'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_DressupDogbone", "Dogbone Dress-up"),
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_DressupDogbone", "Creates a Dogbone Dress-up object from a selected path")}
def IsActive(self):
if FreeCAD.ActiveDocument is not None:
for o in FreeCAD.ActiveDocument.Objects:
if o.Name[:3] == "Job":
return True
return False
def Activated(self):
# check that the selection contains exactly what we want
selection = FreeCADGui.Selection.getSelection()
if len(selection) != 1:
FreeCAD.Console.PrintError(translate("Path_DressupDogbone", "Please select one path object")+"\n")
return
baseObject = selection[0]
if not baseObject.isDerivedFrom("Path::Feature"):
FreeCAD.Console.PrintError(translate("Path_DressupDogbone", "The selected object is not a path")+"\n")
return
# everything ok!
FreeCAD.ActiveDocument.openTransaction(translate("Path_DressupDogbone", "Create Dogbone Dress-up"))
FreeCADGui.addModule('PathScripts.PathDressupDogbone')
FreeCADGui.doCommand("PathScripts.PathDressupDogbone.Create(FreeCAD.ActiveDocument.%s)" % baseObject.Name)
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
if FreeCAD.GuiUp:
# register the FreeCAD command
FreeCADGui.addCommand('Path_DressupDogbone', CommandDressupDogbone())
FreeCAD.Console.PrintLog("Loading DressupDogbone... done\n")