@@ -114,15 +114,153 @@ def subdivide(
114114 return new_vertices , new_faces
115115
116116
117+ def _subdivide_to_size_pass (vertices , faces , index , max_edge ):
118+ """
119+ Run a single crack-free refinement pass.
120+
121+ Every edge longer than `max_edge` is bisected at a *single* midpoint vertex
122+ that is shared by both faces adjacent to the edge, so neighboring faces stay
123+ in sync and no T-junctions (cracks) are introduced. Each face is then split
124+ with a template chosen by how many of its three edges are being bisected
125+ (0, 1, 2, or 3); when two edges are split the residual quad is divided along
126+ its shorter diagonal.
127+
128+ Parameters
129+ ------------
130+ vertices : (n, 3) float
131+ Vertices in space
132+ faces : (m, 3) int
133+ Indices of vertices which make up triangles
134+ index : (m,) int
135+ Original face index carried by each current face
136+ max_edge : float
137+ Maximum allowed edge length
138+
139+ Returns
140+ ------------
141+ vertices : (n + k, 3) float
142+ Vertices with one shared midpoint appended per bisected edge
143+ faces : (p, 3) int
144+ Refined faces
145+ index : (p,) int
146+ Original face index for each refined face
147+ changed : bool
148+ False if no edge exceeded `max_edge` (nothing to do)
149+ """
150+ faces = np .asarray (faces , dtype = np .int64 )
151+ vertices = np .asarray (vertices , dtype = np .float64 )
152+
153+ # the unique edges of the mesh and the length of each
154+ edges = np .sort (faces_to_edges (faces ), axis = 1 )
155+ unique , inverse = grouping .unique_rows (edges )
156+ unique_edges = edges [unique ]
157+ lengths = ((vertices [unique_edges [:, 0 ]] - vertices [unique_edges [:, 1 ]]) ** 2 ).sum (
158+ axis = 1
159+ ) ** 0.5
160+ long_edge = lengths > max_edge
161+
162+ # every edge is already short enough: done
163+ if not long_edge .any ():
164+ return vertices , faces , index , False
165+
166+ # assign one shared midpoint vertex to each long edge (-1 for edges left intact)
167+ midpoint_id = np .full (len (unique ), - 1 , dtype = np .int64 )
168+ midpoint_id [long_edge ] = np .arange (int (long_edge .sum ())) + len (vertices )
169+ midpoints = vertices [unique_edges [long_edge ]].mean (axis = 1 )
170+ new_vertices = np .vstack ((vertices , midpoints ))
171+
172+ # midpoint id for each of the 3 edges of every face, columns (v0v1, v1v2, v2v0)
173+ face_mid = midpoint_id [inverse ].reshape ((- 1 , 3 ))
174+ v0 , v1 , v2 = faces .T
175+ m0 , m1 , m2 = face_mid .T
176+ split = face_mid >= 0
177+ n_split = split .sum (axis = 1 )
178+
179+ new_faces = []
180+ new_index = []
181+
182+ def emit (mask , * triangles ):
183+ # `triangles` is a flat sequence of masked column arrays, 3 columns per
184+ # output face; reshape interleaves a face's children, so index uses repeat
185+ rows = np .nonzero (mask )[0 ]
186+ if len (rows ) == 0 :
187+ return
188+ block = np .column_stack (triangles ).reshape ((- 1 , 3 ))
189+ new_faces .append (block )
190+ new_index .append (np .repeat (index [rows ], len (triangles ) // 3 ))
191+
192+ def quad (mask , p , q , r , s ):
193+ # split quad (p, q, r, s) (wound CCW) into two triangles along its shorter
194+ # diagonal; the two triangle blocks are stacked, so index uses tile
195+ rows = np .nonzero (mask )[0 ]
196+ if len (rows ) == 0 :
197+ return
198+ shorter_pr = (
199+ ((new_vertices [p ] - new_vertices [r ]) ** 2 ).sum (axis = 1 )
200+ <= ((new_vertices [q ] - new_vertices [s ]) ** 2 ).sum (axis = 1 )
201+ )[:, None ]
202+ t1 = np .where (shorter_pr , np .column_stack ((p , q , r )), np .column_stack ((p , q , s )))
203+ t2 = np .where (shorter_pr , np .column_stack ((p , r , s )), np .column_stack ((q , r , s )))
204+ new_faces .append (np .vstack ((t1 , t2 )))
205+ new_index .append (np .tile (index [rows ], 2 ))
206+
207+ # 0 marked edges: the face is already fine, keep it unchanged
208+ keep = n_split == 0
209+ emit (keep , v0 [keep ], v1 [keep ], v2 [keep ])
210+
211+ # 3 marked edges: regular 1 -> 4 split
212+ m = n_split == 3
213+ emit (
214+ m ,
215+ v0 [m ],
216+ m0 [m ],
217+ m2 [m ],
218+ m0 [m ],
219+ v1 [m ],
220+ m1 [m ],
221+ m2 [m ],
222+ m1 [m ],
223+ v2 [m ],
224+ m0 [m ],
225+ m1 [m ],
226+ m2 [m ],
227+ )
228+
229+ # 1 marked edge: bisect from the edge midpoint to the opposite vertex
230+ m = (n_split == 1 ) & split [:, 0 ]
231+ emit (m , v0 [m ], m0 [m ], v2 [m ], m0 [m ], v1 [m ], v2 [m ])
232+ m = (n_split == 1 ) & split [:, 1 ]
233+ emit (m , v0 [m ], v1 [m ], m1 [m ], v0 [m ], m1 [m ], v2 [m ])
234+ m = (n_split == 1 ) & split [:, 2 ]
235+ emit (m , v0 [m ], v1 [m ], m2 [m ], m2 [m ], v1 [m ], v2 [m ])
236+
237+ # 2 marked edges: a corner triangle plus a quad split along its shorter diagonal
238+ m = (n_split == 2 ) & split [:, 0 ] & split [:, 1 ] # edges v0v1, v1v2
239+ emit (m , m0 [m ], v1 [m ], m1 [m ])
240+ quad (m , v0 [m ], m0 [m ], m1 [m ], v2 [m ])
241+
242+ m = (n_split == 2 ) & split [:, 1 ] & split [:, 2 ] # edges v1v2, v2v0
243+ emit (m , m2 [m ], m1 [m ], v2 [m ])
244+ quad (m , v0 [m ], v1 [m ], m1 [m ], m2 [m ])
245+
246+ m = (n_split == 2 ) & split [:, 0 ] & split [:, 2 ] # edges v0v1, v2v0
247+ emit (m , v0 [m ], m0 [m ], m2 [m ])
248+ quad (m , m0 [m ], v1 [m ], v2 [m ], m2 [m ])
249+
250+ new_faces = np .vstack (new_faces ).astype (np .int64 )
251+ new_index = np .concatenate (new_index )
252+ return new_vertices , new_faces , new_index , True
253+
254+
117255def subdivide_to_size (vertices , faces , max_edge , max_iter = 10 , return_index = False ):
118256 """
119257 Subdivide a mesh until every edge is shorter than a
120258 specified length.
121259
122- Every edge longer than `max_edge` is bisected at a single shared
123- midpoint, so the two faces on either side stay in sync and a
124- watertight input stays watertight — no T-junctions ( cracks) are
125- introduced. Faces already small enough are left untouched .
260+ Unlike calling `subdivide` with a subset of faces, this splits edges shared
261+ between a refined and an unrefined face on *both* sides, so a watertight input
262+ stays watertight ( no T-junctions / cracks are introduced). Only edges longer
263+ than `max_edge` are bisected, so faces that are already small are left intact .
126264
127265 Parameters
128266 ------------
@@ -148,97 +286,29 @@ def subdivide_to_size(vertices, faces, max_edge, max_iter=10, return_index=False
148286 original face for each new face.
149287 """
150288 # copy inputs and make sure dtype is correct
151- vertices = np .array (vertices , dtype = np .float64 , copy = True )
152- faces = np .array (faces , dtype = np .int64 , copy = True )
289+ current_vertices = np .array (vertices , dtype = np .float64 , copy = True )
290+ current_faces = np .array (faces , dtype = np .int64 , copy = True )
291+
153292 # map each current face back to its original face index
154- index = np .arange (len (faces ))
293+ current_index = np .arange (len (faces ))
155294
295+ # loop through iteration cap, bisecting all long edges conformally each pass
156296 for i in range (max_iter + 1 ):
157- # the unique edges of the current mesh and the length of each
158- edges = np .sort (faces_to_edges (faces ), axis = 1 )
159- unique , inverse = grouping .unique_rows (edges )
160- edges_unique = edges [unique ]
161- # length uses xyz only — callers may hstack extra columns (e.g. uv)
162- lengths = (
163- (vertices [edges_unique [:, 0 ], :3 ] - vertices [edges_unique [:, 1 ], :3 ]) ** 2
164- ).sum (axis = 1 ) ** 0.5
165- long_edge = lengths > max_edge
166-
167- # every edge is short enough so we're done
168- if not long_edge .any ():
297+ current_vertices , current_faces , current_index , changed = _subdivide_to_size_pass (
298+ current_vertices , current_faces , current_index , max_edge
299+ )
300+ # every edge met the target so we're done
301+ if not changed :
169302 break
170303 # check max_iter before refining again
171304 if i >= max_iter :
172305 raise ValueError ("max_iter exceeded!" )
173306
174- # one shared midpoint vertex per long edge, -1 where left intact
175- midpoint = np .full (len (unique ), - 1 , dtype = np .int64 )
176- midpoint [long_edge ] = np .arange (int (long_edge .sum ())) + len (vertices )
177- vertices = np .vstack ((vertices , vertices [edges_unique [long_edge ]].mean (axis = 1 )))
178-
179- # midpoint id for each face's 3 edges, columns (v0v1, v1v2, v2v0)
180- face_mid = midpoint [inverse ].reshape ((- 1 , 3 ))
181- split = face_mid >= 0
182- # number of edges marked for bisection on each face: 0, 1, 2, or 3
183- count = split .sum (axis = 1 )
184-
185- # 0 marked edges — face passes through unchanged
186- keep = count == 0
187- faces_keep = faces [keep ]
188- index_keep = index [keep ]
189-
190- # 1 marked edge — rotate so the split edge is (a, b) so a single
191- # template handles all three cases: fan the midpoint to the
192- # opposite vertex as [a, p, c], [p, b, c]
193- one = count == 1
194- j = np .argmax (split [one ], axis = 1 )
195- r = np .arange (len (j ))
196- f1 , mid1 = faces [one ], face_mid [one ]
197- a , b , c = f1 [r , j ], f1 [r , (j + 1 ) % 3 ], f1 [r , (j + 2 ) % 3 ]
198- p = mid1 [r , j ]
199- faces_one = np .column_stack ((a , p , c , p , b , c )).reshape ((- 1 , 3 ))
200- index_one = np .repeat (index [one ], 2 )
201-
202- # 2 marked edges — rotate so the unsplit edge is (c, a), again so
203- # one template covers all three cases: a corner triangle [p, b, q]
204- # plus the quad (a, p, q, c) cut along its shorter diagonal
205- two = count == 2
206- j = (np .argmin (split [two ], axis = 1 ) + 1 ) % 3
207- r = np .arange (len (j ))
208- f2 , mid2 = faces [two ], face_mid [two ]
209- a , b , c = f2 [r , j ], f2 [r , (j + 1 ) % 3 ], f2 [r , (j + 2 ) % 3 ]
210- p , q = mid2 [r , j ], mid2 [r , (j + 1 ) % 3 ]
211- # the shorter of the two quad diagonals: a-q versus p-c (xyz only)
212- use_aq = (
213- ((vertices [a , :3 ] - vertices [q , :3 ]) ** 2 ).sum (axis = 1 )
214- <= ((vertices [p , :3 ] - vertices [c , :3 ]) ** 2 ).sum (axis = 1 )
215- )[:, None ]
216- corner = np .column_stack ((p , b , q ))
217- quad_a = np .where (use_aq , np .column_stack ((a , p , q )), np .column_stack ((a , p , c )))
218- quad_b = np .where (use_aq , np .column_stack ((a , q , c )), np .column_stack ((p , q , c )))
219- faces_two = np .vstack ((corner , quad_a , quad_b ))
220- index_two = np .tile (index [two ], 3 )
221-
222- # 3 marked edges — the regular 1 -> 4 split, same winding as subdivide
223- three = count == 3
224- f3 , mid3 = faces [three ], face_mid [three ]
225- v0 , v1 , v2 = f3 .T
226- m0 , m1 , m2 = mid3 .T
227- faces_three = np .column_stack (
228- (v0 , m0 , m2 , m0 , v1 , m1 , m2 , m1 , v2 , m0 , m1 , m2 )
229- ).reshape ((- 1 , 3 ))
230- index_three = np .repeat (index [three ], 4 )
231-
232- faces = np .vstack ((faces_keep , faces_one , faces_two , faces_three )).astype (
233- np .int64
234- )
235- index = np .concatenate ((index_keep , index_one , index_two , index_three ))
236-
237307 if return_index :
238- assert len (index ) == len (faces )
239- return vertices , faces , index
308+ assert len (current_index ) == len (current_faces )
309+ return current_vertices , current_faces , current_index
240310
241- return vertices , faces
311+ return current_vertices , current_faces
242312
243313
244314def subdivide_loop (vertices , faces , iterations = None ):
0 commit comments