|
| 1 | +"""MeshAutomaskingSettings move — a runnable example. |
| 2 | +
|
| 3 | +Witnesses the 5.2 move of sculpt automasking RNA off ``Brush`` into |
| 4 | +``MeshAutomaskingSettings``, reached via ``.mesh_automasking_settings``. |
| 5 | +The old ``Brush.use_automasking_*`` / ``Brush.automasking_*`` attributes |
| 6 | +are a bool/float you can read on 4.5 LTS and 5.1; the same getattr raises |
| 7 | +AttributeError on 5.2. Version-guarded code (current location, with a |
| 8 | +Brush fallback) reads on all three. |
| 9 | +
|
| 10 | +Pathology: no geometry, no gallery still. Factory-empty has zero brushes; |
| 11 | +the script creates one with ``bpy.data.brushes.new(..., mode="SCULPT")``. |
| 12 | +``mode="SCULPT"`` is load-bearing on 5.2: a default-mode ``new(name)`` |
| 13 | +leaves ``mesh_automasking_settings is None``. |
| 14 | +
|
| 15 | +Subset (location move + nested identifier shortening, not all 18+): |
| 16 | +
|
| 17 | +- ``use_automasking_topology`` — bool, same identifier in both places |
| 18 | +- ``use_automasking_cavity`` — bool, same identifier in both places |
| 19 | +- cavity factor — ``Brush.automasking_cavity_factor`` vs |
| 20 | + ``MeshAutomaskingSettings.cavity_factor`` |
| 21 | +
|
| 22 | + blender --background --python mesh_automasking_settings.py -- |
| 23 | + blender --background --python mesh_automasking_settings.py -- --assume-brush-attrs |
| 24 | +""" |
| 25 | +import argparse |
| 26 | +import sys |
| 27 | + |
| 28 | +import bpy |
| 29 | + |
| 30 | +MOVED_AT = (5, 2, 0) |
| 31 | +BRUSH_NAME = "ProbeBrush" |
| 32 | + |
| 33 | + |
| 34 | +def fail(msg, code): |
| 35 | + print(f"ERROR: {msg}", file=sys.stderr) |
| 36 | + return code |
| 37 | + |
| 38 | + |
| 39 | +def type_present(): |
| 40 | + return hasattr(bpy.types, "MeshAutomaskingSettings") |
| 41 | + |
| 42 | + |
| 43 | +def read_current(brush): |
| 44 | + """Version-safe reader. None mas falls through to the Brush attrs.""" |
| 45 | + mas = getattr(brush, "mesh_automasking_settings", None) |
| 46 | + if mas is not None: |
| 47 | + return ( |
| 48 | + mas.use_automasking_topology, |
| 49 | + mas.use_automasking_cavity, |
| 50 | + mas.cavity_factor, |
| 51 | + ) |
| 52 | + return ( |
| 53 | + brush.use_automasking_topology, |
| 54 | + brush.use_automasking_cavity, |
| 55 | + brush.automasking_cavity_factor, |
| 56 | + ) |
| 57 | + |
| 58 | + |
| 59 | +def build(): |
| 60 | + bpy.ops.wm.read_factory_settings(use_empty=True) |
| 61 | + n_before = len(bpy.data.brushes) |
| 62 | + brush = bpy.data.brushes.new(BRUSH_NAME, mode="SCULPT") |
| 63 | + return brush, n_before |
| 64 | + |
| 65 | + |
| 66 | +def check(brush, n_before, assume_brush_attrs=False): |
| 67 | + ver = tuple(bpy.app.version) |
| 68 | + legacy = ver < MOVED_AT |
| 69 | + require_brush_attrs = assume_brush_attrs or legacy |
| 70 | + print( |
| 71 | + f"blender={ver} assume_brush_attrs={assume_brush_attrs} " |
| 72 | + f"legacy={legacy} require_brush_attrs={require_brush_attrs} " |
| 73 | + f"n_brushes_before={n_before} created={BRUSH_NAME!r}" |
| 74 | + ) |
| 75 | + |
| 76 | + if brush is None or brush.bl_rna.identifier != "Brush": |
| 77 | + return fail("SCULPT brush was not created", 3) |
| 78 | + |
| 79 | + present = type_present() |
| 80 | + print(f"MeshAutomaskingSettings_type={present}") |
| 81 | + if legacy and present: |
| 82 | + return fail( |
| 83 | + f"MeshAutomaskingSettings exists on {ver}; it must not before 5.2", |
| 84 | + 4, |
| 85 | + ) |
| 86 | + if not legacy and not present: |
| 87 | + return fail( |
| 88 | + f"MeshAutomaskingSettings missing on {ver}; 5.2+ must define it", |
| 89 | + 4, |
| 90 | + ) |
| 91 | + |
| 92 | + has_topo = hasattr(brush, "use_automasking_topology") |
| 93 | + has_cavity = hasattr(brush, "use_automasking_cavity") |
| 94 | + has_factor = hasattr(brush, "automasking_cavity_factor") |
| 95 | + mas = getattr(brush, "mesh_automasking_settings", None) |
| 96 | + print( |
| 97 | + f"brush.use_automasking_topology={has_topo} " |
| 98 | + f"use_automasking_cavity={has_cavity} " |
| 99 | + f"automasking_cavity_factor={has_factor} " |
| 100 | + f"mesh_automasking_settings={mas!r}" |
| 101 | + ) |
| 102 | + |
| 103 | + if require_brush_attrs: |
| 104 | + if not (has_topo and has_cavity and has_factor): |
| 105 | + return fail( |
| 106 | + "old Brush automasking attributes missing when required " |
| 107 | + f"(topo={has_topo} cavity={has_cavity} factor={has_factor})", |
| 108 | + 5, |
| 109 | + ) |
| 110 | + try: |
| 111 | + topo = brush.use_automasking_topology |
| 112 | + cavity = brush.use_automasking_cavity |
| 113 | + factor = brush.automasking_cavity_factor |
| 114 | + except AttributeError as exc: |
| 115 | + return fail(f"old Brush getattr raised on {ver}: {exc}", 5) |
| 116 | + print( |
| 117 | + f"legacy_read topology={topo!r} cavity={cavity!r} " |
| 118 | + f"automasking_cavity_factor={factor!r}" |
| 119 | + ) |
| 120 | + if type(topo) is not bool or type(cavity) is not bool: |
| 121 | + return fail( |
| 122 | + f"legacy topology/cavity not bool: {type(topo).__name__}/" |
| 123 | + f"{type(cavity).__name__}", |
| 124 | + 5, |
| 125 | + ) |
| 126 | + if type(factor) is not float: |
| 127 | + return fail( |
| 128 | + f"legacy automasking_cavity_factor is " |
| 129 | + f"{type(factor).__name__}={factor!r}, not float", |
| 130 | + 5, |
| 131 | + ) |
| 132 | + else: |
| 133 | + if has_topo or has_cavity or has_factor: |
| 134 | + return fail( |
| 135 | + "old Brush automasking attributes still present on 5.2+", |
| 136 | + 6, |
| 137 | + ) |
| 138 | + if mas is None: |
| 139 | + return fail( |
| 140 | + "mesh_automasking_settings is None on 5.2+ " |
| 141 | + "(create with mode='SCULPT')", |
| 142 | + 6, |
| 143 | + ) |
| 144 | + |
| 145 | + try: |
| 146 | + topo, cavity, factor = read_current(brush) |
| 147 | + except AttributeError as exc: |
| 148 | + return fail(f"current-location read raised: {exc}", 7) |
| 149 | + print( |
| 150 | + f"current_read topology={topo!r} cavity={cavity!r} " |
| 151 | + f"cavity_factor={factor!r}" |
| 152 | + ) |
| 153 | + if type(topo) is not bool or type(cavity) is not bool: |
| 154 | + return fail( |
| 155 | + f"current topology/cavity not bool: {type(topo).__name__}/" |
| 156 | + f"{type(cavity).__name__}", |
| 157 | + 7, |
| 158 | + ) |
| 159 | + if type(factor) is not float: |
| 160 | + return fail( |
| 161 | + f"current cavity_factor is {type(factor).__name__}={factor!r}, " |
| 162 | + "not float", |
| 163 | + 7, |
| 164 | + ) |
| 165 | + return 0 |
| 166 | + |
| 167 | + |
| 168 | +def main(): |
| 169 | + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] |
| 170 | + p = argparse.ArgumentParser() |
| 171 | + p.add_argument( |
| 172 | + "--assume-brush-attrs", |
| 173 | + action="store_true", |
| 174 | + help="falsification: read automasking off Brush as if it never moved", |
| 175 | + ) |
| 176 | + args = p.parse_args(argv) |
| 177 | + |
| 178 | + brush, n_before = build() |
| 179 | + code = check(brush, n_before, assume_brush_attrs=args.assume_brush_attrs) |
| 180 | + if code: |
| 181 | + return code |
| 182 | + print("mesh-automasking-settings OK") |
| 183 | + return 0 |
| 184 | + |
| 185 | + |
| 186 | +if __name__ == "__main__": |
| 187 | + try: |
| 188 | + sys.exit(main()) |
| 189 | + except Exception as e: |
| 190 | + import traceback |
| 191 | + traceback.print_exc() |
| 192 | + print(f"FATAL: {e}", file=sys.stderr) |
| 193 | + sys.exit(1) |
0 commit comments