Skip to content

Commit 6d9c75a

Browse files
TMHSDigitalclaude
andauthored
fix(rules): allow BMesh ownership transfer; make the handoff sites leak-proof (#438)
always-free-bmesh called `return bm` and builder hand-offs wrong, yet the codebase relies on both: traffic-cones' cone_bm()/board_bm() return a BMesh the caller frees, and custom-normals-shade builds a BMesh and hands it to finish_mesh(), which frees it in finally. The rule now allows transfer when every path has exactly one owner that frees, and names what stays wrong (an owner-less path). The sites themselves had owner-less paths: the four traffic-cones callers freed outside finally, and the three custom-normals builders could raise between bmesh.new() and the hand-off. Callers now free in finally; builders free on failure (except BaseException: free; raise) and hand off on success. Closes #401 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent fd832c5 commit 6d9c75a

3 files changed

Lines changed: 127 additions & 78 deletions

File tree

‎examples/custom-normals-shade/custom_normals_shade.py‎

Lines changed: 74 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -237,22 +237,26 @@ def build_shell(name):
237237
"""Side band lofted along Y through wear-ring stations, capped by the
238238
two pressed faces. The face/band edge is a true 90-deg break."""
239239
bm = bmesh.new()
240-
wl = bm.verts.layers.float.new(WEAR)
241-
prof = rrect(0.0)
242-
n = len(prof)
243-
rings = []
244-
for y, wear in ((-D / 2, 1.0), (-D / 2 + EDGE_BAND, 0.0), (0.0, 0.0),
245-
(D / 2 - EDGE_BAND, 0.0), (D / 2, 1.0)):
246-
ring = [bm.verts.new((x, y, z)) for x, z in prof]
247-
for v in ring:
248-
v[wl] = wear
249-
rings.append(ring)
250-
for ra, rb in zip(rings, rings[1:]):
251-
for i in range(n):
252-
j = (i + 1) % n
253-
bm.faces.new((ra[i], ra[j], rb[j], rb[i]))
254-
pressed_face(bm, wl, rings[0], -D / 2, -1.0)
255-
pressed_face(bm, wl, rings[-1], D / 2, 1.0)
240+
try:
241+
wl = bm.verts.layers.float.new(WEAR)
242+
prof = rrect(0.0)
243+
n = len(prof)
244+
rings = []
245+
for y, wear in ((-D / 2, 1.0), (-D / 2 + EDGE_BAND, 0.0), (0.0, 0.0),
246+
(D / 2 - EDGE_BAND, 0.0), (D / 2, 1.0)):
247+
ring = [bm.verts.new((x, y, z)) for x, z in prof]
248+
for v in ring:
249+
v[wl] = wear
250+
rings.append(ring)
251+
for ra, rb in zip(rings, rings[1:]):
252+
for i in range(n):
253+
j = (i + 1) % n
254+
bm.faces.new((ra[i], ra[j], rb[j], rb[i]))
255+
pressed_face(bm, wl, rings[0], -D / 2, -1.0)
256+
pressed_face(bm, wl, rings[-1], D / 2, 1.0)
257+
except BaseException:
258+
bm.free() # finish_mesh() frees it on success
259+
raise
256260
return finish_mesh(name, bm)
257261

258262

@@ -264,27 +268,31 @@ def sweep(name, path, section, axis, closed=False, wear=0.0, matrix=None):
264268
pts = [Vector(p) for p in path]
265269
n = len(pts)
266270
bm = bmesh.new()
267-
wl = bm.verts.layers.float.new(WEAR)
268-
rings = []
269-
for i, p in enumerate(pts):
270-
if closed:
271-
t = (pts[(i + 1) % n] - pts[i - 1])
272-
else:
273-
t = pts[min(i + 1, n - 1)] - pts[max(i - 1, 0)]
274-
nrm = axis.cross(t.normalized()).normalized() # outward for CCW paths
275-
ring = [bm.verts.new(p + nrm * u + axis * v) for u, v in section]
276-
for v in ring:
277-
v[wl] = wear
278-
rings.append(ring)
279-
k = len(section)
280-
for i in range(n if closed else n - 1):
281-
ra, rb = rings[i], rings[(i + 1) % n]
282-
for j in range(k):
283-
jj = (j + 1) % k
284-
bm.faces.new((ra[j], ra[jj], rb[jj], rb[j]))
285-
if not closed:
286-
bm.faces.new(rings[0])
287-
bm.faces.new(rings[-1][::-1])
271+
try:
272+
wl = bm.verts.layers.float.new(WEAR)
273+
rings = []
274+
for i, p in enumerate(pts):
275+
if closed:
276+
t = (pts[(i + 1) % n] - pts[i - 1])
277+
else:
278+
t = pts[min(i + 1, n - 1)] - pts[max(i - 1, 0)]
279+
nrm = axis.cross(t.normalized()).normalized() # outward for CCW paths
280+
ring = [bm.verts.new(p + nrm * u + axis * v) for u, v in section]
281+
for v in ring:
282+
v[wl] = wear
283+
rings.append(ring)
284+
k = len(section)
285+
for i in range(n if closed else n - 1):
286+
ra, rb = rings[i], rings[(i + 1) % n]
287+
for j in range(k):
288+
jj = (j + 1) % k
289+
bm.faces.new((ra[j], ra[jj], rb[jj], rb[j]))
290+
if not closed:
291+
bm.faces.new(rings[0])
292+
bm.faces.new(rings[-1][::-1])
293+
except BaseException:
294+
bm.free() # finish_mesh() frees it on success
295+
raise
288296
return finish_mesh(name, bm, matrix)
289297

290298

@@ -298,31 +306,35 @@ def lathe(name, profile, segments=16, closed=False, wear=None, matrix=None):
298306
profile makes a ring. `wear` is an optional per-profile-point list."""
299307
wear = list(wear) if wear else [0.0] * len(profile)
300308
bm = bmesh.new()
301-
wl = bm.verts.layers.float.new(WEAR)
302-
bot = top = None
303-
if not closed and profile[0][0] == 0.0:
304-
bot = bm.verts.new((0.0, 0.0, profile[0][1]))
305-
profile, wear = profile[1:], wear[1:]
306-
if not closed and profile[-1][0] == 0.0:
307-
top = bm.verts.new((0.0, 0.0, profile[-1][1]))
308-
profile, wear = profile[:-1], wear[:-1]
309-
rings = []
310-
for i in range(segments):
311-
a = 2.0 * math.pi * i / segments
312-
ring = [bm.verts.new((r * math.cos(a), r * math.sin(a), z)) for r, z in profile]
313-
for v, w in zip(ring, wear):
314-
v[wl] = w
315-
rings.append(ring)
316-
m = len(profile)
317-
for i in range(segments):
318-
j = (i + 1) % segments
319-
for k in range(m if closed else m - 1):
320-
kk = (k + 1) % m
321-
bm.faces.new((rings[i][k], rings[j][k], rings[j][kk], rings[i][kk]))
322-
if bot is not None:
323-
bm.faces.new((rings[j][0], rings[i][0], bot))
324-
if top is not None:
325-
bm.faces.new((rings[i][-1], rings[j][-1], top))
309+
try:
310+
wl = bm.verts.layers.float.new(WEAR)
311+
bot = top = None
312+
if not closed and profile[0][0] == 0.0:
313+
bot = bm.verts.new((0.0, 0.0, profile[0][1]))
314+
profile, wear = profile[1:], wear[1:]
315+
if not closed and profile[-1][0] == 0.0:
316+
top = bm.verts.new((0.0, 0.0, profile[-1][1]))
317+
profile, wear = profile[:-1], wear[:-1]
318+
rings = []
319+
for i in range(segments):
320+
a = 2.0 * math.pi * i / segments
321+
ring = [bm.verts.new((r * math.cos(a), r * math.sin(a), z)) for r, z in profile]
322+
for v, w in zip(ring, wear):
323+
v[wl] = w
324+
rings.append(ring)
325+
m = len(profile)
326+
for i in range(segments):
327+
j = (i + 1) % segments
328+
for k in range(m if closed else m - 1):
329+
kk = (k + 1) % m
330+
bm.faces.new((rings[i][k], rings[j][k], rings[j][kk], rings[i][kk]))
331+
if bot is not None:
332+
bm.faces.new((rings[j][0], rings[i][0], bot))
333+
if top is not None:
334+
bm.faces.new((rings[i][-1], rings[j][-1], top))
335+
except BaseException:
336+
bm.free() # finish_mesh() frees it on success
337+
raise
326338
return finish_mesh(name, bm, matrix)
327339

328340

‎rules/always-free-bmesh.mdc‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,35 @@ class MyOperator(bpy.types.Operator):
6161
self.bm = bmesh.new() # leaks on operator unregister
6262
```
6363

64+
## Ownership transfer is allowed, if exactly one owner frees
65+
66+
A function whose job is to build a BMesh may return it, and a builder may
67+
hand it to a finisher, as long as every path has exactly one owner that frees
68+
it:
69+
70+
```python
71+
# OK: a factory returns a new BMesh; the CALLER owns it and frees in finally
72+
cb = cone_bm(tone)
73+
try:
74+
merge_bm(scene_bm, cb, matrix)
75+
finally:
76+
cb.free()
77+
78+
# OK: build, then hand off to a finisher that frees in its own finally;
79+
# free here only if the build itself fails
80+
bm = bmesh.new()
81+
try:
82+
build_geometry(bm)
83+
except BaseException:
84+
bm.free()
85+
raise
86+
return finish_mesh(name, bm) # finish_mesh(): try: bm.to_mesh(me) finally: bm.free()
87+
```
88+
89+
What stays wrong is a path with **no** owner: a caller that frees a returned
90+
BMesh outside `finally`, a build step that can raise before the hand-off,
91+
or an object attribute (`self.bm`) that nothing frees.
92+
6493
## The required pattern
6594

6695
```python

‎showcase/traffic-cones/traffic_cones.py‎

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -822,25 +822,31 @@ def build_cones(bm, flags):
822822
for i, (x, y, yaw) in enumerate(STANDING):
823823
cb = cone_bm(CONE_TONES[k], lean_deg=LEAN_DEG if (flags["lean_cone"] and i == 1) else 0.0,
824824
float_band=FLOAT_BAND if (flags["float_band"] and i == 0) else 0.0)
825-
lift = FLOAT_CONE if (flags["float_cone"] and i == 2) else 0.0
826-
M = Matrix.Translation((x, y, SLAB_T - SEAT + lift)) @ Matrix.Rotation(
827-
math.radians(yaw), 4, "Z")
828-
merge_bm(bm, cb, M)
829-
cb.free()
825+
try:
826+
lift = FLOAT_CONE if (flags["float_cone"] and i == 2) else 0.0
827+
M = Matrix.Translation((x, y, SLAB_T - SEAT + lift)) @ Matrix.Rotation(
828+
math.radians(yaw), 4, "Z")
829+
merge_bm(bm, cb, M)
830+
finally:
831+
cb.free()
830832
k += 1
831833
for j, yaw in enumerate(STACK_YAW):
832834
cb = cone_bm(CONE_TONES[k])
833-
lift = LOOSE_STACK if (flags["loose_stack"] and j == len(STACK_YAW) - 1) else 0.0
834-
M = Matrix.Translation((STACK_XY[0], STACK_XY[1],
835-
SLAB_T - SEAT + j * STACK_PITCH + lift)) @ Matrix.Rotation(
836-
math.radians(yaw), 4, "Z")
837-
merge_bm(bm, cb, M)
838-
cb.free()
835+
try:
836+
lift = LOOSE_STACK if (flags["loose_stack"] and j == len(STACK_YAW) - 1) else 0.0
837+
M = Matrix.Translation((STACK_XY[0], STACK_XY[1],
838+
SLAB_T - SEAT + j * STACK_PITCH + lift)) @ Matrix.Rotation(
839+
math.radians(yaw), 4, "Z")
840+
merge_bm(bm, cb, M)
841+
finally:
842+
cb.free()
839843
k += 1
840844
cb = cone_bm(CONE_TONES[k])
841-
M, _phi = tipped_matrix(cb)
842-
merge_bm(bm, cb, M)
843-
cb.free()
845+
try:
846+
M, _phi = tipped_matrix(cb)
847+
merge_bm(bm, cb, M)
848+
finally:
849+
cb.free()
844850

845851

846852
# --------------------------------------------------------------------------
@@ -999,8 +1005,10 @@ def keep(verts, tone):
9991005
M = Matrix((X, d, n_out)).transposed().to_4x4()
10001006
M.translation = C
10011007
bb = board_bm(n_corner, 0.2 + 0.2 * bi + (0.4 if back else 0.0))
1002-
merge_bm(bm, bb, M)
1003-
bb.free()
1008+
try:
1009+
merge_bm(bm, bb, M)
1010+
finally:
1011+
bb.free()
10041012
ux = LEG_X + (LEG_SPLIT if back else -LEG_SPLIT)
10051013
for su in (-1.0, 1.0):
10061014
for sv_ in (-1.0, 1.0):

0 commit comments

Comments
 (0)