diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 7900614..6423226 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -68,6 +68,7 @@ "examples/driver-wave", "examples/gn-instance-grid", "examples/gn-sdf-remesh", + "examples/grease-pencil-rosette", "examples/parent-inverse-orrery", "examples/shader-node-group", "examples/shape-key-blend", diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml index e7f02c5..92d5733 100644 --- a/.github/workflows/blender-smoke.yml +++ b/.github/workflows/blender-smoke.yml @@ -274,3 +274,15 @@ jobs: # on its closed form. Exits non-zero on failure. xvfb-run -a "$BLENDER" --background \ --python examples/parent-inverse-orrery/parent_inverse_orrery.py -- + + - name: Shipped example - grease pencil rosette (GPv3 attribute API) + run: | + set -euo pipefail + # Frame-independent check only (no render): five nested rose curves drawn + # as GPv3 strokes; asserts the version-gated datablock address (4.5: + # grease_pencils_v3 with grease_pencils still legacy; 5.x: grease_pencils + # is GPv3, legacy names gone), lazy attribute-layer materialization from + # point writes, and a closed-form round-trip of every position through + # the raw POINT attribute buffer. Exits non-zero on failure. + xvfb-run -a "$BLENDER" --background \ + --python examples/grease-pencil-rosette/grease_pencil_rosette.py -- diff --git a/AGENTS.md b/AGENTS.md index c659622..9f55e65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ The content base (counts are CI-enforced against README.md and the manifest): - 2 templates: `extension-addon-template` for Extensions Platform add-ons, and `headless-batch-script-template` for unattended batch jobs. - 17 snippets covering canonical patterns. -- 16 examples under `examples//`: runnable scripts that assert a real +- 17 examples under `examples//`: runnable scripts that assert a real API contract with deterministic checks, exit non-zero on failure, and optionally render a still via `--output`. Each is executed headless on Blender 4.5 LTS and 5.1 by `blender-smoke.yml`; its render ships in the @@ -41,7 +41,7 @@ Blender-Developer-Tools/ rules/.mdc # 6 rule files templates// # 2 starter templates snippets/.py # 17 standalone Python snippets - examples// # 16 runnable smoke-gated examples (+ gallery.json) + examples// # 17 runnable smoke-gated examples (+ gallery.json) scripts/build_gallery.py # generates docs/gallery/ (stdlib only) scripts/site/ # vendored landing-page build (build_site.py + template) docs/gallery/ # committed generated gallery pages + hero assets diff --git a/CLAUDE.md b/CLAUDE.md index f01b0c1..0564b28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ skills//SKILL.md - AI workflow definitions, 12 total rules/.mdc - Anti-pattern rules, 6 total templates// - Starter projects, 2 total snippets/.py - Standalone code patterns, 17 total -examples// - Runnable smoke-gated examples, 16 total (+ gallery.json) +examples// - Runnable smoke-gated examples, 17 total (+ gallery.json) scripts/build_gallery.py - Regenerates docs/gallery/ from gallery.json (stdlib only) scripts/site/ - Vendored landing-page build (Jinja2) docs/gallery/ - Committed generated gallery pages + hero renders @@ -80,11 +80,11 @@ v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namespace`, application handler registration, shader node group with cross-version `interface` API, `foreach_get` bulk vertex read, version-branch skeleton, and USD export with `evaluation_mode='RENDER'`. -## Examples (16) +## Examples (17) Runnable scripts at `examples//`, each asserting a real API contract with deterministic checks (exit non-zero on failure) and optionally rendering a still via -`--output`. All sixteen run headless on Blender 4.5 LTS and 5.1 in `blender-smoke.yml`; +`--output`. All seventeen run headless on Blender 4.5 LTS and 5.1 in `blender-smoke.yml`; their renders ship in the site gallery at `docs/gallery/`. `examples/gallery.json` is the gallery's source of truth. When authoring a new one, copy the anatomy of `examples/bmesh-gear/` (script structure, README shape, dark-studio render recipe) and diff --git a/README.md b/README.md index 6512c6a..bc0d673 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,14 @@

- 12 skills  •  6 rules  •  2 templates  •  17 snippets  •  16 examples + 12 skills  •  6 rules  •  2 templates  •  17 snippets  •  17 examples

--- ## Overview -This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 16 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support. +This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 17 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support. 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. @@ -280,6 +280,23 @@ carries arms, planets, and a two-level moon through spinning pivots. Asserts bar `.parent =` really teleports, `matrix_world` is stale until `view_layer.update()`, and every orbit lands on its closed form. + + + + +Grease pencil rosette: five nested neon rose curves drawn as tapered Grease Pencil v3 strokes, cyan through magenta to red, glowing against a dark studio wall with a soft blue halo + + + +### [grease-pencil-rosette](examples/grease-pencil-rosette/) + +Five nested rose curves drawn with the Grease Pencil v3 attribute API — layer → +`frames.new().drawing` → `add_strokes` → per-point position, radius, opacity, and +vertex color. Asserts the GPv3 address break: on 4.5 GPv3 is `grease_pencils_v3` +while `grease_pencils` is still legacy; on 5.x legacy is gone and GPv3 owns the +name. Point writes lazily materialize attribute layers, and every position +round-trips through the raw `POINT` buffer. + diff --git a/ROADMAP.md b/ROADMAP.md index 86dcfe3..11ead1b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -97,6 +97,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo - Refresh the `slotted-actions-animation` skill against any 5.2 changes - Bump `blender_version_min` in the templates if 5.2 APIs are used - Additional snippets for asset library scripting, EXR baking, multi-file extensions +- Gallery coverage follow-ups from the GPv3 review: `armature-bend` (edit_bones + vertex groups + pose evaluation) and `text-version-stamp` (TextCurve self-labeling renders); lower priority: light-linking, bulk `pixels.foreach_set`, VSE sequences-to-strips witness (`grease-pencil-rosette` shipped first as the strongest 4.x-to-5.x drift witness) ## Future (uncommitted) diff --git a/docs/gallery/assets/grease-pencil-rosette-hero.webp b/docs/gallery/assets/grease-pencil-rosette-hero.webp new file mode 100644 index 0000000..843ee32 Binary files /dev/null and b/docs/gallery/assets/grease-pencil-rosette-hero.webp differ diff --git a/docs/gallery/grease-pencil-rosette/index.html b/docs/gallery/grease-pencil-rosette/index.html new file mode 100644 index 0000000..6fb8d4a --- /dev/null +++ b/docs/gallery/grease-pencil-rosette/index.html @@ -0,0 +1,535 @@ + + + + + + grease-pencil-rosette — Examples — Blender Developer Tools + + + + + + + + + + + + + + + + + + +
+

grease-pencil-rosette

+

Grease Pencil v3's attribute-based API — layer → frames.new().drawing → add_strokes → per-point position/radius/opacity/vertex_color — drawing five nested neon rose curves.

+
+
+ +

Rendered headless by the example itself — click to zoom.

+
witnesses The GPv3 address break: 4.5 keeps GPv3 at grease_pencils_v3 while grease_pencils is still legacy GPencil (frame.strokes, no .drawing); 5.x deletes legacy and GPv3 takes over the grease_pencils name. Point writes lazily materialize attribute layers, and every position round-trips through the raw POINT buffer.
+
+
blender --background --python examples/grease-pencil-rosette/grease_pencil_rosette.py --
+ +
+
+

A runnable example that draws five nested neon rose curves as Grease Pencil v3 strokes — layer → frames.new(1).drawingadd_strokes([counts]) → per-point position, radius, opacity, vertex_color — the attribute-based API that replaced legacy GPencil across the 4.x-to-5.x window, the largest bpy API break the gallery witnesses.

+

What it witnesses: GPv3 lives at *different addresses* on the two supported versions, and the check asserts each side's actual contract:

+
  • Blender 4.5 LTS — GPv3 is bpy.data.grease_pencils_v3 (GreasePencilv3), while bpy.data.grease_pencils still holds legacy GPencil whose frames carry .strokes directly and have no .drawing. Same collection name, incompatible API — the trap is asserted, not just avoided.
  • Blender 5.x — legacy is gone: GPv3 took over the bpy.data.grease_pencils name, and grease_pencils_v3 / bpy.types.GPencilStroke no longer exist.
+

On the shared GPv3 surface it then asserts that stroke points are *views over attribute layers*: writing pt.radius / pt.opacity / pt.vertex_color lazily materializes radius, opacity, vertex_color (POINT) and cyclic (CURVE) layers in drawing.attributes, and every closed-form rose position round-trips exactly through the raw position attribute buffer via foreach_get.

+

Run

+
# Cheap correctness check (no render) — the CI check:
+blender --background --python grease_pencil_rosette.py --
+
+# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
+blender --background --python grease_pencil_rosette.py -- --output rosette.png
+blender --background --python grease_pencil_rosette.py -- --output rosette.png --engine cycles
+

It exits non-zero on failure (wrong version gate, structural mismatch, missing attribute layers, or attribute-buffer deviation from the closed form). The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1.

+
+
+

Source

+
+ examples/grease-pencil-rosette/grease_pencil_rosette.py + View on GitHub → +
+
"""A neon rose-curve set drawn with GPv3 strokes — a runnable example.
+
+Witnesses the Grease Pencil v3 rewrite, the largest bpy API break in the
+4.x-to-5.x window. GPv3 is present on both supported versions but lives at
+DIFFERENT addresses:
+
+- Blender 4.5 LTS: GPv3 is `bpy.data.grease_pencils_v3` (`GreasePencilv3`);
+  `bpy.data.grease_pencils` still holds LEGACY GPencil datablocks whose frames
+  carry `.strokes` directly — same collection name, incompatible API.
+- Blender 5.x: legacy is gone, GPv3 took over the `bpy.data.grease_pencils`
+  name (`GreasePencil`), and `grease_pencils_v3` / `GPencilStroke` no longer
+  exist.
+
+The shared GPv3 surface is attribute-based: layer -> frames.new(n).drawing ->
+add_strokes([counts]) -> point.position/radius/opacity/vertex_color, where
+stroke points are views over attribute layers that materialize lazily in
+`drawing.attributes`. The check asserts the address divergence on each side,
+the legacy trap on 4.5, the structural contract, lazy attribute
+materialization, and a closed-form round-trip of every position through the
+raw POINT attribute buffer.
+
+By default it runs only the correctness check (no render) — the CI smoke
+check. Pass --output to also render a still:
+
+    blender --background --python grease_pencil_rosette.py --                # check only
+    blender --background --python grease_pencil_rosette.py -- --output r.png # + render
+"""
+import bpy, sys, os, math, argparse, colorsys
+
+RINGS = 5           # nested rose curves, one stroke each
+POINTS = 192        # samples per stroke
+R_OUTER = 1.55      # radius of the outermost rose
+BASE_RADIUS = 0.02  # base line half-width in world units
+TOL = 1e-4
+
+
+def rose_point(ring, i):
+    """Closed-form sample i of ring's rose curve r = a*(0.72 + 0.28*cos(k*t)),
+    laid out upright in the XZ plane. Single source of truth for build & check."""
+    k = 3 + ring                          # petal frequency
+    a = R_OUTER * (1.0 - ring / (RINGS + 1.5))
+    phase = ring * math.pi / 7.0
+    t = 2.0 * math.pi * i / POINTS
+    r = a * (0.72 + 0.28 * math.cos(k * t))
+    return (r * math.cos(t + phase), 0.0, r * math.sin(t + phase))
+
+
+def point_radius(ring, i):
+    """Calligraphic taper: width swells on the petal tips."""
+    k = 3 + ring
+    t = 2.0 * math.pi * i / POINTS
+    return BASE_RADIUS * (0.55 + 1.45 * (0.5 + 0.5 * math.cos(k * t)))
+
+
+def ring_color(ring, i):
+    """Neon hue per ring, drifting slightly along the stroke."""
+    h = (0.52 + ring / RINGS * 0.55 + 0.04 * math.sin(2 * math.pi * i / POINTS)) % 1.0
+    r, g, b = colorsys.hsv_to_rgb(h, 0.96, 1.0)
+    return (r, g, b, 1.0)
+
+
+def gp_data_new(name):
+    """THE version gate this example exists for: GPv3 datablock creation."""
+    if bpy.app.version >= (5, 0, 0):
+        return bpy.data.grease_pencils.new(name)      # GPv3 owns the name in 5.x
+    return bpy.data.grease_pencils_v3.new(name)       # 4.5 LTS: GPv3 lives at _v3
+
+
+def build_rosette():
+    bpy.ops.wm.read_factory_settings(use_empty=True)
+    gp = gp_data_new("Rosette")
+    layer = gp.layers.new("Ink")
+    frame = layer.frames.new(1)
+    drawing = frame.drawing
+
+    drawing.add_strokes([POINTS] * RINGS)
+    for ring, stroke in enumerate(drawing.strokes):
+        stroke.cyclic = True
+        for i, pt in enumerate(stroke.points):
+            pt.position = rose_point(ring, i)
+            pt.radius = point_radius(ring, i)
+            pt.opacity = 1.0
+            pt.vertex_color = ring_color(ring, i)
+
+    mat = bpy.data.materials.new("Neon Ink")
+    bpy.data.materials.create_gpencil_data(mat)       # same helper on 4.5 and 5.x
+    mat.grease_pencil.color = (1.0, 1.0, 1.0, 1.0)    # vertex colors carry the hue
+    gp.materials.append(mat)
+
+    obj = bpy.data.objects.new("Rosette", gp)
+    bpy.context.collection.objects.link(obj)
+    return obj
+
+
+def check_version_gate():
+    """Assert the API break each side actually exposes."""
+    if bpy.app.version >= (5, 0, 0):
+        if hasattr(bpy.data, "grease_pencils_v3") or hasattr(bpy.types, "GPencilStroke"):
+            print("ERROR: 5.x still exposes legacy GP names — gate is wrong", file=sys.stderr)
+            return 3
+        print("5.x contract: grease_pencils is GPv3; _v3 alias and GPencilStroke are gone")
+    else:
+        if not hasattr(bpy.data, "grease_pencils_v3") or not hasattr(bpy.types, "GPencilStroke"):
+            print("ERROR: 4.5 is missing grease_pencils_v3 or legacy GPencilStroke", file=sys.stderr)
+            return 3
+        # The trap: on 4.5 `bpy.data.grease_pencils` is LEGACY GPencil. Its frames
+        # carry `.strokes` directly and have no `.drawing` — code written for one
+        # API fails on the other despite the identical collection name.
+        legacy = bpy.data.grease_pencils.new("_legacy_probe")
+        try:
+            lframe = legacy.layers.new("L").frames.new(1)
+            if hasattr(lframe, "drawing") or not hasattr(lframe, "strokes"):
+                print("ERROR: 4.5 grease_pencils did not behave as legacy GPencil", file=sys.stderr)
+                return 3
+        finally:
+            bpy.data.grease_pencils.remove(legacy)
+        print("4.5 contract: grease_pencils is legacy (frame.strokes); GPv3 lives at _v3")
+    return 0
+
+
+def check(obj):
+    code = check_version_gate()
+    if code:
+        return code
+
+    if obj.type != 'GREASEPENCIL':
+        print(f"ERROR: object type {obj.type!r} != 'GREASEPENCIL'", file=sys.stderr)
+        return 4
+
+    gp = obj.data
+    if len(gp.layers) != 1:
+        print(f"ERROR: {len(gp.layers)} layers != 1", file=sys.stderr)
+        return 4
+    frame = gp.layers[0].frames[0]
+    if frame.frame_number != 1:
+        print(f"ERROR: frame_number {frame.frame_number} != 1", file=sys.stderr)
+        return 4
+    drawing = frame.drawing
+
+    strokes = drawing.strokes
+    if len(strokes) != RINGS or any(len(s.points) != POINTS for s in strokes):
+        print(f"ERROR: stroke topology != {RINGS} x {POINTS}", file=sys.stderr)
+        return 5
+    if not all(s.cyclic for s in strokes):
+        print("ERROR: not every stroke is cyclic", file=sys.stderr)
+        return 5
+
+    # Lazy materialization: writing through the point view must have created
+    # these attribute layers on the drawing (they are absent on a fresh drawing).
+    attrs = {a.name: (a.domain, a.data_type) for a in drawing.attributes}
+    expected = {
+        "position": ('POINT', 'FLOAT_VECTOR'),
+        "radius": ('POINT', 'FLOAT'),
+        "opacity": ('POINT', 'FLOAT'),
+        "vertex_color": ('POINT', 'FLOAT_COLOR'),
+        "cyclic": ('CURVE', 'BOOLEAN'),
+    }
+    for name, sig in expected.items():
+        if attrs.get(name) != sig:
+            print(f"ERROR: attribute {name!r} is {attrs.get(name)} != {sig}", file=sys.stderr)
+            return 6
+
+    # Round-trip: the raw POINT attribute buffer must hold every closed-form
+    # position — stroke points are views over this buffer, not copies.
+    pos = drawing.attributes["position"]
+    n = len(pos.data)
+    if n != RINGS * POINTS:
+        print(f"ERROR: position buffer {n} points != {RINGS * POINTS}", file=sys.stderr)
+        return 7
+    buf = [0.0] * (3 * n)
+    pos.data.foreach_get("vector", buf)
+    worst = 0.0
+    for ring in range(RINGS):
+        for i in range(POINTS):
+            j = 3 * (ring * POINTS + i)
+            ex, ey, ez = rose_point(ring, i)
+            worst = max(worst, abs(buf[j] - ex), abs(buf[j + 1] - ey), abs(buf[j + 2] - ez))
+    if worst > TOL:
+        print(f"ERROR: attribute buffer deviates {worst} > {TOL} from closed form", file=sys.stderr)
+        return 7
+
+    if len(gp.materials) != 1 or gp.materials[0].grease_pencil is None:
+        print("ERROR: grease pencil material missing its gpencil settings", file=sys.stderr)
+        return 8
+
+    print(f"rings={RINGS} points/stroke={POINTS} attrs=lazy-materialized "
+          f"round-trip worst={worst:.2e} object=GREASEPENCIL")
+    return 0
+
+
+def eevee_engine_id():
+    return 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT'
+
+
+def render_still(obj, path, engine):
+    scene = bpy.context.scene
+    obj.data.layers[0].use_lights = False   # neon ink stays unlit and vivid
+    obj.location = (0.0, 0.0, 1.9)
+    obj.rotation_euler = (math.radians(4), 0.0, 0.0)
+    # unlit saturated ink wants the graphic transform, not AgX's filmic desaturation
+    scene.view_settings.view_transform = 'Standard'
+
+    import bmesh
+    floor_me = bpy.data.meshes.new("Floor")
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=30.0)
+        bm.to_mesh(floor_me)
+    finally:
+        bm.free()
+    fmat = bpy.data.materials.new("Studio")
+    fmat.use_nodes = True
+    fb = fmat.node_tree.nodes["Principled BSDF"]
+    fb.inputs["Base Color"].default_value = (0.045, 0.05, 0.065, 1.0)
+    fb.inputs["Roughness"].default_value = 0.35
+    floor_me.materials.append(fmat)
+    floor = bpy.data.objects.new("Floor", floor_me)
+    scene.collection.objects.link(floor)
+    wall = bpy.data.objects.new("Wall", floor_me.copy())
+    wall.location = (0.0, 6.0, 0.0)
+    wall.rotation_euler = (math.radians(90), 0.0, 0.0)
+    scene.collection.objects.link(wall)
+
+    world = bpy.data.worlds.new("World")
+    world.use_nodes = True
+    world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.01, 0.012, 0.02, 1.0)
+    scene.world = world
+
+    def light(name, loc, energy, size, col, rot):
+        ld = bpy.data.lights.new(name, 'AREA')
+        ld.energy = energy; ld.size = size; ld.color = col
+        ob = bpy.data.objects.new(name, ld)
+        ob.location = loc
+        ob.rotation_euler = tuple(math.radians(a) for a in rot)
+        scene.collection.objects.link(ob)
+
+    # the strokes are unlit; this only paints a soft halo on the wall behind them
+    light("Halo", (0.0, 3.2, 1.9), 260.0, 7.0, (0.4, 0.45, 1.0), (90, 0, 0))
+
+    cam_data = bpy.data.cameras.new("Cam")
+    cam_data.lens = 50.0
+    cam = bpy.data.objects.new("Cam", cam_data)
+    cam.location = (0.0, -9.5, 1.9)
+    cam.rotation_euler = (math.radians(90), 0.0, 0.0)
+    scene.collection.objects.link(cam)
+    scene.camera = cam
+
+    scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
+    if engine == 'cycles':
+        scene.cycles.samples = 32
+    else:
+        try:
+            scene.eevee.taa_render_samples = 64
+        except AttributeError:
+            pass
+    scene.render.resolution_x = 1280
+    scene.render.resolution_y = 720
+    scene.render.image_settings.file_format = 'PNG'
+    scene.render.filepath = path
+    bpy.ops.render.render(write_still=True)
+    return os.path.exists(path) and os.path.getsize(path) > 0
+
+
+def main():
+    argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
+    p = argparse.ArgumentParser()
+    p.add_argument("--output", default=None, help="optional: render a still PNG here")
+    p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"),
+                   help="render engine for --output (cycles for GPU-less hosts)")
+    args = p.parse_args(argv)
+
+    obj = build_rosette()
+    code = check(obj)
+    if code:
+        return code
+
+    if args.output:
+        if not render_still(obj, os.path.abspath(args.output), args.engine):
+            print("ERROR: render produced no file", file=sys.stderr)
+            return 9
+        print(f"rendered still {args.output}")
+
+    print("grease-pencil-rosette OK")
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as e:
+        import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+
+
+ +
+
+ generated from examples/gallery.json + CC-BY-NC-ND-4.0 + exit 0 +
+
+ + + diff --git a/docs/gallery/index.html b/docs/gallery/index.html index a24b72a..2ffc648 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -186,6 +186,7 @@

Examples Gallery

+ @@ -375,6 +376,17 @@

parent-inverse-orrery

View example +
+ + grease-pencil-rosette — Grease Pencil v3's attribute-based API — layer → frames + +
+

grease-pencil-rosette

+

Grease Pencil v3's attribute-based API — layer → frames.new().drawing → add_strokes → per-point position/radius/opacity/vertex_color — drawing five nested neon rose curves.

+

witnesses The GPv3 address break: 4.5 keeps GPv3 at grease_pencils_v3 while grease_pencils is still legacy GPencil (frame.strokes, no .drawing); 5.x deletes legacy and GPv3 takes over the grease_pencils name. Point writes lazily materialize attribute layers, and every position round-trips through the raw POINT buffer.

+ View example +
+