Skip to content

Commit 5cac0d6

Browse files
feat: add mesh-automasking-settings move example
Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ae3ab05 commit 5cac0d6

8 files changed

Lines changed: 293 additions & 9 deletions

File tree

.cursor-plugin/plugin.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"examples/light-link-studio",
111111
"examples/lightmap-uv-channel",
112112
"examples/lod-decimate-chain",
113+
"examples/mesh-automasking-settings",
113114
"examples/mesh-hygiene-audit",
114115
"examples/modular-kit-snap",
115116
"examples/ngon-triangulate",

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ a `.cursor-plugin/plugin.json` manifest so the ecosystem drift checker
2020
classifies it as a `cursor-plugin`. This is content the AI loads when the user
2121
asks Blender questions or works on Blender add-ons in Cursor or Claude Code.
2222

23-
The content base is 16 skills, 9 rules, 3 templates, 27 snippets, and 58
23+
The content base is 16 skills, 9 rules, 3 templates, 27 snippets, and 59
2424
examples (counts are CI-enforced against README.md and the manifest). The full
2525
inventory tables and per-item purposes live in `CLAUDE.md`. Example anatomy
2626
and authoring rules: copy `examples/bmesh-gear/`; the render look is specified
@@ -35,7 +35,7 @@ Blender-Developer-Tools/
3535
rules/<rule-name>.mdc # 9 rule files
3636
templates/<template-name>/ # 3 starter templates
3737
snippets/<snippet-name>.py # 27 standalone Python snippets
38-
examples/<name>/ # 58 runnable smoke-gated examples (+ gallery.json)
38+
examples/<name>/ # 59 runnable smoke-gated examples (+ gallery.json)
3939
examples/gallery_framing.py # shared Layer 1 framing measurement (render path only)
4040
scripts/build_gallery.py # generates docs/gallery/ (stdlib only)
4141
scripts/site/ # vendored landing-page build (build_site.py + template)

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ skills/<skill-name>/SKILL.md - AI workflow definitions, 16 total
2121
rules/<rule-name>.mdc - Anti-pattern rules, 9 total
2222
templates/<template-name>/ - Starter projects, 3 total
2323
snippets/<snippet-name>.py - Standalone code patterns, 27 total
24-
examples/<name>/ - Runnable smoke-gated examples, 58 total (+ gallery.json)
24+
examples/<name>/ - Runnable smoke-gated examples, 59 total (+ gallery.json)
2525
scripts/build_gallery.py - Regenerates docs/gallery/ from gallery.json (stdlib only)
2626
scripts/site/ - Vendored landing-page build (Jinja2)
2727
docs/gallery/ - Committed generated gallery pages + hero renders
@@ -99,7 +99,7 @@ v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namesp
9999

100100
AI asset pipeline track: `decimate_to_budget.py`, `convex_hull_collider.py`, `lod_chain.py` (helper duplicated, not imported), `gltf_draco_export.py`, `export_preset_unity.py`, `export_preset_godot.py`, `export_preset_unreal.py`, `setup_bake_target_image.py`, `bake_normal_high_to_low.py`, `save_baked_image.py`.
101101

102-
## Examples (58)
102+
## Examples (59)
103103

104104
Runnable scripts at `examples/<name>/`, each asserting a real API contract with
105105
deterministic checks (exit non-zero on failure) and optionally rendering a still via

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</p>
1919

2020
<p align="center">
21-
<strong>16 skills</strong> &nbsp;&bull;&nbsp; <strong>9 rules</strong> &nbsp;&bull;&nbsp; <strong>3 templates</strong> &nbsp;&bull;&nbsp; <strong>27 snippets</strong> &nbsp;&bull;&nbsp; <strong>58 examples</strong>
21+
<strong>16 skills</strong> &nbsp;&bull;&nbsp; <strong>9 rules</strong> &nbsp;&bull;&nbsp; <strong>3 templates</strong> &nbsp;&bull;&nbsp; <strong>27 snippets</strong> &nbsp;&bull;&nbsp; <strong>59 examples</strong>
2222
</p>
2323

2424
<p align="center">
@@ -36,7 +36,7 @@
3636

3737
## Overview
3838

39-
This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, and 58 examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable.
39+
This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, and 59 examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable.
4040

4141
The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly.
4242

@@ -601,7 +601,7 @@ and every orbit lands on its closed form.
601601
</details>
602602

603603
<details>
604-
<summary><strong>Context &amp; Grease Pencil</strong> — 4 examples</summary>
604+
<summary><strong>Context &amp; Grease Pencil</strong> — 5 examples</summary>
605605

606606
<table>
607607
<tr>
@@ -630,6 +630,21 @@ Custom ID properties are removed with `del id_block[key]`, not `property_unset`.
630630
IDs are built with `bpy.data.objects.new` — the snippet `__main__` keys off
631631
`active_object` and is dark headless. Same `del` on 4.5 LTS and 5.x.
632632

633+
</td>
634+
</tr>
635+
<tr>
636+
<td width="46%" valign="middle">
637+
check-only, no gallery still — no geometry
638+
</td>
639+
<td valign="middle">
640+
641+
### [mesh-automasking-settings](examples/mesh-automasking-settings/)
642+
643+
`MeshAutomaskingSettings` is absent on 4.5.11 and 5.1.2, present on 5.2.1.
644+
Old `Brush` automasking attributes are gone on 5.2; read
645+
`.mesh_automasking_settings` instead. `--assume-brush-attrs` is red only
646+
on 5.2.
647+
633648
</td>
634649
</tr>
635650
<tr>

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
143143
- ~~VSE `use_linear_modifiers` removal~~ **SHIPPED** as `examples/vse-linear-modifiers/` — bool on 4.5.11 and 5.1.2, `AttributeError` on 5.2.1; `--assume-present` red only on 5.2; pathology, no gallery still
144144
- ~~GN Compare / Random Value socket identifier rename~~ **SHIPPED** as `examples/gn-socket-rename/``A_INT` / `Min_001` present on 4.5.11 and 5.1.2, absent on 5.2.1; enabled-name lookup wires all three; `--legacy-ids` red only on 5.2
145145
- ~~Evaluated mesh datablock `evaluated_get().data.name` is generic `Mesh` on 4.5.11 and 5.1.2 and equals the source name on 5.2.1 (`to_mesh().name` stays the source name on all three)~~ **SHIPPED** as `examples/eval-mesh-datablock-name/``evaluated_get().data.name` is `Mesh` on 4.5.11 and 5.1.2, `SourceMesh` on 5.2.1; `to_mesh().name` stays `SourceMesh` on all three; `--assume-distinct-names` red only on 5.2; pathology, no gallery still
146-
- `MeshAutomaskingSettings` move: old `Brush` automasking attributes gone on 5.2
146+
- ~~`MeshAutomaskingSettings` move: old `Brush` automasking attributes gone on 5.2~~ **SHIPPED** as `examples/mesh-automasking-settings/` — type absent on 4.5.11 and 5.1.2, present on 5.2.1; old `Brush` attrs gone; `--assume-brush-attrs` red only on 5.2; pathology, no gallery still
147147
- ~~GAMMA_CROSS blend-curve witness~~ **SHIPPED** as `examples/vse-gamma-cross/` — the cross blends in a gamma-0.5 space: `((1-t)·√A + t·√B)²` with `t = (frame − start)/duration`, never 1 inside the effect; mid-cross dips 0.115 below the sRGB lerp from crimson/teal (closed form (0.341, 0.349, 0.463) confirmed per frame); AgX-default sampling poisons the fit (0.146 red-channel error, `view_transform='Standard'` mandatory); deleting a consumed input orphans-and-deletes the effect — follow-up to `vse-cut-list`
148148
- Falsy `bpy_prop_collection` trap snippet: an empty collection is falsy, so `editor.strips or editor.sequences` silently falls through to the legacy accessor on an empty timeline — always branch on `hasattr`; likely generalizes across the API (found authoring `vse-cut-list`)
149149
- ~~Collision compound witness~~ **SHIPPED** as `examples/collision-hull-proxy/` — game-prop collision as a compound of convex pieces, each a `bmesh.ops.convex_hull` of a coarse `sec(π/n)`-inflated cage (containment 4.4e-08, watertight, positive signed volume, Euler 2, per-piece 255-face budget: body 70, caps 60×3, compound 250); a hull of the dense render mesh measures 380 faces — over budget — which is why pipelines hull cages; proud details cost cage rows, concave grooves are free; byte-identical on 4.5.11 and 5.1.2
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Mesh automasking settings move
2+
3+
Pathology witness for the 5.2 move of sculpt automasking RNA off `Brush`
4+
into `MeshAutomaskingSettings`. There is no geometry and **no gallery
5+
still** (same class as [`vse-linear-modifiers`](../vse-linear-modifiers/),
6+
[`ngon-triangulate`](../ngon-triangulate/)).
7+
8+
Follows the version-gated assertion shape of
9+
[`gn-modifier-inputs`](../gn-modifier-inputs/) (per-version contract, exit
10+
0 on every matrix leg). Scaffolding matches
11+
[`cross-version-property-delete`](../cross-version-property-delete/)
12+
(`check()` returns, argparse naive-API flag, FATAL wrapper). That example
13+
does **not** version-branch, so the gate itself is copied from
14+
`gn-modifier-inputs`, not from `del`.
15+
16+
**Headless:** factory-empty has **zero** brushes. The script creates one
17+
with `bpy.data.brushes.new("ProbeBrush", mode="SCULPT")`. `mode="SCULPT"`
18+
is load-bearing on 5.2: a default-mode `new(name)` leaves
19+
`mesh_automasking_settings is None`.
20+
21+
**Subset** (not all 18+ attributes): `use_automasking_topology` and
22+
`use_automasking_cavity` keep their identifiers after the move; cavity
23+
factor does not (`Brush.automasking_cavity_factor` vs
24+
`MeshAutomaskingSettings.cavity_factor`). That pair covers the location
25+
move and the nested identifier shortening.
26+
27+
**What it witnesses:** `bpy.types.MeshAutomaskingSettings` is absent on
28+
4.5.11 and 5.1.2 and present on 5.2.1. Old `Brush.use_automasking_*` /
29+
`Brush.automasking_*` getattr works on 4.5/5.1 and is `AttributeError` on
30+
5.2. Reading `.mesh_automasking_settings` (with a Brush fallback) works
31+
on all three.
32+
33+
**What failure each check would catch:**
34+
35+
- exit 3 — SCULPT brush never landed
36+
- exit 4 — `MeshAutomaskingSettings` type presence wrong for this Blender
37+
- exit 5 — old Brush attributes missing when required
38+
(`--assume-brush-attrs` on 5.2 lands here)
39+
- exit 6 — old Brush attributes still present, or mas pointer is None, on 5.2+
40+
- exit 7 — current-location read raised or returned the wrong types
41+
42+
`--assume-brush-attrs` is the falsifier: skip the version gate and demand
43+
the 4.5 Brush RNA. It exits **0 on 4.5.11 and 5.1.2** (the old API still
44+
works) and **5 on 5.2.1**. That is unlike `--same-axis`, which is red on
45+
every binary.
46+
47+
No `SMOKE_SKIP`. Every matrix leg exercises the contract.
48+
49+
## Run
50+
51+
```bash
52+
blender --background --python mesh_automasking_settings.py --
53+
blender --background --python mesh_automasking_settings.py -- --assume-brush-attrs
54+
```
55+
56+
## Exit codes
57+
58+
Per-script sequential checks. `9` is a valid check code; there is no rule
59+
against it.
60+
61+
| Code | Meaning |
62+
| --- | --- |
63+
| 0 | Success |
64+
| 1 | Uncaught exception (FATAL wrapper) |
65+
| 2 | argparse / usage |
66+
| 3 | SCULPT brush was not created |
67+
| 4 | `MeshAutomaskingSettings` type presence wrong for this version |
68+
| 5 | Old Brush automasking attributes missing when required (`--assume-brush-attrs` on 5.2) |
69+
| 6 | Old Brush attributes still present, or mas is None, on 5.2+ |
70+
| 7 | Current-location read raised or returned the wrong types |
71+
72+
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
73+
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
74+
Smoke does not pass `--assume-brush-attrs`.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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)

tests/smoke/catalog.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,6 @@
7272
{"name": "bake-normal-high-to-low", "script": "examples/bake-normal-high-to-low/bake_normal_high_to_low.py"},
7373
{"name": "vse-linear-modifiers", "script": "examples/vse-linear-modifiers/vse_linear_modifiers.py"},
7474
{"name": "gn-socket-rename", "script": "examples/gn-socket-rename/gn_socket_rename.py"},
75-
{"name": "eval-mesh-datablock-name", "script": "examples/eval-mesh-datablock-name/eval_mesh_datablock_name.py"}
75+
{"name": "eval-mesh-datablock-name", "script": "examples/eval-mesh-datablock-name/eval_mesh_datablock_name.py"},
76+
{"name": "mesh-automasking-settings", "script": "examples/mesh-automasking-settings/mesh_automasking_settings.py"}
7677
]

0 commit comments

Comments
 (0)