From 073904d91e9d02e34ba9e4ac8500238c5095337b Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:19:25 -0400 Subject: [PATCH] fix: seal the water trough and drop bellows The trough was a slatted U with an end-on camera, so water had nowhere to sit. Replace it with a watertight hull and plank ends, and remove the bellows piece that never read as a bellows. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor --- .cursor-plugin/plugin.json | 1 - AGENTS.md | 2 +- CLAUDE.md | 2 +- README.md | 11 +- ROADMAP.md | 4 +- docs/gallery/assets/bellows-hero.webp | Bin 12566 -> 0 bytes docs/gallery/assets/water-trough-hero.webp | Bin 23520 -> 14560 bytes docs/gallery/bellows/index.html | 1229 -------------------- docs/gallery/index.html | 19 +- docs/gallery/water-trough/index.html | 198 ++-- showcase/bellows/README.md | 73 -- showcase/bellows/bellows.py | 905 -------------- showcase/bellows/preview.webp | Bin 11376 -> 0 bytes showcase/gallery.json | 16 +- showcase/water-trough/README.md | 29 +- showcase/water-trough/preview.webp | Bin 19448 -> 12690 bytes showcase/water-trough/water_trough.py | 182 ++- tests/smoke/catalog.json | 1 - 18 files changed, 208 insertions(+), 2464 deletions(-) delete mode 100644 docs/gallery/assets/bellows-hero.webp delete mode 100644 docs/gallery/bellows/index.html delete mode 100644 showcase/bellows/README.md delete mode 100644 showcase/bellows/bellows.py delete mode 100644 showcase/bellows/preview.webp diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index b66c268..ca937be 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -157,7 +157,6 @@ "showcase/grindstone", "showcase/hand-pump", "showcase/signpost", - "showcase/bellows", "showcase/chopping-block" ] } diff --git a/AGENTS.md b/AGENTS.md index c78af34..e7f61cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ classifies it as a `cursor-plugin`. This is content the AI loads when the user asks Blender questions or works on Blender add-ons in Cursor or Claude Code. The content base is 16 skills, 9 rules, 3 templates, 27 snippets, 59 -examples, and 21 showcase pieces (counts are CI-enforced against README.md) +examples, and 20 showcase pieces (counts are CI-enforced against README.md) and the manifest). The full inventory tables and per-item purposes live in `CLAUDE.md`. Example anatomy and authoring rules: copy `examples/bmesh-gear/`; showcase conventions: `showcase/README.md`. The render look is specified diff --git a/CLAUDE.md b/CLAUDE.md index ccf118d..ff55f12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ rules/.mdc - Anti-pattern rules, 9 total templates// - Starter projects, 3 total snippets/.py - Standalone code patterns, 27 total examples// - Runnable smoke-gated examples, 59 total (+ gallery.json) -showcase// - Budget-conformance props, 21 pieces (sibling of examples/; see showcase/README.md) +showcase// - Budget-conformance props, 20 pieces (sibling of examples/; see showcase/README.md) scripts/build_gallery.py - Regenerates docs/gallery/ from examples/gallery.json + showcase/gallery.json scripts/site/ - Vendored landing-page build (Jinja2) docs/gallery/ - Committed generated gallery pages + hero renders diff --git a/README.md b/README.md index 758430e..0114e5f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

- 16 skills  •  9 rules  •  3 templates  •  27 snippets  •  59 examples  •  21 showcase pieces + 16 skills  •  9 rules  •  3 templates  •  27 snippets  •  59 examples  •  20 showcase pieces

@@ -37,7 +37,7 @@ ## Overview -This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, 59 examples, and 21 showcase pieces** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable. +This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, 59 examples, and 20 showcase pieces** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable. 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. @@ -89,12 +89,11 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show Park bench: a wrought-iron bench with scrolled legs and wooden slats on a dark studio floor Wheelbarrow: a staved wooden tray with iron straps, a spoked wheel, rear legs, and handles on a dark studio floor Anvil: a London-pattern anvil on a hooped timber stump on a dark studio floor -Water trough: a staved wooden trough on trestle legs with iron straps on a dark studio floor +Water trough: a watertight wooden trough with plank ends, contained water, and trestle legs on a dark studio floor Hitching post: a square timber post with a pyramidal cap, cross-arm, iron bands, and two hitching rings on a dark studio floor Grindstone: a stone wheel on a timber A-frame with an iron axle, hubs, crank, and tray on a dark studio floor Hand pump: a cast-iron village pump on a wooden plinth with a spout and handle on a dark studio floor Signpost: a timber post with two painted fingerboards, iron straps, and a pyramidal cap on a dark studio floor -Bellows: hinged timber paddles with a leather bag and iron nozzle on a dark studio floor Chopping block: a hooped timber stump with an embedded axe on a dark studio floor [`shipping-crate`](showcase/shipping-crate/) — procedural crate through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets. Falsifier `--skip-decimate` exits 9. @@ -125,7 +124,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show [`anvil`](showcase/anvil/) — procedural London-pattern anvil on a timber stump through the same pipeline. Falsifier `--skip-decimate` exits 9. -[`water-trough`](showcase/water-trough/) — procedural staved water trough on a timber stand through the same pipeline. Falsifier `--skip-decimate` exits 9. +[`water-trough`](showcase/water-trough/) — procedural watertight water trough on a timber stand through the same pipeline. Falsifier `--skip-decimate` exits 9. [`hitching-post`](showcase/hitching-post/) — procedural timber hitching post with a cross-arm and iron rings through the same pipeline. Falsifier `--skip-decimate` exits 9. @@ -135,8 +134,6 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show [`signpost`](showcase/signpost/) — procedural timber fingerboard signpost through the same pipeline. Falsifier `--skip-decimate` exits 9. -[`bellows`](showcase/bellows/) — procedural blacksmith bellows with timber paddles, leather bag, and iron nozzle through the same pipeline. Falsifier `--skip-decimate` exits 9. - [`chopping-block`](showcase/chopping-block/) — procedural hooped timber stump with an embedded axe through the same pipeline. Falsifier `--skip-decimate` exits 9. ## Examples diff --git a/ROADMAP.md b/ROADMAP.md index 01e2e30..cba5c00 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -118,12 +118,12 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo - ~~Wrought-iron park bench (slatted seat, scrolled legs) as a game-prop showcase piece~~ **SHIPPED** as `showcase/park-bench/` — scrolled iron legs, slatted seat and back; `--skip-decimate` exits 9 on the LOD1 ratio band - ~~Wooden wheelbarrow (staved tray, single wheel, handles) as a game-prop showcase piece~~ **SHIPPED** as `showcase/wheelbarrow/` — staved U-tray, iron straps, spoked wheel, rear legs; `--skip-decimate` exits 9 on the LOD1 ratio band - ~~Blacksmith anvil on a timber stump as a game-prop showcase piece~~ **SHIPPED** as `showcase/anvil/` — London-pattern horn, face, heel, waist, hooped stump; `--skip-decimate` exits 9 on the LOD1 ratio band -- ~~Staved wooden water trough on a stand as a game-prop showcase piece~~ **SHIPPED** as `showcase/water-trough/` — U-staves, end boards, iron straps, trestle legs; `--skip-decimate` exits 9 on the LOD1 ratio band +- ~~Staved wooden water trough on a stand as a game-prop showcase piece~~ **SHIPPED** as `showcase/water-trough/` — watertight U-hull, plank ends, contained water, iron straps, trestle legs; `--skip-decimate` exits 9 on the LOD1 ratio band - ~~Hitching post with iron ring as a game-prop showcase piece~~ **SHIPPED** as `showcase/hitching-post/` — square post, pyramidal cap, cross-arm, iron shoe, bands, two rings; `--skip-decimate` exits 9 on the LOD1 ratio band - ~~Grindstone on a timber trestle as a game-prop showcase piece~~ **SHIPPED** as `showcase/grindstone/` — stone wheel, A-frame trestle, iron axle, hubs, crank; `--skip-decimate` exits 9 on the LOD1 ratio band - ~~Cast-iron village hand pump as a game-prop showcase piece~~ **SHIPPED** as `showcase/hand-pump/` — wooden plinth, column, spout, handle, wooden grip; `--skip-decimate` exits 9 on the LOD1 ratio band - ~~Timber fingerboard signpost as a game-prop showcase piece~~ **SHIPPED** as `showcase/signpost/` — square post, pyramidal cap, two painted boards, iron shoe and straps; `--skip-decimate` exits 9 on the LOD1 ratio band -- ~~Blacksmith bellows as a game-prop showcase piece~~ **SHIPPED** as `showcase/bellows/` — hinged timber paddles, leather bag, iron nozzle; `--skip-decimate` exits 9 on the LOD1 ratio band +- ~~Blacksmith bellows as a game-prop showcase piece~~ **DROPPED** — leather bag never read; removed from the showcase tree rather than shipping a crate-shaped miss - ~~Chopping block with embedded axe as a game-prop showcase piece~~ **SHIPPED** as `showcase/chopping-block/` — hooped timber stump, kerf, embedded axe; `--skip-decimate` exits 9 on the LOD1 ratio band - Wooden bucket with iron bands and a rope bail as a game-prop showcase piece - Wall-mounted torch sconce as a game-prop showcase piece diff --git a/docs/gallery/assets/bellows-hero.webp b/docs/gallery/assets/bellows-hero.webp deleted file mode 100644 index bcf9a981c5802003e4d1952531b6697a7a4f3611..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12566 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 diff --git a/docs/gallery/assets/water-trough-hero.webp b/docs/gallery/assets/water-trough-hero.webp index 2395fd8287c6881604205665587c30df8b8296f6..e76f7cd6c7752239c478651109ce8072b00bb72d 100644 GIT binary patch literal 14560 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 KSxlx?&;S58MLHV* literal 23520 zcmV(;K-<4kNk&H4TL1u8MM6+kP&gpWTL1tsF#??dDgXu00zOeFkw&AUqAsGc2&vEt z32APYzg@3m#5eG}?WBLxWptPXx4Ahge*8byfBuK}6dJp#*O`Y$_cO`M&V189*>kwx zZ!~&I`JYn9{tw$99K46!^Xq@(?zbYI_x!i0mDBbElQA_y=lGBL&tQMypVz-zy9)Wb z{+;Qa>X-fRsXzTMPybjCKn|B*-QEwLl>ZL@VgDb%_uLQgo^0zCb!X>aKEHbXaPt&g(LwISc&yRr*)-dMVB|%f0Cld>4z!n0)SPKDR zG@l0~^E@)k>*JeV(~Ye#wwwWAECqoA*b4z*D;x!YuoeQq)Z#*T?YCJ}M+L6~-nCT_ZpRfUp>7&=xKK5#D({d5MYYK2i>} zjz{JwxHAhkzSKFdJJ<)He3Co1s6LGkgieWe<16tml_7mB_STu6lMS#Ol$C^Yj0S6>p;w~az z|7dJ~{}F@#xYO;3V<+IjMGG0{pKgvT+`nEGEc3p7lMOo@6K_I{SQ0D+x#QuZ@$t+U z|9D62!!=ou0vNL&Ct7V^{&%inKab%EIp6#78b2Vzl(^yJ;SZBl>Kw9o^5&fjofG0oA96~LxzM|7@$M~98EVS2 zF!081DT8?KjT z3I2$5%%>H1@wOHbvs9?u8vhb;E1P{=Zpx-9-x*EnU7p`x{4I^C9&66(jf*N$ zl+!0%Abf0-P+hkR7JvsMzC64?YuxO(WVfBR~{RR#URrT_o$-P#yZJ_;w1 zt5IKN--aRDAM)?GfxJQSSXa%zku|TIJb>A1TaA*u@#>w;>rGz{fU;@yR=+nhnC!7JQlP?1 z%jT~O=PpxM46fBm1jE%xfJz%8<|7+o3H$E?IyWbe9koIi`LA!Dw+QazXi4In*(JsF zTJ8N!GIYpoN{@dAQC}UmwLZ)Lc_R@bz9)aAe_J>TtFHd%f3WN0;I-Q<%H|#3UdR5x+m)8c^{GlWM%~fCbIL zdA=iAVq`gu!ZT~+<7-|GZL?j%Y{?QJrF?BwrcStV3=) zI6qFWXUDrjO(b9c;D{B%g@C#1dSXsdvOM#keRmWRNAJUgwL&Yk9Jc^i3u##`;`RAZ zUv^9agU{P>uuT2eD8y-gc+R!J&82*k)ZvFFAbDO4(;kSGsuMv>m&h+WYYzaPAMsGU z&uQqo^_s1GVeh|Z>tw#*IP~@74C|lQDV72W!qorx{V#=KryCC&B{-z`UQ~#|>M=R$ z20R`ZvBNf#GN-H<&wKRbXFEbrd1T;;xqQ(Ca$HH}YNd@qBcWHT;kD&PwZmP9}T$#*K`}&WPDPcxCa)Ypa_Ly}Mw* zl404io%Ao@a)}Cfw*?Z_Y6kw3D0k?Tr;1=2i3J$oe(eV<+YK=GVO|Iu08Uc(Ee|RNfdi z*$Os6kxPwe7;U;B8#O^wfc2+$tchd3Iq|EnTVBn~Vfk*{6r>#6@6JJc0R;;d^FP@v z0!NMui}b{6X04A}>g%hdgD|kqDoD>dI>4Sy3zB5Y2eoHX@5;+t9C>72eO3QL5aZXp zpZ#xj%pnSBow;7jPY2@1Y=BWirm{N>1sPZ`>i8vSV_YvIq7a;-3-0uUr8r>;mx2!b z)lU;{P`!W$2lgV#OZwRIgGHB-W4CjMGW(F*tc-T;$iI72j=l^Jqo+0^x32b}(A|zWtem7dsSS{JcsKo{IW8uM-3Ia8F;h zX^GlR&>FmjW1QYlzAa>MKfkb7#uHJea?O5UKIXY4PRH5^JCB47x^j?daqC2iw8rMG zd9vk}+tBS>IU($WPCVrLxIdNJEDj+d>Ukgu&0eY?Q2y)(6Q(_dudBQKV7=fNj^dX* zSifd&mIHK}n`XW;x3qhSY=8*9#x=9W?MfJqJBblx3J_lFucKg6f@Al2FHyVX$4jed zXn4<+HcZYh#iFCrUJoPBT5?j?FO|h2o#8*+k7&9{U@Se>Rq1!N=4Q*E!!v-14RN7* z3(xQgplE(`H8c9TPbiZ758^WOj`pyHSc|*?t8P6DWBNjG{(`TfWTkQiR+XB&0*3H8 z0}_rrb`x6<{0F>s8+mC_Ec?f#M`OUZVb*2aP>I#!y#uwmp!hBK=Rv%y39!)#jIfNo zGS)A_V4rw%cn1$|82G!geq!#WKfl5vq)gx<2UUwr8H-6<@Wt!}X1gMYBb zbeH~qd6*qszV^b_LFea0yeU0oWzXmva8quN%vum8h!y!OK6edvpZzCSxljry*)&|+ zsP`Ejz2c1MI);#O$n337Y@BU#h+i!L752NmHaC|W&B`r~%1TZ4aorb|C6d3R{M*as zeKEN0yf3-k)Kh)pij;I4J^v8-#9#;>ry6V!GfTo(%ppv~*ujWv^PK@NlaECy>pF)NwEmJA!U6+V6nVF&)N_ zcVegp8-EHgyw4<*k`agQnU3XR3Y^(<_X}U1AtonOyj{s)0S-P)e~Abkhj9=m!k*TI z!i^rZWm~qmx2A+Z4qEc`aSYh27$_6spJDnfl-J~8T1jLXkIug8phT6z1HzUwblAaZ z7j5zP`>BogzEFucU{)LsLiuVYr2MBk3fzuu-{u@_N1CKqU4mPcwx&z)nI(hZ@~B^% z!W#AIPzX^sdXIZ{P|kx}R3BVAdhpBl=ZHGayga~Kj)IP~)b4z>SfgtJ+X(KHd_JdQ zcgsXTockS%ijiqgiGqTql8)?Aadg{kiF1>VOi+mm92xdw!NA=Mt)6mr_A0cG5nI)f z5rI^HMYWNfOR`m6RBLqE-D3Y;N#s{FSbqH}hE3#4_~Mqc0X=znz!n0-*eZI~;cN5& z{zz>H#xcYXF`Z(+AxsHxmr7yDh|md)o2_moGn0(QH1OEaf(K9j(nPxbr&Mo{ArpfX z(pN4{g^c3FE?;R0w)AU65+JKS!F@Xq(u+1uGBXw^Hs2xP=Tye|C5`~F_60_&ON2kL z%aGr325r8yZPOYh6kYeZ%|e0UB9w7JbPet&|MjcO6Fr8tERt>fgav+E{_Qm1=2_iE zoxI{rCLJkB;Plkqjz?>nNUU%tldc>?#EH+LhSZ6CfnsHZl&}g1uvF*pOeV#|7YM^z z{iSg<%NgOMPn}yE}Sd@g~ zdS;5jj7A>rCz6?4m=r)6#hhJ%3%#c_H((AV2 z1(9?Mn`B)Rd5I#_IR&QkN!?oTYi(_-EiX3}vxV&op-xMLHc4cd+4vX#1I__iV5Sc0+*Lxy#D-M^mi)2q=Z1hh>^jxk3P?hO!k!J3L zQP0b6Wj3~M|5>>@n})gno>bsYb5S-*a!g1KYpJG99iP}{a1gB6kX^`U*4T2mLQ)k| z2^1N|z2NFh(}!EM#n!_GTKA)iiS1}5ft&++IegTG?8bM~4f77fQt{i&n-y3~e^ouO zpJeNYPBymQ(Yvi{g(X!aA$)?T4)w zJAV|L6$?rBKlcvcA|fK%=4pB$ksKe>TA2nQChxt93}bUvyxBQq_Myj z0soLTwd2D{u6^(DOBwrCN!Eya;%V)o5M*xt9v{+Rf21CBRZ&-(p0&K}!Fce>^UN4^ zCmUO9*9^7mTdgk!w${8FWPVf>Z^GwrOcyZ)PL}^)Aobu7o!*oHI|OcXU6vN6bq{dR z!Yl#Vop-2_%>8&}$(1?3X*WMSYypeXk`TEIrX(I9@04TwnZQs5es9tWFBqrrxeiRm z(<;RukWR$Cc{!whFZ+;6B?;abTA7mpt7F!V%}K`A+Sh{*8zn(jnB3K`HcmFhkoE!$_CZm5~_E+Zl2ekCT&#T!wP6 z79PPakaRLksip_XYUjV{z=aOpbHtdd}A#|(n1JG^MV;AUT?V@@>%9`cBDud zM&8nk=YkMX^lzKDX^L|*WaDdXZLP#qiF~vFBG$S(S7)jnos(m9GXB0q6NC zumJx0AO7ngEpav@>b}2^j9M*+6q;x0eDRO^&WwxD%4ppoSY2g_KBas7tF-(k&)0^% z3_o1Wt=5C$Uc-&826Kv5bm`+bnCH@YI)s2Vxj8zbf0ACiZE87>V_m2Dt%yNoSdJyQ zCYvLTkDpcy^9f+%5i53e0flX@^Vi#LmgmZ3qz#X1yd0=W9-}|-A|}HhtpuTjH>h>? z*IUgJ{M6B42YZ{{teD2t5%WA*H)9ofOEOZLoAp9H(#642DcRU9Bm!E8RtYvn=4$%nW-oXTx8xq-MJAi7Icf+{tH_=fR4zF z84<2z=8Zbn9@+Mc)7^J7kk=n{Om4AzJc$bV+oua)viMZl1f+A|>%lBIsjHkN z_i2zn)!Jvc*}_$TVl3h_LhEy*(UyD|pcO(%m8%R#L)EW-jD?+0ltn1wy54 z)}>@(+-}$NEQuR#$WfpTkW7`*)FuTNUzaqbaZjzWs6Z?3`+t2|trcK_J-1bEz`_td zWE+Nz39H05m`yX{mn5CXfB*^F$&aO(@|w(j8ToO8AtO8j|NnE-DdDj!OIN(6Z#XKL zO>OK$9(rhkQ{$94smr&#VrWeFvwh#DkDr-Y!r0S|P*<)J{<8qQ(Id%Q%>O7p#~^i_ zs2w-GU_hggRY_6IQP5v#*0twD4$F9tRm|yDKr%%4Al2nd9~J= z$VNh{8c<)t;<`=FXIS|Z`=Vm=JFuWgyN_$Z)Gj?_S=^sH8)eervU{NYxbo;TM)6rN}d)ER>5~~F- z0}hdO7nm6X{*Zim=z8VMU40vs{e~7HC}yo|#QqP{xXGr-<@zQagq^@IjJS3gxsl8a zz>MBzgVR;ljiFU|Io;3z000zo0>?q5d#CBS0oa!L!cIZ61#<@|4svTXCT&t%P(6x| z8k@R1DT4L)X`rBj)CI+ZLN5oO3UfKoIq)1*?%385gM>09)1v3X2tLOpc43t8+CaC>`Z3QF1SpnYW zH5>u{K(MI$ht$96Xxmr)SPg$t)L!Lf#(&(IIJR22k-?2U-4>dfL?*6(7m^rxoD%6tH%>F!=z4s6pMs(LA(}4qF?_46OIV8D}u$(_FhpY9j7pN^|55d%0GN>3k1-mt_dY%z_m{K{HX^B25mUfproxi=hl#`3Z12y-H`kSxW$LpNe+UQL8r56HD zfehEDTq_DN&Rytzjk~a87?Ar>BA?tVqlVW}#z+s!Ws?y1knN^mKzmEVmY`nNlHd4` z&AAMhO}s!YCK_6pf>yD%Ifu@}ZzJm*+f!&6OUKX`$wxG@=7#yLz&GHGGdI49-czP5 z=Y|dR9!X}wx$NBc7Q;{AKmeL!MgV|_xAY$QB{Iy!I*emuZP930MCF~;0gRhgc(SAnP)-M;@QX` zX#ycud8)LbQ)ejqwFd&a&r+qePWI3Ty+91+Yjo)mN$c(#e33egNft-5krT(TQyQ!Z(gK#0iWmyafF>-^QKhK?{$P z_G@E$@(>Q6DEhN2Wh0@Ti2CdtHABby#p#~;1Ph}c$}bO(o(_abU-DK}16R#<4`7IP z4kUBr7`+UuJ9P@zcG@!OhFYod+X>fVpj&UA!>k+B2w>2BH7tHVrLT1lyz=z<2 zKsa^4dD-m>re}jDj&=U*oLQ3=>Iix^pp+s={xP;V2nQ1={L2*bY_>rD`VY&8Lp+Q) zX$JW{TXVRW1#dLa|KQre`NJeUnteb;a@YzXy-MV_hjmlw-OIV@Wy%M({9Grq{qF}DVEPR|u63`Auv;Z&z>ZaW3 z1Lr(1O-Yt(-)EkYaGMr6&uG*MNN&Cub{)gB7yj#6XaPmAL}a#bSe5TYI!Pet3&7xg z0G=!Pv?{xC!2EwVLmV{I@Gb%ODV7W4=vy5Hd79a@cYT&h6lI~`t*%{rC19K?FY*Dw z$0Z|49;iF^Weo*fIzzNQokjL$>94`enD!yHszk50GghATYrbzd^!4Ye1D0cf|8j$c z$TR-8KFXnISo=zH)U)spjqyO0fN4YUPEQjT`1Jd-g>u+dPtP4e9TQW+ABG|6Bw7G9 zz#;A1fIR0Ww_r%-02zQ8@Rm1WZ8)}5b`-Tr78^%Safkhkc3~S(N)aS}WudD$)ss)w zF*I|}RqR~*&Pe;8zT^6zGFro}08iS~{7DMuW||-T8(1^8ZO>Zk>`u3|E(K_S04|Qk zTa^=Y$Dn7N4u-9yXj)rtgDkBoqs%jHOwm+D;f{nAd2mHf;v-_4cg9`C3uRIzI(A4* zU}KOkPN>;7-K|@k9HOHIeYfQtB(B)Exkk>j@4D-&WhH*!7#WOnI9PoY# zvC!M*3^frgqzn;D6C4vZ%BWAUa)Jq1FT1eg)+&htwo+2mc?o2Qs1fRS;sqo|jl||E zwJ}AVjOR5Vtj(Vw02{8Z%p5v^1hcLH>WBb;LjIeW{q(-Ho!4m^jmQ(vyync9w_8-; z>SVagXe(v*d4okWAl@64w+_*AqvwS{q=2gbd=Yeyhpxu6w~#bG0%#xbKw6~7R^K%* z5qef6R+_`8vVL7+zNmwA1Y=C9Y+r|H({&G6hWdSC9hiA~27$k?t*fsCzzWHeHJ@dEZUd;*mEmsIL5vu1{d6FC0kF96m2 zzp#z{V93l$V_;#{jSl8v%kHy4J~Ouvs%TbF7uLG`7Ce#(q@uA|26Xj`h!e<+rfGOP z9HomfDQpUimFxr-f}M>-MxL!dXzbc5N3p8F`UDI^A7Q=9J`)kLflPZ0YPj2bovgPq zlvD|6``TiR@_K*)&+rF*pv}lfAlCk>b0TfYXg~k}{iCWWLACyulZ-YXVOR(v*^=px z)ppwLCO2qKwZ2xI9ng3~%hVsHa@egX9>O~7#gDF5z$!sYa1tNXmVsZUb|;v$kLpt2 zBUIudx|UzKmQ(EMS^^oIjV|^~$o!Bv@}rd!Pdj%2M#7a93oX>{K*$WjMF$+@Y|e@{ z?^}}EmzLVS*R;)c*g)OPT)K zAt<8uz?mZ)4^N|QAo!`_AlWdlUfso}X4-HY4mZQnnPIB>*#0B(pRHFV>@wtrG;drk zp=%K0Dzqp(-%Uxhb;cJ-y!73(j$<64YL`D=h)cf?niucQ(wxa`3GI1vD-ZyM*pMF> zD1ZbO0DrL4=|3dCz?j{sZfx6ptvEZN@Q0VELOP({1BI#pAUk|APEsX~1RPT(AiI}- z9C?cbI2tU_!MR|ZYj`=iCmI))DW%N%)HDv_t8Q?paW3(`R93>Z8RPLgg*!*0Q*>d9 z;YPru8aFRB4{69piwN(0F=NxC8Ghis^l-*QE(;G2O`dn~6<)Idf{ma;)MNR3KIJ(L zN!1DJ7N4iE*(uc{X&eM;gT!dE5V8{=G)2e;dl<>3T#^kHjdDuZ10E3p{vq1{J?STL zRASn~*EK;P`Ivj;by*3-g?gSQ+egivmnaLvn~jMvag;Lan&$`Z*dWR69i4X?M^>tN z;+bi@LImB{U6!CGR=JwobMFtr!-Y^1UbL`7VU-ztw+3nz19%i;Z^+vmNh+NmsXEE+ zRYdF&zj&Q#s)6({Pi>H7uZ*?eLh}*oq*%>CBX(#vUtqrtnks(ZK2nDg`HidJtwgpi z_!&J>5EUDcfhcMR%76gewLanB5sBxD3?Es#^KZ4_0ABDMZik5w%tv+c&JR)Fn0G6{q3t8h(l8Hj9TU*x zJ@AW&3T2;vD9Kt?Z26~F zRLrXUtT_-6Ov*wKtda{j}ud)HhJTxPOnqAv^eMG3GD>0QGH^tQMU|ZC4LHM z%N&R|%-B>_;bugS?aQ{#r?353lxU}B`41QI#QzJhFyR-Hy+|N&i+>R8w+Ch}tna3K zodT5k+TMi0BSLNNoFS1hc1eppTq$861dBjm*aI>E=TA`uB^qJWzXc(9&&HN)!g(@=H+`^Ny_1xM@HpP|}lbu*G^ zuoH5CuwfF?k)4Ui>2pc;?waQeSFlS%J(}SXuvC4>3n_oB^nr&z=|lgs3Y%^WwN#R< zcV-xILa1R=o9%(kAj;2G2a0erO@ z#?BX!VM^q?I>k|nrfXc$Iw}O%jt6iVaCQNTL2ug5mL<&VFVK}W#4E3uP-Tuj&n0*- zy9F7~_2*M(tk8>*T0a1B?;V3a01_@JuW83?Y2QU7@A2J%KG)?1>Y~sqMbODCNAJzU z0m?wxP})z_eXYugk;yCEMbuX_TZ5!#$%h%(U-DdhpOovH@k+=IQ)NcG9ZfwpD1IZie?cuM zR^Km}LeX7qutkAVz!uoQbTcYuSI4172&GCm^~#g1X`MG<5o`hIK|zo^MP8{_sHhl< z-H-J?#O&h=>A%#!{8X9XCH=|S)AnEz6qxbKIf&WZV}BJ6MKn&c%&&{_fhp9$Yc$c{ z`Z+G}F-Lghmq*qy9E@{erExl}W$1bc2#D6nRz&vJWBWV~@od}L2Y;#hY80c9*7wT% zgAknmh|7-J>YTX%lRUT; z;EZ}|EJdg;b!dgrj)Qk`YrKWZ&~4k^oxi|s&R)Kwrh-^EmCRXF-rc`!60w=1y~)!n z9SC89t>(7U>n@37S!da*u_UIMPMeHz^nCwG#AKvxCSL^uMX|&X5xeEe{g5Zgp)jwA zP|?&ba8~~up$QH2GVVhyW1Ufh(I&eBN(i-kFV^Pf&)x)EPk(7shk=hR(P-wZ$LfB*5L^ z5xfb~pbefopiZ&2QnY;mrM^T)M6RC=lHEAoG~g{Be%T%7U3lf6^nfX;(CWUOm(5H5 zQWa!0&WKhjAiJl-KQB~*c}wdhy;JsF7Q>QK-*nG}HeTc0G5(fi6)IzGL_GqvZZQZq zst*@AGYp`Aral3^HM1*43!jksk4Cw%9L~6*A;NgM#*2n6V8;37=K&cL+zaw z_MA7)j>DeuU9xGZ-HM3v8-rmd*e$29_Elz_zItuO3!U+#!rl!Kip}JXLuhDTfAaTM zA0ViLE6}ahX~3D9kD=$S*4;I@xIDlND^~zWxV$%sl|j*M$0?$Oph zt8)OHZpEaH5r%+oSRktfWu6k-_rd^P6UAlR=A)An9SSw&ckM6G_)s2lD943(d1onn z_;z}f=*SJTwj?P!hyT}Om~4M$S$pkjf56Jg&J|STo^%91fX{xY_DRzATM^xK*xT#C0Z0j?R_5gO^N-2Pxw*2BSr*SOE_)GKJ(xnk~G*22;0;pUCcqIYcQH|nr zBO4r|L1m>Ef(X$Iw=F12oT^Pu#c&cm$bw>bPj>(4LU%G@37onqY__79 z*akrxMyA6j6y8_oy@@dFrKn(W%c+~U(B=jFA`L2MXBswe`S*3=sx;y1$Ba39+kC4C z06{~`A;@P4syY2D%l+Jqm1m>Y8k%~flK1#dmete(8M0+%1kmS4Ihrg)8BuWT&Ce?M zydT6YmQ%0?t`-<9iwjXi;4oq2q6D$Io#Mm-8h61kjZJ7qsDKwCrXqvb4ggzCk?ypi zZGw$w#ZgWWo}$G$-=-wlRqJk~)70!UIhR{g-XBhAxzUq#XS3BZ5VlW~Ia}hE6$GrV zHf>gR%lq;izx~4A(13q}Vwy%!83r)h`!!hb+6XzFjkra z3T5_o@yVeG@DBUf^$tCUdg#_3Ovy8^DUP{-!|v8~bM9)I=cqMw;{I#kmg!J#X@bPt zh*9X|2My{mqjr04h`~?QHf%=0$Gx(ib5r86dHl33Xk%{Emw zqmu_7`VOVpuNn*$*r(v2QymVo);u?p(u`ziMaly6*BPd7dr-Pmv46>zS1vH|x$F*m zAs!Yc2inqWqKeoS2d3y_b*tJk(m-0|0R1zJS#^lJ%?9P6Kq?$l+WcaMs#MQJ&1`sf ztH?vf`Q?s%yu@~0F?}-HaNv{>NTBxtjr8|9G=_@Ea%+le{65^dXykHLH7f9)sI?j@ zUjk6DG{d`$F-Pgn)X;NIoPEW*DgOxj3w4496DMS1*bNDX*V$2Qz4KfgZjybv{<;%9 zjdt7vU|85Z9-gZyMFu4jRV7Q*nR&%WUQ#fe?Rwk340{^^&6;yUj2cl%q%0kU2_k-%C)W65)vf3&;I9egrW6gH^*!D z@cS5_wphM)V`_)=Z^iU84&yTOcu+`?2>kVj%0t|<0I!YWy4h3YjcW1dKKTKJu9dc# z0X9@tq3abz2eLeTC&`CRRvkG)IN{4@P_^(!a66qBFwXfXl#o_1&MXWi#gcYO`km)8 z`l7x%a@zW-?DVw1A;hc`-A!TGc?H`p$=hdr+oxtiCrta3X81uHR;2l_V7w}&@(uOu zPox`(m#PwIEQg&F>-oqtbRe341vm|-Tq^MrBqk~>2*tK`{vXO)J?77tH0XCvaIpRH zTG{z&BefPd7v-AGc5jbrz~SeG6b8bGLT?XGSh*?P^S5z3N6=~9>e#tfRCv!j^12tN zkCWBENVlrgBx*OavV`G@^O{3<75b3}w8(m-$_OB;XD5-{g_7I3r+tY{zI@C%i_mw? zwJu-tg=7H7Yf2Y1-KqN5lqW*8#)Mg-8EppoH7L1XJoGqIORkos8Tiaf-0)~GI8}c6 zIQ?s`r4zG=;GmfcBZGI3QL^htY4oe}j@QzWa9@mbT6Zkiv?OFP8aZ_fkb-neAff(1 zwwl1S#T;02o`~+O)N2*mzODn*HWlPYDkED-l_MxpjZm2=VrcLDI zoq8Np!|dL8b(#B52Yz};9XIh~7&dOpW4MY@iSDR;qtNpM@H+48L3KK`=L#7L>=ke5 zVd=DHb;kZ*-nYCi-HMvx>p@8ll8MB4KNeW4Z%yw6pv*1|DeJN?X$I~c-r@)cOJYfr zCcJ%qsOS^twZOu)Ym7Ed8X_$E%f7Sx0Bb!blZlJckeFu<d&+N82HrM4j32Vem4& zMzWpjIUADgafkeZjhUd-Y~(+?1Z_xim`#l$g%VckV{6PGJ<8m+@3GpQJA>%-Xf-RU z7G5H&YqE@SUGW{4+dxW}ikM{j^VMD6~`Ey?y zsq!ox=_NL5V_+bPn=+TVUK~z+5Pb}EwjWF7!69kkONQXj==IYw`M&wdlTh$C5qrW#B2`p~dK&{>hx8}tJ zbsO92m(vaTai7FfZ_Fm6eb@{EV|guqT^WfzKk4m)W1i1!^AlBeM0s@?`P#>J zw!Xk&PY!oYuWE&D4fU7D-@R`^~y8`>3~hN;V9hS?}h5|S^>zPUde?-t+%`T(3C zH}%7#C^6UXX4z)lu#kIiL1 z^+&oSbT@WszW-oa2Oh&Z80z~F%Xl_cXUFk1z}gq=9$!zP6Y%W7cy^2H4DOjRHu+Z%gEtB2-Eg4>hWKX}UR8LVVE{po zvLQqA-F{Kbrr!v#?C|U-PdiU-e|izntWS~&X5BoUfZA~T!^8~nW$6=@ijq&W>JV2V z{9dEIy%+>NkjS9@+JS0rSOx3KRlaV!i#`PqV$57#xC}*EnbkAhJFDb!&Kk4}O<#K= z{X?WnP&}SuHjKSJ_aQISH@`%LBq-K4ZAVtWS_iCO@s}u_MGVpw|86_#RtzRY zWT!r1_*t%m0b-Ul?SW6uL)S|=fI55->~bq;db}DFamx)?1}F-gG)nXjp%x}p(+Uaa z%2$T}Ha^GxzB|%jXd1KQ<~{h&5``&@h3eTpjU1Xl&n}NT1p!@}x+^2K=m!dXXm69$ zqk*U_?N9q6j{H+DEd79=AATeWJk3(d0~XysJ}r%*{bhM56bpbYQz7}g;08;^~t>^T1-9PO}cGF4U>TCji9SRk>_oNGR8+Lt27e<3Gec=_@j zjn*U*aizD2jv&I-ul%h8*J1YQ{M=l9jNa-&$_lpPs0Br~uF zeDbZWgPr7+wJ|`~U)l-ZPv^-Uhm~wCV^8OsZ=c9<1!wkVX?BP-4tClz zIg9XYREN_0fw;i^vU7;JB7k%-FmG{+$%opDkWy7hJpb;`3i#VIWC$ilkya`N5X5m4 z10`fH`7{178Eu9F@MD`1zuvO1jfZpcNUlPWn-f$@P(Zmw{7|?$~qt$H+ z><;E-X)F9eY-d~p<<6AF0uE_PiJMZ7F@5SvxAHO=1HT1fNv3tCd@p*0PL1*CpK05* zAU&4s^-%GMy4L4NnQe)RP;+-uLa*OQ>m6ME5zI^oPB^SgPQBjFL|C$0e&mH)zc@JT z5I681CPWqB#U=s5ttluF<;3wbSD`2rAD{O23dK)f^K$=m;bDY%^GkWbG8WSuR~qx2 z9tH63fZd>ANA;&G&VWQZ_1PRCtBqR^(I%$&{^Amuc&nP_VL42_Sj;W{i5bOZm@XF7 zl4^rDjugQtyh9YFWMNwfO0U9_Es%3CDg5y1Q|K30rW9rTk+bl2o|+UjA|z$18Xw~V5Drczfs=UrlIG-|HW z-A|EW_#|#zhG0OA-DdB@uPP~jy&)W}M#P;%!8}_e6p<2B>spqhH_^}fVn$e#Vl~EP zk=3Gn9y2;J@51tmBoy}z{cU*irr*xP>wSc^aV$^HlkX=!q3KL^EZdmBdUM~NZvd)EY zehngGBzXBLz;B+-atMsAWrR)5iAP4m(hqErqU!4L!e_dKBf$|j%5Q_vxbU>|FrOUC z=7n@gSAPe%5LS9+YB^x)NJn|(*7j%&%SRAGyz|`bvMWi&9tFW#$h_9IpVk-3n3je^ z>M)uwD>`REzF62@Zw&=j=Y8!Uk^b{y0Wc4fV}AMEX&o9Pqn6CR?bg3t==?}bSdp2w zed&+;Zu=KFVX}?I#D)MsikAV%WzsDp+{k!xE5b%svS~kY0rV_z;(Pe}Y~hWYO={?#rjY6nyE`V&Z+ljBIx@^T zA)ZuYIIA<0Od2Wk?_8~=4w5PLOclTe5ACK8sc0tZ#MDzdZ@9*{QAY@)@?8%Umj^lwjOr=p(^YP}+A zHQH6ODf^huws{@X?gj2)eiKqX8Q7XGZw134HmU3`yQ*Y>HFZS(4hD8I%59e!NVHKN zaSedk48jU7gzSfTEiMCSY*b%+j|!=ymtj<6#%&Gjtr8N|_mq~!yDO8Or@OkI#RPj7 zV@y15b*i1(?C9-E>Xe0C>k@X4gz%I470%<(1O(5;y9QsTNM~Ck*eQ!CDXN{+S!^H8f&zTV^#X`#x5BX0X1ejPibF@pTBhasZ9Tgw8&Pb(W%g7nu#%{yl#6 z$QoZkt?2&h@BzV$kO(lp&;Q$_;6B;_@Rw-`K4}Whv#z^_&WKQEAV0Y@SfFq5AE8lJ z+4=zsK8dEz)2)%83#}EalMNE65<^857ZJQ%H_d#39+atOIvl58r&k|nC$^IPhN==p z?F)gu7ViDBvpEgdkfbwT44_16!MOl^xunf61pJthL3p$>lIy}#_O?TWR5YZf3twBG z9A5_^;dJG4-w6xuf!Soe%PY)B2pR5rGC&W+q$z-M`Q`|`~fn9_uOd}2qB%k9Y zIQh)93?e^QE!c+Ti*Px3E@X?YAZf*_=(8+zV+fSziqY)^$8kk(bpaiYexQ!>?S35D z2zw+Q63|c;DZhsKuU#t){;VPxzSbe13b|Gb!ku-Vd#c@i3YFjW4l0aaNK57_`n5AT zaMkfKYTD&_jFYKy@aW&7_nV`smoSr-K=DZ|=*;~XicJLf&Xf4h5t{367xc9ne3^n9 zaFObStSOJ?v+82r#b3Gk$E=b#R(44S>jM@EhF@ z62lk>___+zgM2L-@yshlj{VFdFQC0W9l;4cNm*(KvuJP{z!$474b>LVN=flT786gL zW;=)-HVm9rr$a1cb2xv^a;y#zj-K=Ew&AD+)JiV`*YgMk5mjv-vrGG3KOv{Zul6sp zIGC5u2!KzlYqP{aY^65y8oKvfdHkTB75Jo_W`oklsJvE}sS!rEjHbqOz;UzPTL`@5 z;E6}SIu#xO^Co%Ma8bMM?Jcii1*-zb?eY^wK3sC*FMMpz20o73L^~q1NV8dTDcxoW z23C%nd|ysSB!|_b1glkjc8x6X0V|x7xeC09nxrA$!P@T9Lm+_P6fj4`e;%3m4s8t9 z5p_eh>UsR!Q=qWmmS1?B9FcTn-BQGx@w--#SN!7m1taqTB6M zCvjql-}u1_A%pD9_{qW^aoE&-7nmg}2z*IYk4T~Lb0|^6DBSy`u^SX(w$*mF5e{Pj z?J#0K%;kI_z8ok3#ZKrTM_w;H2Lf?rE3+|};bVoA5zt(9taV@*F#Gi-DuP0S*|;&) zW9#0@;w*0Ia+UR4qkx@}IgS8F0v)T+vno=P-){1!Q~ zZ%u#vu}qz{8#Ki0BLLNmAJ<*_)+Ce+)3?4E zly?IJa!OuTay%tbZuItXChCaWa-R8|#L9>=pF-=JO$>u%*THycN`cEBqi%7G`3U*9^YEI;_eex}OsIiQr z*3UTfR!2Q$NnjYn$A#G^SsTFt_{fSC5>4jbY3ydeJD(RgHOjcg-No_G##_{SB8HT7h)?lgfqz$DzC}!iW23keDDemRG8h0VHprp zOoHQ{1omqpY^LtuBqP>RcP*3NlhTM?krIrf_NPaiJ=y@9>b>bB(+}1WOQ~)MIHAVE z6xj2AX_53Qy-O%dO`^EQ*msq<+uK5CoI^zF!`lm;fhdpR!@lKn01CU4*vM=ghaI0h zRwt{js3`Jfpv}vi;bjp3ML(}UkMuHgBB}|0@ycC=0^(&>iU3!8n_cL|f)E9wUc$sU3s(a@V zy*XV%eC;pJ>q`;m4*x1HyhG|9aC2}sMHzW>5pU=A*Z{|E>GWmRL?CK#E6Zo92o%ay zJOnjo#WRH6Ss&*ol9y*{aSna~!=re6bgD?#5T(rJ(0Z+FM>w>OC81hn$zB4};iNw>h z>GlIS^&)OzniM}Uwu4YLa$?rR#SEiR#vw!Xn`zkCH+S`ZT?8j&9KU!;)PPfd5N^&O zxC9+{;&)TuRJ65_BYZScaG)(_>Koxz@*pcdS}iD+sF}Bxl=U3uW-ch)(ixB-*xJ9v zY@N2|xS`Ni3VQ@?+f4wDx>0)*u?kie|jlo%|1bLAE z^jHJopLTyQQA-5M(WG!sn63!{U18V5|H$WD~!^2`SLr_8bu50-Fb%RD<4QW{kRQ`^22xK}SZC*y9F>jbA5(BIMB z4#?NdIm^@F98pH*;Fk}**5EW#*yC=X#@T3B^V)=o zM7-fyAhU3_nP{o!miGzxoKaw9tP=h;RRGiQtmMlegT zUKg-wFC~v%ut8qn7EI$aPatu6wSd5~5*L)$e+eb}j+GbhqXYF`HJ%p zB&Yl2!6MN)W~AkmZBWLIC8wy$MNf;S59$!~ zGN*PjSdF+9aSF|>ZY+A*vSN9$+j=K2$&Zx1pKJaNInNzB3^UDv4uQQQw~OxIWCIjJ_nyDPQu2jFm?NgWPOiQ^@!*X_#6)tJXd>WiErbP4(}>Vs~q$Z zLV@VB)YgqlXgz<9yb8~`lX8q&^T+^6?f%nwcsi?>5PdFh)6lrxzhpNJuUv^%;&9cv zi;*C>)GGN*JVy;uZ>GJ5tC4NwMevW#vqr9c-hlMd^qPRJ-Z}+l8DHX=6TfK^x6@$E zOCfTmd6Wo%2-*d)Km4Hj{*9}q0*c64;}OkG@Se{%e1Rc zYzPrA35s*z5cDE>9Bb=?l&_6(yJ=K+Jb}WuNtLSSi)|y|bk;tbDXS--_Sa}QJ!wx_ z)ZQ8m5TVnOyK|H(7~&rZ-$7xAYYt_>o>z+F-WcgE7f5~C>LoQ2RR#xh^f7P^pY?veoLhQ_*d-+h5(0#RMw(3IHty?7pBDUCt;VmW$^&5j(TX#7T5#vX&1^!}{c?3_~`=!!c?-oIP-GCrY{e{|Bbj2W1DZ zB=x|R&Jlb}OBy{!rXQ2DP4aZ zYNI!E4HJ3k)XMqPfp_&D{JdUQp$p##hu;sY~tO z4`VXjyvy4#&yg8=C!$`?HKYYJ3kyNPrCjlyN#l38GIvM1(7E`h+>oucF;= z#S{m)M_TF{nN5<-$Rr(4$mxi9!T9B^Ofz`CH#T;9<^SMx{eLNYOf)ifL}ZO&y6eLFc=R@k@o=K?q%zy2P}P{NKG*~ z5fN3z_X^2hgGvq=;_6!syXqjP?sC+dCnwB+v8;5==Aa#q#`9KNhsy&Bq7ko9bZ`g8 z)s^?V3SySGFdtBc4Mrt`FcTN>wV@fa!IaiHy8vuJI8$Z#+EG8uOg584*jh%C-oBzd z=KU)VOa{~5GVga(z-!h9TUYQhm4@fgr!m`bqpU9pnFCMUfrNv0jzqLxLq0-uPveMSrO{hkgiAMfhe^T7l zWC)1YyktV7O3I>Y^ZSp^($%Y}GuWnmpMKD(ZEnekrAwovgf%xt#P7TV8>N*PK{mQE zhFTWLH)owdgM=)^FMtT%tyUp)%uvat!|5b|%=8guQ)I`J>`|(QUxG!|?lQ$Vr5pDP)BU_l{$468g!FYhmjd*na9ZRC^CMT4*tF8h_7 z=mZ3yJRFM`mc_@4>6eJ|W1Kv!hLu7}-=rZ@kTcWAhJ;lI|L@0tzNX#q>)H9-i6}}6 z^{jrDzmhnKL5+=R{%}ku$s7Z$ja#ddq5}pu?O1)DGQ@x%R~!B=y~_O&L0>E-r(|Uo zE_5#`7mxmh9|7~zz2hcryN`A8?$ZVOlWbS;60lp$7}w}1(}nWgx5==jaTWIH-<76W zEOq!ffmZ8Gci^LlR`Ht~Yz2ByyO#eJ2)g|_-PrU^DiMa5v2o>Ch7NEAaom<&V(kPr zv~7{j=z{$!u%d-{310%od5>x1_1$`nDB-kw`G$M}s)w3?2V9+~uEM`DQALS4 zn^av+Rvm_LKTcNed`PyZ?T?btF%*1(M%OaVckyxH+fx>K71e#f{3BK+&EciC3tga*@rl^{tiERaaPyP6`++jxZXCjZn7wPW^p-R3{gg#mg`-Qau z92aL8;N^WJ-4_33s7SXeK3G}`As_7VT0lj~WbMQ6LQoMYYMsbei?ge?Kr+6*QZ3Qv9(8C%CnB(_RyKDs)vNc1vg883vBly_#Gr8E}`^aVIx>EBMBfkFu=1+3gp+HmY|yNw1Zi2#m?eq=i1kA zk}E7Y7n`-E#`MWbDRbCcYb)N3#2N@^07W4d)k<`Lg`+jJJBmYEi4qOB1YHT-#P^?_ zy1vl^x;1f~63y_upQ%baVL|@G55hB-_AsrC6~E46SlkCERo(FoO*=>G zz$Up=urlVJPmqsc?2xLIj;VmanruhRI`smT!vd`fdQ@J~jVIrp1ZmqCGp6RX5%5HjOilcW5SN)xZ`WF1;!1pXN28E* z_0CTA-h`HV&7_G}Rwc+F3i`f&R0_BwK6h+h^=jNX(N@7To?k~y} zIbToyx=IS!~WY*YC3_BY@D&U!?S)NP%tf3ziY15_`=_{dotvO7fU`#%245srX1 z9JAlY!j9^VcB0jL(){QYN1Q|WByu_t~^16*fkrM8$|dtTi27wh3J9b#TgUhA44ByxarthT=e{3Tg~1UAV` z^}O&ga(*SM?T*E(W%-|Q)X&RPOS>tweEdYbKA_OaLN3O9kJzOdU(RO{?6^jG&iFHj!pS~>R*GHw z@<*ww-<1uKj^EVD&V-nq!n=)25INq}A2ffa%%Z2rnkXZXOh|zX8fG5yM^fz9D=}?O z0O0`jx}Us_3FK2#MbPw1)Vlmed=FgpafMrgGn_^lCbtWcXc$QMo-$S8!SRYQzbhCw z#*{WIm@gL(9%j9TJhf;d5jA;()O76MZh(L#Agj39)v{EB_HWO zsG>T@yFSg4_hG!o#2La5)xPhrS{ARQni{6vD=#1G8DV!<{scRyleE7NwgK<#Td=zm z3Z8}4!NGDJgujw*4J6bQZTsK9-Cuww z{${_Y&BRxGESr;Nv-}r%_qKp0~)fhdE-ztJ(bCk+%~kxwz)vV)ua3F+m;azEP>@{9xKw{c1F zj1AMM3URUK_-KY$#?T)dHEv0g5J{m9Z2}S;co*DKZBVjGTCd`w1;^c$uk##PmZAJQ zGj+hN=-BKe-KE;)Q@mj;9X)q>K6{aqg=SzW(t}fhkv-;kJivWk^cIFb$M2pPf`-FK zvyz%EeVm_65$a!?w47+?o=~^ubC*g{rW*LnmcXNJ5r#-Xd>lLF>4p##ERLRNGTe;j zggYZ1nh|?IY7D&?dTESqDJEmR%H@e>smpW3C!AB0PrWYi;25dy-{<|pUGOal5|#pp z00Cr{gP?luJP&3gPC6Q-b`D5u0+}_&F@5iu=YeuwHI1(U8w5lC5fY6MDhvnlMJRAZ zJ_92QM*cVkB6ZkSkI2Lzvr4m-l=};d8^w@mM|wFpk4HgCTO3K00?nDxma2At@!QpN zaQuSC!KZ(ydUK7Xx_B{?W^f^TW9neX@!VBcnN8$)T*%uAL2Gh+cX?Vuh$-b2-JRK8 zgyeMglPI~E{rd(=K(qLYLNfH!u!p72k-9vT=3V+4(dudQ`R#8Ey4R#>`Dm$9wCg@K z)O1hHfE#KrPO6KE2h)+FysVOi%cW$3D*E&4LZOo|DEYP5_T|6jbSFkASXxWtgm*3J z*6?L^C)Fxr-xEklFC%=UPTaf`8SGn2&PsO(BLN`1>l1Zq-#mK8`ylS;5YkA1m?-aHNIHowl7g}w|;Te>1J>yQo#Eu7UM%t zs9&nUlF48I1z6Mo1kX-#T^oStb!8ZTv>qxDCxw_O${I0&XuJIV@qE>y?qk>16=UWS zokdpSRecXy5u!(wT4n603nQ+ac%OAD=n`=~V6*J<&WKEl#3*p%*|bKIJOs_>qi~SLCtqdjS^0i}6Ei4`gj1+)`!MXQ)-}G;9>aM z%9FFGK}bKogn6q5M)Da*$?8pDjgVZe0a>}k#usAr;Kx6$jP2Bq;{Wa+@G*l=l*o2{ zNOphv>5Iyn%78+Y2{1`0&t7J4*y9+1tGg+btRUQ(M)Ff~T-f$a`@fgTV?gwfse>oW z;N+5BKj6t&uM?Lr%T{8z;h}8~2M+~!@5d`H1zrjD;n}UFSbQ1)4;4ECzISc#F^yrJ z5Zx#y=R6-dTS@4g$v1;PLRrx`Nce2gf4%D98+nj#d zP_r|cr2c!KnS70QEwIMabpb|-*~to%omx6c%XBVP1xJ9$^z`%4p$u9xMXqtdX}ivf z3cB++B2}c!fG}4`(xRz8vAH%q#x5I0e6ZUd(Qc1Tu>5M!9|RD__0`S zcqB$_)<}5RbTV44>kf89xJh45*3Cv10UK)pV{duOAkw;b9NWw@a0-$C+M8R4$Nj*k zAcNs7Ke;iYn-2L24lCbiJ^Pa$U>q6^FL5tCrI6RFljZB4Ei*}#YAH32fz|vgvUvX@ zEAI<1AO&HQa}s>Z*{<54f^y?a8z2YTrW0+2TuEt=8^5!}6m)Zh{0xPLbFi5yY1KURC8>+R@=mhs9(OA__&_J(6d>`7#I=QZIhhZUj3>BET|0^nYBQGb*>fW`_j zXBI(qCTM%c(E(j5WNAGVC6n8I>+>3n4p-ehEEI-cBPP@jgJ$2vb~S4?h3ayF`IHDF zH^42a{U9qZ9PRvtK>!7+0jP)<6ki~IkTL>j!Au_6*P$&pS)qYyuA1B8290i$fzGsr z{N32-ZKPGRG#dKmiPz3|&A{IZ%7(fniU>__2XIOOIKx0=hnY-zT~d|nAf#z4NinqlXlzh9I;Y@r#l*k7L_zX$X`)VSA?;$9?gC+TOzK;L&@41MzsrU$RnX|$^D^RGR&X!c0T@<O&6jRz)4%j)rf}c5+#`%DiCv|SF#y80c zCytX6p7a0@6!HLM^-kOX001*~*E2TS`tzx>pInTMx`yw>fB+PW$3_WmR0Mk(ihu@J zB3w&=^FROq2j}+RmdRnGb&9fGdjv)5;EqEDV009QGAOu_bTsOc2 fd5qK9DT$T9aaSHAq$Ju5>6wZ8PY8@a000001Dthr diff --git a/docs/gallery/bellows/index.html b/docs/gallery/bellows/index.html deleted file mode 100644 index 6b2735c..0000000 --- a/docs/gallery/bellows/index.html +++ /dev/null @@ -1,1229 +0,0 @@ - - - - - - bellows — Examples — Blender Developer Tools - - - - - - - - - - - - - - - - - -

-
-

bellows

-

A procedural blacksmith bellows through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

-
-
- -

Rendered headless by the example itself — click to zoom.

-
witnesses Recomputed: 1272 tris, three materials with 294 leather and 48 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.630×0.403×0.277 m, LOD ratios in band (5.2 COLLAPSE 2 tris leaner on LOD2), convex collider 148 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.
-
-
blender --background --python showcase/bellows/bellows.py --
- -
-
-

A showcase piece, not an example. Procedural blacksmith bellows (hinged timber paddles, leather bag, iron nozzle) then the shipped pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain, convex collider, Unity glTF export.

-

It asserts budget conformance of the generated result. It does not witness an API contract. "It rendered without error" is not a check.

-

Composes skills mesh-editing-and-bmesh, bake-high-to-low, depsgraph-and-evaluated-data, engine-export-presets, and snippets bake_normal_high_to_low.py, setup_bake_target_image.py, lod_chain.py / decimate_to_budget.py, convex_hull_collider.py, export_preset_unity.py (helpers copied, not imported as a package).

-

Budgets

-

Declared as named constants; every gate recomputes from the mesh, materials, UVs, evaluated LOD, collider, or export file.

-

| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) | | --- | --- | --- | | Base triangles | 1180–1380 | 1272 / 1272 / 1272 | | LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 | | LOD2 ratio | 0.10–0.35 of base | 0.2186 / 0.2186 / 0.2170 | | Materials | exactly 3 distinct, ≥24 leather and metal faces | 3 slots, 294 leather, 48 metal | | UVs | in 0..1, AABB overlap ≤ 1e-5 | in range, overlap 0 | | Outer AABB | (0.630, 0.403, 0.277) m ± 0.01 | (0.6303, 0.4031, 0.2765), zmin 0 | | Collider tris | ≤ 180 | 148 | | Export | written, size > 0 | 105796 / 105796 / 105788 bytes |

-

DECIMATE COLLAPSE triangle counts are not identical across series — 5.2.1 is 2 tris leaner on LOD2. The gate is a ratio band, not an exact count. Bake pixels are stochastic; the gate is has_data plus operator FINISHED, not byte-identity. Construction uses no RNG. Export byte counts differ by 8 B on 5.2.1 (glTF serializer), not a gated axis.

-

--skip-decimate skips the LOD DECIMATE stage so LOD1 ratio is 1.0 and exit 9 fires. That is the named budget the falsifier violates.

-

Run

-
blender --background --python bellows.py --
-blender --background --python bellows.py -- --skip-decimate
-blender --background --python bellows.py -- --output bellows.png
-

Smoke does not pass --output or --skip-decimate.

-

Exit codes

-

File-local. 9 is a valid check code. 10 is reserved for gallery_framing.check_framing on the --output path.

-

| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Uncaught exception (FATAL wrapper) | | 2 | argparse / usage | | 3 | Mesh did not build / no UV layer | | 4 | Base triangle count outside range | | 5 | Material count ≠ 3 distinct slots, or wood/leather/metal faces missing | | 6 | UVs outside 0..1 | | 7 | UV AABB overlap above tolerance | | 8 | World AABB off declared outer size | | 9 | LOD ratio band (--skip-decimate lands here) | | 10 | Framing gate (render path only) | | 11 | Collider triangle count above ceiling | | 12 | Bake did not finish or image has no data | | 13 | Export file missing or empty | | 14 | --output produced no file |

-
-
-

Source

-
- showcase/bellows/bellows.py - View on GitHub → -
-
"""Game-ready blacksmith bellows — a showcase piece, not an example.
-
-Asserts budget conformance of a procedural bellows (hinged timber
-paddles, leather accordion, iron nozzle) after composing shipped
-pipeline pieces: bmesh construction, UVs, three materials, high-to-low
-normal bake, LOD chain, convex collider, Unity glTF export.
-
-Budgets are declared below and recomputed from the generated result.
-They are not API-contract witnesses. ``--skip-decimate`` skips the LOD
-DECIMATE stage so the LOD-ratio budget fails.
-
-No RNG. Construction is closed-form. DECIMATE COLLAPSE triangle counts
-are not byte-identical across Blender versions — the LOD gate is a
-ratio band, not an exact count.
-
-    blender --background --python bellows.py --
-    blender --background --python bellows.py -- --skip-decimate
-    blender --background --python bellows.py -- --output bellows.png
-"""
-import argparse
-import math
-import os
-import sys
-import tempfile
-import traceback
-
-import bmesh
-import bpy
-from mathutils import Euler, Vector
-
-_REPO = os.path.abspath(
-    os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir)
-)
-sys.path.insert(0, os.path.join(_REPO, "examples"))
-sys.dont_write_bytecode = True
-import gallery_framing  # noqa: E402
-
-BOARD_L = 0.52
-BOARD_W = 0.30
-BOARD_T = 0.034
-OPEN = 0.26
-N_FOLDS = 5
-NOZZLE_L = 0.10
-NOZZLE_R = 0.018
-
-BBOX_TOL = 0.01
-OUTER_SIZE = (0.630, 0.403, 0.277)
-BASE_TRIS_MIN = 1180
-BASE_TRIS_MAX = 1380
-LOD1_RATIO_MIN = 0.32
-LOD1_RATIO_MAX = 0.62
-LOD2_RATIO_MIN = 0.10
-LOD2_RATIO_MAX = 0.35
-LOD1_TARGET = 0.50
-LOD2_TARGET = 0.22
-MATERIAL_COUNT = 3
-UV_EPS = 1e-4
-UV_OVERLAP_MAX = 1e-5
-COLLIDER_TRIS_MAX = 180
-BAKE_RES = 256
-CAGE_EXTRUSION = 0.08
-METAL_FACES_MIN = 24
-WOOD_FACES_MIN = 12
-LEATHER_FACES_MIN = 24
-
-WOOD_IDX = 0
-LEATHER_IDX = 1
-METAL_IDX = 2
-
-
-def eevee_engine_id():
-    return "BLENDER_EEVEE" if bpy.app.version >= (5, 0, 0) else "BLENDER_EEVEE_NEXT"
-
-
-def fail(msg, code):
-    print(f"ERROR: {msg}", file=sys.stderr)
-    return code
-
-
-def triangle_count(mesh):
-    mesh.calc_loop_triangles()
-    return len(mesh.loop_triangles)
-
-
-def evaluated_triangle_count(obj):
-    depsgraph = bpy.context.evaluated_depsgraph_get()
-    eval_obj = obj.evaluated_get(depsgraph)
-    eval_mesh = eval_obj.to_mesh()
-    try:
-        eval_mesh.calc_loop_triangles()
-        return len(eval_mesh.loop_triangles)
-    finally:
-        eval_obj.to_mesh_clear()
-
-
-def deselect_all():
-    for ob in list(bpy.context.view_layer.objects):
-        if ob is None:
-            continue
-        ob.select_set(False)
-
-
-def add_box(bm, loc, scale, mat_idx, euler=(0.0, 0.0, 0.0)):
-    geo = bmesh.ops.create_cube(bm, size=1.0)
-    verts = geo["verts"]
-    rot = Euler(euler).to_matrix()
-    origin = Vector(loc)
-    for v in verts:
-        p = Vector((v.co.x * scale[0], v.co.y * scale[1], v.co.z * scale[2]))
-        v.co = rot @ p + origin
-    faces = {f for v in verts for f in v.link_faces}
-    for f in faces:
-        f.material_index = mat_idx
-    return verts
-
-
-def add_oriented_box(bm, a, b, scale_xy, mat_idx):
-    a = Vector(a)
-    b = Vector(b)
-    delta = b - a
-    length = delta.length
-    if length < 1e-8:
-        return []
-    quat = Vector((0.0, 0.0, 1.0)).rotation_difference(delta.normalized())
-    eul = quat.to_euler("XYZ")
-    return add_box(
-        bm,
-        ((a + b) * 0.5),
-        (scale_xy[0], scale_xy[1], length),
-        mat_idx,
-        euler=(eul.x, eul.y, eul.z),
-    )
-
-
-def add_cyl(bm, loc, radius, depth, segments, mat_idx, euler=(0.0, 0.0, 0.0)):
-    geo = bmesh.ops.create_cone(
-        bm,
-        cap_ends=True,
-        cap_tris=False,
-        segments=segments,
-        radius1=radius,
-        radius2=radius,
-        depth=depth,
-    )
-    verts = geo["verts"]
-    rot = Euler(euler).to_matrix()
-    origin = Vector(loc)
-    for v in verts:
-        v.co = rot @ v.co + origin
-    faces = {f for v in verts for f in v.link_faces}
-    for f in faces:
-        f.material_index = mat_idx
-    return verts
-
-
-def _quad(bm, verts, mat_idx):
-    try:
-        face = bm.faces.new(verts)
-    except ValueError:
-        return None
-    face.material_index = mat_idx
-    return face
-
-
-def add_leather_panel(
-    bm, a0, a1, b0, b1, n_u, n_v, folds, bulge, thickness, outward, mat_idx
-):
-    a0 = Vector(a0)
-    a1 = Vector(a1)
-    b0 = Vector(b0)
-    b1 = Vector(b1)
-    outward = Vector(outward)
-    if outward.length < 1e-8:
-        return []
-    outward = outward.normalized()
-    grid_o = []
-    grid_i = []
-    collected = []
-    for iv in range(n_v + 1):
-        tv = iv / n_v
-        row_o = []
-        row_i = []
-        for iu in range(n_u + 1):
-            tu = iu / n_u
-            p = a0.lerp(a1, tu).lerp(b0.lerp(b1, tu), tv)
-            fold = abs(math.sin(tu * folds * math.pi))
-            puff = math.sin(tv * math.pi)
-            p = p + outward * (0.003 + bulge * fold * puff)
-            vo = bm.verts.new(p + outward * (thickness * 0.5))
-            vi = bm.verts.new(p - outward * (thickness * 0.5))
-            row_o.append(vo)
-            row_i.append(vi)
-            collected.extend((vo, vi))
-        grid_o.append(row_o)
-        grid_i.append(row_i)
-    bm.verts.ensure_lookup_table()
-    for iv in range(n_v):
-        for iu in range(n_u):
-            _quad(
-                bm,
-                (
-                    grid_o[iv][iu],
-                    grid_o[iv][iu + 1],
-                    grid_o[iv + 1][iu + 1],
-                    grid_o[iv + 1][iu],
-                ),
-                mat_idx,
-            )
-            _quad(
-                bm,
-                (
-                    grid_i[iv][iu + 1],
-                    grid_i[iv][iu],
-                    grid_i[iv + 1][iu],
-                    grid_i[iv + 1][iu + 1],
-                ),
-                mat_idx,
-            )
-    for iu in range(n_u):
-        _quad(
-            bm,
-            (grid_o[0][iu], grid_i[0][iu], grid_i[0][iu + 1], grid_o[0][iu + 1]),
-            mat_idx,
-        )
-        _quad(
-            bm,
-            (
-                grid_o[n_v][iu + 1],
-                grid_i[n_v][iu + 1],
-                grid_i[n_v][iu],
-                grid_o[n_v][iu],
-            ),
-            mat_idx,
-        )
-    for iv in range(n_v):
-        _quad(
-            bm,
-            (grid_o[iv][0], grid_o[iv + 1][0], grid_i[iv + 1][0], grid_i[iv][0]),
-            mat_idx,
-        )
-        _quad(
-            bm,
-            (
-                grid_o[iv][n_u],
-                grid_i[iv][n_u],
-                grid_i[iv + 1][n_u],
-                grid_o[iv + 1][n_u],
-            ),
-            mat_idx,
-        )
-    return collected
-
-
-def add_cone(bm, loc, radius1, radius2, depth, segments, mat_idx, euler=(0.0, 0.0, 0.0)):
-    geo = bmesh.ops.create_cone(
-        bm,
-        cap_ends=True,
-        cap_tris=False,
-        segments=segments,
-        radius1=radius1,
-        radius2=radius2,
-        depth=depth,
-    )
-    verts = geo["verts"]
-    rot = Euler(euler).to_matrix()
-    origin = Vector(loc)
-    for v in verts:
-        v.co = rot @ v.co + origin
-    faces = {f for v in verts for f in v.link_faces}
-    for f in faces:
-        f.material_index = mat_idx
-    return verts
-
-
-def pack_uvs(bm, margin=0.08):
-    uv = bm.loops.layers.uv.new("UVMap")
-    faces = list(bm.faces)
-    n = len(faces)
-    cols = max(1, math.ceil(math.sqrt(n)))
-    rows = max(1, math.ceil(n / cols))
-    cell_w = 1.0 / cols
-    cell_h = 1.0 / rows
-    pad_u = margin * cell_w * 0.5
-    pad_v = margin * cell_h * 0.5
-    usable_w = cell_w - 2.0 * pad_u
-    usable_h = cell_h - 2.0 * pad_v
-    for i, face in enumerate(faces):
-        col = i % cols
-        row = i // cols
-        nrm = face.normal
-        ax = abs(nrm.x)
-        ay = abs(nrm.y)
-        az = abs(nrm.z)
-        coords = []
-        for loop in face.loops:
-            co = loop.vert.co
-            if az >= ax and az >= ay:
-                coords.append((co.x, co.y))
-            elif ax >= ay:
-                coords.append((co.y, co.z))
-            else:
-                coords.append((co.x, co.z))
-        xs = [c[0] for c in coords]
-        ys = [c[1] for c in coords]
-        minx, maxx = min(xs), max(xs)
-        miny, maxy = min(ys), max(ys)
-        dx = max(maxx - minx, 1e-8)
-        dy = max(maxy - miny, 1e-8)
-        origin_u = col * cell_w + pad_u
-        origin_v = row * cell_h + pad_v
-        for loop, (x, y) in zip(face.loops, coords):
-            loop[uv].uv = (
-                origin_u + (x - minx) / dx * usable_w,
-                origin_v + (y - miny) / dy * usable_h,
-            )
-
-
-def build_bellows_mesh(name, bevel_offset, bevel_segments):
-    bm = bmesh.new()
-    try:
-        wood = []
-        leather = []
-        metal = []
-        z_bot = BOARD_T
-        open_ang = math.atan2(OPEN, BOARD_L)
-        hinge = Vector((BOARD_L / 2.0 - 0.018, 0.0, z_bot + 0.006))
-
-        wood.extend(
-            add_box(
-                bm,
-                (0.0, 0.0, BOARD_T / 2.0),
-                (BOARD_L, BOARD_W, BOARD_T),
-                WOOD_IDX,
-            )
-        )
-
-        rot = Euler((0.0, -open_ang, 0.0)).to_matrix()
-        local_center = Vector((-BOARD_L / 2.0 + 0.018, 0.0, BOARD_T / 2.0))
-        top_c = hinge + rot @ local_center
-        wood.extend(
-            add_box(
-                bm,
-                (top_c.x, top_c.y, top_c.z),
-                (BOARD_L, BOARD_W, BOARD_T),
-                WOOD_IDX,
-                euler=(0.0, -open_ang, 0.0),
-            )
-        )
-        for y in (-0.065, 0.065):
-            p0 = hinge + rot @ Vector((-BOARD_L / 2.0 + 0.055, y, BOARD_T))
-            p1 = hinge + rot @ Vector((-BOARD_L / 2.0 + 0.055, y, BOARD_T + 0.072))
-            wood.extend(add_oriented_box(bm, p0, p1, (0.022, 0.022), WOOD_IDX))
-        g0 = hinge + rot @ Vector((-BOARD_L / 2.0 + 0.055, -0.085, BOARD_T + 0.072))
-        g1 = hinge + rot @ Vector((-BOARD_L / 2.0 + 0.055, 0.085, BOARD_T + 0.072))
-        wood.extend(add_oriented_box(bm, g0, g1, (0.018, 0.018), WOOD_IDX))
-
-        if bevel_offset > 0.0:
-            edges = list({e for v in wood for e in v.link_edges})
-            ret = bmesh.ops.bevel(
-                bm,
-                geom=edges,
-                offset=bevel_offset,
-                segments=bevel_segments,
-                profile=0.5,
-                affect='EDGES',
-                clamp_overlap=True,
-            )
-            for f in ret.get('faces') or []:
-                f.material_index = WOOD_IDX
-
-        hw = BOARD_W / 2.0 - 0.003
-        hx0 = -BOARD_L / 2.0 + 0.010
-        hx1 = BOARD_L / 2.0 - 0.036
-
-        def top_under(lx, ly):
-            return hinge + rot @ (local_center + Vector((lx, ly, -BOARD_T / 2.0)))
-
-        for side in (-1.0, 1.0):
-            y = side * hw
-            leather.extend(
-                add_leather_panel(
-                    bm,
-                    Vector((hx0, y, z_bot)),
-                    Vector((hx1, y, z_bot)),
-                    top_under(-BOARD_L / 2.0, y),
-                    top_under(BOARD_L / 2.0 - 0.028, y),
-                    8,
-                    5,
-                    N_FOLDS,
-                    0.050,
-                    0.008,
-                    Vector((0.0, side, 0.0)),
-                    LEATHER_IDX,
-                )
-            )
-        leather.extend(
-            add_leather_panel(
-                bm,
-                Vector((hx0, -hw, z_bot)),
-                Vector((hx0, hw, z_bot)),
-                top_under(-BOARD_L / 2.0, -hw),
-                top_under(-BOARD_L / 2.0, hw),
-                6,
-                5,
-                N_FOLDS,
-                0.022,
-                0.008,
-                Vector((-1.0, 0.0, 0.0)),
-                LEATHER_IDX,
-            )
-        )
-
-        nz = z_bot + 0.028
-        metal.extend(
-            add_cyl(
-                bm,
-                (BOARD_L / 2.0 + NOZZLE_L / 2.0 - 0.01, 0.0, nz),
-                NOZZLE_R,
-                NOZZLE_L,
-                12,
-                METAL_IDX,
-                euler=(0.0, math.pi / 2.0, 0.0),
-            )
-        )
-        metal.extend(
-            add_cyl(
-                bm,
-                (BOARD_L / 2.0 + 0.006, 0.0, nz),
-                0.030,
-                0.018,
-                12,
-                METAL_IDX,
-                euler=(0.0, math.pi / 2.0, 0.0),
-            )
-        )
-        metal.extend(
-            add_cone(
-                bm,
-                (BOARD_L / 2.0 + NOZZLE_L - 0.012, 0.0, nz),
-                NOZZLE_R,
-                0.013,
-                0.042,
-                12,
-                METAL_IDX,
-                euler=(0.0, math.pi / 2.0, 0.0),
-            )
-        )
-        metal.extend(
-            add_box(
-                bm,
-                (BOARD_L / 2.0 - 0.04, 0.0, hinge.z),
-                (0.018, 0.070, 0.018),
-                METAL_IDX,
-            )
-        )
-
-        xs = [v.co.x for v in bm.verts]
-        ys = [v.co.y for v in bm.verts]
-        zs = [v.co.z for v in bm.verts]
-        rcx = 0.5 * (min(xs) + max(xs))
-        rcy = 0.5 * (min(ys) + max(ys))
-        zmin = min(zs)
-        for v in bm.verts:
-            v.co.x -= rcx
-            v.co.y -= rcy
-            v.co.z -= zmin
-            if v.co.z < 0.0:
-                v.co.z = 0.0
-
-        pack_uvs(bm)
-        bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
-        for face in bm.faces:
-            face.smooth = False
-        me = bpy.data.meshes.new(name)
-        bm.to_mesh(me)
-        me.update()
-        for poly in me.polygons:
-            poly.use_smooth = False
-    finally:
-        bm.free()
-    out = bpy.data.objects.new(name, me)
-    bpy.context.collection.objects.link(out)
-    return out
-
-
-def principled(name, color, metallic, roughness):
-    mat = bpy.data.materials.new(name)
-    mat.use_nodes = True
-    bsdf = mat.node_tree.nodes["Principled BSDF"]
-    bsdf.inputs["Base Color"].default_value = color
-    bsdf.inputs["Metallic"].default_value = metallic
-    bsdf.inputs["Roughness"].default_value = roughness
-    return mat
-
-
-def assign_slots(obj, wood, leather, metal):
-    mats = obj.data.materials
-    wanted = (wood, leather, metal)
-    for i, mat in enumerate(wanted):
-        if i < len(mats):
-            mats[i] = mat
-        else:
-            mats.append(mat)
-
-
-def world_bbox(obj):
-    corners = [obj.matrix_world @ Vector(c) for c in obj.bound_box]
-    xs = [c.x for c in corners]
-    ys = [c.y for c in corners]
-    zs = [c.z for c in corners]
-    return (min(xs), min(ys), min(zs), max(xs), max(ys), max(zs))
-
-
-def uv_stats(mesh):
-    uv = mesh.uv_layers.active
-    if uv is None:
-        return 0.0, 0.0, 1.0, 1.0, 0, 1.0
-    data = uv.data
-    us = [loop.uv[0] for loop in data]
-    vs = [loop.uv[1] for loop in data]
-    aabbs = []
-    for poly in mesh.polygons:
-        pu = [data[i].uv[0] for i in poly.loop_indices]
-        pv = [data[i].uv[1] for i in poly.loop_indices]
-        aabbs.append((min(pu), min(pv), max(pu), max(pv)))
-    overlap = 0.0
-    for i in range(len(aabbs)):
-        a = aabbs[i]
-        for j in range(i + 1, len(aabbs)):
-            b = aabbs[j]
-            x0 = max(a[0], b[0])
-            y0 = max(a[1], b[1])
-            x1 = min(a[2], b[2])
-            y1 = min(a[3], b[3])
-            overlap += max(0.0, x1 - x0) * max(0.0, y1 - y0)
-    return min(us), min(vs), max(us), max(vs), overlap, len(aabbs)
-
-
-def make_lod(obj, name, ratio, skip_decimate):
-    mesh = obj.data.copy()
-    lod = bpy.data.objects.new(name, mesh)
-    lod.matrix_world = obj.matrix_world.copy()
-    bpy.context.scene.collection.objects.link(lod)
-    if not skip_decimate and 0.0 < ratio < 1.0:
-        mod = lod.modifiers.new("DecimateBudget", "DECIMATE")
-        mod.decimate_type = "COLLAPSE"
-        mod.ratio = ratio
-    return lod
-
-
-def convex_hull_collider(obj, name):
-    mesh = bpy.data.meshes.new(name)
-    bm = bmesh.new()
-    try:
-        bm.from_mesh(obj.data)
-        result = bmesh.ops.convex_hull(bm, input=list(bm.verts))
-        interior = result.get("geom_interior") or []
-        unused = result.get("geom_unused") or []
-        if interior:
-            bmesh.ops.delete(bm, geom=interior, context="VERTS")
-        if unused:
-            bmesh.ops.delete(bm, geom=unused, context="VERTS")
-        bm.to_mesh(mesh)
-        mesh.update()
-    finally:
-        bm.free()
-    collider = bpy.data.objects.new(name, mesh)
-    bpy.context.collection.objects.link(collider)
-    collider.matrix_world = obj.matrix_world.copy()
-    return collider
-
-
-def setup_bake_image(obj, target_mat, size=BAKE_RES):
-    if not obj.data.uv_layers:
-        return None, None
-    img = bpy.data.images.new("BellowsNrm", size, size, alpha=True, float_buffer=False)
-    img.colorspace_settings.name = "Non-Color"
-    nodes = target_mat.node_tree.nodes
-    tex = nodes.new("ShaderNodeTexImage")
-    tex.image = img
-    nodes.active = tex
-    tex.select = True
-    obj.active_material_index = WOOD_IDX
-    return img, tex
-
-
-def bake_normal(high, low):
-    scene = bpy.context.scene
-    scene.render.engine = "CYCLES"
-    scene.cycles.device = "CPU"
-    scene.cycles.samples = 1
-    scene.cycles.use_denoising = False
-    deselect_all()
-    high.select_set(True)
-    low.select_set(True)
-    bpy.context.view_layer.objects.active = low
-    return bpy.ops.object.bake(
-        type="NORMAL",
-        use_selected_to_active=True,
-        cage_extrusion=CAGE_EXTRUSION,
-        use_cage=False,
-        normal_space="TANGENT",
-        margin=4,
-        margin_type="ADJACENT_FACES",
-        use_clear=True,
-        target="IMAGE_TEXTURES",
-    )
-
-
-def export_unity(path, objects):
-    deselect_all()
-    for ob in objects:
-        ob.select_set(True)
-    bpy.context.view_layer.objects.active = objects[0]
-    bpy.ops.export_scene.gltf(
-        filepath=path,
-        use_selection=True,
-        export_yup=True,
-        export_apply=True,
-        export_draco_mesh_compression_enable=False,
-        export_animations=False,
-    )
-
-
-def check(skip_decimate):
-    bpy.ops.wm.read_factory_settings(use_empty=True)
-    low = build_bellows_mesh("BellowsLow", bevel_offset=0.004, bevel_segments=2)
-    high = build_bellows_mesh("BellowsHigh", bevel_offset=0.004, bevel_segments=4)
-    wood = principled("BellowsWood", (0.40, 0.22, 0.08, 1.0), 0.0, 0.58)
-    leather = principled("BellowsLeather", (0.45, 0.16, 0.08, 1.0), 0.0, 0.78)
-    metal = principled("BellowsIron", (0.16, 0.155, 0.15, 1.0), 1.0, 0.35)
-    assign_slots(low, wood, leather, metal)
-    assign_slots(high, wood, leather, metal)
-
-    if low.data is None or len(low.data.polygons) < 6:
-        return fail("bellows mesh did not build", 3), None, None, None, None, None
-
-    base_tris = triangle_count(low.data)
-    mats = [s for s in low.data.materials if s is not None]
-    nmat = len(mats)
-    distinct_mats = len({id(s) for s in mats})
-    idx_counts = {}
-    for poly in low.data.polygons:
-        idx_counts[poly.material_index] = idx_counts.get(poly.material_index, 0) + 1
-    print(f"measured mat_index_counts={idx_counts}")
-    u0, v0, u1, v1, overlap, nfaces = uv_stats(low.data)
-    bb = world_bbox(low)
-    size_x = bb[3] - bb[0]
-    size_y = bb[4] - bb[1]
-    size_z = bb[5] - bb[2]
-
-    img, tex = setup_bake_image(low, wood)
-    if img is None:
-        return fail("bellows has no UV layer", 3), None, None, None, None, None
-    bake_result = bake_normal(high, low)
-
-    lod1 = make_lod(low, "BellowsLOD1", LOD1_TARGET, skip_decimate)
-    lod2 = make_lod(low, "BellowsLOD2", LOD2_TARGET, skip_decimate)
-    bpy.context.view_layer.update()
-    lod1_tris = evaluated_triangle_count(lod1)
-    lod2_tris = evaluated_triangle_count(lod2)
-    r1 = lod1_tris / base_tris if base_tris else 0.0
-    r2 = lod2_tris / base_tris if base_tris else 0.0
-
-    collider_src = build_bellows_mesh(
-        "BellowsColSrc", bevel_offset=0.0, bevel_segments=1
-    )
-    collider = convex_hull_collider(collider_src, "BellowsCollider")
-    bpy.data.objects.remove(collider_src, do_unlink=True)
-    col_tris = triangle_count(collider.data)
-
-    export_path = os.path.join(
-        tempfile.gettempdir(),
-        f"bdt_bellows_{os.getpid()}.glb",
-    )
-    if os.path.exists(export_path):
-        os.remove(export_path)
-    export_unity(export_path, [low, collider])
-    export_size = os.path.getsize(export_path) if os.path.isfile(export_path) else 0
-
-    print(f"blender={tuple(bpy.app.version)} skip_decimate={skip_decimate}")
-    print(
-        f"measured base_tris={base_tris} lod1_tris={lod1_tris} "
-        f"lod2_tris={lod2_tris} r1={r1:.4f} r2={r2:.4f}"
-    )
-    print(
-        f"measured nmat={nmat} uv=({u0:.4f},{v0:.4f})-({u1:.4f},{v1:.4f}) "
-        f"overlap={overlap:.6f} nfaces={nfaces}"
-    )
-    print(
-        f"measured bbox=({size_x:.4f},{size_y:.4f},{size_z:.4f}) "
-        f"outer={OUTER_SIZE} zmin={bb[2]:.4f}"
-    )
-    print(
-        f"measured collider_tris={col_tris} bake={bake_result} "
-        f"bake_has_data={img.has_data} export_bytes={export_size}"
-    )
-
-    if not (BASE_TRIS_MIN <= base_tris <= BASE_TRIS_MAX):
-        return fail(
-            f"base tris {base_tris} not in [{BASE_TRIS_MIN}, {BASE_TRIS_MAX}]",
-            4,
-        ), None, None, None, None, None
-    if nmat != MATERIAL_COUNT or distinct_mats != MATERIAL_COUNT:
-        return fail(
-            f"material slots {nmat} distinct {distinct_mats} != {MATERIAL_COUNT}",
-            5,
-        ), None, None, None, None, None
-    if idx_counts.get(METAL_IDX, 0) < METAL_FACES_MIN:
-        return fail(
-            f"metal faces {idx_counts.get(METAL_IDX, 0)} < {METAL_FACES_MIN}",
-            5,
-        ), None, None, None, None, None
-    if idx_counts.get(LEATHER_IDX, 0) < LEATHER_FACES_MIN:
-        return fail(
-            f"leather faces {idx_counts.get(LEATHER_IDX, 0)} < {LEATHER_FACES_MIN}",
-            5,
-        ), None, None, None, None, None
-    if idx_counts.get(WOOD_IDX, 0) < WOOD_FACES_MIN:
-        return fail(
-            f"wood faces {idx_counts.get(WOOD_IDX, 0)} < {WOOD_FACES_MIN}",
-            5,
-        ), None, None, None, None, None
-    if u0 < -UV_EPS or v0 < -UV_EPS or u1 > 1.0 + UV_EPS or v1 > 1.0 + UV_EPS:
-        return fail(
-            f"UVs outside 0..1: ({u0:.4f},{v0:.4f})-({u1:.4f},{v1:.4f})",
-            6,
-        ), None, None, None, None, None
-    if overlap > UV_OVERLAP_MAX:
-        return fail(
-            f"UV AABB overlap {overlap:.6f} > {UV_OVERLAP_MAX}",
-            7,
-        ), None, None, None, None, None
-    if (
-        abs(size_x - OUTER_SIZE[0]) > BBOX_TOL
-        or abs(size_y - OUTER_SIZE[1]) > BBOX_TOL
-        or abs(size_z - OUTER_SIZE[2]) > BBOX_TOL
-    ):
-        return fail(
-            f"bbox ({size_x:.4f},{size_y:.4f},{size_z:.4f}) "
-            f"off outer {OUTER_SIZE}",
-            8,
-        ), None, None, None, None, None
-    if not (LOD1_RATIO_MIN <= r1 <= LOD1_RATIO_MAX):
-        return fail(
-            f"LOD1 ratio {r1:.4f} not in [{LOD1_RATIO_MIN}, {LOD1_RATIO_MAX}] "
-            "(--skip-decimate is the designed fail)",
-            9,
-        ), None, None, None, None, None
-    if not (LOD2_RATIO_MIN <= r2 <= LOD2_RATIO_MAX):
-        return fail(
-            f"LOD2 ratio {r2:.4f} not in [{LOD2_RATIO_MIN}, {LOD2_RATIO_MAX}]",
-            9,
-        ), None, None, None, None, None
-    if col_tris > COLLIDER_TRIS_MAX:
-        return fail(
-            f"collider tris {col_tris} > {COLLIDER_TRIS_MAX}",
-            11,
-        ), None, None, None, None, None
-    if bake_result != {"FINISHED"} or not img.has_data:
-        return fail(
-            f"bake failed result={bake_result} has_data={img.has_data}",
-            12,
-        ), None, None, None, None, None
-    if export_size <= 0:
-        return fail("export file missing or empty", 13), None, None, None, None, None
-    return 0, low, high, wood, tex, collider
-
-
-def wire_normal(mat, tex):
-    nt = mat.node_tree
-    bsdf = nt.nodes["Principled BSDF"]
-    nrm = nt.nodes.new("ShaderNodeNormalMap")
-    nrm.inputs["Strength"].default_value = 1.0
-    nt.links.new(tex.outputs["Color"], nrm.inputs["Color"])
-    nt.links.new(nrm.outputs["Normal"], bsdf.inputs["Normal"])
-
-
-def render_still(low, wood, tex, path, engine):
-    scene = bpy.context.scene
-    wire_normal(wood, tex)
-    for ob in list(scene.objects):
-        if ob.type == "MESH" and ob != low:
-            ob.hide_render = True
-            ob.hide_viewport = True
-
-    low.rotation_euler.z = math.radians(32.0)
-    low.rotation_euler.x = math.radians(0.0)
-
-    floor_me = bpy.data.meshes.new("Floor")
-    bm = bmesh.new()
-    try:
-        bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=14.0)
-        bm.to_mesh(floor_me)
-    finally:
-        bm.free()
-    fmat = bpy.data.materials.new("Floor")
-    fmat.use_nodes = True
-    fb = fmat.node_tree.nodes["Principled BSDF"]
-    fb.inputs["Base Color"].default_value = (0.03, 0.032, 0.037, 1.0)
-    fb.inputs["Roughness"].default_value = 0.7
-    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, 8.5, 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.02, 0.021, 0.025, 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)
-
-    light("Key", (-3.6, -5.0, 5.4), 660.0, 4.0, (1.0, 0.94, 0.86), (50, 0, -36))
-    light("Fill", (5.0, -3.4, 2.4), 46.0, 8.0, (0.72, 0.82, 1.0), (62, 0, 50))
-    light("Wedge", (2.2, 4.0, 3.8), 600.0, 5.5, (1.0, 0.70, 0.40), (-70, 0, 198))
-
-    cam_data = bpy.data.cameras.new("Cam")
-    cam_data.lens = 50.0
-    cam = bpy.data.objects.new("Cam", cam_data)
-    cam.location = (-0.691, -0.861, 0.231)
-    scene.collection.objects.link(cam)
-    aim = bpy.data.objects.new("Aim", None)
-    aim.location = (-0.04, 0.0, 0.11)
-    scene.collection.objects.link(aim)
-    con = cam.constraints.new("TRACK_TO")
-    con.target = aim
-    con.track_axis = "TRACK_NEGATIVE_Z"
-    con.up_axis = "UP_Y"
-    scene.camera = cam
-
-    scene.render.engine = "CYCLES" if engine == "cycles" else eevee_engine_id()
-    if engine == "cycles":
-        scene.cycles.samples = 32
-        scene.cycles.device = "CPU"
-    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 = (
-        "WEBP" if path.lower().endswith(".webp") else "PNG"
-    )
-    if path.lower().endswith(".webp"):
-        scene.render.image_settings.quality = 90
-    scene.render.filepath = path
-    scene.view_settings.view_transform = "Standard"
-
-    fcode = gallery_framing.check_framing(
-        scene, cam, hero=[low], elements=[low], stage=[floor, wall],
-    )
-    if fcode:
-        return fcode
-    bpy.ops.render.render(write_still=True)
-    if not (os.path.exists(path) and os.path.getsize(path) > 0):
-        return fail("render produced no file", 14)
-    return 0
-
-
-def main():
-    argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
-    p = argparse.ArgumentParser()
-    p.add_argument("--output", default=None)
-    p.add_argument("--engine", default="eevee", choices=("eevee", "cycles"))
-    p.add_argument(
-        "--skip-decimate",
-        action="store_true",
-        help="falsification: skip the LOD DECIMATE stage",
-    )
-    args = p.parse_args(argv)
-
-    code, low, _high, hero_mat, tex, _col = check(args.skip_decimate)
-    if code:
-        return code
-    if args.output:
-        rcode = render_still(low, hero_mat, tex, os.path.abspath(args.output), args.engine)
-        if rcode:
-            return rcode
-        print(f"rendered still {args.output}")
-    print("bellows OK")
-    return 0
-
-
-if __name__ == "__main__":
-    try:
-        sys.exit(main())
-    except Exception as e:
-        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 846bcea..f813ed5 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -272,7 +272,7 @@

Examples Gallery

autocomplete="off" spellcheck="false" aria-label="Search examples" /> - 72 examples + 71 examples
@@ -1041,12 +1041,12 @@

anvil

- water-trough — A procedural staved water trough with water through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract. + water-trough — A procedural watertight water trough through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

water-trough

-

A procedural staved water trough with water through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

-

witnesses Recomputed: 4776 tris, three materials with 168 metal and 60 water faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.162×0.528×0.488 m, LOD ratios in band, convex collider 128 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.

+

A procedural watertight water trough through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

+

witnesses Recomputed: 2484 tris, three materials with 192 metal and 6 water faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.152×0.568×0.509 m, LOD ratios in band (5.2 COLLAPSE 8 tris leaner on LOD2), convex collider 32 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.

View example
@@ -1094,17 +1094,6 @@

signpost

View example
-
- - bellows — A procedural blacksmith bellows through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract. - -
-

bellows

-

A procedural blacksmith bellows through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.

-

witnesses Recomputed: 1272 tris, three materials with 294 leather and 48 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.630×0.403×0.277 m, LOD ratios in band (5.2 COLLAPSE 2 tris leaner on LOD2), convex collider 148 tris, non-empty glTF. --skip-decimate exits 9 on the LOD1 ratio budget.

- View example -
-
chopping-block — A procedural chopping block with an embedded axe through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract. diff --git a/docs/gallery/water-trough/index.html b/docs/gallery/water-trough/index.html index 5f9fc4d..a392708 100644 --- a/docs/gallery/water-trough/index.html +++ b/docs/gallery/water-trough/index.html @@ -4,18 +4,18 @@ water-trough — Examples — Blender Developer Tools - + - + - +