FreeCAD/src/Mod/Arch/ArchVRM.py
2013-03-31 17:54:32 -03:00

649 lines
24 KiB
Python

#***************************************************************************
#* *
#* Copyright (c) 2012 *
#* 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 *
#* *
#***************************************************************************
"The FreeCAD Arch Vector Rendering Module"
import FreeCAD,math,Part,ArchCommands,DraftVecUtils,DraftGeomUtils
MAXLOOP = 10 # the max number of loop before abort
# WARNING: in this module, faces are lists whose first item is the actual OCC face, the
# other items being additional information such as color, etc.
DEBUG = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetBool("ShowVRMDebug")
class Renderer:
"A renderer object"
def __init__(self,wp=None):
"""
Creates a renderer with a default Draft WorkingPlane
Use like this:
import ArchVRM
p = ArchVRM.Renderer()
p.add(App.ActiveDocument.ActiveObject)
p.sort()
p.buildDummy()
"""
self.reset()
if wp:
self.wp = wp
else:
import WorkingPlane
self.wp = WorkingPlane.plane()
if DEBUG: print "Renderer initialized on " + str(self.wp)
def __str__(self):
return "Arch Renderer: " + str(len(self.faces)) + " faces projected on " + str(self.wp)
def reset(self):
"removes all faces from this renderer"
self.objects = []
self.shapes = []
self.faces = []
self.resetFlags()
def resetFlags(self):
"resets all flags of this renderer"
self.oriented = False
self.trimmed = False
self.sorted = False
self.iscut = False
self.joined = False
self.sections = []
self.hiddenEdges = []
def setWorkingPlane(self,wp):
"sets a Draft WorkingPlane or Placement for this renderer"
if isinstance(wp,FreeCAD.Placement):
self.wp.setFromPlacement(wp)
else:
self.wp = wp
if DEBUG: print "Renderer set on " + str(self.wp)
def addFaces(self,faces,color=(0.9,0.9,0.9,1.0)):
"add individual faces to this renderer, optionally with a color"
if DEBUG: print "adding ", len(faces), " faces. Warning, these will get lost if using cut() or join()"
for f in faces:
self.faces.append([f,color])
self.resetFlags()
def addObjects(self,objs):
"add objects to this renderer"
for o in objs:
if o.isDerivedFrom("Part::Feature"):
self.objects.append(o)
color = o.ViewObject.ShapeColor
if o.Shape.Faces:
self.shapes.append([o.Shape,color])
for f in o.Shape.Faces:
self.faces.append([f,color])
self.resetFlags()
if DEBUG: print "adding ", len(self.objects), " objects, ", len(self.faces), " faces"
def addShapes(self,shapes,color=(0.9,0.9,0.9,1.0)):
"add shapes to this renderer, optionally with a color. Warning, these will get lost if using join()"
if DEBUG: print "adding ", len(shapes), " shapes"
for s in shapes:
if s.Faces:
self.shapes.append([s,color])
for f in s.Faces:
self.faces.append([f,color])
self.resetFlags()
def info(self):
"Prints info about the contents of this renderer"
r = str(self)+"\n"
r += "oriented: " + str(self.oriented) + "\n"
r += "trimmed: " + str(self.trimmed) + "\n"
r += "sorted: " + str(self.sorted) + "\n"
r += "contains " + str(len(self.faces)) + " faces\n"
for i in range(len(self.faces)):
r += " face " + str(i) + " : center " + str(self.faces[i][0].CenterOfMass)
r += " : normal " + str(self.faces[i][0].normalAt(0,0))
r += ", " + str(len(self.faces[i][0].Vertexes)) + " verts"
r += ", color: " + self.getFill(self.faces[i][1]) + "\n"
return r
def addLabels(self):
"Add labels on the model to identify faces"
c = 0
for f in self.faces:
l = FreeCAD.ActiveDocument.addObject("App::AnnotationLabel","facelabel")
l.BasePosition = f[0].CenterOfMass
l.LabelText = str(c)
c += 1
def isVisible(self,face):
"returns True if the given face points in the view direction"
normal = face[0].normalAt(0,0)
if DEBUG: print "checking face normal ", normal, " against ", self.wp.axis, " : ", math.degrees(normal.getAngle(self.wp.axis))
if normal.getAngle(self.wp.axis) < math.pi/2:
return True
return False
def reorient(self):
"reorients the faces on the WP"
#print "VRM: start reorient"
if not self.faces:
return
self.faces = [self.projectFace(f) for f in self.faces]
if self.sections:
self.sections = [self.projectFace(f) for f in self.sections]
if self.hiddenEdges:
self.hiddenEdges = [self.projectEdge(e) for e in self.hiddenEdges]
self.oriented = True
#print "VRM: end reorient"
def removeHidden(self):
"removes faces pointing outwards"
if not self.faces:
return
faces = []
for f in self.faces:
if self.isVisible(f):
faces.append(f)
if DEBUG: print len(self.faces)-len(faces) , " faces removed, ", len(faces), " faces retained"
self.faces = faces
self.trimmed = True
def projectFace(self,face):
"projects a single face on the WP"
#print "VRM: projectFace start: ",len(face[0].Vertexes)," verts, ",len(face[0].Edges)," edges"
wires = []
if not face[0].Wires:
if DEBUG: print "Error: Unable to project face on the WP"
return None
norm = face[0].normalAt(0,0)
for w in face[0].Wires:
verts = []
edges = DraftGeomUtils.sortEdges(w.Edges)
#print len(edges)," edges after sorting"
for e in edges:
v = e.Vertexes[0].Point
#print v
v = self.wp.getLocalCoords(v)
verts.append(v)
verts.append(verts[0])
if len(verts) > 2:
#print "new wire with ",len(verts)
wires.append(Part.makePolygon(verts))
try:
sh = ArchCommands.makeFace(wires)
except:
if DEBUG: print "Error: Unable to project face on the WP"
return None
else:
# restoring flipped normals
vnorm = self.wp.getLocalCoords(norm)
if vnorm.getAngle(sh.normalAt(0,0)) > 1:
sh.reverse()
#print "VRM: projectFace end: ",len(sh.Vertexes)," verts"
return [sh]+face[1:]
def projectEdge(self,edge):
"projects a single edge on the WP"
if len(edge.Vertexes) > 1:
v1 = self.wp.getLocalCoords(edge.Vertexes[0].Point)
v2 = self.wp.getLocalCoords(edge.Vertexes[-1].Point)
return Part.Line(v1,v2).toShape()
return edge
def flattenFace(self,face):
"Returns a face where all vertices have Z = 0"
wires = []
for w in face[0].Wires:
verts = []
edges = DraftGeomUtils.sortEdges(w.Edges)
for e in edges:
v = e.Vertexes[0].Point
verts.append(FreeCAD.Vector(v.x,v.y,0))
verts.append(verts[0])
wires.append(Part.makePolygon(verts))
try:
sh = Part.Face(wires)
except:
if DEBUG: print "Error: Unable to flatten face"
return None
else:
return [sh]+face[1:]
def cut(self,cutplane,hidden=False):
"Cuts through the shapes with a given cut plane and builds section faces"
if DEBUG: print "\n\n======> Starting cut\n\n"
if self.iscut:
return
if not self.shapes:
if DEBUG: print "No objects to make sections"
else:
fill = (1.0,1.0,1.0,1.0)
shps = []
for sh in self.shapes:
shps.append(sh[0])
cutface,cutvolume,invcutvolume = ArchCommands.getCutVolume(cutplane,shps)
if cutface and cutvolume:
shapes = []
faces = []
sections = []
for sh in self.shapes:
for sol in sh[0].Solids:
c = sol.cut(cutvolume)
shapes.append([c]+sh[1:])
for f in c.Faces:
faces.append([f]+sh[1:])
print "iscoplanar:",f.Vertexes[0].Point,f.normalAt(0,0),cutface.Vertexes[0].Point,cutface.normalAt(0,0)
if DraftGeomUtils.isCoplanar([f,cutface]):
print "COPLANAR"
sections.append([f,fill])
if hidden:
c = sol.cut(invcutvolume)
self.hiddenEdges.extend(c.Edges)
self.shapes = shapes
self.faces = faces
self.sections = sections
if DEBUG: print "Built ",len(self.sections)," sections, ", len(self.faces), " faces retained"
self.iscut = True
self.oriented = False
self.trimmed = False
self.sorted = False
self.joined = False
if DEBUG: print "\n\n======> Finished cut\n\n"
def isInside(self,vert,face):
"Returns True if the vert is inside the face in Z projection"
# http://paulbourke.net/geometry/insidepoly/
count = 0
p = self.wp.getLocalCoords(vert.Point)
for e in face[0].Edges:
p1 = e.Vertexes[0].Point
p2 = e.Vertexes[-1].Point
if p.y > min(p1.y,p2.y):
if p.y <= max(p1.y,p2.y):
if p.x <= max(p1.x,p2.x):
if p1.y != p2.y:
xinters = (p.y-p1.y)*(p2.x-p1.x)/(p2.y-p1.y)+p1.x
if (p1.x == p2.x) or (p.x <= xinters):
count += 1
if count % 2 == 0:
return False
else:
return True
def zOverlaps(self,face1,face2):
"Checks if face1 overlaps face2 in Z direction"
face1 = self.flattenFace(face1)
face2 = self.flattenFace(face2)
# first we check if one of the verts is inside the other face
for v in face1[0].Vertexes:
if self.isInside(v,face2):
return True
# even so, faces can still overlap if their edges cross each other
for e1 in face1[0].Edges:
for e2 in face2[0].Edges:
if DraftGeomUtils.findIntersection(e1,e2):
return True
return False
def compare(self,face1,face2):
"zsorts two faces. Returns 1 if face1 is closer, 2 if face2 is closer, 0 otherwise"
#print face1,face2
if not face1:
if DEBUG: print "Warning, undefined face!"
return 31
elif not face2:
if DEBUG: print "Warning, undefined face!"
return 32
# theory from
# http://www.siggraph.org/education/materials/HyperGraph/scanline/visibility/painter.htm
# and practical application http://vrm.ao2.it/ (blender vector renderer)
b1 = face1[0].BoundBox
b2 = face2[0].BoundBox
# test 1: if faces don't overlap, no comparison possible
if DEBUG: print "doing test 1"
if b1.XMax < b2.XMin:
return 0
if b1.XMin > b2.XMax:
return 0
if b1.YMax < b2.YMin:
return 0
if b1.YMin > b2.YMax:
return 0
if DEBUG: print "failed, faces bboxes are not distinct"
# test 2: if Z bounds dont overlap, it's easy to know the closest
if DEBUG: print "doing test 2"
if b1.ZMax < b2.ZMin:
return 2
if b2.ZMax < b1.ZMin:
return 1
if DEBUG: print "failed, faces Z are not distinct"
# test 3: all verts of face1 are in front or behind the plane of face2
if DEBUG: print "doing test 3"
norm = face2[0].normalAt(0,0)
behind = 0
front = 0
for v in face1[0].Vertexes:
dv = v.Point.sub(face2[0].Vertexes[0].Point)
dv = DraftVecUtils.project(dv,norm)
if DraftVecUtils.isNull(dv):
behind += 1
front += 1
else:
if dv.getAngle(norm) > 1:
behind += 1
else:
front += 1
if DEBUG: print "front: ",front," behind: ",behind
if behind == len(face1[0].Vertexes):
return 2
elif front == len(face1[0].Vertexes):
return 1
if DEBUG: print "failed, cannot say if face 1 is in front or behind"
# test 4: all verts of face2 are in front or behind the plane of face1
if DEBUG: print "doing test 4"
norm = face1[0].normalAt(0,0)
behind = 0
front = 0
for v in face2[0].Vertexes:
dv = v.Point.sub(face1[0].Vertexes[0].Point)
dv = DraftVecUtils.project(dv,norm)
if DraftVecUtils.isNull(dv):
behind += 1
front += 1
else:
if dv.getAngle(norm) > 1:
behind += 1
else:
front += 1
if DEBUG: print "front: ",front," behind: ",behind
if behind == len(face2[0].Vertexes):
return 1
elif front == len(face2[0].Vertexes):
return 2
if DEBUG: print "failed, cannot say if face 2 is in front or behind"
# test 5: see if faces projections don't overlap, vertexwise
if DEBUG: print "doing test 5"
if not self.zOverlaps(face1,face2):
return 0
elif not self.zOverlaps(face2,face1):
return 0
if DEBUG: print "failed, faces are overlapping"
if DEBUG: print "Houston, all tests passed, and still no results"
return 0
def join(self,otype):
"joins the objects of same type"
walls = []
structs = []
objs = []
for o in obj.Source.Objects:
t = Draft.getType(o)
if t == "Wall":
walls.append(o)
elif t == "Structure":
structs.append(o)
else:
objs.append(o)
for g in [walls,structs]:
if g:
print "group:",g
col = g[0].ViewObject.DiffuseColor[0]
s = g[0].Shape
for o in g[1:]:
try:
fs = s.fuse(o.Shape)
fs = fs.removeSplitter()
except:
print "shape fusion failed"
objs.append([o.Shape,o.ViewObject.DiffuseColor[0]])
else:
s = fs
objs.append([s,col])
def findPosition(self,f1,faces):
"Finds the position of a face in a list of faces"
l = None
h = None
for f2 in faces:
if DEBUG: print "comparing face",str(self.faces.index(f1))," with face",str(self.faces.index(f2))
r = self.compare(f1,f2)
if r == 1:
l = faces.index(f2)
elif r == 2:
if h == None:
h = faces.index(f2)
else:
if faces.index(f2) < h:
h = faces.index(f2)
if l != None:
return l + 1
elif h != None:
return h
else:
return None
def sort(self):
"projects a shape on the WP"
if DEBUG: print "\n\n======> Starting sort\n\n"
if len(self.faces) <= 1:
return
if not self.trimmed:
self.removeHidden()
if DEBUG: print "Done hidden face removal"
if len(self.faces) == 1:
return
if not self.oriented:
self.reorient()
if DEBUG: print "Done reorientation"
faces = self.faces[:]
if DEBUG: print "sorting ",len(self.faces)," faces"
sfaces = []
loopcount = 0
notfoundstack = 0
while faces:
if DEBUG: print "loop ", loopcount
f1 = faces[0]
if sfaces and (notfoundstack < len(faces)):
if DEBUG: print "using ordered stack, notfound = ",notfoundstack
p = self.findPosition(f1,sfaces)
if p == None:
# no position found, we move the face to the end of the pile
faces.remove(f1)
faces.append(f1)
notfoundstack += 1
else:
# position found, we insert it
faces.remove(f1)
sfaces.insert(p,f1)
notfoundstack = 0
else:
# either there is no stack, or no more face can be compared
# find a root, 2 faces that can be compared
if DEBUG: print "using unordered stack, notfound = ",notfoundstack
for f2 in faces[1:]:
if DEBUG: print "comparing face",str(self.faces.index(f1))," with face",str(self.faces.index(f2))
r = self.compare(f1,f2)
print "comparison result:",r
if r == 1:
faces.remove(f2)
sfaces.append(f2)
faces.remove(f1)
sfaces.append(f1)
notfoundstack = 0
break
elif r == 2:
faces.remove(f1)
sfaces.append(f1)
faces.remove(f2)
sfaces.append(f2)
notfoundstack = 0
break
elif r == 31:
faces.remove(f1)
elif r == 32:
faces.remove(f2)
else:
# nothing found, move the face to the end of the pile
faces.remove(f1)
faces.append(f1)
loopcount += 1
if loopcount == MAXLOOP * len(self.faces):
if DEBUG: print "Too many loops, aborting."
break
if DEBUG: print "done Z sorting. ", len(sfaces), " faces retained, ", len(self.faces)-len(sfaces), " faces lost."
self.faces = sfaces
self.sorted = True
if DEBUG: print "\n\n======> Finished sort\n\n"
def buildDummy(self):
"Builds a dummy object with faces spaced on the Z axis, for visual check"
z = 0
if not self.sorted:
self.sort()
faces = []
for f in self.faces[:]:
ff = self.flattenFace(f)[0]
ff.translate(FreeCAD.Vector(0,0,z))
faces.append(ff)
z += 1
if faces:
c = Part.makeCompound(faces)
Part.show(c)
def getFill(self,fill):
"Returns a SVG fill value"
r = str(hex(int(fill[0]*255)))[2:].zfill(2)
g = str(hex(int(fill[1]*255)))[2:].zfill(2)
b = str(hex(int(fill[2]*255)))[2:].zfill(2)
col = "#"+r+g+b
return col
def getPathData(self,w):
"Returns a SVG path data string from a 2D wire"
def tostr(val):
return str(round(val,DraftVecUtils.precision()))
edges = DraftGeomUtils.sortEdges(w.Edges)
v = edges[0].Vertexes[0].Point
svg = 'M '+ tostr(v.x) +' '+ tostr(v.y) + ' '
for e in edges:
if (DraftGeomUtils.geomType(e) == "Line") or (DraftGeomUtils.geomType(e) == "BSplineCurve"):
v = e.Vertexes[-1].Point
svg += 'L '+ tostr(v.x) +' '+ tostr(v.y) + ' '
elif DraftGeomUtils.geomType(e) == "Circle":
r = e.Curve.Radius
v = e.Vertexes[-1].Point
svg += 'A '+ tostr(r) + ' '+ tostr(r) +' 0 0 1 '+ tostr(v.x) +' '
svg += tostr(v.y) + ' '
if len(edges) > 1:
svg += 'Z '
return svg
def getViewSVG(self,linewidth=0.01):
"Returns a SVG fragment from viewed faces"
if DEBUG: print "Printing ", len(self.faces), " faces"
if not self.sorted:
self.sort()
svg = ''
for f in self.faces:
if f:
fill = self.getFill(f[1])
svg +='<path '
svg += 'd="'
for w in f[0].Wires:
svg += self.getPathData(w)
svg += '" '
svg += 'stroke="#000000" '
svg += 'stroke-width="' + str(linewidth) + '" '
svg += 'style="stroke-width:' + str(linewidth) + ';'
svg += 'stroke-miterlimit:1;'
svg += 'stroke-linejoin:round;'
svg += 'stroke-dasharray:none;'
svg += 'fill:' + fill + ';'
svg += 'fill-rule: evenodd'
svg += '"/>\n'
return svg
def getSectionSVG(self,linewidth=0.02):
"Returns a SVG fragment from cut faces"
if DEBUG: print "Printing ", len(self.sections), " sections"
if not self.oriented:
self.reorient()
svg = ''
for f in self.sections:
if f:
fill = self.getFill(f[1])
svg +='<path '
svg += 'd="'
for w in f[0].Wires:
#print "wire with ",len(w.Vertexes)," verts"
svg += self.getPathData(w)
svg += '" '
svg += 'stroke="#000000" '
svg += 'stroke-width="' + str(linewidth) + '" '
svg += 'style="stroke-width:' + str(linewidth) + ';'
svg += 'stroke-miterlimit:1;'
svg += 'stroke-linejoin:round;'
svg += 'stroke-dasharray:none;'
svg += 'fill:' + fill + ';'
svg += 'fill-rule: evenodd'
svg += '"/>\n'
return svg
def getHiddenSVG(self,linewidth=0.02):
"Returns a SVG fragment from cut geometry"
if DEBUG: print "Printing ", len(self.sections), " hidden faces"
if not self.oriented:
self.reorient()
svg = ''
for e in self.hiddenEdges:
svg +='<path '
svg += 'd="'
svg += self.getPathData(e)
svg += '" '
svg += 'stroke="#000000" '
svg += 'stroke-width="' + str(linewidth) + '" '
svg += 'style="stroke-width:' + str(linewidth) + ';'
svg += 'stroke-miterlimit:1;'
svg += 'stroke-linejoin:round;'
svg += 'stroke-dasharray:0.09,0.05;'
svg += 'fill:none;'
svg += '"/>\n'
return svg