From b4d9b9476c317cfd70ce6928ff9f9529d46873a6 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Sat, 22 Jun 2024 07:27:05 -0700 Subject: [PATCH] fix(create): Update straight skeleton component based on refactor --- .../json/HB_Straight_Skeleton.json | 12 +-- .../src/HB Straight Skeleton.py | 99 +++++------------- .../user_objects/HB Straight Skeleton.ghuser | Bin 5538 -> 4958 bytes 3 files changed, 34 insertions(+), 77 deletions(-) diff --git a/honeybee_grasshopper_core/json/HB_Straight_Skeleton.json b/honeybee_grasshopper_core/json/HB_Straight_Skeleton.json index 91081b6..6f30f54 100644 --- a/honeybee_grasshopper_core/json/HB_Straight_Skeleton.json +++ b/honeybee_grasshopper_core/json/HB_Straight_Skeleton.json @@ -1,11 +1,11 @@ { - "version": "1.8.0", + "version": "1.8.1", "nickname": "Skeleton", "outputs": [ [ { "access": "None", - "name": "polyskel", + "name": "skeleton", "description": "A list of line segments that represent the straight skeleton of\nthe input _floor_geo. This will be output from the component no matter\nwhat the input _floor_geo is.", "type": null, "default": null @@ -13,14 +13,14 @@ { "access": "None", "name": "perim_poly", - "description": "A list of breps representing the perimeter polygons of the input\n_floor_geo. This will only be ouput if an offset_ is input and the\nstraight skeleton is not self-intersecting.", + "description": "A list of breps representing the perimeter polygons of the input\n_floor_geo. This will only be ouput if an offset_ is input.", "type": null, "default": null }, { "access": "None", "name": "core_poly", - "description": "A list of breps representing the core polygons of the input\n_floor_geo. This will only be ouput if an offset_ is input and the\nstraight skeleton is not self-intersecting, and the offset is not\nso great as to eliminate the core.", + "description": "A list of breps representing the core polygons of the input\n_floor_geo. This will only be ouput if an offset_ is input and the offset\nis not so great as to eliminate the core.", "type": null, "default": null } @@ -43,8 +43,8 @@ } ], "subcategory": "0 :: Create", - "code": "\ntry: # import the core ladybug_geometry dependencies\n from ladybug_geometry.geometry2d.line import LineSegment2D\n from ladybug_geometry.geometry2d.polygon import Polygon2D\n from ladybug_geometry.geometry3d.pointvector import Point3D\n from ladybug_geometry.geometry3d.face import Face3D\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_geometry:\\n\\t{}'.format(e))\n\ntry: # import the core ladybug_geometry dependencies\n from ladybug_geometry_polyskel.polyskel import skeleton_as_edge_list\n from ladybug_geometry_polyskel.polysplit import perimeter_core_subpolygons\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_geometry:\\n\\t{}'.format(e))\n\ntry: # import the ladybug_{{cad}} dependencies\n from ladybug_{{cad}}.config import tolerance\n from ladybug_{{cad}}.togeometry import to_face3d\n from ladybug_{{cad}}.fromgeometry import from_face3d, from_linesegment2d\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, list_to_data_tree\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\ndef polygon_to_brep(polygon, z_height):\n \"\"\"Convert a ladybug Polygon2D or list of polygon2D into {{Cad}} breps.\"\"\"\n if isinstance(polygon, list): # face with holes\n verts = []\n for poly in polygon:\n verts.append([Point3D(pt.x, pt.y, z_height) for pt in poly])\n return from_face3d(Face3D(verts[0], holes=verts[1:]))\n else:\n verts = [Point3D(pt.x, pt.y, z_height) for pt in polygon]\n return from_face3d(Face3D(verts))\n\n\nif all_required_inputs(ghenv.Component):\n # first extract the straight skeleton from the geometry\n polyskel, boundaries, hole_polygons = [], [], []\n for face in to_face3d(_floor_geo):\n # convert the input geometry into Polygon2D for straight skeleton analysis\n boundary = Polygon2D.from_array([(pt.x, pt.y) for pt in face.boundary])\n if boundary.is_clockwise:\n boundary = boundary.reverse()\n holes, z_height = None, face[0].z\n if face.has_holes:\n holes = []\n for hole in face.holes:\n h_poly = Polygon2D.from_array([(pt.x, pt.y) for pt in hole])\n if not h_poly.is_clockwise:\n h_poly = h_poly.reverse()\n holes.append(h_poly)\n boundaries.append(boundary)\n hole_polygons.append(holes)\n # compute the skeleton and convert to line segments\n skel_lines = skeleton_as_edge_list(boundary, holes, tolerance)\n skel_lines_rh = [from_linesegment2d(LineSegment2D.from_array(line), z_height)\n for line in skel_lines]\n polyskel.append(skel_lines_rh)\n\n # try to compute core/perimeter polygons if an offset_ is input\n if offset_:\n perim_poly, core_poly = [], []\n for bound, holes in zip(boundaries, hole_polygons):\n try:\n perim, core = perimeter_core_subpolygons(\n bound, offset_, holes, tolerance)\n perim_poly.append([polygon_to_brep(p, z_height) for p in perim])\n if holes is None or len(holes) == 0:\n core_poly.append([polygon_to_brep(p, z_height) for p in core])\n else:\n core_poly.append([polygon_to_brep(core, z_height)])\n except (RuntimeError, TypeError) as e:\n print(e)\n perim_poly.append(None)\n core_poly.append(None)\n\n # convert outputs to data trees\n polyskel = list_to_data_tree(polyskel)\n perim_poly = list_to_data_tree(perim_poly)\n core_poly = list_to_data_tree(core_poly)\n", + "code": "\ntry: # import the core ladybug_geometry dependencies\n from ladybug_geometry.geometry3d import LineSegment3D\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_geometry:\\n\\t{}'.format(e))\n\ntry: # import the core ladybug_geometry dependencies\n from ladybug_geometry_polyskel.polyskel import skeleton_as_edge_list\n from ladybug_geometry_polyskel.polysplit import perimeter_core_subfaces_and_skeleton\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_geometry:\\n\\t{}'.format(e))\n\ntry: # import the ladybug_{{cad}} dependencies\n from ladybug_{{cad}}.config import tolerance\n from ladybug_{{cad}}.togeometry import to_face3d\n from ladybug_{{cad}}.fromgeometry import from_face3d, from_linesegment3d\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, list_to_data_tree\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\nif all_required_inputs(ghenv.Component):\n # extract the straight skeleton and sub-faces from the geometry\n skeleton, perim_poly, core_poly = [], [], []\n for face in to_face3d(_floor_geo):\n if offset_ is not None and offset_ > 0:\n skel, perim, core = perimeter_core_subfaces_and_skeleton(\n face, offset_, tolerance)\n skeleton.append([from_linesegment3d(lin) for lin in skel])\n perim_poly.append([from_face3d(p) for p in perim])\n core_poly.append([from_face3d(c) for c in core])\n else:\n skel_2d = skeleton_as_edge_list(\n face.boundary_polygon2d, face.hole_polygon2d,\n tolerance, intersect=True)\n skel_3d = []\n for seg in skel_2d:\n verts_3d = tuple(face.plane.xy_to_xyz(pt) for pt in seg.vertices)\n skel_3d.append(LineSegment3D.from_end_points(*verts_3d))\n skeleton.append([from_linesegment3d(lin) for lin in skel_3d])\n\n # convert outputs to data trees\n skeleton = list_to_data_tree(skeleton)\n perim_poly = list_to_data_tree(perim_poly)\n core_poly = list_to_data_tree(core_poly)\n", "category": "Honeybee", "name": "HB Straight Skeleton", - "description": "Get the straight skeleton of any horizontal planar geometry.\n_\nThis is can also be used to generate core/perimeter sub-polygons if an offset is\ninput AND the straight skeleton is not self-intersecting. In the event of a\nself-intersecting straight skeleton, the output line segments can still be used\nto assist with the manual creation of core/perimeter offsets.\n_\nThis component uses a modified version of the the polyskel package\n(https://github.com/Botffy/polyskel) by Armin Scipiades (aka. @Bottfy),\nwhich is, itself, a Python implementation of the straight skeleton\nalgorithm as described by Felkel and Obdrzalek in their 1998 conference paper\nStraight skeleton implementation\n(https://github.com/Botffy/polyskel/blob/master/doc/StraightSkeletonImplementation.pdf).\n-" + "description": "Get the straight skeleton and core/perimeter sub-faces for any planar geometry.\n_\nThis component uses a modified version of the the polyskel package\n(https://github.com/Botffy/polyskel) by Armin Scipiades (aka. @Bottfy),\nwhich is, itself, a Python implementation of the straight skeleton\nalgorithm as described by Felkel and Obdrzalek in their 1998 conference paper\nStraight skeleton implementation\n(https://github.com/Botffy/polyskel/blob/master/doc/StraightSkeletonImplementation.pdf).\n-" } \ No newline at end of file diff --git a/honeybee_grasshopper_core/src/HB Straight Skeleton.py b/honeybee_grasshopper_core/src/HB Straight Skeleton.py index 0a93dcf..3dfd1cc 100644 --- a/honeybee_grasshopper_core/src/HB Straight Skeleton.py +++ b/honeybee_grasshopper_core/src/HB Straight Skeleton.py @@ -8,12 +8,7 @@ # @license AGPL-3.0-or-later """ -Get the straight skeleton of any horizontal planar geometry. -_ -This is can also be used to generate core/perimeter sub-polygons if an offset is -input AND the straight skeleton is not self-intersecting. In the event of a -self-intersecting straight skeleton, the output line segments can still be used -to assist with the manual creation of core/perimeter offsets. +Get the straight skeleton and core/perimeter sub-faces for any planar geometry. _ This component uses a modified version of the the polyskel package (https://github.com/Botffy/polyskel) by Armin Scipiades (aka. @Bottfy), @@ -21,7 +16,6 @@ algorithm as described by Felkel and Obdrzalek in their 1998 conference paper Straight skeleton implementation (https://github.com/Botffy/polyskel/blob/master/doc/StraightSkeletonImplementation.pdf). - - Args: @@ -33,102 +27,65 @@ self-intersecting, perim_poly and core_poly will be ouput. Returns: - polyskel: A list of line segments that represent the straight skeleton of + report: Reports, errors, warnings, etc. + skeleton: A list of line segments that represent the straight skeleton of the input _floor_geo. This will be output from the component no matter what the input _floor_geo is. perim_poly: A list of breps representing the perimeter polygons of the input - _floor_geo. This will only be ouput if an offset_ is input and the - straight skeleton is not self-intersecting. + _floor_geo. This will only be ouput if an offset_ is input. core_poly: A list of breps representing the core polygons of the input - _floor_geo. This will only be ouput if an offset_ is input and the - straight skeleton is not self-intersecting, and the offset is not - so great as to eliminate the core. + _floor_geo. This will only be ouput if an offset_ is input and the offset + is not so great as to eliminate the core. """ ghenv.Component.Name = 'HB Straight Skeleton' ghenv.Component.NickName = 'Skeleton' -ghenv.Component.Message = '1.8.0' +ghenv.Component.Message = '1.8.1' ghenv.Component.Category = 'Honeybee' ghenv.Component.SubCategory = '0 :: Create' ghenv.Component.AdditionalHelpFromDocStrings = '0' try: # import the core ladybug_geometry dependencies - from ladybug_geometry.geometry2d.line import LineSegment2D - from ladybug_geometry.geometry2d.polygon import Polygon2D - from ladybug_geometry.geometry3d.pointvector import Point3D - from ladybug_geometry.geometry3d.face import Face3D + from ladybug_geometry.geometry3d import LineSegment3D except ImportError as e: raise ImportError('\nFailed to import ladybug_geometry:\n\t{}'.format(e)) try: # import the core ladybug_geometry dependencies from ladybug_geometry_polyskel.polyskel import skeleton_as_edge_list - from ladybug_geometry_polyskel.polysplit import perimeter_core_subpolygons + from ladybug_geometry_polyskel.polysplit import perimeter_core_subfaces_and_skeleton except ImportError as e: raise ImportError('\nFailed to import ladybug_geometry:\n\t{}'.format(e)) try: # import the ladybug_rhino dependencies from ladybug_rhino.config import tolerance from ladybug_rhino.togeometry import to_face3d - from ladybug_rhino.fromgeometry import from_face3d, from_linesegment2d + from ladybug_rhino.fromgeometry import from_face3d, from_linesegment3d from ladybug_rhino.grasshopper import all_required_inputs, list_to_data_tree except ImportError as e: raise ImportError('\nFailed to import ladybug_rhino:\n\t{}'.format(e)) -def polygon_to_brep(polygon, z_height): - """Convert a ladybug Polygon2D or list of polygon2D into Rhino breps.""" - if isinstance(polygon, list): # face with holes - verts = [] - for poly in polygon: - verts.append([Point3D(pt.x, pt.y, z_height) for pt in poly]) - return from_face3d(Face3D(verts[0], holes=verts[1:])) - else: - verts = [Point3D(pt.x, pt.y, z_height) for pt in polygon] - return from_face3d(Face3D(verts)) - - if all_required_inputs(ghenv.Component): - # first extract the straight skeleton from the geometry - polyskel, boundaries, hole_polygons = [], [], [] + # extract the straight skeleton and sub-faces from the geometry + skeleton, perim_poly, core_poly = [], [], [] for face in to_face3d(_floor_geo): - # convert the input geometry into Polygon2D for straight skeleton analysis - boundary = Polygon2D.from_array([(pt.x, pt.y) for pt in face.boundary]) - if boundary.is_clockwise: - boundary = boundary.reverse() - holes, z_height = None, face[0].z - if face.has_holes: - holes = [] - for hole in face.holes: - h_poly = Polygon2D.from_array([(pt.x, pt.y) for pt in hole]) - if not h_poly.is_clockwise: - h_poly = h_poly.reverse() - holes.append(h_poly) - boundaries.append(boundary) - hole_polygons.append(holes) - # compute the skeleton and convert to line segments - skel_lines = skeleton_as_edge_list(boundary, holes, tolerance) - skel_lines_rh = [from_linesegment2d(LineSegment2D.from_array(line), z_height) - for line in skel_lines] - polyskel.append(skel_lines_rh) - - # try to compute core/perimeter polygons if an offset_ is input - if offset_: - perim_poly, core_poly = [], [] - for bound, holes in zip(boundaries, hole_polygons): - try: - perim, core = perimeter_core_subpolygons( - bound, offset_, holes, tolerance) - perim_poly.append([polygon_to_brep(p, z_height) for p in perim]) - if holes is None or len(holes) == 0: - core_poly.append([polygon_to_brep(p, z_height) for p in core]) - else: - core_poly.append([polygon_to_brep(core, z_height)]) - except (RuntimeError, TypeError) as e: - print(e) - perim_poly.append(None) - core_poly.append(None) + if offset_ is not None and offset_ > 0: + skel, perim, core = perimeter_core_subfaces_and_skeleton( + face, offset_, tolerance) + skeleton.append([from_linesegment3d(lin) for lin in skel]) + perim_poly.append([from_face3d(p) for p in perim]) + core_poly.append([from_face3d(c) for c in core]) + else: + skel_2d = skeleton_as_edge_list( + face.boundary_polygon2d, face.hole_polygon2d, + tolerance, intersect=True) + skel_3d = [] + for seg in skel_2d: + verts_3d = tuple(face.plane.xy_to_xyz(pt) for pt in seg.vertices) + skel_3d.append(LineSegment3D.from_end_points(*verts_3d)) + skeleton.append([from_linesegment3d(lin) for lin in skel_3d]) # convert outputs to data trees - polyskel = list_to_data_tree(polyskel) + skeleton = list_to_data_tree(skeleton) perim_poly = list_to_data_tree(perim_poly) core_poly = list_to_data_tree(core_poly) diff --git a/honeybee_grasshopper_core/user_objects/HB Straight Skeleton.ghuser b/honeybee_grasshopper_core/user_objects/HB Straight Skeleton.ghuser index 78109717ab246a4fb7fbd30137e480096ef9c7b2..a40bb143b3061ca2293a01f6e83343f88bd81df0 100644 GIT binary patch literal 4958 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 literal 5538 zcmV;T6$R%NJt@+1d`BulioWhC|#-`APT5R zN2I891Sz5*NCy?r7ykcx-@Wg?x7M3AUuK=jIs5Fh&z$+z8UPrNz?s6_5H5t@!~;O` z_d%fr#Ul)K&Tml>%|`GgO+#LuTVll2?!Jx7w{*BQXh*!1i%moY8?dL z1&8(`ps|=gf2L9BAqYT%D*}io;Gk%fD*=diM|dCzSPT%01VS+ZKvygd9f-vcpdLUk z4=4tT1ELUEPXqxMAVC9gq5)XBqVYiD=>o+7p&ocF5QYHy;1O^j0Za76AaF!JKo=|y zA?by{p^1SA91!mV6ZgV;1fZ}OV(fVyVm2h6NCl0j0iZEnJ_MkqiO&B)jF=3ACE5rN zq&ONw#KRLMLSs-8Km*KQAqZathVU1E8o+;e{U@NSQZqIN8<@VKQzJhuTW1Y#)l}63l0H27xz5#|0wwH>hS-fmOWAx&4ddgtLLWt=>2#<5x zPz)Ss3WMVUp&kf#Ao{N^p>aTIWo0Fz5Evu^hrqZH%Y+gYqXAg{XUG4I$^WZ*Ntg!~ zCg};q6XlnLV_hWwpj!R|)ZpJRCA{EBVPbp4DRuq5uy`LF;(SCd{55f;Bo_@_{+<~o zq8UuQ8*gT!M+4wEA0Zk8eH{zpmF;{{QV@SX`l)_D55rsNX_3^AaIF)KizrQFP2#ks zQlCP}i8hs&zAc`Fgtq&9k@jP&;3Om$7z}hYt@Mqyzy`N6q4b}Y&Eew|VrOWyrZGI# zOasWpbTNWWcQ=yfeH@HeM>UR1N{qu0CgtzUaAlH(HOEY2Y+$+Tk!5RB16kzPF8`p= z{a#+6)_CHj=d0y&iu(&4`C)ENA-_`ja`j8ORuh67vwe!n^Vy?*ICaljWL46`e^_W1 zY<&CnJc@DP`jxszHc14qiik*!nVH#uS+g!vN$n<8LPEk4Zj6D2g=KMTbJN$GET-;O z$oebYyOh*=o*$@(JDqxVzT}Ow!Se1oY1pLFKIpm@mzW%7@i4gGq7X;cs9#p!fYmtN z@^4dCE^U0suFY5aW%JvTW+7BVJ-Ri;L_%tLRrpKb$;XC+6J6&wlyU_!Vqzg0b_D>L zcoI)z@xICm#p@3i54!or4iCF0#;HD+jzE!^qOgX89S7t4*6va=3M1K;{G_43I-S$CjYxEOdai6cJyzpB+5^d1 zstsivAJOeVt~?SZjmStVMvdyc3a%7R;il1n40!usyX2*d-e)T4|dVS^*nSA0L{g$9S)o3#2k#M{|1$+0?J( zL3>%A;A9B=kd7Xm$H@_7-!796yb1ZGKCKp}CLo|r5`J+LDom|><$MecbWL>XwVWd_ z8({DRDDi!v=Yxj-*#>#T&sg!wEubQ&a00xlHT0qh)D!V%mt)e`0{-)*@@M`jP0%jx z|LR3T@!y;6`J4;Tk&tZJ1q3sKc}zut##GE^Oyt2vPt$U-4#(h4eA#VHZK4)M^XNJt2bt>w8v(Wi) z)*VcVUN|`m-S!I(>mTk>G3nZ4+m_q3{Cz>PuopHM?PvY8XNg0a>p_h*#8zF76>P+d zIfOYRISh#FkafhJW+aL93OMHU(6fN8H6+rR?U)@kkH){*#l9&>?4wvJEM}F89F-ES z(cddlJQG4|1u;{ak9V`JpWvjr-u)+8R;-2x2p!U8UxDk>O-`09w6W(Qu7*x==Amcp!82 zW(2sg*1$|7`pTN!@{i0oBTwr>pO@jJ3SV7oEkv2X$cNfl>yVe#*RSkD8kx!Y^Rby# zkGsi4n`8ja!qMQT%vpQWweJ^qM*9<-%XzO!f*8m<$_zCOP9MN_a+CJEA!`u~R$$(R zjz`yu7j(QVRRt;&YomIp zdHBROvnqpJML3hme6W#B@>RZ)#!fkVc~u9H&3dNRnWctg*-E@1TD~WN!7J6YuXf12 z$VdwE*d%^8&lC_tN_k%}4oqTkEq>+f=Jz@(tBG>@rB&l#-DdKeCl!xLHL^ZX`jTYD z`LomKsvBCI{hADpnvUqG8{m%xVmU&B^H9o4{UatQb}Ndq>K7AB&9;S69g>t1KD>_n z&vB6=->l{JPq_fpd29I`rKTAt9@f^BB>L~2DQ`x7WGWsG>Y`VA4xrZYv|b%!lN5Z? zNLG~M&%y5q(oRf}Ak&LWy8$_f$QSb;Mta^#@xLp;-K4fCs=shbUlV?^uRNHG)%ScK zGubl;e|2}UE3I~b@AozTr>o%>wPyy7JTHI-O_yFeT^9CO)6q_2Gw;*~!aYXvwufb%P~vm4<#&bVs;|{6KXDJ>Ta8oye6H zxK>?hSqQa{83QBlz z3fa1Zx6j#g3>n6K?AZxpEq{WTbANCE8@0q05B< z(_PxWeU1ix<1^gTmdx4>X=(v(-E|8-f*ldO+v=qaOQq+_FG{XT@gO>&f+6a0Uw&R z45sx98WF2vtWpl;D?LUP%G3A27%edx@efhjY}xm_CQ8$6g_BDZl*pC03o(zHwXI{B z$yKNya11&0FkLn&5xR7Ef8@#A$A|0!G57o86myzLm|074FgFnxYyYmDcTJyG8(^9QpcV+74FcSD&c z#wHAHDP~AyYjgF(FAt|m7anzG_KL6wAZOJHw(%$yQx?G**?Nv)jo2tdWIt{HC<8~T zc?UawZCsZ#;4_q&ZjEeO8=%|IA4BdauanRGR$G+rRw+pHhttW($EQ_IVYnMtO%o5e?|Wr34Oqa~*~6OlBw0Y40PGy75A!Qgpu5#z z=EME7%dGM>KSQ7Iqn@;@;uk5K1`N7+99L!+FZP8Iw@YYL~y8=saapxOE>{T*i7e2!gP4X0ptm zWDAl&7&UKeCsryEe8q&?lwz0#O8|ND<=sPU zngUzu>;Xs7JO~|juF==@pm{Rmsa%Y!oSr)0Z8>Bqi*mY6LL8Tb+<-rQLJ) zz@LQc9gCk7ScLLTR1^PQ{i!P!{c$8SvUP^Nx5!;{g#^C{@rqBXOK)V1TQJTRRc542 z*;@FEj#%+6%H#Ab27U;|GelhMFKm<7Wq6w6ukrS^ow#L*&`ODZ_Hx7PlHq8FuY%5& zLnGLYPjX()t!jEA%4RzT!I_-ds+f($WR7e2p7(d(*DHFL7-ztS^QE0-Vi`WzdnYI- zxz9M1Ii+&e8{svJ)`ogLfPEjLtZdRo@+HfvxTB>SO?n4O!Ix|+l2KCv&wdq1<*UT4`dSOc+ z-&K1m?s?{UT!U~>+PB;j%c*r!TMK|vY{DUfZ`lB1_&R$U$W4k!v2Gq)ZpxY~18sx1 z)n}2ag=OV+70*gY)kb-@>u-PjeOzGlJT}#Y)aKr6n60Y%$)X2pFG@LxCvsiZf?26VIQ(?v*PCVd!XoO_foF+jb}<>9|LT-6&G*c`B}7pKC;bTbeId9 zN?ssk=2tut_bLjBe{vt;mRi&vRQyBGZ|11(st07E4fAGb*!{))n9Rxs_Q_?9Qr~&p z#{O0DfRk6)uJZR!Gr|9;2fvU>E=;l<>zQ@Av8SWb+dkfwTl^&<3-{>&k-y}a7?yQIY7|!MV zmLqFpquv24w>sKxu>0j;)$D(`r}pV-qWWC>cF5ERkC_4GuWJ*s}aO6e5jjzLAGA4^_K%idqULDbT&$DE^ubG@?>P|#X*tYkFlx8z9LgL zzJnd`QAVfG;KL$R{ie@BlC~z+dcr8m9)R4Qo!O4{9L3~qZ4|hiJo=%ZD-pZhf&d?X zo%iZWV^}$o7Hw_#c^bh=*Ev#47o&DMyAMad(7sfCM=|I5vmK{c+>z;*r$gz|K}(S* z7v@A1MuK~!8;a{-uO>z9e{u`0DD2M(c9^$1&lZT!jXK?xcYWn^yr>qsah8d~E}U*Q z>G@rqR`42Zws_qW|2yw#Wn&}s%UFi-y_5IZoy&P4$C)!tVszr-&qBMSHWTJXLW5SD zOoO&U_wNlyq|T`4PkNfYbUGM%Ytk(_?W@Cql3D|v1uaoH*#&!tuz+bN@tx)qZ-A01gAgodoO=XbYvj=6LV ziC@UQRlgsWanf!x>v(q3cg9NRq`n`Rh+16}-dD%idH1r%d~Pe+NwJj=L-`h~DK7@^ z=k6|V)r_~qKDj(EUt6iCa(vIIc+U3+i~HI$W_LRIgF&ZG!tLWqQ0?Q;E)Z|4ys5|% z>oJ@OqjYc-!I0Rb=xFBiv30ENDP!9<&SYg<>GLLgW|4RY)JFM6d_Ug6>L)GReY2W_8r*HT~e zRDJpNY47uk#l=pLx3_V0LQQ5fSH-ZhPi^f*Thc2Irt8c8{PCi+p z0Nc3qZ&-kZDy4ia16E_HRwocl>}u;461SCPhG31k_F^GRs_s@O#l`F61$c=MbrOQu z!oqCZ*)$PnEns`)E1rjZ)*DHNP%#?_xcSvRGA_&u@O2PJYU8hHGy4pJO+W%V3a()PEa%N<^<-_#>B_2&A2iY55H!0=l z?^y#w-c*tAZ?Yiyaz|scU6U+~^;!gXb>!ED-3(pt`OIaNv=@av`Nsb1(P`kw@*Slz zNa?Xv#tuWfA}H*N`|R7&bUUr&)PjHnT5fw6`kN%Bc=;qO+gmiB&R^JT7>PZJ)mQuNx#uhz#R(~;RWaH8e-A~fqufI2XpGd5f z6`2;#%dTgJ(4MUnt5)hv=O4PC*gS8p{5I0p`87pskewm+k>#QL)OG)S&KF9p6rF@- zq0+6!>Pw%uw&vb2-zD%)S7TW1S6(0D;4Co`Ds2`}S9G6&dd2UDJa3+yD5)XFjF9cI z2Ma$yGketul6Ftz>r$x$ZCrFrFe+;%uxW8L-*neblH`;{7)$i$h@Krt0Tku$;4hQH zSTw;d_&YJGco}+1<#4Ye>3vd*WV_l%ah{;A k67nmG%1gVQw8-I?6Sbue?0