diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json
index 7736cc5..8406623 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -60,8 +60,10 @@
],
"examples": [
"examples/depsgraph-export",
+ "examples/driver-wave",
"examples/gn-sdf-remesh",
"examples/swatch-grid",
- "examples/turntable"
+ "examples/turntable",
+ "examples/wave-displace"
]
}
diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml
index 532d03c..7e78cb8 100644
--- a/.github/workflows/blender-smoke.yml
+++ b/.github/workflows/blender-smoke.yml
@@ -155,3 +155,23 @@ jobs:
# > base. Exits non-zero on failure.
xvfb-run -a "$BLENDER" --background \
--python examples/depsgraph-export/depsgraph_export.py --
+
+ - name: Shipped example - wave displace (foreach bulk IO)
+ run: |
+ set -euo pipefail
+ # Frame-independent check only (no render): 9409 verts displaced via one
+ # foreach_get + one foreach_set; asserts count unchanged, Z span matches the
+ # amplitude, and a probe vertex matches the closed-form wave. Exits non-zero
+ # on failure.
+ xvfb-run -a "$BLENDER" --background \
+ --python examples/wave-displace/wave_displace.py --
+
+ - name: Shipped example - driver wave (driver_namespace + depsgraph)
+ run: |
+ set -euo pipefail
+ # Frame-independent check only (no render): sixteen SCRIPTED drivers call a
+ # driver_namespace function; asserts the driven values on the evaluated copy
+ # AND the flushed-back original both match the closed form. Exits non-zero
+ # on failure.
+ xvfb-run -a "$BLENDER" --background \
+ --python examples/driver-wave/driver_wave.py --
diff --git a/README.md b/README.md
index f24e155..b9b434b 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,34 @@ A depsgraph-evaluated export — builds a cube with `SUBSURF`, measures the eval
`evaluated_get().to_mesh()` / `to_mesh_clear()`, and asserts `wm.obj_export` ships the
modifier-applied geometry (exported vertex count == evaluated > base).
+
+
+
+
+
+
+
+
+### [wave-displace](examples/wave-displace/)
+
+Bulk vertex IO at real scale — 9,409 vertices displaced into a standing wave with **one
+`foreach_get` and one `foreach_set`**, no per-vertex access. Asserts the count is unchanged,
+the Z span matches the amplitude, and a probe vertex matches the closed-form wave.
+
+
+
+
+
+
+
+
+
+### [driver-wave](examples/driver-wave/)
+
+A `driver_namespace` function driving sixteen column heights through SCRIPTED drivers.
+Witnesses the evaluation contract: driven values appear after a view-layer update on the
+evaluated copy **and** the flushed-back original, and both must match the closed form.
+
diff --git a/docs/gallery/assets/driver-wave-hero.webp b/docs/gallery/assets/driver-wave-hero.webp
new file mode 100644
index 0000000..da69c42
Binary files /dev/null and b/docs/gallery/assets/driver-wave-hero.webp differ
diff --git a/docs/gallery/assets/wave-displace-hero.webp b/docs/gallery/assets/wave-displace-hero.webp
new file mode 100644
index 0000000..192c620
Binary files /dev/null and b/docs/gallery/assets/wave-displace-hero.webp differ
diff --git a/docs/gallery/driver-wave/index.html b/docs/gallery/driver-wave/index.html
new file mode 100644
index 0000000..3bffc92
--- /dev/null
+++ b/docs/gallery/driver-wave/index.html
@@ -0,0 +1,417 @@
+
+
+
+
+
+ driver-wave — Examples — Blender Developer Tools
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skip to content
+
A driver_namespace function driving sixteen column heights through SCRIPTED drivers — the sine skyline is entirely driver-evaluated.
+
+
+
+
Rendered headless by the example itself — click to zoom.
+
witnesses Driven values appear after a view-layer update in two places that must agree: the evaluated copy and the original datablock the animation system flushes for display.
A runnable example that drives sixteen column heights from a custom function registered in bpy.app.driver_namespace — the pattern from drivers-and-app-handlers. Each column gets a SCRIPTED driver on Z scale whose expression calls wave_scale(i), producing a sine skyline.
+
What it witnesses: the driver evaluation contract. Driven values appear only after a view-layer update, and they land in two places that must agree: the depsgraph-evaluated copy (evaluated_get(dg).scale) and the original datablock, which the animation system flushes for display. The check asserts both against the closed-form profile.
+
Note for real add-ons: driver_namespace entries do not persist in .blend files — re-register them from a load_post handler, or every driver that calls them fails on file open. Headless, registering before driver creation (as here) is enough.
+
Run
+
# Cheap correctness check (no render) — the CI check:
+blender --background --python driver_wave.py --
+
+# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
+blender --background --python driver_wave.py -- --output driver.png
+blender --background --python driver_wave.py -- --output driver.png --engine cycles
+
It exits non-zero on failure (driven value wrong, or the flush-back disagreed). The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1.
Bulk vertex IO at real scale — 9,409 vertices displaced into a standing wave with one foreach_get and one foreach_set, no per-vertex access.
+
witnesses The bulk path is correct, not just fast: vertex count unchanged, Z span matches the wave amplitude, probe vertex matches the closed form exactly.
A driver_namespace function driving sixteen column heights through SCRIPTED drivers — the sine skyline is entirely driver-evaluated.
+
witnesses Driven values appear after a view-layer update in two places that must agree: the evaluated copy and the original datablock the animation system flushes for display.