Files
create/src/Mod/Draft/draftgeoutils/faces.py

277 lines
8.8 KiB
Python

# ***************************************************************************
# * Copyright (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net> *
# * Copyright (c) 2009, 2010 Ken Cline <cline@frii.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * 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 Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
"""Provides various functions to work with faces."""
## @package faces
# \ingroup draftgeoutils
# \brief Provides various functions to work with faces.
import lazy_loader.lazy_loader as lz
import DraftVecUtils
from FreeCAD import Base
from draftgeoutils.general import precision
from draftgeoutils.geometry import are_coplanar
# Delay import of module until first use because it is heavy
Part = lz.LazyLoader("Part", globals(), "Part")
## \addtogroup draftgeoutils
# @{
def concatenate(shape):
"""Turn several faces into one."""
boundary_edges = getBoundary(shape)
sorted_edges = Part.sortEdges(boundary_edges)
try:
wires = [Part.Wire(edges) for edges in sorted_edges]
face = Part.makeFace(wires, "Part::FaceMakerBullseye")
except Base.FreeCADError:
print("DraftGeomUtils: Fails to join faces into one. "
+ "The precision of the faces would be insufficient")
return shape
else:
if not wires[0].isClosed():
return wires[0]
else:
return face
def getBoundary(shape):
"""Return the boundary edges of a group of faces."""
if isinstance(shape, list):
shape = Part.makeCompound(shape)
# Make a lookup-table where we get the number of occurrences
# to each edge in the fused face
table = dict()
for f in shape.Faces:
for e in f.Edges:
hash_code = e.hashCode()
if hash_code in table:
table[hash_code] = table[hash_code] + 1
else:
table[hash_code] = 1
# Filter out the edges shared by more than one sub-face
bound = list()
for e in shape.Edges:
if table[e.hashCode()] == 1:
bound.append(e)
return bound
def is_coplanar(faces, tol=-1):
"""Return True if all faces in the given list are coplanar.
Parameters
----------
faces: list
List of faces to check coplanarity.
tol: float, optional
It defaults to `-1`, the tolerance of confusion, equal to 1e-7.
Is the maximum deviation to be considered coplanar.
Returns
-------
out: bool
True if all face are coplanar. False in other case.
"""
first_face = faces[0]
for face in faces:
if not are_coplanar(first_face, face, tol):
return False
return True
isCoplanar = is_coplanar
def bind(w1, w2):
"""Bind 2 wires by their endpoints and returns a face."""
if not w1 or not w2:
print("DraftGeomUtils: unable to bind wires")
return None
if w1.isClosed() and w2.isClosed():
d1 = w1.BoundBox.DiagonalLength
d2 = w2.BoundBox.DiagonalLength
if d1 > d2:
# w2.reverse()
return Part.Face([w1, w2])
else:
# w1.reverse()
return Part.Face([w2, w1])
else:
try:
w3 = Part.LineSegment(w1.Vertexes[0].Point,
w2.Vertexes[0].Point).toShape()
w4 = Part.LineSegment(w1.Vertexes[-1].Point,
w2.Vertexes[-1].Point).toShape()
return Part.Face(Part.Wire(w1.Edges+[w3] + w2.Edges+[w4]))
except Part.OCCError:
print("DraftGeomUtils: unable to bind wires")
return None
def cleanFaces(shape):
"""Remove inner edges from coplanar faces."""
faceset = shape.Faces
def find(hc):
"""Find a face with the given hashcode."""
for f in faceset:
if f.hashCode() == hc:
return f
def findNeighbour(hface, hfacelist):
"""Find the first neighbour of a face, and return its index."""
eset = []
for e in find(hface).Edges:
eset.append(e.hashCode())
for i in range(len(hfacelist)):
for ee in find(hfacelist[i]).Edges:
if ee.hashCode() in eset:
return i
return None
# build lookup table
lut = {}
for face in faceset:
for edge in face.Edges:
if edge.hashCode() in lut:
lut[edge.hashCode()].append(face.hashCode())
else:
lut[edge.hashCode()] = [face.hashCode()]
# print("lut:",lut)
# take edges shared by 2 faces
sharedhedges = []
for k, v in lut.items():
if len(v) == 2:
sharedhedges.append(k)
# print(len(sharedhedges)," shared edges:",sharedhedges)
# find those with same normals
targethedges = []
for hedge in sharedhedges:
faces = lut[hedge]
n1 = find(faces[0]).normalAt(0.5, 0.5)
n2 = find(faces[1]).normalAt(0.5, 0.5)
if n1 == n2:
targethedges.append(hedge)
# print(len(targethedges)," target edges:",targethedges)
# get target faces
hfaces = []
for hedge in targethedges:
for f in lut[hedge]:
if f not in hfaces:
hfaces.append(f)
# print(len(hfaces)," target faces:",hfaces)
# sort islands
islands = [[hfaces.pop(0)]]
currentisle = 0
currentface = 0
found = True
while hfaces:
if not found:
if len(islands[currentisle]) > (currentface + 1):
currentface += 1
found = True
else:
islands.append([hfaces.pop(0)])
currentisle += 1
currentface = 0
found = True
else:
f = findNeighbour(islands[currentisle][currentface], hfaces)
if f is not None:
islands[currentisle].append(hfaces.pop(f))
else:
found = False
# print(len(islands)," islands:",islands)
# make new faces from islands
newfaces = []
treated = []
for isle in islands:
treated.extend(isle)
fset = []
for i in isle:
fset.append(find(i))
bounds = getBoundary(fset)
shp = Part.Wire(Part.__sortEdges__(bounds))
shp = Part.Face(shp)
if shp.normalAt(0.5, 0.5) != find(isle[0]).normalAt(0.5, 0.5):
shp.reverse()
newfaces.append(shp)
# print("new faces:",newfaces)
# add remaining faces
for f in faceset:
if not f.hashCode() in treated:
newfaces.append(f)
# print("final faces")
# finishing
fshape = Part.makeShell(newfaces)
if shape.isClosed():
fshape = Part.makeSolid(fshape)
return fshape
def removeSplitter(shape):
"""Return a face from removing the splitter in a list of faces.
This is an alternative, shared edge-based version of Part.removeSplitter.
Returns a face, or `None` if the operation failed.
"""
lookup = dict()
for f in shape.Faces:
for e in f.Edges:
h = e.hashCode()
if h in lookup:
lookup[h].append(e)
else:
lookup[h] = [e]
edges = [e[0] for e in lookup.values() if len(e) == 1]
try:
face = Part.Face(Part.Wire(edges))
except Part.OCCError:
# operation failed
return None
else:
if face.isValid():
return face
return None
## @}