diff --git a/examples/custom-normals-shade/custom_normals_shade.py b/examples/custom-normals-shade/custom_normals_shade.py index b24d24fb..940fa703 100644 --- a/examples/custom-normals-shade/custom_normals_shade.py +++ b/examples/custom-normals-shade/custom_normals_shade.py @@ -237,22 +237,26 @@ def build_shell(name): """Side band lofted along Y through wear-ring stations, capped by the two pressed faces. The face/band edge is a true 90-deg break.""" bm = bmesh.new() - wl = bm.verts.layers.float.new(WEAR) - prof = rrect(0.0) - n = len(prof) - rings = [] - for y, wear in ((-D / 2, 1.0), (-D / 2 + EDGE_BAND, 0.0), (0.0, 0.0), - (D / 2 - EDGE_BAND, 0.0), (D / 2, 1.0)): - ring = [bm.verts.new((x, y, z)) for x, z in prof] - for v in ring: - v[wl] = wear - rings.append(ring) - for ra, rb in zip(rings, rings[1:]): - for i in range(n): - j = (i + 1) % n - bm.faces.new((ra[i], ra[j], rb[j], rb[i])) - pressed_face(bm, wl, rings[0], -D / 2, -1.0) - pressed_face(bm, wl, rings[-1], D / 2, 1.0) + try: + wl = bm.verts.layers.float.new(WEAR) + prof = rrect(0.0) + n = len(prof) + rings = [] + for y, wear in ((-D / 2, 1.0), (-D / 2 + EDGE_BAND, 0.0), (0.0, 0.0), + (D / 2 - EDGE_BAND, 0.0), (D / 2, 1.0)): + ring = [bm.verts.new((x, y, z)) for x, z in prof] + for v in ring: + v[wl] = wear + rings.append(ring) + for ra, rb in zip(rings, rings[1:]): + for i in range(n): + j = (i + 1) % n + bm.faces.new((ra[i], ra[j], rb[j], rb[i])) + pressed_face(bm, wl, rings[0], -D / 2, -1.0) + pressed_face(bm, wl, rings[-1], D / 2, 1.0) + except BaseException: + bm.free() # finish_mesh() frees it on success + raise return finish_mesh(name, bm) @@ -264,27 +268,31 @@ def sweep(name, path, section, axis, closed=False, wear=0.0, matrix=None): pts = [Vector(p) for p in path] n = len(pts) bm = bmesh.new() - wl = bm.verts.layers.float.new(WEAR) - rings = [] - for i, p in enumerate(pts): - if closed: - t = (pts[(i + 1) % n] - pts[i - 1]) - else: - t = pts[min(i + 1, n - 1)] - pts[max(i - 1, 0)] - nrm = axis.cross(t.normalized()).normalized() # outward for CCW paths - ring = [bm.verts.new(p + nrm * u + axis * v) for u, v in section] - for v in ring: - v[wl] = wear - rings.append(ring) - k = len(section) - for i in range(n if closed else n - 1): - ra, rb = rings[i], rings[(i + 1) % n] - for j in range(k): - jj = (j + 1) % k - bm.faces.new((ra[j], ra[jj], rb[jj], rb[j])) - if not closed: - bm.faces.new(rings[0]) - bm.faces.new(rings[-1][::-1]) + try: + wl = bm.verts.layers.float.new(WEAR) + rings = [] + for i, p in enumerate(pts): + if closed: + t = (pts[(i + 1) % n] - pts[i - 1]) + else: + t = pts[min(i + 1, n - 1)] - pts[max(i - 1, 0)] + nrm = axis.cross(t.normalized()).normalized() # outward for CCW paths + ring = [bm.verts.new(p + nrm * u + axis * v) for u, v in section] + for v in ring: + v[wl] = wear + rings.append(ring) + k = len(section) + for i in range(n if closed else n - 1): + ra, rb = rings[i], rings[(i + 1) % n] + for j in range(k): + jj = (j + 1) % k + bm.faces.new((ra[j], ra[jj], rb[jj], rb[j])) + if not closed: + bm.faces.new(rings[0]) + bm.faces.new(rings[-1][::-1]) + except BaseException: + bm.free() # finish_mesh() frees it on success + raise return finish_mesh(name, bm, matrix) @@ -298,31 +306,35 @@ def lathe(name, profile, segments=16, closed=False, wear=None, matrix=None): profile makes a ring. `wear` is an optional per-profile-point list.""" wear = list(wear) if wear else [0.0] * len(profile) bm = bmesh.new() - wl = bm.verts.layers.float.new(WEAR) - bot = top = None - if not closed and profile[0][0] == 0.0: - bot = bm.verts.new((0.0, 0.0, profile[0][1])) - profile, wear = profile[1:], wear[1:] - if not closed and profile[-1][0] == 0.0: - top = bm.verts.new((0.0, 0.0, profile[-1][1])) - profile, wear = profile[:-1], wear[:-1] - rings = [] - for i in range(segments): - a = 2.0 * math.pi * i / segments - ring = [bm.verts.new((r * math.cos(a), r * math.sin(a), z)) for r, z in profile] - for v, w in zip(ring, wear): - v[wl] = w - rings.append(ring) - m = len(profile) - for i in range(segments): - j = (i + 1) % segments - for k in range(m if closed else m - 1): - kk = (k + 1) % m - bm.faces.new((rings[i][k], rings[j][k], rings[j][kk], rings[i][kk])) - if bot is not None: - bm.faces.new((rings[j][0], rings[i][0], bot)) - if top is not None: - bm.faces.new((rings[i][-1], rings[j][-1], top)) + try: + wl = bm.verts.layers.float.new(WEAR) + bot = top = None + if not closed and profile[0][0] == 0.0: + bot = bm.verts.new((0.0, 0.0, profile[0][1])) + profile, wear = profile[1:], wear[1:] + if not closed and profile[-1][0] == 0.0: + top = bm.verts.new((0.0, 0.0, profile[-1][1])) + profile, wear = profile[:-1], wear[:-1] + rings = [] + for i in range(segments): + a = 2.0 * math.pi * i / segments + ring = [bm.verts.new((r * math.cos(a), r * math.sin(a), z)) for r, z in profile] + for v, w in zip(ring, wear): + v[wl] = w + rings.append(ring) + m = len(profile) + for i in range(segments): + j = (i + 1) % segments + for k in range(m if closed else m - 1): + kk = (k + 1) % m + bm.faces.new((rings[i][k], rings[j][k], rings[j][kk], rings[i][kk])) + if bot is not None: + bm.faces.new((rings[j][0], rings[i][0], bot)) + if top is not None: + bm.faces.new((rings[i][-1], rings[j][-1], top)) + except BaseException: + bm.free() # finish_mesh() frees it on success + raise return finish_mesh(name, bm, matrix) diff --git a/rules/always-free-bmesh.mdc b/rules/always-free-bmesh.mdc index d82ab1d1..0e3eff85 100644 --- a/rules/always-free-bmesh.mdc +++ b/rules/always-free-bmesh.mdc @@ -61,6 +61,35 @@ class MyOperator(bpy.types.Operator): self.bm = bmesh.new() # leaks on operator unregister ``` +## Ownership transfer is allowed, if exactly one owner frees + +A function whose job is to build a BMesh may return it, and a builder may +hand it to a finisher, as long as every path has exactly one owner that frees +it: + +```python +# OK: a factory returns a new BMesh; the CALLER owns it and frees in finally +cb = cone_bm(tone) +try: + merge_bm(scene_bm, cb, matrix) +finally: + cb.free() + +# OK: build, then hand off to a finisher that frees in its own finally; +# free here only if the build itself fails +bm = bmesh.new() +try: + build_geometry(bm) +except BaseException: + bm.free() + raise +return finish_mesh(name, bm) # finish_mesh(): try: bm.to_mesh(me) finally: bm.free() +``` + +What stays wrong is a path with **no** owner: a caller that frees a returned +BMesh outside `finally`, a build step that can raise before the hand-off, +or an object attribute (`self.bm`) that nothing frees. + ## The required pattern ```python diff --git a/showcase/traffic-cones/traffic_cones.py b/showcase/traffic-cones/traffic_cones.py index 0bf3e3ca..4d23a11e 100644 --- a/showcase/traffic-cones/traffic_cones.py +++ b/showcase/traffic-cones/traffic_cones.py @@ -822,25 +822,31 @@ def build_cones(bm, flags): for i, (x, y, yaw) in enumerate(STANDING): cb = cone_bm(CONE_TONES[k], lean_deg=LEAN_DEG if (flags["lean_cone"] and i == 1) else 0.0, float_band=FLOAT_BAND if (flags["float_band"] and i == 0) else 0.0) - lift = FLOAT_CONE if (flags["float_cone"] and i == 2) else 0.0 - M = Matrix.Translation((x, y, SLAB_T - SEAT + lift)) @ Matrix.Rotation( - math.radians(yaw), 4, "Z") - merge_bm(bm, cb, M) - cb.free() + try: + lift = FLOAT_CONE if (flags["float_cone"] and i == 2) else 0.0 + M = Matrix.Translation((x, y, SLAB_T - SEAT + lift)) @ Matrix.Rotation( + math.radians(yaw), 4, "Z") + merge_bm(bm, cb, M) + finally: + cb.free() k += 1 for j, yaw in enumerate(STACK_YAW): cb = cone_bm(CONE_TONES[k]) - lift = LOOSE_STACK if (flags["loose_stack"] and j == len(STACK_YAW) - 1) else 0.0 - M = Matrix.Translation((STACK_XY[0], STACK_XY[1], - SLAB_T - SEAT + j * STACK_PITCH + lift)) @ Matrix.Rotation( - math.radians(yaw), 4, "Z") - merge_bm(bm, cb, M) - cb.free() + try: + lift = LOOSE_STACK if (flags["loose_stack"] and j == len(STACK_YAW) - 1) else 0.0 + M = Matrix.Translation((STACK_XY[0], STACK_XY[1], + SLAB_T - SEAT + j * STACK_PITCH + lift)) @ Matrix.Rotation( + math.radians(yaw), 4, "Z") + merge_bm(bm, cb, M) + finally: + cb.free() k += 1 cb = cone_bm(CONE_TONES[k]) - M, _phi = tipped_matrix(cb) - merge_bm(bm, cb, M) - cb.free() + try: + M, _phi = tipped_matrix(cb) + merge_bm(bm, cb, M) + finally: + cb.free() # -------------------------------------------------------------------------- @@ -999,8 +1005,10 @@ def keep(verts, tone): M = Matrix((X, d, n_out)).transposed().to_4x4() M.translation = C bb = board_bm(n_corner, 0.2 + 0.2 * bi + (0.4 if back else 0.0)) - merge_bm(bm, bb, M) - bb.free() + try: + merge_bm(bm, bb, M) + finally: + bb.free() ux = LEG_X + (LEG_SPLIT if back else -LEG_SPLIT) for su in (-1.0, 1.0): for sv_ in (-1.0, 1.0):