From c9026a9561ed603d32a9e4316955982c558aca76 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Mon, 20 Jul 2026 22:12:05 -0400 Subject: [PATCH] feat: add light-link-studio example witnessing receiver collections in pixels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Light linking is the staging feature AI code misses twice: the API lives on the light OBJECT (ld.light_linking is an AttributeError), and the restriction only counts if it survives in pixels. The check renders twice in one pass — linked (4.0x hero/decoy ratio at projected centers) and unlinked in the same run (decoy rises 244% while the hero holds at 0.0% drift) — and documents that EEVEE Next honors linking too (5.5x on 4.5, 5.9x on 5.1), with Cycles pinned for deterministic samples. Signed-off-by: fOuttaMyPaint --- .cursor-plugin/plugin.json | 1 + .github/workflows/blender-smoke.yml | 11 + README.md | 22 +- ROADMAP.md | 2 +- .../assets/light-link-studio-hero.webp | Bin 0 -> 20062 bytes .../light-link-studio-contact-sheet.webp | Bin 0 -> 36792 bytes docs/gallery/index.html | 11 + docs/gallery/light-link-studio/index.html | 545 ++++++++++++++++++ examples/gallery.json | 11 + examples/light-link-studio/README.md | 53 ++ .../light-link-studio/light_link_studio.py | 300 ++++++++++ examples/light-link-studio/preview.webp | Bin 0 -> 13178 bytes 12 files changed, 952 insertions(+), 4 deletions(-) create mode 100644 docs/gallery/assets/light-link-studio-hero.webp create mode 100644 docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp create mode 100644 docs/gallery/light-link-studio/index.html create mode 100644 examples/light-link-studio/README.md create mode 100644 examples/light-link-studio/light_link_studio.py create mode 100644 examples/light-link-studio/preview.webp diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 4b94a74..919a280 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -73,6 +73,7 @@ "examples/gn-sdf-remesh", "examples/grease-pencil-rosette", "examples/image-pixels-testcard", + "examples/light-link-studio", "examples/lod-decimate-chain", "examples/parent-inverse-orrery", "examples/png-exr-alpha", diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml index fc8a357..e4b6a20 100644 --- a/.github/workflows/blender-smoke.yml +++ b/.github/workflows/blender-smoke.yml @@ -437,3 +437,14 @@ jobs: # lerp deviation at mid. Exits non-zero on failure. xvfb-run -a "$BLENDER" --background \ --python examples/vse-gamma-cross/vse_gamma_cross.py -- + + - name: Shipped example - light link studio (receiver collections in pixels) + run: | + set -euo pipefail + # Check only (two tiny Cycles renders): a key linked to the hero + # collection must light only the hero (luminance ratio >= 3x at + # projected centers), and unlinking must raise the decoy >= 50% + # with hero drift <= 5%. Also guards the Object-level API path + # (ld.light_linking is an AttributeError). Exits non-zero on failure. + xvfb-run -a "$BLENDER" --background \ + --python examples/light-link-studio/light_link_studio.py -- diff --git a/README.md b/README.md index e8f805b..144be1d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

- 12 skills  •  6 rules  •  2 templates  •  17 snippets  •  29 examples + 12 skills  •  6 rules  •  2 templates  •  17 snippets  •  30 examples

@@ -36,7 +36,7 @@ ## Overview -This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 29 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support. +This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 30 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support. The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly. @@ -186,7 +186,7 @@ byte images stay straight 8-bit. Also witnesses `EXR color_mode='RGB'` dropping

-Mesh, curves & text — 9 examples +Mesh, curves & text — 10 examples @@ -306,6 +306,22 @@ ending in `length=` vs `frame_end=`, and `left_handle`/`right_handle`/`duration` the deprecated `frame_final_*`. Asserts closed-form spans, GC wiring and clamping, the consumed-input compositing contract, and a save/reload round-trip. + + + + + diff --git a/ROADMAP.md b/ROADMAP.md index 3fc2a95..3251231 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -101,7 +101,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo - ~~UV-layer authoring witness~~ **SHIPPED** as `examples/uv-layer-grid/` — `create_grid(..., calc_uvs=True)` silent no-op without a pre-existing UV layer; closed-form UV fill + explicit assignment fallback; dual-panel render (flat texel (0,0) vs neon checker) - ~~Image save-format witness~~ **SHIPPED** as `examples/png-exr-alpha/` — float→PNG is RGBA16 and false-unpremultiplies as if associated-alpha (closed-form err 0.98 at RGB 0.02 / a=1/255); OpenEXR preserves float RGBA; byte→PNG is straight RGBA8; `EXR color_mode='RGB'` drops alpha - Attribute domain witness: writing a POINT-domain color attribute and reading it as if it were CORNER (or vice versa) silently shears colors across shared verts — companion to `color-attribute-wheel` -- Light-linking collection witness: object light linking / receiver collections so a key illuminates only the hero (common AI miss when staging multi-light studios) +- ~~Light-linking collection witness~~ **SHIPPED** as `examples/light-link-studio/` — the API is `obj.light_linking` on the light OBJECT (`ld.light_linking` is an AttributeError on both versions); linked ratio 4.0x at projected-center luminance samples, unlink raises the decoy 244–251% with 0.0% hero drift in the same two-render check; EEVEE Next honors linking too (5.5x on 4.5.11, 5.9x on 5.1.2), Cycles pinned for deterministic samples - ~~VSE sequences-to-strips witness~~ **SHIPPED** as `examples/vse-cut-list/` — `.sequences` removed on 5.x (4.5 bridges to `.strips`), `new_effect` end kwarg `frame_end=` (4.5) vs `length=` (5.x), `frame_final_*` deprecated in favor of `left_handle`/`right_handle`/`duration`, TRANSFORM effect type removed, GAMMA_CROSS clamps to the source overlap, effect strips consume inputs only when stacked above them, same-scene scene strips render transparent; save/reload round-trip + tiny-render pixel witness - ~~glTF export round-trip witness~~ **SHIPPED** as `examples/gltf-export-roundtrip/` — `export_yup` bakes `(x,y,z)→(x,z,−y)` into vertex data with no node rotation (probed identical on 4.5.11 and 5.1.2), `export_apply` ships the evaluated mesh (one disk vertex per evaluated loop), TEXCOORD_0 is V-flipped on disk, per-triangle material bindings survive; exporter/importer RNA signatures byte-identical between 4.5.11 and 5.1.2 (guarded against future renames); `Mesh.calc_normals()` removal on 5.x surfaced during authoring - ~~LOD decimate chain witness~~ **SHIPPED** as `examples/lod-decimate-chain/` — Decimate COLLAPSE evaluated through the depsgraph is non-destructive (obj.data keeps closed-form counts), evaluated tris hit `ratio × base` within 5% (measured 0.0–0.44%), silhouette bbox survives within 1e-3 (measured 7.7e-6); a stacked Decimate halves the effective ratio, an aggressive 0.02 ratio collapses the nose tip — both caught failure modes diff --git a/docs/gallery/assets/light-link-studio-hero.webp b/docs/gallery/assets/light-link-studio-hero.webp new file mode 100644 index 0000000000000000000000000000000000000000..a6d632aec2d42ffa461fe9e8d0cecb30a93df3d9 GIT binary patch literal 20062 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 F002ez**X9K literal 0 HcmV?d00001 diff --git a/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp b/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp new file mode 100644 index 0000000000000000000000000000000000000000..99dc69acd524e820b769d0c468735ba52bcc5687 GIT binary patch literal 36792 zcmV)0K+eBXNk&Gnj{pEyMM6+kP&go@j{pFW%>tbPDgX*-0X|VCkVd1TC8#3MX$b%d z31@EqtvULdBd5lFQ^W-0&p&T7uH7G-wr@wf7`++UI_Y2JOZyB}V_&}hG5Qzozs-K_ ze~kCc=)WfZo%nxVzwG#P@!$4*!+LA}_xZ2Q51XIv|7v~PewzQ~{$uz5=pXgF_CwbP z_Z!}a_+R;cqyK6@ZvF7T?s~}odG|B&KmH5;*Z==-k5_-*Ut(X}&rc8Dpa1_bAG%+= z{^#HBfB*J%bhGyV{g0>rHNS1*Kjgn+{}=n;?%y@YmVE>CFZ8_a{h;!9^IzisXnlhJ z8~$njcl)R9pZL#{kIV3`;JnK{?EH89FV6q*|1wf%>QVjg(*F;6wSh_V4;X<2|>1m;U2_|MzbFvj6@l{eFFYOUq8!ov{|3tvgzFwjGGJ z)lc&XzCnsX4utHuiI&-KqzE?x4Z&RLMqU5ZU~1{oQxz7StvgzFwC!oy)3v8-PS%}5 z;!*QJ*!UYC17q!Qvo2Ut3M53??eZK%v8Z|C?6aRFkLPDSH@8mtIyano8Oi{Y zvX5BWymevb3`~Y2&RsmhQK`<9fdrt-k5Yfj74qr6T511ED*el*_Updijx9$ve_zm4 zTpF9WdvRUvs}r8Taueh+G~f#SLhvSS(?Ycl7#I4oOKl zmWeU~Wau$?&Ar%2Vw{0qMQNU=t65+P! zfo!MxJAbto)0n#K1#qSVXt<#Sb$477qpXiMfRmeNV1|=Ixd3n)HQCPo$xM_A@-~tV z@y-0M18@Md>Zu1UzdEQGHU<{~LP9SLW@AZCJj}FInAW7q9TwE6R6TzNmJ6HGTu-}Z z3(0LhJg}hAzf~XbN22W1B?K zjr}Tl4v(I3QRB6uj7*TFBkmNRE-Z0*W%1^MYdH#};?g?P%p}s4%F7Zdt&5{90&fhq zl`&#;qp}Qe`2Wz^U$%(py7pL(^U8m*jQM;bNsl+?9y~?p-~4xl`%(Pf3Ji)ml7p!5 z%xf4yy{KG-PZnCRxO-MOYnY}N2W|D+%iT}@71(|>91yb(Qc3|~0R$qMR@8+H<)$-B z)gqV3a`gkwluZcD1XJf7%ZV8>f~KCCr6_=mw~63U(W1$4e~xXAQiqz?4dQsnq*aEO zedrA;xK!wQ4?&QdzYDuBj!L>)Hi!OfVyVi-^{>;bD};K|cMj0G*#BG#hCVi8@;zLD z(mj2rU+eN!tQeEntAzMXs_pjOloti#e5b?H;P$U>8^ft+CHhkn45^u4<8w9JGgwXZ z|4oY*v}Nj~AXbiRx}#VV7X=EnpA5CEE$K4CgCS=Rh>8XVAk&#qkXrp~(KjGykk~o< z@iv}LxxGT?s?K|WNL_a(&tYD~B2zd7SMJD+NCu6YZk<8>sWr8eChZIP;v#ag(L7xKx|*hS6MqXeM*4$4di`Hm_S%^7p&D{4<1}x# z|BgoQ-b6o1Wo7gI|Ns7KD>=x*UTl2)z#aeB3nl&YQ5AfP(-+hJ+^DENsTnV;Xg-@TP zIq;`zR({)aH9RQD)H~&WrFI~D{y!Di9ap$|-P5i zf>qfp@4jx5BNTerFbiVyHyq97a6IWbS#gwbX5TxYHh~Z$^ZGpV4?0xKv7QW&h(Yj{ z%-|2qmkF;O@@|-RT+Mo6|;ZT}bo&U1F8*@hHzU8O?Cj-i6G!+r& zsZ*yn_aaWeyQ1eD!D$E$-skJE{Af5KW++vdM7C#6!jWgc@KoUi8SbFV=8_*!S&*l6 zM{O$$e|vDSIFhe1`4mUp8@hOZZ!!sc||wjw5@ZPuh`M%0xXGxhuTKf!>33{b2* zJQIqo^jo4cB>?AU!4gV-pX;61__h|6{AM>{Ig~7>N+iUl6`YOpd-#Y)BHWv1Ia&Vo z3*2k^xbh%<68jfPY~cebI@T8SnPEYYv(SaVlTZFZ`PcHvhWxYE)|VM@-K6M41pcis zU2aSh27yq^?DzW@TYS= zl)IFrN{~kBoUBjrm{EF}Qk@Xn+FJo`ZVR<~6AKtZ0o4rwkk>UDgSu(j;Nk%8*{REU z3W#h`>DmJi4xPqtJFjnN$PyLe&vrN@8nv2)yQ1(EQ}TEgCux*i%7mJ&bKwWY>e z%wcN!P9mU?*|D2wBXQ$N>8eK*UPXeYPX z_`$+&_t%gJj+XfV21UB2GF0MbMX)Esih;+em=VlLRt<`uBUCML5z(0C{CqcW7v?ig zoGKoh$+c@@h+)<)(_9w;P@~|((fRLtx-F5otcqnbY`q3iT3_T~=I!qDA~1n%HR zyZo(+<7jb|Tg}mgPivCXep#+j=h^ILwO=nc&3T{kU1|%xV(t9j3_KNDRW5;jg%Jn4>TX1e)BS_C@$<)JJPPlCT zD??JCBvEuZ>yzZ^I$CCX=;HpoDegi-Lrijjx25%T!Y~2nJ?;kiQxU)zM#q;YQXc{- zCNMR{WwjlF+w1RR-_xW(kRuq{n#GNF5ECD1H%0}kiS@Tg0*r_$d_hT zX9!N=vW56VU8_qP4hTvFNzo;a@PxFkO)r~y`DDX>S?g;{jJJEiGe}@2_l~U!UF^?R z=C_x(%R)?ME}8p>bh|s!BoK@aBr%FZ65B$mDF!Hed_ojeap|!N;=Lgc%=HN|=roNY zjzfBZ!arTOd)~O3;=-*{oUWZxWO+XVLntq7*fjfnE?dclisYtbeWB){(A@9;eA)n7qSIBYG6 zDX`S+-!I2{o#C{I>*GAsu&?r;`cL98Cp=ga#HevcNxCuV`trQEs{uH5vW z>@39!vw~rDOK9`4lQ^}d##_04%5Td(ZE10q?q717^K14zkhzg2((03g4PTA&@=yF) z`Pa{xIFG;L)CA|0g`ltWj3t3h*R2fHhk>(WWR3{^C4s}ufL&kk)ulJMr>zcoLr7n* z({9EX_0)`Ui#(C)+gj2O%XcriP5Ec7u9#QCsLsFCnj`j}<*<>pgg_KCdAtiGOBk?q%bI0)^+H>V)<_`D}=6jX7RK@zdk+o4;NcJ1T*ZQbF`x7UdSX zYt&0%p*omVe$4Oq`wN}JESR|1b$27Vb(aU*j|95680*^^8)(`sYaxpA#hQ*=?(-$% zHvj*~GdX>U*V|HuN2W=P^@1$ZsaI7=EgqZ@Vh~Qw1 ziY-Z-1JJ=*REc29HhYTxfl@(vVZe6f227u*+HC!zXk|ZIh&U(=fgMPritb->oAS?F zT3ltjm+Xo=_fCO*AcetR)AcVYlZtk=BK5AZV2ew#P3VJ*DU#ND0tF_~m)}mL%{tsu~w1yn+ zjdxaY@6D`Xytu%# zKBFURvKuPS`G>8M0l_oYl^#gr`yT@8I)8dux%wuTCVl2HwrP5MH*{Ry5U6bd$!6Aa ztK{-m#mY43#S;H^dBE?zCZ`j1;1K1Re&ZIa0zx7RDLbBJq3HD3LWWth`kM<>p|k<= z)95Mo*gO(v&^=Wr@thkc!UJ2}DnTzK^K3ee244XmAG=jJ_-GfZHr0-+p8y|Ugl+L4 zjPm5BF2c(aga?#lymx^gcF$Wn6)bPX9fxvL*VU#R7qNS1fr+B4u)lXhh#+qa)0Wrl zNobQC#O_e2EH&Q8kMbP8ke_g2BL1pcQ&;WZaQ>AYxEm>KLzOL+@i996*L$!HZF%DQ z^Dt}*+=-2Te6#c15=_FW7h>HI%i-Kvid8lHcqaI$a^+n8r9G^V+v-AksIH<&JKfY2 zE!w2$>r2|EA_rEM2q;)GIa$8d>h#EGUZCelVP|?Ys}xSU`r&DlN>7;zvG~@VN8NWX zxlF+gj?OD2NoQ+dhba4Qx#I+Vnnyjn5v7Gdhn!h04L_T|BqyHIR!!94z}reII3uwP z(tYr~b0zv|1(f7~a2(AfNgwly1=sjKU18R~%pc3A1)JnA^8VpyiR;w@= zH2w_5rum;imz8m3L|Ql<{GgH|3l9b+MO*c9`b3AyN(^y1ajc}J$uZU+|NoB@_YOw& z?8Kzi0=^}w-C<3j0_Vys(|Bzx$dTk1p(oiSpt9(g_#K~KGYdtXK6b7=p9q2N-=@PA z(|m-(D^0TvzzCeuaCe6QxDa--p-B` z7uTrk+#(k5vp5POndISt5;L0S>b(5J`o|?!yBJf+++kjSk z_rE{e^SlFr_th^!of_iuwWZ-A-c8a zDB`gtT7$(o8tCk!>3oL<7Ec)1mraED2)x>N1nqFUApQ5zS9Z_gfv%q-Yi@N+|>ZX|m0$>b3(@8@S^?(L6i_cKK(HeOTgsCn(xy za3k%h?OBTdH~E_D$GC|?FPBa5&V6gov{zP#)?J0S6)8!EKKlN<;(M0wIGqG}L^s`} zcC0qk%yDG2lbk$%b36S>OXzqm<*Qqql<5i1T)npccJu~kGK0$yQDK21H zX*-*y;3_W6r#Fr8Rdf4M5FO`K*yjN1R4%!V;qPdi zSHbxJJJU)mY$V#@S-1{x3+lrCZ|MKXIMd8PqQa3o1>|6M^J=g$y(j(;WYdzw-ctBP&m6JS92mIne>M%@CgNlwLO5w-L9%3 z*iZ(e^8`-GK>x)mEbSg^lde=xgdfdJ&Q!L8%G*ivHEo2Oq-`>CwB6t@1b4FGWVq&f z^!M9wW&4SQiIg+Czx!0OiZbWd8dz!lC83jdpA;YswfcN^gce%1mH5-#|MJM>ypW=g z)B^8L$;#-VgAJ*@GgN!Slt^(2->4~=f#75zxV%(}b2+x1s%8Q_{@+Mejf4nUNjXLT zDsWnlc|=Ia8DrAhn+ZRH4(MYt_hRCrn`LodWhH@z#W z-hKNfRsRVq1BHS=C<<*0kQ!qELRMCG^gjePY+zS@klG|KK4!qQM4fxRX0p*;g%5#s z0s9W;E-dlqo9Y0NJyIWl$8u&QzEa#lA}bM}w>3dw04RSxv^M(f43dX_SYo$gEaHoR z;1U4NH}2vE-wop%qx%(5Yz<`x2Wd5%4_?OqND((~3vPoJ?D@Ib68jiNT2#uEZiD#k z{P@K{JCDD99?|Cbf017^@s6i4)6{|)pw#fM%2gTti<3;b0qvyzi&fWQyEN?_Pg z(Wg#6;<_O4HWt=ma`TInCOC6eA?o$RzM+j(LIY$!FCtD98t>s<2wPx z`y$-MK&|<7-!6`&(|RWWtMN^Ttm08uAF@=2-F zUV&?A_*yn{0n9UMB)&X1 z$ANyC%9X3=bC;z1EA$46Kl6}C>3C5bwPUy)&e@5^N642J7>L0!!qas90vfTzSoSG<+KQ^&sp+0?$#V629z;NL`Nd>A?z) z7iBJI>jrSDEqc$_Dh=|{?wCLtj_c1CyY`|ss<8q;a7T3+R!)-)2QOr5e~hleK%cs-wI>hh1K{i`u5J4+lkHvX+!(KfD4NztS+LF-P&0o{&TVr9CpY8IRw=$h?XSXXzt;fhoj^@U zxIPN<4gi?+3pYK8RU4YQW`|i8^K#P9NsCj$SKqTC+;O!$CQ87nO-L4}>F`1kTr5RA zfyfggdQTUQrJ|}cjSK77Aw-cCkF1|*_Sqp!Z)%pp2xClo-FhN8FNpo8sJ*=Mr{`3=nx z9ZJ#iNx{YC={PsL&Qpe6DDI+?J^dSYHD!c@u!28J7m1`I^ocWxP(s3_Ptx}R+&ZoBFg0sc3u_xD+-KW zfU-9EBHXx(@}@%?VI7_dMhsU_1_c#K_&4K&)(|-{|Hxrrkjy&R1zXRr$*6&LS=~PC zKb|oaw0||pDSp_{5nB=Y#Dt*{#|+oi$sR}fIq}HJ#N1ea0X@zkwOzrrNnJ9Y9Y=#M zXxM&wzDzX_euD~3mmX*X-kiXN|Z!s3^bY0HaW5&y^WigpE=2>yK;$=})b(kD$ zm2p5@g6|m{=@IoSZAc+N5BDxM;zFcLiuLbh=xt#aC`0>2DBEtQKql|u>UH;irc2Qv z9sOMy21B~_&tHrCoi!%S;ZWHcRFb<&VAwPaF&^3;07G;+l1n#oQE}4d{fykNA@PlM zZDuEoTvjt@wqL}9AJ`vKJP{r1gC(zBR#i$y_f&OQ%C+`#;= zIcurLcKK@erY4LDacrfc=K}PXUjlS74r~H7h~*olGWd8Jrb*qpYbGla+Nu5u#sP>R zzz#F{q`cpa`BRKwtOD89lfJIueXiJ^6IU0r3Hw&BN z-|Pdxe&&rdti;bcK)3;%teHVR@g)2E@5K{$wzTJFpRGYG+)2p&$d z_8Q|g&foFzq}RF6<;1rYs9h|0uf>I-VtGh|Oga*Q4_dkjBGVyu&~Zb($2M@yB74nA zSaXxT(CG~nWrT-l(1CAiiG+000004_W$gkEf1qdkj{1q5$bD5Uew8Lyrit z;PmOgvAs7{6r0INq>(jb~hHIP+XW!f)v)9meQe z)*W=onPo4V#Yfx@oTdI5kzQstaR?3h$w${R{qkuvc;W01U8CR0m{|8$=)&@LTD4Z= z3*uHc*2+sYqg&9)-};@>-CSFI2?oZ`g|r(85dA8UwCY092yr(G*+w!~5*0ImTe+A2 z0i)mgB(`~m6LDu`o&iw{Bq(!23;UPPUoXkNKGOQI=;B5RJgV}%GM-c`V6 ze$||UFm1h?nS_z~0cd_<;~*%!zz&%7&-Zkk){ycxnPxNrv5@W~tmS-k*%Ua?!7z8h z+#)}FI2d?^ z^ky-dHPZ-Nuhi2putQ+5$W_8z>9%oCJS-k2kNOL{&6SX6wI5xc{^)FM0e0RMK9UvM zCe&iq;{@-tZ+=oUy+uVvz$lMYbJ$`)kbQf%E1rVd@Iak2p=evT)^iy-G>Q)x?0=Yc z9tXx1bhW=O6l7kCikaEJBo*bOEy`)CW1|$kd}G!XM&^zc9Xab#oohdB{n#I!YfDTW z-}eFBMv1EGBRA0ZvYH`s6hZP3TuzmN`;vjihmyzfuffP>rzMJ_!;K5h!CQ%F#=}ub z4xlq76Yvd|nEJLHgEbG4p{a!CP5zK(Y`+$mfo8QbO~Jeu9=$pn^nrfouiTyH^wi#q zPA#h#2j89{Nh1GYAgxIpXx5=^EZFim*d|%$Xb*9Ra@L47cM%>@0@`#&b)H4Qvlrn7 zNN)^6gpX~mmG$vrm#A6b-GW1vTqwB>Z;5sbR=EWpy>P9^9<1KbcTb}26A-7x|`m!@o zg}>!w-wK=>N%#K2Gb7}DK?=~|27K!MD4|o8+a(Fl_l8}{gRnDN7ZP}kjSES@S#O;{@TQyE& zazA+DndBer#=lSi&>#Q+$Yn30crV)Y<~#L6_dbXC7qJaB^Jlk~DV^+L5sn#kkNAxG z-g?EATOU;nE)>zj+;fW>=z~lb`L$mPzU{5LpQ~vSTc(ts`k8%!1%yZ0l-m8?UJUM$ z=rqDuPI-O5;`$P`b(UWIcpuF>gMJnTrU`2sz)v74N$j7EjcCh?Fl^_GA>jG-)7^*0 z&@0st{Rr`t@z_G>T&-pn+>FaU^b3j>h~sqskL3Wcb*_p7N~Pfr#|C+JWGqI7#msp{ zTfnCKK}^e+1TT9>7H02LA+3oe$d$bh&)8@pU4`Db3}`q|z(iCRpCScw+4~XV$ur31 z$Z@U)0>AlQo^Udv6Kr;(%cqLsR2IySnBZ2YJ^@<1ZM#`9a!@CB5<)^pC%Qx7?HE_K zX?@#YR#Ifdul8WshsEbmCih}l%2e1M$Td114Ll;Ow&++PQPd2TfbC;O(=AL_3@)jE zAD+u3QUku~si9kC{`;_4)zb7Qc)E;EZ~^gb@b@xHLh{qHb^vM^MWj2rGBH!sdPmmd zIBKd$q5knxj@SV4LZTx8B z7?ROSPY^$wNWcZtK{d1JSvC|=s!oZH(Zm94D{b%eh>S<1r~N(7d^=ErB1}2(9}J9vuOTNFso>$A(E>}sCgwopV>Apu99CaJ;9C2 zzZ$psT!GruI#zQBvjg62@5Tq;c0*PPnfSsz1_fL^dsUKH>;`FO2bGLAXDB*jI;}C_ zw;nn2h27wv;>1inJaAhC1Bwg8cnpYeMjgPkbmAKd5I*;1b479wX?Q0jrhDM%7X3$8bLJQ%+UFL zD~wKAn)L`F>xH7bn5Uw;EA{FeM^|S-mhlO(%<6QN0zWopDAuqQE13E<(xIX-$2L!y zgF3S_Z~3$oX&(TGO!y8!$BoX#Zq5n7Y3Q{J^Y>2S3c9Y>a=hZVYmjW{L&W>eElP8> zDHA{;OdiUKiV~qIIu!#(_r6kBIE!D*R&ibS<=~43v1)(3RDt!<|Cux-e1sJ*(d|Z+ zy!2;Q^pQQXxc)KLQ|QuHP>vReWpI!bm@n{;5A2aamWed%VPZv&EP!y><*|`Q6V61` zS}Pj$Lg^aMb1xuPpZ#%C;B0-mwX!25gHCyk%E(T)F*59w(k|BbIXP)K+7~nIzWuK{ z3yI>Kaw*+k#)-?$8bX@p+5f&^gM(zj7CbCo=JK9<@?p9Vi+CGag_^y!M$5ww|EnFq z12WMZCQ`$Vpf6UNewpVn%}SjJrUwdAKmY^2S+_zs6ph>7YH_RX00mV5O6|74h5Rq8 zw&WHbM{C|1M!z`bEpb$hp|DT(XL|MhuPSs+nJ@bJU^714$|lF;F}9CZ!{#94dZ$mb zhPM7Jwy~DnL}M||@P4jCQz41(zNSrBEWW=t>RtTc86CFXkO!KQYmFUrhNETv@Y>@Himqd%YGp56w$aCrPl1r2s9wDwJ>+EbS5%{&=i?Zjfg>@|QahOv>63nzEu!?uB^sV|TCZj8f!+&itPG3@hqv*$twn$%tortS-Ah zP2dAfeu(Zl)GGn{`E|Rx+hHs{@x8+UYuKB_p1#6sydfrRjR6E8>8Yn2u(gnVoBLXk z5tI@Af)mVlY$!wge!?x)DturVx;oecsgn)BgI(kspA=oIOVcRr@CWFafuA~E4YHh1 zE?_Po*R?DiOEAF}V1Zpdc;obbetD*dF5CIIMi+_N^@Sjj!o*Sl(~QA9ScaQYb|xxB zFDsT@Qfg_BfQz&yXmWx^VQd_hhU2;xyAbB4+*7)7g%=sE^BC8n*bGfOazJGK={$w|s z6A$RdQB9yUutebZWrg?A2H)aF^|&2XDis~W8g?Y+I#whA5g}I=>H(it8Q}ln&J0HY zt%IkZ=2hyCtLGeu_rWYlHYnLuvgI!weCK5WXy>C;n>FI>^e_8r*`T%uzkTt90@i2P zv%|B?8!y#j_Q_nE3&H2se;Rp68mZ`3$wvG##PlhyuG-?qx$%|dEF&3xuLar~wW$Z7 zO)rt@$9CaP%SDiIPCl=%U-pMtgic;7Ex6BN22vr)RWs3gsx62FGN|wYUEN-!@7VDcQwq6| z7t&hfq7i!8OiE5)W{2`69-rU8lci14SW7Q7+0gH*Eg6#p_mw>W4&aX})Pcf_+X3-_ zFlXFP6QT)hR}yIASh7A@V5yU1z8#DQq;nZb%8g6~Wgel;_P?$<2c_tWeeHWN#&;jg ziq%w4LPHykBc}?XXVO@R@%>70l6-mSZK*7^+O~tlH&N!(7!zE?B=NG%L<-004_d!?DDV zF&vN@V6IfB^$B{ShfFkeU<}wpmn|W5O>vBE8}h6RQ0j*OJOZyNt9SI-sNIyk9}~$I zfW7^={}pK{<-I{?6@&hVJx)C~WZL-E)(>|U*OdxcdlF7?xNMiwBS3GLt34JP=+%QS ztTTeDTlfbH>c$4r?OjHRaA}eY_$uU07B7Dut3+hxLHrFDRSoAv{@PW>L&?ddm$pU^ zjYp&?^K#x<0_|=VoI;dJ-jT|+Nu3Yp>Pt)BLeoz})K@?N0aJ@kd-Rrk&a+sn@6hab zm+$)cU)MZbweHkj27Pt#Y)gO0vsflae=w|k^M1=MnTImseI6Qd7HAnX{I^@97zC1d zBNNM1gL%^+gTPnmGN+~oEp&q~+Cf08{yQQ1?Q`KGSM{R{JHwkAAF<_Y%6%{w5} zugL=PW159`?3@GmDfRxXs_~OL5fK?Uo7|~$uBo^o$>tM34d@;BW9xbhcu_97aq3NxOX9nf z(;~ITRY`GOmHMTg`RyaJ@kB^VA{x5$sHm?X6N8J7ivtx3Gqv~6H(&*9l+bC_Z`(#s z2UMry?uVv@^Rz;rlZA~9@(*Rkqyb(2m;$1W*SO8U=+9eQwK%6YsnPvtiWmH z*O^A@PP9BJ-f++==6h>=I`IXFm4EeuBa?w23i2ctB&|js5KbKKOf0Nr?9S zKzUj8QKH`!^Xp|SC2Dpy0Pgn_EnIFea@>q?pr^Ojd#iy?1z6U&(|{3qxtgUg-#$A6 zYFpRv&A|qQu=oB7+&SdNBT{*h8;K*R#mb^zZmF^u{^v|Kc&f(fUmMx)SC0`^0A+Q2magL?T|#JbVQ#@dr^e2AO!X?VolR@$hG zo^b~GhptVCBMAXdc5~{RR^rz1#oT0BI(JOO{+Jdb{)58G7y&HY`~eMXMSNu5PuM~| z-HBu3a+--c2T(7XQ8Vc;w(IBk|(+|-P+Z?3qve<3l+`j@t z@09>jLmP%iBE)7H#zjxQp|2+{bT$#%#yTDlhy$Ojbo4@aWHUATUY3CO-1`Upi%?Zf$@u#%pos&MI{@~S+~?6uuiF5?q33Q&w>1B+!$Z2Q15_9dmrnp4_eaw zlNoKdB9Ch5whOK0HC-L$tA0`DoNN*Gaw+#%)1OVA9y_wXH&6)XSBNisrP+0M9-@H!T5 z6U6%cn31&fpNH)XfvVLpxpDeXn=oK|qo9*7@sdH`OWwHZ`PtkZ+}3P~X&{CEs6&fw zAV4>6$e9%#fKAW0cxGR{U)c46fba7qXNXap;9gsqH6gnYx5M{>Q!1;A#730xw8JOx zr9xTOS_L2}a{)TYAq7-W)A%eo`-;k3Sw$gqi0?*Uv1CZ_`WpTEFBFaY8IwDhVOi-^ z{b|RHLyr)#YuIaFxhBO~L^wEBp%<%A0`f>fW*vKCi=p^=Po{A6)_Ur#Xn16`?Q;(o_rEnO>0y zh^6lbEM1PKGfLYHnFt&HI~!D~0XqYo88WOtj`049hA!P122#E9>*6V35b-C8LSVIg zI5?{VAY)=Q&pfDndcU)_dn=2D1Pc&MnHgKSzz#`37eajrooej$re53lvZZuianl;b zxzm>&pj%P$^udnFzUh%3Jsl@Ih%*qmyRs)G#D&wo>oIHMPj*OostSFPQn%%iWxe=b z{ugpqMd__%`KX&t=_0?yQ?l#Em~inGX`AXL#5qbXM%jubIt;}sNK@(@s(z)tUidum zuOS_%HJ#d_9}59s&pNS58B%5`AqHZ49#cBX70})~Am=D~tr8s8hWspBx}jMK@nnV4 zXmoX`EsuyQimL`VcV_fj(%DAad_gxwab$aXHE*t_2fRK%2}iS2ru>G}!$k}O)`~-fEacRGK8TK`9pK*J$x0}CrEo|ZbOj62aW2kQM zlJq#PS-}Zye_=f&B6#JZtdB9CjiLH|xnzBfgb7u@6h2B0X!a6@PB9}B_zk#@ z6bB1>J;gHPaZS9T6C+Ybpm(M@a$0<1Ow{~7F*9oRjs*9*+e@7*+g?2^vC5=$f@(vJ zJ60?o?Wgiy!KG{q`~eesbgqb;(S>Y4hk8O%aR3ib-l-phq3#1no!G=4N#W&e>R~lO zY}OP*y39ItJrzQs#U9EI{5ds00CtaAy{f@W4G(a9fMZle*)-|E@sVk%$?5;kAkBuolnx7f<5nx1{BFl4v;O#Ikfa$mCNQ50sjj!Y0=Dh+uSPx{q zrPT3{#kT}n=I|>=EwzyLAlTMh8D%RnTOe2}XJ`Byf~Ak%#^=O~`O4qlTGTA%!#2Zr z(O*cNe%TQdUy=RxW(5i;X55G?@ua(MVc%k_Yq=TT{R=m8bdZ2@5}3XE`bup#<-Nt= zCVq3Xxc)c&czx47RG%H25eeg_Cba$&{BJB2|8rGCY+S(N;~G){kEq`^%;%6? z@W|v0cHh2@>I?&iO!cVAdlYvPw_e-EX5Q`HX?LTb)7Sj}LfKy*gbc!b*$t=aMc7uV zuC_(m5C`CFV{61|pw}{gb3W3CC4c_VewS{qO2=Mh@kGkHuuVzn6K2L;ZeG&!1Q`rl@$fK)e0WXrYDSfwnMkbw_RT6=0Y2;B3bty1 zVTVV-8t86}v74T3ul?yNqNPW9Te?~EvaZ+s(UDzpR5y@2L=zP9+AGlm@=P|u~lkbAX?V&b&aM5CH+-~g-6VI04Fkb1L9R(@>*2??>@6;w9 zSCd#haV%-fTzB^2MSdl%4zPmfP{VTNjm2@ws|WGfqrxE!J!>LGDcbWtdj3H35Is%f zsPGMuKhazXJth+J*HJ##_nA8){R9vf148$QZ|LBV zaEBdv>LbJw)D)6~1pToFFcjBB9X;zob)qGAYjz9|x|-<>CY&Zv46fWCR7Qiy$8!T2 zu^~kfW}_2$=^d~D0o~Wb;SQj36hkAWj%WZa!iFsUZFqDI{(DoR5$tJ=lVSp`O|cxs z4>Q;4Vnp1kq#g=x_OsnOh{$VPVav^;l6$szDY640pd#Jz6OoAJBcG?u3Xz{7VEOkW z*Eqs%e}T2*MI>G+EEKQ3=fH%z)iV4oJ+BZ|m^*0vBL4fUq5}flx4~_#ix>a#$I!kf zqjh{)Cxb^DPf|l;T9jrp>gn!Pdw6Q>!(Df3DGSy^U*nA_<($a?Dt}MJ3GQ)@12Q{{ zLIUZj_Ts|fZ^3ALl#O7xEj7>WHlR_Kgr#G@yU!9ei^0%9MVOGV$;aG41*i&5mT4^5 zo%PZ+EYINjH3}f`jL5*hWvHNZ&bm7bn8#;yAU5A6x<*C@=9;>Zw)Qw&vKYBoQ7LfX z%_Gb4v)ilr3uRxa+YrNHgpxH%cQc|7X~HPfvO;T?fgXa?@htlvpBYQ3i8FSClhljS z<=poQ&%EUTLEls=lRC+&)7*lUxl;vNl&&z|LMw zgC=p;s{zkCxNNg3e*S29ZBR6t60b*^;i!0)A{~KouS6y;u{LuKG2pJ?1^{?uHAbged&_GTR*bs)EG-$~TOE6dv+5$c7o0#l1`4>L1Nmti4!y;rL*_i;$vIoW%zE6(-Jd8OMn{GkCnyXl69$wUL%8$v2x1q_ z^9_81%g3{uLn>g`kfxh6I3<>Pqk23#A@x>c)l06#BVm=OO!?a0I~qNqscoBFvvjGd zqxv-CR$SuysH05-^G?X@wN_1{Og1Kehq~(S5{<#1_ClNZw<~DE%^$z`Z!g4*=Kz9- z)pqGqGn!GVD@EkjIP13@uDNHtBY?F?eXs?VPN(H?=8~;8aI0`*lo@roUZGs>&jRJ} z!w7pYpzz+>DyR&D-x4@JxjeC21lR4B-S@p52WH9t4Yw zB=ja_ZEObS&&0Sz0bD9eW$Hbf7p2fz1p{%l8$xg6c<+s57h|rc~cGb^nV>zDry>}f_Y*4w`A%l=RcSF3$`e34RttH5A zjjKXP@$J z9~%QacWPF}5$T%e3+GHJ-Brbn{-SZ*>HUbd@^*N2;vTY(?Gofc`(TWlvYsBPtt_Jx zmC86crtngdDmICkolUhDU0y4BKq>-cON8uKuN~fzqKqK?Y~JIJIU71 zNgQAkajW==>km}LhF@0jfNP+@*SE^IE-h15X%F2kP|ZXjs+blNr?(9cMdPq5h|B|1E~V}1tKtxn|F*}QuF0fv6&1fU+aP&hUe zX+b%D6ayHjKtt^^&az9TA;XB?~9 z&=`4wiKl?{%?h_2eShcNY@~96zoO?9+hX|!G1}BZGA>S!1i-LgaZ)LV` zkzf~2G#`_0060L$zat~}H8Jne)KdXe`n(K(Ox!|2OP=IIF$eM$R>!$V3b*DYxMAFY zYZE51q7+#patOzP>TqfHvsPwBUj3m1pSJ0xmNB=D_vg>Y{mAOr!%RL|NhT88%|JOd zB_~nZTA#%gYsVOQuJxf}LCFM})&`}Teu`I>1zC~FZV}WZ;^EYsm7{Tg6@pzlQ14&@ zO#9Mk+$F)E5~Lu0kXX7oD5-ENJpSevKyRkEwQ*weMeV4x9*9+(IlES{1roH&_}kI| zN2)$s@l=ByH}R%%O20y(wB`m_k*jF5tI`mSd`VhGj@o^SD@?jrnA%@c@!;!_v>yLi zH(%fN<4lVtHySplt;i{ik~gChaR8s?%u-ce#B0VFBr49fnN#FycqwvK{A#VbhG8jZh#_OcpId3=#c3oxQ_*y3hBU`j@QL>N52_R@I67bcSpa#>b zjs5r6BR@?}s$h^%UC;XbKX)a1o9HN>D}<%_{F!r^MqS;@g)G*=1p)5mg~|Z}yADn# zOf=Zls5?ROw}3|F;YD?pbwc`dOB7R)8CNiFI;v5r@+QrHXry_z(7c1eeaA_yNY#NKW;KK(X(&jxQ22R<0X2#UWN(uF z7aaqr-Ddanr(7~)Eqk{VIRQITo$tG3W`a zqL|(_72)4e&yqvw{iJlVw^prfsRP42r@Cqo5>f_dXke=iHjOdDlDrL3~nPd2E7_Np@EAdoZYIXo!2==j4y_P(!yo2}m-eCdesh zJm5rFES1B+n!G|>PH>YSesv1i8dJTv272u9K##(_Su91!BZ}o87I_$$cOAl7FK0oK zjd&^!Yy;NcS{iI@AZus4_F=lE=`b)}5qcHSY6uW2UrI}v|4^bA=RM5-c>Bm2Zl|g4 zGSV23lJv~7iqmkr%_n_~B8h-~5M=Bhmv?TXoB5CC33+2xbf81qEE3AlSuO|KST*L_ za^6#j(|m_apM*uj*GM5jlP97_br^{Zt|llP-+b-D7FcJBo3OZ}w>1$9cX`R!jo)Kx zwV_JmZ5uz{mP+QK)N`K3hwc~`vq#>1&e}u(whbRv_}N}VT6vj}`FE`w`_==i=b_2X zI!(3i9@K4X(xb~i$QYCMF%#HEKO65VF&d`L_YGU1aNv}!h;sUcmUM=Nz{hyLcQvN~ zVwV}NEkGx~cF{lhGT`GSken~Jp9Q3RA44Wgx7qSBX>dmuug$EhR}6(%$!E|WLd^Q) zUd}mlyyPn{l`C`id&3EmJN$EwVYZ9-~+?_F+f^$s79 zmE^kkwZli}8{h{FVcS#LGAOlV@Qxl(A ztKC>^L^*VukR~&;d^?@?3a?)b$@+jJGRiHk2s_cE7=ER$1#z~_SmK1~t6ni+p&xG% z#VW|r_#9QOAKSSkQPh&JdgVs9y{)si`!+?Qz7QHdaa-g6m!6Y{qn1mtahe@R&d_K9 zbfdQ}t^4u)QGH0E(d}iD-pmW0Oghv9_8_$=+KcR7e9E?h?C)J8)p<30 z)xu!vgmpJb9e|#!ztr_CmjnEBvZz$d%2%|gUrEPg$NPPS{LYIU5u(8vXqr#-~Ms|>=WrSIR>yVr}KFlR_7CwfoFLF z>1ID>Wofey4*%cSASe6t#h}1vQWtCzE_9uoIvIT$wjpe6^TYN8u0;t|1WBb)&mEjt zG?_t_K+)a`xHu4PFJ%Q!$yoEj?g z8y4zu@y;cWOR@*a!^5~V2k$7~!x<^w$fP;P(Ay@GD%gSFP=T{x%{b<4+6>!xOb18! zr&3nyymu;50(mcQ>tnLVh{Oyl0r)*tI^4R@w`tqav*p(V7D%aGj%N5rfh{ve3v%~~ z-&ukL8o_{1cH-B*{hC*l!5ZG^{Qs- zGv;KBxBw9(&!tY`yJI-jp8-z^{)twD&+t2F%JTx*A`=)vc63l zxfv8eu|!FNv?~#j)ssuqfoI=nj?o&z;LyNL4?6mk@%3>O zVXKHLsT^J9r>A@B?a>kF;piS1OVlLEJ0M%jdvMmTp8NeQ;`?Pk2484H-b5rK1+2XV`LvKVR zNp{n6TCZ|pucX$oF&`uJg;y#M)6$Z4jpD&9>dy#H+OCB=uD$lY_ska>^^9;Ol0e-j zGR#)7#Z4~*$v#BqOS7%JIysD$VGYYqbCF_x7UgOb9}UpmlrA8|t5XCpVXS1XzstE+XyH-xXOU zRXA{N|IdctY|(M=b|?Q6s$3JEL`SgYxlS%eg{wMjFXAJhiqVP% zhEief5pn%6tFq-$8n`YmK|N`ZTg^a{cc%n$i-ip4T^)m8vuW{Uro*{Ky9UT9X`YVf z1B>(K@s5Dtt#(mB#;zZZR@E9j@R z8Af12O3Z+iW(dqf(GO?I7a!{Y(i;=jIp%Oz)Kxr3N=VFPRd<|uxBbe{vFN?Dh6@pQ z6>g(Ok+z*oA1Cr|d#_OsN8n=7KV*A&u6X1Jnau+I5I3kHlY+`9jF`X=ajZJ>Q_5 zojh6FbLaMlZaw!WNwAhr&MOvJQBQG}DpO+;a3=Ew4#}F?HfpdDd39tC3@85qWxG;& zOPE#NbOBlJr#L9nsG%;tto&6b_AuDcw)wzdS~q9S49htlh{DA*Ql4R%VjA%unq3B+ z-qw3x)shaea@H9HH2|A9W~yU}?@o z4OfLS@b3p-rvJ`ym_7d4Q02ny6N^@sfFltYBv7nvl~4eJ;*@O7^M`0DxVD+ipJ_Ew zUkp$H%**Qlhy}L?Qo6b5Yfjflx6kz6$8C?Kwyfgcyvm`W^pLB6|LG)LQIPa+Mk%wx zWJz6U#Pm5KUuGMiL=SIfiO!(k{Iv(UNmYTAw4Qh#f2 zwx-8SCv;IK?S2#U<+}Z`gjn9O37w{xcU@nyZr>o7>tOb73-S8NG$n%im8ib%q>#~GO6fu&F@<&kZqj5Of7!f6J&wl zSW&(C12g;hI7E7P0Fw^~W|P)~=rt1a=m>NFis^7q=cKkb$J{;XTNxG7!UQ_{Y zf*aWFwQH+%-1C@jtRzXh8XYS9Pxt)Ucv(G~zt{Vm%TDbh#!lb3h)9@cEUM+!mbaq# z_?@tC@eE}pSrdmErHZ@cC;!QTCO|T8zmCa&{zQ%CIK|)^Vd<=1_Qi(B<3{it$EGL& z9DK!t$N#U?)s#bFo}y?2#HRXa7(Ss802Z3g^uw}}rnH5|^7C-$EFdn~{%7qx(Mq(I zD-@?w_dbL%v);L(LOokom{#1mc5Ws!S91%+$t5LWO#FMQ+EFWG=4Ex2ERYHOWebR@ zGCOml$Q!-E>=sKD8)j;6$CAG(ROM+wEg7I_knHaa`*-`ZI%Jj6+lje9``qFor}Rfz znt7>2F(3o16bQ{+!R7qW%1!0l9|lt0mFNmK=i{^VEie+4Nv*GqKG?E+L}F>u10|5e zv{L^74k_=pYVu+N8vz&$&rD#NwZ5?sw)QK?0{MuFCv!130!=c z{B2_wJn#J4)98DtDHsxCCd1FGqSd;aRgfqGqI{xg{u^*dF4QW>g(aqP|7-4gzUaX4 zjHsdW_Ry_!_&mf);`9!_Ps9HynaDt!4T@7*o94e=lPf;MI-y@VDVNV=u{&934jnOa z#P4ZkY#tl)BPsGq##%aO(s~9u+yureBtlQb6Pjtp)}Eg;5DSok*Ph=ELPD*p9?D)9 z-yI0oy&Mk2Vb(UTu$u*yN>VHXFjwYz()o03=#f3Wl9$`tTLtrKZVw1P>;-w{d@pTD zu*IO+>{YL6)Ue+YKk- z=L}9|j#r8l5XvYN!F{=Af4>b1Z)7f}9NVAyob9O5x1m!?;6y8UZLs8Zo#MqI`%g6< zA_DAZGAOd4#3@3Bx~qdA0)ht3COhXQn7AoA6NIg|o(tA}Do+xk%KH_ek5{KSujB0u zRoO+bb`eFFtG%peD!$M6 zFU0~TSs;()yRT?a!68+}FBO=6nEVfONwmKs>V{>t%aX{d+Ob;NCh3+7T+P@Ubk{b% z2M*>QlSclW>WT}_KdZt3+b8CIe>&3=AO9k`#v^VH|Q3$fDVcqRtW*qGDs)4#(K!XZ|+_+#e-=Dk=J1A02bxp@5zXYj#*ZQ z1B=QMAZ|>SEUb3!Hutt0hStRdn{y+G;_6jWVW(ZA$Cn5OWg!F0YRx3Ar(9fFV|<|k zyO50#s${1*_&JY$QR8-e4^)R~2Z7lV?!u^T!h%lqPqml0hm349DCOm;tyvjeF;w(k z_2XktHoYglX%!9*h&;lb7kXHQIqB&H!`Szq&h))w)Dqbn$RILU0y=w-VH)MGOkm-)=2Q6e`k8SpFlRy6!)^#(Z!QLPc590Bt)j+~dE4H*8hqbJYscDUP$8kVRJmKGW?Fljd@wiMZ)E)zT}9i& zNBJHK@sw|&x0=!5+ZQ)DiQY&G3!QeB^gmS2*bv|&`wjMbB8D=^nVBCtt{JF#tZr(0 zrGAAPn7=y>xWwmo+6(o>KO&kCK2q>{g?vC240VrWpARXJ#p+~UjzW6%vHuVU7f93l zer6)AO~N1^dTGY-EyAh)HrB(oZQ7Rx5&i@Sl=rQly4rPR#umslm0O#-^VfnQT1a54Zo+wbGcJHa#80d($5g-a<0< z9De$tt$}IGDY`M}`9y2ZGzZ*vt9;cKsx?vohUJ{NNBeszlWU9X zXw*V}*{amu)gV)53gsIqqS0J~4xqf0De;J%Ep3dm3b-mor52@l#ZYyLS4FRd*uS%N z^NJtdVKO%qo+!9P7XyrzC4L9V?xd)|c^)Iyim_*}#lKm$-rJDkMJN?l4+}KGpvZe9 z`J2M1=*D;R%jqy=j`RYAZF7JES|jT#&7N(LMD2~}o(LNG)=~yFbr>y$XHLQctGSqu zhzCy-sIv?#dwOwbw2DfS=}7Qr zrFfh;U_T{vL+ESBI$hljfigZ+3e;{UfiZrmP|~M|iG&lS=fF2umxUY&57GjA|7HnL zs~-_Zk@Lj30DQ z1tA)!KB6H1_FgsUvl6)P@GROk_zP#weUKcNZ?d$N0^?Vczj4~BrBen{au!aW`s_N! zxK0iwXpe8y}%T& zdcLbP>HOVch4FahCbgS;x4I5A{((5F{VN3NM|TzFdgd7yfq5TmDYGFaH_niOnpVrS zI7|TLliNqsi$91*b8h`bxY~`=f}gE6HnUO?_?kl++Z{0SC=vyR3u!Z>Z zv1siFaMPcDTtPu7$9vASuoXfeEr9@BqRu06%z3L{J74>UqwI5z@H_6o??n()_L7q{ z0A_vCftb$V0TX?|1-BKzvy5Y(y7i^?gddy+9bz;(t*A>v@dRYjq30$20?F15(f}(q zGnpQ#BcF?klok*bUT|JX_#L|r&N3HZjD0K%#3(8@bR6u6wC{9T1rhqPLu@Gu_=Wyy zE+eTe>|EI8Im2&sc=T`$#lRLj!n@-RmmwiyWNAxVpff~6W;QG%;v_VtQaFbW2{>2g z5+bl|iraHs8`Sn7Lo({``A_FBs=|TS@+S#AAjxh{A4kAZb`4SsW4x->MxXLFo5#=w z&hDGatE+iO?;py1WztVN7dg)tRu4dq3Yw@Z%&rhRnkt1W+gMM3;Y^l z26H}eclowrD5^I%_s{>P8%c1u8Wsnq?NMi>?OLVRyglUjh;(1^mWM7wiZq1-<=r84 zIHm}_CfXp7on8(kIfg5K0M{46$ePQw9q~N)RiP{iG#e8r0ujI5+KjK1YTEc+e-~>v zmOg_CMh!T@VdSyRz#`qQj1J0a+OYhG_V1b!Chs^(1l>Kf^d_8P#Lvv$Ef{i~+Er)X zhMs>;03Hw|0k@%)5VQ$W%z`Yt^GPaMO26RBj3f}$ zcFYU1wj3)Vl7Ieb^scMU#L;8T`c%oQ;f}zmxCHAi0!1vZSlvQrFm5YaIE_nTR$bO9`hS(?funFQywkrF z{0%0*tWw~rEf&uIOpb~gLs}0>GB53GYX}jhwIi4xBQ3wG2_)H}n4`f^Ls_ON$!QtJ zCk`ULU4$5x`3p94_!MQ2JO`VZu$u86L?Z`*J9BK3ja*fzQJMU_@cVR{astJ7v%SdZ z6B!s<{lIT^vM9oAS-^cg-K$!+IO3*9+u+Iiu4>+Q>b)_7A5tTGl3CYC)0t6gB@uX4 zzUadBEu+F{k}o1d1@3rU|Mcp9iVmK zeit5Wl1}Q$;lr)g@YD=xtuW`8MzKJRaK+{gKz z`np4{Ac7St&CxOT?uk{KtB~S$eA4r#%l72}ExuBNz-OM`sv`=y5M)zob%qS>B*i39x z{9ZD8`*AagaY z>ZH5J$w59RMnnb5F+GZ7?bMMrqPzX8rSh=Bg83{AfC3}|`(riTYs5A$*|H5_WX9yF zz+RyWlKy=_c$9m|$L`5;bz4DgH>RyBpMyT+8=#UFgI7UNA_gYMa28WII8PGDK~=!0 zL^Wngj@vKO2ZlS#{qL4LSKiJe4mj*t; zXR7H6A9zQBVW0um)NN)DYa{anHCE_L@25^eK6z`&`*19;9Blh$Ke^T{WPM@zJyCaR06{sb$%oym2zm_1j*e=1Qc&9kPu)k7z{78KAS*gh#V z$IlzLvw>@09{jfjpJ1g4*NFJ-Tuxm;Cn zLr$gH3YT|$9}c~onfj_$c1UF2gWTh}zsz>+jtB9CaL%r%tocc7Od&hQ8p-cMoKjQ9 z9c=^vM^(zSZocBq%F&gd7(i(3&AX0zCji!!8QEq$K0hu?c3fpJHGo&U9+9fC(VYWa zYufkB8$`=`G|u`B=2_nha&0ys<$X8jQO(8?A!0qbaJXWmCgp7>POYD)#SK@sPBAfb zG0v;;>Z_AYvQeAhL&5aaIZ8{F7^A~kTdMZG*EQAsHmaz*m+?x!eHI#yz}|xl(CJ@bgGFuR3gF|Lbaz^Pl1K5xne6zv z&Kz185(5z=vb;?{;K;%+_h8CBAOOQio1sW!ez0|vgrXOAVZ1oO9l zYc6E+qr4@|I{Iw}LWNS1^k-#Kx&pJq$3ihgc|@I-mFn1y3x5LAYX5*L1w@ezX{19b z4X8^7*vYrsCWY-5STLxySoT7!re>4NVE=Mc&ukU8YNq8e@p^72B8Y{#ZnrnfzChDy zIcMAMGpCJrqw`hmq=_O&chrvhJFz6J?$a-%v(0J*+5{XOwa!lC2`M;;L4>~PlN@F( z!4N}4LVI_kK=(z}DQX8UjAou@yt2~gmtLJ))2N?P!Asaqd5=#T z=QqDH0KHNqXg?rh&Awd)3u|vZrX!-`r>JW$xld{6LP>Xa?CZSN+Dt&nG};y0?AN}}N+7_VdW9a<%P3eiN-(%* z+qizzA(0%!XzW!WZCxBh4zQ%Go-v{?wx*$=TMZ>iHu72$>H`}grASPfx3dK3@2U81 z4|54)Z6U=wOqK5ouC%BEs72D6C)JPHRlm!E)u?9?zzLK9UgpRhVlY=ugh_lVOR(E* zl2$+BW2iE_>aK z%;%&Wht1^bR)eY-(DN_bra{F{C(YU`gP>n`h{ouiy?0F!^X>xUm_tC~3WHTu^t{*5Y0WcG!46uP&ZtlQ_NaU433lq-KEB+lcWEqIB< z%?|OWodnRY^aQ~r6VZ(={`2XDhlNGo)Xp9OMJ!8x+gU8`P`CeDmH!tHmh$ zX&USYNn?xq^9jermd{$V_N)}uBzG50ZKlzUPT)wy=?U=Q`!cjnoWWi^p?wfEUQD?m zI-t;>B{?%ta8@*2-Om2^2aZVE`CUh!u2Zw)0(A@PVs#L$dkNVKwTV$o0$V06zi|KWwnd^(h&Z#q zwBN>dH7^KG!WEqgMIxi~mpzya`vQVS8DyHURf5~L5upM#DHNi6cXVMD zrWU9NOk>6rJEQWA;Cz_FD{5X4rd?lt_p9nAU4Jq1|NJ=}tTwbqVr#>sns)Oq(kiXv zB-LUmX3Z-Uo#vD{ShO_b5JsErP#1ZLae-&T-Zm8;1#e-NZRyiH{CV)}`gj8c(T`C5 zQct|va@93<(+&B!10*s%5bG4Wt{>;kA0?i~Gb2?qbtq~gVbF_8%NwpC9r9VHHNm({ z1Vx`pUpjlbDrILmx@pe~HcW6#FK2K4&}M*;hdizwsZPNuapo16>xY22(@#ufoJlS` zMelaF(aNZJI+O>oIJnh)!02<|Qr~Dw8O#@-%p8TTT7s5#ZO&^;G=~GR1i7&#T zIUr`;FTjhlv2YPD+4%J9^CU3)T8I8`xeaBQ&S{CGq;e#Oif#4hKTFujBf>(fuZ#eE zn5~W$w??<9LO0M5`w^qwWfXtJK55`EM#FO7SG?#YeIagpEoUnZvf==!KUERmabwH? z0~*@PKg5ULUg9WAlqnt6ZSBzw_V&*JUK-O=Rxti4%_5IN|+Nlm`H*p5r51PxyP!;+Lxn|<@l8vqabOkdBR=Vwt{I` zI`)M%_u*O{B5`jov?vSQ>vEvW9Le@BSiqU&?l$W_4O6eEPS7ui@^yJbDZA*Aj@_&F9^qvh-*pCW-lfozQSIqXsRk7=849wW49Ej&ZVs~By&wrCP_F<6G16b5k% z^PDCI=$|qzG8}=jwQc0Ba!k-_pQgwDi9wSiz%;7dB?t}H%#HY9WsXC2o@!1q=d~e$G&Dk35Cvi*JO+pn=p*yP zVv2a~`^Js){Z(N4Br+OD{#%3-)X2#O0z7uU49QOC-9#d@qA|To%N{Qv9g1aEc6$sj zkimBz)@p*J5*)l;;S{|ysIeJKxEI)Epldtd?=JB$M?!bdnedb_P=9Rr5Ye#Y&o4NZ zS4f5*4smcd35a9SrzgIWY+y}Quw%EF`Fib1o1cj}Ujq8>&M6j-NG6|095;T zRPWb=-o2J7-pVe+d&Z$cfoTMyZ4jF3)bSvzqm$xC8E^c|`=8uQP`QFel_b2P_BCO$ zsukA<1#g+it=9{GouIO;XbmPbRA}o*;VEO4(Nn>1>4)A76Z&1x^c+~8Z@icIv-Q|k ze_bV9!SOg)yMd=F{-QH#bA0dBaA~40n3m>qZ@R!KjF2p<^XGY#6||rRO%_uF1KQXv zsA6URk(=8CjMlDIP{EE%2~%3oqBe4UL{Y&OzDt?i;WA_uWEYB7YD{JJeTZa$N5H=I zr>nJ>JZNPM=B(V2phYsujzGWc09suZy7-kKhxRXFuQ7P3*_TZ&+#6wJ5_Lx}?fM$} z;H*B}301f!KNGf8cl*!SP2CIU^lJYz-a&L%_*aI$Y=_TYc~LwOLGC1H+eO&*=YO?y z9S>V>NiQj92ty@Ku}1^nfydsr?+Awsl%2V6OdV65sdy(|R_~?$QkW}@@O`Q3%`)!n zZWeYXL~#$N08=A0S~C~dV}IhehqvGCT0!b~L#m>tUPaB`10;?^#A#QtmLi2~4OX&g z*P8O>UNAeEv|vK#uD(yXNjr<$HJJ;^VVT|I9pb2@!>qF?a||yXu;e19=gd0!XWzX+ z=KQyZEaM0`!qut2*9$(Z`kq35cLZEEh5`|1qwfGwTvg`$&HaZ2@;xtAJqZ=-tmc&SXQk65A^(x_`WlSB=| zuGMfky~nJWKKiRxX~lTXRMY0DMN9Al+|dpJs%4@UQgL&hKM}pxK!j7eBT4$L5B>Y5 zo|Nd(7}2`i7aC()(K7EzfxxD_>w%KP2Vs*mr_>Mq3yk~ZT=eM5o?PzPY-1o>5y2s( zKLBo(x9&QL4uiuYrc{}wJupG6VPH*|-aV(-7=Eb4O{{P6)96#k_HJv;h0Ef6xI|h& z2oSRd6mFfkp`FAquEKQxz!@Ai}=eNtmrj$IO^M-D(KZPpU=baP$h znz+F7-?M71aAJ+;22LhN8xqM7m&>p)U7zK?oOO73a(Lj~Q>$wxSh+soUMk`m-+z(X%9ONMJ|Lu$$mw&f9Peca;|ybR%xNp}3b6 zs^Pokcz2C9jG_+&TG~nWXw*qj;Fb!hV~E1IPV@ohiw(c{2)W-=BX15Q52>E_&#A2p zSvG53SATd|CXiaZ4v^0H8{BwB_!N3rgN=m7lo)zXIc{^S#xo}fMeP2>p+H{yW?!w! z0$FzXs0}x4$A~y+qf(fB)m&C2riU-RYxfZRi`dhjmjCQ~r=dnr0u(u^suzSOG(yU(^NZs0YWDY%y+l(gaxgI{TRd7+8dKFG!+c)9sFuROyJ@l{6M+tOK~GE0TksHj`GfK2|(9vn9}Vx?CR(T zOS=M)x`Y3_QX3sU-%;ICY5A2|;)#mCsyVIN` z-C8EVd|p2GH_u3c2?Ejgr*4^&w@I_J369rMy$Lgp+F#?sid^Q`D#?z#K)P3ZW~3k5 zdnFCmF0*VtxoCj|k5XgiXcDNB1{K%n6WE*Q-b;=^O*`Pc= z+M^WhrF=#mm|y@DC(IUuH3*@qdZLmVQ3j{;z$?@BLVLdVv+O7{MqYf6ZAW-@_Mh=T z#ApUhcM}O?f6;~3u7qiyz&CQgiVy)e4`2e*@1{rc}*~OO%nD6Ld+5c zXHYOhKHV^=YCdh=Ap`9u|7pK#ugCa_84OSI*Wl2b`&2bLc`vb+IJ3pz4)6je_&d0k z*cw!EaYOpM5K*s?RaaM+YeRZ@M|*@kEyH`{f-SuEiE^*Tr%m&0fSbNkCds@IO` zSiBu>xmTKv$h{)(t~V9l64t&rL=rw^Q~9S1=Y$}ioiMoes-Z;p3=g`eS>D#d-Qy=* z9Cb0q8ZO5DNC0x^`I?dA!4ypDeK>7y7DaL>91u=a-IizmcHSN{WRM%;gLk7rHg2z< z^*6$77$KS-A##fOZI$f95fX8a#-`VIGr=Jpxh?^%_TE;)g%+)u?J_zHOzTeD&ULkQ zJbgyevqW2(OMvjv>%;Tb5c6(Q8}EB48Ya~8DrPbo66}bnA>V%v5+}SiL&0KgRWU)x zB#Z%8q=yiR#KJU?|7i-d9w#?~J@fy^-C@SVBv<}IJ5rqI1oh7qPlgI`)`DjE= zf(c1+D2pnAiT!!MRUz}d%48+A@l7N#@~8oAinWbfGpE?SSj5YPGvlCsy2kq7d7(v9 zF(qn*XWbO^^V%P%8NpCno{?)4`olvc@cw2eXbH3$eVhl-f73RLLp0AD zEnu|)@}l-}a!V{H*hP`nc7EwRKAv9b_vbALCDt3O0>xb3rn^hWo~=4tQcHDl&z9H0 z6Z`t%RNL_>#2riuVGGW4MX+p@{j0_2(0cnl-_eKzt;NO6IfU zhAbDTP~KYQk+O|LSZSh`0Y5VZ8^6t{FNWQpuu4|n3p?^u*%?42x{cpnY1+p4I@&J- z!@TnrT!D0yn$30WYPZJFQWF7HOIsvIPf0-W`Vdb?e-_eUJKbtLOi{=`t6R*7R?yPQ z-i_kyBf&2~moOYRX|=KL@>s@SNv7YS@&D3O^@I<7DQb9~$E|E))N=@{6h zsK4xCfbRBYImu`+`_ADSND#80{=0$&V`H<(fFo$uOVsqvl}G4G;)X3>o)a>5IoF|~ zTu6lDJO(jo01-S;>tcr)#$Il;QWsCuUl`NpF&2VWY2aQj$}UzV=ywT-lGO()rK#Dc ze#0Do{iB@SZH)?HdKYD9;oH^c8R@YFrBNCDAd$ec3*^b=2=xq7g8A~UJ zvfpP;sf-d?OA-=T__B_AreGr*tBDST1@>kR7G03|N4t^L4g3Izq z0sY;47Z7Y&7-_@tZuOIj!yyKdzIbOl>2kQ{uaHBR=N+gQMS&~cirmBNTqx|Od9q8F za=!=^mfDG;Zp4YWe5c>XpAnAdcU&hw4^u4$7(H6B0;3%0Gonlp;yT9hJIK^B&T29OV z%Ccw?0&qEdl?1k}Pui-n%-*T%IRRv+`wvQhK_AT&~>&mkZAUZ|s?C+eBc3)qSzOQHu9oI|8QLh1SYRDkuN+Z@d!H zUo#UKp~)dgaVK3B(tnqx50II@jbz1=SaqMda@I>vDVpK2X11kUg!hLz!Jy>_Iv@>s zu=xak9Ru+xEufJTrGTSoe5LSlKCI_7WLx@e2jMGBmUpPNeToJ&PY{EOb=xl9l6hP^Vuicf0@oV*M|@aq zWECWP6<8iBzDz2}euLjgQ9&`-##3m(1kW#$NliA&17zTsvKYdy4Tg0TXJUcirF3e4 z!L$&jjGbA|3{)sUxrDzG^Tq$E#`pUY#6oXoH*d&c)C2?E{i){B{eD3q)XxB{y?UP` zFn$*T>bsp6@86oi?WoS7WF$r|n9K5jS!dDhL5s~$g4C-)!)xgjm|kj&&t!@+XIoeF zu36VkXjHLuYD6np-`R^oTNb#*!>k>G0+f8p&lL0Nx(sKF{3U;})Fw-U!Gitatnc`F zKF|@We73(B@Y3Tt{3i1HwZpi`?;TE2EKrcSSR29i|%eS#T!+j1V<;>FO z|J&??lbotclu-G+CvVoL*!(pNZ~)4EylLQwR=y9ZNep14{-XMQxP;}98Pn~Gg_$Wd zDj+3q(xmVPoDLSv5rtTPQPj^Hwfq+%FcR&edop&e@txRk(rINy)z@JKRv8V}V^)js zQ3ObhjcGmobqE2_gwr?oB$LD|oFoQe@+n`;AUw~PMq|2t4E|W>VS7;rvNf1`f_EFi zO**KYF)Lf7BjIN#t?20c&C}*Gz0Q4@R;|ywbpHG`Ag9S1HEh6QWNp;Rl@OXJbu-Nr zm&V&nAWMs59xN8JCMBlt0{|Hb+gdKzwP!Q}roB+k0UZuy5CIVQB3xMP?h^AAGl1y9t@^(?ol9q%RlA7 znOwnuENe%OKqGr5aCzRFq06T1^SRK4VVZA^%K<6q>oa1ov~`Gm5a3XbnD;FVZnxWb zKPPo=6gl)apo;P$Dgka6{f9fH$!HP_3YN}H4VPQi4AY0V-+5@U^@MC7sll|Ke;o5| zBQZ-}F2xUOk2h&@6CRdx#B~dmRG|fUbjDj&ipoG1$nz``bt}=Dt%*WbahzP;hvzMk z!^hFx7p`|TLMT=#Rfp^rXx3jSb2wt{;!k4MmCSl^;C z*p6;E$!Tl;9)gN+-z)&%S7{NT%9O;mYkT_#d%3=CW=7P$L0pFf40>vJza`3zET2os z6ZzSq$99nkZp@hPnvS!n7om$b>j6&>u<(nikISRk=Z8^JA3#@l_n>=y<0a?)L2_1@ zr#aMMuUB827bBUC7)JDx1}EnMh{XJJqRT>1R<;OKIi$T)iSWNnKo_Hp+28#u3}f{x(DxvzOii?X zo1(Zfj7&gHr2t`Ow+LlIiSn0IQsq^uDNTG%7q*1kStTg@cY{)sP7o9-<=>$N4+1#6 zl^fB{jp3<81&MZwYrf$>2C$Ug`|9o`hz^0uj?Ylh-hrx+SY_MQhu9C|Jg1n5jbwC@eF zkf)v99G8rEUaB0=V4jzGLMSqW*xYU>W?Y{(T?k!$FbDoHjUV%XG!s;Kc=e9Hbg+1zS{6CxAC`B z0S98TFC)`Bkw*~R0siL@Ai#w+ke)zB(j|+&wTywEwz(VLKt1rMl6H)hiH7$L|3)Vv z?>mV77(n)g_`k7j1l%Ne7?2)40bqp~1~c)vr9)RO3yyOR2v}sV4o&@|kGhsea-TYf z77XK*kl#y15?$AUFdTS*p03dgDM^52yQa+L%qjk=+Hs5U3TPm%&(O2Ic`Gr6! zplZsF*?M_X?PXj~+d(sW2RO$KfVW@*WnuL-N>fT4bOeUFfmpdcnX_sU;FL_yshXo= z62h~1NeFgo20H*0_ZW!(Kc%I4O=OM6U6&;yDRLt;B$4Sx;@ z0)S`j?rIDTz`#vHHFfHU!S&IB$Oi$LxP8#hqwE1nVpditFl2l4r$~?^E2q*DwQ51f z+4&PEW2!OZ#W{@#0d@BFmf%7*vZtNF31wF|r!(@7$_$a6>isd_VY#lwd2X%F++a+1 zp!tcl0%O1p`dV}a%69>5rWMu%kR!}Kj$WWnIw|6Xec+TSqnSCEFW#Y!U{|Q9s@?Gj zAk44b?1T)Qn7^{6s!y~Hj#aJ;pd8-<}O$OJ#Xox0KkK8G#9f9Y1 z)hoTr8pDv{a(Q7?LGCiFU`+ijgwQc`SmwLWR&PCXNMTh{cu>AvJQ*;0BWb}!uKK}a z4g!#69wNC4P0h;G0)j2UvGw{{_{u=MEwBK6;>M!>)1mB#^p}sz?B?cesXwka53dgY zy)4sRf?Ov}F6#+aH@$XPq-C@B8-e#a4jqPrZehR#g$m=ULK~@*q#n=%XCgM?zcGVd z?-DbX~p4V3QOlO5P)w~W!w2IUB_o9ASsWLkZYiOS;;ymNS)@cJ@T&($WQs5yJ z!V-R$1bfgd;@wt@xvFHQCV@#BpuXMMZR zOBofr9@zZo%;H^Fx`P<1nQl8aEW#9JGbGM~p1rf$$7=z#6P z?iO*y2LG$Bp-&vxz1^K1E7UhAJ}1)Oxi&OfR@OQ33+_h9_yY0z-A(SHTAw$}ygT45 zF4bE_NUnY&YFrg|rNmzk&1ScB<5VHjyHwVU>`U%{09Bg^zjela)^}IIB(OW%kBuY& zO`0;&laBK0+U~iA87$+aUblM}tFhRgyUBnNw6u~3$iW@adyVzHZ}^cDCZbwnmq)T& zZK}DM39|6;rLyWM%Mnr;6c|{{G`@Uo{Hzz)S@MEf8Jcq(28dn&OnK z9T7@>$AbHU;`@Y_jPt@zz6lE;Q?50bH06m530UDzj$djvxTi$j@wJ=$pl2%yxGcS+zyAwYXMg*ncd9f!x(kz3W2N6`-XKlm?K;leu z8P1CT972OY%Q!oQwDIMNC(Lr) zGvqMqa1<3J0f%T(DFSQK!HQqO@TfR%Ii{cFDiz<~Z^F}Qul#-}7;`@uLM|uLRM^7g zu5>{^dB*6$RMgi12i79~T!vx<-fg>8dr%?#k*|U#2X%ZpbZm+=MP!WB(Lq2(O*A8} z>C(q^;eU>DSKL2jlOm(yH7F=+vbG0zY;!F3siSo*%eh|k>>sSOT3h6Go5(VRwq5e> z`N`W99FK{>DyeNzLQQtzBY%Mt9gO_?0q?G%9Iuf zF46F;7j~rCws}Fu@iWg$^x~)5;NefCc>RUFSNs4ex!MA^38N*DNnvpkjr{?(m)tRG zBI`p8LIqQg09v3RkVj+h)OKi3t$LCeAz|qV;UT{=9k{RGj2fRI60so`r!*$mG`iI$ zRcGZ}UBAv;X)L0EYE2#nPG(*vS-Pb_s$`A#gVZlw;ph{mc9`(KY3lx6gQ)zzY0C;e zGw;NC{c`J`40`a*{4}vWY5LHphfm?X^u+KgQCG~&^rbL&kcSvU0nU~@|AE^A9uS<3IVCfZ@M{gBQjn#vT+5gF1qOg)=f z?rEK_Y@^aY(bBx4ZQ3$Im4|R9L|5cMt3(bgshXtfV84JH#Ya(D^Mwm2?Rg*=xglKZ z`+7^w{=FDwrFQz}-8Wk1j9}fh=<@m&=Qqyo&P+&zfT}aocfJ0CX_V#JVY#;o#IA_O zh6vt9Y1OEqfZ_&>VfE{$OHkx14+>b@9WmY;+U7z_DQgp?7DF~J?vaJPNbdiae@ndX z%<&k%?1N){jYhIo8KFD++Q0K;^qbrj8P=fG2%)44*3Bp$_TEF90z6nPB{P9$h$_Xe zt5<<7WxBORHP-HXjeKGaUwU^p$J%6=uWF$!2 zH~tV{8~_UVVvcN(v;mjwKa_{-_RY}IGgzS*GVXs?j)cxrh48mN{UQfnn!9+DEwK+2 zNwMcdpPO;s*Nl~A7_y)o-Y=qY(q9xnwXPVR_;j5-3{DDQi{U4kHrok%pGOLVinMru z#*KV!nAHD3FSLepY9@!!Z+M`$eVDlw~#5w^AHjUUB9l8Di*hBE1; z#eO0AR0wcxJn7OS5_wm9vb9fKdT;uF^(SeZs7fu~nMj*{Fg{+$_|M#h zw0Pdnj=`oo+@xq7a7`K>K3HW;yVE?T?|;2jP*356=(AI2K!aPt6CIF zbfVzsgYpkt!@vCqp|C8u1QP@v(8Lc5Uc{rLS0iM_T>8jqS9$%{%X^#A~<8iMp9VN=AJ!S2nSq8S2()Yi&ku?R0%a!DeA zW64Gpw-^dY-peR0+q{*P_wS>DM<0@jVf= zka`Ru&IATS45~v|b`_F-=S=qdecxCD(x_c_s+z^$)X~}Jw*Efq|H-i=P&ACsO@x zy5pz^RgSt~+D;vHNhB^`zpsygGNkDCB|lbptu-Srn-#|foEP)FC<#sw#-ZVOx^+G= zF8+8eN~$OG`<>A70N%hY=UK`v-W)9FpMYL8qo4s=zk@|3qhud0c44kU_w7JLGx1(d zOywzuXNR`yy+1jmHaA`EUDR7jyNa^14yci$!hmXcRjHE6Y0$V5BZ8Q-F^Fa{|6gA$MZ_JB?Fu207 za|oJ6hNRaK%m9j6HCZmsnS9ti1E`(g^P&GlRW`9b*qF2QDKfW6x&Y|<$jG0k$(gxx z_qy?BOGZ**lfWN!()Fmi20rG8S^UbH)kzM?Gr*!B_nAjp0iGpxw2QjunOL>y!>o;AB@G;UdM@wpQrWQw&DI^8}y-+`9Spma13a;~=?j^B>YY;C!sM zQi5iI2;7LvYC{guw4HPv(i_~q0i<62x(H=}KA^sw{}iGNMI8}O%=YN5U)L?sd%FsK z1LH+VZ`)u}I_4Q(dNh-<%h8rHD{Z~&j{d1<)v{74innV*d^k;27II!6#BYu6((+o1 wGUc5y>95kY0nQw?bgAMX6^C5fd2sRu#DD+*0CgW6OjlumD?v_5au5Ij0O>Z@d;kCd literal 0 HcmV?d00001 diff --git a/docs/gallery/index.html b/docs/gallery/index.html index fde0d5f..3e5be68 100644 --- a/docs/gallery/index.html +++ b/docs/gallery/index.html @@ -524,6 +524,17 @@

vse-gamma-cross

View example +
+ + light-link-studio — One key, one hero: a light linked to a receiver collection lights only the hero, proven by two pixel renders in one pass. + +
+

light-link-studio

+

One key, one hero: a light linked to a receiver collection lights only the hero, proven by two pixel renders in one pass. Linked: 4.0x luminance ratio; unlinked in the same check: the decoy rises 244% while the hero holds at 0.0% drift.

+

witnesses The API is obj.light_linking on the light OBJECT (ld.light_linking is an AttributeError). Luminance is sampled at each sphere's projected center; EEVEE Next honors linking too (5.5x on 4.5, 5.9x on 5.1), Cycles pinned for deterministic samples.

+ View example +
+
diff --git a/docs/gallery/light-link-studio/index.html b/docs/gallery/light-link-studio/index.html new file mode 100644 index 0000000..f6ea7eb --- /dev/null +++ b/docs/gallery/light-link-studio/index.html @@ -0,0 +1,545 @@ + + + + + + light-link-studio — Examples — Blender Developer Tools + + + + + + + + + + + + + + + + + + +
+

light-link-studio

+

One key, one hero: a light linked to a receiver collection lights only the hero, proven by two pixel renders in one pass. Linked: 4.0x luminance ratio; unlinked in the same check: the decoy rises 244% while the hero holds at 0.0% drift.

+
+
+ +

Rendered headless by the example itself — click to zoom.

+
witnesses The API is obj.light_linking on the light OBJECT (ld.light_linking is an AttributeError). Luminance is sampled at each sphere's projected center; EEVEE Next honors linking too (5.5x on 4.5, 5.9x on 5.1), Cycles pinned for deterministic samples.
+
+
blender --background --python examples/light-link-studio/light_link_studio.py --
+ +
+
+

A runnable example that stages one key light linked to a hero sphere beside an identical decoy, renders the studio twice in one pass, and proves the whole light-linking contract in pixels — the feature AI-generated staging code most often misses or misaddresses.

+

What it witnesses: the light-linking contract and the traps around it.

+

1. The API is on the light OBJECT, not the Light datablock. ld.light_linking is an AttributeError on both versions (verified); the path is obj.light_linking.receiver_collection (ObjectLightLinking, with blocker_collection alongside). Assignment reads back through the API. 2. Linked, the key lights only the receiver collection. Render one: hero well-lit, decoy at the fill floor — hero/decoy luminance ratio 4.0x (gate ≥ 3x), measured at each sphere's *projected* center so framing can't move the sample off the subject. 3. Unlinked (same check), the restriction vanishes surgically. Render two: the decoy rises 244–251% while the hero drifts 0.0% — the link restricts without dimming anything else. The shipped check therefore demonstrates both states, not just one. 4. The engine note, verified rather than assumed. Light linking also works on EEVEE Next — measured 5.5x linked ratio on 4.5.11 EEVEE Next and 5.9x on 5.1.2 EEVEE, matching Cycles within sampling noise. The check still pins Cycles for deterministic tiny-sample CPU renders; luminance gates need view_transform = 'Standard' (AgX compresses the ratios).

+

What each check catches on failure: inverting the link to the decoy collection (exit 6 — hero drops to 0.179, ratio collapses to 0.22x), an RNA move of the API onto the Light datablock (exit 3, guarded), a lost assignment read-back (exit 5), a link that doesn't restrict (exit 7, rise below 50%), and a non-surgical restriction (exit 8, hero drift above 5%).

+

Version witness: ObjectLightLinking API and the measured ratios match on Blender 4.5 LTS and 5.1 (4.0x linked both; rise 244% vs 251% within sampling noise).

+

The render is the contract at a glance: the blazing orange hero over the LINKED plaque, the cold steel decoy over UNLINKED — one key, one hero.

+

Run

+
# Two-render correctness check (tiny Cycles CPU renders) — the CI check:
+blender --background --python light_link_studio.py --
+
+# Also render the gallery still (Cycles, deterministic samples):
+blender --background --python light_link_studio.py -- --output linked.png
+

It exits non-zero on failure (API moved, assignment lost, ratio below gate, insufficient unlink rise, or hero drift). The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1.

+
+
+

Source

+
+ examples/light-link-studio/light_link_studio.py + View on GitHub → +
+
"""One key, one hero: light linking, proven by pixels — a runnable example.
+
+Witnesses the light-linking contract AI-generated staging code most often
+misses: a light's contribution can be restricted to a receiver collection,
+and the API is on the light OBJECT (``obj.light_linking``), not the Light
+datablock — `ld.light_linking` is an AttributeError. The check renders the
+studio twice in one pass and asserts the whole story in pixels:
+
+1. Linked: the key lights ONLY the hero — the hero reads bright while the
+   identical decoy beside it sits at the fill floor.
+2. Unlinked (same check): the decoy's brightness rises materially while the
+   hero's is unchanged within tolerance — the link is surgical, not a dimmer.
+
+The engine note, verified rather than assumed: light linking also works on
+EEVEE Next — measured 5.5x linked ratio on 4.5.11 EEVEE Next and 5.9x on
+5.1.2 EEVEE, matching Cycles within sampling noise. The check still pins
+Cycles for deterministic tiny-sample CPU renders. The API is stable between
+Blender 4.5 LTS and 5.1 (ObjectLightLinking with
+receiver_collection/blocker_collection on both).
+
+By default it runs the two-render correctness check (no gallery still) — the
+CI smoke check. Pass --output to also render a still:
+
+    blender --background --python light_link_studio.py --                 # check only
+    blender --background --python light_link_studio.py -- --output l.png  # + render
+"""
+import bpy, bmesh, sys, os, math, argparse, tempfile, shutil
+
+IS_5X = bpy.app.version >= (5, 0, 0)
+HERO_RGB = (0.85, 0.30, 0.08)     # hazard orange
+DECOY_RGB = (0.30, 0.34, 0.40)    # cold steel
+KEY_ENERGY = 450.0
+FILL_ENERGY = 18.0
+PXW, PXH = 64, 36
+RATIO_MIN = 3.0                   # hero/decoy luminance ratio, linked
+HERO_LUMA_MIN = 0.25              # hero must be well-lit, linked
+RISE_MIN = 0.5                    # decoy luminance rise when unlinked (relative)
+HERO_DRIFT_MAX = 0.05             # hero brightness change across the two renders
+
+
+def sphere(name, rgb, x, coll):
+    me = bpy.data.meshes.new(name)
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=0.5)
+        bm.to_mesh(me)
+    finally:
+        bm.free()
+    for p in me.polygons:
+        p.use_smooth = True
+    mat = bpy.data.materials.new(name + "M")
+    mat.use_nodes = True
+    b = mat.node_tree.nodes["Principled BSDF"]
+    b.inputs["Base Color"].default_value = (*rgb, 1.0)
+    b.inputs["Roughness"].default_value = 0.45
+    me.materials.append(mat)
+    ob = bpy.data.objects.new(name, me)
+    ob.location = (x, 0.0, 0.55)
+    coll.objects.link(ob)
+    return ob
+
+
+def add_light(sc, name, energy, loc, rot, color, size=1.2):
+    ld = bpy.data.lights.new(name, 'AREA')
+    ld.energy = energy
+    ld.size = size
+    ld.color = color
+    ob = bpy.data.objects.new(name, ld)
+    ob.location = loc
+    ob.rotation_euler = tuple(math.radians(a) for a in rot)
+    sc.collection.objects.link(ob)
+    return ob
+
+
+def build_studio(sc):
+    """Hero and decoy on one stage, plus a low fill that lights both."""
+    hero_c = bpy.data.collections.new("HeroColl")
+    decoy_c = bpy.data.collections.new("DecoyColl")
+    hero = sphere("Hero", HERO_RGB, -1.2, hero_c)
+    decoy = sphere("Decoy", DECOY_RGB, 1.2, decoy_c)
+    sc.collection.children.link(hero_c)
+    sc.collection.children.link(decoy_c)
+
+    floor_me = bpy.data.meshes.new("Floor")
+    bm = bmesh.new()
+    try:
+        bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=30.0)
+        bm.to_mesh(floor_me)
+    finally:
+        bm.free()
+    fmat = bpy.data.materials.new("Studio")
+    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)
+    sc.collection.objects.link(floor)
+    wall = bpy.data.objects.new("Wall", floor_me.copy())
+    wall.location = (0.0, 9.0, 0.0)
+    wall.rotation_euler = (math.radians(90), 0.0, 0.0)
+    sc.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)
+    sc.world = world
+
+    key = add_light(sc, "Key", KEY_ENERGY, (-2.2, -2.5, 3.5), (40, 0, -25),
+                    (1.0, 0.92, 0.82), size=1.4)
+    add_light(sc, "Fill", FILL_ENERGY, (3.0, -2.0, 2.2), (60, 0, 35),
+              (0.75, 0.85, 1.0), size=4.0)
+    add_light(sc, "Rim", 120.0, (0.5, 4.5, 4.0), (-55, 0, 175),
+              (0.6, 0.78, 1.0), size=3.0)
+    # the signature warm wedge raking the back wall (docs/VISUAL-STYLE.md)
+    add_light(sc, "Wedge", 250.0, (-2.5, 3.5, 4.2), (-72, 0, 165),
+              (1.0, 0.76, 0.5), size=5.0)
+
+    cam = bpy.data.cameras.new("Cam")
+    cam.lens = 55.0
+    cam_ob = bpy.data.objects.new("Cam", cam)
+    cam_ob.location = (0.0, -6.4, 1.35)
+    cam_ob.rotation_euler = (math.radians(86), 0.0, 0.0)
+    sc.collection.objects.link(cam_ob)
+    sc.camera = cam_ob
+    return hero, decoy, hero_c, key
+
+
+def setup_render(sc, w, h, samples):
+    # Cycles for deterministic tiny CPU samples; EEVEE Next honors linking
+    # too (measured on both supported versions), but its sampling is not
+    # deterministic enough for a luminance gate
+    sc.render.engine = 'CYCLES'
+    sc.cycles.samples = samples
+    sc.cycles.use_denoising = False
+    sc.render.resolution_x = w
+    sc.render.resolution_y = h
+    sc.render.resolution_percentage = 100
+    sc.render.image_settings.file_format = 'PNG'
+    # Standard keeps luminance ratios meaningful (AgX would compress them)
+    sc.view_settings.view_transform = 'Standard'
+
+
+def luma_at(img, fx, fy):
+    w, h = img.size
+    x, y = int(fx * w), int(fy * h)
+    i = (y * w + x) * 4
+    px = img.pixels
+    return 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2]
+
+
+def render_lumas(sc, tmp, name, hero, decoy):
+    from bpy_extras.object_utils import world_to_camera_view
+    path = os.path.join(tmp, name)
+    sc.render.filepath = path
+    bpy.ops.render.render(write_still=True)
+    img = bpy.data.images.load(path)
+    # sample AT each sphere's projected center (framing-independent), taking
+    # the max over a small disc to stay off edges
+    out = []
+    for ob in (hero, decoy):
+        ndc = world_to_camera_view(sc, sc.camera, ob.matrix_world.translation)
+        best = 0.0
+        for dy in (-2, 0, 2):
+            for dx in (-2, 0, 2):
+                best = max(best, luma_at(img, ndc.x + dx / PXW,
+                                         ndc.y + dy / PXH))
+        out.append(best)
+    bpy.data.images.remove(img)
+    return out[0], out[1]
+
+
+def check(sc, key, hero_c, hero, decoy):
+    # contract 0 (RNA guard): the API is on the light OBJECT
+    ld = key.data
+    if hasattr(ld, "light_linking"):
+        print("ERROR: Light datablock gained light_linking — the API moved; "
+              "this example pins the Object-level path", file=sys.stderr)
+        return 3
+    if not hasattr(key, "light_linking"):
+        print("ERROR: light objects lost light_linking", file=sys.stderr)
+        return 4
+
+    # contract 1 (assignment round-trip through the API itself)
+    key.light_linking.receiver_collection = hero_c
+    if key.light_linking.receiver_collection != hero_c:
+        print("ERROR: receiver_collection assignment did not read back",
+              file=sys.stderr)
+        return 5
+
+    setup_render(sc, PXW, PXH, 8)
+    tmp = tempfile.mkdtemp(prefix="light_link_")
+    try:
+        hero_a, decoy_a = render_lumas(sc, tmp, "linked.png", hero, decoy)
+        key.light_linking.receiver_collection = None
+        hero_b, decoy_b = render_lumas(sc, tmp, "unlinked.png", hero, decoy)
+        key.light_linking.receiver_collection = hero_c  # restore for the still
+    finally:
+        shutil.rmtree(tmp, ignore_errors=True)
+
+    # contract 2 (linked): the hero is lit, the decoy is not
+    ratio_a = hero_a / max(decoy_a, 1e-6)
+    if hero_a < HERO_LUMA_MIN or ratio_a < RATIO_MIN:
+        print(f"ERROR: linked state wrong — hero {hero_a:.3f} (min "
+              f"{HERO_LUMA_MIN}), hero/decoy {ratio_a:.2f}x (min {RATIO_MIN}x)",
+              file=sys.stderr)
+        return 6
+
+    # contract 3 (unlinked): the decoy rises materially, the hero holds
+    rise = (decoy_b - decoy_a) / max(decoy_a, 1e-6)
+    hero_drift = abs(hero_b - hero_a) / max(hero_a, 1e-6)
+    if rise < RISE_MIN:
+        print(f"ERROR: decoy rose only {rise:.1%} when unlinked (min "
+              f"{RISE_MIN:.0%}) — the key was never restricted",
+              file=sys.stderr)
+        return 7
+    if hero_drift > HERO_DRIFT_MAX:
+        print(f"ERROR: hero drifted {hero_drift:.1%} across the unlink "
+              f"(max {HERO_DRIFT_MAX:.0%}) — the link is not surgical",
+              file=sys.stderr)
+        return 8
+
+    print(f"linked:   hero={hero_a:.3f} decoy={decoy_a:.3f} "
+          f"ratio={ratio_a:.1f}x (min {RATIO_MIN}x)")
+    print(f"unlinked: hero={hero_b:.3f} decoy={decoy_b:.3f} "
+          f"rise={rise:.0%} (min {RISE_MIN:.0%}) "
+          f"hero_drift={hero_drift:.1%} (max {HERO_DRIFT_MAX:.0%})")
+    return 0
+
+
+def eevee_engine_id():
+    return 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT'
+
+
+def plaque(sc, text, x):
+    pm = bpy.data.materials.new("PlaqueMetal")
+    pm.use_nodes = True
+    pb = pm.node_tree.nodes["Principled BSDF"]
+    pb.inputs["Base Color"].default_value = (0.42, 0.44, 0.48, 1.0)
+    pb.inputs["Metallic"].default_value = 0.2
+    pb.inputs["Roughness"].default_value = 0.6
+    cu = bpy.data.curves.new("Plaque", 'FONT')
+    cu.body = text
+    cu.align_x = 'CENTER'
+    cu.size = 0.20
+    cu.extrude = 0.006
+    ob = bpy.data.objects.new("Plaque", cu)
+    ob.location = (x, -1.05, 0.01)
+    ob.data.materials.append(pm)
+    sc.collection.objects.link(ob)
+
+
+def render_still(sc, path, engine):
+    plaque(sc, "LINKED", -1.2)
+    plaque(sc, "UNLINKED", 1.2)
+    # the gallery still is the linked state, on Cycles for the same
+    # deterministic sampling as the check
+    sc.render.engine = 'CYCLES'
+    sc.cycles.samples = 48
+    sc.cycles.use_denoising = False
+    sc.render.resolution_x = 1280
+    sc.render.resolution_y = 720
+    sc.render.resolution_percentage = 100
+    sc.render.image_settings.file_format = 'PNG'
+    sc.render.filepath = path
+    sc.view_settings.view_transform = 'Standard'
+    bpy.ops.render.render(write_still=True)
+    return os.path.exists(path) and os.path.getsize(path) > 0
+
+
+def main():
+    argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
+    p = argparse.ArgumentParser()
+    p.add_argument("--output", default=None, help="optional: render a still PNG here")
+    p.add_argument("--engine", default="cycles", choices=("cycles",),
+                   help="render engine — pinned to cycles for deterministic samples")
+    args = p.parse_args(argv)
+
+    bpy.ops.wm.read_factory_settings(use_empty=True)
+    sc = bpy.context.scene
+    hero, decoy, hero_c, key = build_studio(sc)
+    code = check(sc, key, hero_c, hero, decoy)
+    if code:
+        return code
+
+    if args.output:
+        if not render_still(sc, os.path.abspath(args.output), args.engine):
+            print("ERROR: render produced no file", file=sys.stderr)
+            return 9
+        print(f"rendered still {args.output}")
+
+    print("light-link-studio OK")
+    return 0
+
+
+if __name__ == "__main__":
+    try:
+        sys.exit(main())
+    except Exception as e:
+        import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1)
+
+
+
+ +
+
+ generated from examples/gallery.json + CC-BY-NC-ND-4.0 + exit 0 +
+
+ + + diff --git a/examples/gallery.json b/examples/gallery.json index 390f636..1f11be3 100644 --- a/examples/gallery.json +++ b/examples/gallery.json @@ -357,6 +357,17 @@ "sequencer", "rendering" ] + }, + { + "name": "light-link-studio", + "dir": "examples/light-link-studio", + "teaches": "One key, one hero: a light linked to a receiver collection lights only the hero, proven by two pixel renders in one pass. Linked: 4.0x luminance ratio; unlinked in the same check: the decoy rises 244% while the hero holds at 0.0% drift.", + "witnessesFix": "The API is obj.light_linking on the light OBJECT (ld.light_linking is an AttributeError). Luminance is sampled at each sphere's projected center; EEVEE Next honors linking too (5.5x on 4.5, 5.9x on 5.1), Cycles pinned for deterministic samples.", + "hero": "docs/gallery/assets/light-link-studio-hero.webp", + "preview": "examples/light-link-studio/preview.webp", + "tags": [ + "rendering" + ] } ] } diff --git a/examples/light-link-studio/README.md b/examples/light-link-studio/README.md new file mode 100644 index 0000000..1bcb78b --- /dev/null +++ b/examples/light-link-studio/README.md @@ -0,0 +1,53 @@ +# Light Link Studio + +A runnable example that stages one key light linked to a hero sphere beside an +identical decoy, renders the studio twice in one pass, and proves the whole +light-linking contract **in pixels** — the feature AI-generated staging code +most often misses or misaddresses. + +**What it witnesses:** the light-linking contract and the traps around it. + +1. **The API is on the light OBJECT, not the Light datablock.** + `ld.light_linking` is an AttributeError on both versions (verified); the + path is `obj.light_linking.receiver_collection` (ObjectLightLinking, with + `blocker_collection` alongside). Assignment reads back through the API. +2. **Linked, the key lights only the receiver collection.** Render one: + hero well-lit, decoy at the fill floor — hero/decoy luminance ratio + **4.0x** (gate ≥ 3x), measured at each sphere's *projected* center so + framing can't move the sample off the subject. +3. **Unlinked (same check), the restriction vanishes surgically.** Render + two: the decoy rises **244–251%** while the hero drifts **0.0%** — the + link restricts without dimming anything else. The shipped check therefore + demonstrates both states, not just one. +4. **The engine note, verified rather than assumed.** Light linking also + works on EEVEE Next — measured 5.5x linked ratio on 4.5.11 EEVEE Next and + 5.9x on 5.1.2 EEVEE, matching Cycles within sampling noise. The check + still pins Cycles for deterministic tiny-sample CPU renders; luminance + gates need `view_transform = 'Standard'` (AgX compresses the ratios). + +**What each check catches on failure:** inverting the link to the decoy +collection (exit 6 — hero drops to 0.179, ratio collapses to 0.22x), an +RNA move of the API onto the Light datablock (exit 3, guarded), a lost +assignment read-back (exit 5), a link that doesn't restrict (exit 7, rise +below 50%), and a non-surgical restriction (exit 8, hero drift above 5%). + +**Version witness:** ObjectLightLinking API and the measured ratios match on +Blender 4.5 LTS and 5.1 (4.0x linked both; rise 244% vs 251% within sampling +noise). + +The render is the contract at a glance: the blazing orange hero over the +LINKED plaque, the cold steel decoy over UNLINKED — one key, one hero. + +## Run + +```bash +# Two-render correctness check (tiny Cycles CPU renders) — the CI check: +blender --background --python light_link_studio.py -- + +# Also render the gallery still (Cycles, deterministic samples): +blender --background --python light_link_studio.py -- --output linked.png +``` + +It exits non-zero on failure (API moved, assignment lost, ratio below gate, +insufficient unlink rise, or hero drift). The `blender-smoke` workflow runs +the check on Blender 4.5 LTS and 5.1. diff --git a/examples/light-link-studio/light_link_studio.py b/examples/light-link-studio/light_link_studio.py new file mode 100644 index 0000000..001a08c --- /dev/null +++ b/examples/light-link-studio/light_link_studio.py @@ -0,0 +1,300 @@ +"""One key, one hero: light linking, proven by pixels — a runnable example. + +Witnesses the light-linking contract AI-generated staging code most often +misses: a light's contribution can be restricted to a receiver collection, +and the API is on the light OBJECT (``obj.light_linking``), not the Light +datablock — `ld.light_linking` is an AttributeError. The check renders the +studio twice in one pass and asserts the whole story in pixels: + +1. Linked: the key lights ONLY the hero — the hero reads bright while the + identical decoy beside it sits at the fill floor. +2. Unlinked (same check): the decoy's brightness rises materially while the + hero's is unchanged within tolerance — the link is surgical, not a dimmer. + +The engine note, verified rather than assumed: light linking also works on +EEVEE Next — measured 5.5x linked ratio on 4.5.11 EEVEE Next and 5.9x on +5.1.2 EEVEE, matching Cycles within sampling noise. The check still pins +Cycles for deterministic tiny-sample CPU renders. The API is stable between +Blender 4.5 LTS and 5.1 (ObjectLightLinking with +receiver_collection/blocker_collection on both). + +By default it runs the two-render correctness check (no gallery still) — the +CI smoke check. Pass --output to also render a still: + + blender --background --python light_link_studio.py -- # check only + blender --background --python light_link_studio.py -- --output l.png # + render +""" +import bpy, bmesh, sys, os, math, argparse, tempfile, shutil + +IS_5X = bpy.app.version >= (5, 0, 0) +HERO_RGB = (0.85, 0.30, 0.08) # hazard orange +DECOY_RGB = (0.30, 0.34, 0.40) # cold steel +KEY_ENERGY = 450.0 +FILL_ENERGY = 18.0 +PXW, PXH = 64, 36 +RATIO_MIN = 3.0 # hero/decoy luminance ratio, linked +HERO_LUMA_MIN = 0.25 # hero must be well-lit, linked +RISE_MIN = 0.5 # decoy luminance rise when unlinked (relative) +HERO_DRIFT_MAX = 0.05 # hero brightness change across the two renders + + +def sphere(name, rgb, x, coll): + me = bpy.data.meshes.new(name) + bm = bmesh.new() + try: + bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=0.5) + bm.to_mesh(me) + finally: + bm.free() + for p in me.polygons: + p.use_smooth = True + mat = bpy.data.materials.new(name + "M") + mat.use_nodes = True + b = mat.node_tree.nodes["Principled BSDF"] + b.inputs["Base Color"].default_value = (*rgb, 1.0) + b.inputs["Roughness"].default_value = 0.45 + me.materials.append(mat) + ob = bpy.data.objects.new(name, me) + ob.location = (x, 0.0, 0.55) + coll.objects.link(ob) + return ob + + +def add_light(sc, name, energy, loc, rot, color, size=1.2): + ld = bpy.data.lights.new(name, 'AREA') + ld.energy = energy + ld.size = size + ld.color = color + ob = bpy.data.objects.new(name, ld) + ob.location = loc + ob.rotation_euler = tuple(math.radians(a) for a in rot) + sc.collection.objects.link(ob) + return ob + + +def build_studio(sc): + """Hero and decoy on one stage, plus a low fill that lights both.""" + hero_c = bpy.data.collections.new("HeroColl") + decoy_c = bpy.data.collections.new("DecoyColl") + hero = sphere("Hero", HERO_RGB, -1.2, hero_c) + decoy = sphere("Decoy", DECOY_RGB, 1.2, decoy_c) + sc.collection.children.link(hero_c) + sc.collection.children.link(decoy_c) + + floor_me = bpy.data.meshes.new("Floor") + bm = bmesh.new() + try: + bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=30.0) + bm.to_mesh(floor_me) + finally: + bm.free() + fmat = bpy.data.materials.new("Studio") + 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) + sc.collection.objects.link(floor) + wall = bpy.data.objects.new("Wall", floor_me.copy()) + wall.location = (0.0, 9.0, 0.0) + wall.rotation_euler = (math.radians(90), 0.0, 0.0) + sc.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) + sc.world = world + + key = add_light(sc, "Key", KEY_ENERGY, (-2.2, -2.5, 3.5), (40, 0, -25), + (1.0, 0.92, 0.82), size=1.4) + add_light(sc, "Fill", FILL_ENERGY, (3.0, -2.0, 2.2), (60, 0, 35), + (0.75, 0.85, 1.0), size=4.0) + add_light(sc, "Rim", 120.0, (0.5, 4.5, 4.0), (-55, 0, 175), + (0.6, 0.78, 1.0), size=3.0) + # the signature warm wedge raking the back wall (docs/VISUAL-STYLE.md) + add_light(sc, "Wedge", 250.0, (-2.5, 3.5, 4.2), (-72, 0, 165), + (1.0, 0.76, 0.5), size=5.0) + + cam = bpy.data.cameras.new("Cam") + cam.lens = 55.0 + cam_ob = bpy.data.objects.new("Cam", cam) + cam_ob.location = (0.0, -6.4, 1.35) + cam_ob.rotation_euler = (math.radians(86), 0.0, 0.0) + sc.collection.objects.link(cam_ob) + sc.camera = cam_ob + return hero, decoy, hero_c, key + + +def setup_render(sc, w, h, samples): + # Cycles for deterministic tiny CPU samples; EEVEE Next honors linking + # too (measured on both supported versions), but its sampling is not + # deterministic enough for a luminance gate + sc.render.engine = 'CYCLES' + sc.cycles.samples = samples + sc.cycles.use_denoising = False + sc.render.resolution_x = w + sc.render.resolution_y = h + sc.render.resolution_percentage = 100 + sc.render.image_settings.file_format = 'PNG' + # Standard keeps luminance ratios meaningful (AgX would compress them) + sc.view_settings.view_transform = 'Standard' + + +def luma_at(img, fx, fy): + w, h = img.size + x, y = int(fx * w), int(fy * h) + i = (y * w + x) * 4 + px = img.pixels + return 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2] + + +def render_lumas(sc, tmp, name, hero, decoy): + from bpy_extras.object_utils import world_to_camera_view + path = os.path.join(tmp, name) + sc.render.filepath = path + bpy.ops.render.render(write_still=True) + img = bpy.data.images.load(path) + # sample AT each sphere's projected center (framing-independent), taking + # the max over a small disc to stay off edges + out = [] + for ob in (hero, decoy): + ndc = world_to_camera_view(sc, sc.camera, ob.matrix_world.translation) + best = 0.0 + for dy in (-2, 0, 2): + for dx in (-2, 0, 2): + best = max(best, luma_at(img, ndc.x + dx / PXW, + ndc.y + dy / PXH)) + out.append(best) + bpy.data.images.remove(img) + return out[0], out[1] + + +def check(sc, key, hero_c, hero, decoy): + # contract 0 (RNA guard): the API is on the light OBJECT + ld = key.data + if hasattr(ld, "light_linking"): + print("ERROR: Light datablock gained light_linking — the API moved; " + "this example pins the Object-level path", file=sys.stderr) + return 3 + if not hasattr(key, "light_linking"): + print("ERROR: light objects lost light_linking", file=sys.stderr) + return 4 + + # contract 1 (assignment round-trip through the API itself) + key.light_linking.receiver_collection = hero_c + if key.light_linking.receiver_collection != hero_c: + print("ERROR: receiver_collection assignment did not read back", + file=sys.stderr) + return 5 + + setup_render(sc, PXW, PXH, 8) + tmp = tempfile.mkdtemp(prefix="light_link_") + try: + hero_a, decoy_a = render_lumas(sc, tmp, "linked.png", hero, decoy) + key.light_linking.receiver_collection = None + hero_b, decoy_b = render_lumas(sc, tmp, "unlinked.png", hero, decoy) + key.light_linking.receiver_collection = hero_c # restore for the still + finally: + shutil.rmtree(tmp, ignore_errors=True) + + # contract 2 (linked): the hero is lit, the decoy is not + ratio_a = hero_a / max(decoy_a, 1e-6) + if hero_a < HERO_LUMA_MIN or ratio_a < RATIO_MIN: + print(f"ERROR: linked state wrong — hero {hero_a:.3f} (min " + f"{HERO_LUMA_MIN}), hero/decoy {ratio_a:.2f}x (min {RATIO_MIN}x)", + file=sys.stderr) + return 6 + + # contract 3 (unlinked): the decoy rises materially, the hero holds + rise = (decoy_b - decoy_a) / max(decoy_a, 1e-6) + hero_drift = abs(hero_b - hero_a) / max(hero_a, 1e-6) + if rise < RISE_MIN: + print(f"ERROR: decoy rose only {rise:.1%} when unlinked (min " + f"{RISE_MIN:.0%}) — the key was never restricted", + file=sys.stderr) + return 7 + if hero_drift > HERO_DRIFT_MAX: + print(f"ERROR: hero drifted {hero_drift:.1%} across the unlink " + f"(max {HERO_DRIFT_MAX:.0%}) — the link is not surgical", + file=sys.stderr) + return 8 + + print(f"linked: hero={hero_a:.3f} decoy={decoy_a:.3f} " + f"ratio={ratio_a:.1f}x (min {RATIO_MIN}x)") + print(f"unlinked: hero={hero_b:.3f} decoy={decoy_b:.3f} " + f"rise={rise:.0%} (min {RISE_MIN:.0%}) " + f"hero_drift={hero_drift:.1%} (max {HERO_DRIFT_MAX:.0%})") + return 0 + + +def eevee_engine_id(): + return 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT' + + +def plaque(sc, text, x): + pm = bpy.data.materials.new("PlaqueMetal") + pm.use_nodes = True + pb = pm.node_tree.nodes["Principled BSDF"] + pb.inputs["Base Color"].default_value = (0.42, 0.44, 0.48, 1.0) + pb.inputs["Metallic"].default_value = 0.2 + pb.inputs["Roughness"].default_value = 0.6 + cu = bpy.data.curves.new("Plaque", 'FONT') + cu.body = text + cu.align_x = 'CENTER' + cu.size = 0.20 + cu.extrude = 0.006 + ob = bpy.data.objects.new("Plaque", cu) + ob.location = (x, -1.05, 0.01) + ob.data.materials.append(pm) + sc.collection.objects.link(ob) + + +def render_still(sc, path, engine): + plaque(sc, "LINKED", -1.2) + plaque(sc, "UNLINKED", 1.2) + # the gallery still is the linked state, on Cycles for the same + # deterministic sampling as the check + sc.render.engine = 'CYCLES' + sc.cycles.samples = 48 + sc.cycles.use_denoising = False + sc.render.resolution_x = 1280 + sc.render.resolution_y = 720 + sc.render.resolution_percentage = 100 + sc.render.image_settings.file_format = 'PNG' + sc.render.filepath = path + sc.view_settings.view_transform = 'Standard' + bpy.ops.render.render(write_still=True) + return os.path.exists(path) and os.path.getsize(path) > 0 + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + p = argparse.ArgumentParser() + p.add_argument("--output", default=None, help="optional: render a still PNG here") + p.add_argument("--engine", default="cycles", choices=("cycles",), + help="render engine — pinned to cycles for deterministic samples") + args = p.parse_args(argv) + + bpy.ops.wm.read_factory_settings(use_empty=True) + sc = bpy.context.scene + hero, decoy, hero_c, key = build_studio(sc) + code = check(sc, key, hero_c, hero, decoy) + if code: + return code + + if args.output: + if not render_still(sc, os.path.abspath(args.output), args.engine): + print("ERROR: render produced no file", file=sys.stderr) + return 9 + print(f"rendered still {args.output}") + + print("light-link-studio OK") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as e: + import traceback; traceback.print_exc(); print(f"FATAL: {e}", file=sys.stderr); sys.exit(1) diff --git a/examples/light-link-studio/preview.webp b/examples/light-link-studio/preview.webp new file mode 100644 index 0000000000000000000000000000000000000000..5c12da8dfe44901007ae0fea6eb9bb199076c55d GIT binary patch literal 13178 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 literal 0 HcmV?d00001
+Light link studio: a blazing orange hero sphere over a LINKED plaque beside a dim steel decoy over UNLINKED on a dark studio floor - one key light restricted to the hero collection + + +### [light-link-studio](examples/light-link-studio/) + +Object light linking proven in pixels: a key linked to the hero's collection +lights only the hero (4.0x luminance ratio at projected centers), and +unlinking in the same check raises the decoy 244% with 0.0% hero drift. The +API is `obj.light_linking` — the Light datablock has none — and EEVEE Next +honors linking too (measured), with Cycles pinned for deterministic samples. +