From 0f114c1b433e87d8090d65ffd218f2982f94cb78 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Wed, 22 Jul 2026 11:24:07 -0400 Subject: [PATCH] feat: add gp-lineart-contour example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Witness Grease Pencil LINEART modifier contours via the depsgraph, the source_object and edge-flag failure modes, and the thickness→radius trap across 4.5 LTS and 5.1. Signed-off-by: fOuttaMyPaint Co-authored-by: Cursor --- .cursor-plugin/plugin.json | 1 + .github/workflows/blender-smoke.yml | 11 + README.md | 22 +- ROADMAP.md | 2 +- .../assets/gp-lineart-contour-hero.webp | Bin 0 -> 23384 bytes .../gp-lineart-contour-contact-sheet.webp | Bin 0 -> 23964 bytes docs/gallery/gp-lineart-contour/index.html | 664 ++++++++++++++++++ docs/gallery/index.html | 13 + examples/gallery.json | 13 + examples/gp-lineart-contour/README.md | 44 ++ .../gp-lineart-contour/gp_lineart_contour.py | 420 +++++++++++ examples/gp-lineart-contour/preview.webp | Bin 0 -> 22150 bytes 12 files changed, 1186 insertions(+), 4 deletions(-) create mode 100644 docs/gallery/assets/gp-lineart-contour-hero.webp create mode 100644 docs/gallery/contact-sheets/gp-lineart-contour-contact-sheet.webp create mode 100644 docs/gallery/gp-lineart-contour/index.html create mode 100644 examples/gp-lineart-contour/README.md create mode 100644 examples/gp-lineart-contour/gp_lineart_contour.py create mode 100644 examples/gp-lineart-contour/preview.webp diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index f70c3c1..f937c4e 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -73,6 +73,7 @@ "examples/gltf-skin-roundtrip", "examples/gn-instance-grid", "examples/gn-sdf-remesh", + "examples/gp-lineart-contour", "examples/grease-pencil-rosette", "examples/image-pixels-testcard", "examples/light-link-studio", diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml index 9cb33f8..e8f38bf 100644 --- a/.github/workflows/blender-smoke.yml +++ b/.github/workflows/blender-smoke.yml @@ -287,6 +287,17 @@ jobs: xvfb-run -a "$BLENDER" --background \ --python examples/grease-pencil-rosette/grease_pencil_rosette.py -- + - name: Shipped example - gp line art contour (LINEART modifier + thickness trap) + run: | + set -euo pipefail + # Check only (no render): GPv3 object + LINEART modifier; asserts + # source_object is load-bearing (clear → 0 strokes), contour+crease + # off → 0, restore recovers silhouette strokes, and thickness is + # 4.5-only (AttributeError on 5.1; portable path is radius). + # Exits non-zero on failure. + xvfb-run -a "$BLENDER" --background \ + --python examples/gp-lineart-contour/gp_lineart_contour.py -- + - name: Shipped example - armature bend (edit_bones + LBS closed form) run: | set -euo pipefail diff --git a/README.md b/README.md index 7551db6..1b5888c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

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

@@ -36,7 +36,7 @@ ## Overview -This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 33 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 34 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. @@ -482,7 +482,7 @@ and every orbit lands on its closed form.

-Context & Grease Pencil — 2 examples +Context & Grease Pencil — 3 examples @@ -514,6 +514,22 @@ 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. + + + + +
+GP Line Art contour: a faceted teal crystal on a dark studio floor outlined in neon cyan Grease Pencil Line Art strokes, proving modifiers.new LINEART with source_object silhouette evaluation + + +### [gp-lineart-contour](examples/gp-lineart-contour/) + +Grease Pencil `LINEART` modifier contours via the depsgraph — not Freestyle and +not hand-drawn strokes. `source_object` is load-bearing (clear → 0 strokes); +contour+crease off → 0; restore recovers **10** strokes / **34** points on both +binaries. Stroke width: `thickness` exists on 4.5, `AttributeError` on 5.1 — +portable path is `radius`. +
diff --git a/ROADMAP.md b/ROADMAP.md index 1a670f7..a37b23e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -115,7 +115,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo - ~~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 - ~~Custom-normals / shade-by-angle witness~~ **SHIPPED** as `examples/custom-normals-shade/` — the post-4.1 shading contract: `use_auto_smooth`/`use_custom_normals`/`calc_normals` are AttributeError on BOTH 4.5.11 and 5.1.2; `set_sharp_from_angle` sharp sets match an independent dihedral recompute exactly (188/388 edges, 3 meshes); evaluated normal welds/splits exact (0.0 dev); custom split normals survive depsgraph evaluation within int16 quantization (1.407e-04, not float-exact); **divergence**: the legacy `shade_auto_smooth` operator CANCELS headless on 4.5 ("Asset loading is unfinished", mesh untouched, no exception) while 5.1 FINISHES with the Smooth-by-Angle NODES modifier — the data API is the portable path - ~~Sky Texture / sun_elevation witness~~ **SHIPPED** as `examples/sky-texture-sun-elevation/` — World `ShaderNodeTexSky` → Background; `sky_type` is `NISHITA` on 4.5 and `MULTIPLE_SCATTERING` on 5.1 (`NISHITA` gone); `dust_density` is 4.5-only (`aerosol_density` on 5.1); zenith EXR rise 8°→55° measured **2.25x** (5.1.2) / **1.50x** (4.5.11), gate ≥ 1.25; dual-elevation gallery diptych -- Grease Pencil Line Art contour witness: empty GPv3 object + `modifiers.new(..., "LINEART")`; `thickness` on 4.5 vs `radius` on 5.1; evaluated contour stroke counts for a known source mesh +- ~~Grease Pencil Line Art contour witness~~ **SHIPPED** as `examples/gp-lineart-contour/` — GPv3 + `modifiers.new(..., "LINEART")`; `source_object` clear → 0 strokes; contour+crease off → 0; restore recovers **10** strokes / **34** points on 4.5.11 and 5.1.2; `thickness` is 4.5-only (`AttributeError` on 5.1; portable `radius`) - Camera DOF + focus_distance witness: `cam.dof.use_dof`, focus plane vs defocus variance on high-freq cards (Stage deviation — depth needs background content) - Volumetric scatter optical-depth witness: Volume Scatter density → Beer–Lambert transmittance along a known path (Cycles; Stage deviation) - Freestyle SVG / line-set witness: Freestyle line set on a silhouette (full render pass; prefer Line Art first) diff --git a/docs/gallery/assets/gp-lineart-contour-hero.webp b/docs/gallery/assets/gp-lineart-contour-hero.webp new file mode 100644 index 0000000000000000000000000000000000000000..92a46d696506e6faad8377a5da7baa0331c5642c GIT binary patch literal 23384 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

TMX$HZ(;n&XX^(g_N;TI`DGA0PW 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 zZ73DM!zycUh_BRLhiDznbX{03*ZSteVCS?t>b39mHL>yZ4j*d3JMVyMJ&10000000000000000000000000000000000000000 H00000npnU+ literal 0 HcmV?d00001 diff --git a/docs/gallery/gp-lineart-contour/index.html b/docs/gallery/gp-lineart-contour/index.html new file mode 100644 index 0000000..5588aa5 --- /dev/null +++ b/docs/gallery/gp-lineart-contour/index.html @@ -0,0 +1,664 @@ + + + + + + gp-lineart-contour — Examples — Blender Developer Tools + + + + + + + + + + + + + + + + + +

+
+

gp-lineart-contour

+

Grease Pencil LINEART modifier contours via the depsgraph on a faceted crystal. source_object is load-bearing (clear yields 0 strokes); contour+crease off yields 0; restore recovers 10 strokes / 34 points on both 4.5.11 and 5.1.2. Stroke width: thickness exists on 4.5, AttributeError on 5.1 — portable path is radius.

+
+
+ +

Rendered headless by the example itself — click to zoom.

+
witnesses AI code still sets LINEART.thickness on 5.1 (AttributeError) or hand-draws strokes / enables Freestyle instead of modifiers.new(..., 'LINEART'). Clearing source_object or leaving contour+crease off silently yields an empty drawing — the check proves both failure modes and recovery.
+
+
blender --background --python examples/gp-lineart-contour/gp_lineart_contour.py --
+ +
+
+

A runnable example that builds a faceted crystal, attaches a Grease Pencil LINEART modifier with source_type='OBJECT', and proves the contour contract through the depsgraph — including the 4.5 → 5.1 stroke-width trap.

+

What it witnesses: the Line Art modifier contract AI-generated NPR code most often gets wrong.

+
  • Contours are a modifier, not Freestyle and not hand-authored strokes. modifiers.new(..., 'LINEART') on a GPv3 object, with target_layer / target_material set, evaluates silhouette edges into drawing strokes.
  • source_object is load-bearing. Clearing it yields 0 evaluated strokes (proven in the same check pass). Restoring it recovers the contour.
  • Edge-type flags matter. A freshly added LINEART with use_contour and use_crease both off emits 0 strokes; turning contour (+ crease) back on recovers the silhouette (10 strokes / 34 points on both binaries for this crystal + camera).
  • Stroke width renamed. 4.5 exposes both thickness (legacy px, set to 45 here) and radius; 5.1 removes thickness (AttributeError) and keeps radius only (portable path: mod.radius = 0.028).
  • GPv3 address matches grease-pencil-rosette: grease_pencils_v3 on 4.5, grease_pencils on 5.x.
+

What each check catches on failure: wrong GPv3 collection (exit 2), thickness present/absent on the wrong side of 5.0 (exit 3), lost source_object assignment (exit 4), contour too thin (exit 5), source clear not zeroing strokes (exit 6), flags-off or restore failure (exit 7).

+

Version witness: stroke counts match on 4.5.11 LTS and 5.1.2 (10 strokes / 34 points). The divergence is thickness vs radius.

+

Run

+
# Depsgraph contour check — the CI check:
+blender --background --python gp_lineart_contour.py --
+
+# Also render the gallery still:
+blender --background --python gp_lineart_contour.py -- --output lineart.png
+

It exits non-zero on failure. The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1.

+
+
+

Source

+
+ examples/gp-lineart-contour/gp_lineart_contour.py + View on GitHub → +
+
"""Grease Pencil Line Art contours — a runnable example.
+
+Witnesses the Line Art modifier contract AI-generated NPR code most often
+gets wrong across the 4.5 LTS → 5.1 window:
+
+1. Contours come from ``modifiers.new(..., 'LINEART')`` on a GPv3 object,
+   not from freestyle or hand-drawn strokes.
+2. ``source_type='OBJECT'`` + ``source_object`` is load-bearing — clearing
+   the source yields zero evaluated strokes.
+3. ``use_contour`` must be on for silhouette edges; with contour and crease
+   both off the evaluator emits nothing.
+4. Stroke width: 4.5 exposes both ``thickness`` (legacy px) and ``radius``;
+   5.1 removes ``thickness`` (AttributeError) and keeps ``radius`` only.
+5. GPv3 datablock address matches grease-pencil-rosette: ``grease_pencils_v3``
+   on 4.5, ``grease_pencils`` on 5.x.
+
+The check evaluates the modifier through the depsgraph (no bake required)
+and asserts stroke/point lower bounds against the known failure modes above.
+
+By default it runs the correctness check only. Pass --output to render:
+
+    blender --background --python gp_lineart_contour.py --
+    blender --background --python gp_lineart_contour.py -- --output l.png
+"""
+import bpy, bmesh, sys, os, math, argparse
+from mathutils import Matrix
+
+# Faceted crystal (octahedron-ish) — enough silhouette edges to read at thumbnail
+CRYSTAL_SCALE = 1.15
+STROKE_MIN = 1
+POINT_MIN = 4
+RADIUS = 0.028
+
+
+def eevee_engine_id():
+    return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT"
+
+
+def gp_data_new(name):
+    """Version-gated GPv3 datablock creation (same bridge as grease-pencil-rosette)."""
+    if bpy.app.version >= (5, 0, 0):
+        return bpy.data.grease_pencils.new(name)
+    return bpy.data.grease_pencils_v3.new(name)
+
+
+def check_gp_version_gate():
+    if bpy.app.version >= (5, 0, 0):
+        if not hasattr(bpy.data, "grease_pencils"):
+            print("ERROR: grease_pencils missing on 5.x", file=sys.stderr)
+            return 2
+        if hasattr(bpy.data, "grease_pencils_v3"):
+            print("ERROR: grease_pencils_v3 should be gone on 5.x", file=sys.stderr)
+            return 2
+        print("5.x contract: grease_pencils is GPv3; _v3 alias gone")
+    else:
+        if not hasattr(bpy.data, "grease_pencils_v3"):
+            print("ERROR: grease_pencils_v3 missing on 4.5", file=sys.stderr)
+            return 2
+        legacy = bpy.data.grease_pencils.new("LegacyProbe")
+        lframe = legacy.layers.new("L").frames.new(1)
+        if hasattr(lframe, "drawing") or not hasattr(lframe, "strokes"):
+            print("ERROR: 4.5 grease_pencils is not legacy GPencil", file=sys.stderr)
+            return 2
+        bpy.data.grease_pencils.remove(legacy)
+        print("4.5 contract: grease_pencils is legacy; GPv3 lives at _v3")
+    return 0
+
+
+def build_crystal(sc):
+    """A designed faceted crystal — not a raw cube."""
+    me = bpy.data.meshes.new("Crystal")
+    bm = bmesh.new()
+    try:
+        # Dual cone = octahedron-like crystal
+        bmesh.ops.create_cone(
+            bm, cap_ends=False, cap_tris=True,
+            segments=6, radius1=0.85, radius2=0.0, depth=1.4,
+        )
+        # second tip downward
+        geom = bmesh.ops.duplicate(bm, geom=list(bm.verts) + list(bm.edges) + list(bm.faces))
+        verts = [e for e in geom["geom"] if isinstance(e, bmesh.types.BMVert)]
+        bmesh.ops.rotate(
+            bm, verts=verts, cent=(0, 0, 0),
+            matrix=Matrix.Rotation(math.pi, 3, "X"),
+        )
+        bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4)
+        bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
+        bmesh.ops.scale(bm, vec=(CRYSTAL_SCALE,) * 3, verts=bm.verts)
+        bm.to_mesh(me)
+    finally:
+        bm.free()
+    for p in me.polygons:
+        p.use_smooth = False
+    mat = bpy.data.materials.new("CrystalMat")
+    mat.use_nodes = True
+    b = mat.node_tree.nodes["Principled BSDF"]
+    b.inputs["Base Color"].default_value = (0.12, 0.38, 0.48, 1.0)
+    b.inputs["Roughness"].default_value = 0.42
+    b.inputs["Metallic"].default_value = 0.2
+    emit = b.inputs.get("Emission Color") or b.inputs.get("Emission")
+    if emit is not None:
+        emit.default_value = (0.05, 0.2, 0.28, 1.0)
+        b.inputs["Emission Strength"].default_value = 0.15
+    me.materials.append(mat)
+    ob = bpy.data.objects.new("Crystal", me)
+    ob.location = (0.0, 0.0, 1.15)
+    ob.rotation_euler = (math.radians(8), math.radians(12), math.radians(22))
+    sc.collection.objects.link(ob)
+    return ob
+
+
+def build_lineart(sc, source):
+    gp = gp_data_new("LineArtGP")
+    ob = bpy.data.objects.new("LineArt", gp)
+    sc.collection.objects.link(ob)
+
+    layer = gp.layers.new("Contours")
+    layer.frames.new(1)
+
+    mat = bpy.data.materials.new("NeonContour")
+    bpy.data.materials.create_gpencil_data(mat)
+    # Neon cyan ink
+    gp_set = mat.grease_pencil
+    if hasattr(gp_set, "color"):
+        gp_set.color = (0.05, 0.95, 1.0, 1.0)
+    gp.materials.append(mat)
+
+    mod = ob.modifiers.new("LineArt", "LINEART")
+    mod.source_type = "OBJECT"
+    mod.source_object = source
+    mod.use_contour = True
+    mod.use_crease = True
+    if hasattr(mod, "use_material"):
+        mod.use_material = False
+    mod.target_layer = "Contours"
+    mod.target_material = mat
+    # Width: radius is portable; thickness is 4.5-only
+    if hasattr(mod, "thickness"):
+        mod.thickness = 45
+    mod.radius = RADIUS
+
+    # Disable lights on the GP layer so neon stays vivid
+    if hasattr(layer, "use_lights"):
+        layer.use_lights = False
+    return ob, mod, gp
+
+
+def setup_camera(sc):
+    cam_data = bpy.data.cameras.new("Cam")
+    cam_data.lens = 50.0
+    cam = bpy.data.objects.new("Cam", cam_data)
+    cam.location = (3.4, -4.2, 2.4)
+    sc.collection.objects.link(cam)
+    sc.camera = cam
+    aim = bpy.data.objects.new("Aim", None)
+    aim.location = (0.0, 0.0, 1.15)
+    sc.collection.objects.link(aim)
+    tr = cam.constraints.new("TRACK_TO")
+    tr.target = aim
+    tr.track_axis = "TRACK_NEGATIVE_Z"
+    tr.up_axis = "UP_Y"
+    return cam
+
+
+def eval_stroke_counts(ob):
+    dg = bpy.context.evaluated_depsgraph_get()
+    dg.update()
+    eob = ob.evaluated_get(dg)
+    layer = eob.data.layers[0]
+    frame = layer.frames[0]
+    drawing = frame.drawing
+    strokes = list(drawing.strokes)
+    n_strokes = len(strokes)
+    n_points = sum(len(s.points) for s in strokes)
+    return n_strokes, n_points
+
+
+def check_thickness_trap(mod):
+    if bpy.app.version >= (5, 0, 0):
+        if hasattr(mod, "thickness"):
+            print("ERROR: thickness still present on 5.x LINEART", file=sys.stderr)
+            return 3
+        try:
+            _ = mod.thickness
+            print("ERROR: thickness read did not raise on 5.x", file=sys.stderr)
+            return 3
+        except AttributeError:
+            pass
+        if not hasattr(mod, "radius"):
+            print("ERROR: radius missing on 5.x LINEART", file=sys.stderr)
+            return 3
+        print(f"5.x contract: LINEART.radius={mod.radius} thickness=AttributeError")
+    else:
+        if not hasattr(mod, "thickness"):
+            print("ERROR: thickness missing on 4.5 LINEART", file=sys.stderr)
+            return 3
+        if not hasattr(mod, "radius"):
+            print("ERROR: radius missing on 4.5 LINEART", file=sys.stderr)
+            return 3
+        print(
+            f"4.5 contract: LINEART.thickness={mod.thickness} radius={mod.radius}"
+        )
+    return 0
+
+
+def check(sc, source, la_ob, mod):
+    code = check_gp_version_gate()
+    if code:
+        return code
+    code = check_thickness_trap(mod)
+    if code:
+        return code
+
+    if mod.type != "LINEART":
+        print(f"ERROR: modifier type {mod.type!r} != 'LINEART'", file=sys.stderr)
+        return 4
+    if mod.source_type != "OBJECT":
+        print(f"ERROR: source_type {mod.source_type!r} != 'OBJECT'", file=sys.stderr)
+        return 4
+    if mod.source_object != source:
+        print("ERROR: source_object round-trip failed", file=sys.stderr)
+        return 4
+    if not mod.use_contour:
+        print("ERROR: use_contour should be True", file=sys.stderr)
+        return 4
+
+    # Happy path: contours present
+    n_s, n_p = eval_stroke_counts(la_ob)
+    if n_s < STROKE_MIN or n_p < POINT_MIN:
+        print(
+            f"ERROR: evaluated Line Art too thin: strokes={n_s} points={n_p} "
+            f"(need >= {STROKE_MIN} strokes, >= {POINT_MIN} points)",
+            file=sys.stderr,
+        )
+        return 5
+    print(f"contour_ok strokes={n_s} points={n_p} (gates>={STROKE_MIN},>={POINT_MIN})")
+
+    # Failure mode A: clear source_object → zero strokes
+    saved = mod.source_object
+    mod.source_object = None
+    z_s, z_p = eval_stroke_counts(la_ob)
+    mod.source_object = saved
+    if z_s != 0 or z_p != 0:
+        print(
+            f"ERROR: cleared source_object still produced strokes={z_s} points={z_p}",
+            file=sys.stderr,
+        )
+        return 6
+    print("nosource_ok strokes=0 points=0")
+
+    # Failure mode B: rebuild modifier with contour+crease off → zero strokes
+    while la_ob.modifiers:
+        la_ob.modifiers.remove(la_ob.modifiers[0])
+    mod_off = la_ob.modifiers.new("LineArtOff", "LINEART")
+    mod_off.source_type = "OBJECT"
+    mod_off.source_object = source
+    mod_off.use_contour = False
+    mod_off.use_crease = False
+    if hasattr(mod_off, "use_loose"):
+        mod_off.use_loose = False
+    if hasattr(mod_off, "use_intersection"):
+        mod_off.use_intersection = False
+    if hasattr(mod_off, "use_material"):
+        mod_off.use_material = False
+    if hasattr(mod_off, "use_edge_mark"):
+        mod_off.use_edge_mark = False
+    mod_off.target_layer = "Contours"
+    gp_mat = la_ob.data.materials[0]
+    mod_off.target_material = gp_mat
+    mod_off.radius = RADIUS
+    z_s, z_p = eval_stroke_counts(la_ob)
+    if z_s != 0 or z_p != 0:
+        print(
+            f"ERROR: contour+crease off still produced strokes={z_s} points={z_p}",
+            file=sys.stderr,
+        )
+        return 7
+    print("noflags_ok strokes=0 points=0")
+
+    # Restore working Line Art modifier for optional still
+    while la_ob.modifiers:
+        la_ob.modifiers.remove(la_ob.modifiers[0])
+    mod_on = la_ob.modifiers.new("LineArt", "LINEART")
+    mod_on.source_type = "OBJECT"
+    mod_on.source_object = source
+    mod_on.use_contour = True
+    mod_on.use_crease = True
+    mod_on.target_layer = "Contours"
+    mod_on.target_material = gp_mat
+    if hasattr(mod_on, "thickness"):
+        mod_on.thickness = 45
+    mod_on.radius = RADIUS
+    n_s2, n_p2 = eval_stroke_counts(la_ob)
+    if n_s2 < STROKE_MIN or n_p2 < POINT_MIN:
+        print(
+            f"ERROR: contours did not recover after restore: "
+            f"strokes={n_s2} points={n_p2}",
+            file=sys.stderr,
+        )
+        return 7
+    print(
+        f"restored_ok strokes={n_s2} points={n_p2} radius={mod_on.radius}"
+    )
+    return 0
+
+
+def build_studio(sc):
+    # Default-stage-ish dark floor + wall — Line Art neon is the proof
+    def plane(name, size, loc, rot):
+        me = bpy.data.meshes.new(name)
+        bm = bmesh.new()
+        try:
+            bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=size)
+            bm.to_mesh(me)
+        finally:
+            bm.free()
+        mat = bpy.data.materials.new(name + "M")
+        mat.use_nodes = True
+        b = mat.node_tree.nodes["Principled BSDF"]
+        b.inputs["Base Color"].default_value = (0.03, 0.032, 0.037, 1.0)
+        b.inputs["Roughness"].default_value = 0.7
+        me.materials.append(mat)
+        ob = bpy.data.objects.new(name, me)
+        ob.location = loc
+        ob.rotation_euler = rot
+        sc.collection.objects.link(ob)
+        return ob
+
+    plane("Floor", 30.0, (0, 0, 0), (0, 0, 0))
+    plane("Wall", 30.0, (0, 9, 0), (math.radians(90), 0, 0))
+
+    world = bpy.data.worlds.new("World")
+    world.use_nodes = True
+    world.node_tree.nodes["Background"].inputs["Color"].default_value = (
+        0.02, 0.021, 0.025, 1.0,
+    )
+    sc.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)
+        sc.collection.objects.link(ob)
+
+    light("Key", (-4, -5, 6), 420.0, 5.0, (1.0, 0.96, 0.9), (48, 0, -35))
+    light("Fill", (5, -3.5, 2.5), 90.0, 9.0, (0.75, 0.85, 1.0), (65, 0, 50))
+    light("Wedge", (2.5, 5.5, 4.0), 280.0, 6.0, (1.0, 0.76, 0.5), (-68, 0, 190))
+
+
+def render_still(sc, path, engine):
+    sc.render.engine = "CYCLES" if engine == "cycles" else eevee_engine_id()
+    if engine == "cycles":
+        sc.cycles.device = "CPU"
+        sc.cycles.samples = 64
+        sc.cycles.use_denoising = True
+    else:
+        try:
+            sc.eevee.taa_render_samples = 64
+        except AttributeError:
+            pass
+    sc.render.resolution_x = 1280
+    sc.render.resolution_y = 720
+    sc.render.image_settings.file_format = "PNG"
+    sc.render.filepath = path
+    sc.view_settings.view_transform = "Standard"
+    bpy.ops.render.render(write_still=True)
+    return os.path.exists(path) and os.path.getsize(path) > 0
+
+
+def build_scene():
+    bpy.ops.wm.read_factory_settings(use_empty=True)
+    sc = bpy.context.scene
+    build_studio(sc)
+    crystal = build_crystal(sc)
+    la_ob, mod, gp = build_lineart(sc, crystal)
+    setup_camera(sc)
+    return sc, crystal, la_ob, mod
+
+
+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",
+    )
+    args = p.parse_args(argv)
+
+    print(f"binary version: {bpy.app.version} ({bpy.app.version_string})")
+    sc, crystal, la_ob, mod = build_scene()
+    code = check(sc, crystal, la_ob, mod)
+    if code:
+        return code
+
+    if args.output:
+        if not render_still(sc, os.path.abspath(args.output), args.engine):
+            print("ERROR: render produced no file", file=sys.stderr)
+            return 8
+        print(f"rendered still {args.output}")
+
+    print("gp-lineart-contour 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 4d3a5a5..42f69f3 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -191,8 +191,10 @@

Examples Gallery

+ + @@ -571,6 +573,17 @@

sky-texture-sun-elevation

View example +
+ + gp-lineart-contour — Grease Pencil LINEART modifier contours via the depsgraph on a faceted crystal. + +
+

gp-lineart-contour

+

Grease Pencil LINEART modifier contours via the depsgraph on a faceted crystal. source_object is load-bearing (clear yields 0 strokes); contour+crease off yields 0; restore recovers 10 strokes / 34 points on both 4.5.11 and 5.1.2. Stroke width: thickness exists on 4.5, AttributeError on 5.1 — portable path is radius.

+

witnesses AI code still sets LINEART.thickness on 5.1 (AttributeError) or hand-draws strokes / enables Freestyle instead of modifiers.new(..., 'LINEART'). Clearing source_object or leaving contour+crease off silently yields an empty drawing — the check proves both failure modes and recovery.

+ View example +
+