| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | """Provides various functions to work with faces.""" |
| | |
| | |
| | |
| |
|
| | import lazy_loader.lazy_loader as lz |
| |
|
| | import DraftVecUtils |
| | from FreeCAD import Base |
| | from draftgeoutils.geometry import are_coplanar |
| |
|
| | |
| | Part = lz.LazyLoader("Part", globals(), "Part") |
| |
|
| | |
| | |
| |
|
| |
|
| | 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) |
| |
|
| | |
| | |
| | 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 |
| |
|
| | |
| | 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, per_segment=False): |
| | """Bind 2 wires by their endpoints and returns a face / compound of faces. |
| | |
| | If per_segment is True and the wires have the same number of edges, the |
| | wires are processed per segment: a separate face is created for each pair |
| | of edges (one from w1 and one from w2), and the faces are then fused. This |
| | avoids problems with walls based on wires that selfintersect, or that have |
| | a loop that ends in a T-connection (f.e. a wire shaped like a number 6). |
| | """ |
| |
|
| | def create_face(w1, w2): |
| |
|
| | 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() |
| | except Part.OCCError: |
| | print("DraftGeomUtils: unable to bind wires") |
| | return None |
| | if w3.section(w4).Vertexes: |
| | print("DraftGeomUtils: Problem, a segment is self-intersecting, please check!") |
| | f = Part.Face(Part.Wire(w1.Edges + [w3] + w2.Edges + [w4])) |
| | return f |
| |
|
| | if not w1 or not w2: |
| | print("DraftGeomUtils: unable to bind wires") |
| | return None |
| |
|
| | if per_segment and len(w1.Edges) > 1 and len(w1.Edges) == len(w2.Edges): |
| | faces = [] |
| | faces_list = [] |
| | for edge1, edge2 in zip(w1.Edges, w2.Edges): |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | if edge1.section(edge2).Vertexes: |
| | faces_list.append(faces) |
| | faces = [] |
| | continue |
| | else: |
| | face = create_face(edge1, edge2) |
| | if face is None: |
| | return None |
| | faces.append(face) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | if faces_list and faces: |
| | |
| | |
| | |
| | |
| | |
| | if w1.isClosed() and w2.isClosed() and len(faces_list) > 1 and faces_list[0]: |
| | faces_list[0].extend( |
| | faces |
| | ) |
| | else: |
| | faces_list.append(faces) |
| | from collections import Counter |
| |
|
| | if faces_list: |
| | faces_fused_list = [] |
| | for faces in faces_list: |
| | dir = [] |
| | countDir = None |
| | for f in faces: |
| | dir.append(f.normalAt(0, 0).z) |
| | countDir = Counter(dir) |
| | l = len(faces) |
| | m = max(countDir.values()) |
| | if m != l: |
| | print( |
| | "DraftGeomUtils: Problem, the direction of " |
| | + str(l - m) |
| | + " out of " |
| | + str(l) |
| | + " segment is reversed, please check!" |
| | ) |
| | if len(faces) > 1: |
| | |
| | |
| | rf = faces[0] |
| | for f in faces[1:]: |
| | rf = rf.fuse(f).removeSplitter().Faces[0] |
| | |
| | |
| | faces_fused_list.append(rf) |
| | |
| | elif faces: |
| | faces_fused_list.append(faces[0]) |
| |
|
| | return Part.Compound(faces_fused_list) |
| | else: |
| | dir = [] |
| | countDir = None |
| | for f in faces: |
| | dir.append(f.normalAt(0, 0).z) |
| | countDir = Counter(dir) |
| | l = len(faces) |
| | m = max(countDir.values()) |
| | if m != l: |
| | print( |
| | "DraftGeomUtils: Problem, the direction of " |
| | + str(l - m) |
| | + " out of " |
| | + str(l) |
| | + " segment is reversed, please check!" |
| | ) |
| | |
| | |
| | rf = faces[0] |
| | for f in faces[1:]: |
| | rf = rf.fuse(f).removeSplitter().Faces[0] |
| | |
| | |
| | return rf |
| |
|
| | elif w1.isClosed() and w2.isClosed(): |
| | d1 = w1.BoundBox.DiagonalLength |
| | d2 = w2.BoundBox.DiagonalLength |
| | if d1 < d2: |
| | w1, w2 = w2, w1 |
| | |
| | try: |
| | face = Part.Face([w1, w2]) |
| | face.fix(1e-7, 0, 1) |
| | return face |
| | except Part.OCCError: |
| | print("DraftGeomUtils: unable to bind wires") |
| | return None |
| | else: |
| | return create_face(w1, w2) |
| |
|
| |
|
| | 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 |
| |
|
| | |
| | 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()] |
| |
|
| | |
| | |
| | sharedhedges = [] |
| | for k, v in lut.items(): |
| | if len(v) == 2: |
| | sharedhedges.append(k) |
| |
|
| | |
| | |
| | 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) |
| |
|
| | |
| | |
| | hfaces = [] |
| | for hedge in targethedges: |
| | for f in lut[hedge]: |
| | if f not in hfaces: |
| | hfaces.append(f) |
| |
|
| | |
| | |
| | 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 |
| |
|
| | |
| | |
| | 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) |
| |
|
| | |
| | |
| | for f in faceset: |
| | if not f.hashCode() in treated: |
| | newfaces.append(f) |
| |
|
| | |
| | |
| | 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: |
| | |
| | return None |
| | else: |
| | if face.isValid(): |
| | return face |
| |
|
| | return None |
| |
|
| |
|
| | |
| |
|