Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "blender-developer-tools",
"owner": {
"name": "TMHSDigital"
},
"metadata": {
"description": "Blender Python skills and rules for Cursor: correct bpy for Blender 5.2 LTS and 4.5 LTS."
},
"plugins": [
{
"name": "blender-developer-tools",
"source": "./",
"description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development"
}
]
}
180 changes: 2 additions & 178 deletions .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"author": {
"name": "TMHSDigital"
},
"homepage": "https://tmhsdigital.github.io/Blender-Developer-Tools/",
"repository": "https://github.com/TMHSDigital/Blender-Developer-Tools",
"license": "CC-BY-NC-ND-4.0 AND MIT",
"keywords": [
"cursor-plugin",
Expand Down Expand Up @@ -44,183 +46,5 @@
"rules/validate-imported-mesh-scale.mdc",
"rules/no-unapplied-modifiers-on-export.mdc",
"rules/use-correct-axis-rna-per-exporter.mdc"
],
"snippets": [
"snippets/action-ensure-channelbag-for-slot.py",
"snippets/app-handler-registration.py",
"snippets/bake_normal_high_to_low.py",
"snippets/bmesh-load-edit-free.py",
"snippets/canonical-object-creation.py",
"snippets/canonical-object-deletion.py",
"snippets/convex_hull_collider.py",
"snippets/cross-version-property-delete.py",
"snippets/decimate_to_budget.py",
"snippets/depsgraph-evaluated-mesh.py",
"snippets/driver-with-custom-function.py",
"snippets/export_preset_godot.py",
"snippets/export_preset_unity.py",
"snippets/export_preset_unreal.py",
"snippets/foreach-get-vertices.py",
"snippets/foreach-set-vertices.py",
"snippets/gltf_draco_export.py",
"snippets/lod_chain.py",
"snippets/pointerproperty-binding.py",
"snippets/principled-bsdf-material.py",
"snippets/register-classes-factory.py",
"snippets/save_baked_image.py",
"snippets/shader-node-group.py",
"snippets/setup_bake_target_image.py",
"snippets/temp-override-context.py",
"snippets/usd-export-evaluation-mode.py",
"snippets/version-branch-skeleton.py"
],
"templates": [
"templates/ai-asset-pipeline-template",
"templates/extension-addon-template",
"templates/headless-batch-script-template"
],
"examples": [
"examples/armature-bend",
"examples/attribute-domain-shear",
"examples/bake-normal-high-to-low",
"examples/bmesh-gear",
"examples/car-mirror-symmetry",
"examples/coincident-vert-weld",
"examples/collision-hull-proxy",
"examples/color-attribute-wheel",
"examples/compositor-glare",
"examples/cross-version-property-delete",
"examples/curve-bevel-arc",
"examples/custom-normals-shade",
"examples/damped-track-aim",
"examples/degenerate-bevel-weld",
"examples/depsgraph-export",
"examples/driver-wave",
"examples/eval-mesh-datablock-name",
"examples/exit-pre-sidecar",
"examples/export-preset-axis",
"examples/gltf-export-roundtrip",
"examples/gltf-skin-roundtrip",
"examples/gn-bundle-roundtrip",
"examples/gn-instance-grid",
"examples/gn-modifier-inputs",
"examples/gn-sdf-remesh",
"examples/gn-sim-fountain",
"examples/gn-socket-rename",
"examples/gn-zone-iterate",
"examples/gp-lineart-contour",
"examples/grease-pencil-rosette",
"examples/image-pixels-testcard",
"examples/light-link-studio",
"examples/lightmap-uv-channel",
"examples/lod-decimate-chain",
"examples/mesh-automasking-settings",
"examples/mesh-hygiene-audit",
"examples/modular-kit-snap",
"examples/ngon-triangulate",
"examples/parent-inverse-orrery",
"examples/png-exr-alpha",
"examples/prop-origin-transform",
"examples/shader-node-group",
"examples/shape-key-blend",
"examples/sky-texture-sun-elevation",
"examples/soccer-ball-goldberg",
"examples/socket-attach-points",
"examples/swatch-grid",
"examples/temp-override-join",
"examples/text-version-stamp",
"examples/triangulate-tangents",
"examples/turntable",
"examples/unapplied-scale-gltf",
"examples/usd-export-evaluation-mode",
"examples/uv-layer-grid",
"examples/vertex-color-ao",
"examples/vertex-weight-limit",
"examples/vse-cut-list",
"examples/vse-gamma-cross",
"examples/vse-linear-modifiers",
"examples/wave-displace",
"examples/ray-cast-space",
"examples/lattice-deform",
"examples/solidify-even-thickness",
"examples/boolean-exact-volume"
],
"showcase": [
"showcase/shipping-crate",
"showcase/stone-well",
"showcase/wooden-barrel",
"showcase/campfire",
"showcase/market-stall",
"showcase/street-lantern",
"showcase/treasure-chest",
"showcase/terrain-scatter",
"showcase/fence-kit",
"showcase/watchtower",
"showcase/cart",
"showcase/park-bench",
"showcase/wheelbarrow",
"showcase/anvil",
"showcase/water-trough",
"showcase/hitching-post",
"showcase/grindstone",
"showcase/hand-pump",
"showcase/signpost",
"showcase/chopping-block",
"showcase/wooden-bucket",
"showcase/wall-torch",
"showcase/tavern-stool",
"showcase/iron-cauldron",
"showcase/wooden-ladder",
"showcase/hay-bale",
"showcase/crate-stack",
"showcase/stone-archway",
"showcase/rope-bridge",
"showcase/grain-sacks",
"showcase/brazier",
"showcase/apothecary-shelf",
"showcase/wooden-yoke",
"showcase/butter-churn",
"showcase/bookshelf",
"showcase/book-trolley",
"showcase/basketball-hoop",
"showcase/pine-tree",
"showcase/desk-lamp",
"showcase/quad-drone",
"showcase/soccer-goal",
"showcase/fallen-log",
"showcase/office-chair",
"showcase/hover-bike",
"showcase/weight-rack",
"showcase/boulder-cluster",
"showcase/floor-fan",
"showcase/motor-scooter",
"showcase/bowling-pins",
"showcase/pond-edge",
"showcase/stand-mixer",
"showcase/cargo-loader",
"showcase/archery-target",
"showcase/cactus-garden",
"showcase/espresso-machine",
"showcase/road-bicycle",
"showcase/skate-ramp",
"showcase/palm-tree",
"showcase/wingback-armchair",
"showcase/planet-rover",
"showcase/traffic-cones",
"showcase/mushroom-stump",
"showcase/go-kart",
"showcase/broadleaf-oak",
"showcase/farm-tractor",
"showcase/fern-mossy-rock",
"showcase/sea-stack-arch",
"showcase/moka-pot",
"showcase/cricket-wicket",
"showcase/bamboo-clump",
"showcase/toboggan",
"showcase/sundial",
"showcase/mine-cart",
"showcase/garden-gate",
"showcase/ships-wheel",
"showcase/sawhorse-plank"
]
}
47 changes: 30 additions & 17 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ jobs:
import glob
import json
import os
import re
import sys

errors = []
Expand All @@ -174,33 +175,45 @@ jobs:
f"plugin.json version '{manifest.get('version')}' != VERSION '{version}'"
)

# Every manifest path must exist on disk.
for key in ('skills', 'rules', 'snippets', 'templates', 'examples', 'showcase'):
for path in manifest.get(key, []):
if not os.path.exists(path):
errors.append(f'{key}: manifest lists missing path {path}')

# Every content file/dir on disk must be listed in the manifest.
# Cursor's plugin schema (github.com/cursor/plugins schemas/
# plugin.schema.json) sets additionalProperties: false, so any other
# key makes the manifest invalid for Cursor. The old snippets /
# templates / examples / showcase inventory keys did exactly that (#347).
allowed = {'name', 'displayName', 'description', 'version',
'minClientVersions', 'author', 'publisher', 'homepage',
'repository', 'license', 'logo', 'keywords', 'category',
'tags', 'commands', 'agents', 'skills', 'rules', 'hooks',
'variables', 'mcpServers'}
for key in sorted(set(manifest) - allowed):
errors.append(f'plugin.json: {key!r} is not in the Cursor plugin schema')
if not re.fullmatch(r'[a-z0-9]([a-z0-9.-]*[a-z0-9])?', manifest.get('name', '')):
errors.append('plugin.json: name must be kebab-case')

# skills / rules: every listed path exists and every one on disk is listed.
expected = {
'skills': sorted(glob.glob('skills/*/SKILL.md')),
'rules': sorted(glob.glob('rules/*.mdc')),
'snippets': sorted(glob.glob('snippets/*.py')),
'templates': sorted(
d for d in glob.glob('templates/*') if os.path.isdir(d)
),
'examples': sorted(
d for d in glob.glob('examples/*') if os.path.isdir(d)
),
'showcase': sorted(
d for d in glob.glob('showcase/*') if os.path.isdir(d)
),
}
for key, paths in expected.items():
listed = {p.replace('\\', '/') for p in manifest.get(key, [])}
for path in listed:
if not os.path.exists(path):
errors.append(f'{key}: manifest lists missing path {path}')
for path in paths:
if path.replace('\\', '/') not in listed:
errors.append(f'{key}: {path} on disk but not in manifest')

# The marketplace that lets Cursor import this repo as a plugin.
market = json.load(open('.cursor-plugin/marketplace.json'))
for key in sorted(set(market) - {'name', 'owner', 'metadata', 'plugins'}):
errors.append(f'marketplace.json: {key!r} is not in the Cursor marketplace schema')
entries = market.get('plugins') or []
if [e.get('name') for e in entries] != [manifest.get('name')]:
errors.append('marketplace.json: must list exactly this plugin by name')
for e in entries:
for key in sorted(set(e) - {'name', 'source', 'description', 'minClientVersions'}):
errors.append(f'marketplace.json plugin entry: {key!r} not allowed')

# Every showcase piece names a category the gallery build knows.
sys.path.insert(0, 'scripts')
from build_gallery import CATEGORIES
Expand Down
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,10 @@ way, and a one-paragraph rationale. 30 to 80 lines is the right size.
examples, and showcase pieces) match filesystem reality. The counts language in `README.md`
is load-bearing: the job greps for it.
- `validate.yml` also runs a `validate-manifest` job that checks
`.cursor-plugin/plugin.json` against reality: every listed path must exist,
every skill, rule, snippet, template, example, and showcase piece on disk must be listed,
and the manifest `version` must equal `VERSION`. The release pipeline owns
`.cursor-plugin/plugin.json` against Cursor's plugin schema (no keys outside
it; Cursor rejects unknown ones) and against reality: every listed skill
and rule exists and every one on disk is listed, `marketplace.json` lists
this plugin, and the manifest `version` must equal `VERSION`. The release pipeline owns
the manifest `version` line (see `release.yml` below) — never hand-edit it.
- `blender-smoke.yml` executes every shipped example (check-only, no render)
plus snippet/template smoke tests inside REAL headless Blender, on
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ example still carries a `tests/smoke/catalog.json` row and a falsifier.
`examples/gallery.json` is the
gallery's source of truth. When authoring a new gallery example, copy the anatomy of
`examples/bmesh-gear/` (script structure, README shape, dark-studio render recipe) and
wire all of: gallery.json entry (with an `alt` sentence describing the still; the build fails without one), `.cursor-plugin/plugin.json` examples array (CI-gated),
wire all of: gallery.json entry (with an `alt` sentence describing the still; the build fails without one),
a `tests/smoke/catalog.json` row, hero webp (1280×720) in
`docs/gallery/assets/` + its 640×360 card variant `<name>-hero-640.webp` (`python scripts/make_thumbs.py`; the gallery build exits 5 without it) + preview webp (1200×675), then run `python scripts/build_gallery.py`.
A check-only example wires only the `.cursor-plugin/plugin.json` entry, the
`tests/smoke/catalog.json` row, and its README — it is deliberately absent from
A check-only example wires only the
`tests/smoke/catalog.json` row and its README — it is deliberately absent from
`examples/gallery.json` and `docs/gallery/`.
Renders must conform to the gallery look spec at `docs/VISUAL-STYLE.md`.
Render paths gate framing through the shared helper `examples/gallery_framing.py` —
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ budget conformance, never an API contract.
1. Add `showcase/<kebab-name>/` with a script, a README that includes an
exit-code table, and a falsifier that breaks one pipeline stage so a
**named** budget fails.
2. List the directory in `.cursor-plugin/plugin.json` `"showcase"` and add a
2. Add a
`tests/smoke/catalog.json` row with at least one entry in `falsifiers`
(`{"args": ["--your-flag"], "expect_exit": N}`, matching the README
falsifier table). The runner takes opaque script paths.
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ The content is consumed by AI coding agents reading these files directly from a
git clone https://github.com/TMHSDigital/Blender-Developer-Tools.git
```

- **Cursor** — open your project with this checkout available and copy (or symlink) `rules/*.mdc` into your project's `.cursor/rules/`. The rules apply automatically by glob scope; skills are referenced by name in chat.
- **Cursor** — install it as a local plugin from the slim `plugin-dist` branch, then run **Developer: Reload Window**. **Customize** lists the 16 skills and 9 rules; the rules apply by glob scope and the agent loads a skill when its description matches the task.

```bash
git clone --branch plugin-dist --single-branch https://github.com/TMHSDigital/Blender-Developer-Tools.git ~/.cursor/plugins/local/blender-developer-tools
```

(On Windows the folder is `%USERPROFILE%\.cursor\plugins\local\`. Cursor skips symlinks that point outside that folder, so clone or copy rather than link.) Teams can import the repository as a team marketplace instead; it carries `.cursor-plugin/marketplace.json`. For one project only, copy `skills/*` into the project's `.cursor/skills/` and `rules/*.mdc` into `.cursor/rules/` from a checkout. Copying only the rules gives you no skills.
- **Claude Code** — install as a plugin, then run `/skills` to see all 16 skills plus `blender-rules`:

```text
Expand All @@ -83,7 +89,8 @@ blender --background --python examples/bmesh-gear/bmesh_gear.py --
Releases ship often; [CHANGELOG.md](CHANGELOG.md) lists what each one changed.

- **Claude Code plugin** — `/plugin marketplace update blender-developer-tools` refreshes the marketplace and its plugins (from a shell: `claude plugin update blender-developer-tools@blender-developer-tools`). Remove it with `/plugin uninstall blender-developer-tools`, then `/plugin marketplace remove blender-developer-tools`.
- **Checkout** (Cursor rules, the optional always-on Claude rules import, examples) — `git pull` in the clone. Copied `.mdc` files do not update themselves: symlink `rules/*.mdc` into `.cursor/rules/` instead of copying, or re-copy after each pull. To uninstall, delete the copied or linked rules and the `@.../blender-rules.md` line from your `CLAUDE.md`.
- **Cursor local plugin** — `git pull` in `~/.cursor/plugins/local/blender-developer-tools`, then reload the window. Remove that folder to uninstall.
- **Checkout** (per-project Cursor copies, the optional always-on Claude rules import, examples) — `git pull` in the clone. Copied `.mdc` files do not update themselves: symlink `rules/*.mdc` into `.cursor/rules/` instead of copying, or re-copy after each pull. To uninstall, delete the copied or linked rules and the `@.../blender-rules.md` line from your `CLAUDE.md`.

## Supported Blender versions

Expand Down
15 changes: 13 additions & 2 deletions scripts/build_plugin_dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
as a self-contained marketplace (its marketplace.json points at "./"):

skills/ claude/ rules/ snippets/ templates/ LICENSE files
.claude-plugin/plugin.json + marketplace.json, and a short README
.claude-plugin/ and .cursor-plugin/ manifests, and a short README

python scripts/build_plugin_dist.py --out DIR # build (DIR must be absent or empty)
python scripts/build_plugin_dist.py --out DIR --force # replace a previous build in DIR
Expand All @@ -27,7 +27,10 @@

ROOT = Path(__file__).resolve().parent.parent
DIRS = ("skills", "claude", "rules", "snippets", "templates")
FILES = ("LICENSE", ".claude-plugin/plugin.json")
# The Cursor manifest ships too, so Cursor users can install the same slim
# tree as a local plugin (~/.cursor/plugins/local/) instead of the full repo.
FILES = ("LICENSE", ".claude-plugin/plugin.json",
".cursor-plugin/plugin.json", ".cursor-plugin/marketplace.json")
MAX_BYTES = 2_000_000
IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc")

Expand All @@ -43,6 +46,9 @@
/plugin marketplace add TMHSDigital/Blender-Developer-Tools@plugin-dist
/plugin install blender-developer-tools@blender-developer-tools
```

Cursor: clone this branch into `~/.cursor/plugins/local/blender-developer-tools`
and reload the window (Customize then lists the 16 skills and 9 rules).
"""


Expand Down Expand Up @@ -92,6 +98,11 @@ def verify(out: Path, size: int) -> list[str]:
errors.append(f"{d.relative_to(out)} has no SKILL.md")
if not (out / "claude" / "skills" / "blender-rules" / "SKILL.md").is_file():
errors.append("blender-rules skill missing")
cursor = json.loads((out / ".cursor-plugin" / "plugin.json").read_text(encoding="utf-8"))
for key in ("skills", "rules"):
for rel in cursor.get(key, []):
if not (out / rel).is_file():
errors.append(f"Cursor manifest {key} path {rel} missing from the dist")
if size > MAX_BYTES:
errors.append(f"dist is {size} bytes, over the {MAX_BYTES} budget")
return errors
Expand Down
Loading
Loading