From 39713df0977e01d324384d68962c2cd2b00f2942 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 13 Oct 2026 00:40:20 +0200 Subject: [PATCH] smoke_bake: fixture gaps found by the hand mutants: commit-step rollback, bare-mesh rollback, linked Metallic, bake preferences The Source gets a linked Metallic and a Transmission Weight of 1, the bake is forced to fail at the wiring and replace steps and with meshes that have no material, and the cage extrusion and max ray distance preferences must move the normal map. All commits build; no C# touched. Co-Authored-By: Claude Opus 5.5 --- tools/blender/tests/smoke_bake.py | 77 +++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/tools/blender/tests/smoke_bake.py b/tools/blender/tests/smoke_bake.py index f782da7c5..face54528 100644 --- a/tools/blender/tests/smoke_bake.py +++ b/tools/blender/tests/smoke_bake.py @@ -223,42 +223,50 @@ def glb_image_sizes(path): return sorted(sizes) -class FailingBake: - """Stands in for bpy.ops.object.bake: the n-th call raises, the others run the real bake.""" +class FailingCall: + """Stands in for a function: the n-th call raises, the others run the real one.""" def __init__(self, real, fail_on): self.real, self.fail_on, self.calls = real, fail_on, 0 - def __call__(self, **arguments): + def __call__(self, *args, **arguments): self.calls += 1 if self.calls == self.fail_on: - raise RuntimeError("probe: the bake failed") - return self.real(**arguments) + raise RuntimeError("probe: forced failure") + return self.real(*args, **arguments) -def failing_bake_pass(aetheria_ships, label, targets, materials, sources, replace): - """A bake that raises on its second call (the normal map's first target) leaves the Source, the materials, the images, - the scene settings and the selection as they were.""" - real_bpy = aetheria_ships.bpy - patched = FailingBake(bpy.ops.object.bake, 2) - proxy = {name: getattr(bpy, name) for name in dir(bpy) if not name.startswith("__")} - proxy["ops"] = types.SimpleNamespace(object=types.SimpleNamespace(bake=patched)) +def failing_pass(aetheria_ships, label, targets, materials, sources, replace, site, fail_on): + """A run whose fail_on-th call of site raises (the Cycles bake, or one of the add-on's own commit steps) leaves the + Source, the materials, the images, the scene settings and the selection as they were.""" + real_bpy, real_site = aetheria_ships.bpy, getattr(aetheria_ships, site, None) + if site == "bake": + patched = FailingCall(bpy.ops.object.bake, fail_on) + proxy = {name: getattr(bpy, name) for name in dir(bpy) if not name.startswith("__")} + proxy["ops"] = types.SimpleNamespace(object=types.SimpleNamespace(bake=patched)) + replacement = {"bpy": types.SimpleNamespace(**proxy)} + else: + patched = FailingCall(real_site, fail_on) + replacement = {site: patched} def state(): return (wiring(materials), source_state(sources), scene_state(bpy.context.scene), sorted(i.session_uid for i in bpy.data.images), - [len(t.data.materials) for t in targets]) + [len(t.data.materials) for t in targets], sorted(m.name for m in bpy.data.materials)) before = state() - aetheria_ships.bpy = types.SimpleNamespace(**proxy) + for name, value in replacement.items(): + setattr(aetheria_ships, name, value) try: result, message = run(bpy.ops.aetheria.bake_from_source, replace=replace) finally: aetheria_ships.bpy = real_bpy - require(patched.calls == 2, f"{label}: the forced failure did not land on the second bake call ({patched.calls})") - require(result == {"CANCELLED"}, f"{label}: a failing bake was not refused") + if site != "bake": + setattr(aetheria_ships, site, real_site) + require(patched.calls >= fail_on, f"{label}: the forced failure never landed ({patched.calls} calls)") + require(result == {"CANCELLED"}, f"{label}: a failing run was not refused") after = state() - for part, name in enumerate(("wiring", "Source materials", "scene settings and selection", "images", "material slots")): - require(before[part] == after[part], f"{label}: a failed bake left the {name} changed") + for part, name in enumerate(("wiring", "Source materials", "scene settings and selection", "images", "material slots", "materials")): + require(before[part] == after[part], f"{label}: a failed run left the {name} changed") def refusal(label, expected, snapshot, offenders=None): @@ -300,6 +308,12 @@ def passes_one_to_three(aetheria_ships, scene): collection, source = import_fixture() bsdf = next(node for node in source.material_slots[0].material.node_tree.nodes if node.type == "BSDF_PRINCIPLED") require(bsdf.inputs["Metallic"].default_value == 1.0, "The importer no longer gives the fixture Metallic 1.0") + # A linked Metallic and a Transmission Weight darken the bake just as the value does; both are neutralised and restored. + source_tree = source.material_slots[0].material.node_tree + one = source_tree.nodes.new("ShaderNodeValue") + one.outputs[0].default_value = 1.0 + source_tree.links.new(one.outputs[0], bsdf.inputs["Metallic"]) + bsdf.inputs["Transmission Weight"].default_value = 1.0 select([source]) scene.aetheria_new_ship_id, scene.aetheria_new_ship_name = SHIP_ID, "Smoke Bake" scene.aetheria_new_ship_like, scene.aetheria_new_ship_length = "Djinni", 10 @@ -322,7 +336,8 @@ def passes_one_to_three(aetheria_ships, scene): sources = [source] # Pass 1: the albedo is not black, the normal is tangent space, and a failed bake changes nothing. - failing_bake_pass(aetheria_ships, "Pass 1 failing", [hull], [hull_material], sources, replace=False) + failing_pass(aetheria_ships, "Pass 1 failing bake", [hull], [hull_material], sources, False, "bake", 2) + failing_pass(aetheria_ships, "Pass 1 failing wiring", [hull], [hull_material], sources, False, "_link_into", 2) require(bake_images() == (None, None), "Pass 1: a failed bake left images behind") state_source, state_scene = source_state(sources), scene_state(scene) result, message = run(bpy.ops.aetheria.bake_from_source) @@ -342,6 +357,24 @@ def passes_one_to_three(aetheria_ships, scene): albedo, normal = check_images("Pass 2 evaluated", [hull_material.name]) check_maps("Pass 2 evaluated", albedo, normal) hull.modifiers.clear() + z_default = float(pixels(normal)[:, 2].mean()) + preferences = bpy.context.preferences.addons["aetheria_ships"].preferences + preferences.bake_cage_extrusion = 0.02 + result, message = run(bpy.ops.aetheria.bake_from_source, replace=True) + require(result == {"FINISHED"}, f"Pass 2: the 0.02 cage bake was refused: {message}") + z_tight = float(pixels(bake_images()[1])[:, 2].mean()) + preferences.bake_cage_extrusion = 0.1 + result, message = run(bpy.ops.aetheria.bake_from_source, replace=True) + require(result == {"FINISHED"}, f"Pass 2: the 0.1 cage bake was refused: {message}") + z_open = float(pixels(bake_images()[1])[:, 2].mean()) + preferences.bake_max_ray_distance = 1.0 + result, message = run(bpy.ops.aetheria.bake_from_source, replace=True) + require(result == {"FINISHED"}, f"Pass 2: the 1 m ray bake was refused: {message}") + z_short = float(pixels(bake_images()[1])[:, 2].mean()) + preferences.bake_cage_extrusion, preferences.bake_max_ray_distance = 0.05, 0.0 + print(f"PASS 2 preferences: normal z {z_tight:.4f} at cage 0.02, {z_default:.4f} at 0.05 (evaluated), {z_open:.4f} at 0.1, " + f"{z_short:.4f} at 0.1 with a 1 m ray") + require(z_tight > z_open + 0.03 and z_short < z_open - 0.02, "Pass 2: the cage extrusion and max ray distance do not decide the bake") wing, fin = make_render(collection, root, source, "Wing"), make_render(collection, root, source, "Fin") for obj in (wing, fin): @@ -354,6 +387,9 @@ def passes_one_to_three(aetheria_ships, scene): select([hull]) result, message = run(bpy.ops.aetheria.bake_from_source, replace=True) require(result == {"FINISHED"}, f"Pass 2: the left-half bake was refused: {message}") + select([hull, wing, fin], active=hull) + failing_pass(aetheria_ships, "Pass 2 failing with bare meshes", [hull, wing, fin], [hull_material], sources, True, "bake", 2) + select([hull]) alone = written(bake_images()[0]).reshape(SIZE[0], SIZE[0]) # rows are v, columns are u select([hull, wing, fin], active=hull) result, message = run(bpy.ops.aetheria.bake_from_source, replace=True) @@ -416,7 +452,8 @@ def passes_one_to_three(aetheria_ships, scene): require(result == {"CANCELLED"} and "confirm the replace" in message, f"Pass 3: a bake over wired images was not refused: {message}") require(wiring(materials) == before and (digest(old_albedo), digest(old_normal)) == old_digest and bake_images() == (old_albedo, old_normal), "Pass 3: a refused replace changed the images or the wiring") - failing_bake_pass(aetheria_ships, "Pass 3 failing replace", renders, materials, sources, replace=True) + failing_pass(aetheria_ships, "Pass 3 failing replace bake", renders, materials, sources, True, "bake", 2) + failing_pass(aetheria_ships, "Pass 3 failing replace wiring", renders, materials, sources, True, "_remove_node", 1) require(bake_images() == (old_albedo, old_normal) and (digest(old_albedo), digest(old_normal)) == old_digest, "Pass 3: a failed replace changed the old images") result, message = run(bpy.ops.aetheria.bake_from_source, replace=True)