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
70 changes: 70 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Deploy GitHub Pages

# Builds the fleet landing page (docs/index.html, via the shared Developer-Tools-Directory
# template) AND the local examples gallery (docs/gallery/index.html, generated from
# examples/gallery.json by scripts/build_gallery.py). Both ship in the single docs/ artifact;
# the fleet build only writes docs/index.html + docs/fonts/ + docs/assets/, so it never
# clobbers docs/gallery/. When the fleet template gains examples support, the gallery step is
# retired and the data (examples/gallery.json) migrates onto the shared template.

on:
push:
branches: [main]
paths:
- "skills/**"
- "rules/**"
- "mcp-tools.json"
- "site.json"
- ".cursor-plugin/plugin.json"
- "assets/**"
- "examples/gallery.json"
- "docs/gallery/**"
- "scripts/build_gallery.py"
workflow_dispatch:

permissions:
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build-and-deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Checkout site template
uses: actions/checkout@v4
with:
repository: TMHSDigital/Developer-Tools-Directory
sparse-checkout: site-template
path: _template

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- run: pip install Jinja2

- name: Build fleet landing page
run: python _template/site-template/build_site.py --repo-root . --out docs

- name: Build local examples gallery (from examples/gallery.json)
# Stdlib-only; regenerates docs/gallery/index.html so the committed page can never
# drift from gallery.json. The committed docs/gallery/assets/*.webp are untouched.
run: python scripts/build_gallery.py

- uses: actions/configure-pages@v5

- uses: actions/upload-pages-artifact@v4
with:
path: docs

- uses: actions/deploy-pages@v5
id: deployment
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capabl

Runnable, smoke-gated demos live in [`examples/`](examples/) — each is executed headless on
both Blender 4.5 LTS and 5.1 by the `blender-smoke` workflow, so the screenshots reflect code
that actually runs.
that actually runs. Browse them in the
**[examples gallery](https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/)**.

<table>
<tr>
Expand Down
Binary file added docs/gallery/assets/gn-sdf-remesh-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/gallery/assets/swatch-grid-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/gallery/assets/turntable-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
93 changes: 93 additions & 0 deletions docs/gallery/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Examples Gallery — Blender Developer Tools</title>
<meta name="description" content="Runnable, smoke-gated Blender Python examples — each executed headless on Blender 4.5 LTS and 5.1, so every render reflects code that actually runs." />
<style>
:root {
--accent: #7c3aed; --accent-light: #a78bfa;
--bg: #0d1117; --bg2: #161b22; --card: #161b22;
--text: #e6edf3; --text-dim: #9da7b3; --border: #30363d;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(180deg, var(--bg), var(--bg2)); color: var(--text);
line-height: 1.6; min-height: 100vh; }
a { color: var(--accent-light); text-decoration: none; }
a:hover { text-decoration: underline; }
header { max-width: 1100px; margin: 0 auto; padding: 3rem 1.5rem 1.5rem; }
.back { font-size: 0.875rem; color: var(--text-dim); }
h1 { font-size: 2rem; margin: 0.75rem 0 0.5rem; }
header p { color: var(--text-dim); max-width: 60ch; }
main { max-width: 1100px; margin: 0 auto; padding: 1.5rem; display: grid;
grid-template-columns: 1fr; gap: 1.5rem; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px;
overflow: hidden; display: flex; flex-direction: column; }
.card-media { display: block; background: #0b0e13; }
.card-media img { display: block; width: 100%; height: auto; }
.card-body { padding: 1.25rem 1.5rem 1.5rem; }
.card-body h2 { font-size: 1.25rem; margin-bottom: 0.5rem; }
.card-body h2 a { color: var(--text); }
.teaches { color: var(--text); margin-bottom: 0.6rem; }
.witnesses { color: var(--text-dim); font-size: 0.9rem; margin-bottom: 0.9rem; }
.tag { display: inline-block; font-size: 0.72rem; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--accent-light); border: 1px solid var(--accent);
border-radius: 999px; padding: 0.05rem 0.5rem; margin-right: 0.35rem; }
.card-link { font-weight: 500; }
footer { max-width: 1100px; margin: 0 auto; padding: 2rem 1.5rem 3rem;
color: var(--text-dim); font-size: 0.85rem; border-top: 1px solid var(--border); }
@media (min-width: 720px) {
main { grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<header>
<a class="back" href="../">&larr; Blender Developer Tools</a>
<h1>Examples Gallery</h1>
<p>Runnable, smoke-gated demos. Each is executed headless on Blender 4.5 LTS and 5.1 by the
<code>blender-smoke</code> workflow, so every render reflects code that actually runs.</p>
</header>
<main>
<article class="card">
<a class="card-media" href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/swatch-grid">
<img src="assets/swatch-grid-hero.webp" alt="swatch-grid — Procedural Principled materials — metal and dielectric, the emission pattern, and the cross-version set_specular shim" loading="lazy" width="1280" />
</a>
<div class="card-body">
<h2><a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/swatch-grid">swatch-grid</a></h2>
<p class="teaches">Procedural Principled materials — metal and dielectric, the emission pattern, and the cross-version set_specular shim.</p>
<p class="witnesses"><span class="tag">witnesses</span> EEVEE engine-id mapping: BLENDER_EEVEE on 5.x, BLENDER_EEVEE_NEXT on 4.2–4.5.</p>
<a class="card-link" href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/swatch-grid">View example &rarr;</a>
</div>
</article>
<article class="card">
<a class="card-media" href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/turntable">
<img src="assets/turntable-hero.webp" alt="turntable — A slotted-actions Z-rotation turntable keyed through the cross-version channelbag path (get_channelbag_for_slot)" loading="lazy" width="1280" />
</a>
<div class="card-body">
<h2><a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/turntable">turntable</a></h2>
<p class="teaches">A slotted-actions Z-rotation turntable keyed through the cross-version channelbag path (get_channelbag_for_slot).</p>
<p class="witnesses"><span class="tag">witnesses</span> Slotted-actions boundary: ensure-helper channelbag on 5.x, strip.channelbag on 4.4/4.5.</p>
<a class="card-link" href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/turntable">View example &rarr;</a>
</div>
</article>
<article class="card">
<a class="card-media" href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-sdf-remesh">
<img src="assets/gn-sdf-remesh-hero.webp" alt="gn-sdf-remesh — A Geometry Nodes SDF remesh (MeshToSDFGrid → GridToMesh at the SDF zero-level), with a Set Material node carrying the material through the remesh" loading="lazy" width="1280" />
</a>
<div class="card-body">
<h2><a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-sdf-remesh">gn-sdf-remesh</a></h2>
<p class="teaches">A Geometry Nodes SDF remesh (MeshToSDFGrid → GridToMesh at the SDF zero-level), with a Set Material node carrying the material through the remesh.</p>
<p class="witnesses"><span class="tag">witnesses</span> An SDF grid is meshed with Grid to Mesh, not Volume to Mesh; GN geometry needs Set Material or it renders untextured.</p>
<a class="card-link" href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-sdf-remesh">View example &rarr;</a>
</div>
</article>
</main>
<footer>
Generated from <code>examples/gallery.json</code> by <code>scripts/build_gallery.py</code>.
&nbsp;&bull;&nbsp; CC-BY-NC-ND-4.0
</footer>
</body>
</html>
30 changes: 30 additions & 0 deletions examples/gallery.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"_comment": "FORWARD-COMPATIBLE SOURCE OF TRUTH for the examples gallery. The local page at docs/gallery/index.html is GENERATED from this file by scripts/build_gallery.py -- do not hand-edit the HTML. When the fleet template (Developer-Tools-Directory: site-template/build_site.py + template.html.j2) gains examples support (see ROADMAP: 'Fleet Pages examples support'), it reads this same file and the local page is retired. That migration is a lift-and-shift, not a rewrite: keep this schema stable. Per-entry schema: {name, dir, teaches, witnessesFix, hero, preview}; hero/preview/dir are repo-root-relative.",
"repoBaseUrl": "https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main",
"examples": [
{
"name": "swatch-grid",
"dir": "examples/swatch-grid",
"teaches": "Procedural Principled materials — metal and dielectric, the emission pattern, and the cross-version set_specular shim.",
"witnessesFix": "EEVEE engine-id mapping: BLENDER_EEVEE on 5.x, BLENDER_EEVEE_NEXT on 4.2–4.5.",
"hero": "docs/gallery/assets/swatch-grid-hero.webp",
"preview": "examples/swatch-grid/preview.webp"
},
{
"name": "turntable",
"dir": "examples/turntable",
"teaches": "A slotted-actions Z-rotation turntable keyed through the cross-version channelbag path (get_channelbag_for_slot).",
"witnessesFix": "Slotted-actions boundary: ensure-helper channelbag on 5.x, strip.channelbag on 4.4/4.5.",
"hero": "docs/gallery/assets/turntable-hero.webp",
"preview": "examples/turntable/preview.webp"
},
{
"name": "gn-sdf-remesh",
"dir": "examples/gn-sdf-remesh",
"teaches": "A Geometry Nodes SDF remesh (MeshToSDFGrid → GridToMesh at the SDF zero-level), with a Set Material node carrying the material through the remesh.",
"witnessesFix": "An SDF grid is meshed with Grid to Mesh, not Volume to Mesh; GN geometry needs Set Material or it renders untextured.",
"hero": "docs/gallery/assets/gn-sdf-remesh-hero.webp",
"preview": "examples/gn-sdf-remesh/preview.webp"
}
]
}
138 changes: 138 additions & 0 deletions scripts/build_gallery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""Generate the standalone examples gallery page from examples/gallery.json.

This page is a LOCAL, this-repo gallery that rides alongside the fleet-generated
docs/index.html (which build_site.py owns and overwrites). It writes ONLY to
docs/gallery/ so it never collides with the fleet build's docs/index.html,
docs/fonts/, or docs/assets/.

examples/gallery.json is the forward-compatible source of truth: when the fleet
template gains examples support, it consumes the same data and this script and
page are retired. Run after editing gallery.json:

python scripts/build_gallery.py

Stdlib only (no Jinja2), so the Pages workflow can regenerate it without extra deps.
"""
import html
import json
import sys
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent
DATA = REPO / "examples" / "gallery.json"
OUT = REPO / "docs" / "gallery" / "index.html"

CARD = """ <article class="card">
<a class="card-media" href="{href}">
<img src="{hero}" alt="{name} — {teaches_plain}" loading="lazy" width="1280" />
</a>
<div class="card-body">
<h2><a href="{href}">{name}</a></h2>
<p class="teaches">{teaches}</p>
<p class="witnesses"><span class="tag">witnesses</span> {witnesses}</p>
<a class="card-link" href="{href}">View example &rarr;</a>
</div>
</article>"""

PAGE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Examples Gallery — Blender Developer Tools</title>
<meta name="description" content="Runnable, smoke-gated Blender Python examples — each executed headless on Blender 4.5 LTS and 5.1, so every render reflects code that actually runs." />
<style>
:root {{
--accent: #7c3aed; --accent-light: #a78bfa;
--bg: #0d1117; --bg2: #161b22; --card: #161b22;
--text: #e6edf3; --text-dim: #9da7b3; --border: #30363d;
}}
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(180deg, var(--bg), var(--bg2)); color: var(--text);
line-height: 1.6; min-height: 100vh; }}
a {{ color: var(--accent-light); text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
header {{ max-width: 1100px; margin: 0 auto; padding: 3rem 1.5rem 1.5rem; }}
.back {{ font-size: 0.875rem; color: var(--text-dim); }}
h1 {{ font-size: 2rem; margin: 0.75rem 0 0.5rem; }}
header p {{ color: var(--text-dim); max-width: 60ch; }}
main {{ max-width: 1100px; margin: 0 auto; padding: 1.5rem; display: grid;
grid-template-columns: 1fr; gap: 1.5rem; }}
.card {{ background: var(--card); border: 1px solid var(--border); border-radius: 12px;
overflow: hidden; display: flex; flex-direction: column; }}
.card-media {{ display: block; background: #0b0e13; }}
.card-media img {{ display: block; width: 100%; height: auto; }}
.card-body {{ padding: 1.25rem 1.5rem 1.5rem; }}
.card-body h2 {{ font-size: 1.25rem; margin-bottom: 0.5rem; }}
.card-body h2 a {{ color: var(--text); }}
.teaches {{ color: var(--text); margin-bottom: 0.6rem; }}
.witnesses {{ color: var(--text-dim); font-size: 0.9rem; margin-bottom: 0.9rem; }}
.tag {{ display: inline-block; font-size: 0.72rem; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--accent-light); border: 1px solid var(--accent);
border-radius: 999px; padding: 0.05rem 0.5rem; margin-right: 0.35rem; }}
.card-link {{ font-weight: 500; }}
footer {{ max-width: 1100px; margin: 0 auto; padding: 2rem 1.5rem 3rem;
color: var(--text-dim); font-size: 0.85rem; border-top: 1px solid var(--border); }}
@media (min-width: 720px) {{
main {{ grid-template-columns: 1fr 1fr; }}
}}
</style>
</head>
<body>
<header>
<a class="back" href="../">&larr; Blender Developer Tools</a>
<h1>Examples Gallery</h1>
<p>Runnable, smoke-gated demos. Each is executed headless on Blender 4.5 LTS and 5.1 by the
<code>blender-smoke</code> workflow, so every render reflects code that actually runs.</p>
</header>
<main>
{cards}
</main>
<footer>
Generated from <code>examples/gallery.json</code> by <code>scripts/build_gallery.py</code>.
&nbsp;&bull;&nbsp; CC-BY-NC-ND-4.0
</footer>
</body>
</html>
"""


def strip_to_page_relative(repo_rel: str) -> str:
"""docs/gallery/assets/x.webp -> assets/x.webp (relative to the gallery page)."""
prefix = "docs/gallery/"
return repo_rel[len(prefix):] if repo_rel.startswith(prefix) else repo_rel


def main() -> int:
data = json.loads(DATA.read_text(encoding="utf-8"))
base = data["repoBaseUrl"].rstrip("/")
examples = data["examples"]
if not examples:
print("ERROR: no examples in gallery.json", file=sys.stderr)
return 2

cards = []
for ex in examples:
hero_rel = strip_to_page_relative(ex["hero"])
if not (REPO / ex["hero"]).is_file():
print(f"ERROR: hero image missing: {ex['hero']}", file=sys.stderr)
return 3
cards.append(CARD.format(
href=html.escape(f"{base}/{ex['dir']}"),
hero=html.escape(hero_rel),
name=html.escape(ex["name"]),
teaches=html.escape(ex["teaches"]),
teaches_plain=html.escape(ex["teaches"].split(".")[0]),
witnesses=html.escape(ex["witnessesFix"]),
))

OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(PAGE.format(cards="\n".join(cards)), encoding="utf-8")
print(f"Wrote {OUT} ({len(examples)} examples)")
return 0


if __name__ == "__main__":
sys.exit(main())
14 changes: 14 additions & 0 deletions site.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"accent": "#7c3aed",
"accentLight": "#a78bfa",
"heroGradientFrom": "#0d1117",
"heroGradientTo": "#161b22",
"links": {
"github": "https://github.com/TMHSDigital/Blender-Developer-Tools"
},
"installSteps": [
"Open Cursor IDE and go to <code>Settings</code> &gt; <code>Extensions</code>",
"Search for <code>Blender Developer Tools</code>",
"Click <code>Install</code> and reload"
]
}
Loading