Merge branch 'gumyr:dev' into deglob_write

This commit is contained in:
jdegenstein 2025-07-15 14:43:28 -05:00 committed by GitHub
commit 2d2a89bddf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 7427 additions and 1000 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -0,0 +1,771 @@
<?xml version='1.0' encoding='utf-8'?>
<svg width="287.09mm" height="200.730007mm" viewBox="-143.545 -101.454993 287.09 200.730007" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(1,-1)" stroke-linecap="round">
<g fill="none" stroke="rgb(0,0,0)" stroke-width="0.09" id="Visible">
<line x1="88.539518" y1="76.052448" x2="113.571098" y2="61.600459" />
<path d="M 88.539518,76.052448 A 3.4499999999999997,1.991858428704209 0.0 0,1 83.660482,76.052448" />
<path d="M 113.571098,58.783545 A 3.4499999999999997,1.991858428704209 0.0 0,1 114.58158,60.192002" />
<path d="M 114.58158,60.192002 A 3.4499999999999997,1.991858428704209 0.0 0,1 113.571098,61.600459" />
<line x1="83.660482" y1="76.052448" x2="58.628902" y2="61.600459" />
<line x1="88.539518" y1="44.331556" x2="113.571098" y2="58.783545" />
<path d="M 58.628902,61.600459 A 3.4499999999999997,1.991858428704209 0.0 0,1 57.61842,60.192002" />
<path d="M 57.61842,60.192002 A 3.4499999999999997,1.991858428704209 0.0 0,1 58.628902,58.783545" />
<path d="M 83.660482,44.331556 A 3.4499999999999997,1.991858428704209 0.0 0,1 88.539518,44.331556" />
<line x1="58.628902" y1="58.783545" x2="83.660482" y2="44.331556" />
<path d="M 87.542592,73.900687 A 1.9124999999999999,1.1041823898251593 0.0 0,1 88.0125,74.62562" />
<path d="M 88.0125,74.62562 A 1.9125000000000012,1.10418238982516 0.0 0,1 84.1875,74.62562" />
<path d="M 84.1875,74.62562 A 1.9124999999999999,1.1041823898251593 0.0 0,1 84.657408,73.900687" />
<path d="M 59.747898,59.411227 A 1.9124999999999999,1.1041823898251593 0.0 0,1 63.01274,60.192002" />
<path d="M 63.01274,60.192002 A 1.9125000000000012,1.10418238982516 0.0 0,1 59.18774,60.192002" />
<path d="M 59.18774,60.192002 A 1.9124999999999999,1.1041823898251593 0.0 0,1 59.747898,59.411227" />
<path d="M 109.747419,59.411227 A 1.9124999999999999,1.1041823898251593 0.0 0,1 113.01226,60.192002" />
<path d="M 113.01226,60.192002 A 1.9125000000000012,1.10418238982516 0.0 0,1 109.18726,60.192002" />
<path d="M 109.18726,60.192002 A 1.9124999999999999,1.1041823898251593 0.0 0,1 109.747419,59.411227" />
<path d="M 75.997212,54.359155 A 14.287500000000001,8.24889197104678 0.0 0,1 100.3875,60.192002" />
<path d="M 71.8125,60.192002 A 14.287500000000001,8.24889197104678 0.0 0,1 75.997212,54.359155" />
<path d="M 84.747658,44.977609 A 1.9124999999999999,1.1041823898251593 0.0 0,1 88.0125,45.758384" />
<path d="M 88.0125,45.758384 A 1.9125000000000012,1.10418238982516 0.0 0,1 84.1875,45.758384" />
<path d="M 84.1875,45.758384 A 1.9124999999999999,1.1041823898251593 0.0 0,1 84.747658,44.977609" />
<path d="M 79.667096,39.901041 A 1.875,1.0825317547305484 0.0 0,1 82.318746,39.901041" />
<line x1="62.622287" y1="49.741866" x2="79.667096" y2="39.901041" />
<line x1="82.318746" y1="39.901041" x2="83.44835" y2="40.553218" />
<path d="M 62.622287,49.741866 A 1.875,1.0825317547305484 180.0 0,0 62.073112,50.507332" />
<path d="M 83.44835,40.553218 A 3.75,2.165063509461097 180.0 0,0 84.600538,41.006736" />
<path d="M 87.599462,41.006736 A 3.75,2.165063509461097 180.0 0,0 88.75165,40.553218" />
<line x1="89.881254" y1="39.901041" x2="88.75165" y2="40.553218" />
<path d="M 92.532904,39.901041 A 1.875,1.0825317547305484 180.0 0,0 89.881254,39.901041" />
<line x1="92.532904" y1="39.901041" x2="109.577713" y2="49.741866" />
<path d="M 109.577713,49.741866 A 1.875,1.0825317547305484 0.0 0,1 110.126888,50.507332" />
<path d="M 113.571098,55.844158 A 3.4499999999999997,1.991858428704209 0.0 0,1 114.58158,57.252614" />
<line x1="113.571098" y1="55.844158" x2="109.577713" y2="53.538575" />
<line x1="113.571098" y1="58.783545" x2="113.571098" y2="55.844158" />
<line x1="88.539518" y1="44.331556" x2="88.539518" y2="41.392168" />
<line x1="92.532904" y1="43.69775" x2="88.539518" y2="41.392168" />
<line x1="92.532904" y1="43.69775" x2="92.532904" y2="39.901041" />
<line x1="109.577713" y1="53.538575" x2="109.577713" y2="49.741866" />
<line x1="58.628902" y1="58.783545" x2="58.628902" y2="55.844158" />
<path d="M 57.61842,57.252614 A 3.4499999999999997,1.991858428704209 0.0 0,1 58.628902,55.844158" />
<line x1="83.660482" y1="44.331556" x2="83.660482" y2="41.392168" />
<path d="M 83.660482,41.392168 A 3.4499999999999997,1.991858428704209 0.0 0,1 88.539518,41.392168" />
<line x1="58.628902" y1="55.844158" x2="62.622287" y2="53.538575" />
<line x1="62.622287" y1="53.538575" x2="62.622287" y2="49.741866" />
<line x1="79.667096" y1="43.69775" x2="79.667096" y2="39.901041" />
<line x1="79.667096" y1="43.69775" x2="83.660482" y2="41.392168" />
<path d="M 75.997212,55.33895 A 14.287500000000001,8.24889197104678 0.0 0,1 100.3875,61.171798" />
<path d="M 100.3875,61.171798 A 14.287500000000001,8.24889197104678 0.0 0,1 88.48125,69.305315" />
<path d="M 83.71875,69.305315 A 14.287500000000001,8.24889197104678 0.0 0,1 71.8125,61.171798" />
<path d="M 71.8125,61.171798 A 14.287500000000001,8.24889197104678 0.0 0,1 75.997212,55.33895" />
<path d="M 79.720129,19.110997 A 1.7999999999999998,1.0392304845413265 0.0 0,1 82.265713,19.110997" />
<line x1="82.265713" y1="19.110997" x2="83.395317" y2="19.763174" />
<path d="M 83.395317,19.763174 A 3.8249999999999997,2.2083647796503185 180.0 0,0 88.804683,19.763174" />
<line x1="89.934287" y1="19.110997" x2="88.804683" y2="19.763174" />
<path d="M 92.479871,19.110997 A 1.7999999999999998,1.0392304845413263 -180.0 0,0 89.934287,19.110997" />
<line x1="92.479871" y1="19.110997" x2="109.52468" y2="28.951822" />
<path d="M 109.52468,28.951822 A 1.7999999999999998,1.0392304845413263 -3.895368034302951e-15 0,1 110.051888,29.686669" />
<path d="M 62.67532,28.951822 A 1.7999999999999998,1.0392304845413265 180.0 0,0 62.148112,29.686669" />
<line x1="62.67532" y1="28.951822" x2="79.720129" y2="19.110997" />
<path d="M 79.667096,19.080379 A 1.875,1.0825317547305484 0.0 0,1 82.318746,19.080379" />
<line x1="62.622287" y1="28.921204" x2="79.667096" y2="19.080379" />
<line x1="82.318746" y1="19.080379" x2="83.44835" y2="19.732555" />
<path d="M 62.622287,28.921204 A 1.875,1.0825317547305484 180.0 0,0 62.073112,29.686669" />
<path d="M 62.073112,29.686669 A 1.875,1.0825317547305484 180.0 0,0 62.148112,29.989778" />
<path d="M 83.44835,19.732555 A 3.75,2.165063509461097 180.0 0,0 88.75165,19.732555" />
<line x1="89.881254" y1="19.080379" x2="88.75165" y2="19.732555" />
<path d="M 92.532904,19.080379 A 1.875,1.0825317547305484 180.0 0,0 89.881254,19.080379" />
<line x1="92.532904" y1="19.080379" x2="109.577713" y2="28.921204" />
<path d="M 62.148112,35.886925 A 1.875,1.0825317547305484 0.0 0,1 62.073112,35.583816" />
<path d="M 62.073112,35.583816 A 1.875,1.0825317547305484 0.0 0,1 62.148112,35.280707" />
<path d="M 109.577713,28.921204 A 1.875,1.0825317547305484 0.0 0,1 110.126888,29.686669" />
<path d="M 110.126888,29.686669 A 1.875,1.0825317547305484 0.0 0,1 110.051888,29.989778" />
<path d="M 110.051888,35.886925 A 1.875,1.0825317547305484 180.0 0,0 110.126888,35.583816" />
<path d="M 110.126888,35.583816 A 1.875,1.0825317547305484 180.0 0,0 110.051888,35.280707" />
<path d="M 79.667096,12.344282 A 1.875,1.0825317547305484 0.0 0,1 82.318746,12.344282" />
<line x1="62.622287" y1="22.185107" x2="79.667096" y2="12.344282" />
<line x1="82.318746" y1="12.344282" x2="83.44835" y2="12.996458" />
<path d="M 62.622287,22.185107 A 1.875,1.0825317547305484 180.0 0,0 62.073112,22.950572" />
<path d="M 83.44835,12.996458 A 3.75,2.165063509461097 180.0 0,0 88.75165,12.996458" />
<line x1="89.881254" y1="12.344282" x2="88.75165" y2="12.996458" />
<path d="M 92.532904,12.344282 A 1.875,1.0825317547305484 180.0 0,0 89.881254,12.344282" />
<line x1="92.532904" y1="12.344282" x2="109.577713" y2="22.185107" />
<path d="M 109.577713,22.185107 A 1.875,1.0825317547305484 0.0 0,1 110.126888,22.950572" />
<path d="M 84.416202,62.649146 A 2.3812499999999996,1.3748153285077964 0.0 0,1 86.611629,62.278581" />
<path d="M 86.611629,62.278581 A 2.3812499999999996,1.3748153285077964 0.0 0,1 88.425637,63.325899" />
<line x1="88.425637" y1="63.325899" x2="88.425637" y2="72.511485" />
<path d="M 88.425637,72.511485 A 2.3812499999999996,1.3748153285077964 0.0 0,1 88.48125,72.806874" />
<path d="M 88.48125,72.806874 A 2.3812500000000014,1.3748153285077973 0.0 1,1 83.71875,72.806874" />
<path d="M 83.71875,72.806874 A 2.3812499999999996,1.3748153285077964 0.0 0,1 86.611629,71.464167" />
<line x1="86.611629" y1="62.278581" x2="86.611629" y2="71.464167" />
<line x1="86.611629" y1="62.278581" x2="88.425637" y2="63.325899" />
<line x1="88.425637" y1="72.511485" x2="86.611629" y2="71.464167" />
<line x1="82.318746" y1="42.166819" x2="82.318746" y2="39.901041" />
<line x1="83.44835" y1="41.514643" x2="83.44835" y2="40.553218" />
<line x1="88.75165" y1="41.514643" x2="88.75165" y2="40.553218" />
<line x1="89.881254" y1="42.166819" x2="89.881254" y2="39.901041" />
<line x1="109.577713" y1="53.538575" x2="109.577713" y2="51.272797" />
<line x1="82.265713" y1="39.871601" x2="82.265713" y2="19.110997" />
<line x1="79.720129" y1="39.871601" x2="79.720129" y2="19.110997" />
<line x1="83.395317" y1="40.522599" x2="83.395317" y2="19.763174" />
<line x1="88.804683" y1="40.522599" x2="88.804683" y2="19.763174" />
<line x1="89.934287" y1="39.871601" x2="89.934287" y2="19.110997" />
<line x1="92.479871" y1="39.871601" x2="92.479871" y2="19.110997" />
<line x1="109.52468" y1="49.711248" x2="109.52468" y2="28.951822" />
<line x1="62.67532" y1="49.711248" x2="62.67532" y2="28.951822" />
<line x1="79.667096" y1="19.080379" x2="79.667096" y2="12.344282" />
<line x1="82.318746" y1="19.080379" x2="82.318746" y2="12.344282" />
<line x1="62.622287" y1="28.921204" x2="62.622287" y2="22.185107" />
<line x1="83.44835" y1="19.732555" x2="83.44835" y2="12.996458" />
<line x1="88.75165" y1="19.732555" x2="88.75165" y2="12.996458" />
<line x1="89.881254" y1="19.080379" x2="89.881254" y2="12.344282" />
<line x1="92.532904" y1="19.080379" x2="92.532904" y2="12.344282" />
<line x1="109.577713" y1="28.921204" x2="109.577713" y2="22.185107" />
<line x1="114.58158" y1="57.252614" x2="114.58158" y2="60.192002" />
<line x1="57.61842" y1="57.252614" x2="57.61842" y2="60.192002" />
<line x1="71.8125" y1="61.171798" x2="71.8125" y2="60.192002" />
<line x1="100.3875" y1="61.171798" x2="100.3875" y2="60.192002" />
<line x1="62.073112" y1="50.507332" x2="62.073112" y2="53.855642" />
<line x1="110.126888" y1="50.507332" x2="110.126888" y2="53.855642" />
<line x1="110.051888" y1="29.686669" x2="110.051888" y2="50.204223" />
<line x1="62.148112" y1="29.686669" x2="62.148112" y2="50.204223" />
<line x1="62.073112" y1="22.950572" x2="62.073112" y2="29.686669" />
<line x1="62.073112" y1="29.686669" x2="62.073112" y2="35.583816" />
<line x1="110.126888" y1="22.950572" x2="110.126888" y2="29.686669" />
<line x1="110.126888" y1="29.686669" x2="110.126888" y2="35.583816" />
<line x1="83.71875" y1="72.806874" x2="83.71875" y2="60.192002" />
<line x1="88.48125" y1="72.806874" x2="88.48125" y2="60.192002" />
<line x1="-114.3" y1="26.560002" x2="-114.3" y2="73.760002" />
<path d="M -114.3,26.560002 A 4.6,4.6 0.0 0,1 -109.7,21.960002" />
<path d="M -109.7,78.360002 A 4.6,4.6 0.0 0,1 -114.3,73.760002" />
<line x1="-109.7" y1="21.960002" x2="-62.5" y2="21.960002" />
<line x1="-62.5" y1="78.360002" x2="-109.7" y2="78.360002" />
<path d="M -62.5,21.960002 A 4.6,4.6 0.0 0,1 -57.9,26.560002" />
<path d="M -57.9,73.760002 A 4.6,4.6 0.0 0,1 -62.5,78.360002" />
<line x1="-57.9" y1="26.560002" x2="-57.9" y2="73.760002" />
<circle cx="-109.67" cy="26.590002" r="2.55" />
<circle cx="-62.53" cy="26.590002" r="2.55" />
<circle cx="-109.67" cy="73.730002" r="2.55" />
<circle cx="-62.53" cy="73.730002" r="2.55" />
<circle cx="-86.1" cy="50.160002" r="19.05" />
<path d="M -84.389737,52.835002 A 3.175,3.175 0.0 0,1 -87.810263,52.835002" />
<path d="M -87.810263,52.835002 A 3.175,3.175 122.59284353031869 1,1 -84.389737,52.835002" />
<line x1="-87.810263" y1="52.835002" x2="-84.389737" y2="52.835002" />
<line x1="-114.3" y1="-25.080001" x2="-114.3" y2="-29.880001" />
<line x1="-114.3" y1="-29.880001" x2="-114.3" y2="-36.080001" />
<path d="M -114.3,-25.080001 Q -114.285538,-25.080001 -114.271076,-25.080001 Q -114.227872,-25.080001 -114.184668,-25.080001 Q -114.113266,-25.080001 -114.041863,-25.080001 Q -113.94316,-25.080001 -113.844457,-25.080001 Q -113.719694,-25.080001 -113.594931,-25.080001 Q -113.445678,-25.080001 -113.296425,-25.080001 Q -113.124558,-25.080001 -112.952691,-25.080001 Q -112.760372,-25.080001 -112.568053,-25.080001 Q -112.3577,-25.080001 -112.147348,-25.080001 Q -111.921606,-25.080001 -111.695865,-25.080001 Q -111.457574,-25.080001 -111.219284,-25.080001 Q -110.97144,-25.080001 -110.723596,-25.080001 Q -110.216853,-25.080001 -109.7,-25.080001" />
<line x1="-109.7" y1="-25.080001" x2="-62.5" y2="-25.080001" />
<path d="M -62.5,-25.080001 Q -61.983147,-25.080001 -61.476404,-25.080001 Q -61.22856,-25.080001 -60.980716,-25.080001 Q -60.742426,-25.080001 -60.504135,-25.080001 Q -60.278394,-25.080001 -60.052652,-25.080001 Q -59.8423,-25.080001 -59.631947,-25.080001 Q -59.439628,-25.080001 -59.247309,-25.080001 Q -59.075442,-25.080001 -58.903575,-25.080001 Q -58.754322,-25.080001 -58.605069,-25.080001 Q -58.480306,-25.080001 -58.355543,-25.080001 Q -58.25684,-25.080001 -58.158137,-25.080001 Q -58.086734,-25.080001 -58.015332,-25.080001 Q -57.972128,-25.080001 -57.928924,-25.080001 Q -57.914462,-25.080001 -57.9,-25.080001" />
<line x1="-109.7" y1="-25.080001" x2="-109.7" y2="-29.880001" />
<path d="M -114.3,-29.880001 Q -114.285538,-29.880001 -114.271076,-29.880001 Q -114.227872,-29.880001 -114.184668,-29.880001 Q -114.113266,-29.880001 -114.041863,-29.880001 Q -113.94316,-29.880001 -113.844457,-29.880001 Q -113.719694,-29.880001 -113.594931,-29.880001 Q -113.445678,-29.880001 -113.296425,-29.880001 Q -113.124558,-29.880001 -112.952691,-29.880001 Q -112.760372,-29.880001 -112.568053,-29.880001 Q -112.3577,-29.880001 -112.147348,-29.880001 Q -111.921606,-29.880001 -111.695865,-29.880001 Q -111.457574,-29.880001 -111.219284,-29.880001 Q -110.97144,-29.880001 -110.723596,-29.880001 Q -110.216853,-29.880001 -109.7,-29.880001" />
<line x1="-109.7" y1="-29.880001" x2="-102.17" y2="-29.880001" />
<path d="M -114.3,-36.080001 Q -114.29214,-36.080001 -114.284281,-36.080001 Q -114.2608,-36.080001 -114.23732,-36.080001 Q -114.198514,-36.080001 -114.159708,-36.080001 Q -114.106065,-36.080001 -114.052422,-36.080001 Q -113.984616,-36.080001 -113.91681,-36.080001 Q -113.835695,-36.080001 -113.754579,-36.080001 Q -113.661173,-36.080001 -113.567767,-36.080001 Q -113.463246,-36.080001 -113.358725,-36.080001 Q -113.244402,-36.080001 -113.13008,-36.080001 Q -113.007395,-36.080001 -112.884709,-36.080001 Q -112.755204,-36.080001 -112.625698,-36.080001 Q -112.491,-36.080001 -112.356302,-36.080001 Q -112.080898,-36.080001 -111.8,-36.080001" />
<path d="M -57.9,-36.080001 Q -57.90786,-36.080001 -57.915719,-36.080001 Q -57.9392,-36.080001 -57.96268,-36.080001 Q -58.001486,-36.080001 -58.040292,-36.080001 Q -58.093935,-36.080001 -58.147578,-36.080001 Q -58.215384,-36.080001 -58.28319,-36.080001 Q -58.364305,-36.080001 -58.445421,-36.080001 Q -58.538827,-36.080001 -58.632233,-36.080001 Q -58.736754,-36.080001 -58.841275,-36.080001 Q -58.955598,-36.080001 -59.06992,-36.080001 Q -59.192605,-36.080001 -59.315291,-36.080001 Q -59.444796,-36.080001 -59.574302,-36.080001 Q -59.709,-36.080001 -59.843698,-36.080001 Q -60.119102,-36.080001 -60.4,-36.080001" />
<line x1="-60.4" y1="-36.080001" x2="-62.53" y2="-36.080001" />
<path d="M -67.53,-36.080001 Q -67.514281,-36.080001 -67.498561,-36.080001 Q -67.4516,-36.080001 -67.40464,-36.080001 Q -67.327028,-36.080001 -67.249417,-36.080001 Q -67.14213,-36.080001 -67.034844,-36.080001 Q -66.899233,-36.080001 -66.763621,-36.080001 Q -66.601389,-36.080001 -66.439157,-36.080001 Q -66.252346,-36.080001 -66.065534,-36.080001 Q -65.856491,-36.080001 -65.647449,-36.080001 Q -65.418805,-36.080001 -65.19016,-36.080001 Q -64.94479,-36.080001 -64.699419,-36.080001 Q -64.440407,-36.080001 -64.181395,-36.080001 Q -63.912,-36.080001 -63.642605,-36.080001 Q -63.091797,-36.080001 -62.53,-36.080001" />
<path d="M -70.03,-36.080001 Q -69.890044,-36.080001 -69.750089,-36.080001 Q -69.471736,-36.080001 -69.204302,-36.080001 Q -69.074796,-36.080001 -68.945291,-36.080001 Q -68.822605,-36.080001 -68.69992,-36.080001 Q -68.585598,-36.080001 -68.471275,-36.080001 Q -68.366754,-36.080001 -68.262233,-36.080001 Q -68.168827,-36.080001 -68.075421,-36.080001 Q -67.994305,-36.080001 -67.91319,-36.080001 Q -67.845384,-36.080001 -67.777578,-36.080001 Q -67.723935,-36.080001 -67.670292,-36.080001 Q -67.631486,-36.080001 -67.59268,-36.080001 Q -67.5692,-36.080001 -67.545719,-36.080001 Q -67.53786,-36.080001 -67.53,-36.080001" />
<line x1="-102.17" y1="-36.080001" x2="-70.03" y2="-36.080001" />
<path d="M -102.17,-36.080001 Q -102.309956,-36.080001 -102.449911,-36.080001 Q -102.728264,-36.080001 -102.995698,-36.080001 Q -103.125204,-36.080001 -103.254709,-36.080001 Q -103.377395,-36.080001 -103.50008,-36.080001 Q -103.614402,-36.080001 -103.728725,-36.080001 Q -103.833246,-36.080001 -103.937767,-36.080001 Q -104.031173,-36.080001 -104.124579,-36.080001 Q -104.205695,-36.080001 -104.28681,-36.080001 Q -104.354616,-36.080001 -104.422422,-36.080001 Q -104.476065,-36.080001 -104.529708,-36.080001 Q -104.568514,-36.080001 -104.60732,-36.080001 Q -104.6308,-36.080001 -104.654281,-36.080001 Q -104.66214,-36.080001 -104.67,-36.080001" />
<path d="M -109.67,-36.080001 L -109.110178,-36.080001 L -108.557395,-36.080001 L -108.018605,-36.080001 L -107.500581,-36.080001 L -107.00984,-36.080001 L -106.552551,-36.080001 L -106.134466,-36.080001 L -105.760843,-36.080001 L -105.436379,-36.080001 L -105.165156,-36.080001 L -104.950583,-36.080001 L -104.79536,-36.080001 L -104.701439,-36.080001 L -104.67,-36.080001" />
<line x1="-111.8" y1="-36.080001" x2="-109.67" y2="-36.080001" />
<line x1="-102.17" y1="-29.880001" x2="-102.17" y2="-36.080001" />
<line x1="-70.03" y1="-29.880001" x2="-70.03" y2="-36.080001" />
<line x1="-70.03" y1="-29.880001" x2="-62.5" y2="-29.880001" />
<line x1="-62.5" y1="-25.080001" x2="-62.5" y2="-29.880001" />
<line x1="-57.9" y1="-25.080001" x2="-57.9" y2="-29.880001" />
<path d="M -62.5,-29.880001 Q -61.983147,-29.880001 -61.476404,-29.880001 Q -61.22856,-29.880001 -60.980716,-29.880001 Q -60.742426,-29.880001 -60.504135,-29.880001 Q -60.278394,-29.880001 -60.052652,-29.880001 Q -59.8423,-29.880001 -59.631947,-29.880001 Q -59.439628,-29.880001 -59.247309,-29.880001 Q -59.075442,-29.880001 -58.903575,-29.880001 Q -58.754322,-29.880001 -58.605069,-29.880001 Q -58.480306,-29.880001 -58.355543,-29.880001 Q -58.25684,-29.880001 -58.158137,-29.880001 Q -58.086734,-29.880001 -58.015332,-29.880001 Q -57.972128,-29.880001 -57.928924,-29.880001 Q -57.914462,-29.880001 -57.9,-29.880001" />
<line x1="-57.9" y1="-29.880001" x2="-57.9" y2="-36.080001" />
<line x1="-67.05" y1="-25.080001" x2="-67.05" y2="-23.480001" />
<path d="M -105.15,-23.480001 L -104.672377,-23.480001 L -103.263457,-23.480001 L -100.99389,-23.480001 L -97.977481,-23.480001 L -94.365485,-23.480001 L -90.339024,-23.480001 L -86.1,-23.480001 L -81.860976,-23.480001 L -77.834515,-23.480001 L -74.222519,-23.480001 L -71.20611,-23.480001 L -68.936543,-23.480001 L -67.527623,-23.480001 L -67.05,-23.480001" />
<line x1="-104.67" y1="-29.880001" x2="-104.67" y2="-36.080001" />
<line x1="-67.53" y1="-29.880001" x2="-67.53" y2="-36.080001" />
<line x1="-114.2" y1="-36.080001" x2="-114.2" y2="-70.080001" />
<line x1="-104.57" y1="-36.080001" x2="-104.57" y2="-70.080001" />
<line x1="-67.63" y1="-36.080001" x2="-67.63" y2="-70.080001" />
<line x1="-58.0" y1="-36.080001" x2="-58.0" y2="-70.080001" />
<path d="M -57.9,-70.080001 Q -57.90786,-70.080001 -57.915719,-70.080001 Q -57.9392,-70.080001 -57.96268,-70.080001 Q -58.001486,-70.080001 -58.040292,-70.080001 Q -58.093935,-70.080001 -58.147578,-70.080001 Q -58.215384,-70.080001 -58.28319,-70.080001 Q -58.364305,-70.080001 -58.445421,-70.080001 Q -58.538827,-70.080001 -58.632233,-70.080001 Q -58.736754,-70.080001 -58.841275,-70.080001 Q -58.955598,-70.080001 -59.06992,-70.080001 Q -59.192605,-70.080001 -59.315291,-70.080001 Q -59.444796,-70.080001 -59.574302,-70.080001 Q -59.709,-70.080001 -59.843698,-70.080001 Q -60.119102,-70.080001 -60.4,-70.080001" />
<line x1="-60.4" y1="-70.080001" x2="-62.53" y2="-70.080001" />
<path d="M -67.53,-70.080001 Q -67.514281,-70.080001 -67.498561,-70.080001 Q -67.4516,-70.080001 -67.40464,-70.080001 Q -67.327028,-70.080001 -67.249417,-70.080001 Q -67.14213,-70.080001 -67.034844,-70.080001 Q -66.899233,-70.080001 -66.763621,-70.080001 Q -66.601389,-70.080001 -66.439157,-70.080001 Q -66.252346,-70.080001 -66.065534,-70.080001 Q -65.856491,-70.080001 -65.647449,-70.080001 Q -65.418805,-70.080001 -65.19016,-70.080001 Q -64.94479,-70.080001 -64.699419,-70.080001 Q -64.440407,-70.080001 -64.181395,-70.080001 Q -63.912,-70.080001 -63.642605,-70.080001 Q -63.091797,-70.080001 -62.53,-70.080001" />
<path d="M -70.03,-70.080001 Q -69.890044,-70.080001 -69.750089,-70.080001 Q -69.471736,-70.080001 -69.204302,-70.080001 Q -69.074796,-70.080001 -68.945291,-70.080001 Q -68.822605,-70.080001 -68.69992,-70.080001 Q -68.585598,-70.080001 -68.471275,-70.080001 Q -68.366754,-70.080001 -68.262233,-70.080001 Q -68.168827,-70.080001 -68.075421,-70.080001 Q -67.994305,-70.080001 -67.91319,-70.080001 Q -67.845384,-70.080001 -67.777578,-70.080001 Q -67.723935,-70.080001 -67.670292,-70.080001 Q -67.631486,-70.080001 -67.59268,-70.080001 Q -67.5692,-70.080001 -67.545719,-70.080001 Q -67.53786,-70.080001 -67.53,-70.080001" />
<line x1="-102.17" y1="-70.080001" x2="-70.03" y2="-70.080001" />
<path d="M -102.17,-70.080001 Q -102.309956,-70.080001 -102.449911,-70.080001 Q -102.728264,-70.080001 -102.995698,-70.080001 Q -103.125204,-70.080001 -103.254709,-70.080001 Q -103.377395,-70.080001 -103.50008,-70.080001 Q -103.614402,-70.080001 -103.728725,-70.080001 Q -103.833246,-70.080001 -103.937767,-70.080001 Q -104.031173,-70.080001 -104.124579,-70.080001 Q -104.205695,-70.080001 -104.28681,-70.080001 Q -104.354616,-70.080001 -104.422422,-70.080001 Q -104.476065,-70.080001 -104.529708,-70.080001 Q -104.568514,-70.080001 -104.60732,-70.080001 Q -104.6308,-70.080001 -104.654281,-70.080001 Q -104.66214,-70.080001 -104.67,-70.080001" />
<path d="M -109.67,-70.080001 L -109.110178,-70.080001 L -108.557395,-70.080001 L -108.018605,-70.080001 L -107.500581,-70.080001 L -107.00984,-70.080001 L -106.552551,-70.080001 L -106.134466,-70.080001 L -105.760843,-70.080001 L -105.436379,-70.080001 L -105.165156,-70.080001 L -104.950583,-70.080001 L -104.79536,-70.080001 L -104.701439,-70.080001 L -104.67,-70.080001" />
<line x1="-111.8" y1="-70.080001" x2="-109.67" y2="-70.080001" />
<path d="M -114.3,-70.080001 Q -114.29214,-70.080001 -114.284281,-70.080001 Q -114.2608,-70.080001 -114.23732,-70.080001 Q -114.198514,-70.080001 -114.159708,-70.080001 Q -114.106065,-70.080001 -114.052422,-70.080001 Q -113.984616,-70.080001 -113.91681,-70.080001 Q -113.835695,-70.080001 -113.754579,-70.080001 Q -113.661173,-70.080001 -113.567767,-70.080001 Q -113.463246,-70.080001 -113.358725,-70.080001 Q -113.244402,-70.080001 -113.13008,-70.080001 Q -113.007395,-70.080001 -112.884709,-70.080001 Q -112.755204,-70.080001 -112.625698,-70.080001 Q -112.491,-70.080001 -112.356302,-70.080001 Q -112.080898,-70.080001 -111.8,-70.080001" />
<line x1="-57.9" y1="-70.080001" x2="-57.9" y2="-81.080001" />
<path d="M -57.9,-81.080001 Q -57.90786,-81.080001 -57.915719,-81.080001 Q -57.9392,-81.080001 -57.96268,-81.080001 Q -58.001486,-81.080001 -58.040292,-81.080001 Q -58.093935,-81.080001 -58.147578,-81.080001 Q -58.215384,-81.080001 -58.28319,-81.080001 Q -58.364305,-81.080001 -58.445421,-81.080001 Q -58.538827,-81.080001 -58.632233,-81.080001 Q -58.736754,-81.080001 -58.841275,-81.080001 Q -58.955598,-81.080001 -59.06992,-81.080001 Q -59.192605,-81.080001 -59.315291,-81.080001 Q -59.444796,-81.080001 -59.574302,-81.080001 Q -59.709,-81.080001 -59.843698,-81.080001 Q -60.119102,-81.080001 -60.4,-81.080001" />
<line x1="-60.4" y1="-81.080001" x2="-62.53" y2="-81.080001" />
<path d="M -67.53,-81.080001 Q -67.514281,-81.080001 -67.498561,-81.080001 Q -67.4516,-81.080001 -67.40464,-81.080001 Q -67.327028,-81.080001 -67.249417,-81.080001 Q -67.14213,-81.080001 -67.034844,-81.080001 Q -66.899233,-81.080001 -66.763621,-81.080001 Q -66.601389,-81.080001 -66.439157,-81.080001 Q -66.252346,-81.080001 -66.065534,-81.080001 Q -65.856491,-81.080001 -65.647449,-81.080001 Q -65.418805,-81.080001 -65.19016,-81.080001 Q -64.94479,-81.080001 -64.699419,-81.080001 Q -64.440407,-81.080001 -64.181395,-81.080001 Q -63.912,-81.080001 -63.642605,-81.080001 Q -63.091797,-81.080001 -62.53,-81.080001" />
<line x1="-67.53" y1="-70.080001" x2="-67.53" y2="-81.080001" />
<path d="M -70.03,-81.080001 Q -69.890044,-81.080001 -69.750089,-81.080001 Q -69.471736,-81.080001 -69.204302,-81.080001 Q -69.074796,-81.080001 -68.945291,-81.080001 Q -68.822605,-81.080001 -68.69992,-81.080001 Q -68.585598,-81.080001 -68.471275,-81.080001 Q -68.366754,-81.080001 -68.262233,-81.080001 Q -68.168827,-81.080001 -68.075421,-81.080001 Q -67.994305,-81.080001 -67.91319,-81.080001 Q -67.845384,-81.080001 -67.777578,-81.080001 Q -67.723935,-81.080001 -67.670292,-81.080001 Q -67.631486,-81.080001 -67.59268,-81.080001 Q -67.5692,-81.080001 -67.545719,-81.080001 Q -67.53786,-81.080001 -67.53,-81.080001" />
<line x1="-102.17" y1="-81.080001" x2="-70.03" y2="-81.080001" />
<line x1="-104.67" y1="-70.080001" x2="-104.67" y2="-81.080001" />
<path d="M -102.17,-81.080001 Q -102.309956,-81.080001 -102.449911,-81.080001 Q -102.728264,-81.080001 -102.995698,-81.080001 Q -103.125204,-81.080001 -103.254709,-81.080001 Q -103.377395,-81.080001 -103.50008,-81.080001 Q -103.614402,-81.080001 -103.728725,-81.080001 Q -103.833246,-81.080001 -103.937767,-81.080001 Q -104.031173,-81.080001 -104.124579,-81.080001 Q -104.205695,-81.080001 -104.28681,-81.080001 Q -104.354616,-81.080001 -104.422422,-81.080001 Q -104.476065,-81.080001 -104.529708,-81.080001 Q -104.568514,-81.080001 -104.60732,-81.080001 Q -104.6308,-81.080001 -104.654281,-81.080001 Q -104.66214,-81.080001 -104.67,-81.080001" />
<path d="M -109.67,-81.080001 L -109.110178,-81.080001 L -108.557395,-81.080001 L -108.018605,-81.080001 L -107.500581,-81.080001 L -107.00984,-81.080001 L -106.552551,-81.080001 L -106.134466,-81.080001 L -105.760843,-81.080001 L -105.436379,-81.080001 L -105.165156,-81.080001 L -104.950583,-81.080001 L -104.79536,-81.080001 L -104.701439,-81.080001 L -104.67,-81.080001" />
<line x1="-111.8" y1="-81.080001" x2="-109.67" y2="-81.080001" />
<line x1="-114.3" y1="-70.080001" x2="-114.3" y2="-81.080001" />
<path d="M -114.3,-81.080001 Q -114.29214,-81.080001 -114.284281,-81.080001 Q -114.2608,-81.080001 -114.23732,-81.080001 Q -114.198514,-81.080001 -114.159708,-81.080001 Q -114.106065,-81.080001 -114.052422,-81.080001 Q -113.984616,-81.080001 -113.91681,-81.080001 Q -113.835695,-81.080001 -113.754579,-81.080001 Q -113.661173,-81.080001 -113.567767,-81.080001 Q -113.463246,-81.080001 -113.358725,-81.080001 Q -113.244402,-81.080001 -113.13008,-81.080001 Q -113.007395,-81.080001 -112.884709,-81.080001 Q -112.755204,-81.080001 -112.625698,-81.080001 Q -112.491,-81.080001 -112.356302,-81.080001 Q -112.080898,-81.080001 -111.8,-81.080001" />
<line x1="-82.925" y1="-23.480001" x2="-82.925" y2="-19.480001" />
<path d="M -89.275,-4.480001 L -89.195396,-4.480001 L -88.960576,-4.480001 L -88.582315,-4.480001 L -88.07958,-4.480001 L -87.477581,-4.480001 L -86.806504,-4.480001 L -86.1,-4.480001 L -85.393496,-4.480001 L -84.722419,-4.480001 L -84.12042,-4.480001 L -83.617685,-4.480001 L -83.239424,-4.480001 L -83.004604,-4.480001 L -82.925,-4.480001" />
<line x1="-111.8" y1="-29.880001" x2="-111.8" y2="-36.080001" />
<line x1="-109.67" y1="-29.880001" x2="-109.67" y2="-36.080001" />
<line x1="-60.4" y1="-29.880001" x2="-60.4" y2="-36.080001" />
<line x1="-62.53" y1="-29.880001" x2="-62.53" y2="-36.080001" />
<line x1="-111.8" y1="-36.080001" x2="-111.8" y2="-70.080001" />
<line x1="-109.67" y1="-36.080001" x2="-109.67" y2="-70.080001" />
<line x1="-102.17" y1="-36.080001" x2="-102.17" y2="-70.080001" />
<line x1="-70.03" y1="-36.080001" x2="-70.03" y2="-70.080001" />
<line x1="-62.53" y1="-36.080001" x2="-62.53" y2="-70.080001" />
<line x1="-60.4" y1="-36.080001" x2="-60.4" y2="-70.080001" />
<line x1="-60.4" y1="-70.080001" x2="-60.4" y2="-81.080001" />
<line x1="-62.53" y1="-70.080001" x2="-62.53" y2="-81.080001" />
<line x1="-70.03" y1="-70.080001" x2="-70.03" y2="-81.080001" />
<line x1="-102.17" y1="-70.080001" x2="-102.17" y2="-81.080001" />
<line x1="-109.67" y1="-70.080001" x2="-109.67" y2="-81.080001" />
<line x1="-111.8" y1="-70.080001" x2="-111.8" y2="-81.080001" />
<line x1="-105.15" y1="-23.480001" x2="-105.15" y2="-25.080001" />
<line x1="-89.275" y1="-4.480001" x2="-89.275" y2="-23.480001" />
<line x1="-82.925" y1="-4.480001" x2="-82.925" y2="-19.480001" />
<path d="M -28.2,30.096001 Q -28.185538,30.096001 -28.171076,30.096001 Q -28.127872,30.096001 -28.084668,30.096001 Q -28.013266,30.096001 -27.941863,30.096001 Q -27.84316,30.096001 -27.744457,30.096001 Q -27.619694,30.096001 -27.494931,30.096001 Q -27.345678,30.096001 -27.196425,30.096001 Q -27.024558,30.096001 -26.852691,30.096001 Q -26.660372,30.096001 -26.468053,30.096001 Q -26.2577,30.096001 -26.047348,30.096001 Q -25.821606,30.096001 -25.595865,30.096001 Q -25.357574,30.096001 -25.119284,30.096001 Q -24.87144,30.096001 -24.623596,30.096001 Q -24.116853,30.096001 -23.6,30.096001" />
<path d="M 23.6,30.096001 L 24.115037,30.096001 L 24.623596,30.096001 L 25.119284,30.096001 L 25.595865,30.096001 L 26.047348,30.096001 L 26.468053,30.096001 L 26.852691,30.096001 L 27.196425,30.096001 L 27.494931,30.096001 L 27.744457,30.096001 L 27.941863,30.096001 L 28.084668,30.096001 L 28.171076,30.096001 L 28.2,30.096001" />
<line x1="-23.6" y1="30.096001" x2="23.6" y2="30.096001" />
<path d="M 16.07,19.096001 Q 16.209956,19.096001 16.349911,19.096001 Q 16.628264,19.096001 16.895698,19.096001 Q 17.025204,19.096001 17.154709,19.096001 Q 17.277395,19.096001 17.40008,19.096001 Q 17.514402,19.096001 17.628725,19.096001 Q 17.733246,19.096001 17.837767,19.096001 Q 17.931173,19.096001 18.024579,19.096001 Q 18.105695,19.096001 18.18681,19.096001 Q 18.254616,19.096001 18.322422,19.096001 Q 18.376065,19.096001 18.429708,19.096001 Q 18.468514,19.096001 18.50732,19.096001 Q 18.5308,19.096001 18.554281,19.096001 Q 18.56214,19.096001 18.57,19.096001" />
<line x1="-16.07" y1="19.096001" x2="16.07" y2="19.096001" />
<path d="M -16.07,19.096001 Q -16.209956,19.096001 -16.349911,19.096001 Q -16.628264,19.096001 -16.895698,19.096001 Q -17.025204,19.096001 -17.154709,19.096001 Q -17.277395,19.096001 -17.40008,19.096001 Q -17.514402,19.096001 -17.628725,19.096001 Q -17.733246,19.096001 -17.837767,19.096001 Q -17.931173,19.096001 -18.024579,19.096001 Q -18.105695,19.096001 -18.18681,19.096001 Q -18.254616,19.096001 -18.322422,19.096001 Q -18.376065,19.096001 -18.429708,19.096001 Q -18.468514,19.096001 -18.50732,19.096001 Q -18.5308,19.096001 -18.554281,19.096001 Q -18.56214,19.096001 -18.57,19.096001" />
<path d="M 18.57,19.096001 Q 18.585719,19.096001 18.601439,19.096001 Q 18.6484,19.096001 18.69536,19.096001 Q 18.772972,19.096001 18.850583,19.096001 Q 18.95787,19.096001 19.065156,19.096001 Q 19.200767,19.096001 19.336379,19.096001 Q 19.498611,19.096001 19.660843,19.096001 Q 19.847654,19.096001 20.034466,19.096001 Q 20.243509,19.096001 20.452551,19.096001 Q 20.681195,19.096001 20.90984,19.096001 Q 21.15521,19.096001 21.400581,19.096001 Q 21.659593,19.096001 21.918605,19.096001 Q 22.188,19.096001 22.457395,19.096001 Q 23.008203,19.096001 23.57,19.096001" />
<line x1="25.7" y1="19.096001" x2="23.57" y2="19.096001" />
<path d="M -23.57,19.096001 L -23.010178,19.096001 L -22.457395,19.096001 L -21.918605,19.096001 L -21.400581,19.096001 L -20.90984,19.096001 L -20.452551,19.096001 L -20.034466,19.096001 L -19.660843,19.096001 L -19.336379,19.096001 L -19.065156,19.096001 L -18.850583,19.096001 L -18.69536,19.096001 L -18.601439,19.096001 L -18.57,19.096001" />
<path d="M 28.2,19.096001 Q 28.19214,19.096001 28.184281,19.096001 Q 28.1608,19.096001 28.13732,19.096001 Q 28.098514,19.096001 28.059708,19.096001 Q 28.006065,19.096001 27.952422,19.096001 Q 27.884616,19.096001 27.81681,19.096001 Q 27.735695,19.096001 27.654579,19.096001 Q 27.561173,19.096001 27.467767,19.096001 Q 27.363246,19.096001 27.258725,19.096001 Q 27.144402,19.096001 27.03008,19.096001 Q 26.907395,19.096001 26.784709,19.096001 Q 26.655204,19.096001 26.525698,19.096001 Q 26.391,19.096001 26.256302,19.096001 Q 25.980898,19.096001 25.7,19.096001" />
<line x1="-25.7" y1="19.096001" x2="-23.57" y2="19.096001" />
<path d="M -28.2,19.096001 Q -28.19214,19.096001 -28.184281,19.096001 Q -28.1608,19.096001 -28.13732,19.096001 Q -28.098514,19.096001 -28.059708,19.096001 Q -28.006065,19.096001 -27.952422,19.096001 Q -27.884616,19.096001 -27.81681,19.096001 Q -27.735695,19.096001 -27.654579,19.096001 Q -27.561173,19.096001 -27.467767,19.096001 Q -27.363246,19.096001 -27.258725,19.096001 Q -27.144402,19.096001 -27.03008,19.096001 Q -26.907395,19.096001 -26.784709,19.096001 Q -26.655204,19.096001 -26.525698,19.096001 Q -26.391,19.096001 -26.256302,19.096001 Q -25.980898,19.096001 -25.7,19.096001" />
<line x1="-28.2" y1="25.296001" x2="-28.2" y2="19.096001" />
<line x1="-28.2" y1="30.096001" x2="-28.2" y2="25.296001" />
<line x1="28.2" y1="30.096001" x2="28.2" y2="25.296001" />
<line x1="28.2" y1="25.296001" x2="28.2" y2="19.096001" />
<line x1="-23.6" y1="30.096001" x2="-23.6" y2="25.296001" />
<path d="M -28.2,25.296001 Q -28.185538,25.296001 -28.171076,25.296001 Q -28.127872,25.296001 -28.084668,25.296001 Q -28.013266,25.296001 -27.941863,25.296001 Q -27.84316,25.296001 -27.744457,25.296001 Q -27.619694,25.296001 -27.494931,25.296001 Q -27.345678,25.296001 -27.196425,25.296001 Q -27.024558,25.296001 -26.852691,25.296001 Q -26.660372,25.296001 -26.468053,25.296001 Q -26.2577,25.296001 -26.047348,25.296001 Q -25.821606,25.296001 -25.595865,25.296001 Q -25.357574,25.296001 -25.119284,25.296001 Q -24.87144,25.296001 -24.623596,25.296001 Q -24.116853,25.296001 -23.6,25.296001" />
<line x1="23.6" y1="30.096001" x2="23.6" y2="25.296001" />
<path d="M 23.6,25.296001 L 24.115037,25.296001 L 24.623596,25.296001 L 25.119284,25.296001 L 25.595865,25.296001 L 26.047348,25.296001 L 26.468053,25.296001 L 26.852691,25.296001 L 27.196425,25.296001 L 27.494931,25.296001 L 27.744457,25.296001 L 27.941863,25.296001 L 28.084668,25.296001 L 28.171076,25.296001 L 28.2,25.296001" />
<line x1="-23.6" y1="25.296001" x2="-16.07" y2="25.296001" />
<line x1="-16.07" y1="25.296001" x2="-16.07" y2="19.096001" />
<line x1="16.07" y1="25.296001" x2="16.07" y2="19.096001" />
<line x1="16.07" y1="25.296001" x2="23.6" y2="25.296001" />
<path d="M 0.0,31.696001 L 2.132923,31.696001 L 4.239024,31.696001 L 6.291816,31.696001 L 8.265485,31.696001 L 10.135211,31.696001 L 11.877481,31.696001 L 13.470384,31.696001 L 14.89389,31.696001 L 16.130096,31.696001 L 17.163457,31.696001 L 17.980977,31.696001 L 18.572377,31.696001 L 18.930218,31.696001 L 19.05,31.696001" />
<path d="M -19.05,31.696001 L -18.930218,31.696001 L -18.572377,31.696001 L -17.980977,31.696001 L -17.163457,31.696001 L -16.130096,31.696001 L -14.89389,31.696001 L -13.470384,31.696001 L -11.877481,31.696001 L -10.135211,31.696001 L -8.265485,31.696001 L -6.291816,31.696001 L -4.239024,31.696001 L -2.132923,31.696001 L -0.0,31.696001" />
<line x1="18.57" y1="25.296001" x2="18.57" y2="19.096001" />
<line x1="-18.57" y1="25.296001" x2="-18.57" y2="19.096001" />
<line x1="18.47" y1="19.096001" x2="18.47" y2="-14.903999" />
<line x1="28.1" y1="19.096001" x2="28.1" y2="-14.903999" />
<line x1="-28.1" y1="19.096001" x2="-28.1" y2="-14.903999" />
<line x1="-18.47" y1="19.096001" x2="-18.47" y2="-14.903999" />
<path d="M 16.07,-14.903999 Q 16.209956,-14.903999 16.349911,-14.903999 Q 16.628264,-14.903999 16.895698,-14.903999 Q 17.025204,-14.903999 17.154709,-14.903999 Q 17.277395,-14.903999 17.40008,-14.903999 Q 17.514402,-14.903999 17.628725,-14.903999 Q 17.733246,-14.903999 17.837767,-14.903999 Q 17.931173,-14.903999 18.024579,-14.903999 Q 18.105695,-14.903999 18.18681,-14.903999 Q 18.254616,-14.903999 18.322422,-14.903999 Q 18.376065,-14.903999 18.429708,-14.903999 Q 18.468514,-14.903999 18.50732,-14.903999 Q 18.5308,-14.903999 18.554281,-14.903999 Q 18.56214,-14.903999 18.57,-14.903999" />
<line x1="-16.07" y1="-14.903999" x2="16.07" y2="-14.903999" />
<path d="M -16.07,-14.903999 Q -16.209956,-14.903999 -16.349911,-14.903999 Q -16.628264,-14.903999 -16.895698,-14.903999 Q -17.025204,-14.903999 -17.154709,-14.903999 Q -17.277395,-14.903999 -17.40008,-14.903999 Q -17.514402,-14.903999 -17.628725,-14.903999 Q -17.733246,-14.903999 -17.837767,-14.903999 Q -17.931173,-14.903999 -18.024579,-14.903999 Q -18.105695,-14.903999 -18.18681,-14.903999 Q -18.254616,-14.903999 -18.322422,-14.903999 Q -18.376065,-14.903999 -18.429708,-14.903999 Q -18.468514,-14.903999 -18.50732,-14.903999 Q -18.5308,-14.903999 -18.554281,-14.903999 Q -18.56214,-14.903999 -18.57,-14.903999" />
<path d="M 18.57,-14.903999 Q 18.585719,-14.903999 18.601439,-14.903999 Q 18.6484,-14.903999 18.69536,-14.903999 Q 18.772972,-14.903999 18.850583,-14.903999 Q 18.95787,-14.903999 19.065156,-14.903999 Q 19.200767,-14.903999 19.336379,-14.903999 Q 19.498611,-14.903999 19.660843,-14.903999 Q 19.847654,-14.903999 20.034466,-14.903999 Q 20.243509,-14.903999 20.452551,-14.903999 Q 20.681195,-14.903999 20.90984,-14.903999 Q 21.15521,-14.903999 21.400581,-14.903999 Q 21.659593,-14.903999 21.918605,-14.903999 Q 22.188,-14.903999 22.457395,-14.903999 Q 23.008203,-14.903999 23.57,-14.903999" />
<line x1="25.7" y1="-14.903999" x2="23.57" y2="-14.903999" />
<path d="M -23.57,-14.903999 L -23.010178,-14.903999 L -22.457395,-14.903999 L -21.918605,-14.903999 L -21.400581,-14.903999 L -20.90984,-14.903999 L -20.452551,-14.903999 L -20.034466,-14.903999 L -19.660843,-14.903999 L -19.336379,-14.903999 L -19.065156,-14.903999 L -18.850583,-14.903999 L -18.69536,-14.903999 L -18.601439,-14.903999 L -18.57,-14.903999" />
<path d="M 28.2,-14.903999 Q 28.19214,-14.903999 28.184281,-14.903999 Q 28.1608,-14.903999 28.13732,-14.903999 Q 28.098514,-14.903999 28.059708,-14.903999 Q 28.006065,-14.903999 27.952422,-14.903999 Q 27.884616,-14.903999 27.81681,-14.903999 Q 27.735695,-14.903999 27.654579,-14.903999 Q 27.561173,-14.903999 27.467767,-14.903999 Q 27.363246,-14.903999 27.258725,-14.903999 Q 27.144402,-14.903999 27.03008,-14.903999 Q 26.907395,-14.903999 26.784709,-14.903999 Q 26.655204,-14.903999 26.525698,-14.903999 Q 26.391,-14.903999 26.256302,-14.903999 Q 25.980898,-14.903999 25.7,-14.903999" />
<line x1="-25.7" y1="-14.903999" x2="-23.57" y2="-14.903999" />
<path d="M -28.2,-14.903999 Q -28.19214,-14.903999 -28.184281,-14.903999 Q -28.1608,-14.903999 -28.13732,-14.903999 Q -28.098514,-14.903999 -28.059708,-14.903999 Q -28.006065,-14.903999 -27.952422,-14.903999 Q -27.884616,-14.903999 -27.81681,-14.903999 Q -27.735695,-14.903999 -27.654579,-14.903999 Q -27.561173,-14.903999 -27.467767,-14.903999 Q -27.363246,-14.903999 -27.258725,-14.903999 Q -27.144402,-14.903999 -27.03008,-14.903999 Q -26.907395,-14.903999 -26.784709,-14.903999 Q -26.655204,-14.903999 -26.525698,-14.903999 Q -26.391,-14.903999 -26.256302,-14.903999 Q -25.980898,-14.903999 -25.7,-14.903999" />
<line x1="18.57" y1="-14.903999" x2="18.57" y2="-25.903999" />
<path d="M 16.07,-25.903999 Q 16.209956,-25.903999 16.349911,-25.903999 Q 16.628264,-25.903999 16.895698,-25.903999 Q 17.025204,-25.903999 17.154709,-25.903999 Q 17.277395,-25.903999 17.40008,-25.903999 Q 17.514402,-25.903999 17.628725,-25.903999 Q 17.733246,-25.903999 17.837767,-25.903999 Q 17.931173,-25.903999 18.024579,-25.903999 Q 18.105695,-25.903999 18.18681,-25.903999 Q 18.254616,-25.903999 18.322422,-25.903999 Q 18.376065,-25.903999 18.429708,-25.903999 Q 18.468514,-25.903999 18.50732,-25.903999 Q 18.5308,-25.903999 18.554281,-25.903999 Q 18.56214,-25.903999 18.57,-25.903999" />
<line x1="-16.07" y1="-25.903999" x2="16.07" y2="-25.903999" />
<line x1="-18.57" y1="-14.903999" x2="-18.57" y2="-25.903999" />
<path d="M -16.07,-25.903999 Q -16.209956,-25.903999 -16.349911,-25.903999 Q -16.628264,-25.903999 -16.895698,-25.903999 Q -17.025204,-25.903999 -17.154709,-25.903999 Q -17.277395,-25.903999 -17.40008,-25.903999 Q -17.514402,-25.903999 -17.628725,-25.903999 Q -17.733246,-25.903999 -17.837767,-25.903999 Q -17.931173,-25.903999 -18.024579,-25.903999 Q -18.105695,-25.903999 -18.18681,-25.903999 Q -18.254616,-25.903999 -18.322422,-25.903999 Q -18.376065,-25.903999 -18.429708,-25.903999 Q -18.468514,-25.903999 -18.50732,-25.903999 Q -18.5308,-25.903999 -18.554281,-25.903999 Q -18.56214,-25.903999 -18.57,-25.903999" />
<path d="M 18.57,-25.903999 Q 18.585719,-25.903999 18.601439,-25.903999 Q 18.6484,-25.903999 18.69536,-25.903999 Q 18.772972,-25.903999 18.850583,-25.903999 Q 18.95787,-25.903999 19.065156,-25.903999 Q 19.200767,-25.903999 19.336379,-25.903999 Q 19.498611,-25.903999 19.660843,-25.903999 Q 19.847654,-25.903999 20.034466,-25.903999 Q 20.243509,-25.903999 20.452551,-25.903999 Q 20.681195,-25.903999 20.90984,-25.903999 Q 21.15521,-25.903999 21.400581,-25.903999 Q 21.659593,-25.903999 21.918605,-25.903999 Q 22.188,-25.903999 22.457395,-25.903999 Q 23.008203,-25.903999 23.57,-25.903999" />
<line x1="25.7" y1="-25.903999" x2="23.57" y2="-25.903999" />
<path d="M -23.57,-25.903999 L -23.010178,-25.903999 L -22.457395,-25.903999 L -21.918605,-25.903999 L -21.400581,-25.903999 L -20.90984,-25.903999 L -20.452551,-25.903999 L -20.034466,-25.903999 L -19.660843,-25.903999 L -19.336379,-25.903999 L -19.065156,-25.903999 L -18.850583,-25.903999 L -18.69536,-25.903999 L -18.601439,-25.903999 L -18.57,-25.903999" />
<line x1="28.2" y1="-14.903999" x2="28.2" y2="-25.903999" />
<path d="M 28.2,-25.903999 Q 28.19214,-25.903999 28.184281,-25.903999 Q 28.1608,-25.903999 28.13732,-25.903999 Q 28.098514,-25.903999 28.059708,-25.903999 Q 28.006065,-25.903999 27.952422,-25.903999 Q 27.884616,-25.903999 27.81681,-25.903999 Q 27.735695,-25.903999 27.654579,-25.903999 Q 27.561173,-25.903999 27.467767,-25.903999 Q 27.363246,-25.903999 27.258725,-25.903999 Q 27.144402,-25.903999 27.03008,-25.903999 Q 26.907395,-25.903999 26.784709,-25.903999 Q 26.655204,-25.903999 26.525698,-25.903999 Q 26.391,-25.903999 26.256302,-25.903999 Q 25.980898,-25.903999 25.7,-25.903999" />
<line x1="-25.7" y1="-25.903999" x2="-23.57" y2="-25.903999" />
<line x1="-28.2" y1="-14.903999" x2="-28.2" y2="-25.903999" />
<path d="M -28.2,-25.903999 Q -28.19214,-25.903999 -28.184281,-25.903999 Q -28.1608,-25.903999 -28.13732,-25.903999 Q -28.098514,-25.903999 -28.059708,-25.903999 Q -28.006065,-25.903999 -27.952422,-25.903999 Q -27.884616,-25.903999 -27.81681,-25.903999 Q -27.735695,-25.903999 -27.654579,-25.903999 Q -27.561173,-25.903999 -27.467767,-25.903999 Q -27.363246,-25.903999 -27.258725,-25.903999 Q -27.144402,-25.903999 -27.03008,-25.903999 Q -26.907395,-25.903999 -26.784709,-25.903999 Q -26.655204,-25.903999 -26.525698,-25.903999 Q -26.391,-25.903999 -26.256302,-25.903999 Q -25.980898,-25.903999 -25.7,-25.903999" />
<path d="M 0.0,35.696001 C 0.530549,35.696001 1.058689,35.696001 1.524884,35.696001 C 1.58996,35.696001 1.655037,35.696001 1.720114,35.696001 C 1.782255,35.696001 1.844397,35.696001 1.906538,35.696001 C 1.965426,35.696001 2.024313,35.696001 2.083201,35.696001 C 2.138534,35.696001 2.193866,35.696001 2.249199,35.696001 C 2.300693,35.696001 2.352187,35.696001 2.403681,35.696001 C 2.451074,35.696001 2.498466,35.696001 2.545858,35.696001 C 2.588905,35.696001 2.631953,35.696001 2.675,35.696001" />
<path d="M 2.675,35.696001 C 2.999597,35.696001 3.175154,35.696001 3.175,35.696001" />
<line x1="2.675" y1="35.696001" x2="2.675" y2="50.696001" />
<path d="M -3.175,50.696001 L -3.12154,50.696001 L -2.962961,50.696001 L -2.704603,50.696001 L -2.355167,50.696001 L -1.926419,50.696001 L -1.432798,50.696001 L -0.890926,50.696001 L -0.319053,50.696001 L 0.263565,50.696001 L 0.837307,50.696001 L 1.382852,50.696001 L 1.881829,50.696001 L 2.317435,50.696001 L 2.675,50.696001" />
<line x1="2.675" y1="35.696001" x2="2.675" y2="50.696001" />
<line x1="23.57" y1="25.296001" x2="23.57" y2="19.096001" />
<line x1="25.7" y1="25.296001" x2="25.7" y2="19.096001" />
<line x1="-23.57" y1="25.296001" x2="-23.57" y2="19.096001" />
<line x1="-25.7" y1="25.296001" x2="-25.7" y2="19.096001" />
<line x1="16.07" y1="19.096001" x2="16.07" y2="-14.903999" />
<line x1="23.57" y1="19.096001" x2="23.57" y2="-14.903999" />
<line x1="25.7" y1="19.096001" x2="25.7" y2="-14.903999" />
<line x1="-25.7" y1="19.096001" x2="-25.7" y2="-14.903999" />
<line x1="-23.57" y1="19.096001" x2="-23.57" y2="-14.903999" />
<line x1="-16.07" y1="19.096001" x2="-16.07" y2="-14.903999" />
<line x1="16.07" y1="-14.903999" x2="16.07" y2="-25.903999" />
<line x1="-16.07" y1="-14.903999" x2="-16.07" y2="-25.903999" />
<line x1="23.57" y1="-14.903999" x2="23.57" y2="-25.903999" />
<line x1="25.7" y1="-14.903999" x2="25.7" y2="-25.903999" />
<line x1="-23.57" y1="-14.903999" x2="-23.57" y2="-25.903999" />
<line x1="-25.7" y1="-14.903999" x2="-25.7" y2="-25.903999" />
<line x1="-19.05" y1="31.696001" x2="-19.05" y2="30.096001" />
<line x1="19.05" y1="31.696001" x2="19.05" y2="30.096001" />
<line x1="-3.175" y1="50.696001" x2="-3.175" y2="31.696001" />
<line x1="3.175" y1="35.696001" x2="3.175" y2="31.696001" />
<path d="M 133.5,89.25 L -133.5,89.25 L -133.5,89.0 L -133.75,89.0 L -133.75,-89.0 L -133.5,-89.0 L -133.5,-89.25 L 133.5,-89.25 L 133.5,-89.0 L 133.75,-89.0 L 133.75,89.0 L 133.5,89.0 L 133.5,89.25 M -133.25,88.75 L 133.25,88.75 L 133.25,-88.75 L -133.25,-88.75 L -133.25,88.75" />
<path d="M -89.25,99.0 L -89.25,89.0 L -88.75,89.0 L -88.75,99.0 L -89.25,99.0" />
<path d="M -44.75,99.0 L -44.75,89.0 L -44.25,89.0 L -44.25,99.0 L -44.75,99.0" />
<path d="M -0.25,99.0 L -0.25,89.0 L 0.25,89.0 L 0.25,99.0 L -0.25,99.0" />
<path d="M 44.25,99.0 L 44.25,89.0 L 44.75,89.0 L 44.75,99.0 L 44.25,99.0" />
<path d="M 88.75,99.0 L 88.75,89.0 L 89.25,89.0 L 89.25,99.0 L 88.75,99.0" />
<path d="M 143.5,44.75 L 133.5,44.75 L 133.5,44.25 L 143.5,44.25 L 143.5,44.75" />
<path d="M 143.5,0.25 L 133.5,0.25 L 133.5,-0.25 L 143.5,-0.25 L 143.5,0.25" />
<path d="M 143.5,-44.25 L 133.5,-44.25 L 133.5,-44.75 L 143.5,-44.75 L 143.5,-44.25" />
<path d="M 89.25,-99.0 L 89.25,-89.0 L 88.75,-89.0 L 88.75,-99.0 L 89.25,-99.0" />
<path d="M 44.75,-99.0 L 44.75,-89.0 L 44.25,-89.0 L 44.25,-99.0 L 44.75,-99.0" />
<path d="M 0.25,-99.0 L 0.25,-89.0 L -0.25,-89.0 L -0.25,-99.0 L 0.25,-99.0" />
<path d="M -44.25,-99.0 L -44.25,-89.0 L -44.75,-89.0 L -44.75,-99.0 L -44.25,-99.0" />
<path d="M -88.75,-99.0 L -88.75,-89.0 L -89.25,-89.0 L -89.25,-99.0 L -88.75,-99.0" />
<path d="M -143.5,-44.75 L -133.5,-44.75 L -133.5,-44.25 L -143.5,-44.25 L -143.5,-44.75" />
<path d="M -143.5,-0.25 L -133.5,-0.25 L -133.5,0.25 L -143.5,0.25 L -143.5,-0.25" />
<path d="M -143.5,44.25 L -133.5,44.25 L -133.5,44.75 L -143.5,44.75 L -143.5,44.25" />
<path d="M -141.18999,-64.200001 L -141.18999,-69.250001 L -140.31001,-69.250001 L -140.31001,-62.159994 L -140.88999,-62.159994 Q -141.120003,-62.980014 -141.429997,-63.21001 Q -141.73999,-63.440007 -142.76001,-63.569987 L -142.76001,-64.200001 L -141.18999,-64.200001" />
<path d="M 140.81001,-64.200001 L 140.81001,-69.250001 L 141.68999,-69.250001 L 141.68999,-62.159994 L 141.11001,-62.159994 Q 140.879997,-62.980014 140.570003,-63.21001 Q 140.26001,-63.440007 139.23999,-63.569987 L 139.23999,-64.200001 L 140.81001,-64.200001" />
<path d="M -143.279997,-20.119987 L -142.399984,-20.119987 Q -142.299984,-18.430014 -140.970003,-18.430014 Q -140.370003,-18.430014 -139.970003,-18.81001 Q -139.570003,-19.190007 -139.570003,-19.759994 Q -139.570003,-20.609994 -140.529997,-21.159994 L -141.449984,-21.680014 Q -142.56001,-22.309994 -142.970003,-22.945004 Q -143.379997,-23.580014 -143.43999,-24.750001 L -138.720003,-24.750001 L -138.720003,-23.880014 L -142.449984,-23.880014 Q -142.379997,-23.430014 -142.1,-23.100001 Q -141.820003,-22.769987 -141.170003,-22.419987 L -140.170003,-21.880014 Q -138.670003,-21.059994 -138.670003,-19.740007 Q -138.670003,-18.819987 -139.3,-18.239991 Q -139.929997,-17.659994 -140.93999,-17.659994 Q -143.21001,-17.659994 -143.279997,-20.119987" />
<path d="M 138.720003,-20.119987 L 139.600016,-20.119987 Q 139.700016,-18.430014 141.029997,-18.430014 Q 141.629997,-18.430014 142.029997,-18.81001 Q 142.429997,-19.190007 142.429997,-19.759994 Q 142.429997,-20.609994 141.470003,-21.159994 L 140.550016,-21.680014 Q 139.43999,-22.309994 139.029997,-22.945004 Q 138.620003,-23.580014 138.56001,-24.750001 L 143.279997,-24.750001 L 143.279997,-23.880014 L 139.550016,-23.880014 Q 139.620003,-23.430014 139.9,-23.100001 Q 140.179997,-22.769987 140.829997,-22.419987 L 141.829997,-21.880014 Q 143.329997,-21.059994 143.329997,-19.740007 Q 143.329997,-18.819987 142.7,-18.239991 Q 142.070003,-17.659994 141.06001,-17.659994 Q 138.78999,-17.659994 138.720003,-20.119987" />
<path d="M -142.429997,24.549999 Q -142.41001,25.240006 -142.125,25.654996 Q -141.83999,26.069986 -141.079997,26.069986 Q -140.499984,26.069986 -140.16499,25.744986 Q -139.829997,25.419986 -139.829997,24.859993 Q -139.829997,24.209993 -140.225,23.98999 Q -140.620003,23.769986 -141.570003,23.749999 L -141.570003,22.999999 L -141.46001,22.999999 L -141.08999,23.009993 Q -139.620003,23.009993 -139.620003,21.719986 Q -139.620003,21.049999 -140.009993,20.674999 Q -140.399984,20.299999 -141.08999,20.299999 Q -141.81001,20.299999 -142.170003,20.670003 Q -142.529997,21.040006 -142.579997,21.809993 L -143.46001,21.809993 Q -143.299984,19.519986 -141.120003,19.519986 Q -140.029997,19.519986 -139.375,20.119986 Q -138.720003,20.719986 -138.720003,21.730013 Q -138.720003,22.409993 -139.0,22.804996 Q -139.279997,23.199999 -139.920003,23.419986 Q -138.929997,23.809993 -138.929997,24.890006 Q -138.929997,25.799999 -139.504997,26.320003 Q -140.079997,26.840006 -141.08999,26.840006 Q -143.26001,26.840006 -143.31001,24.549999 L -142.429997,24.549999" />
<path d="M 139.570003,24.549999 Q 139.58999,25.240006 139.875,25.654996 Q 140.16001,26.069986 140.920003,26.069986 Q 141.500016,26.069986 141.83501,25.744986 Q 142.170003,25.419986 142.170003,24.859993 Q 142.170003,24.209993 141.775,23.98999 Q 141.379997,23.769986 140.429997,23.749999 L 140.429997,22.999999 L 140.53999,22.999999 L 140.91001,23.009993 Q 142.379997,23.009993 142.379997,21.719986 Q 142.379997,21.049999 141.990007,20.674999 Q 141.600016,20.299999 140.91001,20.299999 Q 140.18999,20.299999 139.829997,20.670003 Q 139.470003,21.040006 139.420003,21.809993 L 138.53999,21.809993 Q 138.700016,19.519986 140.879997,19.519986 Q 141.970003,19.519986 142.625,20.119986 Q 143.279997,20.719986 143.279997,21.730013 Q 143.279997,22.409993 143.0,22.804996 Q 142.720003,23.199999 142.079997,23.419986 Q 143.070003,23.809993 143.070003,24.890006 Q 143.070003,25.799999 142.495003,26.320003 Q 141.920003,26.840006 140.91001,26.840006 Q 138.73999,26.840006 138.68999,24.549999 L 139.570003,24.549999" />
<path d="M -140.51001,65.949999 L -140.51001,64.249999 L -139.629997,64.249999 L -139.629997,65.949999 L -138.579997,65.949999 L -138.579997,66.740006 L -139.629997,66.740006 L -139.629997,71.340006 L -140.279997,71.340006 L -143.499984,66.880013 L -143.499984,65.949999 L -140.51001,65.949999 M -140.51001,66.740006 L -142.729997,66.740006 L -140.51001,69.840006 L -140.51001,66.740006" />
<path d="M 141.48999,65.949999 L 141.48999,64.249999 L 142.370003,64.249999 L 142.370003,65.949999 L 143.420003,65.949999 L 143.420003,66.740006 L 142.370003,66.740006 L 142.370003,71.340006 L 141.720003,71.340006 L 138.500016,66.880013 L 138.500016,65.949999 L 141.48999,65.949999 M 141.48999,66.740006 L 139.270003,66.740006 L 141.48999,69.840006 L 141.48999,66.740006" />
<path d="M -112.51499,-95.680014 L -109.03501,-95.680014 L -109.03501,-94.859994 L -112.51499,-94.859994 L -112.51499,-92.530014 L -108.554997,-92.530014 L -108.554997,-91.709994 L -113.445003,-91.709994 L -113.445003,-99.000001 L -112.51499,-99.000001 L -112.51499,-95.680014" />
<path d="M -112.51499,97.319986 L -109.03501,97.319986 L -109.03501,98.140006 L -112.51499,98.140006 L -112.51499,100.469986 L -108.554997,100.469986 L -108.554997,101.290006 L -113.445003,101.290006 L -113.445003,93.999999 L -112.51499,93.999999 L -112.51499,97.319986" />
<path d="M -68.184993,-95.680014 L -64.215007,-95.680014 L -64.215007,-94.859994 L -68.184993,-94.859994 L -68.184993,-92.530014 L -64.065007,-92.530014 L -64.065007,-91.709994 L -69.115007,-91.709994 L -69.115007,-99.000001 L -63.884993,-99.000001 L -63.884993,-98.180014 L -68.184993,-98.180014 L -68.184993,-95.680014" />
<path d="M -68.184993,97.319986 L -64.215007,97.319986 L -64.215007,98.140006 L -68.184993,98.140006 L -68.184993,100.469986 L -64.065007,100.469986 L -64.065007,101.290006 L -69.115007,101.290006 L -69.115007,93.999999 L -63.884993,93.999999 L -63.884993,94.819986 L -68.184993,94.819986 L -68.184993,97.319986" />
<path d="M -24.940007,-99.000001 L -22.130013,-99.000001 Q -20.75,-99.000001 -19.954997,-98.025001 Q -19.159993,-97.050001 -19.159993,-95.350001 Q -19.159993,-93.650001 -19.95,-92.679997 Q -20.740007,-91.709994 -22.130013,-91.709994 L -24.940007,-91.709994 L -24.940007,-99.000001 M -24.009993,-98.180014 L -24.009993,-92.530014 L -22.290007,-92.530014 Q -21.209993,-92.530014 -20.65,-93.250001 Q -20.090007,-93.969987 -20.090007,-95.359994 Q -20.090007,-96.740007 -20.65,-97.46001 Q -21.209993,-98.180014 -22.290007,-98.180014 L -24.009993,-98.180014" />
<path d="M -24.940007,93.999999 L -22.130013,93.999999 Q -20.75,93.999999 -19.954997,94.974999 Q -19.159993,95.949999 -19.159993,97.649999 Q -19.159993,99.349999 -19.95,100.320003 Q -20.740007,101.290006 -22.130013,101.290006 L -24.940007,101.290006 L -24.940007,93.999999 M -24.009993,94.819986 L -24.009993,100.469986 L -22.290007,100.469986 Q -21.209993,100.469986 -20.65,99.749999 Q -20.090007,99.030013 -20.090007,97.640006 Q -20.090007,96.259993 -20.65,95.53999 Q -21.209993,94.819986 -22.290007,94.819986 L -24.009993,94.819986" />
<path d="M 25.245003,-93.969987 Q 24.81499,-91.590007 22.43501,-91.590007 Q 21.674984,-91.590007 21.069987,-91.850001 Q 20.46499,-92.109994 20.109993,-92.504997 Q 19.754997,-92.900001 19.51499,-93.429997 Q 19.274984,-93.959994 19.18999,-94.450001 Q 19.104997,-94.940007 19.104997,-95.440007 Q 19.104997,-95.930014 19.18999,-96.41001 Q 19.274984,-96.890007 19.509993,-97.415007 Q 19.745003,-97.940007 20.1,-98.329997 Q 20.454997,-98.719987 21.05,-98.975001 Q 21.645003,-99.230014 22.395003,-99.230014 Q 25.06499,-99.230014 25.395003,-96.340007 L 24.43501,-96.340007 Q 24.26499,-97.400001 23.784993,-97.904997 Q 23.304997,-98.409994 22.404997,-98.409994 Q 21.31499,-98.409994 20.675,-97.604997 Q 20.03501,-96.800001 20.03501,-95.430014 Q 20.03501,-94.030014 20.65,-93.220004 Q 21.26499,-92.409994 22.324984,-92.409994 Q 23.195003,-92.409994 23.665007,-92.800001 Q 24.13501,-93.190007 24.295003,-93.969987 L 25.245003,-93.969987" />
<path d="M 25.245003,99.030013 Q 24.81499,101.409993 22.43501,101.409993 Q 21.674984,101.409993 21.069987,101.149999 Q 20.46499,100.890006 20.109993,100.495003 Q 19.754997,100.099999 19.51499,99.570003 Q 19.274984,99.040006 19.18999,98.549999 Q 19.104997,98.059993 19.104997,97.559993 Q 19.104997,97.069986 19.18999,96.58999 Q 19.274984,96.109993 19.509993,95.584993 Q 19.745003,95.059993 20.1,94.670003 Q 20.454997,94.280013 21.05,94.024999 Q 21.645003,93.769986 22.395003,93.769986 Q 25.06499,93.769986 25.395003,96.659993 L 24.43501,96.659993 Q 24.26499,95.599999 23.784993,95.095003 Q 23.304997,94.590006 22.404997,94.590006 Q 21.31499,94.590006 20.675,95.395003 Q 20.03501,96.199999 20.03501,97.569986 Q 20.03501,98.969986 20.65,99.779996 Q 21.26499,100.590006 22.324984,100.590006 Q 23.195003,100.590006 23.665007,100.199999 Q 24.13501,99.809993 24.295003,99.030013 L 25.245003,99.030013" />
<path d="M 67.520003,-99.000001 Q 68.48999,-99.000001 69.079997,-98.429997 Q 69.670003,-97.859994 69.670003,-96.919987 Q 69.670003,-96.259994 69.354997,-95.839991 Q 69.03999,-95.419987 68.33999,-95.150001 C 69.013319,-94.836676 69.349984,-94.306674 69.349984,-93.559994 C 69.347379,-93.145108 69.227588,-92.75989 68.924984,-92.364991 C 68.774984,-92.168322 68.549984,-92.009989 68.249984,-91.889991 C 67.949984,-91.769993 67.596653,-91.709994 67.18999,-91.709994 L 64.229997,-91.709994 L 64.229997,-99.000001 L 67.520003,-99.000001 M 66.96001,-92.530014 Q 68.420003,-92.530014 68.420003,-93.690007 Q 68.420003,-94.850001 66.96001,-94.850001 L 65.16001,-94.850001 L 65.16001,-92.530014 L 66.96001,-92.530014 M 67.429997,-98.180014 L 65.16001,-98.180014 L 65.16001,-95.669987 L 67.429997,-95.669987 Q 68.06001,-95.669987 68.4,-96.014991 Q 68.73999,-96.359994 68.73999,-96.930014 Q 68.73999,-97.230014 68.63999,-97.490007 Q 68.53999,-97.750001 68.229997,-97.965007 Q 67.920003,-98.180014 67.429997,-98.180014" />
<path d="M 67.520003,93.999999 Q 68.48999,93.999999 69.079997,94.570003 Q 69.670003,95.140006 69.670003,96.080013 Q 69.670003,96.740006 69.354997,97.160009 Q 69.03999,97.580013 68.33999,97.849999 C 69.013319,98.163324 69.349984,98.693326 69.349984,99.440006 C 69.347379,99.854892 69.227588,100.24011 68.924984,100.635009 C 68.774984,100.831678 68.549984,100.990011 68.249984,101.110009 C 67.949984,101.230007 67.596653,101.290006 67.18999,101.290006 L 64.229997,101.290006 L 64.229997,93.999999 L 67.520003,93.999999 M 66.96001,100.469986 Q 68.420003,100.469986 68.420003,99.309993 Q 68.420003,98.149999 66.96001,98.149999 L 65.16001,98.149999 L 65.16001,100.469986 L 66.96001,100.469986 M 67.429997,94.819986 L 65.16001,94.819986 L 65.16001,97.330013 L 67.429997,97.330013 Q 68.06001,97.330013 68.4,96.985009 Q 68.73999,96.640006 68.73999,96.069986 Q 68.73999,95.769986 68.63999,95.509993 Q 68.53999,95.249999 68.229997,95.034993 Q 67.920003,94.819986 67.429997,94.819986" />
<path d="M 112.63999,-96.809994 L 113.38999,-99.000001 L 114.429997,-99.000001 L 111.870003,-91.709994 L 110.670003,-91.709994 L 108.070003,-99.000001 L 109.06001,-99.000001 L 109.829997,-96.809994 L 112.63999,-96.809994 M 112.379997,-96.030014 L 110.06001,-96.030014 L 111.26001,-92.709994 L 112.379997,-96.030014" />
<path d="M 112.63999,96.190006 L 113.38999,93.999999 L 114.429997,93.999999 L 111.870003,101.290006 L 110.670003,101.290006 L 108.070003,93.999999 L 109.06001,93.999999 L 109.829997,96.190006 L 112.63999,96.190006 M 112.379997,96.969986 L 110.06001,96.969986 L 111.26001,100.290006 L 112.379997,96.969986" />
<path d="M -0.25,-89.0 L 0.25,-89.0 L 0.25,-74.416667 L 44.25,-74.416667 L 44.25,-89.0 L 44.75,-89.0 L 44.75,-74.416667 L 88.75,-74.416667 L 88.75,-89.0 L 89.25,-89.0 L 89.25,-74.416667 L 133.5,-74.416667 L 133.5,-73.916667 L 44.75,-73.916667 L 44.75,-59.583333 L 133.5,-59.583333 L 133.5,-59.083333 L 44.75,-59.083333 L 44.75,-44.75 L 133.5,-44.75 L 133.5,-44.25 L 0.0,-44.25 L 0.0,-44.5 L -0.25,-44.5 L -0.25,-89.0 M 0.25,-73.916667 L 0.25,-59.583333 L 44.25,-59.583333 L 44.25,-73.916667 L 0.25,-73.916667 M 0.25,-59.083333 L 0.25,-44.75 L 44.25,-44.75 L 44.25,-59.083333 L 0.25,-59.083333" />
<path d="M 2.0,-49.404997 L 2.936665,-49.404997 Q 3.396669,-49.404997 3.66167,-49.079997 Q 3.926671,-48.754997 3.926671,-48.18833 Q 3.926671,-47.621663 3.663336,-47.298329 Q 3.4,-46.974995 2.936665,-46.974995 L 2.0,-46.974995 L 2.0,-49.404997 M 2.310004,-49.131668 L 2.310004,-47.248334 L 2.883333,-47.248334 C 3.361951,-47.246599 3.618055,-47.575616 3.616667,-48.191661 C 3.618055,-48.802917 3.361951,-49.133751 2.883333,-49.131668 L 2.310004,-49.131668" />
<path d="M 4.563346,-48.298334 L 5.886675,-48.298334 L 5.886675,-48.024995 L 4.563346,-48.024995 L 4.563346,-47.248334 L 5.936675,-47.248334 L 5.936675,-46.974995 L 4.253342,-46.974995 L 4.253342,-49.404997 L 5.99668,-49.404997 L 5.99668,-49.131668 L 4.563346,-49.131668 L 4.563346,-48.298334" />
<path d="M 7.326682,-48.374995 C 7.624461,-48.452772 7.773351,-48.583885 7.773351,-48.768332 C 7.772135,-48.871586 7.745123,-48.957295 7.661681,-49.051666 C 7.580668,-49.14326 7.401029,-49.210064 7.153342,-49.208328 C 7.022237,-49.208328 6.909462,-49.191106 6.815017,-49.156662 C 6.720573,-49.122219 6.649461,-49.076109 6.601682,-49.018332 C 6.505255,-48.901739 6.467546,-48.78715 6.466678,-48.648334 L 6.466678,-48.631668 L 6.173351,-48.631668 C 6.175014,-48.896772 6.271678,-49.109337 6.393343,-49.229997 C 6.455565,-49.291108 6.53001,-49.341108 6.616678,-49.379997 C 6.79175,-49.459337 6.949389,-49.480106 7.133344,-49.481668 C 7.419802,-49.484098 7.661336,-49.404794 7.79668,-49.304997 C 7.865567,-49.253883 7.922233,-49.194437 7.966678,-49.12666 C 8.057303,-48.990237 8.081608,-48.868091 8.083344,-48.73833 C 8.083344,-48.589444 8.038345,-48.461666 7.948345,-48.354997 C 7.858346,-48.248327 7.731124,-48.17277 7.566678,-48.128326 L 6.956684,-47.965001 Q 6.736675,-47.904997 6.64668,-47.824995 Q 6.556684,-47.744992 6.556684,-47.604997 Q 6.556684,-47.421663 6.705013,-47.308328 Q 6.853342,-47.194992 7.100011,-47.194992 Q 7.390017,-47.194992 7.54668,-47.323329 Q 7.703342,-47.451666 7.706684,-47.68833 L 8.000011,-47.68833 Q 7.99668,-47.331668 7.763346,-47.133333 Q 7.530013,-46.934999 7.110015,-46.934999 Q 6.710015,-46.934999 6.478348,-47.126666 Q 6.24668,-47.318332 6.24668,-47.648334 Q 6.24668,-48.091661 6.723351,-48.215001 L 7.326682,-48.374995" />
<path d="M 8.850011,-46.974995 L 8.536675,-46.974995 L 8.536675,-49.404997 L 8.850011,-49.404997 L 8.850011,-46.974995" />
<path d="M 11.133355,-48.461659 C 11.133355,-48.67944 11.061688,-48.85833 10.918354,-48.998329 C 10.77502,-49.138328 10.592242,-49.208328 10.37002,-49.208328 C 10.236686,-49.208328 10.117797,-49.18555 10.013352,-49.139996 C 9.801683,-49.049061 9.689469,-48.90815 9.606684,-48.721663 C 9.525288,-48.533963 9.501412,-48.368255 9.500022,-48.198334 C 9.500022,-47.900555 9.5778,-47.661109 9.733355,-47.479997 C 9.888911,-47.298884 10.095577,-47.208328 10.353353,-47.208328 C 10.537793,-47.208328 10.692792,-47.253328 10.818349,-47.343327 C 10.943906,-47.433326 11.023351,-47.556104 11.056684,-47.711659 L 11.373351,-47.711659 C 11.331127,-47.464994 11.220571,-47.273884 11.041683,-47.13833 C 10.862795,-47.002776 10.634462,-46.934999 10.356684,-46.934999 C 10.178906,-46.934999 10.019463,-46.964442 9.878353,-47.023329 C 9.737243,-47.082216 9.625022,-47.156104 9.541688,-47.244992 C 9.458355,-47.333881 9.388911,-47.437216 9.333355,-47.554997 C 9.219814,-47.791948 9.192448,-48.000272 9.190017,-48.215001 C 9.190017,-48.58833 9.29335,-48.892773 9.500016,-49.128331 C 9.706682,-49.363889 9.974461,-49.481668 10.303353,-49.481668 C 10.625575,-49.481668 10.902243,-49.352776 11.133355,-49.094992 L 11.210015,-49.418332 L 11.406684,-49.418332 L 11.406684,-48.121663 L 10.393349,-48.121663 L 10.393349,-48.394992 L 11.133355,-48.394992 L 11.133355,-48.461659" />
<path d="M 13.706684,-46.974995 L 13.413346,-46.974995 L 13.413346,-48.961659 L 12.143349,-46.974995 L 11.806684,-46.974995 L 11.806684,-49.404997 L 12.100022,-49.404997 L 12.100022,-47.434999 L 13.356684,-49.404997 L 13.706684,-49.404997 L 13.706684,-46.974995" />
<path d="M 14.483355,-48.298334 L 15.806684,-48.298334 L 15.806684,-48.024995 L 14.483355,-48.024995 L 14.483355,-47.248334 L 15.856684,-47.248334 L 15.856684,-46.974995 L 14.173351,-46.974995 L 14.173351,-49.404997 L 15.916688,-49.404997 L 15.916688,-49.131668 L 14.483355,-49.131668 L 14.483355,-48.298334" />
<path d="M 16.283355,-49.404997 L 17.22002,-49.404997 Q 17.680024,-49.404997 17.945025,-49.079997 Q 18.210026,-48.754997 18.210026,-48.18833 Q 18.210026,-47.621663 17.946691,-47.298329 Q 17.683355,-46.974995 17.22002,-46.974995 L 16.283355,-46.974995 L 16.283355,-49.404997 M 16.593359,-49.131668 L 16.593359,-47.248334 L 17.166688,-47.248334 C 17.645306,-47.246599 17.90141,-47.575616 17.900022,-48.191661 C 17.90141,-48.802917 17.645306,-49.133751 17.166688,-49.131668 L 16.593359,-49.131668" />
<path d="M 20.540028,-49.404997 Q 20.863357,-49.404997 21.060026,-49.214996 Q 21.256695,-49.024995 21.256695,-48.711659 Q 21.256695,-48.491661 21.151693,-48.35166 Q 21.046691,-48.211659 20.813357,-48.121663 C 21.0378,-48.017222 21.150022,-47.840554 21.150022,-47.591661 C 21.149154,-47.453366 21.109223,-47.32496 21.008355,-47.193327 C 20.910612,-47.064471 20.698875,-46.972738 20.430024,-46.974995 L 19.443359,-46.974995 L 19.443359,-49.404997 L 20.540028,-49.404997 M 20.353364,-47.248334 Q 20.840028,-47.248334 20.840028,-47.634999 Q 20.840028,-48.021663 20.353364,-48.021663 L 19.753364,-48.021663 L 19.753364,-47.248334 L 20.353364,-47.248334 M 20.510026,-49.131668 L 19.753364,-49.131668 L 19.753364,-48.294992 L 20.510026,-48.294992 Q 20.72003,-48.294992 20.833361,-48.409993 Q 20.946691,-48.524995 20.946691,-48.715001 Q 20.946691,-48.815001 20.913357,-48.901666 Q 20.880024,-48.98833 20.776693,-49.059999 Q 20.673362,-49.131668 20.510026,-49.131668" />
<path d="M 22.603364,-48.451666 L 23.516699,-46.974995 L 23.146691,-46.974995 L 22.453364,-48.158328 L 21.740028,-46.974995 L 21.356695,-46.974995 L 22.293359,-48.451666 L 22.293359,-49.404997 L 22.603364,-49.404997 L 22.603364,-48.451666" />
<path d="M 24.180035,-49.058328 L 23.833366,-49.058328 L 23.833366,-49.404997 L 24.180035,-49.404997 L 24.180035,-49.058328" />
<path d="M 24.180035,-47.658328 L 23.833366,-47.658328 L 23.833366,-48.004997 L 24.180035,-48.004997 L 24.180035,-47.658328" />
<path d="M 2.0,-53.744995 L 2.0,-57.389998 L 2.375,-57.389998 L 2.375,-57.055005 Q 2.670003,-57.505005 3.204997,-57.505005 Q 3.725,-57.505005 4.035002,-57.1125 Q 4.345003,-56.719995 4.345003,-56.069995 Q 4.345003,-55.435002 4.045003,-55.064998 Q 3.745003,-54.694995 3.225,-54.694995 Q 2.7,-54.694995 2.415007,-55.124992 L 2.415007,-53.744995 L 2.0,-53.744995 M 3.145003,-55.085002 Q 3.495003,-55.085002 3.702498,-55.364998 Q 3.909994,-55.644995 3.909994,-56.114998 Q 3.909994,-56.560002 3.697494,-56.8375 Q 3.484994,-57.114998 3.145003,-57.114998 Q 2.815007,-57.114998 2.615007,-56.8375 Q 2.415007,-56.560002 2.415007,-56.1 Q 2.415007,-55.639998 2.615007,-55.3625 Q 2.815007,-55.085002 3.145003,-55.085002" />
<path d="M 6.854997,-57.389998 L 6.854997,-54.769995 L 6.440007,-54.769995 L 6.440007,-56.255005 Q 6.440007,-56.655005 6.245003,-56.897502 Q 6.05,-57.139998 5.725,-57.139998 Q 5.475,-57.139998 5.330005,-57.0 Q 5.18501,-56.860002 5.18501,-56.624992 L 5.18501,-54.769995 L 4.770003,-54.769995 L 4.770003,-56.789998 Q 4.770003,-57.114998 4.997502,-57.310002 Q 5.225,-57.505005 5.604997,-57.505005 Q 5.895003,-57.505005 6.095003,-57.395003 Q 6.295003,-57.285002 6.479997,-57.024992 L 6.479997,-57.389998 L 6.854997,-57.389998" />
<path d="M 8.904981,-53.744995 L 8.484977,-53.744995 L 8.484977,-57.389998 L 8.904981,-57.389998 L 8.904981,-53.744995" />
<path d="M 11.699984,-53.744995 L 11.284977,-53.744995 L 11.284977,-55.099992 Q 11.019987,-54.694995 10.479981,-54.694995 Q 9.969987,-54.694995 9.662484,-55.072493 Q 9.354981,-55.449992 9.354981,-56.074992 Q 9.354981,-56.739998 9.659977,-57.122502 Q 9.964974,-57.505005 10.494987,-57.505005 Q 10.764974,-57.505005 10.962476,-57.397502 Q 11.159977,-57.289998 11.329981,-57.044995 L 11.329981,-57.389998 L 11.699984,-57.389998 L 11.699984,-53.744995 M 10.549984,-55.085002 Q 10.884977,-55.085002 11.084977,-55.3625 Q 11.284977,-55.639998 11.284977,-56.110002 Q 11.284977,-56.564998 11.084977,-56.839998 Q 10.884977,-57.114998 10.554981,-57.114998 Q 10.209977,-57.114998 9.999976,-56.8375 Q 9.789974,-56.560002 9.789974,-56.099992 Q 9.789974,-55.644995 9.999976,-55.364998 Q 10.209977,-55.085002 10.549984,-55.085002" />
<path d="M 13.294987,-54.864998 L 13.294987,-57.389998 L 13.734977,-57.389998 L 13.734977,-53.844995 L 13.444987,-53.844995 Q 13.329981,-54.255005 13.174984,-54.370003 Q 13.019987,-54.485002 12.509977,-54.549992 L 12.509977,-54.864998 L 13.294987,-54.864998" />
<path d="M 15.029981,-55.074992 L 15.469987,-55.074992 Q 15.519987,-54.230005 16.184977,-54.230005 Q 16.484977,-54.230005 16.684977,-54.420003 Q 16.884977,-54.610002 16.884977,-54.894995 Q 16.884977,-55.319995 16.404981,-55.594995 L 15.944987,-55.855005 Q 15.389974,-56.169995 15.184977,-56.4875 Q 14.979981,-56.805005 14.949984,-57.389998 L 17.309977,-57.389998 L 17.309977,-56.955005 L 15.444987,-56.955005 Q 15.479981,-56.730005 15.619979,-56.564998 Q 15.759977,-56.399992 16.084977,-56.224992 L 16.584977,-55.955005 Q 17.334977,-55.544995 17.334977,-54.885002 Q 17.334977,-54.424992 17.019979,-54.134993 Q 16.704981,-53.844995 16.199984,-53.844995 Q 15.064974,-53.844995 15.029981,-55.074992" />
<path d="M 18.234977,-54.989998 Q 18.244971,-54.644995 18.387476,-54.4375 Q 18.529981,-54.230005 18.909977,-54.230005 Q 19.199984,-54.230005 19.367481,-54.392505 Q 19.534977,-54.555005 19.534977,-54.835002 Q 19.534977,-55.160002 19.337476,-55.270003 Q 19.139974,-55.380005 18.664974,-55.389998 L 18.664974,-55.764998 L 18.719971,-55.764998 L 18.904981,-55.760002 Q 19.639974,-55.760002 19.639974,-56.405005 Q 19.639974,-56.739998 19.444979,-56.927498 Q 19.249984,-57.114998 18.904981,-57.114998 Q 18.544971,-57.114998 18.364974,-56.929997 Q 18.184977,-56.744995 18.159977,-56.360002 L 17.719971,-56.360002 Q 17.799984,-57.505005 18.889974,-57.505005 Q 19.434977,-57.505005 19.762476,-57.205005 Q 20.089974,-56.905005 20.089974,-56.399992 Q 20.089974,-56.060002 19.949976,-55.8625 Q 19.809977,-55.664998 19.489974,-55.555005 Q 19.984977,-55.360002 19.984977,-54.819995 Q 19.984977,-54.364998 19.697477,-54.104997 Q 19.409977,-53.844995 18.904981,-53.844995 Q 17.819971,-53.844995 17.794971,-54.989998 L 18.234977,-54.989998" />
<path d="M 22.834977,-53.744995 L 22.419971,-53.744995 L 22.419971,-55.099992 Q 22.154981,-54.694995 21.614974,-54.694995 Q 21.104981,-54.694995 20.797477,-55.072493 Q 20.489974,-55.449992 20.489974,-56.074992 Q 20.489974,-56.739998 20.794971,-57.122502 Q 21.099968,-57.505005 21.629981,-57.505005 Q 21.899968,-57.505005 22.097469,-57.397502 Q 22.294971,-57.289998 22.464974,-57.044995 L 22.464974,-57.389998 L 22.834977,-57.389998 L 22.834977,-53.744995 M 21.684977,-55.085002 Q 22.019971,-55.085002 22.219971,-55.3625 Q 22.419971,-55.639998 22.419971,-56.110002 Q 22.419971,-56.564998 22.219971,-56.839998 Q 22.019971,-57.114998 21.689974,-57.114998 Q 21.344971,-57.114998 21.134969,-56.8375 Q 20.924968,-56.560002 20.924968,-56.099992 Q 20.924968,-55.644995 21.134969,-55.364998 Q 21.344971,-55.085002 21.684977,-55.085002" />
<path d="M 7.86499,-54.769995 L 7.449984,-54.769995 L 7.449984,-57.389998 L 7.86499,-57.389998 L 7.86499,-54.769995" />
<path d="M 7.86499,-53.744995 L 7.444987,-53.744995 L 7.444987,-54.269995 L 7.86499,-54.269995 L 7.86499,-53.744995" />
<path d="M 2.0,-64.256668 L 2.936665,-64.256668 Q 3.396669,-64.256668 3.66167,-63.931668 Q 3.926671,-63.606668 3.926671,-63.040001 Q 3.926671,-62.473334 3.663336,-62.15 Q 3.4,-61.826666 2.936665,-61.826666 L 2.0,-61.826666 L 2.0,-64.256668 M 2.310004,-63.983339 L 2.310004,-62.100005 L 2.883333,-62.100005 C 3.361951,-62.09827 3.618055,-62.427287 3.616667,-63.043332 C 3.618055,-63.654588 3.361951,-63.985422 2.883333,-63.983339 L 2.310004,-63.983339" />
<path d="M 5.566667,-63.526666 L 5.816667,-64.256668 L 6.163336,-64.256668 L 5.310004,-61.826666 L 4.910004,-61.826666 L 4.043338,-64.256668 L 4.37334,-64.256668 L 4.630002,-63.526666 L 5.566667,-63.526666 M 5.480002,-63.266672 L 4.706673,-63.266672 L 5.106673,-62.159999 L 5.480002,-63.266672" />
<path d="M 7.356673,-62.100005 L 8.153331,-62.100005 L 8.153331,-61.826666 L 6.246669,-61.826666 L 6.246669,-62.100005 L 7.046669,-62.100005 L 7.046669,-64.256668 L 7.356673,-64.256668 L 7.356673,-62.100005" />
<path d="M 8.769998,-63.150005 L 10.093327,-63.150005 L 10.093327,-62.876666 L 8.769998,-62.876666 L 8.769998,-62.100005 L 10.143327,-62.100005 L 10.143327,-61.826666 L 8.459994,-61.826666 L 8.459994,-64.256668 L 10.203331,-64.256668 L 10.203331,-63.983339 L 8.769998,-63.983339 L 8.769998,-63.150005" />
<path d="M 10.916667,-62.509999 L 10.569998,-62.509999 L 10.569998,-62.856668 L 10.916667,-62.856668 L 10.916667,-62.509999" />
<path d="M 10.916667,-63.909999 L 10.569998,-63.909999 L 10.569998,-64.256668 L 10.916667,-64.256668 L 10.916667,-63.909999" />
<path d="M 2.079997,-69.858325 L 2.520003,-69.858325 Q 2.570003,-69.013338 3.234994,-69.013338 Q 3.534994,-69.013338 3.734994,-69.203337 Q 3.934994,-69.393335 3.934994,-69.678328 Q 3.934994,-70.103328 3.454997,-70.378328 L 2.995003,-70.638338 Q 2.43999,-70.953328 2.234994,-71.270833 Q 2.029997,-71.588338 2.0,-72.173332 L 4.359994,-72.173332 L 4.359994,-71.738338 L 2.495003,-71.738338 Q 2.529997,-71.513338 2.669995,-71.348332 Q 2.809994,-71.183325 3.134994,-71.008325 L 3.634994,-70.738338 Q 4.384994,-70.328328 4.384994,-69.668335 Q 4.384994,-69.208325 4.069995,-68.918327 Q 3.754997,-68.628328 3.25,-68.628328 Q 2.11499,-68.628328 2.079997,-69.858325" />
<path d="M 4.825,-70.458325 Q 4.825,-71.353328 5.119995,-71.820833 Q 5.41499,-72.288338 5.984994,-72.288338 Q 6.55,-72.288338 6.847494,-71.825838 Q 7.144987,-71.363338 7.144987,-70.488338 Q 7.144987,-68.628328 5.984994,-68.628328 Q 5.819987,-68.628328 5.672494,-68.668327 Q 5.525,-68.708325 5.364998,-68.828328 Q 5.204997,-68.948332 5.089998,-69.140828 Q 4.975,-69.333325 4.9,-69.673332 Q 4.825,-70.013338 4.825,-70.458325 M 6.694987,-70.448332 Q 6.694987,-71.203328 6.519987,-71.56333 Q 6.344987,-71.923332 5.975,-71.923332 Q 5.275,-71.923332 5.275,-70.463338 Q 5.275,-69.018335 5.984994,-69.018335 Q 6.694987,-69.018335 6.694987,-70.448332" />
<path d="M 7.63999,-69.858325 L 8.079997,-69.858325 Q 8.129997,-69.013338 8.794987,-69.013338 Q 9.094987,-69.013338 9.294987,-69.203337 Q 9.494987,-69.393335 9.494987,-69.678328 Q 9.494987,-70.103328 9.01499,-70.378328 L 8.554997,-70.638338 Q 7.999984,-70.953328 7.794987,-71.270833 Q 7.58999,-71.588338 7.559994,-72.173332 L 9.919987,-72.173332 L 9.919987,-71.738338 L 8.054997,-71.738338 Q 8.08999,-71.513338 8.229989,-71.348332 Q 8.369987,-71.183325 8.694987,-71.008325 L 9.194987,-70.738338 Q 9.944987,-70.328328 9.944987,-69.668335 Q 9.944987,-69.208325 9.629989,-68.918327 Q 9.31499,-68.628328 8.809994,-68.628328 Q 7.674984,-68.628328 7.63999,-69.858325" />
<path d="M 12.549984,-68.628328 L 10.719987,-68.628328 L 10.454981,-70.558325 L 10.859994,-70.558325 Q 11.01499,-70.373332 11.159985,-70.30083 Q 11.304981,-70.228328 11.509994,-70.228328 Q 11.86499,-70.228328 12.074992,-70.455827 Q 12.284994,-70.683325 12.284994,-71.078328 Q 12.284994,-71.458325 12.07749,-71.678328 Q 11.869987,-71.898332 11.509994,-71.898332 Q 10.93999,-71.898332 10.784994,-71.303328 L 10.344987,-71.303328 Q 10.359994,-71.388338 10.369987,-71.433333 Q 10.379981,-71.478328 10.422485,-71.595833 Q 10.46499,-71.713338 10.512492,-71.790837 Q 10.559994,-71.868335 10.65249,-71.970833 Q 10.744987,-72.073332 10.857487,-72.135832 Q 10.969987,-72.198332 11.142489,-72.243335 Q 11.31499,-72.288338 11.519987,-72.288338 Q 12.054981,-72.288338 12.394987,-71.933333 Q 12.734994,-71.578328 12.734994,-71.018335 Q 12.734994,-70.493335 12.417489,-70.165837 Q 12.099984,-69.838338 11.58999,-69.838338 Q 11.229981,-69.838338 10.934994,-70.053328 L 11.074984,-69.063338 L 12.549984,-69.063338 L 12.549984,-68.628328" />
<path d="M 14.384977,-70.613338 L 13.179981,-70.613338 L 13.179981,-70.973332 L 14.384977,-70.973332 L 14.384977,-70.613338" />
<path d="M 14.829997,-70.458325 Q 14.829997,-71.353328 15.124992,-71.820833 Q 15.419987,-72.288338 15.98999,-72.288338 Q 16.554997,-72.288338 16.85249,-71.825838 Q 17.149984,-71.363338 17.149984,-70.488338 Q 17.149984,-68.628328 15.98999,-68.628328 Q 15.824984,-68.628328 15.67749,-68.668327 Q 15.529997,-68.708325 15.369995,-68.828328 Q 15.209994,-68.948332 15.094995,-69.140828 Q 14.979997,-69.333325 14.904997,-69.673332 Q 14.829997,-70.013338 14.829997,-70.458325 M 16.699984,-70.448332 Q 16.699984,-71.203328 16.524984,-71.56333 Q 16.349984,-71.923332 15.979997,-71.923332 Q 15.279997,-71.923332 15.279997,-70.463338 Q 15.279997,-69.018335 15.98999,-69.018335 Q 16.699984,-69.018335 16.699984,-70.448332" />
<path d="M 19.774984,-68.628328 L 17.944987,-68.628328 L 17.679981,-70.558325 L 18.084994,-70.558325 Q 18.23999,-70.373332 18.384985,-70.30083 Q 18.529981,-70.228328 18.734994,-70.228328 Q 19.08999,-70.228328 19.299992,-70.455827 Q 19.509994,-70.683325 19.509994,-71.078328 Q 19.509994,-71.458325 19.30249,-71.678328 Q 19.094987,-71.898332 18.734994,-71.898332 Q 18.16499,-71.898332 18.009994,-71.303328 L 17.569987,-71.303328 Q 17.584994,-71.388338 17.594987,-71.433333 Q 17.604981,-71.478328 17.647485,-71.595833 Q 17.68999,-71.713338 17.737492,-71.790837 Q 17.784994,-71.868335 17.87749,-71.970833 Q 17.969987,-72.073332 18.082487,-72.135832 Q 18.194987,-72.198332 18.367489,-72.243335 Q 18.53999,-72.288338 18.744987,-72.288338 Q 19.279981,-72.288338 19.619987,-71.933333 Q 19.959994,-71.578328 19.959994,-71.018335 Q 19.959994,-70.493335 19.642489,-70.165837 Q 19.324984,-69.838338 18.81499,-69.838338 Q 18.454981,-69.838338 18.159994,-70.053328 L 18.299984,-69.063338 L 19.774984,-69.063338 L 19.774984,-68.628328" />
<path d="M 21.609977,-70.613338 L 20.404981,-70.613338 L 20.404981,-70.973332 L 21.609977,-70.973332 L 21.609977,-70.613338" />
<path d="M 22.08999,-69.858325 L 22.529997,-69.858325 Q 22.579997,-69.013338 23.244987,-69.013338 Q 23.544987,-69.013338 23.744987,-69.203337 Q 23.944987,-69.393335 23.944987,-69.678328 Q 23.944987,-70.103328 23.46499,-70.378328 L 23.004997,-70.638338 Q 22.449984,-70.953328 22.244987,-71.270833 Q 22.03999,-71.588338 22.009994,-72.173332 L 24.369987,-72.173332 L 24.369987,-71.738338 L 22.504997,-71.738338 Q 22.53999,-71.513338 22.679989,-71.348332 Q 22.819987,-71.183325 23.144987,-71.008325 L 23.644987,-70.738338 Q 24.394987,-70.328328 24.394987,-69.668335 Q 24.394987,-69.208325 24.079989,-68.918327 Q 23.76499,-68.628328 23.259994,-68.628328 Q 22.124984,-68.628328 22.08999,-69.858325" />
<path d="M 25.294987,-69.773332 Q 25.304981,-69.428328 25.447485,-69.220833 Q 25.58999,-69.013338 25.969987,-69.013338 Q 26.259994,-69.013338 26.42749,-69.175838 Q 26.594987,-69.338338 26.594987,-69.618335 Q 26.594987,-69.943335 26.397485,-70.053337 Q 26.199984,-70.163338 25.724984,-70.173332 L 25.724984,-70.548332 L 25.779981,-70.548332 L 25.96499,-70.543335 Q 26.699984,-70.543335 26.699984,-71.188338 Q 26.699984,-71.523332 26.504989,-71.710832 Q 26.309994,-71.898332 25.96499,-71.898332 Q 25.604981,-71.898332 25.424984,-71.71333 Q 25.244987,-71.528328 25.219987,-71.143335 L 24.779981,-71.143335 Q 24.859994,-72.288338 25.949984,-72.288338 Q 26.494987,-72.288338 26.822485,-71.988338 Q 27.149984,-71.688338 27.149984,-71.183325 Q 27.149984,-70.843335 27.009985,-70.645833 Q 26.869987,-70.448332 26.549984,-70.338338 Q 27.044987,-70.143335 27.044987,-69.603328 Q 27.044987,-69.148332 26.757487,-68.88833 Q 26.469987,-68.628328 25.96499,-68.628328 Q 24.879981,-68.628328 24.854981,-69.773332 L 25.294987,-69.773332" />
<path d="M 3.153331,-78.041661 C 3.45111,-78.119439 3.6,-78.250552 3.6,-78.434999 C 3.598784,-78.538252 3.571772,-78.623962 3.48833,-78.718332 C 3.407317,-78.809926 3.227678,-78.876731 2.979991,-78.874995 C 2.848886,-78.874995 2.736111,-78.857773 2.641667,-78.823329 C 2.547222,-78.788885 2.47611,-78.742775 2.428331,-78.684999 C 2.331905,-78.568405 2.294195,-78.453817 2.293327,-78.315001 L 2.293327,-78.298334 L 2.0,-78.298334 C 2.002221,-78.431668 2.023886,-78.549445 2.064996,-78.651666 C 2.147736,-78.859059 2.26947,-78.965934 2.443327,-79.046663 C 2.618399,-79.126003 2.776038,-79.146772 2.959994,-79.148334 C 3.246451,-79.150765 3.487986,-79.071461 3.623329,-78.971663 C 3.692217,-78.920549 3.748882,-78.861104 3.793327,-78.793327 C 3.883952,-78.656904 3.908257,-78.534757 3.909994,-78.404997 C 3.909994,-78.256111 3.864994,-78.128333 3.774995,-78.021663 C 3.684995,-77.914994 3.557773,-77.839437 3.393327,-77.794992 L 2.783333,-77.631668 Q 2.563325,-77.571663 2.473329,-77.491661 Q 2.383333,-77.411659 2.383333,-77.271663 Q 2.383333,-77.08833 2.531662,-76.974995 Q 2.679991,-76.861659 2.92666,-76.861659 Q 3.216667,-76.861659 3.373329,-76.989996 Q 3.529991,-77.118332 3.533333,-77.354997 L 3.82666,-77.354997 Q 3.823329,-76.998334 3.589996,-76.8 Q 3.356662,-76.601666 2.936665,-76.601666 Q 2.536665,-76.601666 2.304997,-76.793332 Q 2.073329,-76.984999 2.073329,-77.315001 Q 2.073329,-77.758328 2.55,-77.881668 L 3.153331,-78.041661" />
<path d="M 6.209994,-77.394992 C 6.114435,-76.866108 5.802214,-76.601666 5.273329,-76.601666 C 5.104434,-76.601666 4.952765,-76.630554 4.818321,-76.68833 C 4.683878,-76.746106 4.577212,-76.818884 4.498324,-76.906662 C 4.419436,-76.994441 4.353324,-77.097219 4.299989,-77.214996 C 4.190888,-77.452285 4.165756,-77.661041 4.163325,-77.884999 C 4.165583,-78.10469 4.191617,-78.308089 4.298324,-78.543332 C 4.350548,-78.659999 4.416104,-78.761664 4.494993,-78.848329 C 4.573881,-78.934993 4.679436,-79.006661 4.811659,-79.06333 C 4.943882,-79.12 5.093327,-79.148334 5.259994,-79.148334 C 5.853324,-79.148334 6.186657,-78.827223 6.259994,-78.184999 L 5.939996,-78.184999 C 5.902214,-78.420553 5.82999,-78.594441 5.723324,-78.706662 C 5.616658,-78.818884 5.463325,-78.874995 5.263325,-78.874995 C 5.021101,-78.874995 4.828879,-78.78555 4.686659,-78.606662 C 4.544439,-78.427774 4.473329,-78.186109 4.473329,-77.881668 C 4.473329,-77.570557 4.541661,-77.325 4.678326,-77.144998 C 4.81499,-76.964996 5.0011,-76.874995 5.236654,-76.874995 C 5.626455,-76.874993 5.819093,-77.048336 5.893327,-77.394992 L 6.209994,-77.394992" />
<path d="M 7.899989,-78.341661 L 8.149989,-79.071663 L 8.496658,-79.071663 L 7.643327,-76.641661 L 7.243327,-76.641661 L 6.37666,-79.071663 L 6.706662,-79.071663 L 6.963325,-78.341661 L 7.899989,-78.341661 M 7.813325,-78.081668 L 7.039996,-78.081668 L 7.439996,-76.974995 L 7.813325,-78.081668" />
<path d="M 9.039996,-76.641661 L 8.729991,-76.641661 L 8.729991,-79.071663 L 10.239996,-79.071663 L 10.239996,-78.798334 L 9.039996,-78.798334 L 9.039996,-76.641661" />
<path d="M 10.833333,-77.965001 L 12.156662,-77.965001 L 12.156662,-77.691661 L 10.833333,-77.691661 L 10.833333,-76.915001 L 12.206662,-76.915001 L 12.206662,-76.641661 L 10.523329,-76.641661 L 10.523329,-79.071663 L 12.266667,-79.071663 L 12.266667,-78.798334 L 10.833333,-78.798334 L 10.833333,-77.965001" />
<path d="M 12.980002,-77.324995 L 12.633333,-77.324995 L 12.633333,-77.671663 L 12.980002,-77.671663 L 12.980002,-77.324995" />
<path d="M 12.980002,-78.724995 L 12.633333,-78.724995 L 12.633333,-79.071663 L 12.980002,-79.071663 L 12.980002,-78.724995" />
<path d="M 2.78501,-84.539168 L 2.78501,-87.064168 L 3.225,-87.064168 L 3.225,-83.519165 L 2.93501,-83.519165 Q 2.820003,-83.929175 2.665007,-84.044173 Q 2.51001,-84.159172 2.0,-84.224162 L 2.0,-84.539168 L 2.78501,-84.539168" />
<path d="M 6.815007,-84.539168 L 6.815007,-87.064168 L 7.254997,-87.064168 L 7.254997,-83.519165 L 6.965007,-83.519165 Q 6.85,-83.929175 6.695003,-84.044173 Q 6.540007,-84.159172 6.029997,-84.224162 L 6.029997,-84.539168 L 6.815007,-84.539168" />
<path d="M 5.190007,-86.544165 L 4.670003,-86.544165 L 4.670003,-87.064168 L 5.190007,-87.064168 L 5.190007,-86.544165" />
<path d="M 5.190007,-84.444165 L 4.670003,-84.444165 L 4.670003,-84.964168 L 5.190007,-84.964168 L 5.190007,-84.444165" />
<path d="M 52.2,-47.24165 L 51.319987,-47.24165 L 51.319987,-53.201644 L 47.509993,-47.24165 L 46.5,-47.24165 L 46.5,-54.531657 L 47.380013,-54.531657 L 47.380013,-48.621663 L 51.15,-54.531657 L 52.2,-54.531657 L 52.2,-47.24165" />
<path d="M 54.069987,-52.19165 L 57.930013,-52.19165 Q 57.930013,-49.14165 55.6,-49.14165 Q 54.509993,-49.14165 53.854997,-49.91665 Q 53.2,-50.69165 53.2,-51.981657 Q 53.2,-53.271663 53.840007,-54.016667 Q 54.480013,-54.76167 55.580013,-54.76167 Q 56.480013,-54.76167 57.070003,-54.281657 Q 57.659993,-53.801644 57.819987,-52.94165 L 56.980013,-52.94165 Q 56.630013,-53.99165 55.609993,-53.99165 Q 54.9,-53.99165 54.495003,-53.50166 Q 54.090007,-53.01167 54.069987,-52.19165 M 57.040007,-51.51167 L 54.090007,-51.51167 Q 54.140007,-50.781657 54.55,-50.346663 Q 54.959993,-49.91167 55.58999,-49.91167 Q 56.219987,-49.91167 56.629997,-50.371663 Q 57.040007,-50.831657 57.040007,-51.51167" />
<path d="M 58.75,-49.29165 L 58.75,-54.531657 L 59.590007,-54.531657 L 59.590007,-51.24165 Q 59.590007,-50.671663 59.95,-50.271663 Q 60.309993,-49.871663 60.819987,-49.871663 Q 61.280013,-49.871663 61.53501,-50.146663 Q 61.790007,-50.421663 61.790007,-50.921663 L 61.790007,-54.531657 L 62.630013,-54.531657 L 62.630013,-51.24165 Q 62.630013,-50.671663 62.990007,-50.271663 Q 63.35,-49.871663 63.859993,-49.871663 Q 64.319987,-49.871663 64.575,-50.146663 Q 64.830013,-50.421663 64.830013,-50.921663 L 64.830013,-54.531657 L 65.669987,-54.531657 L 65.669987,-50.601644 Q 65.669987,-49.89165 65.275,-49.51665 Q 64.880013,-49.14165 64.15,-49.14165 Q 63.630013,-49.14165 63.270003,-49.321647 Q 62.909993,-49.501644 62.540007,-49.94165 Q 62.1,-49.14165 61.130013,-49.14165 Q 60.609993,-49.14165 60.234993,-49.35166 Q 59.859993,-49.56167 59.519987,-50.031657 L 59.519987,-49.29165 L 58.75,-49.29165" />
<path d="M 66.75,-50.84165 L 67.589974,-50.84165 Q 67.639974,-50.36167 67.939974,-50.13667 Q 68.239974,-49.91167 68.819987,-49.91167 Q 69.37998,-49.91167 69.684977,-50.11167 Q 69.989974,-50.31167 69.989974,-50.69165 L 69.989974,-50.91167 Q 69.989974,-51.171663 69.799984,-51.30166 Q 69.609993,-51.431657 69.119987,-51.49165 Q 68.759993,-51.54165 68.619987,-51.561654 Q 68.47998,-51.581657 68.17998,-51.631657 Q 67.87998,-51.681657 67.759977,-51.71665 Q 67.639974,-51.751644 67.414974,-51.826644 Q 67.189974,-51.901644 67.094987,-51.981657 Q 67.0,-52.06167 66.859993,-52.18667 Q 66.719987,-52.31167 66.66499,-52.456657 Q 66.609993,-52.601644 66.56499,-52.79165 Q 66.519987,-52.981657 66.519987,-53.21167 Q 66.519987,-53.921663 66.984993,-54.341667 Q 67.45,-54.76167 68.239974,-54.76167 Q 69.17998,-54.76167 70.019987,-53.99165 Q 70.069987,-54.39165 70.274984,-54.57666 Q 70.47998,-54.76167 70.87998,-54.76167 Q 71.1,-54.76167 71.45,-54.671663 L 71.45,-54.04165 Q 71.359993,-54.06167 71.269987,-54.06167 Q 70.819987,-54.06167 70.819987,-53.651644 L 70.819987,-50.571663 Q 70.819987,-49.871663 70.319987,-49.506657 Q 69.819987,-49.14165 68.85,-49.14165 Q 66.809993,-49.14165 66.75,-50.84165 M 69.989974,-52.881657 Q 69.989974,-53.301644 69.549984,-53.66665 Q 69.109993,-54.031657 68.419987,-54.031657 Q 67.92998,-54.031657 67.659977,-53.811654 Q 67.389974,-53.59165 67.389974,-53.19165 Q 67.389974,-52.79165 67.689974,-52.56665 Q 67.989974,-52.34165 68.359977,-52.281657 Q 68.72998,-52.221663 69.234977,-52.141667 Q 69.739974,-52.06167 69.989974,-51.94165 L 69.989974,-52.881657" />
<path d="M 74.65,-49.901644 L 75.530013,-49.901644 Q 75.630013,-48.21167 76.959993,-48.21167 Q 77.559993,-48.21167 77.959993,-48.591667 Q 78.359993,-48.971663 78.359993,-49.54165 Q 78.359993,-50.39165 77.4,-50.94165 L 76.480013,-51.46167 Q 75.369987,-52.09165 74.959993,-52.72666 Q 74.55,-53.36167 74.490007,-54.531657 L 79.209993,-54.531657 L 79.209993,-53.66167 L 75.480013,-53.66167 Q 75.55,-53.21167 75.829997,-52.881657 Q 76.109993,-52.551644 76.759993,-52.201644 L 77.759993,-51.66167 Q 79.259993,-50.84165 79.259993,-49.521663 Q 79.259993,-48.601644 78.629997,-48.021647 Q 78.0,-47.44165 76.990007,-47.44165 Q 74.719987,-47.44165 74.65,-49.901644" />
<path d="M 81.059993,-49.731657 Q 81.07998,-49.04165 81.36499,-48.62666 Q 81.65,-48.21167 82.409993,-48.21167 Q 82.990007,-48.21167 83.325,-48.53667 Q 83.659993,-48.86167 83.659993,-49.421663 Q 83.659993,-50.071663 83.26499,-50.291667 Q 82.869987,-50.51167 81.919987,-50.531657 L 81.919987,-51.281657 L 82.02998,-51.281657 L 82.4,-51.271663 Q 83.869987,-51.271663 83.869987,-52.56167 Q 83.869987,-53.231657 83.479997,-53.606657 Q 83.090007,-53.981657 82.4,-53.981657 Q 81.67998,-53.981657 81.319987,-53.611654 Q 80.959993,-53.24165 80.909993,-52.471663 L 80.02998,-52.471663 Q 80.190007,-54.76167 82.369987,-54.76167 Q 83.459993,-54.76167 84.11499,-54.16167 Q 84.769987,-53.56167 84.769987,-52.551644 Q 84.769987,-51.871663 84.48999,-51.47666 Q 84.209993,-51.081657 83.569987,-50.86167 Q 84.559993,-50.471663 84.559993,-49.39165 Q 84.559993,-48.481657 83.984993,-47.961654 Q 83.409993,-47.44165 82.4,-47.44165 Q 80.22998,-47.44165 80.17998,-49.731657 L 81.059993,-49.731657" />
<path d="M 91.709993,-51.44165 Q 93.05,-51.79165 93.05,-52.621663 Q 93.05,-52.851644 92.975,-53.056657 Q 92.9,-53.26167 92.71499,-53.471663 Q 92.52998,-53.681657 92.13999,-53.811654 Q 91.75,-53.94165 91.189974,-53.94165 Q 90.6,-53.94165 90.175,-53.786654 Q 89.75,-53.631657 89.534993,-53.371663 Q 89.319987,-53.11167 89.224984,-52.841667 Q 89.12998,-52.571663 89.12998,-52.26167 L 89.12998,-52.21167 L 88.25,-52.21167 Q 88.259993,-52.81167 88.444987,-53.271663 Q 88.62998,-53.731657 88.909977,-54.006657 Q 89.189974,-54.281657 89.57998,-54.456657 Q 89.969987,-54.631657 90.344987,-54.696663 Q 90.719987,-54.76167 91.12998,-54.76167 Q 91.77998,-54.76167 92.294987,-54.61167 Q 92.809993,-54.46167 93.119987,-54.231657 Q 93.42998,-54.001644 93.62998,-53.696647 Q 93.82998,-53.39165 93.90498,-53.106657 Q 93.97998,-52.821663 93.97998,-52.531657 Q 93.97998,-51.86167 93.574984,-51.381657 Q 93.169987,-50.901644 92.42998,-50.701644 L 90.6,-50.21167 Q 89.939974,-50.031657 89.669987,-49.79165 Q 89.4,-49.551644 89.4,-49.131657 Q 89.4,-48.581657 89.844987,-48.24165 Q 90.289974,-47.901644 91.02998,-47.901644 Q 91.9,-47.901644 92.369987,-48.286654 Q 92.839974,-48.671663 92.85,-49.381657 L 93.72998,-49.381657 Q 93.719987,-48.31167 93.019987,-47.716667 Q 92.319987,-47.121663 91.059993,-47.121663 Q 89.859993,-47.121663 89.16499,-47.696663 Q 88.469987,-48.271663 88.469987,-49.26167 Q 88.469987,-50.59165 89.9,-50.96167 L 91.709993,-51.44165" />
<path d="M 96.989974,-49.29165 L 96.12998,-49.29165 L 96.12998,-47.851644 L 95.299967,-47.851644 L 95.299967,-49.29165 L 94.589974,-49.29165 L 94.589974,-49.971663 L 95.299967,-49.971663 L 95.299967,-53.931657 Q 95.299967,-54.331657 95.559977,-54.546663 Q 95.819987,-54.76167 96.309961,-54.76167 Q 96.599967,-54.76167 96.989974,-54.69165 L 96.989974,-53.99165 Q 96.839974,-54.031657 96.589974,-54.031657 Q 96.319987,-54.031657 96.224984,-53.936654 Q 96.12998,-53.84165 96.12998,-53.56167 L 96.12998,-49.971663 L 96.989974,-49.971663 L 96.989974,-49.29165" />
<path d="M 98.259961,-52.19165 L 102.119987,-52.19165 Q 102.119987,-49.14165 99.789974,-49.14165 Q 98.699967,-49.14165 98.044971,-49.91665 Q 97.389974,-50.69165 97.389974,-51.981657 Q 97.389974,-53.271663 98.02998,-54.016667 Q 98.669987,-54.76167 99.769987,-54.76167 Q 100.669987,-54.76167 101.259977,-54.281657 Q 101.849967,-53.801644 102.009961,-52.94165 L 101.169987,-52.94165 Q 100.819987,-53.99165 99.799967,-53.99165 Q 99.089974,-53.99165 98.684977,-53.50166 Q 98.27998,-53.01167 98.259961,-52.19165 M 101.22998,-51.51167 L 98.27998,-51.51167 Q 98.32998,-50.781657 98.739974,-50.346663 Q 99.149967,-49.91167 99.779964,-49.91167 Q 100.409961,-49.91167 100.819971,-50.371663 Q 101.22998,-50.831657 101.22998,-51.51167" />
<path d="M 103.019987,-56.71167 L 103.859993,-56.71167 L 103.859993,-53.981657 Q 104.5,-54.76167 105.469987,-54.76167 Q 106.47998,-54.76167 107.094987,-54.006657 Q 107.709993,-53.251644 107.709993,-52.001644 Q 107.709993,-50.681657 107.104997,-49.911654 Q 106.5,-49.14165 105.459993,-49.14165 Q 104.37998,-49.14165 103.789974,-50.081657 L 103.789974,-49.29165 L 103.019987,-49.29165 L 103.019987,-56.71167 M 105.319987,-49.921663 Q 106.009993,-49.921663 106.424984,-50.481657 Q 106.839974,-51.04165 106.839974,-51.981657 Q 106.839974,-52.871663 106.419987,-53.42666 Q 106.0,-53.981657 105.319987,-53.981657 Q 104.659993,-53.981657 104.259993,-53.42666 Q 103.859993,-52.871663 103.859993,-51.95166 Q 103.859993,-51.031657 104.259993,-50.47666 Q 104.659993,-49.921663 105.319987,-49.921663" />
<path d="M 108.590007,-56.71167 L 109.430013,-56.71167 L 109.430013,-53.981657 Q 110.07002,-54.76167 111.040007,-54.76167 Q 112.05,-54.76167 112.665007,-54.006657 Q 113.280013,-53.251644 113.280013,-52.001644 Q 113.280013,-50.681657 112.675016,-49.911654 Q 112.07002,-49.14165 111.030013,-49.14165 Q 109.95,-49.14165 109.359993,-50.081657 L 109.359993,-49.29165 L 108.590007,-49.29165 L 108.590007,-56.71167 M 110.890007,-49.921663 Q 111.580013,-49.921663 111.995003,-50.481657 Q 112.409993,-51.04165 112.409993,-51.981657 Q 112.409993,-52.871663 111.990007,-53.42666 Q 111.57002,-53.981657 110.890007,-53.981657 Q 110.230013,-53.981657 109.830013,-53.42666 Q 109.430013,-52.871663 109.430013,-51.95166 Q 109.430013,-51.031657 109.830013,-50.47666 Q 110.230013,-49.921663 110.890007,-49.921663" />
<path d="M 114.75,-52.19165 L 118.610026,-52.19165 Q 118.610026,-49.14165 116.280013,-49.14165 Q 115.190007,-49.14165 114.53501,-49.91665 Q 113.880013,-50.69165 113.880013,-51.981657 Q 113.880013,-53.271663 114.52002,-54.016667 Q 115.160026,-54.76167 116.260026,-54.76167 Q 117.160026,-54.76167 117.750016,-54.281657 Q 118.340007,-53.801644 118.5,-52.94165 L 117.660026,-52.94165 Q 117.310026,-53.99165 116.290007,-53.99165 Q 115.580013,-53.99165 115.175016,-53.50166 Q 114.77002,-53.01167 114.75,-52.19165 M 117.72002,-51.51167 L 114.77002,-51.51167 Q 114.82002,-50.781657 115.230013,-50.346663 Q 115.640007,-49.91167 116.270003,-49.91167 Q 116.9,-49.91167 117.31001,-50.371663 Q 117.72002,-50.831657 117.72002,-51.51167" />
<path d="M 122.010026,-50.021663 L 122.010026,-49.171663 Q 121.800033,-49.14165 121.690039,-49.14165 Q 121.290039,-49.14165 120.955046,-49.396647 Q 120.620052,-49.651644 120.260026,-50.24165 L 120.260026,-49.29165 L 119.490039,-49.29165 L 119.490039,-54.531657 L 120.330046,-54.531657 L 120.330046,-51.81167 Q 120.330046,-50.781657 120.750049,-50.411654 Q 121.170052,-50.04165 122.010026,-50.021663" />
<path d="M 51.169987,-62.98999 L 51.169987,-68.11001 Q 51.169987,-68.849984 50.669987,-69.269987 Q 50.169987,-69.68999 49.290007,-69.68999 Q 48.440007,-69.68999 47.93501,-69.294987 Q 47.430013,-68.899984 47.430013,-68.11001 L 47.430013,-62.98999 L 46.5,-62.98999 L 46.5,-68.11001 Q 46.5,-69.220003 47.245003,-69.865007 Q 47.990007,-70.51001 49.290007,-70.51001 Q 50.569987,-70.51001 51.334994,-69.86001 Q 52.1,-69.21001 52.1,-68.11001 L 52.1,-62.98999 L 51.169987,-62.98999" />
<path d="M 53.5,-65.03999 L 53.5,-70.279997 L 54.340007,-70.279997 L 54.340007,-67.38999 Q 54.340007,-66.58999 54.729997,-66.104997 Q 55.119987,-65.620003 55.759994,-65.620003 Q 56.259994,-65.620003 56.55,-65.9 Q 56.840007,-66.179997 56.840007,-66.649984 L 56.840007,-70.279997 L 57.669987,-70.279997 L 57.669987,-66.320003 Q 57.669987,-65.670003 57.219987,-65.279997 Q 56.769987,-64.88999 56.009994,-64.88999 Q 55.430013,-64.88999 55.025,-65.129997 Q 54.619987,-65.370003 54.269987,-65.920003 L 54.269987,-65.03999 L 53.5,-65.03999" />
<path d="M 62.889974,-65.03999 L 62.029981,-65.03999 L 62.029981,-63.599984 L 61.199968,-63.599984 L 61.199968,-65.03999 L 60.489974,-65.03999 L 60.489974,-65.720003 L 61.199968,-65.720003 L 61.199968,-69.679997 Q 61.199968,-70.079997 61.459977,-70.295003 Q 61.719987,-70.51001 62.209961,-70.51001 Q 62.499968,-70.51001 62.889974,-70.43999 L 62.889974,-69.73999 Q 62.739974,-69.779997 62.489974,-69.779997 Q 62.219987,-69.779997 62.124984,-69.684993 Q 62.029981,-69.58999 62.029981,-69.31001 L 62.029981,-65.720003 L 62.889974,-65.720003 L 62.889974,-65.03999" />
<path d="M 64.259961,-68.720003 L 63.379981,-68.720003 Q 63.439974,-70.51001 65.469987,-70.51001 Q 66.459961,-70.51001 67.044971,-70.06001 Q 67.629981,-69.61001 67.629981,-68.849984 Q 67.629981,-68.270003 67.279981,-67.925 Q 66.929981,-67.579997 66.149968,-67.38999 L 65.349968,-67.199984 Q 64.839974,-67.079997 64.609977,-66.904997 Q 64.379981,-66.729997 64.379981,-66.449984 Q 64.379981,-66.08999 64.679981,-65.875 Q 64.979981,-65.66001 65.489974,-65.66001 Q 66.509961,-65.66001 66.539974,-66.499984 L 67.419987,-66.499984 Q 67.409961,-65.729997 66.919971,-65.309993 Q 66.429981,-64.88999 65.519971,-64.88999 Q 64.609961,-64.88999 64.059961,-65.325 Q 63.509961,-65.76001 63.509961,-66.48999 Q 63.509961,-67.11001 63.874968,-67.445003 Q 64.239974,-67.779997 65.169987,-67.999984 L 65.949968,-68.18999 Q 66.379981,-68.28999 66.569971,-68.459993 Q 66.759961,-68.629997 66.759961,-68.920003 Q 66.759961,-69.28999 66.424968,-69.51499 Q 66.089974,-69.73999 65.539974,-69.73999 Q 64.849968,-69.73999 64.584977,-69.46499 Q 64.319987,-69.18999 64.259961,-68.720003" />
<path d="M 73.569987,-65.03999 L 73.569987,-70.279997 L 74.409994,-70.279997 L 74.409994,-66.98999 Q 74.409994,-66.420003 74.769987,-66.020003 Q 75.129981,-65.620003 75.639974,-65.620003 Q 76.1,-65.620003 76.354997,-65.895003 Q 76.609994,-66.170003 76.609994,-66.670003 L 76.609994,-70.279997 L 77.45,-70.279997 L 77.45,-66.98999 Q 77.45,-66.420003 77.809994,-66.020003 Q 78.169987,-65.620003 78.679981,-65.620003 Q 79.139974,-65.620003 79.394987,-65.895003 Q 79.65,-66.170003 79.65,-66.670003 L 79.65,-70.279997 L 80.489974,-70.279997 L 80.489974,-66.349984 Q 80.489974,-65.63999 80.094987,-65.26499 Q 79.7,-64.88999 78.969987,-64.88999 Q 78.45,-64.88999 78.08999,-65.069987 Q 77.729981,-65.249984 77.359994,-65.68999 Q 76.919987,-64.88999 75.95,-64.88999 Q 75.429981,-64.88999 75.054981,-65.1 Q 74.679981,-65.31001 74.339974,-65.779997 L 74.339974,-65.03999 L 73.569987,-65.03999" />
<path d="M 81.589974,-65.03999 L 81.589974,-70.279997 L 82.429981,-70.279997 L 82.429981,-66.98999 Q 82.429981,-66.420003 82.789974,-66.020003 Q 83.149968,-65.620003 83.659961,-65.620003 Q 84.119987,-65.620003 84.374984,-65.895003 Q 84.629981,-66.170003 84.629981,-66.670003 L 84.629981,-70.279997 L 85.469987,-70.279997 L 85.469987,-66.98999 Q 85.469987,-66.420003 85.829981,-66.020003 Q 86.189974,-65.620003 86.699968,-65.620003 Q 87.159961,-65.620003 87.414974,-65.895003 Q 87.669987,-66.170003 87.669987,-66.670003 L 87.669987,-70.279997 L 88.509961,-70.279997 L 88.509961,-66.349984 Q 88.509961,-65.63999 88.114974,-65.26499 Q 87.719987,-64.88999 86.989974,-64.88999 Q 86.469987,-64.88999 86.109977,-65.069987 Q 85.749968,-65.249984 85.379981,-65.68999 Q 84.939974,-64.88999 83.969987,-64.88999 Q 83.449968,-64.88999 83.074968,-65.1 Q 82.699968,-65.31001 82.359961,-65.779997 L 82.359961,-65.03999 L 81.589974,-65.03999" />
<path d="M 69.809994,-65.03999 L 68.769987,-65.03999 L 68.769987,-66.079997 L 69.809994,-66.079997 L 69.809994,-65.03999" />
<path d="M 69.809994,-69.23999 L 68.769987,-69.23999 L 68.769987,-70.279997 L 69.809994,-70.279997 L 69.809994,-69.23999" />
<path d="M 59.689974,-62.98999 L 58.849968,-62.98999 L 58.849968,-64.03999 L 59.689974,-64.03999 L 59.689974,-62.98999" />
<path d="M 59.689974,-65.03999 L 58.859961,-65.03999 L 58.859961,-70.279997 L 59.689974,-70.279997 L 59.689974,-65.03999" />
<path d="M 46.5,-79.071663 L 47.436665,-79.071663 Q 47.896669,-79.071663 48.16167,-78.746663 Q 48.426671,-78.421663 48.426671,-77.854997 Q 48.426671,-77.28833 48.163336,-76.964996 Q 47.9,-76.641661 47.436665,-76.641661 L 46.5,-76.641661 L 46.5,-79.071663 M 46.810004,-78.798334 L 46.810004,-76.915001 L 47.383333,-76.915001 C 47.861951,-76.913266 48.118055,-77.242282 48.116667,-77.858328 C 48.118055,-78.469584 47.861951,-78.800418 47.383333,-78.798334 L 46.810004,-78.798334" />
<path d="M 50.780002,-78.994992 Q 50.693338,-78.934999 50.663336,-78.841667 Q 50.633333,-78.748334 50.633333,-78.611659 Q 50.633333,-78.584999 50.635004,-78.528331 Q 50.636675,-78.471663 50.636675,-78.441661 Q 50.636675,-78.344992 50.626671,-78.274995 Q 50.616667,-78.204997 50.585004,-78.124995 Q 50.519173,-77.964996 50.303342,-77.871663 Q 50.506673,-77.771663 50.596674,-77.634999 Q 50.686675,-77.498334 50.686675,-77.291661 Q 50.686675,-76.978326 50.495009,-76.809993 Q 50.303342,-76.641661 49.946669,-76.641661 L 48.826671,-76.641661 L 48.826671,-79.071663 L 49.136675,-79.071663 L 49.136675,-78.024995 L 49.936675,-78.024995 C 50.213613,-78.026383 50.335287,-78.159168 50.336675,-78.458328 L 50.336675,-78.674995 Q 50.336675,-78.918332 50.403342,-79.071663 L 50.780002,-79.071663 L 50.780002,-78.994992 M 49.886675,-76.915001 C 50.035561,-76.915001 50.152226,-76.945 50.23667,-77.004997 C 50.321114,-77.064994 50.363336,-77.174995 50.363336,-77.334999 C 50.361601,-77.625624 50.217288,-77.749929 49.886675,-77.751666 L 49.136675,-77.751666 L 49.136675,-76.915001 L 49.886675,-76.915001" />
<path d="M 52.480002,-78.341661 L 52.730002,-79.071663 L 53.076671,-79.071663 L 52.22334,-76.641661 L 51.82334,-76.641661 L 50.956673,-79.071663 L 51.286675,-79.071663 L 51.543338,-78.341661 L 52.480002,-78.341661 M 52.393338,-78.081668 L 51.620009,-78.081668 L 52.020009,-76.974995 L 52.393338,-78.081668" />
<path d="M 55.483333,-79.071663 L 56.1,-76.641661 L 55.753342,-76.641661 L 55.306673,-78.615001 L 54.753342,-76.641661 L 54.420009,-76.641661 L 53.880002,-78.615001 L 53.42334,-76.641661 L 53.076671,-76.641661 L 53.7,-79.071663 L 54.040007,-79.071663 L 54.583333,-77.074995 L 55.143338,-79.071663 L 55.483333,-79.071663" />
<path d="M 56.730002,-76.641661 L 56.416667,-76.641661 L 56.416667,-79.071663 L 56.730002,-79.071663 L 56.730002,-76.641661" />
<path d="M 59.110004,-76.641661 L 58.816667,-76.641661 L 58.816667,-78.628326 L 57.546669,-76.641661 L 57.210004,-76.641661 L 57.210004,-79.071663 L 57.503342,-79.071663 L 57.503342,-77.101666 L 58.760004,-79.071663 L 59.110004,-79.071663 L 59.110004,-76.641661" />
<path d="M 61.420009,-78.128326 C 61.420009,-78.346106 61.348342,-78.524996 61.205008,-78.664996 C 61.061674,-78.804995 60.878895,-78.874995 60.656673,-78.874995 C 60.52334,-78.874995 60.404451,-78.852217 60.300006,-78.806662 C 60.088336,-78.715727 59.976123,-78.574816 59.893338,-78.38833 C 59.811942,-78.200629 59.788066,-78.034921 59.786675,-77.865001 C 59.786675,-77.567222 59.864453,-77.327776 60.020009,-77.146663 C 60.175564,-76.965551 60.38223,-76.874995 60.640007,-76.874995 C 60.824447,-76.874995 60.979445,-76.919994 61.105002,-77.009993 C 61.230559,-77.099993 61.310004,-77.22277 61.343338,-77.378326 L 61.660004,-77.378326 C 61.617781,-77.131661 61.507225,-76.940551 61.328337,-76.804997 C 61.149449,-76.669443 60.921116,-76.601666 60.643338,-76.601666 C 60.46556,-76.601666 60.306116,-76.631109 60.165007,-76.689996 C 60.023897,-76.748882 59.911675,-76.82277 59.828342,-76.911659 C 59.745009,-77.000548 59.675564,-77.103883 59.620009,-77.221663 C 59.506467,-77.458615 59.479101,-77.666938 59.476671,-77.881668 C 59.476671,-78.254997 59.580004,-78.55944 59.78667,-78.794998 C 59.993336,-79.030556 60.261115,-79.148334 60.590007,-79.148334 C 60.912229,-79.148334 61.188896,-79.019443 61.420009,-78.761659 L 61.496669,-79.084999 L 61.693338,-79.084999 L 61.693338,-77.78833 L 60.680002,-77.78833 L 60.680002,-78.061659 L 61.420009,-78.061659 L 61.420009,-78.128326" />
<path d="M 64.893338,-76.641661 L 64.6,-76.641661 L 64.6,-78.628326 L 63.330002,-76.641661 L 62.993338,-76.641661 L 62.993338,-79.071663 L 63.286675,-79.071663 L 63.286675,-77.101666 L 64.543338,-79.071663 L 64.893338,-79.071663 L 64.893338,-76.641661" />
<path d="M 66.983333,-76.641661 L 66.983333,-78.348334 Q 66.983333,-78.594992 66.816667,-78.734993 Q 66.65,-78.874995 66.356673,-78.874995 Q 66.07334,-78.874995 65.905008,-78.743327 Q 65.736675,-78.611659 65.736675,-78.348334 L 65.736675,-76.641661 L 65.426671,-76.641661 L 65.426671,-78.348334 Q 65.426671,-78.718332 65.675006,-78.933333 Q 65.92334,-79.148334 66.356673,-79.148334 Q 66.783333,-79.148334 67.038336,-78.931668 Q 67.293338,-78.715001 67.293338,-78.348334 L 67.293338,-76.641661 L 66.983333,-76.641661" />
<path d="M 69.136675,-79.071663 L 69.820009,-77.034999 L 69.820009,-79.071663 L 70.113336,-79.071663 L 70.113336,-76.641661 L 69.683333,-76.641661 L 68.976671,-78.758328 L 68.256673,-76.641661 L 67.826671,-76.641661 L 67.826671,-79.071663 L 68.120009,-79.071663 L 68.120009,-77.034999 L 68.810004,-79.071663 L 69.136675,-79.071663" />
<path d="M 71.743338,-79.071663 Q 72.066667,-79.071663 72.263336,-78.881662 Q 72.460004,-78.691661 72.460004,-78.378326 Q 72.460004,-78.158328 72.355002,-78.018327 Q 72.25,-77.878326 72.016667,-77.78833 C 72.24111,-77.683889 72.353331,-77.507221 72.353331,-77.258328 C 72.352463,-77.120032 72.312533,-76.991627 72.211665,-76.859993 C 72.113922,-76.731138 71.902184,-76.639404 71.633333,-76.641661 L 70.646669,-76.641661 L 70.646669,-79.071663 L 71.743338,-79.071663 M 71.556673,-76.915001 Q 72.043338,-76.915001 72.043338,-77.301666 Q 72.043338,-77.68833 71.556673,-77.68833 L 70.956673,-77.68833 L 70.956673,-76.915001 L 71.556673,-76.915001 M 71.713336,-78.798334 L 70.956673,-78.798334 L 70.956673,-77.961659 L 71.713336,-77.961659 Q 71.92334,-77.961659 72.03667,-78.07666 Q 72.15,-78.191661 72.15,-78.381668 Q 72.15,-78.481668 72.116667,-78.568332 Q 72.083333,-78.654997 71.980002,-78.726666 Q 71.876671,-78.798334 71.713336,-78.798334" />
<path d="M 73.086675,-77.965001 L 74.410004,-77.965001 L 74.410004,-77.691661 L 73.086675,-77.691661 L 73.086675,-76.915001 L 74.460004,-76.915001 L 74.460004,-76.641661 L 72.776671,-76.641661 L 72.776671,-79.071663 L 74.520009,-79.071663 L 74.520009,-78.798334 L 73.086675,-78.798334 L 73.086675,-77.965001" />
<path d="M 76.840007,-78.994992 Q 76.753342,-78.934999 76.72334,-78.841667 Q 76.693338,-78.748334 76.693338,-78.611659 Q 76.693338,-78.584999 76.695009,-78.528331 Q 76.69668,-78.471663 76.69668,-78.441661 Q 76.69668,-78.344992 76.686675,-78.274995 Q 76.676671,-78.204997 76.645009,-78.124995 Q 76.579178,-77.964996 76.363346,-77.871663 Q 76.566678,-77.771663 76.656679,-77.634999 Q 76.74668,-77.498334 76.74668,-77.291661 Q 76.74668,-76.978326 76.555013,-76.809993 Q 76.363346,-76.641661 76.006673,-76.641661 L 74.886675,-76.641661 L 74.886675,-79.071663 L 75.19668,-79.071663 L 75.19668,-78.024995 L 75.99668,-78.024995 C 76.273618,-78.026383 76.395292,-78.159168 76.39668,-78.458328 L 76.39668,-78.674995 Q 76.39668,-78.918332 76.463346,-79.071663 L 76.840007,-79.071663 L 76.840007,-78.994992 M 75.94668,-76.915001 C 76.095566,-76.915001 76.212231,-76.945 76.296674,-77.004997 C 76.381118,-77.064994 76.42334,-77.174995 76.42334,-77.334999 C 76.421605,-77.625624 76.277292,-77.749929 75.94668,-77.751666 L 75.19668,-77.751666 L 75.19668,-76.915001 L 75.94668,-76.915001" />
<path d="M 77.59668,-77.324995 L 77.250011,-77.324995 L 77.250011,-77.671663 L 77.59668,-77.671663 L 77.59668,-77.324995" />
<path d="M 77.59668,-78.724995 L 77.250011,-78.724995 L 77.250011,-79.071663 L 77.59668,-79.071663 L 77.59668,-78.724995" />
<path d="M 48.145003,-87.114168 Q 48.629997,-87.114168 48.925,-86.829167 Q 49.220003,-86.544165 49.220003,-86.074162 Q 49.220003,-85.744165 49.0625,-85.534163 Q 48.904997,-85.324162 48.554997,-85.189168 C 48.891661,-85.032506 49.059994,-84.767505 49.059994,-84.394165 C 49.058691,-84.186722 48.998796,-83.994113 48.847494,-83.796663 C 48.772494,-83.698329 48.659994,-83.619162 48.509994,-83.559163 C 48.359994,-83.499164 48.183328,-83.469165 47.979997,-83.469165 L 46.5,-83.469165 L 46.5,-87.114168 L 48.145003,-87.114168 M 47.865007,-83.879175 Q 48.595003,-83.879175 48.595003,-84.459172 Q 48.595003,-85.039168 47.865007,-85.039168 L 46.965007,-85.039168 L 46.965007,-83.879175 L 47.865007,-83.879175 M 48.1,-86.704175 L 46.965007,-86.704175 L 46.965007,-85.449162 L 48.1,-85.449162 Q 48.415007,-85.449162 48.585002,-85.621663 Q 48.754997,-85.794165 48.754997,-86.079175 Q 48.754997,-86.229175 48.704997,-86.359172 Q 48.654997,-86.489168 48.5,-86.596672 Q 48.345003,-86.704175 48.1,-86.704175" />
<path d="M 49.820003,-87.114168 L 51.225,-87.114168 Q 51.915007,-87.114168 52.312508,-86.626668 Q 52.71001,-86.139168 52.71001,-85.289168 Q 52.71001,-84.439168 52.315007,-83.954167 Q 51.920003,-83.469165 51.225,-83.469165 L 49.820003,-83.469165 L 49.820003,-87.114168 M 50.28501,-86.704175 L 50.28501,-83.879175 L 51.145003,-83.879175 C 51.86293,-83.876572 52.247085,-84.370097 52.245003,-85.294165 C 52.245003,-85.754169 52.151671,-86.104172 51.965007,-86.344173 C 51.778342,-86.584174 51.505008,-86.704175 51.145003,-86.704175 L 50.28501,-86.704175" />
<path d="M 54.345003,-85.554175 L 53.140007,-85.554175 L 53.140007,-85.914168 L 54.345003,-85.914168 L 54.345003,-85.554175" />
<path d="M 55.87002,-84.589168 L 55.87002,-87.114168 L 56.31001,-87.114168 L 56.31001,-83.569165 L 56.02002,-83.569165 Q 55.905013,-83.979175 55.750016,-84.094173 Q 55.59502,-84.209172 55.08501,-84.274162 L 55.08501,-84.589168 L 55.87002,-84.589168" />
<path d="M 92.153331,-78.041661 C 92.45111,-78.119439 92.6,-78.250552 92.6,-78.434999 C 92.598784,-78.538252 92.571772,-78.623962 92.48833,-78.718332 C 92.407317,-78.809926 92.227678,-78.876731 91.979991,-78.874995 C 91.848886,-78.874995 91.736111,-78.857773 91.641667,-78.823329 C 91.547222,-78.788885 91.47611,-78.742775 91.428331,-78.684999 C 91.331905,-78.568405 91.294195,-78.453817 91.293327,-78.315001 L 91.293327,-78.298334 L 91.0,-78.298334 C 91.002221,-78.431668 91.023886,-78.549445 91.064996,-78.651666 C 91.147736,-78.859059 91.26947,-78.965934 91.443327,-79.046663 C 91.618399,-79.126003 91.776038,-79.146772 91.959994,-79.148334 C 92.246451,-79.150765 92.487986,-79.071461 92.623329,-78.971663 C 92.692217,-78.920549 92.748882,-78.861104 92.793327,-78.793327 C 92.883952,-78.656904 92.908257,-78.534757 92.909994,-78.404997 C 92.909994,-78.256111 92.864994,-78.128333 92.774995,-78.021663 C 92.684995,-77.914994 92.557773,-77.839437 92.393327,-77.794992 L 91.783333,-77.631668 Q 91.563325,-77.571663 91.473329,-77.491661 Q 91.383333,-77.411659 91.383333,-77.271663 Q 91.383333,-77.08833 91.531662,-76.974995 Q 91.679991,-76.861659 91.92666,-76.861659 Q 92.216667,-76.861659 92.373329,-76.989996 Q 92.529991,-77.118332 92.533333,-77.354997 L 92.82666,-77.354997 Q 92.823329,-76.998334 92.589996,-76.8 Q 92.356662,-76.601666 91.936665,-76.601666 Q 91.536665,-76.601666 91.304997,-76.793332 Q 91.073329,-76.984999 91.073329,-77.315001 Q 91.073329,-77.758328 91.55,-77.881668 L 92.153331,-78.041661" />
<path d="M 94.839996,-77.965001 L 94.839996,-79.071663 L 95.149989,-79.071663 L 95.149989,-76.641661 L 94.839996,-76.641661 L 94.839996,-77.691661 L 93.589996,-77.691661 L 93.589996,-76.641661 L 93.279991,-76.641661 L 93.279991,-79.071663 L 93.589996,-79.071663 L 93.589996,-77.965001 L 94.839996,-77.965001" />
<path d="M 95.919998,-77.965001 L 97.243327,-77.965001 L 97.243327,-77.691661 L 95.919998,-77.691661 L 95.919998,-76.915001 L 97.293327,-76.915001 L 97.293327,-76.641661 L 95.609994,-76.641661 L 95.609994,-79.071663 L 97.353331,-79.071663 L 97.353331,-78.798334 L 95.919998,-78.798334 L 95.919998,-77.965001" />
<path d="M 97.956673,-77.965001 L 99.280002,-77.965001 L 99.280002,-77.691661 L 97.956673,-77.691661 L 97.956673,-76.915001 L 99.330002,-76.915001 L 99.330002,-76.641661 L 97.646669,-76.641661 L 97.646669,-79.071663 L 99.390007,-79.071663 L 99.390007,-78.798334 L 97.956673,-78.798334 L 97.956673,-77.965001" />
<path d="M 100.666678,-76.915001 L 101.463336,-76.915001 L 101.463336,-76.641661 L 99.556673,-76.641661 L 99.556673,-76.915001 L 100.356673,-76.915001 L 100.356673,-79.071663 L 100.666678,-79.071663 L 100.666678,-76.915001" />
<path d="M 102.176671,-78.724995 L 101.830002,-78.724995 L 101.830002,-79.071663 L 102.176671,-79.071663 L 102.176671,-78.724995" />
<path d="M 102.176671,-77.324995 L 101.830002,-77.324995 L 101.830002,-77.671663 L 102.176671,-77.671663 L 102.176671,-77.324995" />
<path d="M 91.78501,-84.539168 L 91.78501,-87.064168 L 92.225,-87.064168 L 92.225,-83.519165 L 91.93501,-83.519165 Q 91.820003,-83.929175 91.665007,-84.044173 Q 91.51001,-84.159172 91.0,-84.224162 L 91.0,-84.539168 L 91.78501,-84.539168" />
<path d="M -45.9,22.210002 L -55.9,22.210002 L -55.9,21.710002 L -45.9,21.710002 L -45.9,22.210002" />
<path d="M -45.9,78.610002 L -55.9,78.610002 L -55.9,78.110002 L -45.9,78.110002 L -45.9,78.610002" />
<path d="M -46.9,75.360002 A 10.137937550497139,10.137937550497139 -170.5376777919744 0,0 -47.9,78.360002 A 10.137937550497139,10.137937550497139 -9.46232220802563 0,0 -48.9,75.360002 L -48.15,75.641252 L -48.15,55.392249 L -47.65,55.392249 L -47.65,75.641252 L -46.9,75.360002" />
<path d="M -48.9,24.960002 A 10.137937550497139,10.137937550497139 9.462322208025604 0,0 -47.9,21.960002 A 10.137937550497139,10.137937550497139 170.53767779197437 0,0 -46.9,24.960002 L -47.65,24.678752 L -47.65,44.927754 L -48.15,44.927754 L -48.15,24.678752 L -48.9,24.960002" />
<path d="M -49.181003,48.471252 L -49.181003,47.190254 L -47.830006,47.004749 L -47.830006,47.288259 Q -47.959501,47.396756 -48.010252,47.498253 Q -48.061003,47.599749 -48.061003,47.743259 Q -48.061003,47.991756 -47.901755,48.138757 Q -47.742506,48.285759 -47.466003,48.285759 Q -47.200006,48.285759 -47.046003,48.140506 Q -46.892001,47.995254 -46.892001,47.743259 Q -46.892001,47.344256 -47.308503,47.235759 L -47.308503,46.927754 C -47.268832,46.934757 -47.238498,46.94059 -47.2175,46.945254 C -47.196502,46.949918 -47.158586,46.962167 -47.10375,46.982003 C -47.048914,47.001838 -47.003414,47.02284 -46.967248,47.045007 C -46.931082,47.067175 -46.889083,47.099841 -46.84125,47.143006 C -46.793417,47.186171 -46.754918,47.234004 -46.725751,47.286504 C -46.669424,47.389498 -46.616991,47.56093 -46.618997,47.750254 C -46.618997,47.999918 -46.701831,48.204084 -46.8675,48.362754 C -47.033169,48.521424 -47.246669,48.600759 -47.507999,48.600759 C -47.752999,48.600759 -47.951915,48.526674 -48.104748,48.378505 C -48.25758,48.230336 -48.333997,48.037253 -48.333997,47.799256 C -48.333997,47.631252 -48.283832,47.478419 -48.183503,47.340759 L -48.876497,47.438752 L -48.876497,48.471252 L -49.181003,48.471252" />
<path d="M -48.533503,50.494256 Q -48.837999,50.448752 -49.009501,50.261504 Q -49.181003,50.074256 -49.181003,49.790747 Q -49.181003,49.668247 -49.153005,49.558001 Q -49.125006,49.447754 -49.035751,49.323505 Q -48.946497,49.199256 -48.802999,49.110002 Q -48.659501,49.020747 -48.409249,48.961252 Q -48.158997,48.901756 -47.830006,48.901756 Q -46.618997,48.901756 -46.618997,49.734749 Q -46.618997,50.088247 -46.855247,50.317502 Q -47.091497,50.546756 -47.455499,50.546756 Q -47.802001,50.546756 -48.0225,50.333253 Q -48.242999,50.119749 -48.242999,49.787249 Q -48.242999,49.419749 -47.966497,49.216756 C -48.26983,49.219088 -48.50258,49.267504 -48.664748,49.362003 C -48.826915,49.456502 -48.907999,49.592418 -48.907999,49.769749 C -48.909822,49.982589 -48.769677,50.136746 -48.533503,50.186252 L -48.533503,50.494256 M -47.970006,49.748752 Q -47.970006,49.972754 -47.824753,50.102255 Q -47.679501,50.231756 -47.431003,50.231756 Q -47.196497,50.231756 -47.044249,50.091756 Q -46.892001,49.951756 -46.892001,49.738247 Q -46.892001,49.521252 -47.05125,49.377754 Q -47.210499,49.234256 -47.448503,49.234256 Q -47.679501,49.234256 -47.824753,49.377754 Q -47.970006,49.521252 -47.970006,49.748752" />
<path d="M -47.063503,51.341252 L -47.063503,50.977249 L -46.699501,50.977249 L -46.699501,51.341252 L -47.063503,51.341252" />
<path d="M -47.294501,52.716745 L -46.699501,52.716745 L -46.699501,53.024749 L -47.294501,53.024749 L -47.294501,53.392249 L -47.571003,53.392249 L -47.571003,53.024749 L -49.181003,53.024749 L -49.181003,52.797249 L -47.620006,51.670254 L -47.294501,51.670254 L -47.294501,52.716745 M -47.571003,52.716745 L -47.571003,51.939749 L -48.656003,52.716745 L -47.571003,52.716745" />
<path d="M -114.05,9.960002 L -114.05,19.960002 L -114.55,19.960002 L -114.55,9.960002 L -114.05,9.960002" />
<path d="M -57.65,9.960002 L -57.65,19.960002 L -58.15,19.960002 L -58.15,9.960002 L -57.65,9.960002" />
<path d="M -60.9,10.960002 A 10.137937550497139,10.137937550497139 99.46232220802563 0,0 -57.9,11.960002 A 10.137937550497139,10.137937550497139 -99.46232220802563 0,0 -60.9,12.960002 L -60.61875,12.210002 L -80.867752,12.210002 L -80.867752,11.710002 L -60.61875,11.710002 L -60.9,10.960002" />
<path d="M -111.3,12.960002 A 10.137937550497139,10.137937550497139 -80.53767779197439 0,0 -114.3,11.960002 A 10.137937550497139,10.137937550497139 80.53767779197437 0,0 -111.3,10.960002 L -111.58125,11.710002 L -91.332248,11.710002 L -91.332248,12.210002 L -111.58125,12.210002 L -111.3,12.960002" />
<path d="M -87.78875,13.241005 L -89.069748,13.241005 L -89.255252,11.890007 L -88.971743,11.890007 Q -88.863245,12.019503 -88.761749,12.070254 Q -88.660252,12.121005 -88.516743,12.121005 Q -88.268245,12.121005 -88.121244,11.961756 Q -87.974243,11.802507 -87.974243,11.526005 Q -87.974243,11.260007 -88.119495,11.106005 Q -88.264748,10.952003 -88.516743,10.952003 Q -88.915745,10.952003 -89.024243,11.368505 L -89.332248,11.368505 C -89.325245,11.328834 -89.319411,11.298499 -89.314748,11.277502 C -89.310084,11.256504 -89.297834,11.218587 -89.277999,11.163752 C -89.258163,11.108916 -89.237162,11.063415 -89.214994,11.027249 C -89.192827,10.991083 -89.160161,10.949084 -89.116995,10.901252 C -89.07383,10.853419 -89.025998,10.814919 -88.973498,10.785753 C -88.870503,10.729425 -88.699072,10.676992 -88.509748,10.678998 C -88.260084,10.678998 -88.055917,10.761833 -87.897248,10.927502 C -87.738578,11.093171 -87.659243,11.30667 -87.659243,11.568001 C -87.659243,11.813001 -87.733328,12.011917 -87.881497,12.164749 C -88.029665,12.317582 -88.222748,12.393998 -88.460745,12.393998 C -88.62875,12.393998 -88.781583,12.343834 -88.919243,12.243505 L -88.82125,12.936498 L -87.78875,12.936498 L -87.78875,13.241005" />
<path d="M -85.765745,12.593505 Q -85.81125,12.898001 -85.998498,13.069503 Q -86.185745,13.241005 -86.469255,13.241005 Q -86.591755,13.241005 -86.702001,13.213006 Q -86.812248,13.185007 -86.936497,13.095753 Q -87.060745,13.006498 -87.15,12.863001 Q -87.239255,12.719503 -87.29875,12.469251 Q -87.358245,12.218998 -87.358245,11.890007 Q -87.358245,10.678998 -86.525252,10.678998 Q -86.171755,10.678998 -85.9425,10.915248 Q -85.713245,11.151498 -85.713245,11.515501 Q -85.713245,11.862003 -85.926749,12.082502 Q -86.140252,12.303001 -86.472752,12.303001 Q -86.840252,12.303001 -87.043245,12.026498 C -87.040914,12.329832 -86.992498,12.562582 -86.897999,12.724749 C -86.8035,12.886917 -86.667584,12.968001 -86.490252,12.968001 C -86.277413,12.969824 -86.123256,12.829679 -86.07375,12.593505 L -85.765745,12.593505 M -86.51125,12.030007 Q -86.287248,12.030007 -86.157747,11.884755 Q -86.028245,11.739503 -86.028245,11.491005 Q -86.028245,11.256498 -86.168245,11.104251 Q -86.308245,10.952003 -86.521755,10.952003 Q -86.73875,10.952003 -86.882248,11.111252 Q -87.025745,11.270501 -87.025745,11.508505 Q -87.025745,11.739503 -86.882248,11.884755 Q -86.73875,12.030007 -86.51125,12.030007" />
<path d="M -84.91875,11.123505 L -85.282752,11.123505 L -85.282752,10.759503 L -84.91875,10.759503 L -84.91875,11.123505" />
<path d="M -83.543257,11.354503 L -83.543257,10.759503 L -83.235252,10.759503 L -83.235252,11.354503 L -82.867752,11.354503 L -82.867752,11.631005 L -83.235252,11.631005 L -83.235252,13.241005 L -83.462752,13.241005 L -84.589748,11.680007 L -84.589748,11.354503 L -83.543257,11.354503 M -83.543257,11.631005 L -84.320252,11.631005 L -83.543257,12.716005 L -83.543257,11.631005" />
<path d="M -89.525,2.519999 L -89.525,-2.480001 L -89.025,-2.480001 L -89.025,2.519999 L -89.525,2.519999" />
<path d="M -83.175,2.519999 L -83.175,-2.480001 L -82.675,-2.480001 L -82.675,2.519999 L -83.175,2.519999" />
<path d="M -79.925,1.519999 A 10.137937550497139,10.137937550497139 -80.53767779197439 0,0 -82.925,0.519999 A 10.137937550497139,10.137937550497139 80.53767779197437 0,0 -79.925,-0.480001 L -80.20625,0.269999 L -76.925,0.269999 L -76.925,0.769999 L -80.20625,0.769999 L -79.925,1.519999" />
<path d="M -92.275,-0.480001 A 10.137937550497139,10.137937550497139 99.46232220802561 0,0 -89.275,0.519999 A 10.137937550497139,10.137937550497139 -99.46232220802564 0,0 -92.275,1.519999 L -91.99375,0.769999 L -95.275,0.769999 L -95.275,0.269999 L -91.99375,0.269999 L -92.275,-0.480001" />
<path d="M -86.728245,1.153503 Q -86.77375,1.457998 -86.960998,1.6295 Q -87.148245,1.801003 -87.431755,1.801003 Q -87.554255,1.801003 -87.664501,1.773004 Q -87.774748,1.745005 -87.898997,1.65575 Q -88.023245,1.566496 -88.1125,1.422998 Q -88.201755,1.2795 -88.26125,1.029248 Q -88.320745,0.778996 -88.320745,0.450005 Q -88.320745,-0.761004 -87.487752,-0.761004 Q -87.134255,-0.761004 -86.905,-0.524754 Q -86.675745,-0.288504 -86.675745,0.075498 Q -86.675745,0.422 -86.889249,0.642499 Q -87.102752,0.862998 -87.435252,0.862998 Q -87.802752,0.862998 -88.005745,0.586496 C -88.003414,0.889829 -87.954998,1.122579 -87.860499,1.284747 C -87.766,1.446914 -87.630084,1.527998 -87.452752,1.527998 C -87.239913,1.529821 -87.085756,1.389676 -87.03625,1.153503 L -86.728245,1.153503 M -87.47375,0.590005 Q -87.249748,0.590005 -87.120247,0.444753 Q -86.990745,0.2995 -86.990745,0.051003 Q -86.990745,-0.183504 -87.130745,-0.335752 Q -87.270745,-0.488 -87.484255,-0.488 Q -87.70125,-0.488 -87.844748,-0.328751 Q -87.988245,-0.169502 -87.988245,0.068503 Q -87.988245,0.2995 -87.844748,0.444753 Q -87.70125,0.590005 -87.47375,0.590005" />
<path d="M -85.88125,-0.316497 L -86.245252,-0.316497 L -86.245252,-0.6805 L -85.88125,-0.6805 L -85.88125,-0.316497" />
<path d="M -85.177752,0.9995 Q -85.170757,1.241003 -85.071003,1.386249 Q -84.97125,1.531496 -84.705252,1.531496 Q -84.502248,1.531496 -84.385,1.417746 Q -84.267752,1.303996 -84.267752,1.107998 Q -84.267752,0.880498 -84.406003,0.803497 Q -84.544255,0.726496 -84.876755,0.7195 L -84.876755,0.457 L -84.838257,0.457 L -84.70875,0.460498 Q -84.194255,0.460498 -84.194255,0.008996 Q -84.194255,-0.2255 -84.330751,-0.35675 Q -84.467248,-0.488 -84.70875,-0.488 Q -84.960757,-0.488 -85.086755,-0.358499 Q -85.212752,-0.228997 -85.230252,0.040498 L -85.538257,0.040498 Q -85.482248,-0.761004 -84.719255,-0.761004 Q -84.337752,-0.761004 -84.108503,-0.551004 Q -83.879255,-0.341004 -83.879255,0.012505 Q -83.879255,0.250498 -83.977253,0.388749 Q -84.075252,0.527 -84.299255,0.603996 Q -83.952752,0.740498 -83.952752,1.118503 Q -83.952752,1.437 -84.154002,1.619001 Q -84.355252,1.801003 -84.70875,1.801003 Q -85.468257,1.801003 -85.485757,0.9995 L -85.177752,0.9995" />
<path d="M 40.2,-25.653999 L 30.2,-25.653999 L 30.2,-26.153999 L 40.2,-26.153999 L 40.2,-25.653999" />
<path d="M 40.2,50.946001 L 30.2,50.946001 L 30.2,50.446001 L 40.2,50.446001 L 40.2,50.946001" />
<path d="M 39.2,47.696001 A 10.137937550497139,10.137937550497139 -170.5376777919744 0,0 38.2,50.696001 A 10.137937550497139,10.137937550497139 -9.46232220802563 0,0 37.2,47.696001 L 37.95,47.977251 L 37.95,17.596752 L 38.45,17.596752 L 38.45,47.977251 L 39.2,47.696001" />
<path d="M 37.2,-22.903999 A 10.137937550497139,10.137937550497139 9.462322208025604 0,0 38.2,-25.903999 A 10.137937550497139,10.137937550497139 170.53767779197437 0,0 39.2,-22.903999 L 38.45,-23.185249 L 38.45,7.19525 L 37.95,7.19525 L 37.95,-23.185249 L 37.2,-22.903999" />
<path d="M 36.918997,10.854252 L 36.918997,9.19525 L 37.223504,9.19525 L 37.223504,10.535754 Q 37.857001,10.091248 38.3225,9.874252 Q 38.787999,9.657257 39.400499,9.517257 L 39.400499,9.846248 Q 38.805499,9.94775 38.271749,10.191001 Q 37.737999,10.434252 37.177999,10.854252 L 36.918997,10.854252" />
<path d="M 37.566497,12.723254 Q 37.262001,12.67775 37.090499,12.490502 Q 36.918997,12.303254 36.918997,12.019745 Q 36.918997,11.897245 36.946996,11.786999 Q 36.974994,11.676752 37.064249,11.552503 Q 37.153504,11.428254 37.297001,11.339 Q 37.440499,11.249745 37.690751,11.19025 Q 37.941004,11.130754 38.269994,11.130754 Q 39.481004,11.130754 39.481004,11.963748 Q 39.481004,12.317245 39.244754,12.5465 Q 39.008504,12.775754 38.644501,12.775754 Q 38.297999,12.775754 38.0775,12.562251 Q 37.857001,12.348748 37.857001,12.016248 Q 37.857001,11.648748 38.133504,11.445754 C 37.83017,11.448086 37.59742,11.496502 37.435252,11.591001 C 37.273085,11.6855 37.192001,11.821416 37.192001,11.998748 C 37.190178,12.211587 37.330323,12.365744 37.566497,12.41525 L 37.566497,12.723254 M 38.129994,11.97775 Q 38.129994,12.201752 38.275247,12.331253 Q 38.420499,12.460754 38.668997,12.460754 Q 38.903504,12.460754 39.055751,12.320754 Q 39.207999,12.180754 39.207999,11.967245 Q 39.207999,11.75025 39.04875,11.606752 Q 38.889501,11.463254 38.651497,11.463254 Q 38.420499,11.463254 38.275247,11.606752 Q 38.129994,11.75025 38.129994,11.97775" />
<path d="M 39.036497,13.57025 L 39.036497,13.206248 L 39.400499,13.206248 L 39.400499,13.57025 L 39.036497,13.57025" />
<path d="M 37.566497,15.544252 Q 37.262001,15.498748 37.090499,15.3115 Q 36.918997,15.124252 36.918997,14.840743 Q 36.918997,14.718243 36.946996,14.607996 Q 36.974994,14.49775 37.064249,14.373501 Q 37.153504,14.249252 37.297001,14.159998 Q 37.440499,14.070743 37.690751,14.011248 Q 37.941004,13.951752 38.269994,13.951752 Q 39.481004,13.951752 39.481004,14.784745 Q 39.481004,15.138243 39.244754,15.367498 Q 39.008504,15.596752 38.644501,15.596752 Q 38.297999,15.596752 38.0775,15.383249 Q 37.857001,15.169745 37.857001,14.837245 Q 37.857001,14.469745 38.133504,14.266752 C 37.83017,14.269084 37.59742,14.317499 37.435252,14.411999 C 37.273085,14.506498 37.192001,14.642413 37.192001,14.819745 C 37.190178,15.032585 37.330323,15.186742 37.566497,15.236248 L 37.566497,15.544252 M 38.129994,14.798748 Q 38.129994,15.02275 38.275247,15.152251 Q 38.420499,15.281752 38.668997,15.281752 Q 38.903504,15.281752 39.055751,15.141752 Q 39.207999,15.001752 39.207999,14.788243 Q 39.207999,14.571248 39.04875,14.42775 Q 38.889501,14.284252 38.651497,14.284252 Q 38.420499,14.284252 38.275247,14.42775 Q 38.129994,14.571248 38.129994,14.798748" />
<path d="M -95.663994,84.052999 Q -95.325,84.391994 -95.325,84.950001 Q -95.325,85.568009 -95.675996,85.901007 Q -96.026992,86.234005 -96.675,86.234005 L -98.480996,86.234005 L -98.480996,81.860001 L -97.922988,81.860001 L -97.922988,83.714005 L -96.549004,83.714005 Q -96.002988,83.714005 -95.663994,84.052999 M -97.922988,84.205997 L -97.922988,85.741994 L -96.759004,85.741994 Q -96.356992,85.741994 -96.131992,85.537999 Q -95.906992,85.334005 -95.906992,84.974005 Q -95.906992,84.614005 -96.131992,84.410001 Q -96.356992,84.205997 -96.759004,84.205997 L -97.922988,84.205997" />
<path d="M -94.160996,86.234005 L -94.665,86.234005 L -94.665,81.860001 L -94.160996,81.860001 L -94.160996,86.234005" />
<path d="M -93.482988,84.074005 L -92.979004,84.074005 Q -92.949004,84.361994 -92.769004,84.496994 Q -92.589004,84.631994 -92.240996,84.631994 Q -91.905,84.631994 -91.722002,84.511994 Q -91.539004,84.391994 -91.539004,84.164005 L -91.539004,84.031994 Q -91.539004,83.875997 -91.652998,83.797999 Q -91.766992,83.720001 -92.060996,83.684005 Q -92.276992,83.654005 -92.360996,83.642003 Q -92.445,83.630001 -92.625,83.600001 Q -92.805,83.570001 -92.877002,83.549005 Q -92.949004,83.528009 -93.084004,83.483009 Q -93.219004,83.438009 -93.275996,83.390001 Q -93.332988,83.341994 -93.416992,83.266994 Q -93.500996,83.191994 -93.533994,83.105001 Q -93.566992,83.018009 -93.593994,82.904005 Q -93.620996,82.790001 -93.620996,82.651994 Q -93.620996,82.225997 -93.341992,81.973996 Q -93.062988,81.721994 -92.589004,81.721994 Q -92.025,81.721994 -91.520996,82.184005 Q -91.490996,81.944005 -91.367998,81.832999 Q -91.245,81.721994 -91.005,81.721994 Q -90.872988,81.721994 -90.662988,81.775997 L -90.662988,82.154005 Q -90.716992,82.141994 -90.770996,82.141994 Q -91.040996,82.141994 -91.040996,82.388009 L -91.040996,84.235997 Q -91.040996,84.655997 -91.340996,84.875001 Q -91.640996,85.094005 -92.222988,85.094005 Q -93.446992,85.094005 -93.482988,84.074005 M -91.802998,82.379005 Q -92.066992,82.160001 -92.480996,82.160001 Q -92.775,82.160001 -92.937002,82.292003 Q -93.099004,82.424005 -93.099004,82.664005 Q -93.099004,82.904005 -92.919004,83.039005 Q -92.739004,83.174005 -92.517002,83.210001 Q -92.295,83.245997 -91.992002,83.293996 Q -91.689004,83.341994 -91.539004,83.414005 L -91.539004,82.850001 Q -91.539004,82.598009 -91.802998,82.379005" />
<path d="M -90.212988,85.004005 L -90.212988,81.860001 L -89.708984,81.860001 L -89.708984,83.594005 Q -89.708984,84.074005 -89.47499,84.365001 Q -89.240996,84.655997 -88.856992,84.655997 Q -88.556992,84.655997 -88.382988,84.487999 Q -88.208984,84.320001 -88.208984,84.038009 L -88.208984,81.860001 L -87.710996,81.860001 L -87.710996,84.235997 Q -87.710996,84.625997 -87.980996,84.860001 Q -88.250996,85.094005 -88.706992,85.094005 Q -89.05498,85.094005 -89.297988,84.950001 Q -89.540996,84.805997 -89.750996,84.475997 L -89.750996,85.004005 L -90.212988,85.004005" />
<path d="M -83.589004,81.860001 L -82.070996,86.234005 L -82.665,86.234005 L -83.876992,82.531994 L -85.160996,86.234005 L -85.760996,86.234005 L -84.189004,81.860001 L -83.589004,81.860001" />
<path d="M -81.080996,85.004005 L -81.579004,85.004005 L -81.579004,81.860001 L -81.080996,81.860001 L -81.080996,85.004005" />
<path d="M -81.080996,86.234005 L -81.585,86.234005 L -81.585,85.604005 L -81.080996,85.604005 L -81.080996,86.234005" />
<path d="M -80.007012,83.264005 L -77.690996,83.264005 Q -77.690996,85.094005 -79.089004,85.094005 Q -79.743008,85.094005 -80.136006,84.629005 Q -80.529004,84.164005 -80.529004,83.390001 Q -80.529004,82.615997 -80.145,82.168996 Q -79.760996,81.721994 -79.100996,81.721994 Q -78.560996,81.721994 -78.207002,82.010001 Q -77.853008,82.298009 -77.757012,82.814005 L -78.260996,82.814005 Q -78.470996,82.184005 -79.083008,82.184005 Q -79.509004,82.184005 -79.752002,82.477999 Q -79.995,82.771994 -80.007012,83.264005 M -78.225,83.671994 L -79.995,83.671994 Q -79.965,84.110001 -79.719004,84.370997 Q -79.473008,84.631994 -79.09501,84.631994 Q -78.717012,84.631994 -78.471006,84.355997 Q -78.225,84.080001 -78.225,83.671994" />
<path d="M -74.222988,81.860001 L -73.299004,85.004005 L -73.862988,85.004005 L -74.486992,82.555997 L -75.105,85.004005 L -75.716992,85.004005 L -76.316992,82.555997 L -76.959004,85.004005 L -77.510996,85.004005 L -76.599004,81.860001 L -76.035,81.860001 L -75.429004,84.325997 L -74.792988,81.860001 L -74.222988,81.860001" />
<path d="M -104.26499,-85.588009 L -102.177002,-85.588009 L -102.177002,-85.095997 L -104.26499,-85.095997 L -104.26499,-83.698009 L -101.888994,-83.698009 L -101.888994,-83.205997 L -104.822998,-83.205997 L -104.822998,-87.580001 L -104.26499,-87.580001 L -104.26499,-85.588009" />
<path d="M -99.957002,-84.874005 L -99.957002,-84.364005 Q -100.082998,-84.345997 -100.148994,-84.345997 Q -100.388994,-84.345997 -100.58999,-84.498995 Q -100.790986,-84.651993 -101.007002,-85.005997 L -101.007002,-84.435997 L -101.468994,-84.435997 L -101.468994,-87.580001 L -100.96499,-87.580001 L -100.96499,-85.948009 Q -100.96499,-85.330001 -100.712988,-85.107999 Q -100.460986,-84.885997 -99.957002,-84.874005" />
<path d="M -97.236006,-84.792999 Q -97.611006,-84.345997 -98.289014,-84.345997 Q -98.949014,-84.345997 -99.327012,-84.792999 Q -99.70501,-85.240001 -99.70501,-86.032003 Q -99.70501,-86.824005 -99.33001,-87.271007 Q -98.95501,-87.718009 -98.283018,-87.718009 Q -97.623018,-87.718009 -97.242012,-87.274005 Q -96.861006,-86.830001 -96.861006,-86.055997 Q -96.861006,-85.240001 -97.236006,-84.792999 M -98.943018,-85.135001 Q -98.703018,-84.808009 -98.283018,-84.808009 Q -97.857002,-84.808009 -97.62001,-85.138009 Q -97.383018,-85.468009 -97.383018,-86.050001 Q -97.383018,-86.601993 -97.626016,-86.928995 Q -97.869014,-87.255997 -98.283018,-87.255997 Q -98.703018,-87.255997 -98.943018,-86.928995 Q -99.183018,-86.601993 -99.183018,-86.031993 Q -99.183018,-85.461993 -98.943018,-85.135001" />
<path d="M -96.381006,-84.435997 L -96.381006,-87.580001 L -95.877002,-87.580001 L -95.877002,-85.845997 Q -95.877002,-85.365997 -95.643008,-85.075001 Q -95.409014,-84.784005 -95.02501,-84.784005 Q -94.72501,-84.784005 -94.551006,-84.952003 Q -94.377002,-85.120001 -94.377002,-85.401993 L -94.377002,-87.580001 L -93.879014,-87.580001 L -93.879014,-85.204005 Q -93.879014,-84.814005 -94.149014,-84.580001 Q -94.419014,-84.345997 -94.87501,-84.345997 Q -95.222998,-84.345997 -95.466006,-84.490001 Q -95.709014,-84.634005 -95.919014,-84.964005 L -95.919014,-84.435997 L -96.381006,-84.435997" />
<path d="M -91.989014,-84.435997 L -92.50501,-84.435997 L -92.50501,-83.571993 L -93.003018,-83.571993 L -93.003018,-84.435997 L -93.429014,-84.435997 L -93.429014,-84.844005 L -93.003018,-84.844005 L -93.003018,-87.220001 Q -93.003018,-87.460001 -92.847012,-87.589005 Q -92.691006,-87.718009 -92.397021,-87.718009 Q -92.223018,-87.718009 -91.989014,-87.675997 L -91.989014,-87.255997 Q -92.079014,-87.280001 -92.229014,-87.280001 Q -92.391006,-87.280001 -92.448008,-87.222999 Q -92.50501,-87.165997 -92.50501,-86.998009 L -92.50501,-84.844005 L -91.989014,-84.844005 L -91.989014,-84.435997" />
<path d="M -89.331006,-85.588009 L -86.949014,-85.588009 L -86.949014,-85.095997 L -89.331006,-85.095997 L -89.331006,-83.698009 L -86.859014,-83.698009 L -86.859014,-83.205997 L -89.889014,-83.205997 L -89.889014,-87.580001 L -86.751006,-87.580001 L -86.751006,-87.088009 L -89.331006,-87.088009 L -89.331006,-85.588009" />
<path d="M -85.707002,-83.205997 L -86.211006,-83.205997 L -86.211006,-87.580001 L -85.707002,-87.580001 L -85.707002,-83.205997" />
<path d="M -84.64501,-86.175997 L -82.328994,-86.175997 Q -82.328994,-84.345997 -83.727002,-84.345997 Q -84.381006,-84.345997 -84.774004,-84.810997 Q -85.167002,-85.275997 -85.167002,-86.050001 Q -85.167002,-86.824005 -84.782998,-87.271007 Q -84.398994,-87.718009 -83.738994,-87.718009 Q -83.198994,-87.718009 -82.845,-87.430001 Q -82.491006,-87.141993 -82.39501,-86.625997 L -82.898994,-86.625997 Q -83.108994,-87.255997 -83.721006,-87.255997 Q -84.147002,-87.255997 -84.39,-86.962003 Q -84.632998,-86.668009 -84.64501,-86.175997 M -82.862998,-85.768009 L -84.632998,-85.768009 Q -84.602998,-85.330001 -84.357002,-85.069005 Q -84.111006,-84.808009 -83.733008,-84.808009 Q -83.35501,-84.808009 -83.109004,-85.084005 Q -82.862998,-85.360001 -82.862998,-85.768009" />
<path d="M -80.498994,-87.580001 L -79.292998,-84.435997 L -79.857002,-84.435997 L -80.74499,-86.985997 L -81.58499,-84.435997 L -82.148994,-84.435997 L -81.04499,-87.580001 L -80.498994,-87.580001" />
<path d="M -78.97499,-85.365997 L -78.471006,-85.365997 Q -78.441006,-85.078009 -78.261006,-84.943009 Q -78.081006,-84.808009 -77.732998,-84.808009 Q -77.397002,-84.808009 -77.214004,-84.928009 Q -77.031006,-85.048009 -77.031006,-85.275997 L -77.031006,-85.408009 Q -77.031006,-85.564005 -77.145,-85.642003 Q -77.258994,-85.720001 -77.552998,-85.755997 Q -77.768994,-85.785997 -77.852998,-85.797999 Q -77.937002,-85.810001 -78.117002,-85.840001 Q -78.297002,-85.870001 -78.369004,-85.890997 Q -78.441006,-85.911993 -78.576006,-85.956993 Q -78.711006,-86.001993 -78.767998,-86.050001 Q -78.82499,-86.098009 -78.908994,-86.173009 Q -78.992998,-86.248009 -79.025996,-86.335001 Q -79.058994,-86.421993 -79.085996,-86.535997 Q -79.112998,-86.650001 -79.112998,-86.788009 Q -79.112998,-87.214005 -78.833994,-87.466007 Q -78.55499,-87.718009 -78.081006,-87.718009 Q -77.517002,-87.718009 -77.012998,-87.255997 Q -76.982998,-87.495997 -76.86,-87.607003 Q -76.737002,-87.718009 -76.497002,-87.718009 Q -76.36499,-87.718009 -76.15499,-87.664005 L -76.15499,-87.285997 Q -76.208994,-87.298009 -76.262998,-87.298009 Q -76.532998,-87.298009 -76.532998,-87.051993 L -76.532998,-85.204005 Q -76.532998,-84.784005 -76.832998,-84.565001 Q -77.132998,-84.345997 -77.71499,-84.345997 Q -78.938994,-84.345997 -78.97499,-85.365997 M -77.295,-87.060997 Q -77.558994,-87.280001 -77.972998,-87.280001 Q -78.267002,-87.280001 -78.429004,-87.147999 Q -78.591006,-87.015997 -78.591006,-86.775997 Q -78.591006,-86.535997 -78.411006,-86.400997 Q -78.231006,-86.265997 -78.009004,-86.230001 Q -77.787002,-86.194005 -77.484004,-86.146007 Q -77.181006,-86.098009 -77.031006,-86.025997 L -77.031006,-86.590001 Q -77.031006,-86.841993 -77.295,-87.060997" />
<path d="M -74.546982,-84.435997 L -75.062979,-84.435997 L -75.062979,-83.571993 L -75.560986,-83.571993 L -75.560986,-84.435997 L -75.986982,-84.435997 L -75.986982,-84.844005 L -75.560986,-84.844005 L -75.560986,-87.220001 Q -75.560986,-87.460001 -75.40498,-87.589005 Q -75.248975,-87.718009 -74.95499,-87.718009 Q -74.780986,-87.718009 -74.546982,-87.675997 L -74.546982,-87.255997 Q -74.636982,-87.280001 -74.786982,-87.280001 Q -74.948975,-87.280001 -75.005977,-87.222999 Q -75.062979,-87.165997 -75.062979,-86.998009 L -75.062979,-84.844005 L -74.546982,-84.844005 L -74.546982,-84.435997" />
<path d="M -73.57499,-83.205997 L -74.078994,-83.205997 L -74.078994,-83.835997 L -73.57499,-83.835997 L -73.57499,-83.205997" />
<path d="M -73.57499,-84.435997 L -74.072998,-84.435997 L -74.072998,-87.580001 L -73.57499,-87.580001 L -73.57499,-84.435997" />
<path d="M -70.553994,-84.792999 Q -70.928994,-84.345997 -71.607002,-84.345997 Q -72.267002,-84.345997 -72.645,-84.792999 Q -73.022998,-85.240001 -73.022998,-86.032003 Q -73.022998,-86.824005 -72.647998,-87.271007 Q -72.272998,-87.718009 -71.601006,-87.718009 Q -70.941006,-87.718009 -70.56,-87.274005 Q -70.178994,-86.830001 -70.178994,-86.055997 Q -70.178994,-85.240001 -70.553994,-84.792999 M -72.261006,-85.135001 Q -72.021006,-84.808009 -71.601006,-84.808009 Q -71.17499,-84.808009 -70.937998,-85.138009 Q -70.701006,-85.468009 -70.701006,-86.050001 Q -70.701006,-86.601993 -70.944004,-86.928995 Q -71.187002,-87.255997 -71.601006,-87.255997 Q -72.021006,-87.255997 -72.261006,-86.928995 Q -72.501006,-86.601993 -72.501006,-86.031993 Q -72.501006,-85.461993 -72.261006,-85.135001" />
<path d="M -69.698994,-84.435997 L -69.698994,-87.580001 L -69.19499,-87.580001 L -69.19499,-85.845997 Q -69.19499,-85.365997 -68.960996,-85.075001 Q -68.727002,-84.784005 -68.342998,-84.784005 Q -68.042998,-84.784005 -67.868994,-84.952003 Q -67.69499,-85.120001 -67.69499,-85.401993 L -67.69499,-87.580001 L -67.197002,-87.580001 L -67.197002,-85.204005 Q -67.197002,-84.814005 -67.467002,-84.580001 Q -67.737002,-84.345997 -68.192998,-84.345997 Q -68.540986,-84.345997 -68.783994,-84.490001 Q -69.027002,-84.634005 -69.237002,-84.964005 L -69.237002,-84.435997 L -69.698994,-84.435997" />
<path d="M -15.747002,-31.971997 Q -15.980996,-32.049995 -16.317012,-32.049995 Q -16.670996,-32.049995 -16.925996,-31.956997 Q -17.180996,-31.863999 -17.31,-31.708003 Q -17.439004,-31.552007 -17.496006,-31.390005 Q -17.553008,-31.228003 -17.553008,-31.042007 L -17.553008,-31.012007 L -18.080996,-31.012007 Q -18.075,-31.372007 -17.964004,-31.648003 Q -17.853008,-31.923999 -17.68501,-32.088999 Q -17.517012,-32.253999 -17.283008,-32.358999 Q -17.049004,-32.463999 -16.824004,-32.503003 Q -16.599004,-32.542007 -16.353008,-32.542007 Q -15.963008,-32.542007 -15.654004,-32.452007 Q -15.345,-32.362007 -15.159004,-32.223999 Q -14.973008,-32.085991 -14.853008,-31.902993 Q -14.733008,-31.719995 -14.688008,-31.548999 Q -14.643008,-31.378003 -14.643008,-31.203999 Q -14.643008,-30.802007 -14.886006,-30.513999 Q -15.129004,-30.225991 -15.573008,-30.105991 L -16.670996,-29.812007 Q -17.067012,-29.703999 -17.229004,-29.559995 Q -17.390996,-29.415991 -17.390996,-29.163999 Q -17.390996,-28.833999 -17.124004,-28.629995 Q -16.857012,-28.425991 -16.413008,-28.425991 Q -15.890996,-28.425991 -15.609004,-28.656997 Q -15.327012,-28.888003 -15.320996,-29.313999 L -14.793008,-29.313999 Q -14.799004,-28.672007 -15.219004,-28.315005 Q -15.639004,-27.958003 -16.395,-27.958003 Q -17.115,-27.958003 -17.532002,-28.303003 Q -17.949004,-28.648003 -17.949004,-29.242007 Q -17.949004,-30.039995 -17.090996,-30.262007 L -16.005,-30.549995 Q -15.200996,-30.759995 -15.200996,-31.258003 Q -15.200996,-31.395991 -15.245996,-31.518999 Q -15.290996,-31.642007 -15.402002,-31.768003 Q -15.513008,-31.893999 -15.747002,-31.971997" />
<path d="M -13.467012,-28.029995 L -13.971016,-28.029995 L -13.971016,-28.659995 L -13.467012,-28.659995 L -13.467012,-28.029995" />
<path d="M -13.467012,-29.259995 L -13.96502,-29.259995 L -13.96502,-32.403999 L -13.467012,-32.403999 L -13.467012,-29.259995" />
<path d="M -10.101016,-28.029995 L -10.599023,-28.029995 L -10.599023,-29.655991 Q -10.917012,-29.169995 -11.56502,-29.169995 Q -12.177012,-29.169995 -12.546016,-29.622993 Q -12.91502,-30.075991 -12.91502,-30.825991 Q -12.91502,-31.623999 -12.549023,-32.083003 Q -12.183027,-32.542007 -11.547012,-32.542007 Q -11.223027,-32.542007 -10.986025,-32.413003 Q -10.749023,-32.283999 -10.54502,-31.989995 L -10.54502,-32.403999 L -10.101016,-32.403999 L -10.101016,-28.029995 M -12.141025,-29.973999 Q -11.889023,-29.638003 -11.481016,-29.638003 Q -11.079023,-29.638003 -10.839023,-29.971001 Q -10.599023,-30.303999 -10.599023,-30.868003 Q -10.599023,-31.413999 -10.839023,-31.743999 Q -11.079023,-32.073999 -11.47502,-32.073999 Q -11.889023,-32.073999 -12.141025,-31.741001 Q -12.393027,-31.408003 -12.393027,-30.855991 Q -12.393027,-30.309995 -12.141025,-29.973999" />
<path d="M -9.099023,-30.999995 L -6.783008,-30.999995 Q -6.783008,-29.169995 -8.181016,-29.169995 Q -8.83502,-29.169995 -9.228018,-29.634995 Q -9.621016,-30.099995 -9.621016,-30.873999 Q -9.621016,-31.648003 -9.237012,-32.095005 Q -8.853008,-32.542007 -8.193008,-32.542007 Q -7.653008,-32.542007 -7.299014,-32.253999 Q -6.94502,-31.965991 -6.849023,-31.449995 L -7.353008,-31.449995 Q -7.563008,-32.079995 -8.17502,-32.079995 Q -8.601016,-32.079995 -8.844014,-31.786001 Q -9.087012,-31.492007 -9.099023,-30.999995 M -7.317012,-30.592007 L -9.087012,-30.592007 Q -9.057012,-30.153999 -8.811016,-29.893003 Q -8.56502,-29.632007 -8.187021,-29.632007 Q -7.809023,-29.632007 -7.563018,-29.908003 Q -7.317012,-30.183999 -7.317012,-30.592007" />
<path d="M -4.065,-30.412007 L -1.683008,-30.412007 L -1.683008,-29.919995 L -4.065,-29.919995 L -4.065,-28.522007 L -1.593008,-28.522007 L -1.593008,-28.029995 L -4.623008,-28.029995 L -4.623008,-32.403999 L -1.485,-32.403999 L -1.485,-31.912007 L -4.065,-31.912007 L -4.065,-30.412007" />
<path d="M -0.440996,-28.029995 L -0.945,-28.029995 L -0.945,-32.403999 L -0.440996,-32.403999 L -0.440996,-28.029995" />
<path d="M 0.620996,-30.999995 L 2.937012,-30.999995 Q 2.937012,-29.169995 1.539004,-29.169995 Q 0.885,-29.169995 0.492002,-29.634995 Q 0.099004,-30.099995 0.099004,-30.873999 Q 0.099004,-31.648003 0.483008,-32.095005 Q 0.867012,-32.542007 1.527012,-32.542007 Q 2.067012,-32.542007 2.421006,-32.253999 Q 2.775,-31.965991 2.870996,-31.449995 L 2.367012,-31.449995 Q 2.157012,-32.079995 1.545,-32.079995 Q 1.119004,-32.079995 0.876006,-31.786001 Q 0.633008,-31.492007 0.620996,-30.999995 M 2.403008,-30.592007 L 0.633008,-30.592007 Q 0.663008,-30.153999 0.909004,-29.893003 Q 1.155,-29.632007 1.532998,-29.632007 Q 1.910996,-29.632007 2.157002,-29.908003 Q 2.403008,-30.183999 2.403008,-30.592007" />
<path d="M 4.767012,-32.403999 L 5.973008,-29.259995 L 5.409004,-29.259995 L 4.521016,-31.809995 L 3.681016,-29.259995 L 3.117012,-29.259995 L 4.221016,-32.403999 L 4.767012,-32.403999" />
<path d="M 6.291016,-30.189995 L 6.795,-30.189995 Q 6.825,-29.902007 7.005,-29.767007 Q 7.185,-29.632007 7.533008,-29.632007 Q 7.869004,-29.632007 8.052002,-29.752007 Q 8.235,-29.872007 8.235,-30.099995 L 8.235,-30.232007 Q 8.235,-30.388003 8.121006,-30.466001 Q 8.007012,-30.543999 7.713008,-30.579995 Q 7.497012,-30.609995 7.413008,-30.621997 Q 7.329004,-30.633999 7.149004,-30.663999 Q 6.969004,-30.693999 6.897002,-30.714995 Q 6.825,-30.735991 6.69,-30.780991 Q 6.555,-30.825991 6.498008,-30.873999 Q 6.441016,-30.922007 6.357012,-30.997007 Q 6.273008,-31.072007 6.24001,-31.158999 Q 6.207012,-31.245991 6.18001,-31.359995 Q 6.153008,-31.473999 6.153008,-31.612007 Q 6.153008,-32.038003 6.432012,-32.290005 Q 6.711016,-32.542007 7.185,-32.542007 Q 7.749004,-32.542007 8.253008,-32.079995 Q 8.283008,-32.319995 8.406006,-32.431001 Q 8.529004,-32.542007 8.769004,-32.542007 Q 8.901016,-32.542007 9.111016,-32.488003 L 9.111016,-32.109995 Q 9.057012,-32.122007 9.003008,-32.122007 Q 8.733008,-32.122007 8.733008,-31.875991 L 8.733008,-30.028003 Q 8.733008,-29.608003 8.433008,-29.388999 Q 8.133008,-29.169995 7.551016,-29.169995 Q 6.327012,-29.169995 6.291016,-30.189995 M 7.971006,-31.884995 Q 7.707012,-32.103999 7.293008,-32.103999 Q 6.999004,-32.103999 6.837002,-31.971997 Q 6.675,-31.839995 6.675,-31.599995 Q 6.675,-31.359995 6.855,-31.224995 Q 7.035,-31.089995 7.257002,-31.053999 Q 7.479004,-31.018003 7.782002,-30.970005 Q 8.085,-30.922007 8.235,-30.849995 L 8.235,-31.413999 Q 8.235,-31.665991 7.971006,-31.884995" />
<path d="M 10.719023,-29.259995 L 10.203027,-29.259995 L 10.203027,-28.395991 L 9.70502,-28.395991 L 9.70502,-29.259995 L 9.279023,-29.259995 L 9.279023,-29.668003 L 9.70502,-29.668003 L 9.70502,-32.043999 Q 9.70502,-32.283999 9.861025,-32.413003 Q 10.017031,-32.542007 10.311016,-32.542007 Q 10.48502,-32.542007 10.719023,-32.499995 L 10.719023,-32.079995 Q 10.629023,-32.103999 10.479023,-32.103999 Q 10.317031,-32.103999 10.260029,-32.046997 Q 10.203027,-31.989995 10.203027,-31.822007 L 10.203027,-29.668003 L 10.719023,-29.668003 L 10.719023,-29.259995" />
<path d="M 11.691016,-28.029995 L 11.187012,-28.029995 L 11.187012,-28.659995 L 11.691016,-28.659995 L 11.691016,-28.029995" />
<path d="M 11.691016,-29.259995 L 11.193008,-29.259995 L 11.193008,-32.403999 L 11.691016,-32.403999 L 11.691016,-29.259995" />
<path d="M 14.712012,-29.616997 Q 14.337012,-29.169995 13.659004,-29.169995 Q 12.999004,-29.169995 12.621006,-29.616997 Q 12.243008,-30.063999 12.243008,-30.856001 Q 12.243008,-31.648003 12.618008,-32.095005 Q 12.993008,-32.542007 13.665,-32.542007 Q 14.325,-32.542007 14.706006,-32.098003 Q 15.087012,-31.653999 15.087012,-30.879995 Q 15.087012,-30.063999 14.712012,-29.616997 M 13.005,-29.958999 Q 13.245,-29.632007 13.665,-29.632007 Q 14.091016,-29.632007 14.328008,-29.962007 Q 14.565,-30.292007 14.565,-30.873999 Q 14.565,-31.425991 14.322002,-31.752993 Q 14.079004,-32.079995 13.665,-32.079995 Q 13.245,-32.079995 13.005,-31.752993 Q 12.765,-31.425991 12.765,-30.855991 Q 12.765,-30.285991 13.005,-29.958999" />
<path d="M 15.567012,-29.259995 L 15.567012,-32.403999 L 16.071016,-32.403999 L 16.071016,-30.669995 Q 16.071016,-30.189995 16.30501,-29.898999 Q 16.539004,-29.608003 16.923008,-29.608003 Q 17.223008,-29.608003 17.397012,-29.776001 Q 17.571016,-29.943999 17.571016,-30.225991 L 17.571016,-32.403999 L 18.069004,-32.403999 L 18.069004,-30.028003 Q 18.069004,-29.638003 17.799004,-29.403999 Q 17.529004,-29.169995 17.073008,-29.169995 Q 16.72502,-29.169995 16.482012,-29.313999 Q 16.239004,-29.458003 16.029004,-29.788003 L 16.029004,-29.259995 L 15.567012,-29.259995" />
</g>
<g fill="none" stroke="rgb(99,99,99)" stroke-width="0.09" id="Hidden" stroke-dasharray="0.002286 0.271143">
<line x1="88.539518" y1="76.052448" x2="88.539518" y2="73.11306" />
<line x1="88.539518" y1="73.11306" x2="92.532904" y2="70.807478" />
<line x1="92.532904" y1="70.807478" x2="92.532904" y2="67.010769" />
<line x1="109.577713" y1="57.169944" x2="92.532904" y2="67.010769" />
<line x1="109.577713" y1="60.966653" x2="109.577713" y2="57.169944" />
<line x1="109.577713" y1="60.966653" x2="113.571098" y2="58.661071" />
<line x1="113.571098" y1="61.600459" x2="113.571098" y2="58.661071" />
<path d="M 84.747658,73.844845 A 1.9124999999999999,1.1041823898251593 0.0 0,1 87.542592,73.900687" />
<path d="M 84.657408,73.900687 A 1.9124999999999999,1.1041823898251593 0.0 0,1 84.747658,73.844845" />
<path d="M 100.3875,60.192002 A 14.287500000000001,8.24889197104678 0.0 0,1 71.8125,60.192002" />
<line x1="83.660482" y1="76.052448" x2="83.660482" y2="73.11306" />
<path d="M 88.539518,73.11306 A 3.4499999999999997,1.991858428704209 0.0 0,1 83.660482,73.11306" />
<path d="M 92.532904,70.807478 A 1.875,1.0825317547305484 0.0 0,1 89.881254,70.807478" />
<line x1="88.75165" y1="70.155302" x2="89.881254" y2="70.807478" />
<path d="M 83.44835,70.155302 A 3.75,2.165063509461097 0.0 0,1 88.75165,70.155302" />
<line x1="83.44835" y1="70.155302" x2="82.318746" y2="70.807478" />
<path d="M 82.318746,70.807478 A 1.875,1.0825317547305482 3.895368034302951e-15 0,1 79.667096,70.807478" />
<line x1="83.660482" y1="73.11306" x2="79.667096" y2="70.807478" />
<path d="M 84.747658,70.905458 A 1.9124999999999999,1.1041823898251593 0.0 0,1 88.0125,71.686233" />
<path d="M 88.0125,71.686233 A 1.9125000000000012,1.10418238982516 0.0 0,1 84.1875,71.686233" />
<path d="M 84.1875,71.686233 A 1.9124999999999999,1.1041823898251593 0.0 0,1 84.747658,70.905458" />
<path d="M 92.532904,67.010769 A 1.875,1.0825317547305484 0.0 0,1 89.881254,67.010769" />
<path d="M 62.073112,50.507332 A 1.875,1.0825317547305484 180.0 0,0 62.622287,51.272797" />
<path d="M 84.600538,41.006736 A 3.75,2.165063509461097 180.0 0,0 87.599462,41.006736" />
<line x1="62.622287" y1="51.272797" x2="63.75189" y2="51.924974" />
<path d="M 63.75189,54.986836 A 3.75,2.165063509461097 180.0 0,0 64.85024,53.455905" />
<path d="M 64.85024,53.455905 A 3.75,2.165063509461097 180.0 0,0 63.75189,51.924974" />
<line x1="62.622287" y1="55.639013" x2="63.75189" y2="54.986836" />
<path d="M 62.622287,57.169944 A 1.875,1.0825317547305484 0.0 0,1 62.073112,56.404478" />
<path d="M 62.073112,56.404478 A 1.875,1.0825317547305484 0.0 0,1 62.622287,55.639013" />
<path d="M 110.126888,50.507332 A 1.875,1.0825317547305484 0.0 0,1 109.577713,51.272797" />
<line x1="79.667096" y1="67.010769" x2="62.622287" y2="57.169944" />
<line x1="109.577713" y1="51.272797" x2="108.44811" y2="51.924974" />
<path d="M 79.667096,67.010769 A 1.875,1.0825317547305484 180.0 0,0 82.318746,67.010769" />
<path d="M 108.44811,51.924974 A 3.75,2.165063509461097 180.0 0,0 107.34976,53.455905" />
<path d="M 107.34976,53.455905 A 3.75,2.165063509461097 180.0 0,0 108.44811,54.986836" />
<line x1="82.318746" y1="67.010769" x2="83.44835" y2="66.358592" />
<line x1="109.577713" y1="55.639013" x2="108.44811" y2="54.986836" />
<path d="M 88.75165,66.358592 A 3.75,2.165063509461097 180.0 0,0 83.44835,66.358592" />
<path d="M 109.577713,57.169944 A 1.875,1.0825317547305484 180.0 0,0 110.126888,56.404478" />
<path d="M 110.126888,56.404478 A 1.875,1.0825317547305484 180.0 0,0 109.577713,55.639013" />
<line x1="89.881254" y1="67.010769" x2="88.75165" y2="66.358592" />
<path d="M 79.720129,39.93166 A 1.7999999999999998,1.0392304845413265 0.0 0,1 82.265713,39.93166" />
<line x1="82.265713" y1="39.93166" x2="83.395317" y2="40.583837" />
<path d="M 88.804683,40.583837 A 3.8249999999999997,2.2083647796503185 0.0 0,1 83.395317,40.583837" />
<line x1="89.934287" y1="39.93166" x2="88.804683" y2="40.583837" />
<path d="M 89.934287,39.93166 A 1.7999999999999998,1.0392304845413263 3.895368034302951e-15 0,1 92.479871,39.93166" />
<line x1="92.479871" y1="39.93166" x2="109.52468" y2="49.772485" />
<path d="M 109.52468,49.772485 A 1.7999999999999998,1.0392304845413263 -3.895368034302951e-15 0,1 110.051888,50.507332" />
<path d="M 110.051888,50.507332 A 1.7999999999999998,1.0392304845413263 -3.895368034302951e-15 0,1 109.52468,51.242179" />
<line x1="109.52468" y1="51.242179" x2="108.395077" y2="51.894356" />
<path d="M 108.395077,55.017455 A 3.8249999999999997,2.2083647796503185 0.0 0,1 107.27476,53.455905" />
<path d="M 107.27476,53.455905 A 3.8249999999999997,2.2083647796503185 0.0 0,1 108.395077,51.894356" />
<line x1="109.52468" y1="55.669632" x2="108.395077" y2="55.017455" />
<path d="M 109.52468,55.669632 A 1.7999999999999998,1.0392304845413265 0.0 0,1 110.051888,56.404478" />
<path d="M 110.051888,56.404478 A 1.7999999999999998,1.0392304845413265 0.0 0,1 109.52468,57.139325" />
<line x1="109.52468" y1="57.139325" x2="92.479871" y2="66.98015" />
<path d="M 92.479871,66.98015 A 1.7999999999999998,1.0392304845413265 0.0 0,1 89.934287,66.98015" />
<line x1="89.934287" y1="66.98015" x2="88.804683" y2="66.327974" />
<path d="M 83.395317,66.327974 A 3.8249999999999997,2.2083647796503185 0.0 0,1 88.804683,66.327974" />
<line x1="82.265713" y1="66.98015" x2="83.395317" y2="66.327974" />
<path d="M 82.265713,66.98015 A 1.7999999999999998,1.0392304845413263 3.895368034302951e-15 0,1 79.720129,66.98015" />
<line x1="79.720129" y1="66.98015" x2="62.67532" y2="57.139325" />
<path d="M 62.67532,57.139325 A 1.7999999999999998,1.0392304845413263 -3.895368034302951e-15 0,1 62.148112,56.404478" />
<path d="M 62.148112,56.404478 A 1.7999999999999998,1.0392304845413263 -3.895368034302951e-15 0,1 62.67532,55.669632" />
<line x1="62.67532" y1="55.669632" x2="63.804923" y2="55.017455" />
<path d="M 63.804923,51.894356 A 3.8249999999999997,2.2083647796503185 0.0 0,1 64.92524,53.455905" />
<path d="M 64.92524,53.455905 A 3.8249999999999997,2.2083647796503185 0.0 0,1 63.804923,55.017455" />
<line x1="62.67532" y1="51.242179" x2="63.804923" y2="51.894356" />
<path d="M 62.67532,51.242179 A 1.7999999999999998,1.0392304845413265 0.0 0,1 62.148112,50.507332" />
<path d="M 62.148112,50.507332 A 1.7999999999999998,1.0392304845413265 0.0 0,1 62.67532,49.772485" />
<line x1="62.67532" y1="49.772485" x2="79.720129" y2="39.93166" />
<path d="M 109.577713,59.435722 A 1.875,1.0825317547305484 0.0 0,1 110.126888,60.201188" />
<path d="M 110.126888,60.201188 A 1.875,1.0825317547305484 0.0 0,1 109.577713,60.966653" />
<path d="M 114.58158,57.252614 A 3.4499999999999997,1.991858428704209 0.0 0,1 113.571098,58.661071" />
<path d="M 109.577713,53.538575 A 1.875,1.0825317547305482 -3.895368034302951e-15 0,1 110.126888,54.304041" />
<path d="M 110.126888,54.304041 A 1.875,1.0825317547305482 -3.895368034302951e-15 0,1 109.577713,55.069507" />
<line x1="108.44811" y1="55.721683" x2="109.577713" y2="55.069507" />
<path d="M 108.44811,58.783545 A 3.75,2.165063509461097 0.0 0,1 107.34976,57.252614" />
<path d="M 107.34976,57.252614 A 3.75,2.165063509461097 0.0 0,1 108.44811,55.721683" />
<line x1="108.44811" y1="58.783545" x2="109.577713" y2="59.435722" />
<path d="M 109.747419,56.471839 A 1.9124999999999999,1.1041823898251593 0.0 0,1 113.01226,57.252614" />
<path d="M 113.01226,57.252614 A 1.9125000000000012,1.10418238982516 0.0 0,1 109.18726,57.252614" />
<path d="M 109.18726,57.252614 A 1.9124999999999999,1.1041823898251593 0.0 0,1 109.747419,56.471839" />
<line x1="79.667096" y1="70.807478" x2="79.667096" y2="67.010769" />
<line x1="62.622287" y1="60.966653" x2="62.622287" y2="57.169944" />
<line x1="62.622287" y1="60.966653" x2="58.628902" y2="58.661071" />
<line x1="58.628902" y1="61.600459" x2="58.628902" y2="58.661071" />
<path d="M 58.628902,58.661071 A 3.4499999999999997,1.991858428704209 0.0 0,1 57.61842,57.252614" />
<path d="M 59.747898,56.471839 A 1.9124999999999999,1.1041823898251593 0.0 0,1 63.01274,57.252614" />
<path d="M 63.01274,57.252614 A 1.9125000000000012,1.10418238982516 0.0 0,1 59.18774,57.252614" />
<path d="M 59.18774,57.252614 A 1.9124999999999999,1.1041823898251593 0.0 0,1 59.747898,56.471839" />
<path d="M 88.48125,69.305315 A 14.287500000000001,8.24889197104678 0.0 0,1 83.71875,69.305315" />
<path d="M 84.747658,42.038221 A 1.9124999999999999,1.1041823898251593 0.0 0,1 88.0125,42.818996" />
<path d="M 88.0125,42.818996 A 1.9125000000000012,1.10418238982516 0.0 0,1 84.1875,42.818996" />
<path d="M 84.1875,42.818996 A 1.9124999999999999,1.1041823898251593 0.0 0,1 84.747658,42.038221" />
<path d="M 79.667096,43.69775 A 1.875,1.0825317547305484 0.0 0,1 82.318746,43.69775" />
<line x1="83.44835" y1="44.349927" x2="82.318746" y2="43.69775" />
<path d="M 62.622287,55.069507 A 1.875,1.0825317547305484 0.0 0,1 62.073112,54.304041" />
<path d="M 62.073112,54.304041 A 1.875,1.0825317547305484 0.0 0,1 62.622287,53.538575" />
<path d="M 88.75165,44.349927 A 3.75,2.165063509461097 0.0 0,1 83.44835,44.349927" />
<line x1="63.75189" y1="55.721683" x2="62.622287" y2="55.069507" />
<line x1="88.75165" y1="44.349927" x2="89.881254" y2="43.69775" />
<path d="M 63.75189,55.721683 A 3.75,2.165063509461097 0.0 0,1 64.85024,57.252614" />
<path d="M 64.85024,57.252614 A 3.75,2.165063509461097 0.0 0,1 63.75189,58.783545" />
<path d="M 89.881254,43.69775 A 1.875,1.0825317547305482 3.895368034302951e-15 0,1 92.532904,43.69775" />
<line x1="63.75189" y1="58.783545" x2="62.622287" y2="59.435722" />
<path d="M 62.622287,60.966653 A 1.875,1.0825317547305482 -3.895368034302951e-15 0,1 62.073112,60.201188" />
<path d="M 62.073112,60.201188 A 1.875,1.0825317547305482 -3.895368034302951e-15 0,1 62.622287,59.435722" />
<path d="M 110.051888,29.686669 A 1.7999999999999998,1.0392304845413263 -3.895368034302951e-15 0,1 109.52468,30.421516" />
<line x1="109.52468" y1="30.421516" x2="108.395077" y2="31.073693" />
<path d="M 108.395077,31.073693 A 3.8249999999999997,2.2083647796503185 180.0 0,0 107.27476,32.635242" />
<path d="M 107.27476,32.635242 A 3.8249999999999997,2.2083647796503185 180.0 0,0 108.395077,34.196792" />
<line x1="109.52468" y1="34.848969" x2="108.395077" y2="34.196792" />
<path d="M 109.52468,36.318663 A 1.7999999999999998,1.0392304845413265 180.0 0,0 110.051888,35.583816" />
<path d="M 110.051888,35.583816 A 1.7999999999999998,1.0392304845413265 180.0 0,0 109.52468,34.848969" />
<line x1="109.52468" y1="36.318663" x2="92.479871" y2="46.159488" />
<path d="M 92.479871,46.159488 A 1.7999999999999998,1.0392304845413265 0.0 0,1 89.934287,46.159488" />
<line x1="89.934287" y1="46.159488" x2="88.804683" y2="45.507311" />
<path d="M 88.804683,45.507311 A 3.8249999999999997,2.2083647796503185 180.0 0,0 83.395317,45.507311" />
<line x1="82.265713" y1="46.159488" x2="83.395317" y2="45.507311" />
<path d="M 79.720129,46.159488 A 1.7999999999999998,1.0392304845413263 -180.0 0,0 82.265713,46.159488" />
<line x1="79.720129" y1="46.159488" x2="62.67532" y2="36.318663" />
<path d="M 62.67532,36.318663 A 1.7999999999999998,1.0392304845413263 -3.895368034302951e-15 0,1 62.148112,35.583816" />
<path d="M 62.148112,35.583816 A 1.7999999999999998,1.0392304845413263 -3.895368034302951e-15 0,1 62.67532,34.848969" />
<line x1="62.67532" y1="34.848969" x2="63.804923" y2="34.196792" />
<path d="M 63.804923,34.196792 A 3.8249999999999997,2.2083647796503185 180.0 0,0 64.92524,32.635242" />
<path d="M 64.92524,32.635242 A 3.8249999999999997,2.2083647796503185 180.0 0,0 63.804923,31.073693" />
<line x1="62.67532" y1="30.421516" x2="63.804923" y2="31.073693" />
<path d="M 62.148112,29.686669 A 1.7999999999999998,1.0392304845413265 180.0 0,0 62.67532,30.421516" />
<path d="M 62.148112,29.989778 A 1.875,1.0825317547305484 180.0 0,0 62.622287,30.452135" />
<line x1="62.622287" y1="30.452135" x2="63.75189" y2="31.104311" />
<path d="M 63.75189,34.166173 A 3.75,2.165063509461097 180.0 0,0 64.85024,32.635242" />
<path d="M 64.85024,32.635242 A 3.75,2.165063509461097 180.0 0,0 63.75189,31.104311" />
<line x1="62.622287" y1="34.81835" x2="63.75189" y2="34.166173" />
<path d="M 62.622287,36.349281 A 1.875,1.0825317547305484 0.0 0,1 62.148112,35.886925" />
<path d="M 62.148112,35.280707 A 1.875,1.0825317547305484 0.0 0,1 62.622287,34.81835" />
<path d="M 110.051888,29.989778 A 1.875,1.0825317547305484 0.0 0,1 109.577713,30.452135" />
<line x1="79.667096" y1="46.190106" x2="62.622287" y2="36.349281" />
<line x1="109.577713" y1="30.452135" x2="108.44811" y2="31.104311" />
<path d="M 79.667096,46.190106 A 1.875,1.0825317547305484 180.0 0,0 82.318746,46.190106" />
<path d="M 108.44811,31.104311 A 3.75,2.165063509461097 180.0 0,0 107.34976,32.635242" />
<path d="M 107.34976,32.635242 A 3.75,2.165063509461097 180.0 0,0 108.44811,34.166173" />
<line x1="82.318746" y1="46.190106" x2="83.44835" y2="45.53793" />
<line x1="109.577713" y1="34.81835" x2="108.44811" y2="34.166173" />
<path d="M 88.75165,45.53793 A 3.75,2.165063509461097 180.0 0,0 83.44835,45.53793" />
<path d="M 109.577713,36.349281 A 1.875,1.0825317547305484 180.0 0,0 110.051888,35.886925" />
<path d="M 110.051888,35.280707 A 1.875,1.0825317547305484 180.0 0,0 109.577713,34.81835" />
<line x1="89.881254" y1="46.190106" x2="88.75165" y2="45.53793" />
<line x1="109.577713" y1="36.349281" x2="92.532904" y2="46.190106" />
<path d="M 92.532904,46.190106 A 1.875,1.0825317547305484 0.0 0,1 89.881254,46.190106" />
<path d="M 62.073112,22.950572 A 1.875,1.0825317547305484 180.0 0,0 62.622287,23.716038" />
<line x1="62.622287" y1="23.716038" x2="63.75189" y2="24.368215" />
<path d="M 63.75189,27.430077 A 3.75,2.165063509461097 180.0 0,0 64.85024,25.899146" />
<path d="M 64.85024,25.899146 A 3.75,2.165063509461097 180.0 0,0 63.75189,24.368215" />
<line x1="62.622287" y1="28.082253" x2="63.75189" y2="27.430077" />
<path d="M 62.622287,29.613184 A 1.875,1.0825317547305484 0.0 0,1 62.073112,28.847719" />
<path d="M 62.073112,28.847719 A 1.875,1.0825317547305484 0.0 0,1 62.622287,28.082253" />
<path d="M 110.126888,22.950572 A 1.875,1.0825317547305484 0.0 0,1 109.577713,23.716038" />
<line x1="79.667096" y1="39.454009" x2="62.622287" y2="29.613184" />
<line x1="109.577713" y1="23.716038" x2="108.44811" y2="24.368215" />
<path d="M 79.667096,39.454009 A 1.875,1.0825317547305484 180.0 0,0 82.318746,39.454009" />
<path d="M 108.44811,24.368215 A 3.75,2.165063509461097 180.0 0,0 107.34976,25.899146" />
<path d="M 107.34976,25.899146 A 3.75,2.165063509461097 180.0 0,0 108.44811,27.430077" />
<line x1="82.318746" y1="39.454009" x2="83.44835" y2="38.801833" />
<line x1="109.577713" y1="28.082253" x2="108.44811" y2="27.430077" />
<path d="M 88.75165,38.801833 A 3.75,2.165063509461097 180.0 0,0 83.44835,38.801833" />
<path d="M 109.577713,29.613184 A 1.875,1.0825317547305484 180.0 0,0 110.126888,28.847719" />
<path d="M 110.126888,28.847719 A 1.875,1.0825317547305484 180.0 0,0 109.577713,28.082253" />
<line x1="89.881254" y1="39.454009" x2="88.75165" y2="38.801833" />
<line x1="109.577713" y1="29.613184" x2="92.532904" y2="39.454009" />
<path d="M 92.532904,39.454009 A 1.875,1.0825317547305484 0.0 0,1 89.881254,39.454009" />
<path d="M 84.416202,59.219861 A 2.3812499999999996,1.3748153285077964 0.0 0,1 88.48125,60.192002" />
<path d="M 88.48125,60.192002 A 2.3812500000000014,1.3748153285077973 0.0 0,1 83.71875,60.192002" />
<path d="M 83.71875,60.192002 A 2.3812499999999996,1.3748153285077964 0.0 0,1 84.416202,59.219861" />
<line x1="110.126888" y1="56.404478" x2="110.126888" y2="60.201188" />
<line x1="88.0125" y1="71.686233" x2="88.0125" y2="74.62562" />
<line x1="84.1875" y1="71.686233" x2="84.1875" y2="74.62562" />
<line x1="63.01274" y1="57.252614" x2="63.01274" y2="60.192002" />
<line x1="59.18774" y1="57.252614" x2="59.18774" y2="60.192002" />
<line x1="113.01226" y1="57.252614" x2="113.01226" y2="60.192002" />
<line x1="109.18726" y1="57.252614" x2="109.18726" y2="60.192002" />
<line x1="88.0125" y1="42.818996" x2="88.0125" y2="45.758384" />
<line x1="84.1875" y1="42.818996" x2="84.1875" y2="45.758384" />
<line x1="62.073112" y1="53.855642" x2="62.073112" y2="54.304041" />
<line x1="64.85024" y1="53.455905" x2="64.85024" y2="57.252614" />
<line x1="62.073112" y1="56.404478" x2="62.073112" y2="60.201188" />
<line x1="110.126888" y1="53.855642" x2="110.126888" y2="54.304041" />
<line x1="107.34976" y1="53.455905" x2="107.34976" y2="57.252614" />
<line x1="110.051888" y1="50.204223" x2="110.051888" y2="50.507332" />
<line x1="107.27476" y1="32.635242" x2="107.27476" y2="53.455905" />
<line x1="110.051888" y1="35.583816" x2="110.051888" y2="56.404478" />
<line x1="62.148112" y1="35.583816" x2="62.148112" y2="56.404478" />
<line x1="64.92524" y1="32.635242" x2="64.92524" y2="53.455905" />
<line x1="62.148112" y1="50.204223" x2="62.148112" y2="50.507332" />
<line x1="64.85024" y1="25.899146" x2="64.85024" y2="32.635242" />
<line x1="62.073112" y1="28.847719" x2="62.073112" y2="29.686669" />
<line x1="107.34976" y1="25.899146" x2="107.34976" y2="32.635242" />
<line x1="110.126888" y1="28.847719" x2="110.126888" y2="29.686669" />
<line x1="89.881254" y1="70.807478" x2="89.881254" y2="67.010769" />
<line x1="109.577713" y1="59.435722" x2="109.577713" y2="55.639013" />
<line x1="88.75165" y1="70.155302" x2="88.75165" y2="66.358592" />
<line x1="83.44835" y1="70.155302" x2="83.44835" y2="66.358592" />
<line x1="82.318746" y1="70.807478" x2="82.318746" y2="67.010769" />
<line x1="82.318746" y1="43.69775" x2="82.318746" y2="42.166819" />
<line x1="83.44835" y1="44.349927" x2="83.44835" y2="41.514643" />
<line x1="62.622287" y1="55.069507" x2="62.622287" y2="51.272797" />
<line x1="88.75165" y1="44.349927" x2="88.75165" y2="41.514643" />
<line x1="63.75189" y1="55.721683" x2="63.75189" y2="51.924974" />
<line x1="89.881254" y1="43.69775" x2="89.881254" y2="42.166819" />
<line x1="63.75189" y1="58.783545" x2="63.75189" y2="54.986836" />
<line x1="62.622287" y1="59.435722" x2="62.622287" y2="55.639013" />
<line x1="109.577713" y1="55.069507" x2="109.577713" y2="53.538575" />
<line x1="108.44811" y1="55.721683" x2="108.44811" y2="51.924974" />
<line x1="108.44811" y1="58.783545" x2="108.44811" y2="54.986836" />
<line x1="82.265713" y1="39.93166" x2="82.265713" y2="39.871601" />
<line x1="79.720129" y1="39.93166" x2="79.720129" y2="39.871601" />
<line x1="83.395317" y1="40.583837" x2="83.395317" y2="40.522599" />
<line x1="88.804683" y1="40.583837" x2="88.804683" y2="40.522599" />
<line x1="89.934287" y1="39.93166" x2="89.934287" y2="39.871601" />
<line x1="92.479871" y1="39.93166" x2="92.479871" y2="39.871601" />
<line x1="109.52468" y1="49.772485" x2="109.52468" y2="49.711248" />
<line x1="109.52468" y1="51.242179" x2="109.52468" y2="30.421516" />
<line x1="108.395077" y1="51.894356" x2="108.395077" y2="31.073693" />
<line x1="108.395077" y1="55.017455" x2="108.395077" y2="34.196792" />
<line x1="109.52468" y1="55.669632" x2="109.52468" y2="34.848969" />
<line x1="109.52468" y1="57.139325" x2="109.52468" y2="36.318663" />
<line x1="92.479871" y1="66.98015" x2="92.479871" y2="46.159488" />
<line x1="89.934287" y1="66.98015" x2="89.934287" y2="46.159488" />
<line x1="88.804683" y1="66.327974" x2="88.804683" y2="45.507311" />
<line x1="83.395317" y1="66.327974" x2="83.395317" y2="45.507311" />
<line x1="82.265713" y1="66.98015" x2="82.265713" y2="46.159488" />
<line x1="79.720129" y1="66.98015" x2="79.720129" y2="46.159488" />
<line x1="62.67532" y1="57.139325" x2="62.67532" y2="36.318663" />
<line x1="62.67532" y1="55.669632" x2="62.67532" y2="34.848969" />
<line x1="63.804923" y1="55.017455" x2="63.804923" y2="34.196792" />
<line x1="63.804923" y1="51.894356" x2="63.804923" y2="31.073693" />
<line x1="62.67532" y1="51.242179" x2="62.67532" y2="30.421516" />
<line x1="62.67532" y1="49.772485" x2="62.67532" y2="49.711248" />
<line x1="62.622287" y1="30.452135" x2="62.622287" y2="23.716038" />
<line x1="63.75189" y1="31.104311" x2="63.75189" y2="24.368215" />
<line x1="63.75189" y1="34.166173" x2="63.75189" y2="27.430077" />
<line x1="62.622287" y1="34.81835" x2="62.622287" y2="28.082253" />
<line x1="62.622287" y1="36.349281" x2="62.622287" y2="29.613184" />
<line x1="109.577713" y1="30.452135" x2="109.577713" y2="23.716038" />
<line x1="79.667096" y1="46.190106" x2="79.667096" y2="39.454009" />
<line x1="108.44811" y1="31.104311" x2="108.44811" y2="24.368215" />
<line x1="82.318746" y1="46.190106" x2="82.318746" y2="39.454009" />
<line x1="108.44811" y1="34.166173" x2="108.44811" y2="27.430077" />
<line x1="83.44835" y1="45.53793" x2="83.44835" y2="38.801833" />
<line x1="109.577713" y1="34.81835" x2="109.577713" y2="28.082253" />
<line x1="88.75165" y1="45.53793" x2="88.75165" y2="38.801833" />
<line x1="109.577713" y1="36.349281" x2="109.577713" y2="29.613184" />
<line x1="89.881254" y1="46.190106" x2="89.881254" y2="39.454009" />
<line x1="92.532904" y1="46.190106" x2="92.532904" y2="39.454009" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 168 KiB

View file

@ -15,6 +15,8 @@ Cheat Sheet
.. grid-item-card:: 1D - BuildLine .. grid-item-card:: 1D - BuildLine
| :class:`~objects_curve.ArcArcTangentArc`
| :class:`~objects_curve.ArcArcTangentLine`
| :class:`~objects_curve.Bezier` | :class:`~objects_curve.Bezier`
| :class:`~objects_curve.CenterArc` | :class:`~objects_curve.CenterArc`
| :class:`~objects_curve.DoubleTangentArc` | :class:`~objects_curve.DoubleTangentArc`
@ -24,6 +26,8 @@ Cheat Sheet
| :class:`~objects_curve.IntersectingLine` | :class:`~objects_curve.IntersectingLine`
| :class:`~objects_curve.JernArc` | :class:`~objects_curve.JernArc`
| :class:`~objects_curve.Line` | :class:`~objects_curve.Line`
| :class:`~objects_curve.PointArcTangentArc`
| :class:`~objects_curve.PointArcTangentLine`
| :class:`~objects_curve.PolarLine` | :class:`~objects_curve.PolarLine`
| :class:`~objects_curve.Polyline` | :class:`~objects_curve.Polyline`
| :class:`~objects_curve.RadiusArc` | :class:`~objects_curve.RadiusArc`
@ -99,6 +103,7 @@ Cheat Sheet
| :func:`~operations_generic.add` | :func:`~operations_generic.add`
| :func:`~operations_generic.chamfer` | :func:`~operations_generic.chamfer`
| :func:`~operations_part.draft`
| :func:`~operations_part.extrude` | :func:`~operations_part.extrude`
| :func:`~operations_generic.fillet` | :func:`~operations_generic.fillet`
| :func:`~operations_part.loft` | :func:`~operations_part.loft`
@ -228,7 +233,7 @@ Cheat Sheet
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +----------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+
| :class:`~build_enums.Extrinsic` | XYZ, XZY, YZX, YXZ, ZXY, ZYX, XYX, XZX, YZY, YXY, ZXZ, ZYZ | | :class:`~build_enums.Extrinsic` | XYZ, XZY, YZX, YXZ, ZXY, ZYX, XYX, XZX, YZY, YXY, ZXZ, ZYZ |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +----------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+
| :class:`~build_enums.FontStyle` | REGULAR, BOLD, BOLDITALIC, ITALIC | | :class:`~build_enums.FontStyle` | REGULAR, BOLD, BOLDITALIC, ITALIC |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +----------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+
| :class:`~build_enums.FrameMethod` | CORRECTED, FRENET | | :class:`~build_enums.FrameMethod` | CORRECTED, FRENET |
+----------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +----------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------+

View file

@ -24,11 +24,21 @@ Most of the examples show the builder and algebra modes.
:link: examples-benchy :link: examples-benchy
:link-type: ref :link-type: ref
.. grid-item-card:: Bicycle Tire |Builder|
:img-top: assets/examples/bicycle_tire.png
:link: examples-bicycle_tire
:link-type: ref
.. grid-item-card:: Canadian Flag Blowing in The Wind |Builder| |Algebra| .. grid-item-card:: Canadian Flag Blowing in The Wind |Builder| |Algebra|
:img-top: assets/examples/example_canadian_flag_01.png :img-top: assets/examples/example_canadian_flag_01.png
:link: examples-canadian_flag :link: examples-canadian_flag
:link-type: ref :link-type: ref
.. grid-item-card:: Cast Bearing Unit |Builder|
:img-top: assets/examples/cast_bearing_unit.png
:link: examples-cast_bearing_unit
:link-type: ref
.. grid-item-card:: Circuit Board With Holes |Builder| |Algebra| .. grid-item-card:: Circuit Board With Holes |Builder| |Algebra|
:img-top: assets/examples/thumbnail_circuit_board_01.png :img-top: assets/examples/thumbnail_circuit_board_01.png
:link: examples-circuit_board :link: examples-circuit_board
@ -39,6 +49,11 @@ Most of the examples show the builder and algebra modes.
:link: clock_face :link: clock_face
:link-type: ref :link-type: ref
.. grid-item-card:: Fast Grid Holes |Algebra|
:img-top: assets/examples/fast_grid_holes.png
:link: fast_grid_holes
:link-type: ref
.. grid-item-card:: Handle |Builder| |Algebra| .. grid-item-card:: Handle |Builder| |Algebra|
:img-top: assets/examples/handle.png :img-top: assets/examples/handle.png
:link: handle :link: handle
@ -154,6 +169,24 @@ modify it by replacing chimney with a BREP version.
.. ---------------------------------------------------------------------------------------------- .. ----------------------------------------------------------------------------------------------
.. _examples-bicycle_tire:
Bicycle Tire
--------------------------------
.. image:: assets/examples/bicycle_tire.png
:align: center
This example demonstrates how to model a realistic bicycle tire with a
patterned tread using build123d. The key concept showcased here is the
use of wrap_faces to project 2D planar geometry onto a curved 3D
surface.
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/bicycle_tire.py
:start-after: [Code]
:end-before: [End]
.. _examples-build123d_logo: .. _examples-build123d_logo:
Former build123d Logo Former build123d Logo
@ -181,6 +214,23 @@ The builder mode example also generates the SVG file `logo.svg`.
:end-before: [End] :end-before: [End]
.. _examples-cast_bearing_unit:
Cast Bearing Unit
-----------------
.. image:: assets/examples/cast_bearing_unit.png
:align: center
This example demonstrates the creation of a castable flanged bearing housing
using the `draft` operation to add appropriate draft angles for mold release.
.. dropdown:: |Builder| Reference Implementation (Builder Mode)
.. literalinclude:: ../examples/cast_bearing_unit.py
:start-after: [Code]
:end-before: [End]
.. _examples-canadian_flag: .. _examples-canadian_flag:
Canadian Flag Blowing in The Wind Canadian Flag Blowing in The Wind
@ -280,6 +330,32 @@ a detailed and visually appealing clock design.
:class:`~build_common.PolarLocations` are used to position features on the clock face. :class:`~build_common.PolarLocations` are used to position features on the clock face.
.. _fast_grid_holes:
Fast Grid Holes
---------------
.. image:: assets/examples/fast_grid_holes.png
:align: center
.. dropdown:: |Algebra| Reference Implementation (Algebra Mode)
.. literalinclude:: ../examples/fast_grid_holes.py
:start-after: [Code]
:end-before: [End]
This example demonstrates an efficient approach to creating a large number of holes
(625 in this case) in a planar part using build123d.
Instead of modeling and subtracting 3D solids for each hole—which is computationally
expensive—this method constructs a 2D Face from an outer perimeter wire and a list of
hole wires. The entire face is then extruded in a single operation to form the final
3D object. This approach significantly reduces modeling time and complexity.
The hexagonal hole pattern is generated using HexLocations, and each location is
populated with a hexagonal wire. These wires are passed directly to the Face constructor
as holes. On a typical Linux laptop, this script completes in approximately 1.02 seconds,
compared to substantially longer runtimes for boolean subtraction of individual holes in 3D.
.. _handle: .. _handle:
@ -473,7 +549,7 @@ Stud Wall
.. image:: assets/examples/stud_wall.png .. image:: assets/examples/stud_wall.png
:align: center :align: center
This example demonstrates creatings custom `Part` objects and putting them into This example demonstrates creating custom `Part` objects and putting them into
assemblies. The custom object is a `Stud` used in the building industry while assemblies. The custom object is a `Stud` used in the building industry while
the assembly is a `StudWall` created from copies of `Stud` objects for efficiency. the assembly is a `StudWall` created from copies of `Stud` objects for efficiency.
Both the `Stud` and `StudWall` objects use `RigidJoints` to define snap points which Both the `Stud` and `StudWall` objects use `RigidJoints` to define snap points which
@ -527,7 +603,7 @@ Toy Truck
--------- ---------
.. image:: assets/examples/toy_truck.png .. image:: assets/examples/toy_truck.png
:align: center :align: center
.. image:: assets/examples/toy_truck_picture.jpg .. image:: assets/examples/toy_truck_picture.jpg
:align: center :align: center
@ -537,11 +613,11 @@ Toy Truck
:start-after: [Code] :start-after: [Code]
:end-before: [End] :end-before: [End]
This example demonstrates how to design a toy truck using BuildPart and This example demonstrates how to design a toy truck using BuildPart and
BuildSketch in Builder mode. The model includes a detailed body, cab, grill, BuildSketch in Builder mode. The model includes a detailed body, cab, grill,
and bumper, showcasing techniques like sketch reuse, symmetry, tapered and bumper, showcasing techniques like sketch reuse, symmetry, tapered
extrusions, selective filleting, and the use of joints for part assembly. extrusions, selective filleting, and the use of joints for part assembly.
Ideal for learning complex part construction and hierarchical modeling in Ideal for learning complex part construction and hierarchical modeling in
build123d. build123d.
.. _vase: .. _vase:

View file

@ -25,7 +25,7 @@ in pairs - a :class:`~topology.Joint` can only be connected to another :class:`~
Objects may have many joints bound to them each with an identifying label. All :class:`~topology.Joint` Objects may have many joints bound to them each with an identifying label. All :class:`~topology.Joint`
objects have a ``symbol`` property that can be displayed to help visualize objects have a ``symbol`` property that can be displayed to help visualize
their position and orientation (the `ocp-vscode <https://github.com/bernhard-42/vscode-ocp-cad-viewer>`_ viewer their position and orientation (the `ocp-vscode <https://github.com/bernhard-42/vscode-ocp-cad-viewer>`_ viewer
has built-in support for displaying joints). has built-in support for displaying joints).
.. note:: .. note::
@ -41,16 +41,16 @@ The following sections provide more detail on the available joints and describes
Rigid Joint Rigid Joint
*********** ***********
A rigid joint positions two components relative to each another with no freedom of movement. When a A rigid joint positions two components relative to each another with no freedom of movement. When a
:class:`~joints.RigidJoint` is instantiated it's assigned a ``label``, a part to bind to (``to_part``), :class:`~joints.RigidJoint` is instantiated it's assigned a ``label``, a part to bind to (``to_part``),
and a ``joint_location`` which defines both the position and orientation of the joint (see and a ``joint_location`` which defines both the position and orientation of the joint (see
:class:`~geometry.Location`) - as follows: :class:`~geometry.Location`) - as follows:
.. code-block:: python .. code-block:: python
RigidJoint(label="outlet", to_part=pipe, joint_location=path.location_at(1)) RigidJoint(label="outlet", to_part=pipe, joint_location=path.location_at(1))
Once a joint is bound to a part this way, the :meth:`~topology.Joint.connect_to` method can be used to Once a joint is bound to a part this way, the :meth:`~topology.Joint.connect_to` method can be used to
repositioning another part relative to ``self`` which stay fixed - as follows: repositioning another part relative to ``self`` which stay fixed - as follows:
.. code-block:: python .. code-block:: python
@ -74,7 +74,7 @@ flanges are attached to the ends of a curved pipe:
Note how the locations of the joints are determined by the :meth:`~topology.Mixin1D.location_at` method Note how the locations of the joints are determined by the :meth:`~topology.Mixin1D.location_at` method
and how the ``-`` negate operator is used to reverse the direction of the location without changing its and how the ``-`` negate operator is used to reverse the direction of the location without changing its
poosition. Also note that the ``WeldNeckFlange`` class predefines two joints, one at the pipe end and position. Also note that the ``WeldNeckFlange`` class predefines two joints, one at the pipe end and
one at the face end - both of which are shown in the above image (generated by ocp-vscode with the one at the face end - both of which are shown in the above image (generated by ocp-vscode with the
``render_joints=True`` flag set in the ``show`` function). ``render_joints=True`` flag set in the ``show`` function).
@ -105,7 +105,7 @@ Revolute Joint
Component rotates around axis like a hinge. The :ref:`joint_tutorial` covers Revolute Joints in detail. Component rotates around axis like a hinge. The :ref:`joint_tutorial` covers Revolute Joints in detail.
During instantiation of a :class:`~joints.RevoluteJoint` there are three parameters not present with During instantiation of a :class:`~joints.RevoluteJoint` there are three parameters not present with
Rigid Joints: ``axis``, ``angle_reference``, and ``range`` that allow the circular motion to be fully Rigid Joints: ``axis``, ``angle_reference``, and ``range`` that allow the circular motion to be fully
defined. defined.
@ -114,7 +114,7 @@ which allows one to change the relative position of joined parts by changing a s
.. autoclass:: RevoluteJoint .. autoclass:: RevoluteJoint
.. ..
:exclude-members: connect_to :exclude-members: connect_to
.. method:: connect_to(other: RigidJoint, *, angle: float = None) .. method:: connect_to(other: RigidJoint, *, angle: float = None)
@ -151,7 +151,7 @@ of the limits will raise an exception.
.. autoclass:: LinearJoint .. autoclass:: LinearJoint
.. ..
:exclude-members: connect_to :exclude-members: connect_to
.. method:: connect_to(other: RevoluteJoint, *, position: float = None, angle: float = None) .. method:: connect_to(other: RevoluteJoint, *, position: float = None, angle: float = None)
@ -164,10 +164,10 @@ of the limits will raise an exception.
Cylindrical Joint Cylindrical Joint
***************** *****************
A :class:`~joints.CylindricalJoint` allows a component to rotate around and moves along a single axis A :class:`~joints.CylindricalJoint` allows a component to rotate around and moves along a single axis
like a screw combining the functionality of a :class:`~joints.LinearJoint` and a like a screw combining the functionality of a :class:`~joints.LinearJoint` and a
:class:`~joints.RevoluteJoint` joint. The ``connect_to`` for these joints have both ``position`` and :class:`~joints.RevoluteJoint` joint. The ``connect_to`` for these joints have both ``position`` and
``angle`` parameters as shown below extracted from the joint tutorial. ``angle`` parameters as shown below extracted from the joint tutorial.
.. code-block::python .. code-block::python
@ -176,7 +176,7 @@ like a screw combining the functionality of a :class:`~joints.LinearJoint` and a
.. autoclass:: CylindricalJoint .. autoclass:: CylindricalJoint
.. ..
:exclude-members: connect_to :exclude-members: connect_to
.. method:: connect_to(other: RigidJoint, *, position: float = None, angle: float = None) .. method:: connect_to(other: RigidJoint, *, position: float = None, angle: float = None)
@ -195,13 +195,13 @@ is found within a rod end as shown here:
.. literalinclude:: rod_end.py .. literalinclude:: rod_end.py
:emphasize-lines: 40-44,51,53 :emphasize-lines: 40-44,51,53
Note how limits are defined during the instantiation of the ball joint when ensures that the pin or bolt Note how limits are defined during the instantiation of the ball joint when ensures that the pin or bolt
within the rod end does not interfere with the rod end itself. The ``connect_to`` sets the three angles within the rod end does not interfere with the rod end itself. The ``connect_to`` sets the three angles
(only two are significant in this example). (only two are significant in this example).
.. autoclass:: BallJoint .. autoclass:: BallJoint
.. ..
:exclude-members: connect_to :exclude-members: connect_to
.. method:: connect_to(other: RigidJoint, *, angles: RotationLike = None) .. method:: connect_to(other: RigidJoint, *, angles: RotationLike = None)

View file

@ -21,51 +21,53 @@ The following table summarizes all of the available operations. Operations marke
applicable to BuildLine and Algebra Curve, 2D to BuildSketch and Algebra Sketch, 3D to applicable to BuildLine and Algebra Curve, 2D to BuildSketch and Algebra Sketch, 3D to
BuildPart and Algebra Part. BuildPart and Algebra Part.
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| Operation | Description | 0D | 1D | 2D | 3D | Example | | Operation | Description | 0D | 1D | 2D | 3D | Example |
+==============================================+====================================+====+====+====+====+========================+ +==============================================+====================================+====+====+====+====+===================================+
| :func:`~operations_generic.add` | Add object to builder | | ✓ | ✓ | ✓ | :ref:`16 <ex 16>` | | :func:`~operations_generic.add` | Add object to builder | | ✓ | ✓ | ✓ | :ref:`16 <ex 16>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.bounding_box` | Add bounding box as Shape | | ✓ | ✓ | ✓ | | | :func:`~operations_generic.bounding_box` | Add bounding box as Shape | | ✓ | ✓ | ✓ | |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.chamfer` | Bevel Vertex or Edge | | | ✓ | ✓ | :ref:`9 <ex 9>` | | :func:`~operations_generic.chamfer` | Bevel Vertex or Edge | | | ✓ | ✓ | :ref:`9 <ex 9>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_part.extrude` | Draw 2D Shape into 3D | | | | ✓ | :ref:`3 <ex 3>` | | :func:`~operations_part.draft` | Add a draft taper to a part | | | | ✓ | :ref:`examples-cast_bearing_unit` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.fillet` | Radius Vertex or Edge | | | ✓ | ✓ | :ref:`9 <ex 9>` | | :func:`~operations_part.extrude` | Draw 2D Shape into 3D | | | | ✓ | :ref:`3 <ex 3>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_sketch.full_round` | Round-off Face along given Edge | | | ✓ | | :ref:`ttt-24-spo-06` | | :func:`~operations_generic.fillet` | Radius Vertex or Edge | | | ✓ | ✓ | :ref:`9 <ex 9>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_part.loft` | Create 3D Shape from sections | | | | ✓ | :ref:`24 <ex 24>` | | :func:`~operations_sketch.full_round` | Round-off Face along given Edge | | | ✓ | | :ref:`ttt-24-spo-06` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_part.make_brake_formed` | Create sheet metal parts | | | | ✓ | | | :func:`~operations_part.loft` | Create 3D Shape from sections | | | | ✓ | :ref:`24 <ex 24>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_sketch.make_face` | Create a Face from Edges | | | ✓ | | :ref:`4 <ex 4>` | | :func:`~operations_part.make_brake_formed` | Create sheet metal parts | | | | ✓ | |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_sketch.make_hull` | Create Convex Hull from Edges | | | ✓ | | | | :func:`~operations_sketch.make_face` | Create a Face from Edges | | | ✓ | | :ref:`4 <ex 4>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.mirror` | Mirror about Plane | | ✓ | ✓ | ✓ | :ref:`15 <ex 15>` | | :func:`~operations_sketch.make_hull` | Create Convex Hull from Edges | | | ✓ | | |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.offset` | Inset or outset Shape | | ✓ | ✓ | ✓ | :ref:`25 <ex 25>` | | :func:`~operations_generic.mirror` | Mirror about Plane | | ✓ | ✓ | ✓ | :ref:`15 <ex 15>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.project` | Project points, lines or Faces | ✓ | ✓ | ✓ | | | | :func:`~operations_generic.offset` | Inset or outset Shape | | ✓ | ✓ | ✓ | :ref:`25 <ex 25>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_part.project_workplane` | Create workplane for projection | | | | | | | :func:`~operations_generic.project` | Project points, lines or Faces | ✓ | ✓ | ✓ | | |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_part.revolve` | Swing 2D Shape about Axis | | | | ✓ | :ref:`23 <ex 23>` | | :func:`~operations_part.project_workplane` | Create workplane for projection | | | | | |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.scale` | Change size of Shape | | ✓ | ✓ | ✓ | | | :func:`~operations_part.revolve` | Swing 2D Shape about Axis | | | | ✓ | :ref:`23 <ex 23>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_part.section` | Generate 2D slices from 3D Shape | | | | ✓ | | | :func:`~operations_generic.scale` | Change size of Shape | | ✓ | ✓ | ✓ | |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.split` | Divide object by Plane | | ✓ | ✓ | ✓ | :ref:`27 <ex 27>` | | :func:`~operations_part.section` | Generate 2D slices from 3D Shape | | | | ✓ | |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_generic.sweep` | Extrude 1/2D section(s) along path | | | ✓ | ✓ | :ref:`14 <ex 14>` | | :func:`~operations_generic.split` | Divide object by Plane | | ✓ | ✓ | ✓ | :ref:`27 <ex 27>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_part.thicken` | Expand 2D section(s) | | | | ✓ | | | :func:`~operations_generic.sweep` | Extrude 1/2D section(s) along path | | | ✓ | ✓ | :ref:`14 <ex 14>` |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_sketch.trace` | Convert lines to faces | | | ✓ | | | | :func:`~operations_part.thicken` | Expand 2D section(s) | | | | ✓ | |
+----------------------------------------------+------------------------------------+----+----+----+----+------------------------+ +----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
| :func:`~operations_sketch.trace` | Convert lines to faces | | | ✓ | | |
+----------------------------------------------+------------------------------------+----+----+----+----+-----------------------------------+
The following table summarizes all of the selectors that can be used within The following table summarizes all of the selectors that can be used within
the scope of a Builder. Note that they will extract objects from the builder that is the scope of a Builder. Note that they will extract objects from the builder that is
@ -104,6 +106,7 @@ Reference
.. autofunction:: operations_generic.add .. autofunction:: operations_generic.add
.. autofunction:: operations_generic.bounding_box .. autofunction:: operations_generic.bounding_box
.. autofunction:: operations_generic.chamfer .. autofunction:: operations_generic.chamfer
.. autofunction:: operations_part.draft
.. autofunction:: operations_part.extrude .. autofunction:: operations_part.extrude
.. autofunction:: operations_generic.fillet .. autofunction:: operations_generic.fillet
.. autofunction:: operations_sketch.full_round .. autofunction:: operations_sketch.full_round

View file

@ -3,9 +3,7 @@ from bd_warehouse.thread import IsoThread
from ocp_vscode import * from ocp_vscode import *
# Create the thread so the min radius is available below # Create the thread so the min radius is available below
thread = IsoThread( thread = IsoThread(major_diameter=6, pitch=1, length=20, end_finishes=("fade", "raw"))
major_diameter=8, pitch=1.25, length=20, end_finishes=("fade", "raw")
)
inner_radius = 15.89 / 2 inner_radius = 15.89 / 2
inner_gap = 0.2 inner_gap = 0.2
@ -52,4 +50,4 @@ with BuildPart() as ball:
rod_end.part.joints["socket"].connect_to(ball.part.joints["ball"], angles=(5, 10, 0)) rod_end.part.joints["socket"].connect_to(ball.part.joints["ball"], angles=(5, 10, 0))
show(rod_end.part, ball.part) show(rod_end.part, ball.part, s2)

View file

@ -0,0 +1,73 @@
.. _tech_drawing_tutorial:
##########################
Technical Drawing Tutorial
##########################
This example demonstrates how to generate a standard technical drawing of a 3D part
using `build123d`. It creates orthographic and isometric views of a Nema 23 stepper
motor and exports the result as an SVG file suitable for printing or inspection.
Overview
--------
A technical drawing represents a 3D object in 2D using a series of standardized views.
These include:
- **Plan (Top View)** as seen from directly above (Z-axis down)
- **Front Elevation** looking at the object head-on (Y-axis forward)
- **Side Elevation (Right Side)** viewed from the right (X-axis)
- **Isometric Projection** a 3D perspective view to help visualize depth
Each view is aligned to a position on the page and optionally scaled or annotated.
How It Works
------------
The script uses the `project_to_viewport` method to project the 3D part geometry into 2D.
A helper function, `project_to_2d`, sets up the viewport (camera origin and up direction)
and places the result onto a virtual drawing sheet.
The steps involved are:
1. Load or construct a 3D part (in this case, a stepper motor).
2. Define a `TechnicalDrawing` border and title block using A4 page size.
3. Generate each of the standard views and apply transformations to place them.
4. Add dimensions using `ExtensionLine` and labels using `Text`.
5. Export the drawing using `ExportSVG`, separating visible and hidden edges by layer
and style.
Result
------
.. image:: /assets/stepper_drawing.svg
:alt: Stepper motor technical drawing
:class: align-center
:width: 80%
Try It Yourself
---------------
You can modify the script to:
- Replace the part with your own `Part` model
- Adjust camera angles and scale
- Add other views (bottom, rear)
- Enhance with more labels and dimensions
Code
----
.. literalinclude:: technical_drawing.py
:language: python
:start-after: [code]
:end-before: [end]
Dependencies
------------
This example depends on the following packages:
- `build123d`
- `bd_warehouse` (for the `StepperMotor` part)
- `ocp_vscode` (for local preview)

188
docs/technical_drawing.py Normal file
View file

@ -0,0 +1,188 @@
"""
name: technical_drawing.py
by: gumyr
date: May 23, 2025
desc:
Generate a multi-view technical drawing of a part, including isometric and
orthographic projections.
This module demonstrates how to create a standard technical drawing using
`build123d`. It includes:
- Projection of a 3D part to 2D views (plan, front, side, isometric)
- Drawing borders and dimensioning using extension lines
- SVG export of visible and hidden geometry
- Example part: Nema 23 stepper motor from `bd_warehouse.open_builds`
The following standard views are generated:
- Plan View (Top)
- Front Elevation
- Side Elevation (Right Side)
- Isometric Projection
The resulting drawing is exported as an SVG and can be previewed using
the `ocp_vscode` viewer.
license:
Copyright 2025 gumyr
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# [code]
from datetime import date
from bd_warehouse.open_builds import StepperMotor
from build123d import *
from ocp_vscode import show
def project_to_2d(
part: Part,
viewport_origin: VectorLike,
viewport_up: VectorLike,
page_origin: VectorLike,
scale_factor: float = 1.0,
) -> tuple[ShapeList[Edge], ShapeList[Edge]]:
"""project_to_2d
Helper function to generate 2d views translated on the 2d page.
Args:
part (Part): 3d object
viewport_origin (VectorLike): location of viewport
viewport_up (VectorLike): direction of the viewport Y axis
page_origin (VectorLike): center of 2d object on page
scale_factor (float, optional): part scalar. Defaults to 1.0.
Returns:
tuple[ShapeList[Edge], ShapeList[Edge]]: visible & hidden edges
"""
scaled_part = part if scale_factor == 1.0 else scale(part, scale_factor)
visible, hidden = scaled_part.project_to_viewport(
viewport_origin, viewport_up, look_at=(0, 0, 0)
)
visible = [Pos(*page_origin) * e for e in visible]
hidden = [Pos(*page_origin) * e for e in hidden]
return ShapeList(visible), ShapeList(hidden)
# The object that appearing in the drawing
stepper: Part = StepperMotor("Nema23")
# Create a standard technical drawing border on A4 paper
border = TechnicalDrawing(
designed_by="build123d",
design_date=date.fromisoformat("2025-05-23"),
page_size=PageSize.A4,
title="Nema 23 Stepper",
sub_title="Units: mm",
drawing_number="BD-1",
sheet_number=1,
drawing_scale=1,
)
page_size = border.bounding_box().size
# Specify the drafting options for extension lines
drafting_options = Draft(font_size=3.5, decimal_precision=1, display_units=False)
# Lists used to store the 2d visible and hidden lines
visible_lines, hidden_lines = [], []
# Isometric Projection - A 3D view where the part is rotated to reveal three
# dimensions equally.
iso_v, iso_h = project_to_2d(
stepper,
(100, 100, 100),
(0, 0, 1),
page_size * 0.3,
0.75,
)
visible_lines.extend(iso_v)
hidden_lines.extend(iso_h)
# Plan View (Top) - The view from directly above the part (looking down along
# the Z-axis).
vis, _ = project_to_2d(
stepper,
(0, 0, 100),
(0, 1, 0),
(page_size.X * -0.3, page_size.Y * 0.25),
)
visible_lines.extend(vis)
# Dimension the top of the stepper
top_bbox = Curve(vis).bounding_box()
perimeter = Pos(*top_bbox.center()) * Rectangle(top_bbox.size.X, top_bbox.size.Y)
d1 = ExtensionLine(
border=perimeter.edges().sort_by(Axis.X)[-1], offset=1 * CM, draft=drafting_options
)
d2 = ExtensionLine(
border=perimeter.edges().sort_by(Axis.Y)[0], offset=1 * CM, draft=drafting_options
)
# Add a label
l1 = Text("Plan View", 6)
l1.position = vis.sort_by(Axis.Y)[-1].center() + (0, 5 * MM)
# Front Elevation - The primary view, typically looking along the Y-axis,
# showing the height.
vis, _ = project_to_2d(
stepper,
(0, -100, 0),
(0, 0, 1),
(page_size.X * -0.3, page_size.Y * -0.125),
)
visible_lines.extend(vis)
d3 = ExtensionLine(
border=vis.sort_by(Axis.Y)[-1], offset=-5 * MM, draft=drafting_options
)
l2 = Text("Front Elevation", 6)
l2.position = vis.group_by(Axis.Y)[0].sort_by(Edge.length)[-1].center() + (0, -5 * MM)
# Side Elevation - Often refers to the Right Side View, looking along the X-axis.
vis, _ = project_to_2d(
stepper,
(100, 0, 0),
(0, 0, 1),
(0, page_size.Y * 0.15),
)
visible_lines.extend(vis)
side_bbox = Curve(vis).bounding_box()
perimeter = Pos(*side_bbox.center()) * Rectangle(side_bbox.size.X, side_bbox.size.Y)
d4 = ExtensionLine(
border=perimeter.edges().sort_by(Axis.X)[-1], offset=1 * CM, draft=drafting_options
)
l3 = Text("Side Elevation", 6)
l3.position = vis.group_by(Axis.Y)[0].sort_by(Edge.length)[-1].center() + (0, -5 * MM)
# Initialize the SVG exporter
exporter = ExportSVG(unit=Unit.MM)
# Define visible and hidden line layers
exporter.add_layer("Visible")
exporter.add_layer("Hidden", line_color=(99, 99, 99), line_type=LineType.ISO_DOT)
# Add the objects to the appropriate layer
exporter.add_shape(visible_lines, layer="Visible")
exporter.add_shape(hidden_lines, layer="Hidden")
exporter.add_shape(border, layer="Visible")
exporter.add_shape([d1, d2, d3, d4], layer="Visible")
exporter.add_shape([l1, l2, l3], layer="Visible")
# Write the file
exporter.write(f"assets/stepper_drawing.svg")
show(border, visible_lines, d1, d2, d3, d4, l1, l2, l3)
# [end]

File diff suppressed because it is too large Load diff

View file

@ -159,7 +159,7 @@ class Hinge(Compound):
for hole, hole_location in enumerate(hole_locations): for hole, hole_location in enumerate(hole_locations):
CylindricalJoint( CylindricalJoint(
label="hole" + str(hole), label="hole" + str(hole),
axis=hole_location.to_axis(), axis=Axis(hole_location),
linear_range=(-2 * CM, 2 * CM), linear_range=(-2 * CM, 2 * CM),
angular_range=(0, 360), angular_range=(0, 360),
) )

View file

@ -16,3 +16,4 @@ as later tutorials build on the concepts introduced in earlier ones.
examples_1.rst examples_1.rst
tttt.rst tttt.rst
tutorial_surface_modeling.rst tutorial_surface_modeling.rst
tech_drawing_tutorial.rst

109
examples/bicycle_tire.py Normal file
View file

@ -0,0 +1,109 @@
"""
A bicycle tire with tread.
name: bicycle_tire.py
by: Gumyr
date: May 20, 2025
desc:
This example demonstrates how to model a realistic bicycle tire with a
patterned tread using build123d. The key concept showcased here is the
use of wrap_faces to project 2D planar geometry onto a curved 3D
surface.
The tire cross-section is defined using a series of Bezier curves and
revolved to form the main tire body. A 2D tread pattern is created as a
sketch on a plane and then wrapped onto the non-planar revolved surface
using wrap_faces, following a path on the surface. The wrapped faces are
then thickened into 3D solid nubs and copied around the tire using
rotational placement.
This technique is particularly useful for applying surface detailsuch
as grooves, logos, or texturesto curved or freeform geometries in a CAD
model.
Highlights:
- Complex profile creation using multiple Bezier segments.
- Surface wrapping of planar sketches using wrap_faces.
- Solidification of surface features via thicken.
- Circular duplication of solids using rotational transforms.
license:
Copyright 2025 Gumyr
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# [Code]
import copy
from build123d import *
from ocp_vscode import show
wheel_diameter = 740 * MM
with BuildSketch() as tire_profile:
with BuildLine() as build_profile:
l00 = Bezier((0.0, 0.0), (7.05, 0.0), (12.18, 1.54), (15.13, 4.54))
l01 = Bezier(l00 @ 1, (15.81, 5.22), (15.98, 5.44), (16.5, 6.23))
l02 = Bezier(l01 @ 1, (18.45, 9.19), (19.61, 13.84), (19.94, 20.06))
l03 = Bezier(l02 @ 1, (20.1, 23.24), (19.93, 27.48), (19.56, 29.45))
l04 = Bezier(l03 @ 1, (19.13, 31.69), (18.23, 33.67), (16.91, 35.32))
l05 = Bezier(l04 @ 1, (16.26, 36.12), (15.57, 36.77), (14.48, 37.58))
l06 = Bezier(l05 @ 1, (12.77, 38.85), (11.51, 40.28), (10.76, 41.78))
l07 = Bezier(l06 @ 1, (10.07, 43.16), (10.15, 43.81), (11.03, 43.98))
l08 = Bezier(l07 @ 1, (11.82, 44.13), (12.15, 44.55), (12.08, 45.33))
l09 = Bezier(l08 @ 1, (12.01, 46.07), (11.84, 46.43), (11.43, 46.69))
l10 = Bezier(l09 @ 1, (10.98, 46.97), (10.07, 46.7), (9.47, 46.1))
l11 = Bezier(l10 @ 1, (9.03, 45.65), (8.88, 45.31), (8.84, 44.65))
l12 = Bezier(l11 @ 1, (8.78, 43.6), (9.11, 42.26), (9.72, 41.0))
l13 = Bezier(l12 @ 1, (10.43, 39.54), (11.52, 38.2), (12.78, 37.22))
l14 = Bezier(l13 @ 1, (15.36, 35.23), (16.58, 33.76), (17.45, 31.62))
l15 = Bezier(l14 @ 1, (17.91, 30.49), (18.22, 29.27), (18.4, 27.8))
l16 = Bezier(l15 @ 1, (18.53, 26.78), (18.52, 23.69), (18.37, 22.61))
l17 = Bezier(l16 @ 1, (17.8, 18.23), (16.15, 14.7), (13.39, 11.94))
l18 = Bezier(l17 @ 1, (11.89, 10.45), (10.19, 9.31), (8.09, 8.41))
l19 = Bezier(l18 @ 1, (3.32, 6.35), (0.0, 6.64))
mirror(about=Plane.YZ)
make_face()
tire = revolve(Pos(Y=-wheel_diameter / 2) * tire_profile.face(), Axis.X)
with BuildSketch() as tread_pattern:
with Locations((1, 1)):
Trapezoid(15, 12, 60, 120, align=Align.MIN)
with Locations((1, 8)):
with GridLocations(0, 5, 1, 2):
Rectangle(50, 2, mode=Mode.SUBTRACT)
# Define the surface and path that the tread pattern will be wrapped onto
half_road_surface = Face.revolve(Pos(Y=-wheel_diameter / 2) * l00, 360, Axis.X)
tread_path = half_road_surface.edges().sort_by(Axis.X)[0]
# Wrap the planar tread pattern onto the tire's outside surface
tread_faces = half_road_surface.wrap_faces(tread_pattern.faces(), tread_path)
# Mirror the faces to the other half of the tire
tread_faces.extend([mirror(t, Plane.YZ) for t in tread_faces])
# Thicken the tread to become solid nubs
# tread_prime = [Solid.thicken(f, 3 * MM) for f in tread_faces]
tread_prime = [thicken(f, 3 * MM) for f in tread_faces]
# Copy the nubs around the whole tire
tread = [Rot(X=r) * copy.copy(t) for t in tread_prime for r in range(0, 360, 2)]
show(tire, tread)
# [End]

View file

@ -0,0 +1,73 @@
"""
An oval flanged bearing unit with tapered sides created with the draft operation.
name: cast_bearing_unit.py
by: Gumyr
date: May 25, 2025
desc:
This example demonstrates the creation of a castable flanged bearing housing
using the `draft` operation to add appropriate draft angles for mold release.
### Highlights:
- **Component Integration**: The design incorporates a press-fit bore for a
`SingleRowAngularContactBallBearing` and mounting holes for
`SocketHeadCapScrew` fasteners.
- **Draft Angle Application**: Vertical side faces are identified and modified
with a 4-degree draft angle using the `draft()` function. This simulates the
taper needed for cast parts to be removed cleanly from a mold.
- **Filleting**: All edges are filleted to reflect casting-friendly geometry and
improve aesthetics.
- **Parametric Design**: Dimensions such as bolt spacing, bearing size, and
housing depth are parameterized for reuse and adaptation to other sizes.
The result is a realistic, fabrication-aware model that can be used for
documentation, simulation, or manufacturing workflows. The final assembly
includes the housing, inserted bearing, and positioned screws, rendered with
appropriate coloring for clarity.
license:
Copyright 2025 Gumyr
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# [Code]
from build123d import *
from ocp_vscode import show
A, A1, Db2, H, J = 26, 11, 57, 98.5, 76.5
with BuildPart() as oval_flanged_bearing_unit:
with BuildSketch() as plan:
housing = Circle(Db2 / 2)
with GridLocations(J, 0, 2, 1) as bolt_centers:
Circle((H - J) / 2)
make_hull()
extrude(amount=A1)
extrude(housing, amount=A)
drafted_faces = oval_flanged_bearing_unit.faces().filter_by(Axis.Z, reverse=True)
draft(drafted_faces, Plane.XY, 4)
fillet(oval_flanged_bearing_unit.edges(), 1)
with Locations(oval_flanged_bearing_unit.faces().sort_by(Axis.Z)[-1]):
CounterBoreHole(14 / 2, 47 / 2, 14)
with Locations(*bolt_centers):
Hole(5)
oval_flanged_bearing_unit.part.color = Color(0x4C6377)
show(oval_flanged_bearing_unit)
# [End]

View file

@ -0,0 +1,65 @@
"""
A fast way to make many holes.
name: fast_grid_holes.py
by: Gumyr
date: May 31, 2025
desc:
This example demonstrates an efficient approach to creating a large number of holes
(625 in this case) in a planar part using build123d.
Instead of modeling and subtracting 3D solids for each holewhich is computationally
expensivethis method constructs a 2D Face from an outer perimeter wire and a list of
hole wires. The entire face is then extruded in a single operation to form the final
3D object. This approach significantly reduces modeling time and complexity.
The hexagonal hole pattern is generated using HexLocations, and each location is
populated with a hexagonal wire. These wires are passed directly to the Face constructor
as holes. On a typical Linux laptop, this script completes in approximately 1.02 seconds,
compared to substantially longer runtimes for boolean subtraction of individual holes in 3D.
license:
Copyright 2025 Gumyr
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# [Code]
import timeit
from build123d import *
from ocp_vscode import show
start_time = timeit.default_timer()
# Calculate the locations of 625 holes
major_r = 10
hole_locs = HexLocations(major_r, 25, 25)
# Create wires for both the perimeter and all the holes
face_perimeter = Rectangle(500, 600).wire()
hex_hole = RegularPolygon(major_r - 1, 6, major_radius=True).wire()
holes = hole_locs * hex_hole
# Create a new Face from the perimeter and hole wires
grid_pattern = Face(face_perimeter, holes)
# Extrude to a 3D part
grid = extrude(grid_pattern, 1)
print(f"Time: {timeit.default_timer() - start_time:0.3f}s")
show(grid)
# [End]

View file

@ -1,6 +1,7 @@
""" """
Experimental Joint development file Experimental Joint development file
""" """
from build123d import * from build123d import *
from ocp_vscode import * from ocp_vscode import *
@ -72,9 +73,9 @@ swing_arm_hinge_edge: Edge = (
.sort_by(Axis.X)[-2:] .sort_by(Axis.X)[-2:]
.sort_by(Axis.Y)[0] .sort_by(Axis.Y)[0]
) )
swing_arm_hinge_axis = swing_arm_hinge_edge.to_axis() swing_arm_hinge_axis = Axis(swing_arm_hinge_edge)
base_corner_edge = base.edges().sort_by(Axis((0, 0, 0), (1, 1, 0)))[-1] base_corner_edge = base.edges().sort_by(Axis((0, 0, 0), (1, 1, 0)))[-1]
base_hinge_axis = base_corner_edge.to_axis() base_hinge_axis = Axis(base_corner_edge)
j3 = RevoluteJoint("hinge", base, axis=base_hinge_axis, angular_range=(0, 180)) j3 = RevoluteJoint("hinge", base, axis=base_hinge_axis, angular_range=(0, 180))
j4 = RigidJoint("corner", hinge_arm, swing_arm_hinge_axis.location) j4 = RigidJoint("corner", hinge_arm, swing_arm_hinge_axis.location)
base.joints["hinge"].connect_to(hinge_arm.joints["corner"], angle=90) base.joints["hinge"].connect_to(hinge_arm.joints["corner"], angle=90)
@ -86,7 +87,7 @@ slider_arm = JointBox(4, 1, 2, 0.2)
s1 = LinearJoint( s1 = LinearJoint(
"slide", "slide",
base, base,
axis=Edge.make_mid_way(*base_top_edges, 0.67).to_axis(), axis=Axis(Edge.make_mid_way(*base_top_edges, 0.67)),
linear_range=(0, base_top_edges[0].length), linear_range=(0, base_top_edges[0].length),
) )
s2 = RigidJoint("slide", slider_arm, Location(Vector(0, 0, 0))) s2 = RigidJoint("slide", slider_arm, Location(Vector(0, 0, 0)))
@ -111,7 +112,7 @@ j5.connect_to(j6, position=-1, angle=90)
j7 = LinearJoint( j7 = LinearJoint(
"slot", "slot",
base, base,
axis=Edge.make_mid_way(*base_top_edges, 0.33).to_axis(), axis=Axis(Edge.make_mid_way(*base_top_edges, 0.33)),
linear_range=(0, base_top_edges[0].length), linear_range=(0, base_top_edges[0].length),
) )
pin_arm = JointBox(2, 1, 2) pin_arm = JointBox(2, 1, 2)

View file

@ -62,9 +62,9 @@ swing_arm_hinge_edge = (
.sort_by(Axis.X)[-2:] .sort_by(Axis.X)[-2:]
.sort_by(Axis.Y)[0] .sort_by(Axis.Y)[0]
) )
swing_arm_hinge_axis = swing_arm_hinge_edge.to_axis() swing_arm_hinge_axis = Axis(swing_arm_hinge_edge)
base_corner_edge = base.edges().sort_by(Axis((0, 0, 0), (1, 1, 0)))[-1] base_corner_edge = base.edges().sort_by(Axis((0, 0, 0), (1, 1, 0)))[-1]
base_hinge_axis = base_corner_edge.to_axis() base_hinge_axis = Axis(base_corner_edge)
j3 = RevoluteJoint("hinge", base, axis=base_hinge_axis, angular_range=(0, 180)) j3 = RevoluteJoint("hinge", base, axis=base_hinge_axis, angular_range=(0, 180))
j4 = RigidJoint("corner", hinge_arm, swing_arm_hinge_axis.location) j4 = RigidJoint("corner", hinge_arm, swing_arm_hinge_axis.location)
base.joints["hinge"].connect_to(hinge_arm.joints["corner"], angle=90) base.joints["hinge"].connect_to(hinge_arm.joints["corner"], angle=90)
@ -77,7 +77,7 @@ slider_arm = JointBox(4, 1, 2, 0.2)
s1 = LinearJoint( s1 = LinearJoint(
"slide", "slide",
base, base,
axis=Edge.make_mid_way(*base_top_edges, 0.67).to_axis(), axis=Axis(Edge.make_mid_way(*base_top_edges, 0.67)),
linear_range=(0, base_top_edges[0].length), linear_range=(0, base_top_edges[0].length),
) )
s2 = RigidJoint("slide", slider_arm, Location(Vector(0, 0, 0))) s2 = RigidJoint("slide", slider_arm, Location(Vector(0, 0, 0)))
@ -102,7 +102,7 @@ j5.connect_to(j6, position=-1, angle=90)
j7 = LinearJoint( j7 = LinearJoint(
"slot", "slot",
base, base,
axis=Edge.make_mid_way(*base_top_edges, 0.33).to_axis(), axis=Axis(Edge.make_mid_way(*base_top_edges, 0.33)),
linear_range=(0, base_top_edges[0].length), linear_range=(0, base_top_edges[0].length),
) )
pin_arm = JointBox(2, 1, 2) pin_arm = JointBox(2, 1, 2)

View file

@ -75,7 +75,7 @@ with BuildPart() as lego:
exporter = ExportSVG(scale=6) exporter = ExportSVG(scale=6)
exporter.add_shape(plan.sketch) exporter.add_shape(plan.sketch)
exporter.write("assets/lego_step6.svg") exporter.write("assets/lego_step6.svg")
# Substract a rectangle leaving ribs on the block walls # Subtract a rectangle leaving ribs on the block walls
Rectangle( Rectangle(
block_length - 2 * (wall_thickness + ridge_depth), block_length - 2 * (wall_thickness + ridge_depth),
block_width - 2 * (wall_thickness + ridge_depth), block_width - 2 * (wall_thickness + ridge_depth),

View file

@ -34,7 +34,7 @@ plan += locs * Rectangle(width=block_length, height=ridge_width)
locs = GridLocations(lego_unit_size, 0, pip_count, 1) locs = GridLocations(lego_unit_size, 0, pip_count, 1)
plan += locs * Rectangle(width=ridge_width, height=block_width) plan += locs * Rectangle(width=ridge_width, height=block_width)
# Substract a rectangle leaving ribs on the block walls # Subtract a rectangle leaving ribs on the block walls
plan -= Rectangle( plan -= Rectangle(
block_length - 2 * (wall_thickness + ridge_depth), block_length - 2 * (wall_thickness + ridge_depth),
block_width - 2 * (wall_thickness + ridge_depth), block_width - 2 * (wall_thickness + ridge_depth),

View file

@ -163,6 +163,7 @@ __all__ = [
"LinearJoint", "LinearJoint",
"CylindricalJoint", "CylindricalJoint",
"BallJoint", "BallJoint",
"DraftAngleError",
# Exporter classes # Exporter classes
"Export2D", "Export2D",
"ExportDXF", "ExportDXF",
@ -197,6 +198,7 @@ __all__ = [
"add", "add",
"bounding_box", "bounding_box",
"chamfer", "chamfer",
"draft",
"extrude", "extrude",
"fillet", "fillet",
"full_round", "full_round",

View file

@ -50,7 +50,7 @@ import functools
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from itertools import product from itertools import product
from math import sqrt, cos, pi from math import sqrt, cos, pi
from typing import Any, cast, overload, Protocol, Type, TypeVar from typing import Any, cast, overload, Protocol, Type, TypeVar, Generic
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from typing_extensions import Self from typing_extensions import Self
@ -155,6 +155,7 @@ operations_apply_to = {
"add": ["BuildPart", "BuildSketch", "BuildLine"], "add": ["BuildPart", "BuildSketch", "BuildLine"],
"bounding_box": ["BuildPart", "BuildSketch", "BuildLine"], "bounding_box": ["BuildPart", "BuildSketch", "BuildLine"],
"chamfer": ["BuildPart", "BuildSketch", "BuildLine"], "chamfer": ["BuildPart", "BuildSketch", "BuildLine"],
"draft": ["BuildPart"],
"extrude": ["BuildPart"], "extrude": ["BuildPart"],
"fillet": ["BuildPart", "BuildSketch", "BuildLine"], "fillet": ["BuildPart", "BuildSketch", "BuildLine"],
"full_round": ["BuildSketch"], "full_round": ["BuildSketch"],
@ -177,8 +178,11 @@ operations_apply_to = {
B = TypeVar("B", bound="Builder") B = TypeVar("B", bound="Builder")
"""Builder type hint""" """Builder type hint"""
ShapeT = TypeVar("ShapeT", bound=Shape)
"""Builder's are generic shape creators"""
class Builder(ABC):
class Builder(ABC, Generic[ShapeT]):
"""Builder """Builder
Base class for the build123d Builders. Base class for the build123d Builders.
@ -230,7 +234,7 @@ class Builder(ABC):
@property @property
@abstractmethod @abstractmethod
def _obj(self) -> Shape: def _obj(self) -> Shape | None:
"""Object to pass to parent""" """Object to pass to parent"""
raise NotImplementedError # pragma: no cover raise NotImplementedError # pragma: no cover
@ -247,6 +251,8 @@ class Builder(ABC):
@property @property
def new_edges(self) -> ShapeList[Edge]: def new_edges(self) -> ShapeList[Edge]:
"""Edges that changed during last operation""" """Edges that changed during last operation"""
if self._obj is None:
return ShapeList()
before_list = [] if self.obj_before is None else [self.obj_before] before_list = [] if self.obj_before is None else [self.obj_before]
return new_edges(*(before_list + self.to_combine), combined=self._obj) return new_edges(*(before_list + self.to_combine), combined=self._obj)
@ -534,7 +540,8 @@ class Builder(ABC):
""" """
vertex_list: list[Vertex] = [] vertex_list: list[Vertex] = []
if select == Select.ALL: if select == Select.ALL:
for obj_edge in self._obj.edges(): obj_edges = [] if self._obj is None else self._obj.edges()
for obj_edge in obj_edges:
vertex_list.extend(obj_edge.vertices()) vertex_list.extend(obj_edge.vertices())
elif select == Select.LAST: elif select == Select.LAST:
vertex_list = self.lasts[Vertex] vertex_list = self.lasts[Vertex]
@ -578,7 +585,7 @@ class Builder(ABC):
ShapeList[Edge]: Edges extracted ShapeList[Edge]: Edges extracted
""" """
if select == Select.ALL: if select == Select.ALL:
edge_list = self._obj.edges() edge_list = ShapeList() if self._obj is None else self._obj.edges()
elif select == Select.LAST: elif select == Select.LAST:
edge_list = self.lasts[Edge] edge_list = self.lasts[Edge]
elif select == Select.NEW: elif select == Select.NEW:
@ -621,7 +628,7 @@ class Builder(ABC):
ShapeList[Wire]: Wires extracted ShapeList[Wire]: Wires extracted
""" """
if select == Select.ALL: if select == Select.ALL:
wire_list = self._obj.wires() wire_list = ShapeList() if self._obj is None else self._obj.wires()
elif select == Select.LAST: elif select == Select.LAST:
wire_list = Wire.combine(self.lasts[Edge]) wire_list = Wire.combine(self.lasts[Edge])
elif select == Select.NEW: elif select == Select.NEW:
@ -664,7 +671,7 @@ class Builder(ABC):
ShapeList[Face]: Faces extracted ShapeList[Face]: Faces extracted
""" """
if select == Select.ALL: if select == Select.ALL:
face_list = self._obj.faces() face_list = ShapeList() if self._obj is None else self._obj.faces()
elif select == Select.LAST: elif select == Select.LAST:
face_list = self.lasts[Face] face_list = self.lasts[Face]
elif select == Select.NEW: elif select == Select.NEW:
@ -707,7 +714,7 @@ class Builder(ABC):
ShapeList[Solid]: Solids extracted ShapeList[Solid]: Solids extracted
""" """
if select == Select.ALL: if select == Select.ALL:
solid_list = self._obj.solids() solid_list = ShapeList() if self._obj is None else self._obj.solids()
elif select == Select.LAST: elif select == Select.LAST:
solid_list = self.lasts[Solid] solid_list = self.lasts[Solid]
elif select == Select.NEW: elif select == Select.NEW:
@ -744,17 +751,18 @@ class Builder(ABC):
) -> ShapeList: ) -> ShapeList:
"""Extract Shapes""" """Extract Shapes"""
obj_type = self._shape if obj_type is None else obj_type obj_type = self._shape if obj_type is None else obj_type
if self._obj is None:
return ShapeList()
if obj_type == Vertex: if obj_type == Vertex:
result = self._obj.vertices() return self._obj.vertices()
elif obj_type == Edge: if obj_type == Edge:
result = self._obj.edges() return self._obj.edges()
elif obj_type == Face: if obj_type == Face:
result = self._obj.faces() return self._obj.faces()
elif obj_type == Solid: if obj_type == Solid:
result = self._obj.solids() return self._obj.solids()
else: return ShapeList()
result = None
return result
def validate_inputs( def validate_inputs(
self, validating_class, objects: Shape | Iterable[Shape] | None = None self, validating_class, objects: Shape | Iterable[Shape] | None = None
@ -1110,7 +1118,7 @@ class Locations(LocationList):
elif isinstance(point, Vector): elif isinstance(point, Vector):
local_locations.append(Location(point)) local_locations.append(Location(point))
elif isinstance(point, Vertex): elif isinstance(point, Vertex):
local_locations.append(Location(Vector(point.to_tuple()))) local_locations.append(Location(Vector(point)))
elif isinstance(point, tuple): elif isinstance(point, tuple):
local_locations.append(Location(Vector(point))) local_locations.append(Location(Vector(point)))
elif isinstance(point, Plane): elif isinstance(point, Plane):
@ -1377,8 +1385,8 @@ def __gen_context_component_getter(
@functools.wraps(func) @functools.wraps(func)
def getter(select: Select = Select.ALL) -> T2: def getter(select: Select = Select.ALL) -> T2:
# Retrieve the current Builder context based on the method name # Retrieve the current Builder context based on the method name
context = Builder._get_context(func.__name__) context: Builder | None = Builder._get_context(func.__name__)
if not context: if context is None:
raise RuntimeError( raise RuntimeError(
f"{func.__name__}() requires a Builder context to be in scope" f"{func.__name__}() requires a Builder context to be in scope"
) )

View file

@ -36,7 +36,7 @@ from build123d.geometry import Location, Plane
from build123d.topology import Curve, Edge, Face from build123d.topology import Curve, Edge, Face
class BuildLine(Builder): class BuildLine(Builder[Curve]):
"""BuildLine """BuildLine
The BuildLine class is a subclass of Builder for building lines (objects The BuildLine class is a subclass of Builder for building lines (objects
@ -89,7 +89,15 @@ class BuildLine(Builder):
"""Set the current line""" """Set the current line"""
self._line = value self._line = value
_obj = line # Alias _obj to line @property
def _obj(self) -> Curve | None:
"""Alias _obj to line"""
return self._line
@_obj.setter
def _obj(self, value: Curve) -> None:
"""Set the current line"""
self._line = value
def __exit__(self, exception_type, exception_value, traceback): def __exit__(self, exception_type, exception_value, traceback):
"""Upon exiting restore context and send object to parent""" """Upon exiting restore context and send object to parent"""

View file

@ -37,7 +37,7 @@ from build123d.geometry import Location, Plane
from build123d.topology import Edge, Face, Joint, Part, Solid, Wire from build123d.topology import Edge, Face, Joint, Part, Solid, Wire
class BuildPart(Builder): class BuildPart(Builder[Part]):
"""BuildPart """BuildPart
The BuildPart class is another subclass of Builder for building parts The BuildPart class is another subclass of Builder for building parts
@ -80,7 +80,15 @@ class BuildPart(Builder):
"""Set the current part""" """Set the current part"""
self._part = value self._part = value
_obj = part # Alias _obj to part @property
def _obj(self) -> Part | None:
"""Alias _obj to part"""
return self._part
@_obj.setter
def _obj(self, value: Part) -> None:
"""Set the current part"""
self._part = value
@property @property
def pending_edges_as_wire(self) -> Wire: def pending_edges_as_wire(self) -> Wire:

View file

@ -36,7 +36,7 @@ from build123d.geometry import Location, Plane
from build123d.topology import Compound, Edge, Face, ShapeList, Sketch, Wire from build123d.topology import Compound, Edge, Face, ShapeList, Sketch, Wire
class BuildSketch(Builder): class BuildSketch(Builder[Sketch]):
"""BuildSketch """BuildSketch
The BuildSketch class is a subclass of Builder for building planar 2D The BuildSketch class is a subclass of Builder for building planar 2D
@ -83,7 +83,15 @@ class BuildSketch(Builder):
"""Set the builder's object""" """Set the builder's object"""
self._sketch_local = value self._sketch_local = value
_obj = sketch_local # Alias _obj to sketch_local @property
def _obj(self) -> Sketch | None:
"""Alias _obj to sketch"""
return self._sketch_local
@_obj.setter
def _obj(self, value: Sketch) -> None:
"""Set the current sketch"""
self._sketch_local = value
@property @property
def sketch(self): def sketch(self):

View file

@ -277,10 +277,7 @@ class Draft:
if isinstance(path, (Edge, Wire)): if isinstance(path, (Edge, Wire)):
processed_path = path processed_path = path
elif isinstance(path, Iterable): elif isinstance(path, Iterable):
pnts = [ pnts = [Vector(p) for p in path]
Vector(p.to_tuple()) if isinstance(p, Vertex) else Vector(p)
for p in path
]
if len(pnts) == 2: if len(pnts) == 2:
processed_path = Edge.make_line(*pnts) processed_path = Edge.make_line(*pnts)
else: else:
@ -458,7 +455,7 @@ class DimensionLine(BaseSketchObject):
else: else:
self_intersection_area = self_intersection.area self_intersection_area = self_intersection.area
d_line += placed_label d_line += placed_label
bbox_size = d_line.bounding_box().size bbox_size = d_line.bounding_box().diagonal
# Minimize size while avoiding intersections # Minimize size while avoiding intersections
if sketch is None: if sketch is None:
@ -472,7 +469,7 @@ class DimensionLine(BaseSketchObject):
else: else:
common_area = line_intersection.area common_area = line_intersection.area
common_area += self_intersection_area common_area += self_intersection_area
score = (d_line.area - 10 * common_area) / bbox_size.X score = (d_line.area - 10 * common_area) / bbox_size
d_lines[d_line] = score d_lines[d_line] = score
# Sort by score to find the best option # Sort by score to find the best option

View file

@ -29,10 +29,10 @@ license:
# pylint has trouble with the OCP imports # pylint has trouble with the OCP imports
# pylint: disable=no-name-in-module, import-error # pylint: disable=no-name-in-module, import-error
from io import BytesIO from datetime import datetime
import warnings import warnings
from io import BytesIO
from os import PathLike, fsdecode, fspath from os import PathLike, fsdecode, fspath
from typing import Union
import OCP.TopAbs as ta import OCP.TopAbs as ta
from anytree import PreOrderIter from anytree import PreOrderIter
@ -47,7 +47,11 @@ from OCP.RWGltf import RWGltf_CafWriter
from OCP.STEPCAFControl import STEPCAFControl_Controller, STEPCAFControl_Writer from OCP.STEPCAFControl import STEPCAFControl_Controller, STEPCAFControl_Writer
from OCP.STEPControl import STEPControl_Controller, STEPControl_StepModelType from OCP.STEPControl import STEPControl_Controller, STEPControl_StepModelType
from OCP.StlAPI import StlAPI_Writer from OCP.StlAPI import StlAPI_Writer
from OCP.TCollection import TCollection_AsciiString, TCollection_ExtendedString, TCollection_HAsciiString from OCP.TCollection import (
TCollection_AsciiString,
TCollection_ExtendedString,
TCollection_HAsciiString,
)
from OCP.TColStd import TColStd_IndexedDataMapOfStringString from OCP.TColStd import TColStd_IndexedDataMapOfStringString
from OCP.TDataStd import TDataStd_Name from OCP.TDataStd import TDataStd_Name
from OCP.TDF import TDF_Label from OCP.TDF import TDF_Label
@ -262,6 +266,8 @@ def export_step(
unit: Unit = Unit.MM, unit: Unit = Unit.MM,
write_pcurves: bool = True, write_pcurves: bool = True,
precision_mode: PrecisionMode = PrecisionMode.AVERAGE, precision_mode: PrecisionMode = PrecisionMode.AVERAGE,
*, # Too many positional arguments
timestamp: str | datetime | None = None,
) -> bool: ) -> bool:
"""export_step """export_step
@ -302,6 +308,11 @@ def export_step(
header = APIHeaderSection_MakeHeader(writer.Writer().Model()) header = APIHeaderSection_MakeHeader(writer.Writer().Model())
if to_export.label: if to_export.label:
header.SetName(TCollection_HAsciiString(to_export.label)) header.SetName(TCollection_HAsciiString(to_export.label))
if timestamp is not None:
if isinstance(timestamp, datetime):
header.SetTimeStamp(TCollection_HAsciiString(timestamp.isoformat()))
else:
header.SetTimeStamp(TCollection_HAsciiString(timestamp))
# consider using e.g. the non *Value versions instead # consider using e.g. the non *Value versions instead
# header.SetAuthorValue(1, TCollection_HAsciiString("Volker")); # header.SetAuthorValue(1, TCollection_HAsciiString("Volker"));
# header.SetOrganizationValue(1, TCollection_HAsciiString("myCompanyName")); # header.SetOrganizationValue(1, TCollection_HAsciiString("myCompanyName"));

File diff suppressed because it is too large Load diff

View file

@ -32,7 +32,7 @@ import copy as copy_module
from collections.abc import Iterable from collections.abc import Iterable
from math import copysign, cos, radians, sin, sqrt from math import copysign, cos, radians, sin, sqrt
from scipy.optimize import minimize from scipy.optimize import minimize
import sympy # type: ignore import sympy # type: ignore
from build123d.build_common import WorkplaneList, flatten_sequence, validate_inputs from build123d.build_common import WorkplaneList, flatten_sequence, validate_inputs
from build123d.build_enums import ( from build123d.build_enums import (
@ -456,7 +456,7 @@ class Helix(BaseEdgeObject):
defined by cone_angle. defined by cone_angle.
If cone_angle is not 0, radius is the initial helix radius at center. cone_angle > 0 If cone_angle is not 0, radius is the initial helix radius at center. cone_angle > 0
increases the final radius. cone_angle < 0 decreases the final radius. increases the final radius. cone_angle < 0 decreases the final radius.
Args: Args:
pitch (float): distance between loops pitch (float): distance between loops
@ -564,7 +564,7 @@ class FilletPolyline(BaseLineObject):
if len(edges) != 2: if len(edges) != 2:
continue continue
other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex} other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex}
third_edge = Edge.make_line(*[v.to_tuple() for v in other_vertices]) third_edge = Edge.make_line(*[v for v in other_vertices])
fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(radius, [vertex]) fillet_face = Face(Wire(edges + [third_edge])).fillet_2d(radius, [vertex])
fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0]) fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0])
@ -1095,9 +1095,7 @@ class PointArcTangentLine(BaseEdgeObject):
tangent_point = WorkplaneList.localize(point) tangent_point = WorkplaneList.localize(point)
if context is None: if context is None:
# Making the plane validates points and arc are coplanar # Making the plane validates points and arc are coplanar
coplane = Edge.make_line(tangent_point, arc.arc_center).common_plane( coplane = Edge.make_line(tangent_point, arc.arc_center).common_plane(arc)
arc
)
if coplane is None: if coplane is None:
raise ValueError("PointArcTangentLine only works on a single plane.") raise ValueError("PointArcTangentLine only works on a single plane.")
@ -1478,4 +1476,10 @@ class ArcArcTangentArc(BaseEdgeObject):
intersect.reverse() intersect.reverse()
arc = RadiusArc(intersect[0], intersect[1], radius=radius) arc = RadiusArc(intersect[0], intersect[1], radius=radius)
# Check and flip arc if not tangent
_, _, point = start_arc.distance_to_with_closest_points(arc)
if start_arc.tangent_at(point).cross(arc.tangent_at(point)).length > TOLERANCE:
arc = RadiusArc(intersect[0], intersect[1], radius=-radius)
super().__init__(arc, mode) super().__init__(arc, mode)

View file

@ -53,6 +53,7 @@ from build123d.topology import (
Face, Face,
ShapeList, ShapeList,
Sketch, Sketch,
Vertex,
Wire, Wire,
tuplify, tuplify,
topo_explore_common_vertex, topo_explore_common_vertex,
@ -205,7 +206,7 @@ class Polygon(BaseSketchObject):
self.pts = flattened_pts self.pts = flattened_pts
self.align = tuplify(align, 2) self.align = tuplify(align, 2)
poly_pts = [Vector(p) for p in pts] poly_pts = [Vector(p) for p in self.pts]
face = Face(Wire.make_polygon(poly_pts)) face = Face(Wire.make_polygon(poly_pts))
super().__init__(face, rotation, self.align, mode) super().__init__(face, rotation, self.align, mode)
@ -386,7 +387,7 @@ class SlotArc(BaseSketchObject):
self.slot_height = height self.slot_height = height
arc = arc if isinstance(arc, Wire) else Wire([arc]) arc = arc if isinstance(arc, Wire) else Wire([arc])
face = Face(arc.offset_2d(height / 2)).rotate(Axis.Z, rotation) face = Face(arc.offset_2d(height / 2))
super().__init__(face, rotation, None, mode) super().__init__(face, rotation, None, mode)
@ -425,10 +426,10 @@ class SlotCenterPoint(BaseSketchObject):
half_line = point_v - center_v half_line = point_v - center_v
if half_line.length * 2 <= height: if half_line.length <= 0:
raise ValueError( raise ValueError(
f"Slots must have width > height. " "Distance between center and point must be greater than 0 "
"Got: {height=} width={half_line.length * 2} (computed)" f"Got: distance = {half_line.length} (computed)"
) )
face = Face( face = Face(
@ -463,7 +464,7 @@ class SlotCenterToCenter(BaseSketchObject):
rotation: float = 0, rotation: float = 0,
mode: Mode = Mode.ADD, mode: Mode = Mode.ADD,
): ):
if center_separation <= 0: if center_separation < 0:
raise ValueError( raise ValueError(
f"Requires center_separation > 0. Got: {center_separation=}" f"Requires center_separation > 0. Got: {center_separation=}"
) )
@ -474,14 +475,18 @@ class SlotCenterToCenter(BaseSketchObject):
self.center_separation = center_separation self.center_separation = center_separation
self.slot_height = height self.slot_height = height
face = Face( if center_separation > 0:
Wire( face = Face(
[ Wire(
Edge.make_line(Vector(-center_separation / 2, 0, 0), Vector()), [
Edge.make_line(Vector(), Vector(+center_separation / 2, 0, 0)), Edge.make_line(Vector(-center_separation / 2, 0, 0), Vector()),
] Edge.make_line(Vector(), Vector(+center_separation / 2, 0, 0)),
).offset_2d(height / 2) ]
) ).offset_2d(height / 2)
)
else:
face = cast(Face, Circle(height / 2, mode=mode).face())
super().__init__(face, rotation, None, mode) super().__init__(face, rotation, None, mode)
@ -509,7 +514,7 @@ class SlotOverall(BaseSketchObject):
align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER), align: Align | tuple[Align, Align] | None = (Align.CENTER, Align.CENTER),
mode: Mode = Mode.ADD, mode: Mode = Mode.ADD,
): ):
if width <= height: if width < height:
raise ValueError( raise ValueError(
f"Slot requires that width > height. Got: {width=}, {height=}" f"Slot requires that width > height. Got: {width=}, {height=}"
) )
@ -520,7 +525,7 @@ class SlotOverall(BaseSketchObject):
self.width = width self.width = width
self.slot_height = height self.slot_height = height
if width != height: if width > height:
face = Face( face = Face(
Wire( Wire(
[ [
@ -531,6 +536,7 @@ class SlotOverall(BaseSketchObject):
) )
else: else:
face = cast(Face, Circle(width / 2, mode=mode).face()) face = cast(Face, Circle(width / 2, mode=mode).face())
super().__init__(face, rotation, align, mode) super().__init__(face, rotation, align, mode)
@ -544,16 +550,16 @@ class Text(BaseSketchObject):
"Arial Black". Alternatively, a specific font file can be specified with font_path. "Arial Black". Alternatively, a specific font file can be specified with font_path.
Note: Windows 10+ users must "Install for all users" for fonts to be found by name. Note: Windows 10+ users must "Install for all users" for fonts to be found by name.
Not all fonts have every FontStyle available, however ITALIC and BOLDITALIC will
still italicize the font if the respective font file is not available.
text_align specifies alignment of text inside the bounding box, while align the Not all fonts have every FontStyle available, however ITALIC and BOLDITALIC will
still italicize the font if the respective font file is not available.
text_align specifies alignment of text inside the bounding box, while align the
aligns the bounding box itself. aligns the bounding box itself.
Optionally, the Text can be positioned on a non-linear edge or wire with a path and Optionally, the Text can be positioned on a non-linear edge or wire with a path and
position_on_path. position_on_path.
Args: Args:
txt (str): text to render txt (str): text to render
font_size (float): size of the font in model units font_size (float): size of the font in model units
@ -564,10 +570,10 @@ class Text(BaseSketchObject):
text_align (tuple[TextAlign, TextAlign], optional): horizontal text align text_align (tuple[TextAlign, TextAlign], optional): horizontal text align
LEFT, CENTER, or RIGHT. Vertical text align BOTTOM, CENTER, TOP, or LEFT, CENTER, or RIGHT. Vertical text align BOTTOM, CENTER, TOP, or
TOPFIRSTLINE. Defaults to (TextAlign.CENTER, TextAlign.CENTER) TOPFIRSTLINE. Defaults to (TextAlign.CENTER, TextAlign.CENTER)
align (Align | tuple[Align, Align], optional): align MIN, CENTER, or MAX of align (Align | tuple[Align, Align], optional): align MIN, CENTER, or MAX of
object. Defaults to None object. Defaults to None
path (Edge | Wire, optional): path for text to follow. Defaults to None path (Edge | Wire, optional): path for text to follow. Defaults to None
position_on_path (float, optional): the relative location on path to position position_on_path (float, optional): the relative location on path to position
the text, values must be between 0.0 and 1.0. Defaults to 0.0 the text, values must be between 0.0 and 1.0. Defaults to 0.0
rotation (float, optional): angle to rotate object. Defaults to 0 rotation (float, optional): angle to rotate object. Defaults to 0
mode (Mode, optional): combination mode. Defaults to Mode.ADD mode (Mode, optional): combination mode. Defaults to Mode.ADD
@ -782,9 +788,15 @@ class Triangle(BaseSketchObject):
self.vertex_A = topo_explore_common_vertex( self.vertex_A = topo_explore_common_vertex(
self.edge_b, self.edge_c self.edge_b, self.edge_c
) #: vertex 'A' ) #: vertex 'A'
assert isinstance(self.vertex_A, Vertex)
self.vertex_A.topo_parent = self
self.vertex_B = topo_explore_common_vertex( self.vertex_B = topo_explore_common_vertex(
self.edge_a, self.edge_c self.edge_a, self.edge_c
) #: vertex 'B' ) #: vertex 'B'
assert isinstance(self.vertex_B, Vertex)
self.vertex_B.topo_parent = self
self.vertex_C = topo_explore_common_vertex( self.vertex_C = topo_explore_common_vertex(
self.edge_a, self.edge_b self.edge_a, self.edge_b
) #: vertex 'C' ) #: vertex 'C'
assert isinstance(self.vertex_C, Vertex)
self.vertex_C.topo_parent = self

View file

@ -119,11 +119,11 @@ def add(
( (
obj.unwrap(fully=False) obj.unwrap(fully=False)
if isinstance(obj, Compound) if isinstance(obj, Compound)
else obj._obj if isinstance(obj, Builder) else obj else obj._obj if isinstance(obj, Builder) and obj._obj is not None else obj
) )
for obj in object_list for obj in object_list
if not (isinstance(obj, Builder) and obj._obj is None)
] ]
validate_inputs(context, "add", object_iter) validate_inputs(context, "add", object_iter)
if isinstance(context, BuildPart): if isinstance(context, BuildPart):
@ -364,11 +364,14 @@ def chamfer(
return new_sketch return new_sketch
if target._dim == 1: if target._dim == 1:
target = ( if isinstance(target, BaseLineObject):
Wire(target.wrapped) if target.wrapped is None:
if isinstance(target, BaseLineObject) target = Wire([]) # empty wire
else target.wires()[0] else:
) target = Wire(target.wrapped)
else:
target = target.wires()[0]
if not all([isinstance(obj, Vertex) for obj in object_list]): if not all([isinstance(obj, Vertex) for obj in object_list]):
raise ValueError("1D fillet operation takes only Vertices") raise ValueError("1D fillet operation takes only Vertices")
# Remove any end vertices as these can't be filleted # Remove any end vertices as these can't be filleted
@ -376,14 +379,8 @@ def chamfer(
object_list = ShapeList( object_list = ShapeList(
filter( filter(
lambda v: not ( lambda v: not (
isclose_b( isclose_b((Vector(v) - target.position_at(0)).length, 0.0)
(Vector(*v.to_tuple()) - target.position_at(0)).length, or isclose_b((Vector(v) - target.position_at(1)).length, 0.0)
0.0,
)
or isclose_b(
(Vector(*v.to_tuple()) - target.position_at(1)).length,
0.0,
)
), ),
object_list, object_list,
) )
@ -467,11 +464,14 @@ def fillet(
return new_sketch return new_sketch
if target._dim == 1: if target._dim == 1:
target = ( if isinstance(target, BaseLineObject):
Wire(target.wrapped) if target.wrapped is None:
if isinstance(target, BaseLineObject) target = Wire([]) # empty wire
else target.wires()[0] else:
) target = Wire(target.wrapped)
else:
target = target.wires()[0]
if not all([isinstance(obj, Vertex) for obj in object_list]): if not all([isinstance(obj, Vertex) for obj in object_list]):
raise ValueError("1D fillet operation takes only Vertices") raise ValueError("1D fillet operation takes only Vertices")
# Remove any end vertices as these can't be filleted # Remove any end vertices as these can't be filleted
@ -479,14 +479,8 @@ def fillet(
object_list = ShapeList( object_list = ShapeList(
filter( filter(
lambda v: not ( lambda v: not (
isclose_b( isclose_b((Vector(v) - target.position_at(0)).length, 0.0)
(Vector(*v.to_tuple()) - target.position_at(0)).length, or isclose_b((Vector(v) - target.position_at(1)).length, 0.0)
0.0,
)
or isclose_b(
(Vector(*v.to_tuple()) - target.position_at(1)).length,
0.0,
)
), ),
object_list, object_list,
) )
@ -758,9 +752,7 @@ def project(
# The size of the object determines the size of the target projection screen # The size of the object determines the size of the target projection screen
# as the screen is normal to the direction of parallel projection # as the screen is normal to the direction of parallel projection
shape_list = [ shape_list = [Vertex(o) if isinstance(o, Vector) else o for o in object_list]
Vertex(*o.to_tuple()) if isinstance(o, Vector) else o for o in object_list
]
object_size = Compound(children=shape_list).bounding_box(optimal=False).diagonal object_size = Compound(children=shape_list).bounding_box(optimal=False).diagonal
vct_vrt_list = [o for o in object_list if isinstance(o, (Vector, Vertex))] vct_vrt_list = [o for o in object_list if isinstance(o, (Vector, Vertex))]

View file

@ -30,12 +30,13 @@ from __future__ import annotations
from typing import cast from typing import cast
from collections.abc import Iterable from collections.abc import Iterable
from build123d.build_enums import Mode, Until, Kind, Side from build123d.build_enums import GeomType, Mode, Until, Kind, Side
from build123d.build_part import BuildPart from build123d.build_part import BuildPart
from build123d.geometry import Axis, Plane, Vector, VectorLike from build123d.geometry import Axis, Plane, Vector, VectorLike
from build123d.topology import ( from build123d.topology import (
Compound, Compound,
Curve, Curve,
DraftAngleError,
Edge, Edge,
Face, Face,
Shell, Shell,
@ -55,6 +56,59 @@ from build123d.build_common import (
) )
def draft(
faces: Face | Iterable[Face],
neutral_plane: Plane,
angle: float,
) -> Part:
"""Part Operation: draft
Apply a draft angle to the given faces of the part
Args:
faces: Faces to which the draft should be applied.
neutral_plane: Plane defining the neutral direction and position.
angle: Draft angle in degrees.
"""
context: BuildPart | None = BuildPart._get_context("draft")
face_list: ShapeList[Face] = flatten_sequence(faces)
assert all(isinstance(f, Face) for f in face_list), "all faces must be of type Face"
validate_inputs(context, "draft", face_list)
valid_geom_types = {GeomType.PLANE, GeomType.CYLINDER, GeomType.CONE}
unsupported = [f for f in face_list if f.geom_type not in valid_geom_types]
if unsupported:
raise ValueError(
f"Draft not supported on face(s) with geometry: "
f"{', '.join(set(f.geom_type.name for f in unsupported))}"
)
# Check that all the faces are associated with the same Solid
topo_parents = set(f.topo_parent for f in face_list if f.topo_parent is not None)
if len(topo_parents) != 1:
raise ValueError("All faces must share the same topological parent (a Solid)")
parent_solids = next(iter(topo_parents)).solids()
if len(parent_solids) != 1:
raise ValueError("Topological parent must be a single Solid")
# Create the drafted solid
try:
new_solid = parent_solids[0].draft(face_list, neutral_plane, angle)
except DraftAngleError as err:
raise DraftAngleError(
f"Draft operation failed. "
f"Use `err.face` and `err.problematic_shape` for more information.",
face=err.face,
problematic_shape=err.problematic_shape,
) from err
if context is not None:
context._add_to_context(new_solid, clean=False, mode=Mode.REPLACE)
return Part(Compound([new_solid]).wrapped)
def extrude( def extrude(
to_extrude: Face | Sketch | None = None, to_extrude: Face | Sketch | None = None,
amount: float | None = None, amount: float | None = None,
@ -250,11 +304,11 @@ def loft(
new_solid = Solid.make_loft(loft_wires, ruled) new_solid = Solid.make_loft(loft_wires, ruled)
# Try to recover an invalid loft # Try to recover an invalid loft
if not new_solid.is_valid(): if not new_solid.is_valid:
new_solid = Solid(Shell(new_solid.faces() + section_list)) new_solid = Solid(Shell(new_solid.faces() + section_list))
if clean: if clean:
new_solid = new_solid.clean() new_solid = new_solid.clean()
if not new_solid.is_valid(): if not new_solid.is_valid:
raise RuntimeError("Failed to create valid loft") raise RuntimeError("Failed to create valid loft")
if context is not None: if context is not None:
@ -338,12 +392,10 @@ def make_brake_formed(
raise TypeError("station_widths must be either a single number or an iterable") raise TypeError("station_widths must be either a single number or an iterable")
for vertex in line_vertices: for vertex in line_vertices:
others = offset_vertices.sort_by_distance(Vector(vertex.X, vertex.Y, vertex.Z)) others = offset_vertices.sort_by_distance(Vector(vertex))
for other in others[1:]: for other in others[1:]:
if abs(Vector(*(vertex - other).to_tuple()).length - thickness) < 1e-2: if abs(Vector((vertex - other)).length - thickness) < 1e-2:
station_edges.append( station_edges.append(Edge.make_line(vertex, other))
Edge.make_line(vertex.to_tuple(), other.to_tuple())
)
break break
station_edges = station_edges.sort_by(line) station_edges = station_edges.sort_by(line)

View file

@ -32,13 +32,14 @@ from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from scipy.spatial import Voronoi from scipy.spatial import Voronoi
from typing import cast from typing import cast
from build123d.build_enums import Mode, SortBy from build123d.build_enums import Mode, SortBy, Transition
from build123d.topology import ( from build123d.topology import (
Compound, Compound,
Curve, Curve,
Edge, Edge,
Face, Face,
ShapeList, ShapeList,
Shell,
Wire, Wire,
Sketch, Sketch,
topo_explore_connected_edges, topo_explore_connected_edges,
@ -298,10 +299,15 @@ def trace(
else: else:
raise ValueError("No objects to trace") raise ValueError("No objects to trace")
# Group the edges into wires to allow for nice transitions
trace_wires = Wire.combine(trace_edges)
new_faces: list[Face] = [] new_faces: list[Face] = []
for edge in trace_edges: for to_trace in trace_wires:
trace_pen = edge.perpendicular_line(line_width, 0) trace_pen = to_trace.perpendicular_line(line_width, 0)
new_faces.extend(Face.sweep(trace_pen, edge).faces()) new_faces.extend(
Shell.sweep(trace_pen, to_trace, transition=Transition.RIGHT).faces()
)
if context is not None: if context is not None:
context._add_to_context(*new_faces, mode=mode) context._add_to_context(*new_faces, mode=mode)
context.pending_edges = ShapeList() context.pending_edges = ShapeList()

View file

@ -61,12 +61,13 @@ from .one_d import (
topo_explore_connected_faces, topo_explore_connected_faces,
) )
from .two_d import Face, Shell, Mixin2D, sort_wires_by_build_order from .two_d import Face, Shell, Mixin2D, sort_wires_by_build_order
from .three_d import Solid, Mixin3D from .three_d import Solid, Mixin3D, DraftAngleError
from .composite import Compound, Curve, Sketch, Part from .composite import Compound, Curve, Sketch, Part
__all__ = [ __all__ = [
"Shape", "Shape",
"Comparable", "Comparable",
"DraftAngleError",
"ShapePredicate", "ShapePredicate",
"GroupBy", "GroupBy",
"ShapeList", "ShapeList",

View file

@ -60,7 +60,7 @@ from math import radians, inf, pi, cos, copysign, ceil, floor
from typing import Literal, overload, TYPE_CHECKING from typing import Literal, overload, TYPE_CHECKING
from typing_extensions import Self from typing_extensions import Self
from numpy import ndarray from numpy import ndarray
from scipy.optimize import minimize from scipy.optimize import minimize, minimize_scalar
from scipy.spatial import ConvexHull from scipy.spatial import ConvexHull
import OCP.TopAbs as ta import OCP.TopAbs as ta
@ -176,6 +176,7 @@ from build123d.build_enums import (
from build123d.geometry import ( from build123d.geometry import (
DEG2RAD, DEG2RAD,
TOLERANCE, TOLERANCE,
TOL_DIGITS,
Axis, Axis,
Color, Color,
Location, Location,
@ -436,7 +437,7 @@ class Mixin1D(Shape):
if all(a0.is_coaxial(a1) for a0, a1 in combinations(as_axis, 2)): if all(a0.is_coaxial(a1) for a0, a1 in combinations(as_axis, 2)):
origin = as_axis[0].position origin = as_axis[0].position
x_dir = as_axis[0].direction x_dir = as_axis[0].direction
z_dir = as_axis[0].to_plane().x_dir z_dir = Plane(as_axis[0]).x_dir
c_plane = Plane(origin, z_dir=z_dir) c_plane = Plane(origin, z_dir=z_dir)
result = c_plane.shift_origin((0, 0)) result = c_plane.shift_origin((0, 0))
@ -492,7 +493,11 @@ class Mixin1D(Shape):
edge_list: ShapeList[Edge] = ShapeList() edge_list: ShapeList[Edge] = ShapeList()
while explorer.More(): while explorer.More():
edge_list.append(Edge(explorer.Current())) next_edge = Edge(explorer.Current())
next_edge.topo_parent = (
self if self.topo_parent is None else self.topo_parent
)
edge_list.append(next_edge)
explorer.Next() explorer.Next()
return edge_list return edge_list
else: else:
@ -1563,7 +1568,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
Returns: Returns:
Edge: linear Edge between two Edges Edge: linear Edge between two Edges
""" """
flip = first.to_axis().is_opposite(second.to_axis()) flip = Axis(first).is_opposite(Axis(second))
pnts = [ pnts = [
Edge.make_line( Edge.make_line(
first.position_at(i), second.position_at(1 - i if flip else i) first.position_at(i), second.position_at(1 - i if flip else i)
@ -2078,14 +2083,25 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
return None return None
def param_at_point(self, point: VectorLike) -> float: def param_at_point(self, point: VectorLike) -> float:
"""Normalized parameter at point along Edge""" """param_at_point
Args:
point (VectorLike): point on Edge
Raises:
ValueError: point not on edge
RuntimeError: failed to find parameter
Returns:
float: parameter value at point on edge
"""
# Note that this search algorithm would ideally be replaced with # Note that this search algorithm would ideally be replaced with
# an OCP based solution, something like that which is shown below. # an OCP based solution, something like that which is shown below.
# However, there are known issues with the OCP methods for some # However, there are known issues with the OCP methods for some
# curves which may return negative values or incorrect values at # curves which may return negative values or incorrect values at
# end points. Also note that this search takes about 1.5ms while # end points. Also note that this search takes about 1.3ms on a
# the OCP methods take about 0.4ms. # complex curve while the OCP methods take about 0.4ms.
# #
# curve = BRep_Tool.Curve_s(self.wrapped, float(), float()) # curve = BRep_Tool.Curve_s(self.wrapped, float(), float())
# param_min, param_max = BRep_Tool.Range_s(self.wrapped) # param_min, param_max = BRep_Tool.Range_s(self.wrapped)
@ -2095,26 +2111,47 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
point = Vector(point) point = Vector(point)
if not isclose_b(self.distance_to(point), 0, abs_tol=TOLERANCE): separation = self.distance_to(point)
raise ValueError(f"point ({point}) is not on edge") if not isclose_b(separation, 0, abs_tol=TOLERANCE):
raise ValueError(f"point ({point}) is {separation} from edge")
# Function to be minimized # This algorithm finds the normalized [0, 1] parameter of a point on an edge
def func(param: ndarray) -> float: # by minimizing the 3D distance between the edge and the given point.
return (self.position_at(param[0]) - point).length #
# Because some edges (e.g., BSplines) can have multiple local minima in the
# distance function, we subdivide the [0, 1] domain into 2^n intervals
# (logarithmic refinement) and perform a bounded minimization in each subinterval.
#
# The first solution found with an error smaller than the geometric resolution
# is returned. If no such minimum is found after all subdivisions, a runtime error
# is raised.
# Find the u value that results in a point within tolerance of the target max_divisions = 10 # Logarithmic refinement depth
initial_guess = max(
0.0, min(1.0, (point - self.position_at(0)).length / self.length) for division in range(max_divisions):
) intervals = 2**division
result = minimize( step = 1.0 / intervals
func,
x0=initial_guess, for i in range(intervals):
method="Nelder-Mead", lo, hi = i * step, (i + 1) * step
bounds=[(0.0, 1.0)],
tol=TOLERANCE, result = minimize_scalar(
) lambda u: (self.position_at(u) - point).length,
u_value = float(result.x[0]) bounds=(lo, hi),
return u_value method="bounded",
options={"xatol": TOLERANCE / 2},
)
# Early exit if we're below resolution limit
if (
result.fun
< (
self @ (result.x + TOLERANCE) - self @ (result.x - TOLERANCE)
).length
):
return round(float(result.x), TOL_DIGITS)
raise RuntimeError("Unable to find parameter, Edge is too complex")
def project_to_shape( def project_to_shape(
self, self,
@ -2184,6 +2221,12 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
def to_axis(self) -> Axis: def to_axis(self) -> Axis:
"""Translate a linear Edge to an Axis""" """Translate a linear Edge to an Axis"""
warnings.warn(
"to_axis is deprecated and will be removed in a future version. "
"Use 'Axis(Edge)' instead.",
DeprecationWarning,
stacklevel=2,
)
if self.geom_type != GeomType.LINE: if self.geom_type != GeomType.LINE:
raise ValueError( raise ValueError(
f"to_axis is only valid for linear Edges not {self.geom_type}" f"to_axis is only valid for linear Edges not {self.geom_type}"
@ -2192,6 +2235,12 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]):
def to_wire(self) -> Wire: def to_wire(self) -> Wire:
"""Edge as Wire""" """Edge as Wire"""
warnings.warn(
"to_wire is deprecated and will be removed in a future version. "
"Use 'Wire(Edge)' instead.",
DeprecationWarning,
stacklevel=2,
)
return Wire([self]) return Wire([self])
def trim(self, start: float, end: float) -> Edge: def trim(self, start: float, end: float) -> Edge:
@ -2596,7 +2645,7 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
for edge_index, edge in enumerate(edges): for edge_index, edge in enumerate(edges):
for i in range(fragments_per_edge): for i in range(fragments_per_edge):
param = i / (fragments_per_edge - 1) param = i / (fragments_per_edge - 1)
points.append(edge.position_at(param).to_tuple()[:2]) points.append(tuple(edge.position_at(param))[:2])
points_lookup[edge_index * fragments_per_edge + i] = (edge_index, param) points_lookup[edge_index * fragments_per_edge + i] = (edge_index, param)
convex_hull = ConvexHull(points) convex_hull = ConvexHull(points)
@ -2990,13 +3039,13 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
projection_object = BRepProj_Projection( projection_object = BRepProj_Projection(
self.wrapped, self.wrapped,
target_object.wrapped, target_object.wrapped,
gp_Dir(*direction_vector.to_tuple()), gp_Dir(*direction_vector),
) )
else: else:
projection_object = BRepProj_Projection( projection_object = BRepProj_Projection(
self.wrapped, self.wrapped,
target_object.wrapped, target_object.wrapped,
gp_Pnt(*center_point.to_tuple()), gp_Pnt(*center_point),
) )
# Generate a list of the projected wires with aligned orientation # Generate a list of the projected wires with aligned orientation
@ -3067,91 +3116,62 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]):
def to_wire(self) -> Wire: def to_wire(self) -> Wire:
"""Return Wire - used as a pair with Edge.to_wire when self is Wire | Edge""" """Return Wire - used as a pair with Edge.to_wire when self is Wire | Edge"""
warnings.warn(
"to_wire is deprecated and will be removed in a future version. "
"Use 'Wire(Wire)' instead.",
DeprecationWarning,
stacklevel=2,
)
return self return self
def trim(self: Wire, start: float, end: float) -> Wire: def trim(self: Wire, start: float, end: float) -> Wire:
"""trim """Trim a wire between [start, end] normalized over total length.
Create a new wire by keeping only the section between start and end.
Args: Args:
start (float): 0.0 <= start < 1.0 start (float): normalized start position (0.0 to <1.0)
end (float): 0.0 < end <= 1.0 end (float): normalized end position (>0.0 to 1.0)
Raises:
ValueError: start >= end
Returns: Returns:
Wire: trimmed wire Wire: trimmed Wire
""" """
# pylint: disable=too-many-branches
if start >= end: if start >= end:
raise ValueError("start must be less than end") raise ValueError("start must be less than end")
edges = self.edges() # Extract the edges in order
ordered_edges = self.edges().sort_by(self)
# If this is really just an edge, skip the complexity of a Wire # If this is really just an edge, skip the complexity of a Wire
if len(edges) == 1: if len(ordered_edges) == 1:
return Wire([edges[0].trim(start, end)]) return Wire([ordered_edges[0].trim(start, end)])
# For each Edge determine the beginning and end wire parameters total_length = self.length
# Note that u, v values are parameters along the Wire start_len = start * total_length
edges_uv_values: list[tuple[float, float, Edge]] = [] end_len = end * total_length
found_end_of_wire = False # for finding ends of closed wires
for edge in edges:
u = self.param_at_point(edge.position_at(0))
v = self.param_at_point(edge.position_at(1))
if self.is_closed: # Avoid two beginnings or ends
u = (
1 - u
if found_end_of_wire and (isclose_b(u, 0) or isclose_b(u, 1))
else u
)
v = (
1 - v
if found_end_of_wire and (isclose_b(v, 0) or isclose_b(v, 1))
else v
)
found_end_of_wire = (
isclose_b(u, 0)
or isclose_b(u, 1)
or isclose_b(v, 0)
or isclose_b(v, 1)
or found_end_of_wire
)
# Edge might be reversed and require flipping parms
u, v = (v, u) if u > v else (u, v)
edges_uv_values.append((u, v, edge))
trimmed_edges = [] trimmed_edges = []
for u, v, edge in edges_uv_values: cur_length = 0.0
if v < start or u > end: # Edge not needed
continue
if start <= u and v <= end: # keep whole Edge for edge in ordered_edges:
trimmed_edges.append(edge) edge_len = edge.length
edge_start = cur_length
edge_end = cur_length + edge_len
cur_length = edge_end
elif start >= u and end <= v: # Wire trimmed to single Edge if edge_end <= start_len or edge_start >= end_len:
u_edge = edge.param_at_point(self.position_at(start)) continue # skip
v_edge = edge.param_at_point(self.position_at(end))
u_edge, v_edge = (
(v_edge, u_edge) if u_edge > v_edge else (u_edge, v_edge)
)
trimmed_edges.append(edge.trim(u_edge, v_edge))
elif start <= u: # keep start of Edge if edge_start >= start_len and edge_end <= end_len:
u_edge = edge.param_at_point(self.position_at(end)) trimmed_edges.append(edge) # keep whole Edge
if u_edge != 0: else:
trimmed_edges.append(edge.trim(0, u_edge)) # Normalize trim points relative to this edge
trim_start_len = max(start_len, edge_start)
trim_end_len = min(end_len, edge_end)
else: # v <= end keep end of Edge u0 = (trim_start_len - edge_start) / edge_len
v_edge = edge.param_at_point(self.position_at(start)) u1 = (trim_end_len - edge_start) / edge_len
if v_edge != 1:
trimmed_edges.append(edge.trim(v_edge, 1)) if abs(u1 - u0) > TOLERANCE:
trimmed_edges.append(edge.trim(u0, u1))
return Wire(trimmed_edges) return Wire(trimmed_edges)

View file

@ -50,24 +50,27 @@ import copy
import itertools import itertools
import warnings import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Callable, Iterable, Iterator
from functools import reduce
from typing import ( from typing import (
cast as tcast, TYPE_CHECKING,
Any, Any,
Generic, Generic,
Literal,
Optional, Optional,
Protocol, Protocol,
SupportsIndex, SupportsIndex,
TypeVar, TypeVar,
Union, Union,
overload,
TYPE_CHECKING,
) )
from typing import cast as tcast
from collections.abc import Callable, Iterable, Iterator from typing import overload
import OCP.GeomAbs as ga import OCP.GeomAbs as ga
import OCP.TopAbs as ta import OCP.TopAbs as ta
from IPython.lib.pretty import pretty, RepresentationPrinter from anytree import NodeMixin, RenderTree
from IPython.lib.pretty import RepresentationPrinter, pretty
from OCP.Bnd import Bnd_Box, Bnd_OBB
from OCP.BOPAlgo import BOPAlgo_GlueEnum from OCP.BOPAlgo import BOPAlgo_GlueEnum
from OCP.BRep import BRep_Tool from OCP.BRep import BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
@ -98,11 +101,12 @@ from OCP.BRepGProp import BRepGProp, BRepGProp_Face
from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.BRepTools import BRepTools from OCP.BRepTools import BRepTools
from OCP.Bnd import Bnd_Box, Bnd_OBB from OCP.gce import gce_MakeLin
from OCP.GProp import GProp_GProps
from OCP.Geom import Geom_Line from OCP.Geom import Geom_Line
from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf
from OCP.GeomLib import GeomLib_IsPlanarSurface from OCP.GeomLib import GeomLib_IsPlanarSurface
from OCP.gp import gp_Ax1, gp_Ax2, gp_Ax3, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
from OCP.GProp import GProp_GProps
from OCP.ShapeAnalysis import ShapeAnalysis_Curve from OCP.ShapeAnalysis import ShapeAnalysis_Curve
from OCP.ShapeCustom import ShapeCustom, ShapeCustom_RestrictionParameters from OCP.ShapeCustom import ShapeCustom, ShapeCustom_RestrictionParameters
from OCP.ShapeFix import ShapeFix_Shape from OCP.ShapeFix import ShapeFix_Shape
@ -110,26 +114,25 @@ from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
from OCP.TopAbs import TopAbs_Orientation, TopAbs_ShapeEnum from OCP.TopAbs import TopAbs_Orientation, TopAbs_ShapeEnum
from OCP.TopExp import TopExp, TopExp_Explorer from OCP.TopExp import TopExp, TopExp_Explorer
from OCP.TopLoc import TopLoc_Location from OCP.TopLoc import TopLoc_Location
from OCP.TopTools import (
TopTools_IndexedDataMapOfShapeListOfShape,
TopTools_ListOfShape,
TopTools_SequenceOfShape,
)
from OCP.TopoDS import ( from OCP.TopoDS import (
TopoDS, TopoDS,
TopoDS_Compound, TopoDS_Compound,
TopoDS_Edge,
TopoDS_Face, TopoDS_Face,
TopoDS_Iterator, TopoDS_Iterator,
TopoDS_Shape, TopoDS_Shape,
TopoDS_Shell, TopoDS_Shell,
TopoDS_Solid, TopoDS_Solid,
TopoDS_Vertex, TopoDS_Vertex,
TopoDS_Edge,
TopoDS_Wire, TopoDS_Wire,
) )
from OCP.gce import gce_MakeLin from OCP.TopTools import (
from OCP.gp import gp_Ax1, gp_Ax2, gp_Ax3, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec TopTools_IndexedDataMapOfShapeListOfShape,
from anytree import NodeMixin, RenderTree TopTools_ListOfShape,
TopTools_SequenceOfShape,
)
from typing_extensions import Self
from build123d.build_enums import CenterOf, GeomType, Keep, SortBy, Transition from build123d.build_enums import CenterOf, GeomType, Keep, SortBy, Transition
from build123d.geometry import ( from build123d.geometry import (
DEG2RAD, DEG2RAD,
@ -145,19 +148,16 @@ from build123d.geometry import (
VectorLike, VectorLike,
logger, logger,
) )
from typing_extensions import Self
from typing import Literal
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from .zero_d import Vertex # pylint: disable=R0801
from .one_d import Edge, Wire # pylint: disable=R0801
from .two_d import Face, Shell # pylint: disable=R0801
from .three_d import Solid # pylint: disable=R0801
from .composite import Compound # pylint: disable=R0801
from build123d.build_part import BuildPart # pylint: disable=R0801 from build123d.build_part import BuildPart # pylint: disable=R0801
from .composite import Compound # pylint: disable=R0801
from .one_d import Edge, Wire # pylint: disable=R0801
from .three_d import Solid # pylint: disable=R0801
from .two_d import Face, Shell # pylint: disable=R0801
from .zero_d import Vertex # pylint: disable=R0801
Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"] Shapes = Literal["Vertex", "Edge", "Wire", "Face", "Shell", "Solid", "Compound"]
TrimmingTool = Union[Plane, "Shell", "Face"] TrimmingTool = Union[Plane, "Shell", "Face"]
TOPODS = TypeVar("TOPODS", bound=TopoDS_Shape) TOPODS = TypeVar("TOPODS", bound=TopoDS_Shape)
@ -422,6 +422,14 @@ class Shape(NodeMixin, Generic[TOPODS]):
return True return True
@property
def is_null(self) -> bool:
"""Returns true if this shape is null. In other words, it references no
underlying shape with the potential to be given a location and an
orientation.
"""
return self.wrapped is None or self.wrapped.IsNull()
@property @property
def is_planar_face(self) -> bool: def is_planar_face(self) -> bool:
"""Is the shape a planar face even though its geom_type may not be PLANE""" """Is the shape a planar face even though its geom_type may not be PLANE"""
@ -431,6 +439,35 @@ class Shape(NodeMixin, Generic[TOPODS]):
is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE) is_face_planar = GeomLib_IsPlanarSurface(surface, TOLERANCE)
return is_face_planar.IsPlanar() return is_face_planar.IsPlanar()
@property
def is_valid(self) -> bool:
"""Returns True if no defect is detected on the shape S or any of its
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
description of what is checked.
"""
if self.wrapped is None:
return True
chk = BRepCheck_Analyzer(self.wrapped)
chk.SetParallel(True)
return chk.IsValid()
@property
def global_location(self) -> Location:
"""
The location of this Shape relative to the global coordinate system.
This property computes the composite transformation by traversing the
hierarchy from the root of the assembly to this node, combining the
location of each ancestor. It reflects the absolute position and
orientation of the shape in world space, even when the shape is deeply
nested within an assembly.
Note:
This is only meaningful when the Shape is part of an assembly tree
where parent-child relationships define relative placements.
"""
return reduce(lambda loc, n: loc * n.location, self.path, Location())
@property @property
def location(self) -> Location | None: def location(self) -> Location | None:
"""Get this Shape's Location""" """Get this Shape's Location"""
@ -543,6 +580,11 @@ class Shape(NodeMixin, Generic[TOPODS]):
(Vector(principal_props.ThirdAxisOfInertia()), principal_moments[2]), (Vector(principal_props.ThirdAxisOfInertia()), principal_moments[2]),
] ]
@property
def shape_type(self) -> Shapes:
"""Return the shape type string for this class"""
return tcast(Shapes, Shape.shape_LUT[shapetype(self.wrapped)])
@property @property
def static_moments(self) -> tuple[float, float, float]: def static_moments(self) -> tuple[float, float, float]:
""" """
@ -660,15 +702,15 @@ class Shape(NodeMixin, Generic[TOPODS]):
address = node.address address = node.address
name = "" name = ""
loc = ( loc = (
"Center" + str(node.position.to_tuple()) "Center" + str(tuple(node.position))
if show_center if show_center
else "Position" + str(node.position.to_tuple()) else "Position" + str(tuple(node.position))
) )
else: else:
address = id(node) address = id(node)
name = node.__class__.__name__.ljust(9) name = node.__class__.__name__.ljust(9)
loc = ( loc = (
"Center" + str(node.center().to_tuple()) "Center" + str(tuple(node.center()))
if show_center if show_center
else "Location" + repr(node.location) else "Location" + repr(node.location)
) )
@ -758,7 +800,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
[shape.__class__.cast(i) for i in shape.entities(entity_type)] [shape.__class__.cast(i) for i in shape.entities(entity_type)]
) )
for item in shape_list: for item in shape_list:
item.topo_parent = shape item.topo_parent = shape if shape.topo_parent is None else shape.topo_parent
return shape_list return shape_list
@staticmethod @staticmethod
@ -1188,7 +1230,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
"""fix - try to fix shape if not valid""" """fix - try to fix shape if not valid"""
if self.wrapped is None: if self.wrapped is None:
return self return self
if not self.is_valid(): if not self.is_valid:
shape_copy: Shape = copy.deepcopy(self, None) shape_copy: Shape = copy.deepcopy(self, None)
shape_copy.wrapped = tcast(TOPODS, fix(self.wrapped)) shape_copy.wrapped = tcast(TOPODS, fix(self.wrapped))
@ -1332,7 +1374,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
return None return None
if ( if (
not isinstance(shape_intersections, ShapeList) not isinstance(shape_intersections, ShapeList)
and shape_intersections.is_null() and shape_intersections.is_null
): ):
return None return None
return shape_intersections return shape_intersections
@ -1352,18 +1394,6 @@ class Shape(NodeMixin, Generic[TOPODS]):
return False return False
return self.wrapped.IsEqual(other.wrapped) return self.wrapped.IsEqual(other.wrapped)
def is_null(self) -> bool:
"""Returns true if this shape is null. In other words, it references no
underlying shape with the potential to be given a location and an
orientation.
Args:
Returns:
"""
return self.wrapped is None or self.wrapped.IsNull()
def is_same(self, other: Shape) -> bool: def is_same(self, other: Shape) -> bool:
"""Returns True if other and this shape are same, i.e. if they share the """Returns True if other and this shape are same, i.e. if they share the
same TShape with the same Locations. Orientations may differ. Also see same TShape with the same Locations. Orientations may differ. Also see
@ -1379,22 +1409,6 @@ class Shape(NodeMixin, Generic[TOPODS]):
return False return False
return self.wrapped.IsSame(other.wrapped) return self.wrapped.IsSame(other.wrapped)
def is_valid(self) -> bool:
"""Returns True if no defect is detected on the shape S or any of its
subshapes. See the OCCT docs on BRepCheck_Analyzer::IsValid for a full
description of what is checked.
Args:
Returns:
"""
if self.wrapped is None:
return True
chk = BRepCheck_Analyzer(self.wrapped)
chk.SetParallel(True)
return chk.IsValid()
def locate(self, loc: Location) -> Self: def locate(self, loc: Location) -> Self:
"""Apply a location in absolute sense to self """Apply a location in absolute sense to self
@ -1626,6 +1640,12 @@ class Shape(NodeMixin, Generic[TOPODS]):
Args: Args:
loc (Location): new location to set for self loc (Location): new location to set for self
""" """
warnings.warn(
"The 'relocate' method is deprecated and will be removed in a future version."
"Use move, moved, locate, or located instead",
DeprecationWarning,
stacklevel=2,
)
if self.wrapped is None: if self.wrapped is None:
raise ValueError("Cannot relocate an empty shape") raise ValueError("Cannot relocate an empty shape")
if loc.wrapped is None: if loc.wrapped is None:
@ -1677,10 +1697,6 @@ class Shape(NodeMixin, Generic[TOPODS]):
return self._apply_transform(transformation) return self._apply_transform(transformation)
def shape_type(self) -> Shapes:
"""Return the shape type string for this class"""
return tcast(Shapes, Shape.shape_LUT[shapetype(self.wrapped)])
def shell(self) -> Shell | None: def shell(self) -> Shell | None:
"""Return the Shell""" """Return the Shell"""
return None return None
@ -1916,7 +1932,9 @@ class Shape(NodeMixin, Generic[TOPODS]):
) -> Self: ) -> Self:
"""to_splines """to_splines
Approximate shape with b-splines of the specified degree. A shape-processing utility that forces all geometry in a shape to be converted into
BSplines. It's useful when working with tools or export formats that require uniform
geometry, or for downstream processing that only understands BSpline representations.
Args: Args:
degree (int, optional): Maximum degree. Defaults to 3. degree (int, optional): Maximum degree. Defaults to 3.
@ -2144,7 +2162,7 @@ class Shape(NodeMixin, Generic[TOPODS]):
def _ocp_section( def _ocp_section(
self: Shape, other: Vertex | Edge | Wire | Face self: Shape, other: Vertex | Edge | Wire | Face
) -> tuple[list[Vertex], list[Edge]]: ) -> tuple[ShapeList[Vertex], ShapeList[Edge]]:
"""_ocp_section """_ocp_section
Create a BRepAlgoAPI_Section object Create a BRepAlgoAPI_Section object
@ -2162,38 +2180,34 @@ class Shape(NodeMixin, Generic[TOPODS]):
other (Union[Vertex, Edge, Wire, Face]): shape to section with other (Union[Vertex, Edge, Wire, Face]): shape to section with
Returns: Returns:
tuple[list[Vertex], list[Edge]]: section results tuple[ShapeList[Vertex], ShapeList[Edge]]: section results
""" """
if self.wrapped is None or other.wrapped is None: if self.wrapped is None or other.wrapped is None:
return ([], []) return (ShapeList(), ShapeList())
try: section = BRepAlgoAPI_Section(self.wrapped, other.wrapped)
section = BRepAlgoAPI_Section(other.geom_adaptor(), self.wrapped) section.SetRunParallel(True)
except (TypeError, AttributeError): section.Approximation(True)
try: section.ComputePCurveOn1(True)
section = BRepAlgoAPI_Section(self.geom_adaptor(), other.wrapped) section.ComputePCurveOn2(True)
except (TypeError, AttributeError):
return ([], [])
# Perform the intersection calculation
section.Build() section.Build()
# Get the resulting shapes from the intersection # Get the resulting shapes from the intersection
intersection_shape = section.Shape() intersection_shape: TopoDS_Shape = section.Shape()
vertices = [] vertices: list[Vertex] = []
# Iterate through the intersection shape to find intersection points/edges # Iterate through the intersection shape to find intersection points/edges
explorer = TopExp_Explorer(intersection_shape, TopAbs_ShapeEnum.TopAbs_VERTEX) explorer = TopExp_Explorer(intersection_shape, TopAbs_ShapeEnum.TopAbs_VERTEX)
while explorer.More(): while explorer.More():
vertices.append(self.__class__.cast(downcast(explorer.Current()))) vertices.append(self.__class__.cast(downcast(explorer.Current())))
explorer.Next() explorer.Next()
edges = [] edges: ShapeList[Edge] = ShapeList()
explorer = TopExp_Explorer(intersection_shape, TopAbs_ShapeEnum.TopAbs_EDGE) explorer = TopExp_Explorer(intersection_shape, TopAbs_ShapeEnum.TopAbs_EDGE)
while explorer.More(): while explorer.More():
edges.append(self.__class__.cast(downcast(explorer.Current()))) edges.append(self.__class__.cast(downcast(explorer.Current())))
explorer.Next() explorer.Next()
return (vertices, edges) return (ShapeList(set(vertices)), edges)
def _repr_html_(self): def _repr_html_(self):
"""Jupyter 3D representation support""" """Jupyter 3D representation support"""
@ -2326,10 +2340,27 @@ class ShapeList(list[T]):
# ---- Instance Methods ---- # ---- Instance Methods ----
def __add__(self, other: ShapeList) -> ShapeList[T]: # type: ignore def __add__(self, other: Shape | Iterable[Shape]) -> ShapeList[T]: # type: ignore
"""Combine two ShapeLists together operator +""" """Return a new ShapeList that includes other"""
# return ShapeList(itertools.chain(self, other)) # breaks MacOS-13 if isinstance(other, (Vector, Shape)):
return ShapeList(list(self) + list(other)) return ShapeList(tcast(list[T], list(self) + [other]))
if isinstance(other, Iterable) and all(
isinstance(o, (Shape, Vector)) for o in other
):
return ShapeList(list(self) + list(other))
raise TypeError(f"Cannot add object of type {type(other)} to ShapeList")
def __iadd__(self, other: Shape | Iterable[Shape]) -> Self: # type: ignore
"""In-place addition to this ShapeList"""
if isinstance(other, (Vector, Shape)):
self.append(tcast(T, other))
elif isinstance(other, Iterable) and all(
isinstance(o, (Shape, Vector)) for o in other
):
self.extend(other)
else:
raise TypeError(f"Cannot add object of type {type(other)} to ShapeList")
return self
def __and__(self, other: ShapeList) -> ShapeList[T]: def __and__(self, other: ShapeList) -> ShapeList[T]:
"""Intersect two ShapeLists operator &""" """Intersect two ShapeLists operator &"""
@ -2582,29 +2613,27 @@ class ShapeList(list[T]):
if inclusive == (True, True): if inclusive == (True, True):
objects = filter( objects = filter(
lambda o: minimum lambda o: minimum
<= axis.to_plane().to_local_coords(o).center().Z <= Plane(axis).to_local_coords(o).center().Z
<= maximum, <= maximum,
self, self,
) )
elif inclusive == (True, False): elif inclusive == (True, False):
objects = filter( objects = filter(
lambda o: minimum lambda o: minimum
<= axis.to_plane().to_local_coords(o).center().Z <= Plane(axis).to_local_coords(o).center().Z
< maximum, < maximum,
self, self,
) )
elif inclusive == (False, True): elif inclusive == (False, True):
objects = filter( objects = filter(
lambda o: minimum lambda o: minimum
< axis.to_plane().to_local_coords(o).center().Z < Plane(axis).to_local_coords(o).center().Z
<= maximum, <= maximum,
self, self,
) )
elif inclusive == (False, False): elif inclusive == (False, False):
objects = filter( objects = filter(
lambda o: minimum lambda o: minimum < Plane(axis).to_local_coords(o).center().Z < maximum,
< axis.to_plane().to_local_coords(o).center().Z
< maximum,
self, self,
) )

View file

@ -68,7 +68,11 @@ from OCP.BRepClass3d import BRepClass3d_SolidClassifier
from OCP.BRepFeat import BRepFeat_MakeDPrism from OCP.BRepFeat import BRepFeat_MakeDPrism
from OCP.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet from OCP.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet
from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin from OCP.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakePipeShell, BRepOffsetAPI_MakeThickSolid from OCP.BRepOffsetAPI import (
BRepOffsetAPI_DraftAngle,
BRepOffsetAPI_MakePipeShell,
BRepOffsetAPI_MakeThickSolid,
)
from OCP.BRepPrimAPI import ( from OCP.BRepPrimAPI import (
BRepPrimAPI_MakeBox, BRepPrimAPI_MakeBox,
BRepPrimAPI_MakeCone, BRepPrimAPI_MakeCone,
@ -88,7 +92,7 @@ from OCP.TopExp import TopExp
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Solid, TopoDS_Wire from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Solid, TopoDS_Wire
from OCP.gp import gp_Ax2, gp_Pnt from OCP.gp import gp_Ax2, gp_Pnt
from build123d.build_enums import CenterOf, Kind, Transition, Until from build123d.build_enums import CenterOf, GeomType, Kind, Transition, Until
from build123d.geometry import ( from build123d.geometry import (
DEG2RAD, DEG2RAD,
Axis, Axis,
@ -255,7 +259,7 @@ class Mixin3D(Shape):
try: try:
new_shape = self.__class__(chamfer_builder.Shape()) new_shape = self.__class__(chamfer_builder.Shape())
if not new_shape.is_valid(): if not new_shape.is_valid:
raise Standard_Failure raise Standard_Failure
except (StdFail_NotDone, Standard_Failure) as err: except (StdFail_NotDone, Standard_Failure) as err:
raise ValueError( raise ValueError(
@ -339,7 +343,7 @@ class Mixin3D(Shape):
try: try:
new_shape = self.__class__(fillet_builder.Shape()) new_shape = self.__class__(fillet_builder.Shape())
if not new_shape.is_valid(): if not new_shape.is_valid:
raise Standard_Failure raise Standard_Failure
except (StdFail_NotDone, Standard_Failure) as err: except (StdFail_NotDone, Standard_Failure) as err:
raise ValueError( raise ValueError(
@ -431,7 +435,7 @@ class Mixin3D(Shape):
""" """
solid_classifier = BRepClass3d_SolidClassifier(self.wrapped) solid_classifier = BRepClass3d_SolidClassifier(self.wrapped)
solid_classifier.Perform(gp_Pnt(*Vector(point).to_tuple()), tolerance) solid_classifier.Perform(gp_Pnt(*Vector(point)), tolerance)
return solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace() return solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace()
@ -481,7 +485,7 @@ class Mixin3D(Shape):
# Do these numbers work? - if not try with the smaller window # Do these numbers work? - if not try with the smaller window
try: try:
new_shape = self.__class__(fillet_builder.Shape()) new_shape = self.__class__(fillet_builder.Shape())
if not new_shape.is_valid(): if not new_shape.is_valid:
raise fillet_exception raise fillet_exception
except fillet_exception: except fillet_exception:
return __max_fillet(window_min, window_mid, current_iteration + 1) return __max_fillet(window_min, window_mid, current_iteration + 1)
@ -495,7 +499,7 @@ class Mixin3D(Shape):
) )
return return_value return return_value
if not self.is_valid(): if not self.is_valid:
raise ValueError("Invalid Shape") raise ValueError("Invalid Shape")
native_edges = [e.wrapped for e in edge_list] native_edges = [e.wrapped for e in edge_list]
@ -661,7 +665,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
builder.SetMode(coordinate_system) builder.SetMode(coordinate_system)
rotate = True rotate = True
elif isinstance(binormal, (Wire, Edge)): elif isinstance(binormal, (Wire, Edge)):
builder.SetMode(binormal.to_wire().wrapped, True) builder.SetMode(Wire(binormal).wrapped, True)
return rotate return rotate
@ -1229,6 +1233,21 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Sweep the given cross section into a prismatic solid along the provided path Sweep the given cross section into a prismatic solid along the provided path
The is_frenet parameter controls how the profile orientation changes as it
follows along the sweep path. If is_frenet is False, the orientation of the
profile is kept consistent from point to point. The resulting shape has the
minimum possible twisting. Unintuitively, when a profile is swept along a
helix, this results in the orientation of the profile slowly creeping
(rotating) as it follows the helix. Setting is_frenet to True prevents this.
If is_frenet is True the orientation of the profile is based on the local
curvature and tangency vectors of the path. This keeps the orientation of the
profile consistent when sweeping along a helix (because the curvature vector of
a straight helix always points to its axis). However, when path is not a helix,
the resulting shape can have strange looking twists sometimes. For more
information, see Frenet Serret formulas
http://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas.
Args: Args:
section (Union[Face, Wire]): cross section to sweep section (Union[Face, Wire]): cross section to sweep
path (Union[Wire, Edge]): sweep path path (Union[Wire, Edge]): sweep path
@ -1252,7 +1271,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
shapes = [] shapes = []
for wire in [outer_wire] + inner_wires: for wire in [outer_wire] + inner_wires:
builder = BRepOffsetAPI_MakePipeShell(path.to_wire().wrapped) builder = BRepOffsetAPI_MakePipeShell(Wire(path).wrapped)
rotate = False rotate = False
@ -1294,6 +1313,21 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Sweep through a sequence of profiles following a path. Sweep through a sequence of profiles following a path.
The is_frenet parameter controls how the profile orientation changes as it
follows along the sweep path. If is_frenet is False, the orientation of the
profile is kept consistent from point to point. The resulting shape has the
minimum possible twisting. Unintuitively, when a profile is swept along a
helix, this results in the orientation of the profile slowly creeping
(rotating) as it follows the helix. Setting is_frenet to True prevents this.
If is_frenet is True the orientation of the profile is based on the local
curvature and tangency vectors of the path. This keeps the orientation of the
profile consistent when sweeping along a helix (because the curvature vector of
a straight helix always points to its axis). However, when path is not a helix,
the resulting shape can have strange looking twists sometimes. For more
information, see Frenet Serret formulas
http://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas.
Args: Args:
profiles (Iterable[Union[Wire, Face]]): list of profiles profiles (Iterable[Union[Wire, Face]]): list of profiles
path (Union[Wire, Edge]): The wire to sweep the face resulting from the wires over path (Union[Wire, Edge]): The wire to sweep the face resulting from the wires over
@ -1305,7 +1339,7 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
Returns: Returns:
Solid: swept object Solid: swept object
""" """
path_as_wire = path.to_wire().wrapped path_as_wire = Wire(path).wrapped
builder = BRepOffsetAPI_MakePipeShell(path_as_wire) builder = BRepOffsetAPI_MakePipeShell(path_as_wire)
@ -1391,3 +1425,62 @@ class Solid(Mixin3D, Shape[TopoDS_Solid]):
raise RuntimeError("Error applying thicken to given surface") from err raise RuntimeError("Error applying thicken to given surface") from err
return result return result
def draft(self, faces: Iterable[Face], neutral_plane: Plane, angle: float) -> Solid:
"""Apply a draft angle to the given faces of the solid.
Args:
faces: Faces to which the draft should be applied.
neutral_plane: Plane defining the neutral direction and position.
angle: Draft angle in degrees.
Returns:
Solid with the specified draft angles applied.
Raises:
RuntimeError: If draft application fails on any face or during build.
"""
valid_geom_types = {GeomType.PLANE, GeomType.CYLINDER, GeomType.CONE}
for face in faces:
if face.geom_type not in valid_geom_types:
raise ValueError(
f"Face {face} has unsupported geometry type {face.geom_type.name}. "
"Only PLANAR, CYLINDRICAL, and CONICAL faces are supported."
)
draft_angle_builder = BRepOffsetAPI_DraftAngle(self.wrapped)
for face in faces:
draft_angle_builder.Add(
face.wrapped,
neutral_plane.z_dir.to_dir(),
radians(angle),
neutral_plane.wrapped,
Flag=True,
)
if not draft_angle_builder.AddDone():
raise DraftAngleError(
"Draft could not be added to a face.",
face=face,
problematic_shape=draft_angle_builder.ProblematicShape(),
)
try:
draft_angle_builder.Build()
result = Solid(draft_angle_builder.Shape())
except StdFail_NotDone as err:
raise DraftAngleError(
"Draft build failed on the given solid.",
face=None,
problematic_shape=draft_angle_builder.ProblematicShape(),
) from err
return result
class DraftAngleError(RuntimeError):
"""Solid.draft custom exception"""
def __init__(self, message, face=None, problematic_shape=None):
super().__init__(message)
self.face = face
self.problematic_shape = problematic_shape

View file

@ -58,18 +58,18 @@ from __future__ import annotations
import copy import copy
import sys import sys
import warnings import warnings
from typing import Any, overload, TypeVar, TYPE_CHECKING from abc import ABC, abstractmethod
from collections.abc import Iterable, Sequence from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Any, TypeVar, overload
import OCP.TopAbs as ta import OCP.TopAbs as ta
from OCP.BRep import BRep_Tool, BRep_Builder from OCP.BRep import BRep_Builder, BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Surface from OCP.BRepAdaptor import BRepAdaptor_Surface
from OCP.BRepAlgo import BRepAlgo from OCP.BRepAlgo import BRepAlgo
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
from OCP.BRepBuilderAPI import ( from OCP.BRepBuilderAPI import (
BRepBuilderAPI_MakeFace,
BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeFace,
BRepBuilderAPI_MakeWire, BRepBuilderAPI_MakeWire,
) )
from OCP.BRepClass3d import BRepClass3d_SolidClassifier from OCP.BRepClass3d import BRepClass3d_SolidClassifier
@ -80,30 +80,31 @@ from OCP.BRepIntCurveSurface import BRepIntCurveSurface_Inter
from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling, BRepOffsetAPI_MakePipeShell from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeFilling, BRepOffsetAPI_MakePipeShell
from OCP.BRepPrimAPI import BRepPrimAPI_MakeRevol from OCP.BRepPrimAPI import BRepPrimAPI_MakeRevol
from OCP.BRepTools import BRepTools, BRepTools_ReShape from OCP.BRepTools import BRepTools, BRepTools_ReShape
from OCP.GProp import GProp_GProps from OCP.gce import gce_MakeLin
from OCP.Geom import Geom_BezierSurface, Geom_Surface, Geom_RectangularTrimmedSurface from OCP.Geom import Geom_BezierSurface, Geom_RectangularTrimmedSurface, Geom_Surface
from OCP.GeomAbs import GeomAbs_C0
from OCP.GeomAPI import ( from OCP.GeomAPI import (
GeomAPI_ExtremaCurveCurve, GeomAPI_ExtremaCurveCurve,
GeomAPI_PointsToBSplineSurface, GeomAPI_PointsToBSplineSurface,
GeomAPI_ProjectPointOnSurf, GeomAPI_ProjectPointOnSurf,
) )
from OCP.GeomAbs import GeomAbs_C0
from OCP.GeomProjLib import GeomProjLib from OCP.GeomProjLib import GeomProjLib
from OCP.gp import gp_Pnt, gp_Vec
from OCP.GProp import GProp_GProps
from OCP.Precision import Precision from OCP.Precision import Precision
from OCP.ShapeFix import ShapeFix_Solid, ShapeFix_Wire from OCP.ShapeFix import ShapeFix_Solid, ShapeFix_Wire
from OCP.Standard import ( from OCP.Standard import (
Standard_ConstructionError,
Standard_Failure, Standard_Failure,
Standard_NoSuchObject, Standard_NoSuchObject,
Standard_ConstructionError,
) )
from OCP.StdFail import StdFail_NotDone from OCP.StdFail import StdFail_NotDone
from OCP.TColStd import TColStd_HArray2OfReal
from OCP.TColgp import TColgp_HArray2OfPnt from OCP.TColgp import TColgp_HArray2OfPnt
from OCP.TColStd import TColStd_HArray2OfReal
from OCP.TopExp import TopExp from OCP.TopExp import TopExp
from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Shell, TopoDS_Solid from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape, TopoDS_Shell, TopoDS_Solid
from OCP.gce import gce_MakeLin from OCP.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_ListOfShape
from OCP.gp import gp_Pnt, gp_Vec from typing_extensions import Self
from build123d.build_enums import CenterOf, GeomType, Keep, SortBy, Transition from build123d.build_enums import CenterOf, GeomType, Keep, SortBy, Transition
from build123d.geometry import ( from build123d.geometry import (
@ -117,38 +118,36 @@ from build123d.geometry import (
Vector, Vector,
VectorLike, VectorLike,
) )
from typing_extensions import Self
from .one_d import Mixin1D, Edge, Wire from .one_d import Edge, Mixin1D, Wire
from .shape_core import ( from .shape_core import (
Shape, Shape,
ShapeList, ShapeList,
SkipClean, SkipClean,
downcast,
get_top_level_topods_shapes,
_sew_topods_faces, _sew_topods_faces,
shapetype,
_topods_entities, _topods_entities,
_topods_face_normal_at, _topods_face_normal_at,
downcast,
get_top_level_topods_shapes,
shapetype,
) )
from .utils import ( from .utils import (
_extrude_topods_shape, _extrude_topods_shape,
find_max_dimension,
_make_loft, _make_loft,
_make_topods_face_from_wires, _make_topods_face_from_wires,
_topods_bool_op, _topods_bool_op,
find_max_dimension,
) )
from .zero_d import Vertex from .zero_d import Vertex
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from .three_d import Solid # pylint: disable=R0801
from .composite import Compound, Curve # pylint: disable=R0801 from .composite import Compound, Curve # pylint: disable=R0801
from .three_d import Solid # pylint: disable=R0801
T = TypeVar("T", Edge, Wire, "Face") T = TypeVar("T", Edge, Wire, "Face")
class Mixin2D(Shape): class Mixin2D(ABC, Shape):
"""Additional methods to add to Face and Shell class""" """Additional methods to add to Face and Shell class"""
project_to_viewport = Mixin1D.project_to_viewport project_to_viewport = Mixin1D.project_to_viewport
@ -201,6 +200,9 @@ class Mixin2D(Shape):
new_surface = copy.deepcopy(self) new_surface = copy.deepcopy(self)
new_surface.wrapped = downcast(self.wrapped.Complemented()) new_surface.wrapped = downcast(self.wrapped.Complemented())
# As the surface has been modified, the parent is no longer valid
new_surface.topo_parent = None
return new_surface return new_surface
def face(self) -> Face | None: def face(self) -> Face | None:
@ -235,7 +237,7 @@ class Mixin2D(Shape):
while intersect_maker.More(): while intersect_maker.More():
inter_pt = intersect_maker.Pnt() inter_pt = intersect_maker.Pnt()
# Calculate distance along axis # Calculate distance along axis
distance = other.to_plane().to_local_coords(Vector(inter_pt)).Z distance = Plane(other).to_local_coords(Vector(inter_pt)).Z
intersections.append( intersections.append(
( (
intersect_maker.Face(), # TopoDS_Face intersect_maker.Face(), # TopoDS_Face
@ -258,6 +260,11 @@ class Mixin2D(Shape):
return result return result
@abstractmethod
def location_at(self, *args: Any, **kwargs: Any) -> Location:
"""A location from a face or shell"""
pass
def offset(self, amount: float) -> Self: def offset(self, amount: float) -> Self:
"""Return a copy of self moved along the normal by amount""" """Return a copy of self moved along the normal by amount"""
return copy.deepcopy(self).moved(Location(self.normal_at() * amount)) return copy.deepcopy(self).moved(Location(self.normal_at() * amount))
@ -292,7 +299,7 @@ class Mixin2D(Shape):
Raises: Raises:
RuntimeError: wrapping over surface boundary, try difference surface_loc RuntimeError: wrapping over surface boundary, try difference surface_loc
Returns: Returns:
Edge: wraped edge Edge: wrapped edge
""" """
def _intersect_surface_normal( def _intersect_surface_normal(
@ -325,6 +332,9 @@ class Mixin2D(Shape):
world_point, world_point - target_object_center world_point, world_point - target_object_center
) )
if self.wrapped is None:
raise ValueError("Can't wrap around an empty face")
# Initial setup # Initial setup
target_object_center = self.center(CenterOf.BOUNDING_BOX) target_object_center = self.center(CenterOf.BOUNDING_BOX)
@ -338,17 +348,26 @@ class Mixin2D(Shape):
loop_count = 0 loop_count = 0
length_error = sys.float_info.max length_error = sys.float_info.max
while length_error > tolerance and loop_count < max_loops: # Find the location on the surface to start
# Get starting point and normal if planar_edge.position_at(0).length > tolerance:
surface_origin = surface_loc.position # The start point isn't at the surface_loc so wrap a line to find it
surface_normal = surface_loc.z_axis.direction to_start_edge = Edge.make_line((0, 0), planar_edge @ 0)
wrapped_to_start_edge = self._wrap_edge(
to_start_edge, surface_loc, snap_to_face=True, tolerance=tolerance
)
start_pnt = wrapped_to_start_edge @ 1
_, start_normal = _intersect_surface_normal(
start_pnt, (start_pnt - target_object_center)
)
else:
# The start point is at the surface location
start_pnt = surface_loc.position
start_normal = surface_loc.z_axis.direction
while length_error > tolerance and loop_count < max_loops:
# Seed the wrapped path # Seed the wrapped path
wrapped_edge_points: list[VectorLike] = [] wrapped_edge_points: list[VectorLike] = []
planar_position = planar_edge.position_at(0) current_point, current_normal = start_pnt, start_normal
current_point, current_normal = _find_point_on_surface(
surface_origin, surface_normal, planar_position
)
wrapped_edge_points.append(current_point) wrapped_edge_points.append(current_point)
# Subdivide and propagate # Subdivide and propagate
@ -374,8 +393,8 @@ class Mixin2D(Shape):
raise RuntimeError( raise RuntimeError(
f"Length error of {length_error:.6f} exceeds tolerance {tolerance}" f"Length error of {length_error:.6f} exceeds tolerance {tolerance}"
) )
if not wrapped_edge.is_valid(): if wrapped_edge.wrapped is None or not wrapped_edge.is_valid:
raise RuntimeError("Wraped edge is invalid") raise RuntimeError("Wrapped edge is invalid")
if not snap_to_face: if not snap_to_face:
return wrapped_edge return wrapped_edge
@ -520,10 +539,12 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
return None return None
if self.geom_type == GeomType.CYLINDER: if self.geom_type == GeomType.CYLINDER:
return Axis(self.geom_adaptor().Cylinder().Axis()) return Axis(
self.geom_adaptor().Cylinder().Axis() # type:ignore[attr-defined]
)
if self.geom_type == GeomType.TORUS: if self.geom_type == GeomType.TORUS:
return Axis(self.geom_adaptor().Torus().Axis()) return Axis(self.geom_adaptor().Torus().Axis()) # type:ignore[attr-defined]
return None return None
@ -729,7 +750,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
axis = self.axis_of_rotation axis = self.axis_of_rotation
if axis is None or self.radii is None: if axis is None or self.radii is None:
raise ValueError("Can't find curvature of empty object") raise ValueError("Can't find curvature of empty object")
loc = Location(axis.to_plane()) loc = Location(Plane(axis))
axis_circle = Edge.make_circle(self.radii[0]).locate(loc) axis_circle = Edge.make_circle(self.radii[0]).locate(loc)
_, pnt_on_axis_circle, _ = axis_circle.distance_to_with_closest_points( _, pnt_on_axis_circle, _ = axis_circle.distance_to_with_closest_points(
self.center() self.center()
@ -781,8 +802,8 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
"""Return the major and minor radii of a torus otherwise None""" """Return the major and minor radii of a torus otherwise None"""
if self.geom_type == GeomType.TORUS: if self.geom_type == GeomType.TORUS:
return ( return (
self.geom_adaptor().MajorRadius(), self.geom_adaptor().MajorRadius(), # type:ignore[attr-defined]
self.geom_adaptor().MinorRadius(), self.geom_adaptor().MinorRadius(), # type:ignore[attr-defined]
) )
return None return None
@ -794,7 +815,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
self.geom_type in [GeomType.CYLINDER, GeomType.SPHERE] self.geom_type in [GeomType.CYLINDER, GeomType.SPHERE]
and type(self.geom_adaptor()) != Geom_RectangularTrimmedSurface and type(self.geom_adaptor()) != Geom_RectangularTrimmedSurface
): ):
return self.geom_adaptor().Radius() return self.geom_adaptor().Radius() # type:ignore[attr-defined]
else: else:
return None return None
@ -832,6 +853,8 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
Returns: Returns:
Face: extruded shape Face: extruded shape
""" """
if obj.wrapped is None:
raise ValueError("Can't extrude empty object")
return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction))) return Face(TopoDS.Face_s(_extrude_topods_shape(obj.wrapped, direction)))
@classmethod @classmethod
@ -978,11 +1001,13 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
raise ValueError("exterior must be a Wire or list of Edges") raise ValueError("exterior must be a Wire or list of Edges")
for edge in outside_edges: for edge in outside_edges:
if edge.wrapped is None:
raise ValueError("exterior contains empty edges")
surface.Add(edge.wrapped, GeomAbs_C0) surface.Add(edge.wrapped, GeomAbs_C0)
try: try:
surface.Build() surface.Build()
surface_face = Face(surface.Shape()) surface_face = Face(surface.Shape()) # type:ignore[call-overload]
except ( except (
Standard_Failure, Standard_Failure,
StdFail_NotDone, StdFail_NotDone,
@ -994,10 +1019,10 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) from err ) from err
if surface_point_vectors: if surface_point_vectors:
for point in surface_point_vectors: for point in surface_point_vectors:
surface.Add(gp_Pnt(*point.to_tuple())) surface.Add(gp_Pnt(*point))
try: try:
surface.Build() surface.Build()
surface_face = Face(surface.Shape()) surface_face = Face(surface.Shape()) # type:ignore[call-overload]
except StdFail_NotDone as err: except StdFail_NotDone as err:
raise RuntimeError( raise RuntimeError(
"Error building non-planar face with provided surface_points" "Error building non-planar face with provided surface_points"
@ -1007,6 +1032,8 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
if interior_wires: if interior_wires:
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped) makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
for wire in interior_wires: for wire in interior_wires:
if wire.wrapped is None:
raise ValueError("interior_wires contain an empty wire")
makeface_object.Add(wire.wrapped) makeface_object.Add(wire.wrapped)
try: try:
surface_face = Face(makeface_object.Face()) surface_face = Face(makeface_object.Face())
@ -1016,7 +1043,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) from err ) from err
surface_face = surface_face.fix() surface_face = surface_face.fix()
if not surface_face.is_valid(): if not surface_face.is_valid:
raise RuntimeError("non planar face is invalid") raise RuntimeError("non planar face is invalid")
return surface_face return surface_face
@ -1158,7 +1185,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
True, True,
) )
return cls(revol_builder.Shape()) return cls(revol_builder.Shape()) # type:ignore[call-overload]
@classmethod @classmethod
def sew_faces(cls, faces: Iterable[Face]) -> list[ShapeList[Face]]: def sew_faces(cls, faces: Iterable[Face]) -> list[ShapeList[Face]]:
@ -1189,7 +1216,8 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
elif isinstance(top_level_shape, TopoDS_Solid): elif isinstance(top_level_shape, TopoDS_Solid):
sewn_faces.append( sewn_faces.append(
ShapeList( ShapeList(
Face(f) for f in _topods_entities(top_level_shape, "Face") Face(f) # type:ignore[call-overload]
for f in _topods_entities(top_level_shape, "Face")
) )
) )
else: else:
@ -1236,7 +1264,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
builder.Add(profile.wrapped, False, False) builder.Add(profile.wrapped, False, False)
builder.SetTransitionMode(Shape._transModeDict[transition]) builder.SetTransitionMode(Shape._transModeDict[transition])
builder.Build() builder.Build()
result = Face(builder.Shape()) result = Face(builder.Shape()) # type:ignore[call-overload]
if SkipClean.clean: if SkipClean.clean:
result = result.clean() result = result.clean()
@ -1355,8 +1383,10 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
def inner_wires(self) -> ShapeList[Wire]: def inner_wires(self) -> ShapeList[Wire]:
"""Extract the inner or hole wires from this Face""" """Extract the inner or hole wires from this Face"""
outer = self.outer_wire() outer = self.outer_wire()
inners = [w for w in self.wires() if not w.is_same(outer)]
return ShapeList([w for w in self.wires() if not w.is_same(outer)]) for w in inners:
w.topo_parent = self if self.topo_parent is None else self.topo_parent
return ShapeList(inners)
def is_coplanar(self, plane: Plane) -> bool: def is_coplanar(self, plane: Plane) -> bool:
"""Is this planar face coplanar with the provided plane""" """Is this planar face coplanar with the provided plane"""
@ -1387,23 +1417,112 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
""" """
solid_classifier = BRepClass3d_SolidClassifier(self.wrapped) solid_classifier = BRepClass3d_SolidClassifier(self.wrapped)
solid_classifier.Perform(gp_Pnt(*Vector(point).to_tuple()), tolerance) solid_classifier.Perform(gp_Pnt(*Vector(point)), tolerance)
return solid_classifier.IsOnAFace() return solid_classifier.IsOnAFace()
# surface = BRep_Tool.Surface_s(self.wrapped) # surface = BRep_Tool.Surface_s(self.wrapped)
# projector = GeomAPI_ProjectPointOnSurf(Vector(point).to_pnt(), surface) # projector = GeomAPI_ProjectPointOnSurf(Vector(point).to_pnt(), surface)
# return projector.LowerDistance() <= TOLERANCE # return projector.LowerDistance() <= TOLERANCE
@overload
def location_at( def location_at(
self, u: float, v: float, x_dir: VectorLike | None = None self,
) -> Location: surface_point: VectorLike | None = None,
"""Location at the u/v position of face""" *,
origin = self.position_at(u, v) x_dir: VectorLike | None = None,
if x_dir is None: ) -> Location: ...
pln = Plane(origin, z_dir=self.normal_at(u, v))
@overload
def location_at(
self, u: float, v: float, *, x_dir: VectorLike | None = None
) -> Location: ...
def location_at(self, *args, **kwargs) -> Location:
"""location_at
Get the location (origin and orientation) on the surface of the face.
This method supports two overloads:
1. `location_at(u: float, v: float, *, x_dir: VectorLike | None = None) -> Location`
- Specifies the point in normalized UV parameter space of the face.
- `u` and `v` are floats between 0.0 and 1.0.
- Optionally override the local X direction using `x_dir`.
2. `location_at(surface_point: VectorLike, *, x_dir: VectorLike | None = None) -> Location`
- Projects the given 3D point onto the face surface.
- The point must be reasonably close to the face.
- Optionally override the local X direction using `x_dir`.
If no arguments are provided, the location at the center of the face
(u=0.5, v=0.5) is returned.
Args:
u (float): Normalized horizontal surface parameter (optional).
v (float): Normalized vertical surface parameter (optional).
surface_point (VectorLike): A 3D point near the surface (optional).
x_dir (VectorLike, optional): Direction for the local X axis. If not given,
the tangent in the U direction is used.
Returns:
Location: A full 3D placement at the specified point on the face surface.
Raises:
ValueError: If only one of `u` or `v` is provided or invalid keyword args are passed.
"""
surface_point, u, v = None, -1.0, -1.0
if args:
if isinstance(args[0], (Vector, Sequence)):
surface_point = args[0]
elif isinstance(args[0], (int, float)):
u = args[0]
if len(args) == 2 and isinstance(args[1], (int, float)):
v = args[1]
unknown_args = set(kwargs.keys()).difference(
{"surface_point", "u", "v", "x_dir"}
)
if unknown_args:
raise ValueError(f"Unexpected argument(s) {', '.join(unknown_args)}")
surface_point = kwargs.get("surface_point", surface_point)
u = kwargs.get("u", u)
v = kwargs.get("v", v)
user_x_dir = kwargs.get("x_dir", None)
if surface_point is None and u < 0 and v < 0:
u, v = 0.5, 0.5
elif surface_point is None and (u < 0 or v < 0):
raise ValueError("Both u & v values must be specified")
geom_surface: Geom_Surface = self.geom_adaptor()
u_min, u_max, v_min, v_max = self._uv_bounds()
if surface_point is None:
u_val = u_min + u * (u_max - u_min)
v_val = v_min + v * (v_max - v_min)
else: else:
pln = Plane(origin, x_dir=Vector(x_dir), z_dir=self.normal_at(u, v)) projector = GeomAPI_ProjectPointOnSurf(
return Location(pln) Vector(surface_point).to_pnt(), geom_surface
)
u_val, v_val = projector.LowerDistanceParameters()
# Evaluate point and partials
pnt = gp_Pnt()
du = gp_Vec()
dv = gp_Vec()
geom_surface.D1(u_val, v_val, pnt, du, dv)
origin = Vector(pnt)
z_dir = Vector(du).cross(Vector(dv)).normalized()
x_dir = (
Vector(user_x_dir).normalized()
if user_x_dir is not None
else Vector(du).normalized()
)
return Location(Plane(origin=origin, x_dir=x_dir, z_dir=z_dir))
def make_holes(self, interior_wires: list[Wire]) -> Face: def make_holes(self, interior_wires: list[Wire]) -> Face:
"""Make Holes in Face """Make Holes in Face
@ -1443,7 +1562,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
) from err ) from err
surface_face = surface_face.fix() surface_face = surface_face.fix()
# if not surface_face.is_valid(): # if not surface_face.is_valid:
# raise RuntimeError("non planar face is invalid") # raise RuntimeError("non planar face is invalid")
return surface_face return surface_face
@ -1537,7 +1656,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
def outer_wire(self) -> Wire: def outer_wire(self) -> Wire:
"""Extract the perimeter wire from this Face""" """Extract the perimeter wire from this Face"""
return Wire(BRepTools.OuterWire_s(self.wrapped)) outer = Wire(BRepTools.OuterWire_s(self.wrapped))
outer.topo_parent = self if self.topo_parent is None else self.topo_parent
return outer
def position_at(self, u: float, v: float) -> Vector: def position_at(self, u: float, v: float) -> Vector:
"""position_at """position_at
@ -1600,7 +1721,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
(extruded_topods_self,), (target_object.wrapped,), BRepAlgoAPI_Common() (extruded_topods_self,), (target_object.wrapped,), BRepAlgoAPI_Common()
) )
if not topods_shape.IsNull(): if not topods_shape.IsNull():
intersected_shapes.append(Face(topods_shape)) intersected_shapes.append(
Face(topods_shape) # type:ignore[call-overload]
)
else: else:
for target_shell in target_object.shells(): for target_shell in target_object.shells():
topods_shape = _topods_bool_op( topods_shape = _topods_bool_op(
@ -1627,12 +1750,21 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
Approximate planar face with arcs and straight line segments. Approximate planar face with arcs and straight line segments.
This is a utility used internally to convert or adapt a face for Boolean operations. Its
purpose is not typically for general use, but rather as a helper within the Boolean kernel
to ensure input faces are in a compatible and canonical form.
Args: Args:
tolerance (float, optional): Approximation tolerance. Defaults to 1e-3. tolerance (float, optional): Approximation tolerance. Defaults to 1e-3.
Returns: Returns:
Face: approximated face Face: approximated face
""" """
warnings.warn(
"The 'to_arcs' method is deprecated and will be removed in a future version.",
DeprecationWarning,
stacklevel=2,
)
if self.wrapped is None: if self.wrapped is None:
raise ValueError("Cannot approximate an empty shape") raise ValueError("Cannot approximate an empty shape")
@ -1754,6 +1886,71 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
f"{type(planar_shape)}" f"{type(planar_shape)}"
) )
def wrap_faces(
self,
faces: Iterable[Face],
path: Wire | Edge,
start: float = 0.0,
) -> ShapeList[Face]:
"""wrap_faces
Wrap a sequence of 2D faces onto a 3D surface, aligned along a guiding path.
This method places multiple planar `Face` objects (defined in the XY plane) onto a
curved 3D surface (`self`), following a given path (Wire or Edge) that lies on or
closely follows the surface. Each face is spaced along the path according to its
original horizontal (X-axis) position, preserving the relative layout of the input
faces.
The wrapping process attempts to maintain the shape and size of each face while
minimizing distortion. Each face is repositioned to the origin, then individually
wrapped onto the surface starting at a specific point along the path. The face's
new orientation is defined using the path's tangent direction and the surface normal
at that point.
This is particularly useful for placing a series of featuressuch as embossed logos,
engraved labels, or patterned tilesonto a freeform or cylindrical surface, aligned
along a reference edge or curve.
Args:
faces (Iterable[Face]): An iterable of 2D planar faces to be wrapped.
path (Wire | Edge): A curve on the target surface that defines the alignment
direction. The X-position of each face is mapped to a relative position
along this path.
start (float, optional): The relative starting point on the path (between 0.0
and 1.0) where the first face should be placed. Defaults to 0.0.
Returns:
ShapeList[Face]: A list of wrapped face objects, aligned and conformed to the
surface.
"""
path_length = path.length
face_list = list(faces)
first_face_min_x = face_list[0].bounding_box().min.X
# Position each face at the origin and wrap onto surface
wrapped_faces: ShapeList[Face] = ShapeList()
for face in face_list:
bbox = face.bounding_box()
face_center_x = (bbox.min.X + bbox.max.X) / 2
delta_x = face_center_x - first_face_min_x
relative_position_on_wire = start + delta_x / path_length
path_position = path.position_at(relative_position_on_wire)
surface_location = Location(
Plane(
path_position,
x_dir=path.tangent_at(relative_position_on_wire),
z_dir=self.normal_at(path_position),
)
)
assert isinstance(face.position, Vector)
face.position -= (delta_x, 0, 0) # Shift back to origin
wrapped_face = Face.wrap(self, face, surface_location)
wrapped_faces.append(wrapped_face)
return wrapped_faces
def _uv_bounds(self) -> tuple[float, float, float, float]: def _uv_bounds(self) -> tuple[float, float, float, float]:
"""Return the u min, u max, v min, v max values""" """Return the u min, u max, v min, v max values"""
return BRepTools.UVBounds_s(self.wrapped) return BRepTools.UVBounds_s(self.wrapped)
@ -1787,7 +1984,9 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
for w in planar_face.inner_wires() for w in planar_face.inner_wires()
] ]
wrapped_face = Face.make_surface( wrapped_face = Face.make_surface(
wrapped_perimeter, interior_wires=wrapped_holes wrapped_perimeter,
surface_points=[surface_loc.position],
interior_wires=wrapped_holes,
) )
# Potentially flip the wrapped face to match the surface # Potentially flip the wrapped face to match the surface
@ -1889,7 +2088,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
return Wire(wrapped_edges) return Wire(wrapped_edges)
# #
# Part 3: The first and last edges likey don't meet at this point due to # Part 3: The first and last edges likely don't meet at this point due to
# distortion caused by following the surface, so we'll need to join # distortion caused by following the surface, so we'll need to join
# them. # them.
# #
@ -1947,7 +2146,7 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
# #
# Part 5: Validate # Part 5: Validate
# #
if not wrapped_wire.is_valid(): if not wrapped_wire.is_valid:
raise RuntimeError("wrapped wire is not valid") raise RuntimeError("wrapped wire is not valid")
return wrapped_wire return wrapped_wire
@ -2121,6 +2320,28 @@ class Shell(Mixin2D, Shape[TopoDS_Shell]):
BRepGProp.LinearProperties_s(self.wrapped, properties) BRepGProp.LinearProperties_s(self.wrapped, properties)
return Vector(properties.CentreOfMass()) return Vector(properties.CentreOfMass())
def location_at(
self,
surface_point: VectorLike,
*,
x_dir: VectorLike | None = None,
) -> Location:
"""location_at
Get the location (origin and orientation) on the surface of the shell.
Args:
surface_point (VectorLike): A 3D point near the surface.
x_dir (VectorLike, optional): Direction for the local X axis. If not given,
the tangent in the U direction is used.
Returns:
Location: A full 3D placement at the specified point on the shell surface.
"""
# Find the closest Face and get the location from it
face = self.faces().sort_by(lambda f: f.distance_to(surface_point))[0]
return face.location_at(surface_point, x_dir=x_dir)
def sort_wires_by_build_order(wire_list: list[Wire]) -> list[list[Wire]]: def sort_wires_by_build_order(wire_list: list[Wire]) -> list[list[Wire]]:
"""Tries to determine how wires should be combined into faces. """Tries to determine how wires should be combined into faces.

View file

@ -54,6 +54,8 @@ license:
from __future__ import annotations from __future__ import annotations
import itertools import itertools
import warnings
from typing import overload, TYPE_CHECKING from typing import overload, TYPE_CHECKING
from collections.abc import Iterable from collections.abc import Iterable
@ -132,7 +134,8 @@ class Vertex(Shape[TopoDS_Vertex]):
) )
super().__init__(ocp_vx) super().__init__(ocp_vx)
self.X, self.Y, self.Z = self.to_tuple() pnt = BRep_Tool.Pnt_s(self.wrapped)
self.X, self.Y, self.Z = pnt.X(), pnt.Y(), pnt.Z()
# ---- Properties ---- # ---- Properties ----
@ -239,7 +242,7 @@ class Vertex(Shape[TopoDS_Vertex]):
def __sub__(self, other: Vertex | Vector | tuple) -> Vertex: # type: ignore def __sub__(self, other: Vertex | Vector | tuple) -> Vertex: # type: ignore
"""Subtract """Subtract
Substract a Vertex with a Vertex, Vector or Tuple from self Subtract a Vertex with a Vertex, Vector or Tuple from self
Args: Args:
other: Value to add other: Value to add
@ -272,6 +275,12 @@ class Vertex(Shape[TopoDS_Vertex]):
def to_tuple(self) -> tuple[float, float, float]: def to_tuple(self) -> tuple[float, float, float]:
"""Return vertex as three tuple of floats""" """Return vertex as three tuple of floats"""
warnings.warn(
"to_tuple is deprecated and will be removed in a future version. "
"Use 'tuple(Vertex)' instead.",
DeprecationWarning,
stacklevel=2,
)
geom_point = BRep_Tool.Pnt_s(self.wrapped) geom_point = BRep_Tool.Pnt_s(self.wrapped)
return (geom_point.X(), geom_point.Y(), geom_point.Z()) return (geom_point.X(), geom_point.Y(), geom_point.Z())

View file

@ -237,18 +237,16 @@ class TestCommonOperations(unittest.TestCase):
def test_matmul(self): def test_matmul(self):
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
(Edge.make_line((0, 0, 0), (1, 1, 1)) @ 0.5).to_tuple(), (0.5, 0.5, 0.5), 5 Edge.make_line((0, 0, 0), (1, 1, 1)) @ 0.5, (0.5, 0.5, 0.5), 5
) )
def test_mod(self): def test_mod(self):
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(Wire.make_circle(10) % 0.5, (0, -1, 0), 5)
(Wire.make_circle(10) % 0.5).to_tuple(), (0, -1, 0), 5
)
def test_xor(self): def test_xor(self):
helix_loc = Edge.make_helix(2 * pi, 1, 1) ^ 0 helix_loc = Edge.make_helix(2 * pi, 1, 1) ^ 0
self.assertTupleAlmostEquals(helix_loc.position.to_tuple(), (1, 0, 0), 5) self.assertTupleAlmostEquals(helix_loc.position, (1, 0, 0), 5)
self.assertTupleAlmostEquals(helix_loc.orientation.to_tuple(), (-45, 0, 180), 5) self.assertTupleAlmostEquals(helix_loc.orientation, (-45, 0, 180), 5)
class TestLocations(unittest.TestCase): class TestLocations(unittest.TestCase):
@ -256,11 +254,11 @@ class TestLocations(unittest.TestCase):
locs = PolarLocations(1, 5, 45, 90, False).local_locations locs = PolarLocations(1, 5, 45, 90, False).local_locations
for i, angle in enumerate(range(45, 135, 18)): for i, angle in enumerate(range(45, 135, 18)):
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
locs[i].position.to_tuple(), locs[i].position,
Vector(1, 0).rotate(Axis.Z, angle).to_tuple(), Vector(1, 0).rotate(Axis.Z, angle),
5, 5,
) )
self.assertTupleAlmostEquals(locs[i].orientation.to_tuple(), (0, 0, 0), 5) self.assertTupleAlmostEquals(locs[i].orientation, (0, 0, 0), 5)
def test_polar_endpoint(self): def test_polar_endpoint(self):
locs = PolarLocations( locs = PolarLocations(
@ -284,7 +282,7 @@ class TestLocations(unittest.TestCase):
def test_no_centering(self): def test_no_centering(self):
with BuildSketch(): with BuildSketch():
with GridLocations(4, 4, 2, 2, align=(Align.MIN, Align.MIN)) as l: with GridLocations(4, 4, 2, 2, align=(Align.MIN, Align.MIN)) as l:
pts = [loc.to_tuple()[0] for loc in l.locations] pts = [tuple(loc)[0] for loc in l.locations]
self.assertTupleAlmostEquals(pts[0], (0, 0, 0), 5) self.assertTupleAlmostEquals(pts[0], (0, 0, 0), 5)
self.assertTupleAlmostEquals(pts[1], (0, 4, 0), 5) self.assertTupleAlmostEquals(pts[1], (0, 4, 0), 5)
self.assertTupleAlmostEquals(pts[2], (4, 0, 0), 5) self.assertTupleAlmostEquals(pts[2], (4, 0, 0), 5)
@ -329,11 +327,11 @@ class TestLocations(unittest.TestCase):
self.assertAlmostEqual(hloc.radius, 1, 7) self.assertAlmostEqual(hloc.radius, 1, 7)
self.assertAlmostEqual(hloc.diagonal, 2, 7) self.assertAlmostEqual(hloc.diagonal, 2, 7)
self.assertAlmostEqual(hloc.apothem, 3**0.5 / 2, 7) self.assertAlmostEqual(hloc.apothem, 3**0.5 / 2, 7)
def test_centering(self): def test_centering(self):
with BuildSketch(): with BuildSketch():
with GridLocations(4, 4, 2, 2, align=(Align.CENTER, Align.CENTER)) as l: with GridLocations(4, 4, 2, 2, align=(Align.CENTER, Align.CENTER)) as l:
pts = [loc.to_tuple()[0] for loc in l.locations] pts = [tuple(loc)[0] for loc in l.locations]
self.assertTupleAlmostEquals(pts[0], (-2, -2, 0), 5) self.assertTupleAlmostEquals(pts[0], (-2, -2, 0), 5)
self.assertTupleAlmostEquals(pts[1], (-2, 2, 0), 5) self.assertTupleAlmostEquals(pts[1], (-2, 2, 0), 5)
self.assertTupleAlmostEquals(pts[2], (2, -2, 0), 5) self.assertTupleAlmostEquals(pts[2], (2, -2, 0), 5)
@ -343,7 +341,7 @@ class TestLocations(unittest.TestCase):
with BuildSketch(): with BuildSketch():
with Locations((-2, -2), (2, 2)): with Locations((-2, -2), (2, 2)):
with GridLocations(1, 1, 2, 2) as nested_grid: with GridLocations(1, 1, 2, 2) as nested_grid:
pts = [loc.to_tuple()[0] for loc in nested_grid.local_locations] pts = [tuple(loc)[0] for loc in nested_grid.local_locations]
self.assertTupleAlmostEquals(pts[0], (-2.50, -2.50, 0.00), 5) self.assertTupleAlmostEquals(pts[0], (-2.50, -2.50, 0.00), 5)
self.assertTupleAlmostEquals(pts[1], (-2.50, -1.50, 0.00), 5) self.assertTupleAlmostEquals(pts[1], (-2.50, -1.50, 0.00), 5)
self.assertTupleAlmostEquals(pts[2], (-1.50, -2.50, 0.00), 5) self.assertTupleAlmostEquals(pts[2], (-1.50, -2.50, 0.00), 5)
@ -357,8 +355,8 @@ class TestLocations(unittest.TestCase):
with BuildSketch(): with BuildSketch():
with PolarLocations(6, 3): with PolarLocations(6, 3):
with GridLocations(1, 1, 2, 2) as polar_grid: with GridLocations(1, 1, 2, 2) as polar_grid:
pts = [loc.to_tuple()[0] for loc in polar_grid.local_locations] pts = [tuple(loc)[0] for loc in polar_grid.local_locations]
ort = [loc.to_tuple()[1] for loc in polar_grid.local_locations] ort = [tuple(loc)[1] for loc in polar_grid.local_locations]
self.assertTupleAlmostEquals(pts[0], (5.50, -0.50, 0.00), 2) self.assertTupleAlmostEquals(pts[0], (5.50, -0.50, 0.00), 2)
self.assertTupleAlmostEquals(pts[1], (5.50, 0.50, 0.00), 2) self.assertTupleAlmostEquals(pts[1], (5.50, 0.50, 0.00), 2)
@ -390,22 +388,18 @@ class TestLocations(unittest.TestCase):
square = Face.make_rect(1, 1, Plane.XZ) square = Face.make_rect(1, 1, Plane.XZ)
with BuildPart(): with BuildPart():
loc = Locations(square).locations[0] loc = Locations(square).locations[0]
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(loc.position, Location(Plane.XZ).position, 5)
loc.position.to_tuple(), Location(Plane.XZ).position.to_tuple(), 5 self.assertTupleAlmostEquals(loc.orientation, Location(Plane.XZ).orientation, 5)
)
self.assertTupleAlmostEquals(
loc.orientation.to_tuple(), Location(Plane.XZ).orientation.to_tuple(), 5
)
def test_from_plane(self): def test_from_plane(self):
with BuildPart(): with BuildPart():
loc = Locations(Plane.XY.offset(1)).locations[0] loc = Locations(Plane.XY.offset(1)).locations[0]
self.assertTupleAlmostEquals(loc.position.to_tuple(), (0, 0, 1), 5) self.assertTupleAlmostEquals(loc.position, (0, 0, 1), 5)
def test_from_axis(self): def test_from_axis(self):
with BuildPart(): with BuildPart():
loc = Locations(Axis((1, 1, 1), (0, 0, 1))).locations[0] loc = Locations(Axis((1, 1, 1), (0, 0, 1))).locations[0]
self.assertTupleAlmostEquals(loc.position.to_tuple(), (1, 1, 1), 5) self.assertTupleAlmostEquals(loc.position, (1, 1, 1), 5)
def test_multiplication(self): def test_multiplication(self):
circles = GridLocations(2, 2, 2, 2) * Circle(1) circles = GridLocations(2, 2, 2, 2) * Circle(1)
@ -416,25 +410,17 @@ class TestLocations(unittest.TestCase):
def test_grid_attributes(self): def test_grid_attributes(self):
grid = GridLocations(5, 10, 3, 4) grid = GridLocations(5, 10, 3, 4)
self.assertTupleAlmostEquals(grid.size.to_tuple(), (10, 30, 0), 5) self.assertTupleAlmostEquals(grid.size, (10, 30, 0), 5)
self.assertTupleAlmostEquals(grid.min.to_tuple(), (-5, -15, 0), 5) self.assertTupleAlmostEquals(grid.min, (-5, -15, 0), 5)
self.assertTupleAlmostEquals(grid.max.to_tuple(), (5, 15, 0), 5) self.assertTupleAlmostEquals(grid.max, (5, 15, 0), 5)
def test_mixed_sequence_list(self): def test_mixed_sequence_list(self):
locs = Locations((0, 1), [(2, 3), (4, 5)], (6, 7)) locs = Locations((0, 1), [(2, 3), (4, 5)], (6, 7))
self.assertEqual(len(locs.locations), 4) self.assertEqual(len(locs.locations), 4)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(locs.locations[0].position, (0, 1, 0), 5)
locs.locations[0].position.to_tuple(), (0, 1, 0), 5 self.assertTupleAlmostEquals(locs.locations[1].position, (2, 3, 0), 5)
) self.assertTupleAlmostEquals(locs.locations[2].position, (4, 5, 0), 5)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(locs.locations[3].position, (6, 7, 0), 5)
locs.locations[1].position.to_tuple(), (2, 3, 0), 5
)
self.assertTupleAlmostEquals(
locs.locations[2].position.to_tuple(), (4, 5, 0), 5
)
self.assertTupleAlmostEquals(
locs.locations[3].position.to_tuple(), (6, 7, 0), 5
)
class TestProperties(unittest.TestCase): class TestProperties(unittest.TestCase):
@ -449,27 +435,25 @@ class TestRotation(unittest.TestCase):
def test_init(self): def test_init(self):
thirty_by_three = Rotation(30, 30, 30) thirty_by_three = Rotation(30, 30, 30)
box_vertices = Solid.make_box(1, 1, 1).moved(thirty_by_three).vertices() box_vertices = Solid.make_box(1, 1, 1).moved(thirty_by_three).vertices()
self.assertTupleAlmostEquals(tuple(box_vertices[0]), (0.5, -0.4330127, 0.75), 5)
self.assertTupleAlmostEquals(tuple(box_vertices[1]), (0.0, 0.0, 0.0), 7)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
box_vertices[0].to_tuple(), (0.5, -0.4330127, 0.75), 5 tuple(box_vertices[2]), (0.0669872, 0.191987, 1.399519), 5
)
self.assertTupleAlmostEquals(box_vertices[1].to_tuple(), (0.0, 0.0, 0.0), 7)
self.assertTupleAlmostEquals(
box_vertices[2].to_tuple(), (0.0669872, 0.191987, 1.399519), 5
) )
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
box_vertices[3].to_tuple(), (-0.4330127, 0.625, 0.6495190), 5 tuple(box_vertices[3]), (-0.4330127, 0.625, 0.6495190), 5
) )
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
box_vertices[4].to_tuple(), (1.25, 0.2165063, 0.625), 5 tuple(box_vertices[4]), (1.25, 0.2165063, 0.625), 5
) )
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
box_vertices[5].to_tuple(), (0.75, 0.649519, -0.125), 5 tuple(box_vertices[5]), (0.75, 0.649519, -0.125), 5
) )
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
box_vertices[6].to_tuple(), (0.816987, 0.841506, 1.274519), 5 tuple(box_vertices[6]), (0.816987, 0.841506, 1.274519), 5
) )
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
box_vertices[7].to_tuple(), (0.3169872, 1.2745190, 0.52451905), 5 tuple(box_vertices[7]), (0.3169872, 1.2745190, 0.52451905), 5
) )
@ -706,7 +690,7 @@ class TestShapeList(unittest.TestCase):
def test_shapes(self): def test_shapes(self):
with BuildPart() as test: with BuildPart() as test:
Box(1, 1, 1) Box(1, 1, 1)
self.assertIsNone(test._shapes(Compound)) self.assertEqual(test._shapes(Compound), [])
def test_operators(self): def test_operators(self):
with BuildPart() as test: with BuildPart() as test:
@ -744,12 +728,12 @@ class TestValidateInputs(unittest.TestCase):
class TestVectorExtensions(unittest.TestCase): class TestVectorExtensions(unittest.TestCase):
def test_vector_localization(self): def test_vector_localization(self):
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
(Vector(1, 1, 1) + (1, 2)).to_tuple(), (Vector(1, 1, 1) + (1, 2)),
(2, 3, 1), (2, 3, 1),
5, 5,
) )
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
(Vector(3, 3, 3) - (1, 2)).to_tuple(), (Vector(3, 3, 3) - (1, 2)),
(2, 1, 3), (2, 1, 3),
5, 5,
) )
@ -759,16 +743,14 @@ class TestVectorExtensions(unittest.TestCase):
Vector(1, 2, 3) - "four" Vector(1, 2, 3) - "four"
with BuildLine(Plane.YZ): with BuildLine(Plane.YZ):
self.assertTupleAlmostEquals(WorkplaneList.localize((1, 2)), (0, 1, 2), 5)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
WorkplaneList.localize((1, 2)).to_tuple(), (0, 1, 2), 5 WorkplaneList.localize(Vector(1, 1, 1) + (1, 2)),
)
self.assertTupleAlmostEquals(
WorkplaneList.localize(Vector(1, 1, 1) + (1, 2)).to_tuple(),
(1, 2, 3), (1, 2, 3),
5, 5,
) )
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
WorkplaneList.localize(Vector(3, 3, 3) - (1, 2)).to_tuple(), WorkplaneList.localize(Vector(3, 3, 3) - (1, 2)),
(3, 2, 1), (3, 2, 1),
5, 5,
) )
@ -780,7 +762,7 @@ class TestVectorExtensions(unittest.TestCase):
with BuildLine(pln): with BuildLine(pln):
n3 = Line((-50, -40), (0, 0)) n3 = Line((-50, -40), (0, 0))
n4 = Line(n3 @ 1, n3 @ 1 + (0, 10)) n4 = Line(n3 @ 1, n3 @ 1 + (0, 10))
self.assertTupleAlmostEquals((n4 @ 1).to_tuple(), (0, 0, -25), 5) self.assertTupleAlmostEquals((n4 @ 1), (0, 0, -25), 5)
class TestWorkplaneList(unittest.TestCase): class TestWorkplaneList(unittest.TestCase):
@ -794,8 +776,8 @@ class TestWorkplaneList(unittest.TestCase):
def test_localize(self): def test_localize(self):
with BuildLine(Plane.YZ): with BuildLine(Plane.YZ):
pnts = WorkplaneList.localize((1, 2), (2, 3)) pnts = WorkplaneList.localize((1, 2), (2, 3))
self.assertTupleAlmostEquals(pnts[0].to_tuple(), (0, 1, 2), 5) self.assertTupleAlmostEquals(pnts[0], (0, 1, 2), 5)
self.assertTupleAlmostEquals(pnts[1].to_tuple(), (0, 2, 3), 5) self.assertTupleAlmostEquals(pnts[1], (0, 2, 3), 5)
def test_invalid_workplane(self): def test_invalid_workplane(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):

View file

@ -72,7 +72,7 @@ class AddTests(unittest.TestCase):
# Add Edge # Add Edge
with BuildLine() as test: with BuildLine() as test:
add(Edge.make_line((0, 0, 0), (1, 1, 1))) add(Edge.make_line((0, 0, 0), (1, 1, 1)))
self.assertTupleAlmostEquals((test.wires()[0] @ 1).to_tuple(), (1, 1, 1), 5) self.assertTupleAlmostEquals(test.wires()[0] @ 1, (1, 1, 1), 5)
# Add Wire # Add Wire
with BuildLine() as wire: with BuildLine() as wire:
Polyline((0, 0, 0), (1, 1, 1), (2, 0, 0), (3, 1, 1)) Polyline((0, 0, 0), (1, 1, 1), (2, 0, 0), (3, 1, 1))
@ -94,13 +94,11 @@ class AddTests(unittest.TestCase):
add(Solid.make_box(10, 10, 10), rotation=(0, 0, 45)) add(Solid.make_box(10, 10, 10), rotation=(0, 0, 45))
self.assertAlmostEqual(test.part.volume, 1000, 5) self.assertAlmostEqual(test.part.volume, 1000, 5)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
( test.part.edges()
test.part.edges() .group_by(Axis.Z)[-1]
.group_by(Axis.Z)[-1] .group_by(Axis.X)[-1]
.group_by(Axis.X)[-1] .sort_by(Axis.Y)[0]
.sort_by(Axis.Y)[0] % 1,
% 1
).to_tuple(),
(sqrt(2) / 2, sqrt(2) / 2, 0), (sqrt(2) / 2, sqrt(2) / 2, 0),
5, 5,
) )
@ -405,7 +403,7 @@ class LocationsTests(unittest.TestCase):
with BuildPart(): with BuildPart():
with Locations(Location(Vector())): with Locations(Location(Vector())):
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
LocationList._get_context().locations[0].to_tuple()[0], (0, 0, 0), 5 tuple(LocationList._get_context().locations[0])[0], (0, 0, 0), 5
) )
def test_errors(self): def test_errors(self):
@ -524,7 +522,7 @@ class OffsetTests(unittest.TestCase):
def test_face_offset_with_holes(self): def test_face_offset_with_holes(self):
sk = Rectangle(100, 100) - GridLocations(80, 80, 2, 2) * Circle(5) sk = Rectangle(100, 100) - GridLocations(80, 80, 2, 2) * Circle(5)
sk2 = offset(sk, -5) sk2 = offset(sk, -5)
self.assertTrue(sk2.face().is_valid()) self.assertTrue(sk2.face().is_valid)
self.assertLess(sk2.area, sk.area) self.assertLess(sk2.area, sk.area)
self.assertEqual(len(sk2), 1) self.assertEqual(len(sk2), 1)
@ -680,12 +678,12 @@ class ProjectionTests(unittest.TestCase):
def test_project_point(self): def test_project_point(self):
pnt: Vector = project(Vector(1, 2, 3), Plane.XY)[0] pnt: Vector = project(Vector(1, 2, 3), Plane.XY)[0]
self.assertTupleAlmostEquals(pnt.to_tuple(), (1, 2, 0), 5) self.assertTupleAlmostEquals(pnt, (1, 2, 0), 5)
pnt: Vector = project(Vertex(1, 2, 3), Plane.XZ)[0] pnt: Vector = project(Vertex(1, 2, 3), Plane.XZ)[0]
self.assertTupleAlmostEquals(pnt.to_tuple(), (1, 3, 0), 5) self.assertTupleAlmostEquals(pnt, (1, 3, 0), 5)
with BuildSketch(Plane.YZ) as s1: with BuildSketch(Plane.YZ) as s1:
pnt = project(Vertex(1, 2, 3), mode=Mode.PRIVATE)[0] pnt = project(Vertex(1, 2, 3), mode=Mode.PRIVATE)[0]
self.assertTupleAlmostEquals(pnt.to_tuple(), (2, 3, 0), 5) self.assertTupleAlmostEquals(pnt, (2, 3, 0), 5)
def test_multiple_results(self): def test_multiple_results(self):
with BuildLine() as l1: with BuildLine() as l1:
@ -883,7 +881,7 @@ class TestSweep(unittest.TestCase):
Rectangle(2 * lip, 2 * lip, align=(Align.CENTER, Align.CENTER)) Rectangle(2 * lip, 2 * lip, align=(Align.CENTER, Align.CENTER))
sweep(sections=sk2.sketch, path=topedgs, mode=Mode.SUBTRACT) sweep(sections=sk2.sketch, path=topedgs, mode=Mode.SUBTRACT)
self.assertTrue(p.part.is_valid()) self.assertTrue(p.part.is_valid)
def test_path_error(self): def test_path_error(self):
e1 = Edge.make_line((0, 0), (1, 0)) e1 = Edge.make_line((0, 0), (1, 0))

View file

@ -205,7 +205,7 @@ class BuildLineTests(unittest.TestCase):
l3 = Line((0, 0), (10, 10)) l3 = Line((0, 0), (10, 10))
l4 = IntersectingLine((0, 10), (1, -1), l3) l4 = IntersectingLine((0, 10), (1, -1), l3)
self.assertTupleAlmostEquals((l4 @ 1).to_tuple(), (5, 5, 0), 5) self.assertTupleAlmostEquals(l4 @ 1, (5, 5, 0), 5)
self.assertTrue(isinstance(l4, Edge)) self.assertTrue(isinstance(l4, Edge))
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@ -214,22 +214,20 @@ class BuildLineTests(unittest.TestCase):
def test_jern_arc(self): def test_jern_arc(self):
with BuildLine() as jern: with BuildLine() as jern:
j1 = JernArc((1, 0), (0, 1), 1, 90) j1 = JernArc((1, 0), (0, 1), 1, 90)
self.assertTupleAlmostEquals((jern.line @ 1).to_tuple(), (0, 1, 0), 5) self.assertTupleAlmostEquals(jern.line @ 1, (0, 1, 0), 5)
self.assertAlmostEqual(j1.radius, 1) self.assertAlmostEqual(j1.radius, 1)
self.assertAlmostEqual(j1.length, pi / 2) self.assertAlmostEqual(j1.length, pi / 2)
with BuildLine(Plane.XY.offset(1)) as offset_l: with BuildLine(Plane.XY.offset(1)) as offset_l:
off1 = JernArc((1, 0), (0, 1), 1, 90) off1 = JernArc((1, 0), (0, 1), 1, 90)
self.assertTupleAlmostEquals((offset_l.line @ 1).to_tuple(), (0, 1, 1), 5) self.assertTupleAlmostEquals(offset_l.line @ 1, (0, 1, 1), 5)
self.assertAlmostEqual(off1.radius, 1) self.assertAlmostEqual(off1.radius, 1)
self.assertAlmostEqual(off1.length, pi / 2) self.assertAlmostEqual(off1.length, pi / 2)
plane_iso = Plane(origin=(0, 0, 0), x_dir=(1, 1, 0), z_dir=(1, -1, 1)) plane_iso = Plane(origin=(0, 0, 0), x_dir=(1, 1, 0), z_dir=(1, -1, 1))
with BuildLine(plane_iso) as iso_l: with BuildLine(plane_iso) as iso_l:
iso1 = JernArc((0, 0), (0, 1), 1, 180) iso1 = JernArc((0, 0), (0, 1), 1, 180)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(iso_l.line @ 1, (-sqrt(2), -sqrt(2), 0), 5)
(iso_l.line @ 1).to_tuple(), (-sqrt(2), -sqrt(2), 0), 5
)
self.assertAlmostEqual(iso1.radius, 1) self.assertAlmostEqual(iso1.radius, 1)
self.assertAlmostEqual(iso1.length, pi) self.assertAlmostEqual(iso1.length, pi)
@ -240,11 +238,11 @@ class BuildLineTests(unittest.TestCase):
self.assertFalse(l2.is_closed) self.assertFalse(l2.is_closed)
circle_face = Face(Wire([l1])) circle_face = Face(Wire([l1]))
self.assertAlmostEqual(circle_face.area, pi, 5) self.assertAlmostEqual(circle_face.area, pi, 5)
self.assertTupleAlmostEquals(circle_face.center().to_tuple(), (0, 1, 0), 5) self.assertTupleAlmostEquals(circle_face.center(), (0, 1, 0), 5)
self.assertTupleAlmostEquals(l1.vertex().to_tuple(), l2.start.to_tuple(), 5) self.assertTupleAlmostEquals(l1.vertex(), l2.start, 5)
l1 = JernArc((0, 0), (1, 0), 1, 90) l1 = JernArc((0, 0), (1, 0), 1, 90)
self.assertTupleAlmostEquals((l1 @ 1).to_tuple(), (1, 1, 0), 5) self.assertTupleAlmostEquals(l1 @ 1, (1, 1, 0), 5)
self.assertTrue(isinstance(l1, Edge)) self.assertTrue(isinstance(l1, Edge))
def test_polar_line(self): def test_polar_line(self):
@ -252,38 +250,38 @@ class BuildLineTests(unittest.TestCase):
with BuildLine(): with BuildLine():
a1 = PolarLine((0, 0), sqrt(2), 45) a1 = PolarLine((0, 0), sqrt(2), 45)
d1 = PolarLine((0, 0), sqrt(2), direction=(1, 1)) d1 = PolarLine((0, 0), sqrt(2), direction=(1, 1))
self.assertTupleAlmostEquals((a1 @ 1).to_tuple(), (1, 1, 0), 5) self.assertTupleAlmostEquals(a1 @ 1, (1, 1, 0), 5)
self.assertTupleAlmostEquals((a1 @ 1).to_tuple(), (d1 @ 1).to_tuple(), 5) self.assertTupleAlmostEquals(a1 @ 1, d1 @ 1, 5)
self.assertTrue(isinstance(a1, Edge)) self.assertTrue(isinstance(a1, Edge))
self.assertTrue(isinstance(d1, Edge)) self.assertTrue(isinstance(d1, Edge))
with BuildLine(): with BuildLine():
a2 = PolarLine((0, 0), 1, 30) a2 = PolarLine((0, 0), 1, 30)
d2 = PolarLine((0, 0), 1, direction=(sqrt(3), 1)) d2 = PolarLine((0, 0), 1, direction=(sqrt(3), 1))
self.assertTupleAlmostEquals((a2 @ 1).to_tuple(), (sqrt(3) / 2, 0.5, 0), 5) self.assertTupleAlmostEquals(a2 @ 1, (sqrt(3) / 2, 0.5, 0), 5)
self.assertTupleAlmostEquals((a2 @ 1).to_tuple(), (d2 @ 1).to_tuple(), 5) self.assertTupleAlmostEquals(a2 @ 1, d2 @ 1, 5)
with BuildLine(): with BuildLine():
a3 = PolarLine((0, 0), 1, 150) a3 = PolarLine((0, 0), 1, 150)
d3 = PolarLine((0, 0), 1, direction=(-sqrt(3), 1)) d3 = PolarLine((0, 0), 1, direction=(-sqrt(3), 1))
self.assertTupleAlmostEquals((a3 @ 1).to_tuple(), (-sqrt(3) / 2, 0.5, 0), 5) self.assertTupleAlmostEquals(a3 @ 1, (-sqrt(3) / 2, 0.5, 0), 5)
self.assertTupleAlmostEquals((a3 @ 1).to_tuple(), (d3 @ 1).to_tuple(), 5) self.assertTupleAlmostEquals(a3 @ 1, d3 @ 1, 5)
with BuildLine(): with BuildLine():
a4 = PolarLine((0, 0), 1, angle=30, length_mode=LengthMode.HORIZONTAL) a4 = PolarLine((0, 0), 1, angle=30, length_mode=LengthMode.HORIZONTAL)
d4 = PolarLine( d4 = PolarLine(
(0, 0), 1, direction=(sqrt(3), 1), length_mode=LengthMode.HORIZONTAL (0, 0), 1, direction=(sqrt(3), 1), length_mode=LengthMode.HORIZONTAL
) )
self.assertTupleAlmostEquals((a4 @ 1).to_tuple(), (1, 1 / sqrt(3), 0), 5) self.assertTupleAlmostEquals(a4 @ 1, (1, 1 / sqrt(3), 0), 5)
self.assertTupleAlmostEquals((a4 @ 1).to_tuple(), (d4 @ 1).to_tuple(), 5) self.assertTupleAlmostEquals(a4 @ 1, d4 @ 1, 5)
with BuildLine(Plane.XZ): with BuildLine(Plane.XZ):
a5 = PolarLine((0, 0), 1, angle=30, length_mode=LengthMode.VERTICAL) a5 = PolarLine((0, 0), 1, angle=30, length_mode=LengthMode.VERTICAL)
d5 = PolarLine( d5 = PolarLine(
(0, 0), 1, direction=(sqrt(3), 1), length_mode=LengthMode.VERTICAL (0, 0), 1, direction=(sqrt(3), 1), length_mode=LengthMode.VERTICAL
) )
self.assertTupleAlmostEquals((a5 @ 1).to_tuple(), (sqrt(3), 0, 1), 5) self.assertTupleAlmostEquals(a5 @ 1, (sqrt(3), 0, 1), 5)
self.assertTupleAlmostEquals((a5 @ 1).to_tuple(), (d5 @ 1).to_tuple(), 5) self.assertTupleAlmostEquals(a5 @ 1, d5 @ 1, 5)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
PolarLine((0, 0), 1) PolarLine((0, 0), 1)
@ -292,7 +290,7 @@ class BuildLineTests(unittest.TestCase):
"""Test spline with no tangents""" """Test spline with no tangents"""
with BuildLine() as test: with BuildLine() as test:
s1 = Spline((0, 0), (1, 1), (2, 0)) s1 = Spline((0, 0), (1, 1), (2, 0))
self.assertTupleAlmostEquals((test.edges()[0] @ 1).to_tuple(), (2, 0, 0), 5) self.assertTupleAlmostEquals(test.edges()[0] @ 1, (2, 0, 0), 5)
self.assertTrue(isinstance(s1, Edge)) self.assertTrue(isinstance(s1, Edge))
def test_radius_arc(self): def test_radius_arc(self):
@ -333,19 +331,17 @@ class BuildLineTests(unittest.TestCase):
"""Test center arc as arc and circle""" """Test center arc as arc and circle"""
with BuildLine() as arc: with BuildLine() as arc:
CenterArc((0, 0), 10, 0, 180) CenterArc((0, 0), 10, 0, 180)
self.assertTupleAlmostEquals((arc.edges()[0] @ 1).to_tuple(), (-10, 0, 0), 5) self.assertTupleAlmostEquals(arc.edges()[0] @ 1, (-10, 0, 0), 5)
with BuildLine() as arc: with BuildLine() as arc:
CenterArc((0, 0), 10, 0, 360) CenterArc((0, 0), 10, 0, 360)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(arc.edges()[0] @ 0, arc.edges()[0] @ 1, 5)
(arc.edges()[0] @ 0).to_tuple(), (arc.edges()[0] @ 1).to_tuple(), 5
)
with BuildLine(Plane.XZ) as arc: with BuildLine(Plane.XZ) as arc:
CenterArc((0, 0), 10, 0, 360) CenterArc((0, 0), 10, 0, 360)
self.assertTrue(Face(arc.wires()[0]).is_coplanar(Plane.XZ)) self.assertTrue(Face(arc.wires()[0]).is_coplanar(Plane.XZ))
with BuildLine(Plane.XZ) as arc: with BuildLine(Plane.XZ) as arc:
CenterArc((-100, 0), 100, -45, 90) CenterArc((-100, 0), 100, -45, 90)
self.assertTupleAlmostEquals((arc.edges()[0] @ 0.5).to_tuple(), (0, 0, 0), 5) self.assertTupleAlmostEquals(arc.edges()[0] @ 0.5, (0, 0, 0), 5)
arc = CenterArc((-100, 0), 100, 0, 360) arc = CenterArc((-100, 0), 100, 0, 360)
self.assertTrue(Face(Wire([arc])).is_coplanar(Plane.XY)) self.assertTrue(Face(Wire([arc])).is_coplanar(Plane.XY))
@ -729,6 +725,15 @@ class BuildLineTests(unittest.TestCase):
self.assertGreater(side_sign * coincident_dir, 0) self.assertGreater(side_sign * coincident_dir, 0)
self.assertGreater(center_dir, 0) self.assertGreater(center_dir, 0)
# Verify arc is tangent for a reversed start arc
c1 = CenterArc((0, 80), 40, 0, -180)
c2 = CenterArc((80, 0), 40, 90, 180)
arc = ArcArcTangentArc(c1, c2, 25, side=Side.RIGHT)
_, _, point = c1.distance_to_with_closest_points(arc)
self.assertAlmostEqual(
c1.tangent_at(point).cross(arc.tangent_at(point)).length, 0, 5
)
## Error Handling ## Error Handling
start_arc = CenterArc(start_point, start_r, 0, 360) start_arc = CenterArc(start_point, start_r, 0, 360)
end_arc = CenterArc(end_point, end_r, 0, 360) end_arc = CenterArc(end_point, end_r, 0, 360)

View file

@ -28,6 +28,8 @@ license:
import unittest import unittest
from math import pi, sin from math import pi, sin
from unittest.mock import MagicMock, patch
from build123d import * from build123d import *
from build123d import LocationList, WorkplaneList from build123d import LocationList, WorkplaneList
@ -56,7 +58,6 @@ class TestAlign(unittest.TestCase):
class TestMakeBrakeFormed(unittest.TestCase): class TestMakeBrakeFormed(unittest.TestCase):
def test_make_brake_formed(self): def test_make_brake_formed(self):
# TODO: Fix so this test doesn't raise a DeprecationWarning from NumPy
with BuildPart() as bp: with BuildPart() as bp:
with BuildLine() as bl: with BuildLine() as bl:
Polyline((0, 0), (5, 6), (10, 1)) Polyline((0, 0), (5, 6), (10, 1))
@ -71,6 +72,67 @@ class TestMakeBrakeFormed(unittest.TestCase):
self.assertAlmostEqual(sheet_metal.bounding_box().max.Z, 1, 2) self.assertAlmostEqual(sheet_metal.bounding_box().max.Z, 1, 2)
class TestPartOperationDraft(unittest.TestCase):
def setUp(self):
self.box = Box(10, 10, 10).solid()
self.sides = self.box.faces().filter_by(Axis.Z, reverse=True)
self.bottom_face = self.box.faces().sort_by(Axis.Z)[0]
self.neutral_plane = Plane(self.bottom_face)
def test_successful_draft(self):
"""Test that a draft operation completes successfully"""
result = draft(self.sides, self.neutral_plane, 5)
self.assertIsInstance(result, Part)
self.assertLess(self.box.volume, result.volume)
with BuildPart() as draft_box:
Box(10, 10, 10)
draft(
draft_box.faces().filter_by(Axis.Z, reverse=True),
Plane.XY.offset(-5),
5,
)
self.assertLess(draft_box.part.volume, 1000)
def test_invalid_face_type(self):
"""Test that a ValueError is raised for unsupported face types"""
torus = Torus(5, 1).solid()
with self.assertRaises(ValueError) as cm:
draft([torus.faces()[0]], self.neutral_plane, 5)
def test_faces_from_multiple_solids(self):
"""Test that using faces from different solids raises an error"""
box2 = Box(5, 5, 5).solid()
mixed = [self.sides[0], box2.faces()[0]]
with self.assertRaises(ValueError) as cm:
draft(mixed, self.neutral_plane, 5)
self.assertIn("same topological parent", str(cm.exception))
def test_faces_from_multiple_parts(self):
"""Test that using faces from different solids raises an error"""
box2 = Box(5, 5, 5).solid()
part: Part = Part() + [self.box, Pos(X=10) * box2]
mixed = [part.faces().sort_by(Axis.X)[0], part.faces().sort_by(Axis.X)[-1]]
with self.assertRaises(ValueError) as cm:
draft(mixed, self.neutral_plane, 5)
def test_bad_draft_faces(self):
with self.assertRaises(DraftAngleError):
draft(self.bottom_face, self.neutral_plane, 10)
@patch("build123d.topology.three_d.BRepOffsetAPI_DraftAngle")
def test_draftangleerror_from_solid_draft(self, mock_draft_angle):
"""Simulate a failure in AddDone and catch DraftAngleError"""
mock_builder = MagicMock()
mock_builder.AddDone.return_value = False
mock_builder.ProblematicShape.return_value = "ShapeX"
mock_draft_angle.return_value = mock_builder
with self.assertRaises(DraftAngleError) as cm:
draft(self.sides, self.neutral_plane, 5)
class TestBuildPart(unittest.TestCase): class TestBuildPart(unittest.TestCase):
"""Test the BuildPart Builder derived class""" """Test the BuildPart Builder derived class"""
@ -171,7 +233,7 @@ class TestBuildPart(unittest.TestCase):
def test_named_plane(self): def test_named_plane(self):
with BuildPart(Plane.YZ) as test: with BuildPart(Plane.YZ) as test:
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(
WorkplaneList._get_context().workplanes[0].z_dir.to_tuple(), WorkplaneList._get_context().workplanes[0].z_dir,
(1, 0, 0), (1, 0, 0),
5, 5,
) )

View file

@ -92,9 +92,7 @@ class TestBuildSketch(unittest.TestCase):
with BuildLine(): with BuildLine():
l1 = Line((0, 0), (10, 0)) l1 = Line((0, 0), (10, 0))
Line(l1 @ 1, (10, 10)) Line(l1 @ 1, (10, 10))
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(test.consolidate_edges() @ 1, (10, 10, 0), 5)
(test.consolidate_edges() @ 1).to_tuple(), (10, 10, 0), 5
)
def test_mode_intersect(self): def test_mode_intersect(self):
with BuildSketch() as test: with BuildSketch() as test:
@ -224,6 +222,11 @@ class TestBuildSketchObjects(unittest.TestCase):
self.assertAlmostEqual(test.sketch.area, 0.5, 5) self.assertAlmostEqual(test.sketch.area, 0.5, 5)
self.assertEqual(p.faces()[0].normal_at(), Vector(0, 0, 1)) self.assertEqual(p.faces()[0].normal_at(), Vector(0, 0, 1))
# test iterable input
points_nervure = [(0.0, 0.0), (10.0, 0.0), (0.0, 5.0)]
riri = Polygon(points_nervure, align=Align.NONE)
self.assertEqual(len(riri.vertices()), 3)
def test_rectangle(self): def test_rectangle(self):
with BuildSketch() as test: with BuildSketch() as test:
r = Rectangle(20, 10) r = Rectangle(20, 10)
@ -263,9 +266,7 @@ class TestBuildSketchObjects(unittest.TestCase):
self.assertEqual(r.align, (Align.CENTER, Align.CENTER)) self.assertEqual(r.align, (Align.CENTER, Align.CENTER))
self.assertEqual(r.mode, Mode.ADD) self.assertEqual(r.mode, Mode.ADD)
self.assertAlmostEqual(test.sketch.area, (3 * sqrt(3) / 2) * 2**2, 5) self.assertAlmostEqual(test.sketch.area, (3 * sqrt(3) / 2) * 2**2, 5)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(test.sketch.faces()[0].normal_at(), (0, 0, 1), 5)
test.sketch.faces()[0].normal_at().to_tuple(), (0, 0, 1), 5
)
self.assertAlmostEqual(r.apothem, 2 * sqrt(3) / 2) self.assertAlmostEqual(r.apothem, 2 * sqrt(3) / 2)
def test_regular_polygon_minor_radius(self): def test_regular_polygon_minor_radius(self):
@ -277,9 +278,7 @@ class TestBuildSketchObjects(unittest.TestCase):
self.assertEqual(r.align, (Align.CENTER, Align.CENTER)) self.assertEqual(r.align, (Align.CENTER, Align.CENTER))
self.assertEqual(r.mode, Mode.ADD) self.assertEqual(r.mode, Mode.ADD)
self.assertAlmostEqual(test.sketch.area, (3 * sqrt(3) / 4) * (0.5 * 2) ** 2, 5) self.assertAlmostEqual(test.sketch.area, (3 * sqrt(3) / 4) * (0.5 * 2) ** 2, 5)
self.assertTupleAlmostEquals( self.assertTupleAlmostEquals(test.sketch.faces()[0].normal_at(), (0, 0, 1), 5)
test.sketch.faces()[0].normal_at().to_tuple(), (0, 0, 1), 5
)
def test_regular_polygon_align(self): def test_regular_polygon_align(self):
with BuildSketch() as align: with BuildSketch() as align:
@ -303,7 +302,7 @@ class TestBuildSketchObjects(unittest.TestCase):
poly_pts = [Vector(v) for v in regular_poly.vertices()] poly_pts = [Vector(v) for v in regular_poly.vertices()]
polar_pts = [p.position for p in PolarLocations(1, side_count)] polar_pts = [p.position for p in PolarLocations(1, side_count)]
for poly_pt, polar_pt in zip(poly_pts, polar_pts): for poly_pt, polar_pt in zip(poly_pts, polar_pts):
self.assertTupleAlmostEquals(poly_pt.to_tuple(), polar_pt.to_tuple(), 5) self.assertTupleAlmostEquals(poly_pt, polar_pt, 5)
def test_regular_polygon_min_sides(self): def test_regular_polygon_min_sides(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@ -325,8 +324,8 @@ class TestBuildSketchObjects(unittest.TestCase):
def test_slot_center_point(self): def test_slot_center_point(self):
with BuildSketch() as test: with BuildSketch() as test:
s = SlotCenterPoint((0, 0), (2, 0), 2) s = SlotCenterPoint((0, 0), (2, 0), 2)
self.assertTupleAlmostEquals(s.slot_center.to_tuple(), (0, 0, 0), 5) self.assertTupleAlmostEquals(s.slot_center, (0, 0, 0), 5)
self.assertTupleAlmostEquals(s.point.to_tuple(), (2, 0, 0), 5) self.assertTupleAlmostEquals(s.point, (2, 0, 0), 5)
self.assertEqual(s.slot_height, 2) self.assertEqual(s.slot_height, 2)
self.assertEqual(s.rotation, 0) self.assertEqual(s.rotation, 0)
self.assertEqual(s.mode, Mode.ADD) self.assertEqual(s.mode, Mode.ADD)
@ -334,25 +333,39 @@ class TestBuildSketchObjects(unittest.TestCase):
self.assertEqual(s.faces()[0].normal_at(), Vector(0, 0, 1)) self.assertEqual(s.faces()[0].normal_at(), Vector(0, 0, 1))
def test_slot_center_to_center(self): def test_slot_center_to_center(self):
height = 2
with BuildSketch() as test: with BuildSketch() as test:
s = SlotCenterToCenter(4, 2) s = SlotCenterToCenter(4, height)
self.assertEqual(s.center_separation, 4) self.assertEqual(s.center_separation, 4)
self.assertEqual(s.slot_height, 2) self.assertEqual(s.slot_height, height)
self.assertEqual(s.rotation, 0) self.assertEqual(s.rotation, 0)
self.assertEqual(s.mode, Mode.ADD) self.assertEqual(s.mode, Mode.ADD)
self.assertAlmostEqual(test.sketch.area, pi + 4 * 2, 5) self.assertAlmostEqual(test.sketch.area, pi + 4 * height, 5)
self.assertEqual(s.faces()[0].normal_at(), Vector(0, 0, 1)) self.assertEqual(s.faces()[0].normal_at(), Vector(0, 0, 1))
# Circle degenerate
s1 = SlotCenterToCenter(0, height)
self.assertTrue(len(s1.edges()) == 1)
self.assertEqual(s1.edge().geom_type, GeomType.CIRCLE)
self.assertAlmostEqual(s1.edge().radius, height / 2)
def test_slot_overall(self): def test_slot_overall(self):
height = 2
with BuildSketch() as test: with BuildSketch() as test:
s = SlotOverall(6, 2) s = SlotOverall(6, height)
self.assertEqual(s.width, 6) self.assertEqual(s.width, 6)
self.assertEqual(s.slot_height, 2) self.assertEqual(s.slot_height, height)
self.assertEqual(s.rotation, 0) self.assertEqual(s.rotation, 0)
self.assertEqual(s.mode, Mode.ADD) self.assertEqual(s.mode, Mode.ADD)
self.assertAlmostEqual(test.sketch.area, pi + 4 * 2, 5) self.assertAlmostEqual(test.sketch.area, pi + 4 * height, 5)
self.assertEqual(s.faces()[0].normal_at(), Vector(0, 0, 1)) self.assertEqual(s.faces()[0].normal_at(), Vector(0, 0, 1))
# Circle degenerat
s1 = SlotOverall(2, height)
self.assertTrue(len(s1.edges()) == 1)
self.assertEqual(s1.edge().geom_type, GeomType.CIRCLE)
self.assertAlmostEqual(s1.edge().radius, height / 2)
def test_text(self): def test_text(self):
with BuildSketch() as test: with BuildSketch() as test:
t = Text("test", 2) t = Text("test", 2)
@ -419,6 +432,9 @@ class TestBuildSketchObjects(unittest.TestCase):
self.assertTupleAlmostEquals(tri.vertex_A, (3, 4, 0), 5) self.assertTupleAlmostEquals(tri.vertex_A, (3, 4, 0), 5)
self.assertTupleAlmostEquals(tri.vertex_B, (0, 0, 0), 5) self.assertTupleAlmostEquals(tri.vertex_B, (0, 0, 0), 5)
self.assertTupleAlmostEquals(tri.vertex_C, (3, 0, 0), 5) self.assertTupleAlmostEquals(tri.vertex_C, (3, 0, 0), 5)
self.assertEqual(tri.vertex_A.topo_parent, tri)
self.assertEqual(tri.vertex_B.topo_parent, tri)
self.assertEqual(tri.vertex_C.topo_parent, tri)
tri = Triangle(c=5, C=90, a=3) tri = Triangle(c=5, C=90, a=3)
self.assertAlmostEqual(tri.area, (3 * 4) / 2, 5) self.assertAlmostEqual(tri.area, (3 * 4) / 2, 5)
@ -525,7 +541,7 @@ class TestBuildSketchObjects(unittest.TestCase):
self.assertLess(tri_round.area, tri.area) self.assertLess(tri_round.area, tri.area)
# Test flipping the face # Test flipping the face
flipped = -Rectangle(34, 10).face() flipped = -Face.make_rect(34, 10)
rounded = full_round((flipped.edges() << Axis.X)[0]).face() rounded = full_round((flipped.edges() << Axis.X)[0]).face()
self.assertEqual(flipped.normal_at(), rounded.normal_at()) self.assertEqual(flipped.normal_at(), rounded.normal_at())
@ -533,9 +549,9 @@ class TestBuildSketchObjects(unittest.TestCase):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"slot,args", "slot,args",
[ [
(SlotOverall, (5, 10)), (SlotOverall, (9, 10)),
(SlotCenterToCenter, (-1, 10)), (SlotCenterToCenter, (-1, 10)),
(SlotCenterPoint, ((0, 0, 0), (2, 0, 0), 10)), (SlotCenterPoint, ((0, 0, 0), (0, 0, 0), 10)),
], ],
) )
def test_invalid_slots(slot, args): def test_invalid_slots(slot, args):

View file

@ -33,7 +33,7 @@ import unittest
import numpy as np import numpy as np
from OCP.gp import gp_Ax1, gp_Dir, gp_Pnt from OCP.gp import gp_Ax1, gp_Dir, gp_Pnt
from build123d.geometry import Axis, Location, Plane, Vector from build123d.geometry import Axis, Location, Plane, Vector
from build123d.topology import Edge from build123d.topology import Edge, Vertex
class AlwaysEqual: class AlwaysEqual:
@ -65,10 +65,18 @@ class TestAxis(unittest.TestCase):
self.assertAlmostEqual(test_axis.position, (1, 2, 3), 5) self.assertAlmostEqual(test_axis.position, (1, 2, 3), 5)
self.assertAlmostEqual(test_axis.direction, (0, 0, 1), 5) self.assertAlmostEqual(test_axis.direction, (0, 0, 1), 5)
with self.assertRaises(ValueError):
Axis("one")
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Axis("one", "up") Axis("one", "up")
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Axis(one="up") Axis(one="up")
with self.assertRaises(ValueError):
bad_edge = Edge()
bad_edge.wrapped = Vertex(0, 1, 2).wrapped
Axis(edge=bad_edge)
with self.assertRaises(ValueError):
Axis(gp_ax1=Edge.make_line((0, 0), (1, 0)))
def test_axis_from_occt(self): def test_axis_from_occt(self):
occt_axis = gp_Ax1(gp_Pnt(1, 1, 1), gp_Dir(0, 1, 0)) occt_axis = gp_Ax1(gp_Pnt(1, 1, 1), gp_Dir(0, 1, 0))
@ -100,11 +108,16 @@ class TestAxis(unittest.TestCase):
self.assertAlmostEqual(y_axis.position, (0, 0, 1), 5) self.assertAlmostEqual(y_axis.position, (0, 0, 1), 5)
self.assertAlmostEqual(y_axis.direction, (0, 1, 0), 5) self.assertAlmostEqual(y_axis.direction, (0, 1, 0), 5)
def test_axis_to_plane(self): def test_from_location(self):
x_plane = Axis.X.to_plane() axis = Axis(Location((1, 2, 3), (-90, 0, 0)))
self.assertTrue(isinstance(x_plane, Plane)) self.assertAlmostEqual(axis.position, (1, 2, 3), 6)
self.assertAlmostEqual(x_plane.origin, (0, 0, 0), 5) self.assertAlmostEqual(axis.direction, (0, 1, 0), 6)
self.assertAlmostEqual(x_plane.z_dir, (1, 0, 0), 5)
# def test_axis_to_plane(self):
# x_plane = Axis.X.to_plane()
# self.assertTrue(isinstance(x_plane, Plane))
# self.assertAlmostEqual(x_plane.origin, (0, 0, 0), 5)
# self.assertAlmostEqual(x_plane.z_dir, (1, 0, 0), 5)
def test_axis_is_coaxial(self): def test_axis_is_coaxial(self):
self.assertTrue(Axis.X.is_coaxial(Axis((0, 0, 0), (1, 0, 0)))) self.assertTrue(Axis.X.is_coaxial(Axis((0, 0, 0), (1, 0, 0))))
@ -179,7 +192,7 @@ class TestAxis(unittest.TestCase):
self.assertIsNone(Axis.X.intersect(Axis((0, 1, 1), (0, 0, 1)))) self.assertIsNone(Axis.X.intersect(Axis((0, 1, 1), (0, 0, 1))))
intersection = Axis((1, 2, 3), (0, 0, 1)) & Plane.XY intersection = Axis((1, 2, 3), (0, 0, 1)) & Plane.XY
self.assertAlmostEqual(intersection.to_tuple(), (1, 2, 0), 5) self.assertAlmostEqual(intersection, (1, 2, 0), 5)
arc = Edge.make_circle(20, start_angle=0, end_angle=180) arc = Edge.make_circle(20, start_angle=0, end_angle=180)
ax0 = Axis((-20, 30, 0), (4, -3, 0)) ax0 = Axis((-20, 30, 0), (4, -3, 0))
@ -213,10 +226,10 @@ class TestAxis(unittest.TestCase):
# self.assertTrue(len(intersections.vertices(), 2)) # self.assertTrue(len(intersections.vertices(), 2))
# np.testing.assert_allclose( # np.testing.assert_allclose(
# intersection.vertices()[0].to_tuple(), (-1, 0, 5), 5 # intersection.vertices()[0], (-1, 0, 5), 5
# ) # )
# np.testing.assert_allclose( # np.testing.assert_allclose(
# intersection.vertices()[1].to_tuple(), (1, 0, 5), 5 # intersection.vertices()[1], (1, 0, 5), 5
# ) # )
def test_axis_equal(self): def test_axis_equal(self):

View file

@ -51,10 +51,10 @@ class TestCompound(unittest.TestCase):
box1 = Solid.make_box(1, 1, 1) box1 = Solid.make_box(1, 1, 1)
box2 = Solid.make_box(1, 1, 1, Plane((1, 0, 0))) box2 = Solid.make_box(1, 1, 1, Plane((1, 0, 0)))
combined = Compound([box1]).fuse(box2, glue=True) combined = Compound([box1]).fuse(box2, glue=True)
self.assertTrue(combined.is_valid()) self.assertTrue(combined.is_valid)
self.assertAlmostEqual(combined.volume, 2, 5) self.assertAlmostEqual(combined.volume, 2, 5)
fuzzy = Compound([box1]).fuse(box2, tol=1e-6) fuzzy = Compound([box1]).fuse(box2, tol=1e-6)
self.assertTrue(fuzzy.is_valid()) self.assertTrue(fuzzy.is_valid)
self.assertAlmostEqual(fuzzy.volume, 2, 5) self.assertAlmostEqual(fuzzy.volume, 2, 5)
def test_remove(self): def test_remove(self):

View file

@ -27,6 +27,7 @@ license:
""" """
import math import math
import numpy as np
import unittest import unittest
from unittest.mock import patch, PropertyMock from unittest.mock import patch, PropertyMock
@ -36,7 +37,7 @@ from build123d.geometry import Axis, Plane, Vector
from build123d.objects_curve import CenterArc, EllipticalCenterArc from build123d.objects_curve import CenterArc, EllipticalCenterArc
from build123d.objects_sketch import Circle, Rectangle, RegularPolygon from build123d.objects_sketch import Circle, Rectangle, RegularPolygon
from build123d.operations_generic import sweep from build123d.operations_generic import sweep
from build123d.topology import Edge, Face from build123d.topology import Edge, Face, Wire
from OCP.GeomProjLib import GeomProjLib from OCP.GeomProjLib import GeomProjLib
@ -121,7 +122,7 @@ class TestEdge(unittest.TestCase):
for end in [0, 1]: for end in [0, 1]:
self.assertAlmostEqual( self.assertAlmostEqual(
edge.position_at(end), edge.position_at(end),
edge.to_wire().position_at(end), Wire(edge).position_at(end),
5, 5,
) )
@ -233,7 +234,7 @@ class TestEdge(unittest.TestCase):
for i, loc in enumerate(locs): for i, loc in enumerate(locs):
self.assertAlmostEqual( self.assertAlmostEqual(
loc.position, loc.position,
Vector(1, 0, 0).rotate(Axis.Z, i * 90).to_tuple(), Vector(1, 0, 0).rotate(Axis.Z, i * 90),
5, 5,
) )
self.assertAlmostEqual(loc.orientation, (0, 0, 0), 5) self.assertAlmostEqual(loc.orientation, (0, 0, 0), 5)
@ -272,6 +273,27 @@ class TestEdge(unittest.TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
edge.param_at_point((-1, 1)) edge.param_at_point((-1, 1))
def test_param_at_point_bspline(self):
# Define a complex spline with inflections and non-monotonic behavior
curve = Edge.make_spline(
[
(-2, 0, 0),
(-10, 1, 0),
(0, 0, 0),
(1, -2, 0),
(2, 0, 0),
(1, 1, 0),
]
)
# Sample N points along the curve using position_at and check that
# param_at_point returns approximately the same param (inverted)
N = 20
for u in np.linspace(0.0, 1.0, N):
p = curve.position_at(u)
u_back = curve.param_at_point(p)
self.assertAlmostEqual(u, u_back, delta=1e-6, msg=f"u={u}, u_back={u_back}")
def test_conical_helix(self): def test_conical_helix(self):
helix = Edge.make_helix(1, 4, 1, normal=(-1, 0, 0), angle=10, lefthand=True) helix = Edge.make_helix(1, 4, 1, normal=(-1, 0, 0), angle=10, lefthand=True)
self.assertAlmostEqual(helix.bounding_box().min.X, -4, 5) self.assertAlmostEqual(helix.bounding_box().min.X, -4, 5)

View file

@ -50,6 +50,7 @@ from build123d.objects_sketch import (
Polygon, Polygon,
Rectangle, Rectangle,
RegularPolygon, RegularPolygon,
Text,
Triangle, Triangle,
) )
from build123d.operations_generic import fillet, offset from build123d.operations_generic import fillet, offset
@ -64,7 +65,7 @@ class TestFace(unittest.TestCase):
bottom_edge = Edge.make_circle(radius=1, end_angle=90) bottom_edge = Edge.make_circle(radius=1, end_angle=90)
top_edge = Edge.make_circle(radius=1, plane=Plane((0, 0, 1)), end_angle=90) top_edge = Edge.make_circle(radius=1, plane=Plane((0, 0, 1)), end_angle=90)
curved = Face.make_surface_from_curves(bottom_edge, top_edge) curved = Face.make_surface_from_curves(bottom_edge, top_edge)
self.assertTrue(curved.is_valid()) self.assertTrue(curved.is_valid)
self.assertAlmostEqual(curved.area, math.pi / 2, 5) self.assertAlmostEqual(curved.area, math.pi / 2, 5)
self.assertAlmostEqual( self.assertAlmostEqual(
curved.normal_at(), (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 5 curved.normal_at(), (math.sqrt(2) / 2, math.sqrt(2) / 2, 0), 5
@ -73,7 +74,7 @@ class TestFace(unittest.TestCase):
bottom_wire = Wire.make_circle(1) bottom_wire = Wire.make_circle(1)
top_wire = Wire.make_circle(1, Plane((0, 0, 1))) top_wire = Wire.make_circle(1, Plane((0, 0, 1)))
curved = Face.make_surface_from_curves(bottom_wire, top_wire) curved = Face.make_surface_from_curves(bottom_wire, top_wire)
self.assertTrue(curved.is_valid()) self.assertTrue(curved.is_valid)
self.assertAlmostEqual(curved.area, 2 * math.pi, 5) self.assertAlmostEqual(curved.area, 2 * math.pi, 5)
def test_center(self): def test_center(self):
@ -168,6 +169,13 @@ class TestFace(unittest.TestCase):
flipped_square = -square flipped_square = -square
self.assertAlmostEqual(flipped_square.normal_at(), (0, 0, -1), 5) self.assertAlmostEqual(flipped_square.normal_at(), (0, 0, -1), 5)
# Ensure the topo_parent is cleared when a face is negated
# (otherwise the original Rectangle would be the topo_parent)
flipped = -Rectangle(34, 10).face()
left_edge = flipped.edges().sort_by(Axis.X)[0]
parent_face = left_edge.topo_parent
self.assertAlmostEqual(flipped.normal_at(), parent_face.normal_at(), 5)
def test_offset(self): def test_offset(self):
bbox = Face.make_rect(2, 2, Plane.XY).offset(5).bounding_box() bbox = Face.make_rect(2, 2, Plane.XY).offset(5).bounding_box()
self.assertAlmostEqual(bbox.min, (-1, -1, 5), 5) self.assertAlmostEqual(bbox.min, (-1, -1, 5), 5)
@ -182,7 +190,7 @@ class TestFace(unittest.TestCase):
happy = Face(outer, inners) happy = Face(outer, inners)
self.assertAlmostEqual(happy.area, math.pi * (10**2 - 2), 5) self.assertAlmostEqual(happy.area, math.pi * (10**2 - 2), 5)
outer = Edge.make_circle(10, end_angle=180).to_wire() outer = Wire(Edge.make_circle(10, end_angle=180))
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Face(outer, inners) Face(outer, inners)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@ -191,7 +199,7 @@ class TestFace(unittest.TestCase):
outer = Wire.make_circle(10) outer = Wire.make_circle(10)
inners = [ inners = [
Wire.make_circle(1).locate(Location((-2, 2, 0))), Wire.make_circle(1).locate(Location((-2, 2, 0))),
Edge.make_circle(1, end_angle=180).to_wire().locate(Location((2, 2, 0))), Wire(Edge.make_circle(1, end_angle=180)).locate(Location((2, 2, 0))),
] ]
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Face(outer, inners) Face(outer, inners)
@ -302,7 +310,7 @@ class TestFace(unittest.TestCase):
for j in range(4 - i % 2) for j in range(4 - i % 2)
] ]
cylinder_walls_with_holes = cylinder_wall.make_holes(projected_wires) cylinder_walls_with_holes = cylinder_wall.make_holes(projected_wires)
self.assertTrue(cylinder_walls_with_holes.is_valid()) self.assertTrue(cylinder_walls_with_holes.is_valid)
self.assertLess(cylinder_walls_with_holes.area, cylinder_wall.area) self.assertLess(cylinder_walls_with_holes.area, cylinder_wall.area)
def test_is_inside(self): def test_is_inside(self):
@ -376,7 +384,7 @@ class TestFace(unittest.TestCase):
surface_points=[Vector(0, 0, -5)], surface_points=[Vector(0, 0, -5)],
interior_wires=[hole], interior_wires=[hole],
) )
self.assertTrue(surface.is_valid()) self.assertTrue(surface.is_valid)
self.assertEqual(surface.geom_type, GeomType.BSPLINE) self.assertEqual(surface.geom_type, GeomType.BSPLINE)
bbox = surface.bounding_box() bbox = surface.bounding_box()
self.assertAlmostEqual(bbox.min, (-50.5, -24.5, -5.113393280136395), 5) self.assertAlmostEqual(bbox.min, (-50.5, -24.5, -5.113393280136395), 5)
@ -422,15 +430,15 @@ class TestFace(unittest.TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Face.sweep(edge, Polyline((0, 0), (0.1, 0), (0.2, 0.1))) Face.sweep(edge, Polyline((0, 0), (0.1, 0), (0.2, 0.1)))
def test_to_arcs(self): # def test_to_arcs(self):
with BuildSketch() as bs: # with BuildSketch() as bs:
with BuildLine() as bl: # with BuildLine() as bl:
Polyline((0, 0), (1, 0), (1.5, 0.5), (2, 0), (2, 1), (0, 1), (0, 0)) # Polyline((0, 0), (1, 0), (1.5, 0.5), (2, 0), (2, 1), (0, 1), (0, 0))
fillet(bl.vertices(), radius=0.1) # fillet(bl.vertices(), radius=0.1)
make_face() # make_face()
smooth = bs.faces()[0] # smooth = bs.faces()[0]
fragmented = smooth.to_arcs() # fragmented = smooth.to_arcs()
self.assertLess(len(smooth.edges()), len(fragmented.edges())) # self.assertLess(len(smooth.edges()), len(fragmented.edges()))
def test_outer_wire(self): def test_outer_wire(self):
face = (Face.make_rect(1, 1) - Face.make_rect(0.5, 0.5)).face() face = (Face.make_rect(1, 1) - Face.make_rect(0.5, 0.5)).face()
@ -457,6 +465,37 @@ class TestFace(unittest.TestCase):
face = Cylinder(1, 1).faces().filter_by(GeomType.CYLINDER)[0] face = Cylinder(1, 1).faces().filter_by(GeomType.CYLINDER)[0]
self.assertAlmostEqual(face.normal_at(0, 1), (1, 0, 0), 5) self.assertAlmostEqual(face.normal_at(0, 1), (1, 0, 0), 5)
def test_location_at(self):
face = Face.make_rect(1, 1)
# Default center (u=0, v=0)
loc = face.location_at(0, 0)
self.assertAlmostEqual(loc.position, (-0.5, -0.5, 0), 5)
self.assertAlmostEqual(loc.z_axis.direction, (0, 0, 1), 5)
# Using surface_point instead of u,v
point = face.position_at(0, 0)
loc2 = face.location_at(point)
self.assertAlmostEqual(loc2.position, (-0.5, -0.5, 0), 5)
self.assertAlmostEqual(loc2.z_axis.direction, (0, 0, 1), 5)
# Bad args
with self.assertRaises(ValueError):
face.location_at(0)
with self.assertRaises(ValueError):
face.location_at(center=(0, 0))
# Curved surface: verify z-direction is outward normal
face = Cylinder(1, 1).faces().filter_by(GeomType.CYLINDER)[0]
loc3 = face.location_at(0, 1)
self.assertAlmostEqual(loc3.z_axis.direction, (1, 0, 0), 5)
# Curved surface: verify center
face = Cylinder(1, 1).faces().filter_by(GeomType.CYLINDER)[0]
loc4 = face.location_at()
self.assertAlmostEqual(loc4.position, (-1, 0, 0), 5)
self.assertAlmostEqual(loc4.z_axis.direction, (-1, 0, 0), 5)
def test_without_holes(self): def test_without_holes(self):
# Planar test # Planar test
frame = (Rectangle(1, 1) - Rectangle(0.5, 0.5)).face() frame = (Rectangle(1, 1) - Rectangle(0.5, 0.5)).face()
@ -876,7 +915,7 @@ class TestFace(unittest.TestCase):
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
surface.wrap(star.outer_wire(), target) surface.wrap(star.outer_wire(), target)
@patch.object(Wire, "is_valid", return_value=False) @patch.object(Wire, "is_valid", new_callable=PropertyMock, return_value=False)
def test_wrap_invalid_wire(self, mock_is_valid): def test_wrap_invalid_wire(self, mock_is_valid):
surface = Cone(5, 2, 10).faces().filter_by(GeomType.PLANE, reverse=True)[0] surface = Cone(5, 2, 10).faces().filter_by(GeomType.PLANE, reverse=True)[0]
target = surface.location_at(0.5, 0.5, x_dir=(1, 0, 0)) target = surface.location_at(0.5, 0.5, x_dir=(1, 0, 0))
@ -888,6 +927,22 @@ class TestFace(unittest.TestCase):
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
surface.wrap(star, target) surface.wrap(star, target)
def test_wrap_faces(self):
sphere = Solid.make_sphere(50, angle1=-90).face()
surface = sphere.face()
path: Edge = (
sphere.cut(
Solid.make_cylinder(80, 100, Plane.YZ).locate(Location((-50, 0, -70)))
)
.edges()
.sort_by(Axis.Z)[0]
.reversed()
)
text = Text(txt="ei", font_size=15, align=(Align.MIN, Align.CENTER))
wrapped_faces = surface.wrap_faces(text.faces(), path, 0.2)
self.assertEqual(len(wrapped_faces), 3)
self.assertTrue(all(not f.is_planar_face for f in wrapped_faces))
def test_revolve(self): def test_revolve(self):
l1 = Edge.make_line((3, 0), (3, 2)) l1 = Edge.make_line((3, 0), (3, 2))
revolved = Face.revolve(l1, 360, Axis.Y) revolved = Face.revolve(l1, 360, Axis.Y)

View file

@ -40,11 +40,11 @@ class TestImportExport(unittest.TestCase):
original_box = Solid.make_box(1, 1, 1) original_box = Solid.make_box(1, 1, 1)
export_step(original_box, "test_box.step") export_step(original_box, "test_box.step")
step_box = import_step("test_box.step") step_box = import_step("test_box.step")
self.assertTrue(step_box.is_valid()) self.assertTrue(step_box.is_valid)
self.assertAlmostEqual(step_box.volume, 1, 5) self.assertAlmostEqual(step_box.volume, 1, 5)
export_brep(step_box, "test_box.brep") export_brep(step_box, "test_box.brep")
brep_box = import_brep("test_box.brep") brep_box = import_brep("test_box.brep")
self.assertTrue(brep_box.is_valid()) self.assertTrue(brep_box.is_valid)
self.assertAlmostEqual(brep_box.volume, 1, 5) self.assertAlmostEqual(brep_box.volume, 1, 5)
os.remove("test_box.step") os.remove("test_box.step")
os.remove("test_box.brep") os.remove("test_box.brep")

View file

@ -27,7 +27,6 @@ license:
""" """
import json import json
import os
import unittest import unittest
from build123d.geometry import ( from build123d.geometry import (
Axis, Axis,
@ -52,7 +51,7 @@ class TestGeomEncode(unittest.TestCase):
c_json = json.dumps(Color("red"), cls=GeomEncoder) c_json = json.dumps(Color("red"), cls=GeomEncoder)
color = json.loads(c_json, object_hook=GeomEncoder.geometry_hook) color = json.loads(c_json, object_hook=GeomEncoder.geometry_hook)
self.assertEqual(Color("red").to_tuple(), color.to_tuple()) self.assertEqual(tuple(Color("red")), tuple(color))
loc = Location((0, 1, 2), (4, 8, 16)) loc = Location((0, 1, 2), (4, 8, 16))
l_json = json.dumps(loc, cls=GeomEncoder) l_json = json.dumps(loc, cls=GeomEncoder)

View file

@ -26,7 +26,6 @@ license:
""" """
# Always equal to any other object, to test that __eq__ cooperation is working
import copy import copy
import json import json
import math import math
@ -34,7 +33,6 @@ import os
import unittest import unittest
from random import uniform from random import uniform
import numpy as np
from OCP.gp import ( from OCP.gp import (
gp_Ax1, gp_Ax1,
gp_Dir, gp_Dir,
@ -51,6 +49,8 @@ from build123d.topology import Edge, Solid, Vertex
class AlwaysEqual: class AlwaysEqual:
"""Always equal to any other object, to test that __eq__ cooperation is working"""
def __eq__(self, other): def __eq__(self, other):
return True return True
@ -59,7 +59,7 @@ class TestLocation(unittest.TestCase):
def test_location(self): def test_location(self):
loc0 = Location() loc0 = Location()
T = loc0.wrapped.Transformation().TranslationPart() T = loc0.wrapped.Transformation().TranslationPart()
np.testing.assert_allclose((T.X(), T.Y(), T.Z()), (0, 0, 0), 1e-6) self.assertAlmostEqual((T.X(), T.Y(), T.Z()), (0, 0, 0), 5)
angle = math.degrees( angle = math.degrees(
loc0.wrapped.Transformation().GetRotation().GetRotationAngle() loc0.wrapped.Transformation().GetRotation().GetRotationAngle()
) )
@ -69,19 +69,19 @@ class TestLocation(unittest.TestCase):
loc0 = Location((0, 0, 1)) loc0 = Location((0, 0, 1))
T = loc0.wrapped.Transformation().TranslationPart() T = loc0.wrapped.Transformation().TranslationPart()
np.testing.assert_allclose((T.X(), T.Y(), T.Z()), (0, 0, 1), 1e-6) self.assertAlmostEqual((T.X(), T.Y(), T.Z()), (0, 0, 1), 5)
# List # List
loc0 = Location([0, 0, 1]) loc0 = Location([0, 0, 1])
T = loc0.wrapped.Transformation().TranslationPart() T = loc0.wrapped.Transformation().TranslationPart()
np.testing.assert_allclose((T.X(), T.Y(), T.Z()), (0, 0, 1), 1e-6) self.assertAlmostEqual((T.X(), T.Y(), T.Z()), (0, 0, 1), 5)
# Vector # Vector
loc1 = Location(Vector(0, 0, 1)) loc1 = Location(Vector(0, 0, 1))
T = loc1.wrapped.Transformation().TranslationPart() T = loc1.wrapped.Transformation().TranslationPart()
np.testing.assert_allclose((T.X(), T.Y(), T.Z()), (0, 0, 1), 1e-6) self.assertAlmostEqual((T.X(), T.Y(), T.Z()), (0, 0, 1), 5)
# rotation + translation # rotation + translation
loc2 = Location(Vector(0, 0, 1), Vector(0, 0, 1), 45) loc2 = Location(Vector(0, 0, 1), Vector(0, 0, 1), 45)
@ -103,13 +103,8 @@ class TestLocation(unittest.TestCase):
# Test creation from the OCP.gp.gp_Trsf object # Test creation from the OCP.gp.gp_Trsf object
loc4 = Location(gp_Trsf()) loc4 = Location(gp_Trsf())
np.testing.assert_allclose(loc4.to_tuple()[0], (0, 0, 0), 1e-7) self.assertAlmostEqual(tuple(loc4)[0], (0, 0, 0), 5)
np.testing.assert_allclose(loc4.to_tuple()[1], (0, 0, 0), 1e-7) self.assertAlmostEqual(tuple(loc4)[1], (0, 0, 0), 5)
# Test creation from Plane and Vector
loc4 = Location(Plane.XY, (0, 0, 1))
np.testing.assert_allclose(loc4.to_tuple()[0], (0, 0, 1), 1e-7)
np.testing.assert_allclose(loc4.to_tuple()[1], (0, 0, 0), 1e-7)
# Test composition # Test composition
loc4 = Location((0, 0, 0), Vector(0, 0, 1), 15) loc4 = Location((0, 0, 0), Vector(0, 0, 1), 15)
@ -119,7 +114,7 @@ class TestLocation(unittest.TestCase):
loc7 = loc4**2 loc7 = loc4**2
T = loc5.wrapped.Transformation().TranslationPart() T = loc5.wrapped.Transformation().TranslationPart()
np.testing.assert_allclose((T.X(), T.Y(), T.Z()), (0, 0, 1), 1e-6) self.assertAlmostEqual((T.X(), T.Y(), T.Z()), (0, 0, 1), 5)
angle5 = math.degrees( angle5 = math.degrees(
loc5.wrapped.Transformation().GetRotation().GetRotationAngle() loc5.wrapped.Transformation().GetRotation().GetRotationAngle()
@ -165,21 +160,46 @@ class TestLocation(unittest.TestCase):
t.SetRotationPart(q) t.SetRotationPart(q)
loc2 = Location(t) loc2 = Location(t)
np.testing.assert_allclose(loc1.to_tuple()[0], loc2.to_tuple()[0], 1e-6) self.assertAlmostEqual(tuple(loc1)[0], tuple(loc2)[0], 5)
np.testing.assert_allclose(loc1.to_tuple()[1], loc2.to_tuple()[1], 1e-6) self.assertAlmostEqual(tuple(loc1)[1], tuple(loc2)[1], 5)
loc1 = Location((1, 2), 34) loc1 = Location((1, 2), 34)
np.testing.assert_allclose(loc1.to_tuple()[0], (1, 2, 0), 1e-6) self.assertAlmostEqual(tuple(loc1)[0], (1, 2, 0), 5)
np.testing.assert_allclose(loc1.to_tuple()[1], (0, 0, 34), 1e-6) self.assertAlmostEqual(tuple(loc1)[1], (0, 0, 34), 5)
rot_angles = (-115.00, 35.00, -135.00) rot_angles = (-115.00, 35.00, -135.00)
loc2 = Location((1, 2, 3), rot_angles) loc2 = Location((1, 2, 3), rot_angles)
np.testing.assert_allclose(loc2.to_tuple()[0], (1, 2, 3), 1e-6) self.assertAlmostEqual(tuple(loc2)[0], (1, 2, 3), 5)
np.testing.assert_allclose(loc2.to_tuple()[1], rot_angles, 1e-6) self.assertAlmostEqual(tuple(loc2)[1], rot_angles, 5)
loc3 = Location(loc2) loc3 = Location(loc2)
np.testing.assert_allclose(loc3.to_tuple()[0], (1, 2, 3), 1e-6) self.assertAlmostEqual(tuple(loc3)[0], (1, 2, 3), 5)
np.testing.assert_allclose(loc3.to_tuple()[1], rot_angles, 1e-6) self.assertAlmostEqual(tuple(loc3)[1], rot_angles, 5)
def test_location_kwarg_parameters(self):
loc = Location(position=(10, 20, 30))
self.assertAlmostEqual(loc.position, (10, 20, 30), 5)
loc = Location(position=(10, 20, 30), orientation=(10, 20, 30))
self.assertAlmostEqual(loc.position, (10, 20, 30), 5)
self.assertAlmostEqual(loc.orientation, (10, 20, 30), 5)
loc = Location(
position=(10, 20, 30), orientation=(90, 0, 90), ordering=Extrinsic.XYZ
)
self.assertAlmostEqual(loc.position, (10, 20, 30), 5)
self.assertAlmostEqual(loc.orientation, (0, 90, 90), 5)
loc = Location((10, 20, 30), orientation=(10, 20, 30))
self.assertAlmostEqual(loc.position, (10, 20, 30), 5)
self.assertAlmostEqual(loc.orientation, (10, 20, 30), 5)
loc = Location(plane=Plane.isometric)
self.assertAlmostEqual(loc.position, (0, 0, 0), 5)
self.assertAlmostEqual(loc.orientation, (45.00, 35.26, 30.00), 2)
loc = Location(location=Location())
self.assertAlmostEqual(loc.position, (0, 0, 0), 5)
def test_location_parameters(self): def test_location_parameters(self):
loc = Location((10, 20, 30)) loc = Location((10, 20, 30))
@ -240,15 +260,16 @@ class TestLocation(unittest.TestCase):
loc1 = Location((1, 2, 3), (90, 45, 22.5)) loc1 = Location((1, 2, 3), (90, 45, 22.5))
loc2 = copy.copy(loc1) loc2 = copy.copy(loc1)
loc3 = copy.deepcopy(loc1) loc3 = copy.deepcopy(loc1)
self.assertAlmostEqual(loc1.position, loc2.position.to_tuple(), 6) self.assertAlmostEqual(loc1.position, loc2.position, 6)
self.assertAlmostEqual(loc1.orientation, loc2.orientation.to_tuple(), 6) self.assertAlmostEqual(loc1.orientation, loc2.orientation, 6)
self.assertAlmostEqual(loc1.position, loc3.position.to_tuple(), 6) self.assertAlmostEqual(loc1.position, loc3.position, 6)
self.assertAlmostEqual(loc1.orientation, loc3.orientation.to_tuple(), 6) self.assertAlmostEqual(loc1.orientation, loc3.orientation, 6)
def test_to_axis(self): # deprecated
axis = Location((1, 2, 3), (-90, 0, 0)).to_axis() # def test_to_axis(self):
self.assertAlmostEqual(axis.position, (1, 2, 3), 6) # axis = Location((1, 2, 3), (-90, 0, 0)).to_axis()
self.assertAlmostEqual(axis.direction, (0, 1, 0), 6) # self.assertAlmostEqual(axis.position, (1, 2, 3), 6)
# self.assertAlmostEqual(axis.direction, (0, 1, 0), 6)
def test_equal(self): def test_equal(self):
loc = Location((1, 2, 3), (4, 5, 6)) loc = Location((1, 2, 3), (4, 5, 6))

View file

@ -40,7 +40,9 @@ from build123d.build_enums import (
from build123d.geometry import Axis, Location, Plane, Vector from build123d.geometry import Axis, Location, Plane, Vector
from build123d.objects_curve import Polyline from build123d.objects_curve import Polyline
from build123d.objects_part import Box, Cylinder from build123d.objects_part import Box, Cylinder
from build123d.topology import Compound, Edge, Face, Wire from build123d.operations_part import extrude
from build123d.operations_generic import fillet
from build123d.topology import Compound, Edge, Face, Solid, Wire
class TestMixin1D(unittest.TestCase): class TestMixin1D(unittest.TestCase):
@ -53,10 +55,8 @@ class TestMixin1D(unittest.TestCase):
5, 5,
) )
# Not sure what PARAMETER mode returns - but it's in the ballpark # Not sure what PARAMETER mode returns - but it's in the ballpark
point = ( point = Edge.make_line((0, 0, 0), (1, 1, 1)).position_at(
Edge.make_line((0, 0, 0), (1, 1, 1)) 0.5, position_mode=PositionMode.PARAMETER
.position_at(0.5, position_mode=PositionMode.PARAMETER)
.to_tuple()
) )
self.assertTrue(all([0.0 < v < 1.0 for v in point])) self.assertTrue(all([0.0 < v < 1.0 for v in point]))
@ -119,10 +119,8 @@ class TestMixin1D(unittest.TestCase):
(-1, 0, 0), (-1, 0, 0),
5, 5,
) )
tangent = ( tangent = Edge.make_circle(1, start_angle=0, end_angle=90).tangent_at(
Edge.make_circle(1, start_angle=0, end_angle=90) 0.0, position_mode=PositionMode.PARAMETER
.tangent_at(0.0, position_mode=PositionMode.PARAMETER)
.to_tuple()
) )
self.assertTrue(all([0.0 <= v <= 1.0 for v in tangent])) self.assertTrue(all([0.0 <= v <= 1.0 for v in tangent]))
@ -364,6 +362,29 @@ class TestMixin1D(unittest.TestCase):
wire = Wire.make_rect(1, 1) wire = Wire.make_rect(1, 1)
self.assertAlmostEqual(wire.volume, 0, 5) self.assertAlmostEqual(wire.volume, 0, 5)
def test_edges(self):
box = Solid.make_box(1, 1, 1)
top_x = box.faces().sort_by(Axis.Z)[-1].edges().sort_by(Axis.X)[-1]
self.assertEqual(top_x.topo_parent, box)
self.assertTrue(isinstance(top_x, Edge))
self.assertAlmostEqual(top_x.center(), (1, 0.5, 1), 5)
def test_edges_topo_parent(self):
phone_case_plan = Face.make_rect(80, 150) - Face.make_rect(
25, 25, Plane((-20, 55))
)
phone_case = extrude(phone_case_plan, 2)
window_edges = phone_case.faces().sort_by(Axis.Z)[-1].inner_wires()[0].edges()
for e in window_edges:
self.assertEqual(e.topo_parent, phone_case)
phone_case_f = fillet(window_edges, 1)
self.assertLess(phone_case_f.volume, phone_case.volume)
perimeter = phone_case_f.faces().sort_by(Axis.Z)[-1].outer_wire().edges()
for e in perimeter:
self.assertEqual(e.topo_parent, phone_case_f)
phone_case_ff = fillet(perimeter, 1)
self.assertLess(phone_case_ff.volume, phone_case_f.volume)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -27,7 +27,7 @@ license:
""" """
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch, PropertyMock
from build123d.build_enums import CenterOf, Kind from build123d.build_enums import CenterOf, Kind
from build123d.geometry import Axis, Plane from build123d.geometry import Axis, Plane
@ -67,7 +67,7 @@ class TestMixin3D(unittest.TestCase):
face = box.faces().sort_by(Axis.Z)[0] face = box.faces().sort_by(Axis.Z)[0]
self.assertRaises(ValueError, box.chamfer, 0.1, None, edge, face=face) self.assertRaises(ValueError, box.chamfer, 0.1, None, edge, face=face)
@patch.object(Shape, "is_valid", return_value=False) @patch.object(Shape, "is_valid", new_callable=PropertyMock, return_value=False)
def test_chamfer_invalid_shape_raises_error(self, mock_is_valid): def test_chamfer_invalid_shape_raises_error(self, mock_is_valid):
box = Solid.make_box(1, 1, 1) box = Solid.make_box(1, 1, 1)
@ -111,7 +111,7 @@ class TestMixin3D(unittest.TestCase):
d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism( d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(
None, [f], additive=False None, [f], additive=False
) )
self.assertTrue(d.is_valid()) self.assertTrue(d.is_valid)
self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5) self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5)
# face with depth # face with depth
@ -119,7 +119,7 @@ class TestMixin3D(unittest.TestCase):
d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism( d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(
None, [f], depth=0.5, thru_all=False, additive=False None, [f], depth=0.5, thru_all=False, additive=False
) )
self.assertTrue(d.is_valid()) self.assertTrue(d.is_valid)
self.assertAlmostEqual(d.volume, 1 - 0.5**3, 5) self.assertAlmostEqual(d.volume, 1 - 0.5**3, 5)
# face until # face until
@ -128,7 +128,7 @@ class TestMixin3D(unittest.TestCase):
d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism( d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(
None, [f], up_to_face=limit, thru_all=False, additive=False None, [f], up_to_face=limit, thru_all=False, additive=False
) )
self.assertTrue(d.is_valid()) self.assertTrue(d.is_valid)
self.assertAlmostEqual(d.volume, 1 - 0.5**3, 5) self.assertAlmostEqual(d.volume, 1 - 0.5**3, 5)
# wire # wire
@ -136,7 +136,7 @@ class TestMixin3D(unittest.TestCase):
d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism( d = Solid.make_box(1, 1, 1, Plane((-0.5, -0.5, 0))).dprism(
None, [w], additive=False None, [w], additive=False
) )
self.assertTrue(d.is_valid()) self.assertTrue(d.is_valid)
self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5) self.assertAlmostEqual(d.volume, 1 - 0.5**2, 5)
def test_center(self): def test_center(self):

View file

@ -123,6 +123,8 @@ class TestPlane(unittest.TestCase):
Plane() Plane()
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
Plane(o, z_dir="up") Plane(o, z_dir="up")
with self.assertRaises(TypeError):
Plane(o, forward="up")
# rotated location around z # rotated location around z
loc = Location((0, 0, 0), (0, 0, 45)) loc = Location((0, 0, 0), (0, 0, 45))
@ -211,6 +213,54 @@ class TestPlane(unittest.TestCase):
self.assertAlmostEqual(p.y_dir, expected[i][1], 6) self.assertAlmostEqual(p.y_dir, expected[i][1], 6)
self.assertAlmostEqual(p.z_dir, expected[i][2], 6) self.assertAlmostEqual(p.z_dir, expected[i][2], 6)
def test_plane_from_axis(self):
origin = Vector(1, 2, 3)
direction = Vector(0, 0, 1)
axis = Axis(origin, direction)
plane = Plane(axis)
self.assertEqual(plane.origin, origin)
self.assertTrue(plane.z_dir, direction.normalized())
self.assertAlmostEqual(plane.x_dir.length, 1.0, places=12)
self.assertAlmostEqual(plane.y_dir.length, 1.0, places=12)
self.assertAlmostEqual(plane.z_dir.length, 1.0, places=12)
def test_plane_from_axis_with_x_dir(self):
origin = Vector(0, 0, 0)
z_dir = Vector(0, 0, 1)
x_dir = Vector(1, 0, 0)
axis = Axis(origin, z_dir)
plane = Plane(axis, x_dir)
self.assertEqual(plane.origin, origin)
self.assertEqual(plane.z_dir, z_dir.normalized())
self.assertEqual(plane.x_dir, x_dir.normalized())
self.assertEqual(plane.y_dir, z_dir.cross(x_dir).normalized())
def test_plane_from_axis_with_kwargs(self):
axis = Axis((0, 0, 0), (0, 1, 0))
x_dir = Vector(1, 0, 0)
plane = Plane(axis=axis, x_dir=x_dir)
self.assertEqual(plane.z_dir, Vector(0, 1, 0))
self.assertEqual(plane.x_dir, x_dir.normalized())
def test_plane_from_axis_without_x_dir(self):
axis = Axis((0, 0, 0), (1, 0, 0))
plane = Plane(axis)
self.assertEqual(plane.z_dir, Vector(1, 0, 0))
self.assertAlmostEqual(plane.x_dir.length, 1.0, places=12)
self.assertAlmostEqual(plane.y_dir.length, 1.0, places=12)
self.assertGreater(plane.z_dir.cross(plane.x_dir).dot(plane.y_dir), 0.99)
def test_plane_from_axis_invalid_x_dir(self):
axis = Axis((0, 0, 0), (0, 0, 1))
with self.assertRaises(ValueError):
Plane(axis, x_dir=(0, 0, 0))
with self.assertRaises(TypeError):
Plane(axis, "front")
def test_plane_neg(self): def test_plane_neg(self):
p = Plane( p = Plane(
origin=(1, 2, 3), origin=(1, 2, 3),
@ -273,11 +323,13 @@ class TestPlane(unittest.TestCase):
np.testing.assert_allclose(target_point, local_box_vertices[i], 1e-7) np.testing.assert_allclose(target_point, local_box_vertices[i], 1e-7)
def test_localize_vertex(self): def test_localize_vertex(self):
vertex = Vertex(random.random(), random.random(), random.random()) v_x, v_y, v_z = (random.random(), random.random(), random.random())
np.testing.assert_allclose( vertex = Vertex(v_x, v_y, v_z)
Plane.YZ.to_local_coords(vertex).to_tuple(), self.assertAlmostEqual(
Plane.YZ.to_local_coords(Vector(vertex)).to_tuple(), Plane.YZ.to_local_coords(Vector(vertex)), (v_y, v_z, v_x), 5
5, )
self.assertAlmostEqual(
Vector(Plane.YZ.to_local_coords(vertex)), (v_y, v_z, v_x), 5
) )
def test_repr(self): def test_repr(self):

View file

@ -94,10 +94,6 @@ class TestProjection(unittest.TestCase):
self.assertAlmostEqual(projection[0].position_at(0), (0, 1, 0), 5) self.assertAlmostEqual(projection[0].position_at(0), (0, 1, 0), 5)
self.assertAlmostEqual(projection[0].arc_center, (0, 0, 0), 5) self.assertAlmostEqual(projection[0].arc_center, (0, 0, 0), 5)
def test_to_axis(self):
with self.assertRaises(ValueError):
Edge.make_circle(1, end_angle=30).to_axis()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -29,10 +29,11 @@ license:
# Always equal to any other object, to test that __eq__ cooperation is working # Always equal to any other object, to test that __eq__ cooperation is working
import unittest import unittest
from random import uniform from random import uniform
from unittest.mock import patch from unittest.mock import PropertyMock, patch
import numpy as np import numpy as np
from build123d.build_enums import CenterOf, Keep from anytree import PreOrderIter
from build123d.build_enums import CenterOf, GeomType, Keep
from build123d.geometry import ( from build123d.geometry import (
Axis, Axis,
Color, Color,
@ -43,7 +44,7 @@ from build123d.geometry import (
Rotation, Rotation,
Vector, Vector,
) )
from build123d.objects_part import Box, Cylinder from build123d.objects_part import Box, Cone, Cylinder, Sphere
from build123d.objects_sketch import Circle from build123d.objects_sketch import Circle
from build123d.operations_part import extrude from build123d.operations_part import extrude
from build123d.topology import ( from build123d.topology import (
@ -100,7 +101,7 @@ class TestShape(unittest.TestCase):
Shape.combined_center(objs, center_of=CenterOf.GEOMETRY) Shape.combined_center(objs, center_of=CenterOf.GEOMETRY)
def test_shape_type(self): def test_shape_type(self):
self.assertEqual(Vertex().shape_type(), "Vertex") self.assertEqual(Vertex().shape_type, "Vertex")
def test_scale(self): def test_scale(self):
self.assertAlmostEqual(Solid.make_box(1, 1, 1).scale(2).volume, 2**3, 5) self.assertAlmostEqual(Solid.make_box(1, 1, 1).scale(2).volume, 2**3, 5)
@ -109,10 +110,10 @@ class TestShape(unittest.TestCase):
box1 = Solid.make_box(1, 1, 1) box1 = Solid.make_box(1, 1, 1)
box2 = Solid.make_box(1, 1, 1, Plane((1, 0, 0))) box2 = Solid.make_box(1, 1, 1, Plane((1, 0, 0)))
combined = box1.fuse(box2, glue=True) combined = box1.fuse(box2, glue=True)
self.assertTrue(combined.is_valid()) self.assertTrue(combined.is_valid)
self.assertAlmostEqual(combined.volume, 2, 5) self.assertAlmostEqual(combined.volume, 2, 5)
fuzzy = box1.fuse(box2, tol=1e-6) fuzzy = box1.fuse(box2, tol=1e-6)
self.assertTrue(fuzzy.is_valid()) self.assertTrue(fuzzy.is_valid)
self.assertAlmostEqual(fuzzy.volume, 2, 5) self.assertAlmostEqual(fuzzy.volume, 2, 5)
def test_faces_intersected_by_axis(self): def test_faces_intersected_by_axis(self):
@ -245,7 +246,7 @@ class TestShape(unittest.TestCase):
# invalid_object = box.fillet(0.75, box.edges()) # invalid_object = box.fillet(0.75, box.edges())
# invalid_object.max_fillet(invalid_object.edges()) # invalid_object.max_fillet(invalid_object.edges())
@patch.object(Shape, "is_valid", return_value=False) @patch.object(Shape, "is_valid", new_callable=PropertyMock, return_value=False)
def test_max_fillet_invalid_shape_raises_error(self, mock_is_valid): def test_max_fillet_invalid_shape_raises_error(self, mock_is_valid):
box = Solid.make_box(1, 1, 1) box = Solid.make_box(1, 1, 1)
@ -317,8 +318,8 @@ class TestShape(unittest.TestCase):
c0 = Edge.make_circle(1).locate(Location((0, 2.1, 0))) c0 = Edge.make_circle(1).locate(Location((0, 2.1, 0)))
c1 = Edge.make_circle(1) c1 = Edge.make_circle(1)
closest = c0.closest_points(c1) closest = c0.closest_points(c1)
self.assertAlmostEqual(closest[0], c0.position_at(0.75).to_tuple(), 5) self.assertAlmostEqual(closest[0], c0.position_at(0.75), 5)
self.assertAlmostEqual(closest[1], c1.position_at(0.25).to_tuple(), 5) self.assertAlmostEqual(closest[1], c1.position_at(0.25), 5)
def test_distance_to(self): def test_distance_to(self):
c0 = Edge.make_circle(1).locate(Location((0, 2.1, 0))) c0 = Edge.make_circle(1).locate(Location((0, 2.1, 0)))
@ -347,19 +348,19 @@ class TestShape(unittest.TestCase):
obj = Solid() obj = Solid()
self.assertIs(obj, obj.clean()) self.assertIs(obj, obj.clean())
def test_relocate(self): # def test_relocate(self):
box = Solid.make_box(10, 10, 10).move(Location((20, -5, -5))) # box = Solid.make_box(10, 10, 10).move(Location((20, -5, -5)))
cylinder = Solid.make_cylinder(2, 50).move(Location((0, 0, 0), (0, 90, 0))) # cylinder = Solid.make_cylinder(2, 50).move(Location((0, 0, 0), (0, 90, 0)))
box_with_hole = box.cut(cylinder) # box_with_hole = box.cut(cylinder)
box_with_hole.relocate(box.location) # box_with_hole.relocate(box.location)
self.assertEqual(box.location, box_with_hole.location) # self.assertEqual(box.location, box_with_hole.location)
bbox1 = box.bounding_box() # bbox1 = box.bounding_box()
bbox2 = box_with_hole.bounding_box() # bbox2 = box_with_hole.bounding_box()
self.assertAlmostEqual(bbox1.min, bbox2.min, 5) # self.assertAlmostEqual(bbox1.min, bbox2.min, 5)
self.assertAlmostEqual(bbox1.max, bbox2.max, 5) # self.assertAlmostEqual(bbox1.max, bbox2.max, 5)
def test_project_to_viewport(self): def test_project_to_viewport(self):
# Basic test # Basic test
@ -459,44 +460,56 @@ class TestShape(unittest.TestCase):
def test_ocp_section(self): def test_ocp_section(self):
# Vertex # Vertex
verts, edges = Vertex(1, 2, 0)._ocp_section(Vertex(1, 2, 0)) verts, edges = Vertex(1, 2, 0)._ocp_section(Vertex(1, 2, 0))
self.assertListEqual(verts, []) # ? self.assertEqual(len(verts), 1)
self.assertListEqual(edges, []) self.assertEqual(len(edges), 0)
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
verts, edges = Vertex(1, 2, 0)._ocp_section(Edge.make_line((0, 0), (2, 4))) verts, edges = Vertex(1, 2, 0)._ocp_section(Edge.make_line((0, 0), (2, 4)))
self.assertListEqual(verts, []) # ? self.assertEqual(len(verts), 1)
self.assertListEqual(edges, []) self.assertEqual(len(edges), 0)
self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_rect(5, 5)) verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_rect(5, 5))
np.testing.assert_allclose(tuple(verts[0]), (1, 2, 0), 1e-5) self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
self.assertListEqual(edges, []) self.assertListEqual(edges, [])
verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_plane(Plane.XY)) verts, edges = Vertex(1, 2, 0)._ocp_section(Face.make_plane(Plane.XY))
np.testing.assert_allclose(tuple(verts[0]), (1, 2, 0), 1e-5) self.assertAlmostEqual(Vector(verts[0]), (1, 2, 0), 5)
self.assertListEqual(edges, []) self.assertListEqual(edges, [])
# spline = Spline((-10, 10, -10), (-10, -5, -5), (20, 0, 5)) cylinder = Face.extrude(Edge.make_circle(5, Plane.XY.offset(-10)), (0, 0, 20))
# cylinder = Pos(Z=-10) * extrude(Circle(5), 20) cylinder2 = Face.extrude(Edge.make_circle(5, Plane.YZ.offset(-10)), (20, 0, 0))
# cylinder2 = (Rot((0, 90, 0)) * cylinder).face() pln = Plane.XY
# pln = Plane.XY
# box1 = Box(10, 10, 10, align=(Align.CENTER, Align.CENTER, Align.MIN))
# box2 = Pos(Z=-10) * box1
# # vertices, edges = ocp_section(spline, Face.make_rect(1e6, 1e6, pln)) v_edge = Edge.make_line((-5, 0, -20), (-5, 0, 20))
# vertices1, edges1 = spline.ocp_section(Face.make_plane(pln)) vertices1, edges1 = cylinder._ocp_section(v_edge)
# print(vertices1, edges1) vertices1 = ShapeList(vertices1).sort_by(Axis.Z)
self.assertEqual(len(vertices1), 2)
# vertices2, edges2 = cylinder.ocp_section(Face.make_plane(pln)) self.assertAlmostEqual(Vector(vertices1[0]), (-5, 0, -10), 5)
# print(vertices2, edges2) self.assertAlmostEqual(Vector(vertices1[1]), (-5, 0, 10), 5)
self.assertEqual(len(edges1), 1)
self.assertAlmostEqual(edges1[0].length, 20, 5)
# vertices3, edges3 = cylinder2.ocp_section(Face.make_plane(pln)) vertices2, edges2 = cylinder._ocp_section(Face.make_plane(pln))
# print(vertices3, edges3) self.assertEqual(len(vertices2), 1)
self.assertEqual(len(edges2), 1)
self.assertAlmostEqual(Vector(vertices2[0]), (5, 0, 0), 5)
self.assertEqual(edges2[0].geom_type, GeomType.CIRCLE)
self.assertAlmostEqual(edges2[0].radius, 5, 5)
# # vertices4, edges4 = cylinder2.ocp_section(cylinder) vertices4, edges4 = cylinder2._ocp_section(cylinder)
self.assertGreaterEqual(len(vertices4), 0)
self.assertGreaterEqual(len(edges4), 2)
self.assertTrue(all(e.geom_type == GeomType.ELLIPSE for e in edges4))
# vertices5, edges5 = box1.ocp_section(Face.make_plane(pln)) cylinder3 = Cylinder(5, 20).solid()
# print(vertices5, edges5) cylinder4 = Rotation(0, 90, 0) * cylinder3
# vertices6, edges6 = box1.ocp_section(box2.faces().sort_by(Axis.Z)[-1]) vertices5, edges5 = cylinder3._ocp_section(cylinder4)
self.assertGreaterEqual(len(vertices5), 0)
self.assertGreaterEqual(len(edges5), 2)
self.assertTrue(all(e.geom_type == GeomType.ELLIPSE for e in edges5))
def test_copy_attributes_to(self): def test_copy_attributes_to(self):
box = Box(1, 1, 1) box = Box(1, 1, 1)
@ -526,7 +539,7 @@ class TestShape(unittest.TestCase):
self.assertEqual(hash(empty), 0) self.assertEqual(hash(empty), 0)
self.assertFalse(empty.is_same(Solid())) self.assertFalse(empty.is_same(Solid()))
self.assertFalse(empty.is_equal(Solid())) self.assertFalse(empty.is_equal(Solid()))
self.assertTrue(empty.is_valid()) self.assertTrue(empty.is_valid)
empty_bbox = empty.bounding_box() empty_bbox = empty.bounding_box()
self.assertEqual(tuple(empty_bbox.size), (0, 0, 0)) self.assertEqual(tuple(empty_bbox.size), (0, 0, 0))
self.assertIs(empty, empty.mirror(Plane.XY)) self.assertIs(empty, empty.mirror(Plane.XY))
@ -560,10 +573,10 @@ class TestShape(unittest.TestCase):
empty.moved(Location()) empty.moved(Location())
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
box.moved(empty_loc) box.moved(empty_loc)
with self.assertRaises(ValueError): # with self.assertRaises(ValueError):
empty.relocate(Location()) # empty.relocate(Location())
with self.assertRaises(ValueError): # with self.assertRaises(ValueError):
box.relocate(empty_loc) # box.relocate(empty_loc)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
empty.distance_to(Vector(1, 1, 1)) empty.distance_to(Vector(1, 1, 1))
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@ -615,5 +628,57 @@ class TestShape(unittest.TestCase):
self.assertIsNone(Vertex(1, 1, 1).compound()) self.assertIsNone(Vertex(1, 1, 1).compound())
class TestGlobalLocation(unittest.TestCase):
def test_global_location_hierarchy(self):
# Create a hierarchy: root → child → grandchild
root = Box(1, 1, 1)
root.location = Location((10, 0, 0))
child = Box(1, 1, 1)
child.location = Location((0, 20, 0))
child.parent = root
grandchild = Box(1, 1, 1)
grandchild.location = Location((0, 0, 30))
grandchild.parent = child
# Compute expected global location manually
expected_location = root.location * child.location * grandchild.location
self.assertAlmostEqual(
grandchild.global_location.position, expected_location.position
)
self.assertAlmostEqual(
grandchild.global_location.orientation, expected_location.orientation
)
def test_global_location_in_assembly(self):
cone = Cone(2, 1, 3)
cone.label = "Cone"
box = Box(1, 2, 3)
box.label = "Box"
sphere = Sphere(1)
sphere.label = "Sphere"
assembly1 = Compound(label="Assembly1", children=[cone])
assembly1.move(Location((3, 3, 3), (90, 0, 0)))
assembly2 = Compound(label="Assembly2", children=[assembly1, box])
assembly2.move(Location((2, 4, 6), (0, 0, 90)))
assembly3 = Compound(label="Assembly3", children=[assembly2, sphere])
assembly3.move(Location((3, 6, 9)))
deep_shape: Shape = next(
iter(PreOrderIter(assembly3, filter_=lambda n: n.label in ("Cone")))
)
# print(deep_shape.path)
self.assertAlmostEqual(
deep_shape.global_location.position, (2, 13, 18), places=6
)
self.assertAlmostEqual(
deep_shape.global_location.orientation, (0, 90, 90), places=6
)
from ocp_vscode import show
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -32,7 +32,6 @@ import math
import re import re
import unittest import unittest
import numpy as np
from IPython.lib import pretty from IPython.lib import pretty
from build123d.build_common import GridLocations, PolarLocations from build123d.build_common import GridLocations, PolarLocations
from build123d.build_enums import GeomType, SortBy from build123d.build_enums import GeomType, SortBy
@ -64,7 +63,9 @@ class TestShapeList(unittest.TestCase):
actual_lines = actual.splitlines() actual_lines = actual.splitlines()
self.assertEqual(len(actual_lines), len(expected_lines)) self.assertEqual(len(actual_lines), len(expected_lines))
for actual_line, expected_line in zip(actual_lines, expected_lines): for actual_line, expected_line in zip(actual_lines, expected_lines):
start, end = re.split(r"at 0x[0-9a-f]+", expected_line, maxsplit=2, flags=re.I) start, end = re.split(
r"at 0x[0-9a-f]+", expected_line, maxsplit=2, flags=re.I
)
self.assertTrue(actual_line.startswith(start)) self.assertTrue(actual_line.startswith(start))
self.assertTrue(actual_line.endswith(end)) self.assertTrue(actual_line.endswith(end))
@ -302,7 +303,7 @@ class TestShapeList(unittest.TestCase):
def test_vertex(self): def test_vertex(self):
sl = ShapeList([Edge.make_circle(1)]) sl = ShapeList([Edge.make_circle(1)])
np.testing.assert_allclose(sl.vertex().to_tuple(), (1, 0, 0), 1e-5) self.assertAlmostEqual(tuple(sl.vertex()), (1, 0, 0), 5)
sl = ShapeList([Face.make_rect(1, 1), Face.make_rect(1, 1, Plane((4, 4)))]) sl = ShapeList([Face.make_rect(1, 1), Face.make_rect(1, 1, Plane((4, 4)))])
with self.assertWarns(UserWarning): with self.assertWarns(UserWarning):
sl.vertex() sl.vertex()
@ -403,5 +404,59 @@ class TestShapeList(unittest.TestCase):
) )
class TestShapeListAddition(unittest.TestCase):
def setUp(self):
# Create distinct faces to test with
self.face1 = Box(1, 1, 1).faces().sort_by(Axis.Z)[0] # bottom face
self.face2 = Box(1, 1, 1).faces().sort_by(Axis.Z)[-1] # top face
self.face3 = Box(1, 1, 1).faces().sort_by(Axis.X)[0] # side face
def test_add_single_shape(self):
sl = ShapeList([self.face1])
result = sl + self.face2
self.assertIsInstance(result, ShapeList)
self.assertEqual(len(result), 2)
self.assertIn(self.face1, result)
self.assertIn(self.face2, result)
def test_add_shape_list(self):
sl1 = ShapeList([self.face1])
sl2 = ShapeList([self.face2, self.face3])
result = sl1 + sl2
self.assertIsInstance(result, ShapeList)
self.assertEqual(len(result), 3)
self.assertListEqual(result, [self.face1, self.face2, self.face3])
def test_iadd_single_shape(self):
sl = ShapeList([self.face1])
sl_id_before = id(sl)
sl += self.face2
self.assertEqual(id(sl), sl_id_before) # in-place mutation
self.assertEqual(len(sl), 2)
self.assertListEqual(sl, [self.face1, self.face2])
def test_iadd_shape_list(self):
sl = ShapeList([self.face1])
sl += ShapeList([self.face2, self.face3])
self.assertEqual(len(sl), 3)
self.assertListEqual(sl, [self.face1, self.face2, self.face3])
def test_add_vector(self):
vector = Vector(1, 2, 3)
sl = ShapeList([vector])
sl += Vector(4, 5, 6)
self.assertEqual(len(sl), 2)
self.assertIsInstance(sl[0], Vector)
self.assertIsInstance(sl[1], Vector)
def test_add_invalid_type(self):
sl = ShapeList([self.face1])
with self.assertRaises(TypeError):
_ = sl + 123 # type: ignore
with self.assertRaises(TypeError):
sl += "not a shape" # type: ignore
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -41,12 +41,12 @@ class TestShells(unittest.TestCase):
def test_shell_init(self): def test_shell_init(self):
box_faces = Solid.make_box(1, 1, 1).faces() box_faces = Solid.make_box(1, 1, 1).faces()
box_shell = Shell(box_faces) box_shell = Shell(box_faces)
self.assertTrue(box_shell.is_valid()) self.assertTrue(box_shell.is_valid)
def test_shell_init_single_face(self): def test_shell_init_single_face(self):
face = Solid.make_cone(1, 0, 2).faces().filter_by(GeomType.CONE).first face = Solid.make_cone(1, 0, 2).faces().filter_by(GeomType.CONE).first
shell = Shell(face) shell = Shell(face)
self.assertTrue(shell.is_valid()) self.assertTrue(shell.is_valid)
def test_center(self): def test_center(self):
box_faces = Solid.make_box(1, 1, 1).faces() box_faces = Solid.make_box(1, 1, 1).faces()
@ -71,9 +71,9 @@ class TestShells(unittest.TestCase):
x_section = Rot(90) * Spline((0, -5), (-3, -2), (-2, 0), (-3, 2), (0, 5)) x_section = Rot(90) * Spline((0, -5), (-3, -2), (-2, 0), (-3, 2), (0, 5))
surface = sweep(x_section, Circle(5).wire()) surface = sweep(x_section, Circle(5).wire())
single_face = Shell(surface.face()) single_face = Shell(surface.face())
self.assertTrue(single_face.is_valid()) self.assertTrue(single_face.is_valid)
single_face = Shell(surface.faces()) single_face = Shell(surface.faces())
self.assertTrue(single_face.is_valid()) self.assertTrue(single_face.is_valid)
def test_sweep(self): def test_sweep(self):
path_c1 = JernArc((0, 0), (-1, 0), 1, 180) path_c1 = JernArc((0, 0), (-1, 0), 1, 180)
@ -116,6 +116,13 @@ class TestShells(unittest.TestCase):
outer_vol = 3 * 12 * 7 outer_vol = 3 * 12 * 7
self.assertAlmostEqual(thick.volume, outer_vol - inner_vol) self.assertAlmostEqual(thick.volume, outer_vol - inner_vol)
def test_location_at(self):
shell = Solid.make_cylinder(1, 2).shell()
top_center = shell.location_at((0, 0, 2))
self.assertAlmostEqual(top_center.position, (0, 0, 2), 5)
self.assertAlmostEqual(top_center.z_axis.direction, (0, 0, 1), 5)
self.assertAlmostEqual(top_center.x_axis.direction, (1, 0, 0), 5)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -29,19 +29,27 @@ license:
import math import math
import unittest import unittest
# Mocks for testing failure cases
from unittest.mock import MagicMock, patch
from build123d.build_enums import GeomType, Kind, Until from build123d.build_enums import GeomType, Kind, Until
from build123d.geometry import ( from build123d.geometry import Axis, Location, Plane, Pos, Vector
Axis,
BoundBox,
Location,
OrientedBoundBox,
Plane,
Pos,
Vector,
)
from build123d.objects_curve import Spline from build123d.objects_curve import Spline
from build123d.objects_part import Box, Torus
from build123d.objects_sketch import Circle, Rectangle from build123d.objects_sketch import Circle, Rectangle
from build123d.topology import Compound, Edge, Face, Shell, Solid, Vertex, Wire from build123d.topology import (
Compound,
DraftAngleError,
Edge,
Face,
Shell,
Solid,
Vertex,
Wire,
)
import build123d
from OCP.BRepOffsetAPI import BRepOffsetAPI_DraftAngle
from OCP.StdFail import StdFail_NotDone
class TestSolid(unittest.TestCase): class TestSolid(unittest.TestCase):
@ -51,7 +59,7 @@ class TestSolid(unittest.TestCase):
box = Solid(box_shell) box = Solid(box_shell)
self.assertAlmostEqual(box.area, 6, 5) self.assertAlmostEqual(box.area, 6, 5)
self.assertAlmostEqual(box.volume, 1, 5) self.assertAlmostEqual(box.volume, 1, 5)
self.assertTrue(box.is_valid()) self.assertTrue(box.is_valid)
def test_extrude(self): def test_extrude(self):
v = Edge.extrude(Vertex(1, 1, 1), (0, 0, 1)) v = Edge.extrude(Vertex(1, 1, 1), (0, 0, 1))
@ -254,5 +262,66 @@ class TestSolid(unittest.TestCase):
self.assertAlmostEqual(obb2.volume, 40, 4) self.assertAlmostEqual(obb2.volume, 40, 4)
class TestSolidDraft(unittest.TestCase):
def setUp(self):
# Create a simple box to test draft
self.box: Solid = Box(10, 10, 10).solid()
self.sides = self.box.faces().filter_by(Axis.Z, reverse=True)
self.bottom_face: Face = self.box.faces().sort_by(Axis.Z)[0]
self.neutral_plane = Plane(self.bottom_face)
def test_successful_draft(self):
"""Test that a draft operation completes successfully on a planar face"""
drafted = self.box.draft(self.sides, self.neutral_plane, 5)
self.assertIsInstance(drafted, Solid)
self.assertNotEqual(drafted.volume, self.box.volume)
def test_unsupported_geometry(self):
"""Test that a ValueError is raised on unsupported face geometry"""
# Create toroidal face to simulate unsupported geometry
torus = Torus(5, 1).solid()
with self.assertRaises(ValueError) as cm:
torus.draft([torus.faces()[0]], self.neutral_plane, 5)
self.assertIn("unsupported geometry type", str(cm.exception))
@patch("build123d.topology.three_d.BRepOffsetAPI_DraftAngle")
def test_adddone_failure_raises_draftangleerror(self, mock_draft_api):
"""Test that failure of AddDone() raises DraftAngleError"""
mock_builder = MagicMock()
mock_builder.AddDone.return_value = False
mock_builder.ProblematicShape.return_value = "BadShape"
mock_draft_api.return_value = mock_builder
with self.assertRaises(DraftAngleError) as cm:
self.box.draft(self.sides, self.neutral_plane, 5)
self.assertEqual(cm.exception.face, self.sides[0])
self.assertEqual(cm.exception.problematic_shape, "BadShape")
self.assertIn("Draft could not be added", str(cm.exception))
@patch.object(
build123d.topology.three_d.BRepOffsetAPI_DraftAngle,
"Build",
side_effect=StdFail_NotDone,
)
def test_build_failure_raises_draftangleerror(self, mock_draft_api):
"""Test that Build() failure raises DraftAngleError"""
with self.assertRaises(DraftAngleError) as cm:
self.box.draft(self.sides, self.neutral_plane, 5)
self.assertIsNone(cm.exception.face)
self.assertEqual(
cm.exception.problematic_shape, cm.exception.problematic_shape
) # Not None
self.assertIn("Draft build failed", str(cm.exception))
def test_draftangleerror_contents(self):
"""Test that DraftAngleError stores face and problematic shape"""
err = DraftAngleError("msg", face="face123", problematic_shape="shape456")
self.assertEqual(str(err), "msg")
self.assertEqual(err.face, "face123")
self.assertEqual(err.problematic_shape, "shape456")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -191,26 +191,26 @@ class TestWire(unittest.TestCase):
e1 = Edge.make_line((1, 0), (1, 1)) e1 = Edge.make_line((1, 0), (1, 1))
w0 = Wire.make_circle(1) w0 = Wire.make_circle(1)
w1 = Wire(e0) w1 = Wire(e0)
self.assertTrue(w1.is_valid()) self.assertTrue(w1.is_valid)
w2 = Wire([e0]) w2 = Wire([e0])
self.assertAlmostEqual(w2.length, 1, 5) self.assertAlmostEqual(w2.length, 1, 5)
self.assertTrue(w2.is_valid()) self.assertTrue(w2.is_valid)
w3 = Wire([e0, e1]) w3 = Wire([e0, e1])
self.assertTrue(w3.is_valid()) self.assertTrue(w3.is_valid)
self.assertAlmostEqual(w3.length, 2, 5) self.assertAlmostEqual(w3.length, 2, 5)
w4 = Wire(w0.wrapped) w4 = Wire(w0.wrapped)
self.assertTrue(w4.is_valid()) self.assertTrue(w4.is_valid)
w5 = Wire(obj=w0.wrapped) w5 = Wire(obj=w0.wrapped)
self.assertTrue(w5.is_valid()) self.assertTrue(w5.is_valid)
w6 = Wire(obj=w0.wrapped, label="w6", color=Color("red")) w6 = Wire(obj=w0.wrapped, label="w6", color=Color("red"))
self.assertTrue(w6.is_valid()) self.assertTrue(w6.is_valid)
self.assertEqual(w6.label, "w6") self.assertEqual(w6.label, "w6")
np.testing.assert_allclose(tuple(w6.color), (1.0, 0.0, 0.0, 1.0), 1e-5) np.testing.assert_allclose(tuple(w6.color), (1.0, 0.0, 0.0, 1.0), 1e-5)
w7 = Wire(w6) w7 = Wire(w6)
self.assertTrue(w7.is_valid()) self.assertTrue(w7.is_valid)
c0 = Polyline((0, 0), (1, 0), (1, 1)) c0 = Polyline((0, 0), (1, 0), (1, 1))
w8 = Wire(c0) w8 = Wire(c0)
self.assertTrue(w8.is_valid()) self.assertTrue(w8.is_valid)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Wire(bob="fred") Wire(bob="fred")

View file

@ -260,6 +260,11 @@ class DimensionLineTestCase(unittest.TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
DimensionLine([(0, 0, 0), (5, 0, 0)], draft=metric, arrows=(False, False)) DimensionLine([(0, 0, 0), (5, 0, 0)], draft=metric, arrows=(False, False))
def test_vertical(self):
d_line = DimensionLine([(0, 0), (0, 100)], Draft())
bbox = d_line.bounding_box()
self.assertAlmostEqual(bbox.size.Y, 100, 5) # numbers within
class ExtensionLineTestCase(unittest.TestCase): class ExtensionLineTestCase(unittest.TestCase):
def test_min_x(self): def test_min_x(self):

View file

@ -30,8 +30,10 @@ import json
import os import os
import re import re
import unittest import unittest
from typing import Optional from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional
from zoneinfo import ZoneInfo
import pytest import pytest
@ -39,7 +41,7 @@ from build123d.build_common import GridLocations
from build123d.build_enums import Unit from build123d.build_enums import Unit
from build123d.build_line import BuildLine from build123d.build_line import BuildLine
from build123d.build_sketch import BuildSketch from build123d.build_sketch import BuildSketch
from build123d.exporters3d import export_gltf, export_step, export_brep, export_stl from build123d.exporters3d import export_brep, export_gltf, export_step, export_stl
from build123d.geometry import Color, Pos, Vector, VectorLike from build123d.geometry import Color, Pos, Vector, VectorLike
from build123d.objects_curve import Line from build123d.objects_curve import Line
from build123d.objects_part import Box, Sphere from build123d.objects_part import Box, Sphere
@ -144,6 +146,29 @@ class TestExportStep(DirectApiTestCase):
os.chmod("box_read_only.step", 0o777) # Make the file read/write os.chmod("box_read_only.step", 0o777) # Make the file read/write
os.remove("box_read_only.step") os.remove("box_read_only.step")
def test_export_step_timestamp_datetime(self):
b = Box(1, 1, 1)
t = datetime(2025, 5, 6, 21, 30, 25)
self.assertTrue(export_step(b, "box.step", timestamp=t))
with open("box.step", "r") as file:
step_data = file.read()
os.remove("box.step")
self.assertEqual(
re.findall("FILE_NAME\\('[^']*','([^']*)'", step_data),
["2025-05-06T21:30:25"],
)
def test_export_step_timestamp_str(self):
b = Box(1, 1, 1)
self.assertTrue(export_step(b, "box.step", timestamp="0000-00-00T00:00:00"))
with open("box.step", "r") as file:
step_data = file.read()
os.remove("box.step")
self.assertEqual(
re.findall("FILE_NAME\\('[^']*','([^']*)'", step_data),
["0000-00-00T00:00:00"],
)
class TestExportGltf(DirectApiTestCase): class TestExportGltf(DirectApiTestCase):
def test_export_gltf(self): def test_export_gltf(self):

View file

@ -208,7 +208,7 @@ class TestHollowImport(unittest.TestCase):
export_stl(test_shape, "test.stl") export_stl(test_shape, "test.stl")
importer = Mesher() importer = Mesher()
stl = importer.read("test.stl") stl = importer.read("test.stl")
self.assertTrue(stl[0].is_valid()) self.assertTrue(stl[0].is_valid)
class TestImportDegenerateTriangles(unittest.TestCase): class TestImportDegenerateTriangles(unittest.TestCase):
@ -221,7 +221,7 @@ class TestImportDegenerateTriangles(unittest.TestCase):
stl = importer.read("cyl_w_rect_hole.stl")[0] stl = importer.read("cyl_w_rect_hole.stl")[0]
self.assertEqual(type(stl), Solid) self.assertEqual(type(stl), Solid)
self.assertTrue(stl.is_manifold) self.assertTrue(stl.is_manifold)
self.assertTrue(stl.is_valid()) self.assertTrue(stl.is_valid)
self.assertEqual(sum(f.area == 0 for f in stl.faces()), 0) self.assertEqual(sum(f.area == 0 for f in stl.faces()), 0)