Skip to content

Commit

Permalink
Extract Path Array logic into separate functions
Browse files Browse the repository at this point in the history
The Path Array logic is pretty useful to be used directly by macros.
But until now it was encapsulated in the PathArray class and could only
be used by creating a full PathArray object. So we can't use it inside
other Objects to create a repeated shape.

This commit extracts the logic into a separate function that returns a
list of placements. One can use this placements now to copy a shape and
place it accordingly.
  • Loading branch information
furti authored and yorikvanhavre committed May 8, 2019
1 parent aaae829 commit 9cfb84d
Showing 1 changed file with 174 additions and 113 deletions.
287 changes: 174 additions & 113 deletions src/Mod/Draft/Draft.py
Expand Up @@ -3372,6 +3372,170 @@ def makeWorkingPlaneProxy(placement):
obj.Placement = placement
return obj

def getParameterFromV0(edge, offset):
'''return parameter at distance offset from edge.Vertexes[0]'''
'''sb method in Part.TopoShapeEdge???'''

lpt = edge.valueAt(edge.getParameterByLength(0))
vpt = edge.Vertexes[0].Point

if not DraftVecUtils.equals(vpt, lpt):
# this edge is flipped
length = edge.Length - offset
else:
# this edge is right way around
length = offset

return (edge.getParameterByLength(length))


def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal=None):
'''Orient shape to tangent at parm offset along edge.'''
import functools
# http://en.wikipedia.org/wiki/Euler_angles
# start with null Placement point so translate goes to right place.
placement = FreeCAD.Placement()
# preserve global orientation
placement.Rotation = globalRotation

placement.move(RefPt + xlate)

if not align:
return placement

# unit +Z Probably defined elsewhere?
z = FreeCAD.Vector(0, 0, 1)
# y = FreeCAD.Vector(0, 1, 0) # unit +Y
x = FreeCAD.Vector(1, 0, 0) # unit +X
nullv = FreeCAD.Vector(0, 0, 0)

# get local coord system - tangent, normal, binormal, if possible
t = edge.tangentAt(getParameterFromV0(edge, offset))
t.normalize()

try:
if normal:
n = normal
else:
n = edge.normalAt(getParameterFromV0(edge, offset))
n.normalize()
b = (t.cross(n))
b.normalize()
# no normal defined here
except FreeCAD.Base.FreeCADError:
n = nullv
b = nullv
FreeCAD.Console.PrintLog(
"Draft PathArray.orientShape - Cannot calculate Path normal.\n")

lnodes = z.cross(b)

try:
# Can't normalize null vector.
lnodes.normalize()
except:
# pathological cases:
pass
# 1) can't determine normal, don't align.
if n == nullv:
psi = 0.0
theta = 0.0
phi = 0.0
FreeCAD.Console.PrintWarning(
"Draft PathArray.orientShape - Path normal is Null. Cannot align.\n")
elif abs(b.dot(z)) == 1.0: # 2) binormal is || z
# align shape to tangent only
psi = math.degrees(DraftVecUtils.angle(x, t, z))
theta = 0.0
phi = 0.0
FreeCAD.Console.PrintWarning(
"Draft PathArray.orientShape - Gimbal lock. Infinite lnodes. Change Path or Base.\n")
else: # regular case
psi = math.degrees(DraftVecUtils.angle(x, lnodes, z))
theta = math.degrees(DraftVecUtils.angle(z, b, lnodes))
phi = math.degrees(DraftVecUtils.angle(lnodes, t, b))

rotations = [placement.Rotation]

if psi != 0.0:
rotations.insert(0, FreeCAD.Rotation(z, psi))
if theta != 0.0:
rotations.insert(0, FreeCAD.Rotation(lnodes, theta))
if phi != 0.0:
rotations.insert(0, FreeCAD.Rotation(b, phi))

if len(rotations) == 1:
finalRotation = rotations[0]
else:
finalRotation = functools.reduce(
lambda rot1, rot2: rot1.multiply(rot2), rotations)

placement.Rotation = finalRotation

return placement


def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align):
'''Calculates the placements of a shape along a given path so that each copy will be distributed evenly'''
import Part
import DraftGeomUtils

closedpath = DraftGeomUtils.isReallyClosed(pathwire)
normal = DraftGeomUtils.getNormal(pathwire)
path = Part.__sortEdges__(pathwire.Edges)
ends = []
cdist = 0

for e in path: # find cumulative edge end distance
cdist += e.Length
ends.append(cdist)

placements = []

# place the start shape
pt = path[0].Vertexes[0].Point
placements.append(calculatePlacement(
shapeRotation, path[0], 0, pt, xlate, align, normal))

# closed path doesn't need shape on last vertex
if not(closedpath):
# place the end shape
pt = path[-1].Vertexes[-1].Point
placements.append(calculatePlacement(
shapeRotation, path[-1], path[-1].Length, pt, xlate, align, normal))

if count < 3:
return placements

# place the middle shapes
if closedpath:
stop = count
else:
stop = count - 1
step = float(cdist) / stop
remains = 0
travel = step
for i in range(1, stop):
# which edge in path should contain this shape?
# avoids problems with float math travel > ends[-1]
iend = len(ends) - 1

for j in range(0, len(ends)):
if travel <= ends[j]:
iend = j
break

# place shape at proper spot on proper edge
remains = ends[iend] - travel
offset = path[iend].Length - remains
pt = path[iend].valueAt(getParameterFromV0(path[iend], offset))

placements.append(calculatePlacement(
shapeRotation, path[iend], offset, pt, xlate, align, normal))

travel += step

return placements

#---------------------------------------------------------------------------
# Python Features definitions
Expand Down Expand Up @@ -5501,125 +5665,22 @@ def getWireFromSubs(self,obj):
sl.append(e)
return Part.Wire(sl)

def getParameterFromV0(self, edge, offset):
'''return parameter at distance offset from edge.Vertexes[0]'''
'''sb method in Part.TopoShapeEdge???'''
lpt = edge.valueAt(edge.getParameterByLength(0))
vpt = edge.Vertexes[0].Point
if not DraftVecUtils.equals(vpt,lpt):
# this edge is flipped
length = edge.Length - offset
else:
# this edge is right way around
length = offset
return(edge.getParameterByLength(length))

def orientShape(self,shape,edge,offset,RefPt,xlate,align,normal=None):
'''Orient shape to tangent at parm offset along edge.'''
# http://en.wikipedia.org/wiki/Euler_angles
import Part
import DraftGeomUtils
import math
z = FreeCAD.Vector(0,0,1) # unit +Z Probably defined elsewhere?
y = FreeCAD.Vector(0,1,0) # unit +Y
x = FreeCAD.Vector(1,0,0) # unit +X
nullv = FreeCAD.Vector(0,0,0)
nullPlace =FreeCAD.Placement()
ns = shape.copy()
ns.Placement.Base = nullPlace.Base # reset Placement point so translate goes to right place.
ns.Placement.Rotation = shape.Placement.Rotation # preserve global orientation
ns.translate(RefPt+xlate)
if not align:
return ns

# get local coord system - tangent, normal, binormal, if possible
t = edge.tangentAt(self.getParameterFromV0(edge,offset))
t.normalize()
try:
if normal:
n = normal
else:
n = edge.normalAt(self.getParameterFromV0(edge,offset))
n.normalize()
b = (t.cross(n))
b.normalize()
except FreeCAD.Base.FreeCADError: # no normal defined here
n = nullv
b = nullv
FreeCAD.Console.PrintLog ("Draft PathArray.orientShape - Cannot calculate Path normal.\n")
lnodes = z.cross(b)
try:
lnodes.normalize() # Can't normalize null vector.
except:
pass # pathological cases:
if n == nullv: # 1) can't determine normal, don't align.
psi = 0.0
theta = 0.0
phi = 0.0
FreeCAD.Console.PrintWarning("Draft PathArray.orientShape - Path normal is Null. Cannot align.\n")
elif abs(b.dot(z)) == 1.0: # 2) binormal is || z
psi = math.degrees(DraftVecUtils.angle(x,t,z)) # align shape to tangent only
theta = 0.0
phi = 0.0
FreeCAD.Console.PrintWarning("Draft PathArray.orientShape - Gimbal lock. Infinite lnodes. Change Path or Base.\n")
else: # regular case
psi = math.degrees(DraftVecUtils.angle(x,lnodes,z))
theta = math.degrees(DraftVecUtils.angle(z,b,lnodes))
phi = math.degrees(DraftVecUtils.angle(lnodes,t,b))
if psi != 0.0:
ns.rotate(RefPt,z,psi)
if theta != 0.0:
ns.rotate(RefPt,lnodes,theta)
if phi != 0.0:
ns.rotate(RefPt,b,phi)
return ns

def pathArray(self,shape,pathwire,count,xlate,align):
'''Distribute shapes along a path.'''
import Part
import DraftGeomUtils
closedpath = DraftGeomUtils.isReallyClosed(pathwire)
normal = DraftGeomUtils.getNormal(pathwire)
path = Part.__sortEdges__(pathwire.Edges)
ends = []
cdist = 0
for e in path: # find cumulative edge end distance
cdist += e.Length
ends.append(cdist)

placements = calculatePlacementsOnPath(
shape.Placement.Rotation, pathwire, count, xlate, align)

base = []
pt = path[0].Vertexes[0].Point # place the start shape
ns = self.orientShape(shape,path[0],0,pt,xlate,align,normal)
base.append(ns)
if not(closedpath): # closed path doesn't need shape on last vertex
pt = path[-1].Vertexes[-1].Point # place the end shape
ns = self.orientShape(shape,path[-1],path[-1].Length,pt,xlate,align,normal)
base.append(ns)
if count < 3:
return(Part.makeCompound(base))

for placement in placements:
ns = shape.copy()
ns.Placement = placement

# place the middle shapes
if closedpath:
stop = count
else:
stop = count - 1
step = float(cdist)/stop
remain = 0
travel = step
for i in range(1,stop):
# which edge in path should contain this shape?
iend = len(ends) - 1 # avoids problems with float math travel > ends[-1]
for j in range(0,len(ends)):
if travel <= ends[j]:
iend = j
break
# place shape at proper spot on proper edge
remains = ends[iend] - travel
offset = path[iend].Length - remains
pt = path[iend].valueAt(self.getParameterFromV0(path[iend],offset))
ns = self.orientShape(shape,path[iend],offset,pt,xlate,align,normal)
base.append(ns)
travel += step
return(Part.makeCompound(base))

return (Part.makeCompound(base))

class _PointArray(_DraftObject):
"The Draft Point Array object"
Expand Down

0 comments on commit 9cfb84d

Please sign in to comment.