Skip to content

Commit 0c56a8a

Browse files
TMHSDigitalclaude
andcommitted
fix: text-version-stamp presentation, the stamp on a plinth
Render path only: the numerals stand on a plinth with the caption set into its face; the floating caption and edge glow bar are gone. Standard view. Contract and exit codes unchanged on 4.5.11, 5.1.2 and 5.2.1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent 60255eb commit 0c56a8a

8 files changed

Lines changed: 75 additions & 49 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ evaluated topology 1044 verts / 1028 faces.
404404
</tr>
405405
<tr>
406406
<td width="46%" valign="middle">
407-
<a href="examples/text-version-stamp/"><img src="examples/text-version-stamp/preview.webp" alt="Text version stamp: beveled brass 5.1.2 numerals standing on a dark studio floor over a glowing teal underline, with a small steel BLENDER caption above — the body text is the live bpy.app.version_string" /></a>
407+
<a href="examples/text-version-stamp/"><img src="examples/text-version-stamp/preview.webp" alt="Text version stamp: beveled brass version numerals standing on a dark plinth with a steel BLENDER caption set into its face — the body text is the live bpy.app.version_string" /></a>
408408
</td>
409409
<td valign="middle">
410410

2.33 KB
Loading
1006 Bytes
Loading

‎docs/gallery/text-version-stamp/index.html‎

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
<meta property="og:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/text-version-stamp-hero.webp" />
1717
<meta property="og:image:width" content="1280" />
1818
<meta property="og:image:height" content="720" />
19-
<meta property="og:image:alt" content="The word BLENDER above the Blender version number in large gold 3D characters." />
19+
<meta property="og:image:alt" content="The Blender version number in large gold 3D characters standing on a dark plinth, the word BLENDER set into its face." />
2020
<meta property="og:site_name" content="Blender Developer Tools" />
2121
<meta name="twitter:card" content="summary_large_image" />
2222
<meta name="twitter:title" content="text-version-stamp — Examples — Blender Developer Tools" />
2323
<meta name="twitter:description" content="The TextCurve data API — curves.new(type=&#x27;FONT&#x27;), live body text from bpy.app.version_string, extrude and bevel_depth solids, and evaluated-mesh conversion — so every render self-documents which Blender produced it." />
2424
<meta name="twitter:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/text-version-stamp-hero.webp" />
25-
<meta name="twitter:image:alt" content="The word BLENDER above the Blender version number in large gold 3D characters." />
25+
<meta name="twitter:image:alt" content="The Blender version number in large gold 3D characters standing on a dark plinth, the word BLENDER set into its face." />
2626
<style>
2727
/* fonts are deployed by the landing build (docs/fonts/) */
2828
@font-face { font-family: 'Barlow Condensed'; font-weight: 600; font-display: swap;
@@ -268,8 +268,8 @@ <h1>text-version-stamp</h1>
268268
<p>The TextCurve data API — curves.new(type=&#x27;FONT&#x27;), live body text from bpy.app.version_string, extrude and bevel_depth solids, and evaluated-mesh conversion — so every render self-documents which Blender produced it.</p>
269269
</header>
270270
<main id="main">
271-
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: The word BLENDER above the Blender version number in large gold 3D characters.">
272-
<img src="../assets/text-version-stamp-hero.webp" alt="The word BLENDER above the Blender version number in large gold 3D characters." width="1280" height="720" />
271+
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: The Blender version number in large gold 3D characters standing on a dark plinth, the word BLENDER set into its face.">
272+
<img src="../assets/text-version-stamp-hero.webp" alt="The Blender version number in large gold 3D characters standing on a dark plinth, the word BLENDER set into its face." width="1280" height="720" />
273273
</button>
274274
<p class="zoom-hint">Rendered headless by the example itself — click to zoom.</p>
275275
<div class="callout"><span class="tag">witnesses</span> TextCurve solids are exactly predictable: evaluated z-extent = 2 × (extrude + bevel_depth), bevel widens the outline by 2 × bevel_depth, editing body regenerates geometry — and version_string is not bare semver on LTS (&quot;4.5.11 LTS&quot;), so parse bpy.app.version instead.</div>
@@ -281,6 +281,8 @@ <h1>text-version-stamp</h1>
281281
<p>A runnable example that builds a beveled 3D stamp of the running Blender version — a <code>TextCurve</code> (<code>bpy.data.curves.new(type=&#x27;FONT&#x27;)</code>) whose <code>body</code> is the live <code>bpy.app.version_string</code>, so every render self-documents which Blender produced it. CI artifacts made this way are self-labeling.</p>
282282
<p><strong>What it witnesses:</strong> the TextCurve data-API contract. <code>curves.new(type=&#x27;FONT&#x27;)</code> returns a Curve subclass, the built-in font (&quot;Bfont Regular&quot;) is always loaded even headless, <code>body</code> is plain assignable text that regenerates geometry on edit (more characters → strictly wider evaluated mesh), and <code>extrude</code> / <code>bevel_depth</code> produce exactly predictable solids: the evaluated mesh z-extent equals 2 × (extrude + bevel_depth) and the round bevel widens the outline in-plane by 2 × bevel_depth. Flat text (no extrude, no bevel) is already <em>filled</em> — faces exist — but strictly planar. The check also witnesses the depsgraph lifetime hazard: after <code>to_mesh_clear()</code> the returned Mesh reference is dead and any access raises <code>ReferenceError</code>.</p>
283283
<p><strong>Version divergence:</strong> the string format itself differs — <code>&quot;5.1.2&quot;</code> on 5.x but <code>&quot;4.5.11 LTS&quot;</code> on 4.5 (the suffix is part of <code>version_string</code>). The check therefore asserts the contract that holds on both: <code>version_string</code> starts with the dotted <code>bpy.app.version</code> tuple. Code that parses <code>version_string</code> as a bare semver breaks on every LTS build; branch on the <code>bpy.app.version</code> tuple instead.</p>
284+
<h2>Staging</h2>
285+
<p>The numerals stand on a dark plinth, with the steel caption set into the plinth&#x27;s front face. The caption used to float in the air above the stamp, and an emissive bar lay along the bottom edge of the frame. The still renders under the Standard view transform on the house stage. Render path only; the checked text body and its extents are unchanged.</p>
284286
<h2>Run</h2>
285287
<pre><code># Cheap correctness check (no render) — the CI check:
286288
blender --background --python text_version_stamp.py --
@@ -464,7 +466,12 @@ <h2>Source</h2>
464466
s = <span class="n">3.1</span> / width
465467
obj.scale = (s, s, s)
466468
obj.rotation_euler = (math.radians(<span class="n">90</span>), <span class="n">0.0</span>, <span class="n">0.0</span>)
467-
obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, s * height / <span class="n">2</span> + <span class="n">0.02</span>)
469+
<span class="c"># The stamp stands on a dark plinth, and the caption is set into the</span>
470+
<span class="c"># plinth&#x27;s front face. It used to float in the air above the stamp,</span>
471+
<span class="c"># with a glowing bar along the bottom edge of the frame.</span>
472+
plinth_h = <span class="n">0.30</span>
473+
plinth_d = <span class="n">0.56</span>
474+
obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, plinth_h + s * height / <span class="n">2</span> + <span class="n">0.02</span>)
468475

469476
<span class="c"># small steel caption above, also a TextCurve</span>
470477
cap = bpy.data.curves.new(<span class="s">&quot;Caption&quot;</span>, type=<span class="s">&#x27;FONT&#x27;</span>)
@@ -480,28 +487,29 @@ <h2>Source</h2>
480487
cap.materials.append(cmat)
481488
cap_obj = bpy.data.objects.new(<span class="s">&quot;Caption&quot;</span>, cap)
482489
cap_obj.rotation_euler = (math.radians(<span class="n">90</span>), <span class="n">0.0</span>, <span class="n">0.0</span>)
483-
cap_obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, s * height + <span class="n">0.62</span>)
490+
cap_obj.location = (<span class="n">0.0</span>, -plinth_d / <span class="n">2</span> - cap.extrude, plinth_h / <span class="n">2</span>)
484491
scene.collection.objects.link(cap_obj)
485492

486-
<span class="c"># teal emissive underline bar grounding the stamp</span>
487493
<span class="k">import</span> bmesh
488-
bar_me = bpy.data.meshes.new(<span class="s">&quot;Bar&quot;</span>)
494+
plinth_me = bpy.data.meshes.new(<span class="s">&quot;Plinth&quot;</span>)
489495
bm = bmesh.new()
490496
<span class="k">try</span>:
491497
bmesh.ops.create_cube(bm, size=<span class="n">1.0</span>)
492-
bm.to_mesh(bar_me)
498+
bmesh.ops.bevel(bm, geom=list(bm.edges), offset=<span class="n">0.012</span>, segments=<span class="n">2</span>,
499+
profile=<span class="n">0.5</span>, affect=<span class="s">&quot;EDGES&quot;</span>, clamp_overlap=<span class="k">True</span>)
500+
bm.to_mesh(plinth_me)
493501
<span class="k">finally</span>:
494502
bm.free()
495-
bmat = bpy.data.materials.new(<span class="s">&quot;Glow&quot;</span>)
496-
bmat.use_nodes = <span class="k">True</span>
497-
bb = bmat.node_tree.nodes[<span class="s">&quot;Principled BSDF&quot;</span>]
498-
bb.inputs[<span class="s">&quot;Emission Color&quot;</span>].default_value = (<span class="n">0.1</span>, <span class="n">0.9</span>, <span class="n">0.8</span>, <span class="n">1.0</span>)
499-
bb.inputs[<span class="s">&quot;Emission Strength&quot;</span>].default_value = <span class="n">14.0</span>
500-
bar_me.materials.append(bmat)
501-
bar = bpy.data.objects.new(<span class="s">&quot;Bar&quot;</span>, bar_me)
502-
bar.scale = (<span class="n">3.9</span>, <span class="n">0.06</span>, <span class="n">0.02</span>)
503-
bar.location = (<span class="n">0.0</span>, -<span class="n">0.35</span>, <span class="n">0.02</span>)
504-
scene.collection.objects.link(bar)
503+
pmat = bpy.data.materials.new(<span class="s">&quot;Plinth&quot;</span>)
504+
pmat.use_nodes = <span class="k">True</span>
505+
pb = pmat.node_tree.nodes[<span class="s">&quot;Principled BSDF&quot;</span>]
506+
pb.inputs[<span class="s">&quot;Base Color&quot;</span>].default_value = (<span class="n">0.045</span>, <span class="n">0.045</span>, <span class="n">0.05</span>, <span class="n">1.0</span>)
507+
pb.inputs[<span class="s">&quot;Roughness&quot;</span>].default_value = <span class="n">0.45</span>
508+
plinth_me.materials.append(pmat)
509+
plinth = bpy.data.objects.new(<span class="s">&quot;Plinth&quot;</span>, plinth_me)
510+
plinth.scale = (<span class="n">3.5</span>, plinth_d, plinth_h)
511+
plinth.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, plinth_h / <span class="n">2</span>)
512+
scene.collection.objects.link(plinth)
505513

506514
<span class="c"># dark studio: floor + back wall</span>
507515
floor_me = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>)
@@ -514,8 +522,8 @@ <h2>Source</h2>
514522
fmat = bpy.data.materials.new(<span class="s">&quot;Studio&quot;</span>)
515523
fmat.use_nodes = <span class="k">True</span>
516524
fb = fmat.node_tree.nodes[<span class="s">&quot;Principled BSDF&quot;</span>]
517-
fb.inputs[<span class="s">&quot;Base Color&quot;</span>].default_value = (<span class="n">0.05</span>, <span class="n">0.055</span>, <span class="n">0.065</span>, <span class="n">1.0</span>)
518-
fb.inputs[<span class="s">&quot;Roughness&quot;</span>].default_value = <span class="n">0.35</span>
525+
fb.inputs[<span class="s">&quot;Base Color&quot;</span>].default_value = (<span class="n">0.03</span>, <span class="n">0.032</span>, <span class="n">0.037</span>, <span class="n">1.0</span>)
526+
fb.inputs[<span class="s">&quot;Roughness&quot;</span>].default_value = <span class="n">0.7</span>
519527
floor_me.materials.append(fmat)
520528
floor = bpy.data.objects.new(<span class="s">&quot;Floor&quot;</span>, floor_me)
521529
scene.collection.objects.link(floor)
@@ -548,10 +556,10 @@ <h2>Source</h2>
548556
<span class="c"># Reframed: the old (0,-8.5,1.15) fixed 86.5° pitch left the stamp at</span>
549557
<span class="c"># 0.663 fill with a dead lower third; moved in with an aim on the stamp&#x27;s</span>
550558
<span class="c"># vertical center so text, caption, and underline bar balance the frame.</span>
551-
cam.location = (<span class="n">0.0</span>, -<span class="n">6.0</span>, <span class="n">1.5</span>)
559+
cam.location = (<span class="n">0.0</span>, -<span class="n">5.45</span>, <span class="n">1.55</span>)
552560
scene.collection.objects.link(cam)
553561
aim = bpy.data.objects.new(<span class="s">&quot;Aim&quot;</span>, <span class="k">None</span>)
554-
aim.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">1.05</span>)
562+
aim.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.62</span>)
555563
scene.collection.objects.link(aim)
556564
con = cam.constraints.new(<span class="s">&#x27;TRACK_TO&#x27;</span>)
557565
con.target = aim
@@ -571,13 +579,15 @@ <h2>Source</h2>
571579
scene.render.resolution_y = <span class="n">720</span>
572580
scene.render.image_settings.file_format = <span class="s">&#x27;PNG&#x27;</span>
573581
scene.render.filepath = path
582+
<span class="c"># AgX would dull the brass toward tan (docs/VISUAL-STYLE.md)</span>
583+
scene.view_settings.view_transform = <span class="s">&#x27;Standard&#x27;</span>
574584
<span class="c"># Layer 1 framing gate (silhouette matte) — exit 10 on violation, before</span>
575585
<span class="c"># the beauty render so a defective composition ships no artifact. The hero</span>
576-
<span class="c"># is the version stamp; caption and underline bar count for margins.</span>
586+
<span class="c"># is the version stamp; caption and plinth count for margins.</span>
577587
fcode = gallery_framing.check_framing(
578588
scene, cam,
579589
hero=[obj],
580-
elements=[obj, cap_obj, bar],
590+
elements=[obj, cap_obj, plinth],
581591
stage=[floor, wall],
582592
)
583593
<span class="k">if</span> fcode:
@@ -625,7 +635,7 @@ <h2>Source</h2>
625635
</main>
626636
<dialog class="lightbox" id="lightbox" aria-label="Full-size render">
627637
<button class="lightbox-close" type="button" autofocus>Close</button>
628-
<img src="../assets/text-version-stamp-hero.webp" alt="The word BLENDER above the Blender version number in large gold 3D characters." />
638+
<img src="../assets/text-version-stamp-hero.webp" alt="The Blender version number in large gold 3D characters standing on a dark plinth, the word BLENDER set into its face." />
629639
</dialog>
630640
<footer>
631641
<div class="statusbar">

‎examples/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@
259259
"name": "text-version-stamp",
260260
"dir": "examples/text-version-stamp",
261261
"teaches": "The TextCurve data API — curves.new(type='FONT'), live body text from bpy.app.version_string, extrude and bevel_depth solids, and evaluated-mesh conversion — so every render self-documents which Blender produced it.",
262-
"alt": "The word BLENDER above the Blender version number in large gold 3D characters.",
262+
"alt": "The Blender version number in large gold 3D characters standing on a dark plinth, the word BLENDER set into its face.",
263263
"witnessesFix": "TextCurve solids are exactly predictable: evaluated z-extent = 2 × (extrude + bevel_depth), bevel widens the outline by 2 × bevel_depth, editing body regenerates geometry — and version_string is not bare semver on LTS (\"4.5.11 LTS\"), so parse bpy.app.version instead.",
264264
"hero": "docs/gallery/assets/text-version-stamp-hero.webp",
265265
"preview": "examples/text-version-stamp/preview.webp",

‎examples/text-version-stamp/README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ asserts the contract that holds on both: `version_string` starts with the dotted
2222
`bpy.app.version` tuple. Code that parses `version_string` as a bare semver breaks
2323
on every LTS build; branch on the `bpy.app.version` tuple instead.
2424

25+
## Staging
26+
27+
The numerals stand on a dark plinth, with the steel caption set into the
28+
plinth's front face. The caption used to float in the air above the
29+
stamp, and an emissive bar lay along the bottom edge of the frame. The
30+
still renders under the Standard view transform on the house stage.
31+
Render path only; the checked text body and its extents are unchanged.
32+
2533
## Run
2634

2735
```bash
2.58 KB
Loading

‎examples/text-version-stamp/text_version_stamp.py‎

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,12 @@ def render_still(obj, path, engine):
159159
s = 3.1 / width
160160
obj.scale = (s, s, s)
161161
obj.rotation_euler = (math.radians(90), 0.0, 0.0)
162-
obj.location = (0.0, 0.0, s * height / 2 + 0.02)
162+
# The stamp stands on a dark plinth, and the caption is set into the
163+
# plinth's front face. It used to float in the air above the stamp,
164+
# with a glowing bar along the bottom edge of the frame.
165+
plinth_h = 0.30
166+
plinth_d = 0.56
167+
obj.location = (0.0, 0.0, plinth_h + s * height / 2 + 0.02)
163168

164169
# small steel caption above, also a TextCurve
165170
cap = bpy.data.curves.new("Caption", type='FONT')
@@ -175,28 +180,29 @@ def render_still(obj, path, engine):
175180
cap.materials.append(cmat)
176181
cap_obj = bpy.data.objects.new("Caption", cap)
177182
cap_obj.rotation_euler = (math.radians(90), 0.0, 0.0)
178-
cap_obj.location = (0.0, 0.0, s * height + 0.62)
183+
cap_obj.location = (0.0, -plinth_d / 2 - cap.extrude, plinth_h / 2)
179184
scene.collection.objects.link(cap_obj)
180185

181-
# teal emissive underline bar grounding the stamp
182186
import bmesh
183-
bar_me = bpy.data.meshes.new("Bar")
187+
plinth_me = bpy.data.meshes.new("Plinth")
184188
bm = bmesh.new()
185189
try:
186190
bmesh.ops.create_cube(bm, size=1.0)
187-
bm.to_mesh(bar_me)
191+
bmesh.ops.bevel(bm, geom=list(bm.edges), offset=0.012, segments=2,
192+
profile=0.5, affect="EDGES", clamp_overlap=True)
193+
bm.to_mesh(plinth_me)
188194
finally:
189195
bm.free()
190-
bmat = bpy.data.materials.new("Glow")
191-
bmat.use_nodes = True
192-
bb = bmat.node_tree.nodes["Principled BSDF"]
193-
bb.inputs["Emission Color"].default_value = (0.1, 0.9, 0.8, 1.0)
194-
bb.inputs["Emission Strength"].default_value = 14.0
195-
bar_me.materials.append(bmat)
196-
bar = bpy.data.objects.new("Bar", bar_me)
197-
bar.scale = (3.9, 0.06, 0.02)
198-
bar.location = (0.0, -0.35, 0.02)
199-
scene.collection.objects.link(bar)
196+
pmat = bpy.data.materials.new("Plinth")
197+
pmat.use_nodes = True
198+
pb = pmat.node_tree.nodes["Principled BSDF"]
199+
pb.inputs["Base Color"].default_value = (0.045, 0.045, 0.05, 1.0)
200+
pb.inputs["Roughness"].default_value = 0.45
201+
plinth_me.materials.append(pmat)
202+
plinth = bpy.data.objects.new("Plinth", plinth_me)
203+
plinth.scale = (3.5, plinth_d, plinth_h)
204+
plinth.location = (0.0, 0.0, plinth_h / 2)
205+
scene.collection.objects.link(plinth)
200206

201207
# dark studio: floor + back wall
202208
floor_me = bpy.data.meshes.new("Floor")
@@ -209,8 +215,8 @@ def render_still(obj, path, engine):
209215
fmat = bpy.data.materials.new("Studio")
210216
fmat.use_nodes = True
211217
fb = fmat.node_tree.nodes["Principled BSDF"]
212-
fb.inputs["Base Color"].default_value = (0.05, 0.055, 0.065, 1.0)
213-
fb.inputs["Roughness"].default_value = 0.35
218+
fb.inputs["Base Color"].default_value = (0.03, 0.032, 0.037, 1.0)
219+
fb.inputs["Roughness"].default_value = 0.7
214220
floor_me.materials.append(fmat)
215221
floor = bpy.data.objects.new("Floor", floor_me)
216222
scene.collection.objects.link(floor)
@@ -243,10 +249,10 @@ def light(name, loc, energy, size, col, rot):
243249
# Reframed: the old (0,-8.5,1.15) fixed 86.5° pitch left the stamp at
244250
# 0.663 fill with a dead lower third; moved in with an aim on the stamp's
245251
# vertical center so text, caption, and underline bar balance the frame.
246-
cam.location = (0.0, -6.0, 1.5)
252+
cam.location = (0.0, -5.45, 1.55)
247253
scene.collection.objects.link(cam)
248254
aim = bpy.data.objects.new("Aim", None)
249-
aim.location = (0.0, 0.0, 1.05)
255+
aim.location = (0.0, 0.0, 0.62)
250256
scene.collection.objects.link(aim)
251257
con = cam.constraints.new('TRACK_TO')
252258
con.target = aim
@@ -266,13 +272,15 @@ def light(name, loc, energy, size, col, rot):
266272
scene.render.resolution_y = 720
267273
scene.render.image_settings.file_format = 'PNG'
268274
scene.render.filepath = path
275+
# AgX would dull the brass toward tan (docs/VISUAL-STYLE.md)
276+
scene.view_settings.view_transform = 'Standard'
269277
# Layer 1 framing gate (silhouette matte) — exit 10 on violation, before
270278
# the beauty render so a defective composition ships no artifact. The hero
271-
# is the version stamp; caption and underline bar count for margins.
279+
# is the version stamp; caption and plinth count for margins.
272280
fcode = gallery_framing.check_framing(
273281
scene, cam,
274282
hero=[obj],
275-
elements=[obj, cap_obj, bar],
283+
elements=[obj, cap_obj, plinth],
276284
stage=[floor, wall],
277285
)
278286
if fcode:

0 commit comments

Comments
 (0)