From 48373150afbf603ece22356dbdd5cfe7aa6dd934 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Wed, 22 Jul 2026 14:52:30 -0400 Subject: [PATCH] feat: add car-mirror-symmetry example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Witness the Mirror + depsgraph contract on a subject whose failure is unmissable: a generic hatchback lofted as one half (14 stations, 9-point rings) and completed by the Mirror modifier. Closed forms assert the datablock keeps only the authored half (126/231/106, 28 centerline verts) while the evaluated mesh is exactly 2n-c (224/434/212), watertight with Euler 2, every evaluated vertex paired at negated X with zero deviation. Wheels and lamps mirror about object origins parked on the symmetry plane — offset the data, never the object. Hide the mirror and the render is literally half a car. Signed-off-by: fOuttaMyPaint --- .cursor-plugin/plugin.json | 1 + .github/workflows/blender-smoke.yml | 13 + README.md | 23 +- ROADMAP.md | 2 +- .../assets/car-mirror-symmetry-hero.webp | Bin 0 -> 12762 bytes docs/gallery/car-mirror-symmetry/index.html | 772 ++++++++++++++++++ .../car-mirror-symmetry-contact-sheet.webp | Bin 0 -> 42636 bytes docs/gallery/index.html | 11 + examples/car-mirror-symmetry/README.md | 68 ++ .../car_mirror_symmetry.py | 526 ++++++++++++ examples/car-mirror-symmetry/preview.webp | Bin 0 -> 11592 bytes examples/gallery.json | 12 + 12 files changed, 1425 insertions(+), 3 deletions(-) create mode 100644 docs/gallery/assets/car-mirror-symmetry-hero.webp create mode 100644 docs/gallery/car-mirror-symmetry/index.html create mode 100644 docs/gallery/contact-sheets/car-mirror-symmetry-contact-sheet.webp create mode 100644 examples/car-mirror-symmetry/README.md create mode 100644 examples/car-mirror-symmetry/car_mirror_symmetry.py create mode 100644 examples/car-mirror-symmetry/preview.webp diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 3bb4cca..21616c1 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -61,6 +61,7 @@ "examples": [ "examples/armature-bend", "examples/bmesh-gear", + "examples/car-mirror-symmetry", "examples/color-attribute-wheel", "examples/collision-hull-proxy", "examples/compositor-glare", diff --git a/.github/workflows/blender-smoke.yml b/.github/workflows/blender-smoke.yml index 3b85ec4..d232c3a 100644 --- a/.github/workflows/blender-smoke.yml +++ b/.github/workflows/blender-smoke.yml @@ -529,3 +529,16 @@ jobs: # failure. xvfb-run -a "$BLENDER" --background \ --python examples/soccer-ball-goldberg/soccer_ball_goldberg.py -- + + - name: Shipped example - car mirror symmetry (Mirror modifier + depsgraph) + run: | + set -euo pipefail + # Check only (no render): a hatchback lofted as one half and + # completed by the Mirror modifier; asserts the datablock keeps the + # half (126/231/106, 28 centerline verts), the evaluated mesh is + # exactly 2n-c (224/434/212), watertight with Euler 2, every + # evaluated vertex has an exact negated-X partner, and wheels/lamps + # mirror about object origins parked on the symmetry plane. Exits + # non-zero on failure. + xvfb-run -a "$BLENDER" --background \ + --python examples/car-mirror-symmetry/car_mirror_symmetry.py -- diff --git a/README.md b/README.md index 5526a24..55d8a34 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

- 12 skills  •  6 rules  •  2 templates  •  17 snippets  •  37 examples + 12 skills  •  6 rules  •  2 templates  •  17 snippets  •  38 examples

@@ -36,7 +36,7 @@ ## Overview -This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 37 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 38 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. @@ -720,6 +720,25 @@ bound by face vertex count, never enumeration order. Sibling to witnesses polyhedral invariants plus per-face-class material binding. The smoothed render inverts on sight if the panel binding does. + + + + +Car mirror symmetry: a red low-poly hatchback on a dark studio floor, light windshield band and white headlamp pair — lofted as one half and completed by the Mirror modifier evaluated through the depsgraph, proving 2n-c counts, exact negated-X partners, a welded watertight centerline, and wheels mirrored about origins on the plane + + + +### [car-mirror-symmetry](examples/car-mirror-symmetry/) + +The Mirror + depsgraph contract: the datablock holds only the authored half +(126/231/106, 28 centerline verts) while the evaluated mesh is the welded +whole — exactly `2n − c` verts, watertight with Euler 2, every vertex paired +at negated X (deviation 0.0). Wheels and lamps mirror about object origins +parked **on** the symmetry plane — offset the data, never the object. +Companion to [`depsgraph-export`](examples/depsgraph-export/) (evaluated-vs- +original) and [`shape-key-blend`](examples/shape-key-blend/) (non-destructive +evaluation). Break the mirror and the render is literally half a car. + diff --git a/ROADMAP.md b/ROADMAP.md index 6a7f5ab..eb8fe4b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -121,7 +121,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo - Freestyle SVG / line-set witness: Freestyle line set on a silhouette (full render pass; prefer Line Art first) - ~~prop-origin-transform witness~~ **SHIPPED** as `examples/prop-origin-transform/` — origin to base center + data-API scale apply + `matrix_parent_inverse` for a bolted accessory; scale `(1,1,1)`, local `min.z==0`, world AABB delta 0, bare jump ~0.43 m, MPI err ~3e-8; stale `matrix_world` until `view_layer.update()`; byte-identical on 4.5.11 and 5.1.2 - ~~Goldberg polyhedron topology witness~~ **SHIPPED** as `examples/soccer-ball-goldberg/` — soccer ball as a truncated icosahedron: bmesh icosphere cut at 1/3 per edge, faces ordered by link-topology walks; closed forms 60/90/32 + Euler 2, census 12×5+20×6, uniform degree 3, edge dev 6.5e-06, planarity 1.9e-06 (independent Newell normal), circumsphere dev 9.0e-06, panels bound by face vertex count (enumeration-order probes exit 13); float32 noise floor ~9e-6 → 3e-5 gates; byte-identical on 4.5.11 and 5.1.2 -- Mirror-modifier symmetry witness: half-model completed by Mirror evaluated through the depsgraph — evaluated-vs-original counts (2n − welded centerline), exact ±X partner for every evaluated vertex, merge-threshold weld proof, failure dramatically visible (half a subject / split seam). Strongest subject: a generic vehicle body +- ~~Mirror-modifier symmetry witness: half-model completed by Mirror evaluated through the depsgraph~~ **SHIPPED** as `examples/car-mirror-symmetry/` — generic hatchback loft (14 stations × 9-point half-rings); datablock keeps the half (126/231/106, 28 centerline), evaluated is exactly 2n−c (224/434/212), watertight Euler 2, ±X partner dev 0.0; wheels/lamps mirror about origins parked on the plane; probes: merge off → 252 (exit 5), centerline off-plane → 27≠28 (exit 4), axis off → 126 half-car (exit 5), applied → exit 3; byte-identical on 4.5.11 and 5.1.2 - Chess-piece lathe witness: a turned piece (rook/pawn) via Screw modifier or bmesh spin from an authored profile — evaluated verts == profile × steps (minus pole merges), ring radii match the profile closed form, axis of revolution exact - Dice-pair per-face witness: material/attribute assignment driven by face normal direction, opposite-faces-sum-to-7 as an independently computed invariant, pip placement from a closed-form lattice - Bicycle-wheel radial witness: spokes via radial Array with object offset — exact angular spacing, spoke count, hub/rim concentricity, every evaluated spoke endpoint on its computed rim coordinate diff --git a/docs/gallery/assets/car-mirror-symmetry-hero.webp b/docs/gallery/assets/car-mirror-symmetry-hero.webp new file mode 100644 index 0000000000000000000000000000000000000000..ac3430e83bfd4f7a69aa7f07fcd9bd30bd7cfd12 GIT binary patch literal 12762 zcmV<0F(uAYNk&G}F#rHpMM6+kP&gpQF#rIt>;attDgXu00zOeFkw&AUp{AjS)Q32p<w5$Kz4p!Uhx!ltuiNi@Ur%55y+Oa>`hR-3`U-T!e(L&S|Nrdw z>A#gfH$QRq%kn<7EK~h&L{1<6X6PmS%v^YbB(V9%!eK$M;P%MILC<5^Di`9{{Kr8 z4L$4R+aMoeXyYcREj8eIX|Du_VE_*;H^Nz`AUNbg7!NG{)XLZ5=Jeb4T7?r|hyZ@3 z*Ma4xybmx>Eln^ywAYS%w&!7lViAMrnqL=UEjllrhDSHOy|a}c!iu>C6`A9R#;gB_ z=Y7m~d zeeSjam)hz_x2ZHxej_l`W~?9b6Br(pJMr$2@KT{)VG(K(eeZWoRJy{ zhxldHpS2-1j!Qd`>9o`}JL-d4800Hf?x6vdHe*OT@M)e$Htbeop)Hgch&jd+i4_SY z4%xzGP)+~&dn|I8eE!bXAQIps_R6H|nu ze)!*lBP7RdpYEI@Z55)hOugC)YrqB!JU~cZ&zC#YFWq$>$kEVZPC^&P9B$Lx;po&m zNfPBZbhk$H3RR;tH&*pA<7`0_j?Mog<4205(R|v4J;WuxUsKXX2@>2hKCT$8A7 z0~p*4J}i;IamG2E)!F)lL{fVSq0Mo`WS*3bYH31cK)(55Xu6NDvD<706w8|4`7?H* za=QVLMe&@?3mY2-v22NR;b9$c&_kAdxAgiHsX?!3lLAr`B6Y`(h&j|XGsGzqU5aWWMK*;KjLs;dQ}m-@c{Mt;z6Eu=`IsB1f++_t zi=2+&3B6_gR^oJH!nNm$>}>~kn%zcojuwhJZqw1k=GG)iOqtY~$gs^+JfC{=OZKs! zKu3n4hZ~gu=fu(6x#=A&ap*N2PD855{Ia#KLUoQufHMQ?(W+U}XE(k~zZ9oW{y=^%^?) zN)fUkd2{Z=f&N(hqu?pjI0Rbh3>wNc=g-P)SRx+aHY3G%qb>=5^yUswlHD;4H+}FW zWbX5HfJzWDT|rSCkoRYjhtzYz{gpIN@TuzkPy<;->q3I0a_nk%#s>H`ElW&H1aW$GkZ?2yD=28h5%nih8M7Z zr&UmlnsG$N03mqHSUslEec1%X{d5iFiBb)Mm0nx6$h z!3Sa<)bQH`pcTN=ro?)~a$#3Kg#iDC-M=>hpf$T(!oWp+AVQ?P8MvC2UvU%3VAAK$rlbRDqN4W=cGgGyU$n^<9wf%*y@Aq{}A1 z|K`=Vr9f~6hm5;sbXslbT-eSNX;_=&XVFXPhKrFGu}_E-1B?6JWSoTCJ`4TsILVyx z=S#56iWF+IV{g%==A!IlbIK9;kuPJ$SH0(p9k9i@QRVp97x^r$XZQ_ILRT;L#_bUR zR_U`m>#R1kp|XqYv38oAFh9*$exYScN(U=PVzv1XH#@zMnYTAVijxCeWc^xq8jafA zKS6Y8M!BYXUW@~d7dH}~!B%=X!5TFc&dda&0c25oMMgsVgM%b>cD!vAbVDS47` zkk^*k+27>}?ft-isXu0C|7?`7jhxVqq&4uHnhAv*sb-i0*Iy2k9FU$81>Gd+Q9sOS zP817VB{8#wux^>?&U>4!|8!I0{2AJ_U8RoTS3dgkNEj%4v{>mLFMDJfetLGotufpG#Zg2k!xW2`y=hjCB`sv2=`}vbX-tIEc zvEGWc=sxXi&Ug;VAj{eHA{_;U9V-nWw2gclgJ2Al1{e#2FZJvK5$UG)OInxE<{ zbQcr7@+r;Md22v8Y@VyOzt{_m=&NNmIcZ^3l1TdEC5%0D%Fg#>H~;4M`w3YpmMlO1 z%J3Qp6U;_l5W9A>9z&2`>&VhoBcrG0IrMlNRhQpoBrks_O$`35JjLW{L?P75rm5rT zy4~^!@NpchQUlenHL#Zk0GZpTZxdYjp);(tD64r7nc(#}P#p?WNzD(r|E`*F=WR@R zGvpNaP*qn^TRUemF07=>-k{m+-1R?LN}u2*XKq_JOov1v zUsX{)zL0?YlJ=6P#KDh>;n zJgDp4a2}vtK*3S(K~lqJ`#WMnbF5yZJ4@`lH3Ss-{FT^HE?I(cF(=(QdEXL1QXLOY znR0QT!y1F1lqNCAG#PeAH-kb=$E4FJX-(nMSnPA4v^*$te@ZZ|DwwYVJm;nsDxIYK z?GqPapJ8+mE6RRx%OT{?RzVYzIx6yb_6Y(IAqR2@z!p%3U70{%@FS2cYW>k38RzBF zb=&!LxQD~*95~UN;kBf@jn>Fsxrq2|2@GiaP~{#<`i!AftEi;^_3@MDm$OPY0x4MW z-gc__5n*&7!Azt|5i!R@Cd$Yc3p+4@PL(Bq!i#b|Q5;rN9Beo(X-be(fQFqxmeHr% zM0Ol>xw$x!>?8>M$9ng`Mkk6eOLZho3)G|op>KbxSFbTJvm{?|*UjA(sm6uuR+m`v zQsp=tjg5TLl8$MvpvA0Mb6a8Ej>KRbe5~oQad8IhKuC~?6bb%YPD!NLA}DEO^SUi# zNM6mnPzm;62;{}d=LXu@f9+=wn>${46PjYfvXB@i?W-y2H+2&li0MObAM+WZZe1=+%;fLL}^j0>MOxYY#gdVh@qh z{<%J!JFw6K*#tcfJa$^5ogvX>BStyNy?FZ$%fd+aEZBJd3b|#0%g{!x4V(`j-|6qbLiDjWc%AhS z(9PM_^GZ^}uSr=|D8K~kd}j&f6st!Oipss?zjt?k?S{E#7^4Z*W$CB9EMk9uL(USb z%u6Y3AH=0`O7?sXffA5W+YZmPx$_Er@f>j~e0#Vk5VM@C?OScFg`*}Lf6QFdAzB13g4 zF{OCHf^S{n0V?>49#e#3auNgTs)m6uBdB}fcLz>$Ek*mgz@C@=DKaH{I@H3H<%$Kz zfLxyCm@)2-l5hCe2TV+0syD>DI6sFh+izo2vi_A^EK*j%jb~K|>M@g}W>Mpt5L2Qd;PIa*yRE{CGtU(;U3awlQ<<$SE%~u*nRl zYP;aakbr;x%p&jld=}H5o=1k_t&#sx{PlVYK}cgx5ebbT>bV7IAzp+0-xbS5oWQ56 z)Pa*AUEw2S3whZE#*5nCb8V0XI-piu&U^wBU86&%)Nty9lil?cFzIh>m-{Z$6o9V< z1N?YtZ|{)LSJs7S5{Vv|bK>mh9 zM`LPKEEmpGn?SB2#X}TdUX~QZ=%Ok&D*129FPGy4yo`2yo9yvIQ1{07tNDadwD zrg9~BEMOIu{dcJJ$ZTl&XS?98SRQbPpaA~$G$+&0ewhI3wZErXdpZ~_rfg3*4cbn6 zX#3tVqF6MN;=C&QYAicUTu@LE0BMCi$EVlysUv=?C0iKtOaMo%O}*dI!Ll;aG(-VkGSFIL^n4n0~)@rpV7d3*#HcBf_s+wl+!vR$EN5N=gVy?0zD=615;fJK%;2b+8QFhUp`63sz7gMmB@2IR}#0ozMrD&o#~K zx40D>la2A6u-YVgcKCf?&R8G-016DgJxAc5yn}&6Tyy%78>L}2z^(MJz(Yy-0 zSr25|wqE*g#x{3Z^pJWYYj2b%R#gKViU*={nbMhVHxfk!mkC`TaaQ?2k$jL#vH;CV zI45<2=nG`$wl*R*nJx=yyqBwGS5IV_tXaf>Gx1GHyfWxHzCgO%F^ALw{ZoKg``HrX zIHLiQQZ7NTnH?oOphqAh@Xa}reM950YV_G>6ZK)%2t4vE&h8j(+QP?VI3j<9DWtm4 zrPt!bVKh>UpqR-WK6H&1h~4*a(2&GpTc1!ec@iz9bs3DEsT_H7#;E3d7TE| zq0r4V`3*NNo6G0u`jp)Ck~6vD2&>;$<+Gc}gJwUVpBo1;m|A~u9>_FhRY>FnuNw!} zQhj7XGx>?k#_aqrW*?YP{(M^@*wOIL2Ns@@(@1Aha6G+Hp=*I_(P0fz-@c^{E70@+ zIw8OSE{yXnk1ze5^lCL13QNi}Z!AXI(W8h<8~_AYs2FljER%>m1vfI0WgY5h!HkmE zq!mHh9C$ik8@=-Bu<~oQ=SSY$U<-0+Ng@dD(%O;+Vu*Dx5R6zHT-gh0tC0%@lGy7_ z)>xUsXuicW1#0KgRE7;cu>=D4;7DrVHe1(c%-7^!*+666H|(dR%jr6dPVFD`)d60Guy@v#_!e)l$CrdrUjXw4$n>-Z*998o>q1?yTlq3kXzDC=ag zq4vRY7MM-0>T>#bK!zdwq+z2+B_T+|%?^nHA5zQ`%3@GdgzQX=0f%Kfw$4$Ph&fIo z9e^d7W_}+Z-hk^okiN6=`8F>h;r*X}RcQjqWxh&whTRMzLIY#iflY)IjoroGHTIr(TV0CUon8NK?6^-Y}F0*3hKPzwQ5`-o4 zutsicX@eTreK?0aX0(;DvgKBtHFvaY(-+#O-E0syOFZRSkIWk=f`-gTr-H7pNfqg` z(CGyWk2K1d!4Uyn_dp1neIQ*v;Dj0Q!o>tFvgVLHLhXsJs@8(A44wh@{_&R9Od*rU z_11Bs7%DQ`0;ix>974BAoK|GCA@)1JcJJDD4h(7SX9L4hICB=pz>P?PQ^N9s7woyd zl&;(jRQ#buA%)J#`J0;5ZCY~K+R~J%V_#(C@yUMVVbOeNt2$N3!@rGf@-AN&RL1o% zb}qmdDCIQ%T9a{`uH{}AIz4tRgS0UbpSz<%YFnMd`@ zEkLmNQf^M5jYtYHiI-$j#(56mU{{IthJG0FwEND0o;0!K>OXC;n4k^oQNf8 z1D#a6zXSCIAnl+>i5}rahj*9D~+YEh>@Yz{hc` zo3-887n9S#mKS*5&K6>kjubY-(=R~?-xzzmH5=>IksCor#R6K~8b!oDf%+f2F%AfB zceX2+PbdHc37sNI!L@3X#5jV{A+sh1k3>R>3*C1WGlxyaR+2v#2xRS&w#g=U=9`}6 z_s}MK032VNjxSqVfqALR0mV%(q!fip8X0K!^YTsmdbqRLC#LblIGtZ8CWdF1GIVwz zRJv{3dji`PW*7FbgQ&8lCyT+=*=d^F*{7OZ3UNn-1I56;Yi&q zJs25lY(F9S*}k{LJ!0d>d+fjAWc}fb_rj&X$Nk&*EWVc>oI+NHgaTf?G8{=n^tJf+ zeKSCc;QPYtqbrV4aTX?y?t`2sZVX{KPB-QezuD0!rlT8iylC1MG+NHu}^{e|Ky)R*Df4 zI(bJ=^zE2xXyG!j;Hl1zW8!bX5tZZ53}k9LutAd_PlUagZG*+=>!86^Cb?IH)t#}! zfvCgLV+QuS-YjzGsruex`k|K-;5Yt1^ogTn1Ks{(+#D4v&94-LN=-*V;zB4D`{+kF zeEAp@VTR}mb;5fD^nWnp-*IfcaqW!Sq0GH8bl_$E0Jp@%kweobuq`8Gkd%$qbEbqy zTAzCQaWl+$dE2m|4Wi<`W%uM*2cB5?XosKey-VNz)R(=svnXKlelIbiBqmP{$&&;Q z)lH=5Dq33BPXb^*>~GHv=qhgyNLtJR4jy+^IaD2Rxa!;F$#F&~zyY=RO#_4ai<=I3 zOvWB3 zK<<)#zwIN42Wnc~bJq$D7e%~{z{*s9k8en)BzkCQJSJ)KDor0d?%OS^_lp_0E}2+( z&iea&{tK~_up!$4wwvjdHwLsgj`r0dx3&0zR7Y4XoIL4gBtQJU3-}B@%>R2YP2QMwl3mb!%%F?IepK8NqHSX}}WX*0SA>B~K)=*Yyb!GV?ojE#XD4 z#M{DdsIP@k+EhtrxF<#cPIbZIIq_}=jmYk9FG&4P2_g^+F?eZ}a3}tOL3x0>(!`j% zrm<%|XKDX2`~R;wtkE6-pF5oX{o{VwhO*-|somIH4u5~Pt79CM9_GcaN{s)WwD98< zCOa)NQT7zbm7kOYSEV_cR#fqvT>=67@dn#EmWFOw>|!^&n9|Bz$F>5nO!`89MJJ!L z(E*d-q$iAKl=PCgSit&yZL;Uxa|4WwB}e^(!c;<4?T z9Qz@VxFdYzX!qJMB| zs-5R_)eP`N)MBFzKDBPr^n&y}r}CD~rf_y?MR>`uAuk=ZD;FDu3p=-nQwtMqbY+h5rErp3AkfNjx%#02eu2&04BLP z&|XLurgn8RHgOxIpfXD*Zb@j(xHY3>QiwIHwt-7<6KU=}-!Um%xjEllEmdrxADde;_S7T&%v$K)@4RJ2uvV44&TS8%A| zX^8Tppp{oW&CR{kmS*}APw54oz8Z8+_3%*H-@rQ;nMTG@E^+KhWdxugJo%UGqPYkJsccZB;oO9}#2S6K%l{Pf-N5;ZvZYi9 zvbE`&q8jGGW>X!d-BQ3~+XRXrc~+hM#W&sjtVTPQ^Fgjr*6ATw___Q`i=6njpJvw( z9LHnd5M6_RU8_%X!RtIjRS*jkj2OgFvI%E%>VSqyXTvi;tHTdv&h2HUlpDT_3kZBF zzWU^Fo3`}d$ZG|4;2ne(8dU2^yKhS^`P+!Sjp#W0uozpmQJ8Ra%_^c*Xbj|juuiCi zV{t7&=0*8QdSw&BAd`rPmn8a7)jxk2OROs*|gw=NaM^=z2kk#X{soR>y^ zv0yL&Us29QpqI8CE;He_(3B=3>zt^*?S&1QQ9l!S7o3)6)7Cl?3}+9>Qkp4>5yX-^ zM5wnb%LJhH>M4WFL2EovrsCv3|%S0ERJF%(=FhC~~|P>$L9(>1Kc4 z-dfI&=Ei+)iZir)CCuqzb@1bTjYA}_SdbZGC|JCjOZsAYbdwn)+YkmpOdrMvnPrFg z>g7|^H_p@R^t-6etWC!v=}ux+`u-Sagp_W1fZ{<8?2RU6+|=4S^xmdlLujIycWH^f ztl@=QG_A-=R94Gb#S9MtJB_XV^z)f{aXnK2Y4+T>So@XmAAhI1*x)Uds@#-Alr;U< z7KDO#Ow$8MB)Y)ADT%Ee%#PBQNg>@o6`}XAf>odD@_3K5gfdONo-iXDmQ~$(`*pmp z!w|g^QBySiSYTh6rOGAvOBXpmN(Vf)1(Hyn7?pz0iS3OrTXAQ<=KgpV%W#@r!V^!Z zPPLTQm5G`fA6q%!mxwpuoBRLPM1VHSUH|y|o%rI=<-d#VMHc0H5)jnst0OyAp>G?Hjo2m*wQB<=)RfyHj8BpAEVRidll9X8ETAu_MWcy$lOfj8N`lCLCs z$zo8L5R_c14M5!(ko$zK&Jp94IGi)s>#GiJ+s~?fyKKDp9l_gN;QW`OWRitWK=1Gg{GvGw}zcl zk?F8JH~9WXsgI8}`~y8E-oIR5Sea`+ z_b%YNe37a$5mLC?T%08o0%OdKbD9bj@Et~c_SImFK{`gp9)JNenBOD>*U+B|pXn|+ zijz^?PFA>ZeF$YrW4Ko$TK9W@D(C5o2`XYOY~QQfH8E8MtR)nz(qQY5TFvdkh;iZ4 zjIX?p#e}h%hNI%+0XU2)yx?>yxj3CwCs+JXR;7CdpL}atSn@uE{^Pp^Z>Ooox<1Ag zE4dE)jVKNQfTvBR5{p2<67X3PeHSGY%07Lp6Mz1!KQ8SENT$Uqtlbf*p*<>59MYTL9#gTwV`YmCMDYr2GLZ#vy$# zQPrcXzYV&3B23l=?D;}T{0m7fULHp+m%Hsi*H?VF3}g$)zb-0RVRr*Dfr!+hX@$*H zasY`MFhYrur!TdUEE2yazM2w^2VQkL@|*4S_zwrNqR+N2({K1J4<=-l2rhUUO#a25 zU=1ek4g0{Hl8^YesEb+qUl|dgNbm8MQY#C?UfZ@Ki&=^|x=Bo})eflWhxGJd^#v_K z(iCt(kCboKi5;{{KKc$G2YNizNn=5iUWm{L|*!r}@hjMa$UN#v=M4$41uZNdw zL$#W4Kg?IIhw#>XTv=0$avB~?HsmkajNH91mE6pJ z%$aHM?{pNsU&sYRYK_Rp%NFflIzuIA)cPPVi80p>5CTMA0E8m2T zyHlovz#U67iB#c}lYs4u7ZFzJ_+xZ;GXu*4V$xp!JrTLiv5OOKALO+*gl}$| zI4V8}%@HssAr>TJ$$DfBJjoM$BSIdiDzwUZwAQzT#J33Ae3cXnPCJ>tS5$wqw{s)4 zI4vM92my2Cu$qwOi&FzuB%|Bzu9kS6t@|+y6-gDAlYR;J4K!CF?&p;KVTUM23ftXA zF6&)xJ}{$I)qbzTLf;|0k@uRpb6;(2FkPy|Kg7Yd7Ce9unlN9rx%kE{ln+nP8<|zj zez3?}@genPs*o|i0K@uP`Z^LI1#9kyV2$@t2QAjvWxEzQ#k7XRWc`1rFNZarcK?8r zuVfwXvQ9}xd;M_*C`Ea5ITk|Z2?|prH}J34A9546**Fqm*WZQ|{e~CGD=$^>M3Q#1yfQZ@5Wp*Ldp7~YbE+_1z#cz*LWnbq4 zEFr0rSKMo83eL+TKuAUml-Xr7&+PykKNnXsI{HP-4$yx)6LXR*?;y4w2=}{nS<`>u zF(pV1T#>+ZHn}W-bHIzc@4#C&+mx!oC3H}}ZT@$Vipt1MxHM$B3x7IHFn(GAxY`ttL6(DFM$5xwgNg?k*%0 z-{^v1Xvv64I3X@;ew8wPL(l*^4$}LRaod#svCJ-a4Q(6WRwqv5lhAj;RpTy_R}lli zmeBn{me0AU%(4IZ5&wmWj}f$U+^d*zLa_p5nu9J)O?irGWzO>#C|(N8gC`KUTwKy;F&>wHmqDrHC`YN#FvNQlR?BSI|rJ8}u;Y8FuNYw0kicjiy^ zI=OU?d){=7=;%*6lVVPcZ%gn&9}lA9^95mC(aE4th;PmQ<1s@fBPhEF-u69#Hd%dDnh2px2{LWiF6$g!fb=d( zo(75)`O>9vDSd1eX6i)^ z=ipfdLai-}&349&j$#m1XWd3z;2~5tc}}3Hay!g0!OJo|crSLSi}c6u=RifzwdjZX zHBWR>!&o|s=2k*~Uq8X{u8j9?*#wKp>`}sgR*kQ;#Tho70bV42-ZJ;rW#^1NkP{@* zxYsgjp&0P~e4EQ9%xY@C5F_`$v~4i4%XjBX-lc8JBF-YaQn z5osPMB5!2u8GWye@5W@;h7zkAKPG!~UFKHtWeqhIxCMwuCKW0uZ~#{q9{9$d!X%tR zGm^i)e%TxS2O`V*}knlXsus(3 z7~h_9B(4xTV|A|{S)fM=(fm>a!p(Sh32o)0Xn1iZ*&HYMzIUD+4gsf;p&g^P{SWfH zzE|ajw6fJ0b!DyCRx9_{0K0E?`bH*_4^Iiv7A7@yrd_cg(>!2i9-ks{r2k%mix{oI z)&kFxb)LQIctJ;#h!nGbH^b6TZz+T4M*DM?K4T{?>ggx92yX`C`Ngr7zlLsj7Wkn2 zXRk~ywLm6w?;ET%i)BS+^C-duUM23IUlA+$sh=pdH)ph2&?JNh1nMjeF`F+hBjnSw z#j>=j$(182_ArqCGdsOPf+dY4u_jMllE*U-z6&~>CiomGZ;GO=KRG@qLvzO82FBz= zyz&Oeo%GsXV_J@yjLU9}h>D$e}nm z%qjNmFS4~(aY!;&cS=I3po1puqpZ0cXMNCuI08JDncnEL+PdMh*anFQW{)8S@vpI? z0Q9Ox)d`E@E&N9!Tl|=zpJ^bniNg*4Hfv!&Tu;R~ss3&t4onxkn{LyPR3F8`TdKZMD1*X<1iv*t63Fk z6Y|AZ`(++K=>saR2!HzXLx2DY0l8K2I0Y<=Ld}+B)r3qQz-AEZalkkdDCddJy_S*h zBO>%d@-vCpPw?IW#gVOgq|fL88_}Im=ShU*$qG$W$Z+Jjp*K9GlO!N7Ne^25fR1*H z)9z$NBc&|3qCw9t&H&3N*hHKNfoO`!QE3y8%<<1KyGO5|E9OnCDuI9$9s3U%-d@

mdLWAe!q?v5X&|BVlHEAlJ;ZJGKF*XXtrdF!aMrWJP-uR2+Ne?;Wchc6%%7LH9#2 zr|nm1a)eh{abghLCkf);|BnIJHYFR6KmfX|kZ+2QATr@R1PRA?*&CdxP>DfJJ0i1c zYL7wExEoFh+R?om234lQ)z`)94GtHJ#WszsjM#zYo({s?)W6UGS|9`xd&pbdS~+E$ zURO`-2%@M-W7-8)pl9Q)-r#C5LzQE)^U-^m8y<3}F~T>NApyxsTzThpeNVLj06r@6 z1!Ss{NK3$j<<54_uAf4%jUX%bbgz|WQbniJZ<5O9QFTI)aLN73NyIMflrtgE8z$Fo`2M00nXY02;DJNl<_Q9l{Hv zFXV_aSKv=W)G2rUm4b5f;Q^B_eM@y(pq@Qn&;S4c04@c7RLm6!00CMxN7+Hp=i#RW g*j@#F2rpqr!NSl5@@0dZKl-pD0DC|H1jhgX0J>kRYybcN literal 0 HcmV?d00001 diff --git a/docs/gallery/car-mirror-symmetry/index.html b/docs/gallery/car-mirror-symmetry/index.html new file mode 100644 index 0000000..d41ceea --- /dev/null +++ b/docs/gallery/car-mirror-symmetry/index.html @@ -0,0 +1,772 @@ + + + + + + car-mirror-symmetry — Examples — Blender Developer Tools + + + + + + + + + + + + + + + + + +

+
+

car-mirror-symmetry

+

A generic hatchback lofted as one half (14 stations, 9-point rings) and completed by the Mirror modifier, evaluated through the depsgraph. Wheels and lamps mirror about object origins parked on the symmetry plane.

+
+
+ +

Rendered headless by the example itself — click to zoom.

+
witnesses Mirror is a modifier, not applied data: the original keeps the authored half while the evaluated mesh is the welded, watertight, exactly symmetric whole.
+
+
blender --background --python examples/car-mirror-symmetry/car_mirror_symmetry.py --
+ +
+
+

A runnable example that builds a generic hatchback as one half — a loft of 14 stations, each a 9-point half-ring from the bottom centerline out to the roof centerline — and completes it with a Mirror modifier, evaluated through the depsgraph, following depsgraph-and-evaluated-data. Wheels and lamps are separate objects mirrored the idiomatic way: the object origin sits on the symmetry plane and the mesh data is offset — mirror mirrors about the object's own origin, so you offset the data, never the object.

+

What it witnesses: the original datablock keeps only the authored half while the depsgraph carries the mirrored whole, and both directions are closed forms:

+
  • Original holds the half. Body datablock is exactly 126 verts / 231 edges / 106 faces (14 × 9, loft closed forms), with exactly 28 centerline verts (probe: modifier applied into data → exit 3, datablock reads (224, 434, 212)).
  • Evaluated is the welded whole. Exactly 2n − c = 224 verts (probe: use_mirror_merge = False → exit 5, 252 verts — the doubled seam; probe: one centerline vert pulled off the plane → exit 4, 27 ≠ 28 welded; probe: mirror axis off → exit 5, 126 — the half-car). The evaluated shell is watertight (every edge borders 2 faces) with Euler characteristic 2 — the two halves weld into a topological sphere.
  • Exact ±X partners. Every evaluated vertex has a partner at negated X: measured deviation 0.000e+00 (mirror copies exactly; tol 2e-5 guards float32), evaluated bbox symmetric (min.x == −max.x).
  • Mirrored parts. Wheels (96/81 half → 192/162 evaluated) and lamps (8/6 → 16/12) each double across the plane with partner deviation 0.0 — and each object origin reads x == 0.
+

Why the checks target the modifier, not the base: mutating a non-plane base vertex cannot break the ±X pairing — the evaluated set is always half ∪ mirror(half), symmetric by construction. Realistic failures live in the modifier (merge off, axis off, modifier applied into data, origin off the plane), which is what the probes break.

+

Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2. Mirror, evaluated_get / to_mesh / to_mesh_clear (no argument — passing the mesh raises TypeError on both), and TRACK_TO constraint behavior are stable across the pair; only the EEVEE engine id is version-gated.

+

The render is the proof: hide the mirror and the still is literally half a car — halved windshield and hood at the centerline, one headlamp. Render notes: the loft is faceted by design (flat shading, no bevel modifier — the modifier stack is Mirror only, so counts stay closed-form); the window band is glass by construction class (steepest roof-rise slope is the windshield, steepest drop the rear window, ring segment 5 the side windows), and the glass is dielectric — metallic glass mirrors the key light and renders the windshield as a hot salmon slab.

+

Run

+
# Cheap correctness check (no render) — the CI check:
+blender --background --python car_mirror_symmetry.py --
+
+# Also render a still (EEVEE on a GPU host; use --engine cycles on GPU-less hosts):
+blender --background --python car_mirror_symmetry.py -- --output car.png
+blender --background --python car_mirror_symmetry.py -- --output car.png --engine cycles
+

It exits non-zero on failure (applied mirror, doubled centerline, unwelded seam, broken symmetry, or a mirrored part off its plane origin). The blender-smoke workflow runs the check on Blender 4.5 LTS and 5.1.

+
+
+

Source

+
+ examples/car-mirror-symmetry/car_mirror_symmetry.py + View on GitHub → +
+
"""A generic hatchback built as one half and completed by the Mirror modifier
+— a runnable example.
+
+Witnesses the Mirror + depsgraph contract from depsgraph-and-evaluated-data:
+the original datablock keeps only the authored half, while the depsgraph
+carries the mirrored whole. Closed forms: evaluated vertex count is exactly
+2n - c (c = welded centerline verts), every evaluated vertex has an exact
+partner at negated X, the merge threshold actually welds (no doubled
+centerline), the evaluated shell is watertight with Euler characteristic 2,
+and the wheels mirror about their object origins sitting ON the symmetry
+plane. Failure is dramatically visible: a car with one side missing.
+
+By default it runs only the correctness check (no render) — the CI smoke
+check. Pass --output to also render a still:
+
+    blender --background --python car_mirror_symmetry.py --                 # check only
+    blender --background --python car_mirror_symmetry.py -- --output c.png  # + render
+"""
+import bpy, bmesh, sys, os, math, argparse
+
+# --- closed-form construction parameters -----------------------------------
+# 14 loft stations along Y (front at -Y), each a 9-point half-ring from the
+# bottom centerline out and up to the roof centerline. Fields:
+# (y, w_side, z_floor, z_sill, z_side, w_glass, z_glass_base, w_roof, z_roof)
+STATIONS = [
+    (-1.95, 0.74, 0.34, 0.36, 0.52, 0.05, 0.62, 0.04, 0.64),  # nose tip
+    (-1.80, 0.83, 0.30, 0.32, 0.55, 0.08, 0.70, 0.07, 0.73),  # front bumper
+    (-1.65, 0.86, 0.28, 0.28, 0.54, 0.10, 0.72, 0.09, 0.74),
+    (-1.30, 0.87, 0.28, 0.62, 0.66, 0.11, 0.74, 0.10, 0.76),  # front arch peak
+    (-0.95, 0.86, 0.28, 0.28, 0.54, 0.12, 0.78, 0.11, 0.80),
+    (-0.60, 0.85, 0.28, 0.28, 0.55, 0.38, 0.90, 0.34, 0.95),  # hood -> cowl
+    (-0.30, 0.84, 0.28, 0.28, 0.56, 0.54, 1.02, 0.50, 1.30),  # windshield
+    ( 0.20, 0.83, 0.28, 0.28, 0.57, 0.60, 1.05, 0.56, 1.45),  # roof front
+    ( 0.70, 0.83, 0.28, 0.28, 0.57, 0.60, 1.05, 0.55, 1.44),  # roof rear
+    ( 0.95, 0.84, 0.28, 0.28, 0.56, 0.58, 1.04, 0.52, 1.38),
+    ( 1.30, 0.85, 0.28, 0.62, 0.68, 0.50, 1.00, 0.44, 1.24),  # rear arch peak
+    ( 1.65, 0.84, 0.28, 0.28, 0.55, 0.34, 0.95, 0.28, 1.10),  # hatch
+    ( 1.90, 0.80, 0.30, 0.32, 0.53, 0.12, 0.86, 0.11, 0.92),  # tail
+    ( 2.00, 0.72, 0.34, 0.36, 0.50, 0.05, 0.78, 0.04, 0.78),  # rear bumper
+]
+RING = 9                       # points per half-ring, p0 and p8 on the centerline
+WHEEL_SEG = 16
+WHEEL_RINGS = [                # (x, radius) profile: bead, tread, sidewall, rim, cap
+    (0.63, 0.30), (0.67, 0.33), (0.83, 0.33), (0.87, 0.30), (0.89, 0.19), (0.90, 0.15),
+]
+WHEEL_Y = (-1.3, 1.3)          # front / rear axle positions
+WHEEL_Z = 0.33                 # axle height == tire radius: wheels rest on z=0
+
+N_BODY = len(STATIONS) * RING                    # 126
+F_BODY = (len(STATIONS) - 1) * (RING - 1) + 2    # 104 quads + 2 caps
+E_BODY = (len(STATIONS) * (RING - 1)             # ring edges
+          + (len(STATIONS) - 1) * RING           # longitudinal edges
+          + 2)                                   # cap closing edges
+CENTERLINE = len(STATIONS) * 2                   # p0 + p8 per station: 28
+N_WHEEL = len(WHEEL_RINGS) * WHEEL_SEG           # 96
+F_WHEEL = (len(WHEEL_RINGS) - 1) * WHEEL_SEG + 1 # strips + hubcap ngon
+
+MERGE_THRESHOLD = 1.0e-3
+TOL_SYMM = 2.0e-5    # evaluated partner deviation (float32 storage; mirror copies exact)
+TOL_BBOX = 1.0e-5    # |min.x + max.x| on the evaluated body
+TOL_PLANE = 1.0e-6   # |x| this small counts as on the symmetry plane
+
+
+def half_ring(st):
+    """One 9-point half cross-section, bottom centerline -> roof centerline."""
+    y, w, zf, zsill, zside, wg, zgb, wr, zroof = st
+    return [
+        (0.0, y, zf),                 # p0 bottom centerline
+        (0.55 * w, y, zf - 0.02),     # p1 underbody
+        (0.95 * w, y, zsill),         # p2 sill (rises over wheel arches)
+        (w, y, zside),                # p3 lower door (widest)
+        (0.99 * w, y, zside + 0.10),  # p4 shoulder
+        (wg, y, zgb),                 # p5 greenhouse base
+        (wr, y, zroof),               # p6 roof edge
+        (0.55 * wr, y, zroof + 0.015),# p7 roof crown
+        (0.0, y, zroof),              # p8 top centerline
+    ]
+
+
+def _newell(pts):
+    nx = ny = nz = 0.0
+    for i, p in enumerate(pts):
+        q = pts[(i + 1) % len(pts)]
+        nx += (p[1] - q[1]) * (p[2] + q[2])
+        ny += (p[2] - q[2]) * (p[0] + q[0])
+        nz += (p[0] - q[0]) * (p[1] + q[1])
+    n = math.sqrt(nx * nx + ny * ny + nz * nz)
+    return (nx / n, ny / n, nz / n)
+
+
+def build_car():
+    bpy.ops.wm.read_factory_settings(use_empty=True)
+
+    # -- half body loft ------------------------------------------------------
+    rings = [half_ring(st) for st in STATIONS]
+    me = bpy.data.meshes.new("CarBodyHalf")
+    bm = bmesh.new()
+    try:
+        bv = [[bm.verts.new(co) for co in ring] for ring in rings]
+        faces = []
+        for i in range(len(STATIONS) - 1):
+            for k in range(RING - 1):
+                faces.append(bm.faces.new(
+                    (bv[i][k], bv[i][k + 1], bv[i + 1][k + 1], bv[i + 1][k])))
+        caps = [bm.faces.new(bv[0]), bm.faces.new(bv[-1])]
+        # winding: the probe side quad must face +X; flip everything if the
+        # loft convention came out inward (mirror does not fix winding)
+        probe = [v.co for v in (bv[8][3], bv[8][4], bv[9][4], bv[9][3])]
+        if _newell(probe)[0] < 0:
+            for f in faces + caps:
+                f.normal_flip()
+        # caps must point away from the body (front -Y, rear +Y)
+        if _newell([v.co for v in bv[0]])[1] > 0:
+            caps[0].normal_flip()
+        if _newell([v.co for v in bv[-1]])[1] < 0:
+            caps[1].normal_flip()
+        bm.to_mesh(me)
+    finally:
+        bm.free()  # the ownership contract from always-free-bmesh
+
+    body = bpy.data.objects.new("CarBody", me)
+    bpy.context.collection.objects.link(body)
+    mirror = body.modifiers.new("MirrorHalf", 'MIRROR')
+    mirror.use_axis[0] = True
+    mirror.use_clip = True            # centerline verts cannot leave x=0
+    mirror.use_mirror_merge = True    # weld the two halves shut
+    mirror.merge_threshold = MERGE_THRESHOLD
+
+    # -- wheels: own Mirror each, object origins ON the symmetry plane -------
+    wheels = []
+    for y in WHEEL_Y:
+        wme = bpy.data.meshes.new("WheelHalf")
+        bm = bmesh.new()
+        try:
+            wr = []
+            for x, r in WHEEL_RINGS:
+                wr.append([bm.verts.new(
+                    (x, r * math.cos(2.0 * math.pi * s / WHEEL_SEG),
+                     r * math.sin(2.0 * math.pi * s / WHEEL_SEG)))
+                    for s in range(WHEEL_SEG)])
+            for j in range(len(WHEEL_RINGS) - 1):
+                for s in range(WHEEL_SEG):
+                    bm.faces.new((wr[j][s], wr[j][(s + 1) % WHEEL_SEG],
+                                  wr[j + 1][(s + 1) % WHEEL_SEG], wr[j + 1][s]))
+            bm.faces.new(wr[-1])  # hubcap ngon
+            bm.to_mesh(wme)
+        finally:
+            bm.free()
+        wheel = bpy.data.objects.new("WheelFront" if y < 0 else "WheelRear", wme)
+        wheel.location = (0.0, y, WHEEL_Z)  # origin on the plane: mirror mirrors DATA
+        bpy.context.collection.objects.link(wheel)
+        wm = wheel.modifiers.new("MirrorHalf", 'MIRROR')
+        wm.use_axis[0] = True
+        wm.use_mirror_merge = True
+        wm.merge_threshold = MERGE_THRESHOLD
+        wheels.append(wheel)
+
+    # materials exist in both modes (slot layout is part of the scene contract)
+    for name in ("Paint", "Glass", "Trim", "Tire", "Hubcap", "Headlamp", "Taillamp"):
+        bpy.data.materials.new(name)
+    for mat_name in ("Paint", "Glass", "Trim"):
+        body.data.materials.append(bpy.data.materials[mat_name])
+    for w in wheels:
+        for mat_name in ("Tire", "Hubcap"):
+            w.data.materials.append(bpy.data.materials[mat_name])
+
+    # -- lamps: small mirrored boxes, same origin-on-plane idiom as wheels ---
+    def lamp(name, x0, x1, y0, y1, z0, z1):
+        lme = bpy.data.meshes.new(name + "Half")
+        bm = bmesh.new()
+        try:
+            bmesh.ops.create_cube(bm, size=1.0)
+            bmesh.ops.scale(bm, vec=(x1 - x0, y1 - y0, z1 - z0), verts=bm.verts)
+            bmesh.ops.translate(bm, vec=((x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2),
+                                verts=bm.verts)
+            bm.to_mesh(lme)
+        finally:
+            bm.free()
+        ob = bpy.data.objects.new(name, lme)
+        bpy.context.collection.objects.link(ob)  # origin at world origin: on the plane
+        lm = ob.modifiers.new("MirrorHalf", 'MIRROR')
+        lm.use_axis[0] = True
+        lm.use_mirror_merge = True
+        lm.merge_threshold = MERGE_THRESHOLD
+        ob.data.materials.append(bpy.data.materials[name])
+        return ob
+
+    headlamp = lamp("Headlamp", 0.38, 0.78, -1.97, -1.88, 0.40, 0.50)
+    taillamp = lamp("Taillamp", 0.30, 0.70, 1.97, 2.01, 0.50, 0.62)
+
+    _assign_body_materials(body)
+    _assign_wheel_materials(wheels)
+    return {"body": body,
+            "mirrored": [(w, N_WHEEL, F_WHEEL) for w in wheels]
+                        + [(headlamp, 8, 6), (taillamp, 8, 6)]}
+
+
+def _assign_body_materials(body):
+    """Deterministic panel classes by construction position (not hand-picked):
+    side windows are ring segment 5 at cabin stations; the windshield and
+    rear window are the full slopes (segments 5..7) at the steepest roof-rise
+    and roof-drop station pairs; pillars/roof sides stay paint, underbody/
+    sill/caps/bumpers are trim, everything else paint."""
+    PAINT, GLASS, TRIM = 0, 1, 2
+    n_st = len(STATIONS)
+    dz = [STATIONS[i + 1][8] - STATIONS[i][8] for i in range(n_st - 1)]
+    shield_pair = max(range(n_st - 1), key=lambda i: dz[i])      # windshield slope
+    cabin = [i for i in range(n_st - 1)
+             if STATIONS[i][5] >= 0.30 and STATIONS[i + 1][5] >= 0.30]
+    rear_pair = min(cabin, key=lambda i: dz[i])                  # rear-window slope
+    for poly in body.data.polygons:
+        i, k = divmod(poly.index, RING - 1)
+        if i >= n_st - 1:            # cap ngons (front/rear)
+            poly.material_index = TRIM
+            continue
+        both_cabin = STATIONS[i][5] >= 0.30 and STATIONS[i + 1][5] >= 0.30
+        if both_cabin and (k == 5 or (i in (shield_pair, rear_pair) and 5 <= k <= 7)):
+            poly.material_index = GLASS
+        elif k <= 1 or (i in (0, n_st - 2) and k <= 2):
+            poly.material_index = TRIM       # underbody/sill + bumper bands
+        else:
+            poly.material_index = PAINT
+
+
+def _assign_wheel_materials(wheels):
+    for w in wheels:
+        for poly in w.data.polygons:
+            j = poly.index // WHEEL_SEG
+            poly.material_index = 1 if j >= len(WHEEL_RINGS) - 2 else 0
+
+
+def _eval_mesh(obj, dg):
+    ev = obj.evaluated_get(dg)
+    me = ev.to_mesh()
+    try:
+        verts = [(ev.matrix_world @ v.co) for v in me.vertices]
+        edges = len(me.edges)
+        faces = len(me.polygons)
+        yield_v = (verts, edges, faces)
+    finally:
+        ev.to_mesh_clear()  # no argument: clears this object's evaluated mesh
+    return yield_v
+
+
+def _symmetry_dev(verts, tol_plane):
+    """Max deviation between every vertex and its negated-X partner.
+
+    Buckets by rounded (y, z, |x|): on-plane verts must be alone in their
+    bucket; off-plane buckets must pair exactly one +X with one -X, and the
+    pair's coordinate deltas are the measured error."""
+    buckets = {}
+    for v in verts:
+        key = (round(v.y, 5), round(v.z, 5), round(abs(v.x), 5))
+        buckets.setdefault(key, []).append(v)
+    dev = 0.0
+    lone = 0
+    for key, members in buckets.items():
+        if key[2] <= tol_plane:
+            if len(members) != 1:
+                lone += 1
+            continue
+        pos = [m for m in members if m.x > 0]
+        neg = [m for m in members if m.x < 0]
+        if len(pos) != 1 or len(neg) != 1:
+            lone += 1
+            continue
+        p, n = pos[0], neg[0]
+        dev = max(dev, abs(p.x + n.x), abs(p.y - n.y), abs(p.z - n.z))
+    return dev, lone
+
+
+def check(objs):
+    body = objs["body"]
+    me = body.data
+
+    # 1. the original datablock holds ONLY the authored half
+    got = (len(me.vertices), len(me.edges), len(me.polygons))
+    if got != (N_BODY, E_BODY, F_BODY):
+        print(f"ERROR: body datablock {got} != half-model closed form "
+              f"{(N_BODY, E_BODY, F_BODY)} — the mirror must live in the "
+              f"modifier stack, not in applied data", file=sys.stderr)
+        return 3
+    c = sum(1 for v in me.vertices if abs(v.co.x) <= TOL_PLANE)
+    if c != CENTERLINE:
+        print(f"ERROR: {c} authored centerline verts != {CENTERLINE}",
+              file=sys.stderr)
+        return 4
+
+    bpy.context.view_layer.update()
+    dg = bpy.context.evaluated_depsgraph_get()
+    verts, e_eval, f_eval = _eval_mesh(body, dg)
+
+    # 2. evaluated counts: exactly 2n - c, and watertight Euler 2
+    want_v = 2 * N_BODY - CENTERLINE
+    if len(verts) != want_v:
+        print(f"ERROR: evaluated body has {len(verts)} verts != 2n-c = {want_v} "
+              f"— merge is not welding the centerline (doubled seam)", file=sys.stderr)
+        return 5
+    on_plane = sum(1 for v in verts if abs(v.x) <= TOL_PLANE)
+    if on_plane != CENTERLINE:
+        print(f"ERROR: {on_plane} evaluated on-plane verts != {CENTERLINE} "
+              f"(merge threshold must weld, not duplicate)", file=sys.stderr)
+        return 6
+    euler = len(verts) - e_eval + f_eval
+    if euler != 2:
+        print(f"ERROR: evaluated Euler {euler} != 2 — mirrored shell is not a "
+              f"closed solid", file=sys.stderr)
+        return 7
+    bm = bmesh.new()
+    try:
+        ev = body.evaluated_get(dg)
+        ev_me = ev.to_mesh()
+        try:
+            bm.from_mesh(ev_me)
+        finally:
+            ev.to_mesh_clear()
+        bad = sum(1 for e in bm.edges if len(e.link_faces) != 2)
+    finally:
+        bm.free()
+    if bad:
+        print(f"ERROR: {bad} non-manifold edge(s) in the evaluated shell",
+              file=sys.stderr)
+        return 8
+
+    # 3. every evaluated vertex has an exact negated-X partner
+    dev, lone = _symmetry_dev(verts, TOL_PLANE)
+    if lone:
+        print(f"ERROR: {lone} evaluated vert(s) lack a mirrored partner",
+              file=sys.stderr)
+        return 9
+    if dev > TOL_SYMM:
+        print(f"ERROR: mirror partner deviation {dev:.3e} > tol {TOL_SYMM:.1e}",
+              file=sys.stderr)
+        return 10
+    xmin = min(v.x for v in verts)
+    xmax = max(v.x for v in verts)
+    bbox_asym = abs(xmin + xmax)
+    if bbox_asym > TOL_BBOX:
+        print(f"ERROR: evaluated bbox asymmetric by {bbox_asym:.3e} "
+              f"(tol {TOL_BBOX:.1e})", file=sys.stderr)
+        return 11
+
+    # 4. mirrored parts (wheels, lamps): each mirrored about an object origin
+    # that sits ON the plane — the data is offset, the object is not
+    part_lines = []
+    for w, n_half, f_half in objs["mirrored"]:
+        if abs(w.location.x) > TOL_PLANE:
+            print(f"ERROR: {w.name} origin x={w.location.x} — mirror mirrors "
+                  f"about the object origin; it must sit on the plane",
+                  file=sys.stderr)
+            return 12
+        if len(w.data.vertices) != n_half or len(w.data.polygons) != f_half:
+            print(f"ERROR: {w.name} datablock "
+                  f"{(len(w.data.vertices), len(w.data.polygons))} != "
+                  f"{(n_half, f_half)}", file=sys.stderr)
+            return 13
+        wv, _, wf = _eval_mesh(w, dg)
+        if len(wv) != 2 * n_half or wf != 2 * f_half:
+            print(f"ERROR: {w.name} evaluated {(len(wv), wf)} != "
+                  f"{(2 * n_half, 2 * f_half)}", file=sys.stderr)
+            return 14
+        wdev, wlone = _symmetry_dev(wv, TOL_PLANE)
+        if wlone or wdev > TOL_SYMM:
+            print(f"ERROR: {w.name} partner check: {wlone} lone, dev {wdev:.3e}",
+                  file=sys.stderr)
+            return 15
+        if min(v.x for v in wv) >= 0.0:
+            print(f"ERROR: {w.name} evaluated mesh stayed on one side — "
+                  f"mirror produced no mirrored half", file=sys.stderr)
+            return 16
+        part_lines.append(f"{w.name} sym_dev={wdev:.3e}")
+
+    print(f"body half={got[0]}/{got[1]}/{got[2]} centerline={c} | "
+          f"eval={len(verts)}/{e_eval}/{f_eval} euler=2 manifold=True | "
+          f"sym_dev={dev:.3e} (tol {TOL_SYMM:.1e}) bbox_asym={bbox_asym:.3e}")
+    print("mirrored parts | " + " | ".join(part_lines)
+          + " | origins on plane, evaluated spans both sides")
+    return 0
+
+
+def eevee_engine_id():
+    return 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT'
+
+
+def _finish_materials():
+    def principled(name):
+        m = bpy.data.materials[name]
+        m.use_nodes = True
+        return m.node_tree.nodes["Principled BSDF"]
+    p = principled("Paint")
+    p.inputs["Base Color"].default_value = (0.48, 0.015, 0.022, 1.0)
+    p.inputs["Metallic"].default_value = 0.5
+    p.inputs["Roughness"].default_value = 0.32
+    g = principled("Glass")
+    # dielectric, not metal: metallic glass mirrors the key light across the
+    # whole windshield and it renders as a hot salmon slab
+    g.inputs["Base Color"].default_value = (0.02, 0.026, 0.036, 1.0)
+    g.inputs["Metallic"].default_value = 0.0
+    g.inputs["Roughness"].default_value = 0.3
+    t = principled("Trim")
+    t.inputs["Base Color"].default_value = (0.02, 0.021, 0.026, 1.0)
+    t.inputs["Roughness"].default_value = 0.6
+    tire = principled("Tire")
+    tire.inputs["Base Color"].default_value = (0.012, 0.013, 0.016, 1.0)
+    tire.inputs["Roughness"].default_value = 0.85
+    hub = principled("Hubcap")
+    hub.inputs["Base Color"].default_value = (0.62, 0.64, 0.68, 1.0)
+    hub.inputs["Metallic"].default_value = 1.0
+    hub.inputs["Roughness"].default_value = 0.28
+    head = principled("Headlamp")
+    head.inputs["Base Color"].default_value = (0.85, 0.9, 0.95, 1.0)
+    head.inputs["Emission Color"].default_value = (0.9, 0.95, 1.0, 1.0)
+    head.inputs["Emission Strength"].default_value = 1.2
+    tail = principled("Taillamp")
+    tail.inputs["Base Color"].default_value = (0.3, 0.008, 0.01, 1.0)
+    tail.inputs["Emission Color"].default_value = (0.8, 0.02, 0.02, 1.0)
+    tail.inputs["Emission Strength"].default_value = 0.9
+
+
+def render_still(objs, path, engine):
+    scene = bpy.context.scene
+    _finish_materials()
+    body = objs["body"]
+    for poly in body.data.polygons:
+        poly.use_smooth = False  # crisp loft panels
+
+    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)
+    scene.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)
+    scene.collection.objects.link(wall)
+
+    world = bpy.data.worlds.new("World")
+    world.use_nodes = True
+    # metallic paint needs a faint ambient or the flanks die to black
+    world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.024, 0.026, 0.032, 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)
+
+    # default-stage rig per docs/VISUAL-STYLE.md
+    light("Key", (-4.0, -5.0, 6.0), 550.0, 5.0, (1.0, 0.96, 0.9), (48, 0, -35))
+    light("Fill", (5.0, -3.5, 2.5), 120.0, 9.0, (0.75, 0.85, 1.0), (65, 0, 50))
+    light("Rim", (3.0, 4.5, 5.0), 320.0, 4.0, (0.6, 0.78, 1.0), (-55, 0, 155))
+    light("Wedge", (2.5, 5.5, 4.0), 400.0, 6.0, (1.0, 0.76, 0.5), (-68, 0, 190))
+
+    aim = bpy.data.objects.new("Aim", None)
+    aim.location = (0.0, 0.0, 0.55)
+    scene.collection.objects.link(aim)
+    cam_data = bpy.data.cameras.new("Cam")
+    cam_data.lens = 52.0
+    cam = bpy.data.objects.new("Cam", cam_data)
+    cam.location = (4.4, -5.3, 2.1)
+    scene.collection.objects.link(cam)
+    track = cam.constraints.new('TRACK_TO')  # data API, not bpy.ops (damped-track-aim)
+    track.target = aim
+    track.track_axis = 'TRACK_NEGATIVE_Z'
+    track.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
+    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 = 'PNG'
+    scene.render.filepath = path
+    # AgX would flatten the candy paint toward chalk (docs/VISUAL-STYLE.md)
+    scene.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="eevee", choices=("eevee", "cycles"),
+                   help="render engine for --output (cycles for GPU-less hosts)")
+    args = p.parse_args(argv)
+
+    objs = build_car()
+    code = check(objs)
+    if code:
+        return code
+
+    if args.output:
+        if not render_still(objs, os.path.abspath(args.output), args.engine):
+            print("ERROR: render produced no file", file=sys.stderr)
+            return 6
+        print(f"rendered still {args.output}")
+
+    print("car-mirror-symmetry 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/docs/gallery/contact-sheets/car-mirror-symmetry-contact-sheet.webp b/docs/gallery/contact-sheets/car-mirror-symmetry-contact-sheet.webp new file mode 100644 index 0000000000000000000000000000000000000000..c398c5925eeb80881dfb20e125b44d156de31a3f GIT binary patch literal 42636 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

i3+Ahy%o_e^@Q^tQ{F&i}r$R@=G6s*8`Qz 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⁡g$GXk^t&%yo4N+P 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 View example +

+ + car-mirror-symmetry — A generic hatchback lofted as one half (14 stations, 9-point rings) and completed by the Mirror modifier, evaluated through the depsgraph. + +
+

car-mirror-symmetry

+

A generic hatchback lofted as one half (14 stations, 9-point rings) and completed by the Mirror modifier, evaluated through the depsgraph. Wheels and lamps mirror about object origins parked on the symmetry plane.

+

witnesses Mirror is a modifier, not applied data: the original keeps the authored half while the evaluated mesh is the welded, watertight, exactly symmetric whole.

+ View example +
+