Skip to content

Commit 1cd36af

Browse files
TMHSDigitalclaude
andauthored
fix: wooden-barrel, wooden-bucket, wave-displace and color-attribute-wheel quality pass (#233)
* fix: wooden-barrel quality pass, boarded heads, stave tone and iron hoops Heads are four boards cut from the croze outline, with a head-board budget (exit 17, --one-piece-head). Per-stave tone and grain, rusted iron hoops, the temp glb removed after measuring, and the gallery alt fixed (it said three hoops; there are four). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * fix: wooden-bucket quality pass, laid rope bail clear of the ear plates The bail is three-strand rope and its rings now stand clear of the ear plates the old rope ran through (exit 17, --sunk-rope). The bottom is three boards (exit 17, --one-piece-bottom). OUTER_SIZE refit to the honest envelope; --float-handle now keeps the envelope still. Per-stave wood, iron hoops, temp glb removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * fix: wave-displace presentation, the wave as a tile on the studio floor Render path only: a skirted copy of the checked grid stands on the floor instead of bleeding off three frame edges. 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> * fix: color-attribute-wheel presentation, a board on a plinth Render path only: the zero-thickness disc gets a Solidify body and rests on a plinth with a strut, instead of hovering above the floor. Contract and exit codes unchanged on 4.5.11, 5.1.2 and 5.2.1. Also regenerates the gallery index for all four items. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> * docs: showcase conventions for boarded heads, laid rope and mount clearance From the wooden-barrel and wooden-bucket passes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> --------- Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 4aa6b6c commit 1cd36af

29 files changed

Lines changed: 1574 additions & 265 deletions

‎README.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
111111
<p align="center">
112112
<a href="showcase/shipping-crate/"><img src="showcase/shipping-crate/preview.webp" width="24%" alt="Shipping crate: a wooden slat crate with iron corner brackets on a dark studio floor, warm wedge on the back wall" /></a>
113113
<a href="showcase/stone-well/"><img src="showcase/stone-well/preview.webp" width="24%" alt="Stone well: a round brick well with a shingled pyramid roof on posts, rope and bucket, on a dark studio floor" /></a>
114-
<a href="showcase/wooden-barrel/"><img src="showcase/wooden-barrel/preview.webp" width="24%" alt="Wooden barrel: a 20-stave wine-cask with croze-seated heads, chord-lofted iron hoops, and a bung on a dark studio floor" /></a>
114+
<a href="showcase/wooden-barrel/"><img src="showcase/wooden-barrel/preview.webp" width="24%" alt="Wooden barrel: a 20-stave wine-cask with a head of four boards, chord-lofted iron hoops, and a bung on a dark studio floor" /></a>
115115
<a href="showcase/campfire/"><img src="showcase/campfire/preview.webp" width="24%" alt="Campfire: a two-course stone fire ring with a tripod of logs on a dark studio floor, warm wedge on the back wall" /></a>
116116
<a href="showcase/market-stall/"><img src="showcase/market-stall/preview.webp" width="24%" alt="Market stall: a timber frame with post feet, slatted counter, rafters, and a striped awning on a dark studio floor, warm wedge on the back wall" /></a>
117117
<a href="showcase/street-lantern/"><img src="showcase/street-lantern/preview.webp" width="24%" alt="Street lantern: an iron post with brass collars, a braced arm, and a muntined amber cage on a dark studio floor" /></a>
@@ -129,7 +129,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
129129
<a href="showcase/hand-pump/"><img src="showcase/hand-pump/preview.webp" width="24%" alt="Hand pump: a cast-iron village pump on a wooden plinth with a spout and handle on a dark studio floor" /></a>
130130
<a href="showcase/signpost/"><img src="showcase/signpost/preview.webp" width="24%" alt="Signpost: a timber post with two painted fingerboards, iron straps, and a pyramidal cap on a dark studio floor" /></a>
131131
<a href="showcase/chopping-block/"><img src="showcase/chopping-block/preview.webp" width="24%" alt="Chopping block: a bark-sided log round with growth rings and drying checks on its sawn top, a riveted iron hoop, and a felling axe buried in the top, on a dark studio floor" /></a>
132-
<a href="showcase/wooden-bucket/"><img src="showcase/wooden-bucket/preview.webp" width="24%" alt="Wooden bucket: a coopered pail with a round rope bail through ear rings and chord-lofted iron hoops on a dark studio floor" /></a>
132+
<a href="showcase/wooden-bucket/"><img src="showcase/wooden-bucket/preview.webp" width="24%" alt="Wooden bucket: a coopered pail with a three-strand rope bail through iron ear rings and chord-lofted iron hoops on a dark studio floor" /></a>
133133
<a href="showcase/wall-torch/"><img src="showcase/wall-torch/preview.webp" width="24%" alt="Wall torch: a dressed stone plaque with an iron sconce, wooden haft, and flame on a dark studio floor" /></a>
134134
<a href="showcase/tavern-stool/"><img src="showcase/tavern-stool/preview.webp" width="24%" alt="Tavern stool: a round wooden seat on turned splayed legs with stretchers and iron ferrules on a dark studio floor" /></a>
135135
<a href="showcase/iron-cauldron/"><img src="showcase/iron-cauldron/preview.webp" width="24%" alt="Iron cauldron: a bulged iron pot hanging from a timber tripod on a dark studio floor" /></a>
@@ -143,7 +143,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
143143

144144
[`stone-well`](showcase/stone-well/) — procedural round stone well, shingled roof, windlass, rope and bucket through the same pipeline. Falsifier `--skip-decimate` exits 9.
145145

146-
[`wooden-barrel`](showcase/wooden-barrel/) — procedural 20-stave wine-cask with croze-seated heads and chord-lofted hoops through the same pipeline. Falsifier `--round-band` exits 18.
146+
[`wooden-barrel`](showcase/wooden-barrel/) — procedural 20-stave wine-cask with boarded, croze-seated heads and chord-lofted hoops through the same pipeline. Falsifiers `--round-band` exits 18, `--one-piece-head` exits 17.
147147

148148
[`campfire`](showcase/campfire/) — procedural cobble ring, logs, and ash through the same pipeline. Falsifier `--skip-decimate` exits 9.
149149

@@ -179,7 +179,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
179179

180180
[`chopping-block`](showcase/chopping-block/) — procedural hooped timber stump with an embedded axe through the same pipeline. Falsifier `--skip-decimate` exits 9.
181181

182-
[`wooden-bucket`](showcase/wooden-bucket/) — procedural coopered pail with a round bail through the ear rings through the same pipeline. Falsifier `--float-handle` exits 17.
182+
[`wooden-bucket`](showcase/wooden-bucket/) — procedural coopered pail with a three-strand rope bail through the ear rings through the same pipeline. Falsifiers `--float-handle` and `--sunk-rope` exit 17.
183183

184184
[`wall-torch`](showcase/wall-torch/) — procedural wall-mounted torch sconce with a stone plaque, iron bracket, and flame through the same pipeline. Falsifiers `--skip-decimate` (9), `--stray-vert` (15), `--float-arm` (17).
185185

@@ -257,7 +257,7 @@ the compositor on and is exactly zero with it off — and that EEVEE has no `use
257257
</tr>
258258
<tr>
259259
<td width="46%" valign="middle">
260-
<a href="examples/color-attribute-wheel/"><img src="examples/color-attribute-wheel/preview.webp" alt="Color attribute wheel: an HSV color wheel disc with a white center fading into a vivid rainbow rim, leaning upright toward the camera on a dark studio floor with a warm light pool behind it" /></a>
260+
<a href="examples/color-attribute-wheel/"><img src="examples/color-attribute-wheel/preview.webp" alt="Color attribute wheel: an HSV color wheel board with a white center fading into a vivid rainbow rim, leaning back on a dark plinth on a dark studio floor with a warm light pool behind it" /></a>
261261
</td>
262262
<td valign="middle">
263263

@@ -361,7 +361,7 @@ then re-reads its own render and fails unless the pixels prove the flat-vs-check
361361
</tr>
362362
<tr>
363363
<td width="46%" valign="middle">
364-
<a href="examples/wave-displace/"><img src="examples/wave-displace/preview.webp" alt="Wave displace: a glossy sapphire-blue surface displaced into smooth standing-wave dunes against a black backdrop" /></a>
364+
<a href="examples/wave-displace/"><img src="examples/wave-displace/preview.webp" alt="Wave displace: a sapphire-blue tile whose top is displaced into smooth standing-wave dunes, standing on a dark studio floor" /></a>
365365
</td>
366366
<td valign="middle">
367367

2.71 KB
Loading
-2.09 KB
Loading
7.57 KB
Loading
4.44 KB
Loading

‎docs/gallery/color-attribute-wheel/index.html‎

Lines changed: 58 additions & 9 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/color-attribute-wheel-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="A disc painted with a full hue wheel that fades to white at the centre." />
19+
<meta property="og:image:alt" content="A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth." />
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="color-attribute-wheel — Examples — Blender Developer Tools" />
2323
<meta name="twitter:description" content="The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain, filled by expanding per-vertex HSV across face corners with foreach_get/foreach_set, then wired into a shader Attribute node." />
2424
<meta name="twitter:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/color-attribute-wheel-hero.webp" />
25-
<meta name="twitter:image:alt" content="A disc painted with a full hue wheel that fades to white at the centre." />
25+
<meta name="twitter:image:alt" content="A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth." />
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>color-attribute-wheel</h1>
268268
<p>The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain, filled by expanding per-vertex HSV across face corners with foreach_get/foreach_set, then wired into a shader Attribute node.</p>
269269
</header>
270270
<main id="main">
271-
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A disc painted with a full hue wheel that fades to white at the centre.">
272-
<img src="../assets/color-attribute-wheel-hero.webp" alt="A disc painted with a full hue wheel that fades to white at the centre." width="1280" height="720" />
271+
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth.">
272+
<img src="../assets/color-attribute-wheel-hero.webp" alt="A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth." 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> CORNER-domain attributes are sized to loop count, not vertex count; the check asserts the sizing, that active_color points at it, and that the Attribute node is actually linked to Base Color, not just present.</div>
@@ -280,6 +280,8 @@ <h1>color-attribute-wheel</h1>
280280
<section class="detail-section md">
281281
<p>A runnable example that builds an HSV color wheel disc entirely with <code>bmesh</code> and colors it with <code>Mesh.color_attributes.new()</code> — the modern attributes API, not the deprecated <code>Mesh.vertex_colors</code> alias AI code keeps reaching for. It witnesses the domain trap that comes with it: a <code>CORNER</code>-domain attribute is sized to <code>len(mesh.loops)</code>, not <code>len(mesh.vertices)</code>, so per-vertex data has to be expanded across face corners before it is written. The material wires the same attribute into a Shader <code>Attribute</code> node (<code>attribute_type=&#x27;GEOMETRY&#x27;</code>) feeding Base Color — the step AI code most often skips, leaving the mesh gray even when the attribute data is correct.</p>
282282
<p><strong>What it witnesses:</strong> a <code>FLOAT_COLOR</code> attribute on the <code>CORNER</code> domain, filled with one <code>foreach_get</code> (loop → vertex index) and one <code>foreach_set</code> (loop color), never a per-loop Python assignment. The check asserts the attribute is sized to the loop count and <em>not</em> the vertex count, that it is <code>color_attributes.active_color</code> (so a renderer or exporter actually picks it up), and that several probe loops match the closed-form HSV value for the vertex they reference. A separate check in the render path confirms the <code>Attribute</code> node is actually linked to Base Color, not just present in the node tree.</p>
283+
<h2>Staging</h2>
284+
<p>The disc is zero-thickness, and the still used to show it as a paper-thin oval hovering 8 cm above the floor. The render path now gives it a render-only Solidify body (5 cm) and stands it at the same 52° lean on a dark plinth, with a strut behind it, easel-style. The height is computed so the lowest point of the rim rests on the plinth. The mesh <code>check()</code> asserts is unchanged: the modifier is added after the check and only in the render path. Measured framing on 5.2.1: fill 0.794 y, every margin ≥ 0.094.</p>
283285
<h2>Run</h2>
284286
<pre><code># Cheap correctness check (no render) — the CI check:
285287
blender --background --python color_attribute_wheel.py --
@@ -326,6 +328,7 @@ <h2>Source</h2>
326328
&quot;&quot;&quot;</span>
327329
<span class="k">import</span> bpy, bmesh, sys, os, math, colorsys, argparse
328330
<span class="k">from</span> array <span class="k">import</span> array
331+
<span class="k">from</span> mathutils <span class="k">import</span> Vector
329332

330333
RINGS = <span class="n">14</span>
331334
SEGMENTS = <span class="n">72</span>
@@ -500,8 +503,54 @@ <h2>Source</h2>
500503
<span class="c"># stand the disc up toward the camera like an easel: the wheel is the</span>
501504
<span class="c"># subject, so it should present nearly face-on and fill the frame instead</span>
502505
<span class="c"># of lying foreshortened on the floor.</span>
503-
obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">1.34</span>)
504-
obj.rotation_euler = (math.radians(<span class="n">52</span>), <span class="n">0.0</span>, math.radians(<span class="n">10</span>))
506+
<span class="c"># render-only: a Solidify body so the disc reads as a painted board, not</span>
507+
<span class="c"># a zero-thickness sheet, resting on a dark plinth and propped from</span>
508+
<span class="c"># behind by a strut, easel-style, instead of hovering above the floor.</span>
509+
disc_t = <span class="n">0.05</span>
510+
plinth_h = <span class="n">0.12</span>
511+
tilt = math.radians(<span class="n">52</span>)
512+
solid = obj.modifiers.new(<span class="s">&quot;Board&quot;</span>, <span class="s">&#x27;SOLIDIFY&#x27;</span>)
513+
solid.thickness = disc_t
514+
solid.offset = -<span class="n">1.0</span>
515+
<span class="c"># lowest rim point sits R_OUTER * sin(tilt) below the centre, and the</span>
516+
<span class="c"># board&#x27;s back face another disc_t * cos(tilt) below that</span>
517+
obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, plinth_h + R_OUTER * math.sin(tilt) + disc_t * math.cos(tilt))
518+
obj.rotation_euler = (tilt, <span class="n">0.0</span>, math.radians(<span class="n">10</span>))
519+
520+
stand_mat = bpy.data.materials.new(<span class="s">&quot;Stand&quot;</span>)
521+
stand_mat.use_nodes = <span class="k">True</span>
522+
sb = stand_mat.node_tree.nodes[<span class="s">&quot;Principled BSDF&quot;</span>]
523+
sb.inputs[<span class="s">&quot;Base Color&quot;</span>].default_value = (<span class="n">0.012</span>, <span class="n">0.012</span>, <span class="n">0.014</span>, <span class="n">1.0</span>)
524+
sb.inputs[<span class="s">&quot;Roughness&quot;</span>].default_value = <span class="n">0.45</span>
525+
526+
<span class="k">def</span> stand_part(name, build):
527+
me = bpy.data.meshes.new(name)
528+
bm = bmesh.new()
529+
<span class="k">try</span>:
530+
build(bm)
531+
bm.to_mesh(me)
532+
<span class="k">finally</span>:
533+
bm.free()
534+
me.materials.append(stand_mat)
535+
ob = bpy.data.objects.new(name, me)
536+
scene.collection.objects.link(ob)
537+
<span class="k">return</span> ob
538+
539+
bpy.context.view_layer.update()
540+
mw = obj.matrix_world
541+
rim_low = mw @ Vector((<span class="n">0.0</span>, -R_OUTER, <span class="n">0.0</span>))
542+
plinth = stand_part(<span class="s">&quot;Plinth&quot;</span>, <span class="k">lambda</span> bm: bmesh.ops.create_cube(bm, size=<span class="n">1.0</span>))
543+
plinth.scale = (<span class="n">1.5</span>, <span class="n">0.9</span>, plinth_h)
544+
plinth.location = (rim_low.x, rim_low.y + <span class="n">0.25</span>, plinth_h / <span class="n">2.0</span>)
545+
plinth.rotation_euler = (<span class="n">0.0</span>, <span class="n">0.0</span>, math.radians(<span class="n">10</span>))
546+
<span class="c"># the strut runs from the board&#x27;s back, above centre, down to the plinth</span>
547+
top = mw @ Vector((<span class="n">0.0</span>, <span class="n">0.45</span>, -disc_t))
548+
foot = Vector((top.x, top.y + <span class="n">0.95</span>, plinth_h))
549+
span = top - foot
550+
strut = stand_part(<span class="s">&quot;Strut&quot;</span>, <span class="k">lambda</span> bm: bmesh.ops.create_cone(
551+
bm, cap_ends=<span class="k">True</span>, segments=<span class="n">16</span>, radius1=<span class="n">0.035</span>, radius2=<span class="n">0.035</span>, depth=span.length))
552+
strut.location = (top + foot) / <span class="n">2.0</span>
553+
strut.rotation_euler = Vector((<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">1.0</span>)).rotation_difference(span).to_euler()
505554

506555
floor_me = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>)
507556
bm = bmesh.new()
@@ -550,13 +599,13 @@ <h2>Source</h2>
550599
light(<span class="s">&quot;Wedge&quot;</span>, (<span class="n">2.0</span>, <span class="n">5.2</span>, <span class="n">3.6</span>), <span class="n">220.0</span>, <span class="n">6.0</span>, (<span class="n">1.0</span>, <span class="n">0.76</span>, <span class="n">0.5</span>), (-<span class="n">68</span>, <span class="n">0</span>, <span class="n">190</span>))
551600

552601
aim = bpy.data.objects.new(<span class="s">&quot;Aim&quot;</span>, <span class="k">None</span>)
553-
aim.location = obj.location
602+
aim.location = obj.location - Vector((<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.2</span>))
554603
scene.collection.objects.link(aim)
555604

556605
cam_data = bpy.data.cameras.new(<span class="s">&quot;Cam&quot;</span>)
557606
cam_data.lens = <span class="n">50.0</span>
558607
cam = bpy.data.objects.new(<span class="s">&quot;Cam&quot;</span>, cam_data)
559-
cam.location = (<span class="n">0.0</span>, -<span class="n">8.0</span>, <span class="n">2.4</span>)
608+
cam.location = (<span class="n">0.0</span>, -<span class="n">9.4</span>, <span class="n">2.5</span>)
560609
con = cam.constraints.new(<span class="s">&#x27;TRACK_TO&#x27;</span>)
561610
con.target = aim
562611
con.track_axis = <span class="s">&#x27;TRACK_NEGATIVE_Z&#x27;</span>
@@ -618,7 +667,7 @@ <h2>Source</h2>
618667
</main>
619668
<dialog class="lightbox" id="lightbox" aria-label="Full-size render">
620669
<button class="lightbox-close" type="button" autofocus>Close</button>
621-
<img src="../assets/color-attribute-wheel-hero.webp" alt="A disc painted with a full hue wheel that fades to white at the centre." />
670+
<img src="../assets/color-attribute-wheel-hero.webp" alt="A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth." />
622671
</dialog>
623672
<footer>
624673
<div class="statusbar">
51.4 KB
Loading
-336 Bytes
Loading
158 Bytes
Loading
454 Bytes
Loading

0 commit comments

Comments
 (0)