Skip to content

Commit f83733a

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): build gp-lineart-contour's crystal as a bipyramid, and mount it
build_crystal duplicated an open cone and flipped it over the original. The two crossed into an hourglass with open hexagonal ends and an inner spike, which the old gallery alt described as an hourglass-shaped solid. It is now the hexagonal bipyramid the docstring intended. The crystal also hovered 0.13 m above the floor; a two-tier hex mount, built after the check from the lowest evaluated vertex, now holds its lower tip. The still dollies along the check camera's bearing so the mount stays in frame. The corrected mesh changes the measured Line Art counts from 10 strokes / 34 points to 2 / 12, identical on 4.5.11, 5.1.2 and 5.2.1. The gates are lower bounds (>=1, >=4) and are unchanged, as are every assertion and exit code (default 0, --no-contour 4, --output 0). Shared gallery.json and gallery index updates for all three examples ride here. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent 07bef23 commit f83733a

8 files changed

Lines changed: 172 additions & 55 deletions

File tree

1.32 KB
Loading
14 KB
Loading

‎docs/gallery/gp-lineart-contour/index.html‎

Lines changed: 82 additions & 27 deletions
Large diffs are not rendered by default.

‎docs/gallery/index.html‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ <h2><a href="driver-wave/">driver-wave</a></h2>
409409
</article>
410410
<article class="card" data-tags="mesh bmesh">
411411
<a class="card-media" href="bmesh-gear/" tabindex="-1" aria-hidden="true">
412-
<img src="assets/bmesh-gear-hero.webp" alt="A gold fourteen-tooth gear lying at an angle, its face covered in concentric grooves." loading="lazy" decoding="async" />
412+
<img src="assets/bmesh-gear-hero.webp" alt="A gold fourteen-tooth gear leaning back on a dark display wedge, its face covered in concentric grooves." loading="lazy" decoding="async" />
413413
</a>
414414
<div class="card-body">
415415
<h2><a href="bmesh-gear/">bmesh-gear</a></h2>
@@ -651,7 +651,7 @@ <h2><a href="vertex-weight-limit/">vertex-weight-limit</a></h2>
651651
</article>
652652
<article class="card" data-tags="mesh uv">
653653
<a class="card-media" href="triangulate-tangents/" tabindex="-1" aria-hidden="true">
654-
<img src="assets/triangulate-tangents-hero.webp" alt="A round silver buckler on a stand, its face brushed in circular grooves with an anisotropic highlight." loading="lazy" decoding="async" />
654+
<img src="assets/triangulate-tangents-hero.webp" alt="A round silver buckler leaning back on a low stand, its face brushed in circular grooves with an anisotropic highlight." loading="lazy" decoding="async" />
655655
</a>
656656
<div class="card-body">
657657
<h2><a href="triangulate-tangents/">triangulate-tangents</a></h2>
@@ -728,11 +728,11 @@ <h2><a href="sky-texture-sun-elevation/">sky-texture-sun-elevation</a></h2>
728728
</article>
729729
<article class="card" data-tags="grease-pencil line-art modifiers">
730730
<a class="card-media" href="gp-lineart-contour/" tabindex="-1" aria-hidden="true">
731-
<img src="assets/gp-lineart-contour-hero.webp" alt="A translucent hourglass-shaped solid outlined by cyan Line Art contour strokes." loading="lazy" decoding="async" />
731+
<img src="assets/gp-lineart-contour-hero.webp" alt="A translucent teal crystal, a six-sided double pyramid set tip-down in a dark hex mount, its edges traced by cyan Line Art contour strokes." loading="lazy" decoding="async" />
732732
</a>
733733
<div class="card-body">
734734
<h2><a href="gp-lineart-contour/">gp-lineart-contour</a></h2>
735-
<p class="teaches">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.</p>
735+
<p class="teaches">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 2 strokes / 12 points on 4.5.11, 5.1.2 and 5.2.1. Stroke width: thickness exists on 4.5, AttributeError on 5.1 — portable path is radius.</p>
736736
<p class="witnesses"><span class="tag">witnesses</span> AI code still sets LINEART.thickness on 5.1 (AttributeError) or hand-draws strokes / enables Freestyle instead of modifiers.new(..., &#x27;LINEART&#x27;). Clearing source_object or leaving contour+crease off silently yields an empty drawing — the check proves both failure modes and recovery.</p>
737737
<a class="card-link" href="gp-lineart-contour/">View example<span class="sr-only"> gp-lineart-contour</span> <span aria-hidden="true">&rarr;</span></a>
738738
</div>

‎examples/gallery.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"name": "bmesh-gear",
8787
"dir": "examples/bmesh-gear",
8888
"teaches": "A 14-tooth gear built entirely with bmesh — profile ring, face, extrude — with bm.free() in a try/finally, exactly as the ownership contract demands.",
89-
"alt": "A gold fourteen-tooth gear lying at an angle, its face covered in concentric grooves.",
89+
"alt": "A gold fourteen-tooth gear leaning back on a dark display wedge, its face covered in concentric grooves.",
9090
"witnessesFix": "Parametric bmesh topology is exactly predictable: verts, edges, and faces match their closed forms, and every edge borders exactly two faces (watertight).",
9191
"hero": "docs/gallery/assets/bmesh-gear-hero.webp",
9292
"preview": "examples/bmesh-gear/preview.webp",
@@ -380,7 +380,7 @@
380380
"name": "triangulate-tangents",
381381
"dir": "examples/triangulate-tangents",
382382
"teaches": "A machined buckler verifying the tangent-space contract a game engine's normal mapping depends on. Deterministic triangulation, unit orthogonal tangent frames, and the edge/UV-delta formula matching mikktspace within welding tolerance.",
383-
"alt": "A round silver buckler on a stand, its face brushed in circular grooves with an anisotropic highlight.",
383+
"alt": "A round silver buckler leaning back on a low stand, its face brushed in circular grooves with an anisotropic highlight.",
384384
"witnessesFix": "calc_tangents aborts on any ngon; planar UVs on a cylindrical wall collapse tangents onto normals; a MeshUVLoopLayer handle held across calc_tangents dangles on 4.5 (471 phantom flips, silent exit 0) while the mikktspace math is byte-identical on both versions.",
385385
"hero": "docs/gallery/assets/triangulate-tangents-hero.webp",
386386
"preview": "examples/triangulate-tangents/preview.webp",
@@ -473,8 +473,8 @@
473473
{
474474
"name": "gp-lineart-contour",
475475
"dir": "examples/gp-lineart-contour",
476-
"teaches": "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.",
477-
"alt": "A translucent hourglass-shaped solid outlined by cyan Line Art contour strokes.",
476+
"teaches": "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 2 strokes / 12 points on 4.5.11, 5.1.2 and 5.2.1. Stroke width: thickness exists on 4.5, AttributeError on 5.1 — portable path is radius.",
477+
"alt": "A translucent teal crystal, a six-sided double pyramid set tip-down in a dark hex mount, its edges traced by cyan Line Art contour strokes.",
478478
"witnessesFix": "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.",
479479
"hero": "docs/gallery/assets/gp-lineart-contour-hero.webp",
480480
"preview": "examples/gp-lineart-contour/preview.webp",

‎examples/gp-lineart-contour/README.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ often gets wrong.
1414
strokes (proven in the same check pass). Restoring it recovers the contour.
1515
- **Edge-type flags matter.** A freshly added LINEART with `use_contour` and
1616
`use_crease` both off emits **0** strokes; turning contour (+ crease) back on
17-
recovers the silhouette (**10** strokes / **34** points on both binaries for
18-
this crystal + camera).
17+
recovers the silhouette (**2** strokes / **12** points on every binary for
18+
this crystal + camera; Line Art chains the bipyramid's edges into long
19+
strokes, so the count is low and the gates are lower bounds).
1920
- **Stroke width renamed.** 4.5 exposes both `thickness` (legacy px, set to 45
2021
here) and `radius`; 5.1 removes `thickness` (`AttributeError`) and keeps
2122
`radius` only (portable path: `mod.radius = 0.028`).
@@ -27,8 +28,15 @@ often gets wrong.
2728
`source_object` assignment (exit 4), contour too thin (exit 5), source clear
2829
not zeroing strokes (exit 6), flags-off or restore failure (exit 7).
2930

30-
**Version witness:** stroke counts match on 4.5.11 LTS and 5.1.2
31-
(10 strokes / 34 points). The divergence is `thickness` vs `radius`.
31+
**Version witness:** stroke counts match on 4.5.11 LTS, 5.1.2 and 5.2.1 LTS
32+
(2 strokes / 12 points). The divergence is `thickness` vs `radius`.
33+
34+
**The still:** the crystal is a hexagonal bipyramid (an equator ring and two
35+
apexes), set tip-down in a two-tier hex mount on the floor. The mount is
36+
render staging, derived from the crystal's lowest evaluated vertex; it is built
37+
after the check, so it never enters the Line Art counts. The still's camera
38+
drops its aim and dollies in along the check camera's bearing, so the crystal
39+
fills the frame with the mount's foot inside it.
3240

3341
## Run
3442

‎examples/gp-lineart-contour/gp_lineart_contour.py‎

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,19 @@
3838

3939
# Faceted crystal (octahedron-ish) — enough silhouette edges to read at thumbnail
4040
CRYSTAL_SCALE = 1.15
41+
CRYSTAL_R = 0.72 # bipyramid equator radius (before CRYSTAL_SCALE)
42+
CRYSTAL_APEX = 0.88 # apex height above/below the equator
4143
STROKE_MIN = 1
4244
POINT_MIN = 4
4345
RADIUS = 0.028
4446

47+
# render staging only (not part of the check)
48+
MOUNT_R = 0.46 # hex plinth base radius
49+
MOUNT_BASE_H = 0.07 # base tier height, standing on the floor
50+
MOUNT_SOCKET = 0.10 # depth the crystal's lower tip sinks into the mount
51+
STILL_AIM_Z = 1.05 # still camera aim height (the check's is 1.15)
52+
STILL_DOLLY = 0.94 # still camera distance, relative to the check's
53+
4554

4655
def eevee_engine_id():
4756
return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT"
@@ -82,19 +91,16 @@ def build_crystal(sc):
8291
me = bpy.data.meshes.new("Crystal")
8392
bm = bmesh.new()
8493
try:
85-
# Dual cone = octahedron-like crystal
86-
bmesh.ops.create_cone(
87-
bm, cap_ends=False, cap_tris=True,
88-
segments=6, radius1=0.85, radius2=0.0, depth=1.4,
89-
)
90-
# second tip downward
91-
geom = bmesh.ops.duplicate(bm, geom=list(bm.verts) + list(bm.edges) + list(bm.faces))
92-
verts = [e for e in geom["geom"] if isinstance(e, bmesh.types.BMVert)]
93-
bmesh.ops.rotate(
94-
bm, verts=verts, cent=(0, 0, 0),
95-
matrix=Matrix.Rotation(math.pi, 3, "X"),
96-
)
97-
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4)
94+
# Hexagonal bipyramid: one equator ring, an apex above and below.
95+
# (Two overlapped open cones, the earlier build, crossed into an
96+
# hourglass with open ends and an inner spike.)
97+
ring = [bm.verts.new((CRYSTAL_R * math.cos(math.pi * k / 3),
98+
CRYSTAL_R * math.sin(math.pi * k / 3), 0.0))
99+
for k in range(6)]
100+
for apex_z in (CRYSTAL_APEX, -CRYSTAL_APEX):
101+
apex = bm.verts.new((0.0, 0.0, apex_z))
102+
for k in range(6):
103+
bm.faces.new((ring[k], ring[(k + 1) % 6], apex))
98104
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
99105
bmesh.ops.scale(bm, vec=(CRYSTAL_SCALE,) * 3, verts=bm.verts)
100106
bm.to_mesh(me)
@@ -362,7 +368,54 @@ def light(name, loc, energy, size, col, rot):
362368
light("Wedge", (2.5, 5.5, 4.0), 280.0, 6.0, (1.0, 0.76, 0.5), (-68, 0, 190))
363369

364370

371+
def build_mount(sc, crystal):
372+
"""Render staging only: a two-tier hex plinth seated on the floor whose
373+
top swallows the crystal's lower tip, so the crystal is set in a mount
374+
instead of hovering. Derived from the evaluated crystal; the crystal
375+
itself (and so every Line Art count above) is untouched."""
376+
bpy.context.view_layer.update()
377+
mw = crystal.matrix_world
378+
tip = min((mw @ v.co for v in crystal.data.vertices), key=lambda p: p.z)
379+
top = tip.z + MOUNT_SOCKET
380+
me = bpy.data.meshes.new("Mount")
381+
bm = bmesh.new()
382+
try:
383+
for r, z0, z1 in ((MOUNT_R, -0.003, MOUNT_BASE_H),
384+
(MOUNT_R * 0.72, MOUNT_BASE_H - 0.003, top)):
385+
bmesh.ops.create_cone(
386+
bm, cap_ends=True, segments=6, radius1=r, radius2=r,
387+
depth=z1 - z0,
388+
matrix=Matrix.Translation((tip.x, tip.y, (z0 + z1) / 2))
389+
@ Matrix.Rotation(crystal.rotation_euler.z, 4, "Z"),
390+
)
391+
bm.to_mesh(me)
392+
finally:
393+
bm.free()
394+
for p in me.polygons:
395+
p.use_smooth = False
396+
mat = bpy.data.materials.new("MountStone")
397+
mat.use_nodes = True
398+
b = mat.node_tree.nodes["Principled BSDF"]
399+
b.inputs["Base Color"].default_value = (0.06, 0.062, 0.07, 1.0)
400+
b.inputs["Roughness"].default_value = 0.55
401+
me.materials.append(mat)
402+
ob = bpy.data.objects.new("Mount", me)
403+
sc.collection.objects.link(ob)
404+
return ob
405+
406+
365407
def render_still(sc, path, engine):
408+
source = sc.objects["Crystal"]
409+
mount = build_mount(sc, source)
410+
# reframe for the still only (the check ran from the setup_camera view):
411+
# lower the aim so the mount's foot clears the bottom edge, and dolly in
412+
# along the same bearing so the crystal still fills the frame
413+
aim = sc.objects["Aim"]
414+
cam = sc.camera
415+
bearing = cam.location - aim.location
416+
aim.location.z = STILL_AIM_Z
417+
cam.location = aim.location + bearing * STILL_DOLLY
418+
bpy.context.view_layer.update()
366419
sc.render.engine = "CYCLES" if engine == "cycles" else eevee_engine_id()
367420
if engine == "cycles":
368421
sc.cycles.device = "CPU"
@@ -380,16 +433,17 @@ def render_still(sc, path, engine):
380433
sc.view_settings.view_transform = "Standard"
381434
# Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
382435
# the beauty render so a defective composition ships no artifact. The two
383-
# renderable subjects are the crystal source mesh and the GP stroke object.
436+
# hero subjects are the crystal source mesh and the GP stroke object; the
437+
# mount is staging, so it must clear the edges but does not count as fill.
384438
stage = [o for o in sc.objects if o.name in {"Floor", "Wall"}]
385439
hero = [
386440
o for o in sc.objects
387-
if o.type in {"MESH", "GREASEPENCIL"} and o not in stage
441+
if o.type in {"MESH", "GREASEPENCIL"} and o not in stage and o != mount
388442
]
389443
fcode = gallery_framing.check_framing(
390444
sc, sc.camera,
391445
hero=hero,
392-
elements=hero,
446+
elements=hero + [mount],
393447
stage=stage,
394448
)
395449
if fcode:
2.39 KB
Loading

0 commit comments

Comments
 (0)