Coverage for trimesh/path/repair.py: 100%
43 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2repair.py
3--------------
5Try to fix problems with closed regions.
6"""
8import numpy as np
9from scipy.spatial import cKDTree
11from .. import util
12from . import segments
15def fill_gaps(path, distance=0.025):
16 """
17 Find vertices without degree 2 and try to connect to
18 other vertices. Operations are done in-place.
20 Parameters
21 ------------
22 segments : trimesh.path.Path2D
23 Line segments defined by start and end points
24 """
26 # find any vertex without degree 2 (connected to two things)
27 broken = np.array([k for k, d in dict(path.vertex_graph.degree()).items() if d != 2])
29 # if all vertices have correct connectivity, exit
30 if len(broken) == 0:
31 return
33 # first find broken vertices with distance
34 tree = cKDTree(path.vertices[broken])
35 pairs = tree.query_pairs(r=distance, output_type="ndarray")
37 connect_seg = []
38 if len(pairs) > 0:
39 end_points = {tuple(sorted(e.end_points)) for e in path.entities}
40 pair_set = {tuple(i) for i in np.sort(broken[pairs], axis=1)}
42 # we don't want to connect entities to themselves so do a set
43 # difference
44 mask = np.array(list(pair_set.difference(end_points)))
46 if len(mask) > 0:
47 connect_seg = path.vertices[mask]
49 # a set of values we can query intersections with quickly
50 broken_set = set(broken)
51 # query end points set vs path.dangling to avoid having
52 # to compute every single path and discrete curve
53 dangle = [
54 i
55 for i, e in enumerate(path.entities)
56 if len(broken_set.intersection(e.end_points)) > 0
57 ]
59 segs = []
60 # mask for which entities to keep
61 keep = np.ones(len(path.entities), dtype=bool)
62 # save a reference to the line class to avoid circular import
63 line_class = None
65 for entity_index in dangle:
66 # only consider line entities
67 if path.entities[entity_index].__class__.__name__ != "Line":
68 continue
70 if line_class is None:
71 line_class = path.entities[entity_index].__class__
73 # get discrete version of entity
74 points = path.entities[entity_index].discrete(path.vertices)
75 # turn connected curve into segments
76 seg_idx = util.stack_lines(np.arange(len(points)))
77 # append the segments to our collection
78 segs.append(points[seg_idx])
79 # remove this entity and replace with segments
80 keep[entity_index] = False
82 # combine segments with connection segments
83 all_segs = util.vstack_empty((util.vstack_empty(segs), connect_seg))
85 # go home early
86 if len(all_segs) == 0:
87 return
89 # split segments at broken vertices so topology can happen
90 split = segments.split(all_segs, path.vertices[broken])
91 # merge duplicate segments
92 final_seg = segments.unique(split)
94 # add line segments in as line entities
95 entities = []
96 for i in range(len(final_seg)):
97 entities.append(line_class(points=np.arange(2) + (i * 2) + len(path.vertices)))
99 # replace entities with new entities
100 path.entities = np.append(path.entities[keep], entities)
101 path.vertices = np.vstack((path.vertices, np.vstack(final_seg)))
102 path._cache.clear()
103 path.process()