FreeCAD / src /Mod /BIM /ArchVRM.py
AbdulElahGwaith's picture
Upload folder using huggingface_hub
985c397 verified
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2012 Yorik van Havre <yorik@uncreated.net> *
# * *
# * This file is part of FreeCAD. *
# * *
# * FreeCAD is free software: you can redistribute it and/or modify it *
# * under the terms of the GNU Lesser General Public License as *
# * published by the Free Software Foundation, either version 2.1 of the *
# * License, or (at your option) any later version. *
# * *
# * FreeCAD 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 *
# * Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Lesser General Public *
# * License along with FreeCAD. If not, see *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
"The FreeCAD Arch Vector Rendering Module"
## @package ArchVRM
# \ingroup ARCH
# \brief The Arch Vector Rendering Module
#
# This module provides the Renderer Class, that allows one to
# produce SVG renderings of projected shapes, with filled faces.
# It is used by the "Solid" mode of Arch views in TechDraw and Drawing,
# and is called from ArchSectionPlane code.
import math
import FreeCAD
import ArchCommands
import Draft
import DraftVecUtils
import DraftGeomUtils
import Part
from draftutils import params
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 = params.get_param_arch("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.PlaneBase()
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.align_to_placement(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 = Part.__sortEdges__(w.Edges)
# print(len(edges)," edges after sorting")
for e in edges:
v = e.Vertexes[0].Point
# print(v)
v = self.wp.get_local_coords(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 Exception:
if DEBUG:
print("Error: Unable to project face on the WP")
return None
else:
# restoring flipped normals
vnorm = self.wp.get_local_coords(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.get_local_coords(edge.Vertexes[0].Point)
v2 = self.wp.get_local_coords(edge.Vertexes[-1].Point)
return Part.LineSegment(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 = Part.__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 Part.OCCError:
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"
if not face:
return False
# http://paulbourke.net/geometry/insidepoly/
count = 0
p = self.wp.get_local_coords(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)
if (not face1) or (not face2):
return False
# 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 don't 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"
import Part
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 Part.OCCError:
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 is None:
h = faces.index(f2)
else:
if faces.index(f2) < h:
h = faces.index(f2)
if l is not None:
return l + 1
elif h is not 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 is 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:
if f1 in faces:
faces.remove(f1)
elif r == 32:
if f2 in faces:
faces.remove(f2)
else:
# nothing found, move the face to the end of the pile
if f1 in faces:
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, Draft.precision()))
edges = Part.__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 = (
'<g stroke="#000000" stroke-width="'
+ str(linewidth)
+ '" style="stroke-width:'
+ str(linewidth)
)
svg += ';stroke-miterlimit:1;stroke-linejoin:round;stroke-dasharray:none;">\n'
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 += '" style="fill:' + fill + ';fill-rule: evenodd;"/>\n'
svg += "</g>\n"
return svg
def getSectionSVG(self, linewidth=0.02, fillpattern=None):
"Returns a SVG fragment from cut faces"
if DEBUG:
print("Printing ", len(self.sections), " sections")
if not self.oriented:
self.reorient()
svg = (
'<g stroke="#000000" stroke-width="'
+ str(linewidth)
+ '" style="stroke-width:'
+ str(linewidth)
)
svg += ';stroke-miterlimit:1;stroke-linejoin:round;stroke-dasharray:none;">\n'
for f in self.sections:
if f:
if fillpattern:
if "#" in fillpattern: # color
fill = fillpattern
else:
fill = "url(#" + fillpattern + ")" # pattern name
else:
fill = "none" # none
svg += "<path "
svg += 'd="'
for w in f[0].Wires:
# print("wire with ",len(w.Vertexes)," verts")
svg += self.getPathData(w)
svg += '" style="fill:' + fill + ';fill-rule: evenodd;"/>\n'
svg += "</g>\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 = (
'<g stroke="#000000" stroke-width="'
+ str(linewidth)
+ '" style="stroke-width:'
+ str(linewidth)
)
svg += (
';stroke-miterlimit:1;stroke-linejoin:round;stroke-dasharray:0.09,0.05;fill:none;">\n'
)
for e in self.hiddenEdges:
svg += "<path "
svg += 'd="'
svg += self.getPathData(e)
svg += '"/>\n'
svg += "</g>\n"
return svg