From c8ba49abdc35f44e1d3530d161d7ddca0d66ef59 Mon Sep 17 00:00:00 2001 From: KuangZheng Date: Thu, 16 Jan 2025 11:34:48 +0800 Subject: [PATCH 1/2] feat: add support to c4d and blender --- assets/HDAs/OmooLab_Integration.hda | 4 +- assets/HDAs/OmooLab_Pipeline.hda | 4 +- plugins/blender/omoo_asset/__init__.py | 2 +- plugins/blender/omoo_asset/io.py | 160 +++- .../omoo_asset_alembic/RSBaseMaterialNode.c4d | Bin 0 -> 271530 bytes .../plugins/omoo_asset_alembic/menu.pyp | 70 ++ .../omoo_asset_alembic/omoo_assets_abc.pyp | 698 ++++++++++++++++++ 7 files changed, 913 insertions(+), 25 deletions(-) create mode 100644 plugins/cinema4d/plugins/omoo_asset_alembic/RSBaseMaterialNode.c4d create mode 100644 plugins/cinema4d/plugins/omoo_asset_alembic/menu.pyp create mode 100644 plugins/cinema4d/plugins/omoo_asset_alembic/omoo_assets_abc.pyp diff --git a/assets/HDAs/OmooLab_Integration.hda b/assets/HDAs/OmooLab_Integration.hda index 10200b1..d19a3cd 100644 --- a/assets/HDAs/OmooLab_Integration.hda +++ b/assets/HDAs/OmooLab_Integration.hda @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3213804a49ef7a3564c551b9a94a3c9fcfa7c03852c6e8f24168fd78891550a -size 192699 +oid sha256:f4ba4a00f0a44beb6d96e4675ea42d329d3e9446e8a632072a639cd4d6ae164b +size 192523 diff --git a/assets/HDAs/OmooLab_Pipeline.hda b/assets/HDAs/OmooLab_Pipeline.hda index 6d328fe..bb8bdd3 100644 --- a/assets/HDAs/OmooLab_Pipeline.hda +++ b/assets/HDAs/OmooLab_Pipeline.hda @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2cef20ec3b3fd78081db00c65c7b4eb4537ed8bb40402e2424d372993f757df -size 1178653 +oid sha256:f1ee8d3ea233aa7dcbe847dff6f5829306573501d1794a05d7b107bb44d6ccbd +size 5453015 diff --git a/plugins/blender/omoo_asset/__init__.py b/plugins/blender/omoo_asset/__init__.py index 6e9683b..0970edd 100644 --- a/plugins/blender/omoo_asset/__init__.py +++ b/plugins/blender/omoo_asset/__init__.py @@ -29,7 +29,7 @@ "author": "MaNan", "description": "", "blender": (2, 80, 0), - "version": (0, 0, 1), + "version": (0, 0, 3), "location": "File > Import-Export", "warning": "", "category": "Import-Export" diff --git a/plugins/blender/omoo_asset/io.py b/plugins/blender/omoo_asset/io.py index 6a0d697..b6232f9 100644 --- a/plugins/blender/omoo_asset/io.py +++ b/plugins/blender/omoo_asset/io.py @@ -18,10 +18,30 @@ NODE_DICT = { + "BaseColorMix": { + "type": "mix", + "to": ["SurfaceShader:Base Color"] + }, + "SubsurfaceRadiusMix": { + "type": "mix", + "to": ["SurfaceShader:Subsurface Radius"] + }, + "PointColor": { + "type": "attribute", # 用来标记它是一个从顶点色读取的节点 + "default": "displayColor", + "source": "PointColor:varname", # 顶点色的默认来源 + "to": ["BaseColorMix:2","SubsurfaceRadiusMix:2"] + }, + "PointColorWeight": { + "type": "float", + "default": 0.0, + "source": "PointColorWeight:value", + "to": ["BaseColorMix:0","SubsurfaceRadiusMix:0"] + }, "BaseColor": { "type": "color_tex", "source": "BaseColor:file", - "to": ["SurfaceShader:Base Color"] + "to": ["BaseColorMix:1"] }, "Metalness": { "type": "float_tex", @@ -95,7 +115,7 @@ "SubsurfaceRadius": { "type": "color_tex", "source": "SubsurfaceRadius:file", - "to": ["SurfaceShader:Subsurface Radius"] + "to": ["SubsurfaceRadiusMix:1"] }, "SubsurfaceScale": { "type": "float", @@ -187,7 +207,8 @@ "default": 0.0, "source": ":displacement_on", "to": ["DisplacementShader:Scale"] - } + }, + } PROP_DICT = { @@ -253,9 +274,12 @@ def execute(self, context): geometry_paths = [] for geometry_prim in geometries_prim.GetChildren(): geometry_paths.append(geometry_prim.GetPath()) + + print("file_path",file_path) # import usd - bpy.ops.wm.usd_import(filepath=file_path.as_posix(), relative_path=True) + # bpy.ops.wm.usd_import(filepath=file_path.as_posix(), relative_path=True) + bpy.ops.wm.usd_import(filepath=str(file_path), relative_path=True) # edit materials materials = set() @@ -282,7 +306,11 @@ def execute(self, context): links = material.node_tree.links # Modify/Create "Surface Shader" and "Displacement Shader" - surface_node = nodes.get("Principled BSDF") + surface_node = None + for node in nodes: + if node.type == 'BSDF_PRINCIPLED': + surface_node = node + break # 找到后立即停止遍历 surface_node.name = "SurfaceShader" surface_node.label = "SurfaceShader" @@ -291,8 +319,13 @@ def execute(self, context): displacement_node.label = "DisplacementShader" displacement_node.inputs[1].default_value = 0 - out_node = nodes.get("Material Output") - + # 查找类型为 OUTPUT_MATERIAL 的节点 + out_node = None + for node in nodes: + if node.type == 'OUTPUT_MATERIAL': + out_node = node + break # 找到后停止遍历 + links.new( displacement_node.outputs[0], out_node.inputs['Displacement'] @@ -342,23 +375,42 @@ def execute(self, context): # get texture default value tex_default_value = 0.0 if node_type == "float_tex" else ( 0.0, 0.0, 0.0, 0.0) + if shader_prim.IsValid(): shader_default = shader_prim.GetAttribute( "inputs:default") if shader_default.IsValid(): - tex_default_value = shader_default.Get(0) - if node_type != "float_tex": - tex_default_value = ( - tex_default_value[0], tex_default_value[1], tex_default_value[2], 1.0) - # print(tex_default_value) + if shader_default.Get(0) != None: + tex_default_value = shader_default.Get(0) + print(f"{node_name}",tex_default_value) + if node_type != "float_tex": + tex_default_value = ( + tex_default_value[0], tex_default_value[1], tex_default_value[2], 1.0) + + if node_value: # blender cannot read UNC path with slash node_value = node_value.replace("/", "\\") node = nodes.new('ShaderNodeTexImage') node.image = bpy.data.images.load(node_value) node.image.source = 'TILED' - node.image.colorspace_settings.name = 'Linear Rec.709 (sRGB)'\ - if node_type == "color_tex" else 'Raw' + + # 尝试设置颜色空间为 Linear Rec.709 (sRGB) 或 Linear Rec.709 + try: + if node_type == "color_tex": + # 首先尝试设置为 'Linear Rec.709 (sRGB)' + node.image.colorspace_settings.name = 'Linear Rec.709 (sRGB)' + else: + # 如果是非颜色纹理,使用 'Non-Color' + node.image.colorspace_settings.name = 'Raw' + except Exception as e: + print(f"Error setting colorspace for {node_value}: {e}") + # 如果出错,则设置为 'Linear Rec.709' + if node_type == "color_tex": + node.image.colorspace_settings.name = 'Linear Rec.709' + else: + # 如果是非颜色纹理,使用 'Non-Color' + node.image.colorspace_settings.name = 'Non-Color' else: # if not get any texture use constant value node node = nodes.new('ShaderNodeRGB') \ @@ -396,7 +448,15 @@ def execute(self, context): elif node_type == "float": node = nodes.new('ShaderNodeValue') node.outputs[0].default_value = node_value - + + elif node_type == "attribute": + # 新增:处理顶点色的读取节点 + node = nodes.new('ShaderNodeAttribute') + node.attribute_name = node_value # 使用 default 或 source 中的属性名 + + elif node_type == "mix": + # 创建一个 Mix RGB 节点 + node = nodes.new('ShaderNodeMixRGB') else: ... @@ -434,7 +494,8 @@ def execute(self, context): surface_node.inputs[prop_name].default_value = prop_value - surface_node.subsurface_method = 'BURLEY' + # surface_node.subsurface_method = 'BURLEY' + surface_node.subsurface_method = 'RANDOM_WALK' nodes["SubsurfaceScale"].outputs[0].default_value *= 10 nodes["CombinedNormal"].inputs[0].default_value = 0.5 @@ -443,17 +504,76 @@ def execute(self, context): material.cycles.displacement_method = 'DISPLACEMENT' def add_driver(prop_name): - driver = nodes[prop_name].outputs[0].driver_add( - "default_value") + + # 获取目标节点的输出端口 + output_port = nodes[prop_name].outputs[0] + + # 创建驱动器 + driver = output_port.driver_add("default_value") + + # 创建驱动器变量 var1 = driver.driver.variables.new() var1.name = prop_name var1.targets[0].id_type = 'MATERIAL' var1.targets[0].id = material var1.targets[0].data_path = f'["{prop_name}"]' + + # 设置驱动器表达式 driver.driver.expression = var1.name - add_driver('scene_scale') - add_driver('displacement_on') + for link in nodes[prop_name].outputs[0].links: + material.node_tree.links.remove(link) + + # add_driver('scene_scale') + # add_driver('displacement_on') + + # link the nodes + for node_name in NODE_DICT.keys(): + node = nodes.get(node_name) + to_list = NODE_DICT[node_name]["to"] + + for to in to_list: + # print(node_name, to) + target_node = nodes.get(to.split(":")[0]) + target_input = to.split(":")[1] + links.new( + node.outputs[0], + target_node.inputs[ + int(target_input) + if target_input.isnumeric() else target_input + ] + ) + + # 创建 半透 BSDF 节点 + translucent_bsdf = nodes.new(type="ShaderNodeBsdfTranslucent") + translucent_bsdf.location = (-400, 200) + translucent_bsdf.name = "TranslucentBSDF" + # 创建 Ambient Occlusion (AO) 节点 + ao_node = nodes.new(type="ShaderNodeAmbientOcclusion") + ao_node.location = (-600, 200) + ao_node.name = "AO" + ao_node.label = "AO" + # 创建混合着色器节点 + mix_shader = nodes.new(type="ShaderNodeMixShader") + mix_shader.location = (-200, 100) + mix_shader.name = "MixShader" + mix_shader.label = "MixShader" + # 创建 Gamma 节点 + gamma_node = nodes.new(type="ShaderNodeGamma") + gamma_node.location = (-200, 0) + gamma_node.name = "Gamma" + gamma_node.label = "Gamma" + gamma_node.inputs[1].default_value = 0.45 + + links.new(ao_node.outputs[0], mix_shader.inputs[0]) # AO 到混合着色器 + links.new(surface_node.outputs["BSDF"], mix_shader.inputs[2]) # SurfaceShader 到混合着色器 + links.new(translucent_bsdf.outputs["BSDF"], mix_shader.inputs[1]) + links.new(mix_shader.outputs[0], out_node.inputs["Surface"]) # 混合着色器到材质输出 + base_color_mix = nodes.get("BaseColorMix") + links.new(base_color_mix.outputs[0], translucent_bsdf.inputs["Color"]) + point_color = nodes.get("PointColor") + links.new(point_color.outputs[0], gamma_node.inputs[0]) + links.new(gamma_node.outputs[0], base_color_mix.inputs[2]) return {'FINISHED'} diff --git a/plugins/cinema4d/plugins/omoo_asset_alembic/RSBaseMaterialNode.c4d b/plugins/cinema4d/plugins/omoo_asset_alembic/RSBaseMaterialNode.c4d new file mode 100644 index 0000000000000000000000000000000000000000..7319743bd4e30adac46facc65547e3a2090aa5d3 GIT binary patch literal 271530 zcmeEv2Ygh;^Y}}F9LGT}^dX`y@UQ)*;k+op}KbjygmG}0X?3LP_X} zEJ97_p`b656YA{l#|OJGAg-k=1LDk1d@z6yDi{!#9n1&a_@Fx<^x%X2NRV#37l6&1 zqd)jU{lb+_O+ZB0?hdbRNeCZg`34b4@*uT&bDt6O^K7;&9{~zCrFnDTMxCF>EBdqn z6vaQv3Zjs(twYL1NM1pI*shfDyHdjMXh{dij4F&yjW|`l<^`0W&o94%2ipC{fWWZb zbpR1}PUwa%K;58FEf|!<5l~pWa4WGIph|DCO=`~1(SiYS3tRHRP(Ij-4-Vmjt@$7b z5E}f>=qthNTfBuJNm~*{vWwdwiDU|4hNBn|tWpxqfH-w6fIJyU*70+!Cv$LX*ucrr z&ihgkfbZ}%&mQ4R_}Pj{LX5akj`uk^+I~4@KbgblDJ5HkI>XO#hy$YX6UhR;bax@t zB#Oqip>0BMD&7M=-l0%y7QnR-Q14PGT9WO&FDn5>^)mwB1kik=tbvlD(tK9d_NXCy zEzGyil6BlB>DWAjxV39ES0BJmfuhrRM6-4>$y;s&plkv2%rLk3-CfyxkBm>mkKx?$U;;3 z9+({Q8i$Ill*)262KbHj5y^rb2%>V5Y=O_uK%~cGpE9xtB4CSqhLeR#;%2dfirAq5 zN;M`?zIMB&9G|=61fV2rd96RH`-b?nSNCVXfi?|OW~qi1P)Xl zpfCd~$!kJVIMgWKg1gbY62kUe}A`5kmAO+=$m&60P@aO@J%WXYamPuLe_np*UQveW-*zR6;3618vkETno4K z_DyfMZ4>WF<>8d364=t#!qS6p%y?^2NkXEa^lmDFEp0=UW}C^xhm2sWhoPf&-0?9$ zpH;0`0QtIzRi5XOB=Le=k&X-Qx>ct~uHh^XHY4 z1@b!&T)Ld{u6d#Osna z%Xwf6tmsU4w7^P^CzQZoi=n7@K2g)|%nE+6vsLR-1X80~=}&`JWFqMRBUnvl^QvjX z`c=@2Hp?ItR}+qWl$yv#K1++{uf>E8!OrMc4)R`r$wzk@SAZZLQH1hqhzueVm&!yC z0U*!#THEsmXat=eJ!>zg|L$F=l(4_FsWFhUSU9-nOzycsbNeiXjDr!YQmYZ#bVxZDm^6q zLWcKHAOXswX!|rwK0f)$=T|DMzfRsE3l(~yeE0%XB$VRYK@^25V{=6V3Ui6Q4yuJp zG`<5#>}yaK%5T$mk{U^c@2pxT3z-;BqBwdbLV@wet-fO zAqT&POZ-BY0mQD-mWU-%pimhH6;7hS^^qdL73c9mJ%C902csLh5Y!53k^KxA?b*Rq zVm$hT7vq>eOvDNqxL4H^j40D9p5pZyrIw<$q!f5^wL8b;Ks6I~KQSO^x!q|#h;4}_#}4(l%dssY z>Zi%I7$}eI(V~$Taml-2vS!+cQTl9I>{-eqjBJ~ZJ!w3hfsGzv61JC2HV#mk(#9bz z8@YnWKpsd(;16827<-b7(n!iDNYbdHMhBrbMnCOXcZ~ZGt1Fwzni@{Unrn8%AXoK; zS?CTzmD~LR9I#nP*4V$01+UaGY)DW*V3B6fwa~;knn9!JJMcgX-ZITF?n0(-jXRy~ zTaISXwU$ypn9?+Y&cNyw%V1q-hNL;rN1DNorIMC_VuWsYh6hUe@IX6uq{Zg^gqJGm z!UOG|F(CFE_W|U276}y((qRh9Ai+^1W(NvMT%i!FHO5_yQpsTi(Jezc(S?fH&&0)q z-jU3Usly<`F$3HjHfF(KIn>Qc20Ka};M#InhxR`@8qD! zAg=LOfFjufRhpszSg1An2l`+&^|}|{@D9i-_OtVij4dr ztN{@nW@X*l#&peyCR*2&6BXm(7mp9Rpg1RFL4t4^{~Es_%U5(*gNBgJ8vQp0-HQPr z9PnRMD_+LGiXbT#6Z|n=ig&oO8aooYm*w&ikc|O7R&hb1y8{p8>`Q}&NIaQT=;Z@B z08WHM2+3%zr7t=)phk;iI-&smN<+WLfB^s^Go~UvKw*PK^Qb4IaIJt^47uoTv1tDQGTY*ASw@#7 z!R0haK3$O{BXcq_WNiRNL6uyr47VT``-N42Vgwt_VO|=tNeqVw099kOOrP zXCA{kW=xU|18h)Uo99%4GE@#-fT{qLkr-K$zyo=(qMd`E|DnS@Zk#y?C_I4;25K{%pK7Z>!;0~s3p1EJHQ0NO@_q3%Ee`a?`AMSp}V5@bLP zwI8m6$*!b$!(1{#XNHXG zVdwx0oKN@W^&*81t4)!%s26E%ixf@dEm|(2Y>&Cs8$MUJ8N)Xkc)wn?uDSznTPs=Z zohyQ276-$qx3xR4&jY6{BZ5%ala1p{JwAois(zfTA1ASqvul&=KwiC3QCNyGm-=y1 zD4gnHcp|MIC$S3GX6R8rPS&bO`Qv2il$wl_jsK60lUR`G4|0C>^GRA5)V)5eM1o-a z7SE~I&nK~tFv6)HCkf)ex9U+pPS&cH^2bSH<^uof&nG)hx~c=_xa+n!w&x-Q3%RHO z&cyy97ji9_Pof!Rv(eE{q0Xu=_*y;@#ph7a{?J|G3;j#kb>ll?oLtjoS3LPrElnlY zcVR}%w$-xb@wQf{rq+d2iB?Ckz=0ZaSjMb97oXMn|6LrP@kGL-c(YFMU@} zG$;SdqM=su1)_lIzuh&E3|@WbZ3Iw?0*Uw}Avcj06(r)Nd|o1`7P=sj-DtxgyQ_~u z&OQNOO9^F7wDd%sbcMYZ?`7kSC0I);zz=Mo9pxIYCDE+~fz|K1kjg{<@LKl=j-s5YOb1sb=i_3+JrAp{ z-4dFbIJNKVL`1b@x3o*E_7pX}CZhT7zbu++Oi^h?0fQ4xQ7KVDVqHv8>4S3BTP^su zPP(G*rl@plL7MebRBGI%ZuL`CBnV~}#+y&oZ{uJcVT4mZMXle)5q-By7527Ju#cm) z|Ll2Se!xqpYZ9^?tD|x9zcNLAqb9;RS|`G(rBJtiiVDx{n3*;IPo}7pz5!?KwA^Tl zO2pPgDUZ~NQr7Ym6|ZG*=k9cy>*HH~Yzt6*e2av@gmCroE$9S}K<b3QO@sTE7JY z@@4f+C@xDdJdxIK!PKfq`SES(sG5wHhwEsx{IA5fG4IsiNX?--5l(%4%iHh3o*e#| z-W4+5dZA$mmQ3#o{ZHasO5cDp)A4O+O_cIrohW53$G7x8Lv^=cN{80OsU4^j5!I62 z(xR#M7EI5Y>?-g7FN>xcTQIbufWZm3U?@>RVqI*(5FRzr(tUN(6?L}-L$?;BS-%B? zBRb|!{S+06qJD~MO-)RZiPTR~N#poG^8S$eDJt$R#)K`I;|udJvwr>*l~C1WoZMSS z4Z=8+Q`FM4HJPIBt`nuKZr=sW*?2Ln;m}>fbzug<@3iqt5+ z&8|s&`%N8$gZ8%;FETj03?~eSBN1kwblaD_l0L9)_o-_3>@3YAHXyEnQXpSK7C=`l^mb z%UX_atGyrJZEv;T_utlPXPpSAmf~Cbs6us5aS+FABAOll7erHSttqRCXtw{CMN^Ga z9JHc9XevC#L5T_y>*5qg=>o|r_#|`n9;Dq?CtXo@r#R@=f;8(-aquPq^$!|=IO=Bs z|I)@m{VI2@hO98Ls-Feasz~{>0OI?W8>>G~ZmpwnvX*86)d^ZCd58xs)U2TPf);y1 zLVl}H_^DNP<5)s|dXIc_5R?c+3~)ZqXVjziFP2xoN4L~bkD7eTCA{a-s?tx2*@i=t zu>@f2+%|#_M)Cy$Ife;Q_+xLpcTL&i?@IDf$-;t3LR37USbh@tApTq-`c$G5wp@6w z&2}hB!#9aVy}=a1Irtw_v>nM$O<}-N^vgC1K*ad<@8S9ZP%#vWn!Nxj7EqW=TtgfW zw0g(~AMwD_W$0~SyX5c^Ft{2+B7E605DhqJ;sC66Asq3G72?siL6zt$p9b_5P$m9iXb$@BX9=W20Xb71lXGA| zRJWud3JKV{B@NMc;fIl7=2a z8vU3;NEsy!*-yse9`s^TG1#9Edh@{n0Gg8LKy!(HkZ5)wXy^}F_{~=Uifz8p*Z{2w zFjpz|8s;m-UW8vM>;AAru>0fZz`n+m7?cdp*kDqHt1M=>IBP27KtJes4kQ%8&IYit2LLHV=gh5 ztceo|G|UsC^g@(Dh$;q@DHco2C5|?i_?EfEQb^>h#dslVf)F)Ph?)#2Q~Xacm-xQ9 z#1G6R?t?_EOIFa!`xy|8a*~Dz7!ao(1d#jjQAxu?B+BQV(N}`kw?LwjG(1eAeB~%J zx&Vd8CVs(_R6&mCJ|#q*rch;=6sGkvpiD{W7juc{%_Uwimskdg{9e97p-#Kw3tD-V zLU9b~8inHUf1N^chCh0}dfeEqei5I&<}-1<+qzTz_WMllliL6Mpw2)WpZY{TY~S+2 zi);V9;2KXHO{W-B~H}Wo$hPQz*$}%@RA)trh8=r;G^qnVtaM)*AeDJvP zn46I04=T&K7{d(T|SS&)d85a*rE2j}p? z5BcE7d~hy+{GOUep&qgE#z#Wbd=ll0io*pwF94LO)-E)cxX4^04uVaQ-3TRFQEUPb zMcGV9Eor!!L?JxBfYF_RGWQZ%Ny9A^AxE~hfd=2_`x1`~=$_Z_UEG&ax zQFJ-RUe3?)fDb<8ff6Pzl|VOj@`Y4!*$|!l1&P!Pey#4j{N>>2HZa zA`U0GiBv&cGRPd+U~`E>1c}slKB3MKK_Z7+KXZwAmS}=o8SPsbr%K3v17dW45i;Zk zxFGgCY$|pkEQp;B14=xgEOIcnNJBEdqf`kFGP&kQNN}LZNAZG9LNb`gldTRwRG=lY|8A$|6b)-=N{E6#CwNM+yVjg5}ni({DP ze76QNH&Ov9p2~2a-2rkqq(C{I1TOz}@5E+MNDYO|_CTX_k}f+g5{)td#jx2ZlO%$L zBJu~Nk~_A1=I)VnId)=e%cDzv?+1;@Gkh^spo0fIM2DGw6W!k?>ChD;sQWmEjsyy^ z!y=aIUyzF0Tt*f#zC#IiWDWMEDl$Ycz2-;9yl2e-v_L4j{Z3(6}+%EXTZ-}imiQ8X0z7EQse}Oy(v-7*bGC6Flhi z+TJ7-a_~`MLKGhJk+gTJuqc66M0Gx70CMD*vo-UI{w7JaOD?K*$>FtcELLbilW3uY zVn`H6)0)HzQE@_)&~G;p`rRhjLGrt4sE~sn5GCqi_ zWb2jkQ{_BRhNCmcY=x~3M`ottST;uUMbi=~J4ryIk;qsO(Ui!RKt0HHmiwz62Tj{( ze1|Mc$t=F|kCl^`A4xU7BW*pOLS{jvL@$)(1{M`(T)IPUO!)jlPb>)+Z~Ml79C~I& zk+6b}DP`wF2$V=#LPfZDn-bw@sl5a~6+t)>u8_#e3{@tPI6z)3VAQ7)Nec!<9NC z?lLbX6@&fxpf?{J0H7(U3^bSM2ZF0#K~B1qyQn0E)*EwG#_X zje$h-l~O*L%SSxF(gb0p*lTkurH}POrAnqaHm+1*aUF?5yx|K9r3q0gAxaG>ERKdy z2#1_(Pi8QwxF<9DpoS#JS!S1vqEIJTKC(y@i9mIsTCxGfZ7erYYcA2;GP@*~s+?oM zc|w$4h%yLK#eg!!Vu`uL(dH80GM88iiF|DxFGNicq9zJaINmeG{}jk?D)D`Di659t z+y{wRd90w9_cI_GjU|!;42V+?0?3_9N+gF!6la-Ta+pMMmf0mIgauDh1vwpmN{Bj5 zp~_Ij=mMU829zm5{9-Qgyt%{+<`T;wk>AT#C=|QQF1bpfIEHkMLY1NN=mP$)Qz*{x z_dB4FgEGC$F1bZzF}=(#xlLs;z05B8gUVvGyiPB-1ABKMiz)HmHJ4~^nO$&w#o@@ zhb%0EUr}`Bm@MX|oS)+XAAHCIr7Zyz2|C?SyQFN;X#=1GQxtd7f&oP8$$+@a_(7+XA9PB+`I!d*Xi6&Pf=;O) zlr$UvN&O)UUp*8|^;p{ig*gHM#p8&16Ocq21BvD**P?$3aI;H%eQ_2rIrTm~%DhxWM!k|+s3_7Le zf=;Qqpi^ot=#&bBPH7(5V$K9Wsu!XRLX^3nQ(6M~d7VXy9c%w+bBX4HPHCx-jqyU% z1R-jo5M?gtl$r}VrRIW8skxw2x)0dEx?}~qw4VVR0C<1_aq2;mXG(Bba)(G1C+L(4 zgHEY1=#-wM3bH|`^pp^FnnIP4bplW3f==l#kl$10C(Vj~;Z;`8WBkThJk4&?yxLozewB%M{NG%_W)(I;G}o$?iVfU~k(#<3av4$^b&Q3s?4LYS;C_;{GsjVc66Ld-|4?3m%pi}x4MVAdarR9La zy@|W;0UvzG17$4%6bU-rQM+XPpi}0+6t#iW77PgW$eQuN=6ui*K%QR6I+G|;Ac$i` zql*yLRfuvD(&{Ee34%`g>%>rzxu8?l6Ns76JDItlQ)Vvcl$i@UWx}9S<_c^u{U~$e zgQ$bBK-r*E=0TxYmoDqa6oLUn#t%AW{Ge0D4?1PuOj+DF0{}E76>~wS%nwSMjsIl+ zkcF=v3Z{CjZGplZ0f6FhM177Ani>O%<|~aAl8grwmN~98L6{>BP~1xCZ=^t_N~X9q zfT_abIueCw;0to42~jE`N)0H?83dg&e$Xl72c0s0&?yrJoibt2DH8^rGIK$v%v{hZ zGZ%Eqgh8h)k8Cj~=#=S&D1#7XF6fk%Kz?p#ky0m}&1iFp=7LW0_z7QI#|t@|AVf_R zqRa)IGIK$v%v{hZGZ%Eq_5mAMm#jdS_A_7u01q%APCZES%$3|B62%ERWx}9SCJZ`d zC#ixQ4|Ph2I!&R<$U1>1b3v!<7szj_m(QC^G#7Nr%7mO)DIaDr}BeN*<48F+an!xvdfw>VbDqLck@;{Web3oDV`UaOEedB%FG3wvW@)K zZ6ce)2|8t)NfcrYUm%j5fHE6&%C=C19NAJ^NfamOlvN&d%J@O2>??{c8+6Lb0fl=L zchCbq_>cwpPl=XpTj{T{MiD3~?L@>9afCnGZa1Lq_6T&($J_22^edZ)By@y|z@s3l z0i95QKSU||*HoFUQ5xI;giB^sivG%N$F#~_`$n5abL`t&0UAHvN1(Bj=zz;>0s9X6 zXJrrb66;d*lcr?=K<}!|6tUWCYH%3(_s_y72_j zs$<2^6=j4xG$n~20o6S$J56h_bs-3CPQE@i z*xzwjQnI7ndBU1#L Qgfh*L8xa>255L8)$LG*F7IdBN1GL2Rn8Y?#sSRmJJ0vKF zZ(6QCAN@wW5|Ksux$t{F`rRTeQ;CjyS*=CCd+QN{==aZnPbGiff!`VUH_~zRTOS`A zfZ7Hp@*Fcbzf(EC(=zp1l*a?*anfcNARh1qEs?2n65`O^3H|GUtR$8QM)u-Jgb;y5 z06K=`N_eBco-2Pjc(58B zG(+1#&C%E^9Zt#(r9nFJ$xj3k{}w@n|3DB6781m(D8K!PiVH|iNbhQN0g)OJfWj4f z2?q(0OeEV_bs?-BtZW>tDh?8zkODOzX+tC6va+_ZZP3t8(n#7EW!Nf5mSyp>PZTzg|lCJ(6_qB{YdEd!BRX?GTv{maiZQFHw zwR?}(dV2Kp^y=?DAV3iq6g(s(G%h|NaVQdano6CXk*OJF$S)`?DlQp4@tsNUzBhTw z`?KeKICtJh^A{{!@yW_npRWFF&F7moZ`rzS`;MJo?LTnv(BUITk9~XU^qI5ge){>B z^H;83yME)ho4?<>UH;&&hmRir{p9I0XqOdX!-!0@T@I*S*0#1bwsz1iE9)X?xPxtj z&aMrc`Nh~N^O|>Y8!u_$KWq8MeT}-h$KG$5sy``h<>YatTRAihx6HDJO{iAOn1-?K zI#0-KtWf9KI1oNWdD`>H;$Df$Pm?rX9ou>4y`LSwAL9D@i3e#NwqLw-t>ia!b?3#MaY4f144i2Bih7H+kb?kWZKfV>jJD(i*cK*vtrw04B zih43XdCbZe%QAkrcO>$m!?rKJt{^5}E`Pp$%&IG1A3PqlW!DFX<94rH(&gjiLtEzl zy5{k)$9ooU`uKp){`hNlcH@u4^t~HX)@)Mp@nLUxuE|VGc)Iueg(-ynqK^;0Ibn3_ zZo@xcz3p1ueqUk7SI+d)rhZ%8`_9((H>dwt+Q+|{w;u5KtD6)Q>eOmX_YGq|{pn6h#;L86@XyQsZ00&|MD~#mWz%;}==C(~+@PeZ zQSwGXTi0G5)vMpWujb~Y_ui0ec)ovG&+8fEr+sccDcT{g^!p#b{}cU7S}-L&J#Aik z+U#MoL!7QJiLlxJ=$}HL&Fg#~-+0n(y@%_qYTHCxCi=BzydB^*`n3#O~ z*}6H0x_!E8Z1(*-52kv>y}GY$VxV+=>(mcNbZ#&>+I4(N_6u1BG2xxJPwZ*%>6c$l zJmgk9asT6k!Jad|7=9!_@z-O&xn1Z#`&YXTpFf@IYdhxGF?)w^_W8s|R^0TjvIY;A z3@T4}v~BsS$(_PxH}UkAT@8Ec(C62C14EaOaJlWi_|f|PQRkQLU6A&;iTb0wn@7X% z+6;W}hubc8Z@fJFVB7fT=`RNSv*xAzqUL;u2bz~&?~d)aevEhCjel1BcD2u(Z~M$@ z`?ss@mDasiPd|H8UmSO6oMXohKlys^8=$`Ot9^$V-P`;z^-+sMS+`$Q5M|@SZW?~S zm{RuR#`3WE-A~4^xc$}GPv>4OX}Ub4!KrU^lv6$#a<==={!^@`&a)|PI^e9;bLZk+ zdH;O#Nd!mMy?w;_(?~9~cotkcG+$a6F%b~kBPg=I{yXb|z27XpS6klk2_oixA zz{HuMZ$3JDv*VNsqLK4ECG8&1nRv8;1ZYe*RQ|P?I+#VSL~xsrSCkGT@Z3U%X_)^$V~^I z#!mV2%RZ-H`Q!cZz^UaQR z=o+=HV4U~W9ZULqIXY}U-eaQd+m&{Xow}93S2{>j)+8vR*WtI@o!vEXZ1c(I`vm>+ z$<(7m-#+?h^fzmdZ~m>9?8kVUnXW0jeC|hGo=`YWy4$|*j6uOyo~(P@vH0Adx67Ri z+V_u3JT>O^2Y#!6zHt1@@OdAd2AGA6WdV129<$doe70vd}Io={KyYCJ6)@r}*^N(ElykU!&K%!ttqw<`M zWgD7l7CxT#dCD!XRv#7K>vryA_XGMbdw;HWZK3-9i;maYbZ@Xb@%!xCm(OL~ne}`~ zaQO%O6Bl|-zVYtm!^xft-uzxV|ECeBm$V#KFkJiR!7Y=*iyV5nAL;q=!Rzkh93MTi zdG7rp;>^SO53??IANtMaGamMv4{y3KGJN~1=LZEEG6r@D>Nexfoo?+nEicV&^z^+? zChS|Q?j7Id)WMk5?am}r5R*Uo{`0u3qWn1@439i^G4e=5ua>=??w_8WGb!Y&eydK+ z`Ka%vHRH;?+6HH|-UL1nnV-%r-MoH#+O?DGzq|U%5~l^)JL|LWZ4Dg0dbsy@J=eaSUO08) z+zw+S^Bx|%|K2-$eos1Ozvt&GHy^II?|wS$jdSA+16vobTL0Yn(i=<1Ut78*=!=yv z98RnpM+^_V^8BBoA!j33_wMoPUu(N;pWDi1eQs&^m3_9_V`bMJUM#zn=XEgUgJ=Dg ztiS70{@#p-TIZ+dOS}fP+x@j>b;zPgy&MNCe*P=%m+kFW4BMNKa_hq58C!I%-u`|` z_a~FCE{?eR#khvITPMGD;k#~Iw>bPb&C&Xln||0br-3=w=CypWcTD#)WhVyr95djo z?nc%Px>vPoEfm&U^gR5U} z<)mq9IBjJ(-r@Sg^?eimS^3iT_NHU6E-jDSw|&;K!lLcx)~OZIUY=KHyF@IXxg*u{ z&k>JWmMv}Nqd7kD@g=MB`6mW0WzQ`1SGfBNyKKabjWdgRDUZC>fRZAz<%OW9NX zdtO_T+q9{?VB5#XkIa~{RIYxqsC@XLC9k=C|5%^$icjeDGzS;W`9kg0>7JA4X0BX3 zVr0;v1(SCiyRg6S^pk-$(jF^HrH`W~oIh~uW%n@usE_n%bJEK`56@ZStPi@XoK@Oj z7|~|JCx!RguY0|H{6~Rj?)K}S7NXuV^XROdo!f46n|7#VZ=c0QhkJe9=U{N-Y0H!K zd8^y*IHU1SUVM7$#d+(GtyX`r?c=2#eVUdJ8}Kmo!___BSg7rL$!|@-&=tGpu75am z_qL7y^mx(n+>G~jTX$+!{Q9^}F9JTfoe_FQXI(PkX6W=gPpuBVv*FJJf7&*EXZ*e! z!AI^*QQ08>Y8P|(EwP-7`Hse&AB^bLdvc=#pQ#3ZSMfIri;F$BDnrG#nb|z=T+*^rR7Z@O)E&%Kbh)6;#Qx$~U z$-tIfu?m11^jJS}|B_B$m8VR}@7v^;^1OAU=8Ych884Z4RUMMiZN~-M!K&}QyQOtb zm_Ny}6N>4+%osnWQ`YmrPtu0(1{)gRAzW6#T$QPcMPp1G-l*!#uOm(JTm zFMRqeRMUT6ad_FrIk_`cla6IK*w?1*(Y4Q>j@j_f+sV6EZQ1(O z={LTy5!1x9oVo|6Q&8!brcuw7zOr*QYDW z4$EFusLqER_Un0h=B0s8ny-73I{e*@%`YC@ba8&y2j4wC8R+=nTz9{H4-U-L7=BaQ zwE6YRW@o=?9C@?!WL%W)_099wzq#;|-?8b%m%6v<78x6u`25@NPJce@OuKhy?tj0> zGV4A5d(tmY=-BK2{v+j$6Ha+}JKtNUy>YTnzmcK+S8d%g|Ig>)GbjJN@-MG>{<_qS zE0)gv`?DvD_I3K@@w1nUKD+&_;P4l#li%p|!|Frw3;8SBhV;pE`tpE$>&y$^CoEVP z)Z_j4@-}S9c(S=Hp&;>GziB>q4!^#w%gciO^Utgumi}#Un>Rgg%5uM5J3cH+vQ-n3 zl&`xOvE^?1xjlUnpC|45?ZSwSx>e`DIko%iSMOa+ew?LV^LSu!%E1ANn;hXQZZrATl_YT)eJ>6D#N(VLzIjnp9_B*?LroI@{A-61`7jfWSt4W6? zcKy3f@ZK=~v1sIp<)zeeR0M_m6#^(nk4<^Znx;@AV$osEtctZ~q3z5}u}w z`(UtD(<{0SBLlYm^gR0M;mG#Ceb{Nf;i%mb-$9Axu^;VAKlkkNjg_j0S#Kr%k)H9z z?scOcJudn>@j-IZrweob-g@!lJsIsEPk8dKlk4io`j_P~(YM~395A!Up>tcGo{PsX z9>33V=&2>GPmYPXw#RPclkvr0UKsWMrVBr7{2qS&&Nj!HKQ-^VWvYsB_y4+Zdef2( zQRTa5tvpn=$Kkt<3sxWLdF$`qo4U>2G-PcTC;1=pH~p^5_4ZvCzBoL%^G}C%5m^iO zTK`r+j4E2zAYp3H((b?85&1T&@)i|^g>`$f`Ocylzihs;xyu~&npeIZmJz>WR?_(A z&qsgSwD|grbGtq{l(K2)go0tZrXM~2toz{1$v>m4z?fo2f9&q$_a+vV;{Ej_# zcPY0!zhK<-cP1g5Vn8pK67oyoQQe9vk~WMzP}Q%1_iUM>1`?wP8PTwdvOe^znnkCO{7 zzPW68YKz-NtNgNO`!6V;T{6gUY4^l`mcF`ZSb3{&I&SE_aAn@l-qT+XJpZ(- z=k#3j%KN37iG|x2ZG3SDy=GGu^e`7mr5#kaDhY`|`~3*{+{x0(u&z4L-ei z>R->7l>D}H(YoeAar&V{PPce@Z`Gt_6~sK%EJ=@U)5G%H|8<~~i({1Q zilJSbcrLg<=g@%P))3`QKeq0&p!4FSmGd6-?X@kW`;ddNw#40GfA?rGz3Az(aiK#O zdd~P~;kc^_pFWDKAUb+}lef5n`23G0kKVW$c(Q`n*w6FjvD03XPxd`;ushTXSE zkB>>%d2HpPuL?I6Zu(lAIeTRG#hqWjzD~Nh<;stGz14r)+7Gkm{rSx4n-+=R9>4$K z_~U->-~OfV*}j)fsiL}n_jT%>%9*DcD5kOGv&#G zlnP?)xTj-wyezLEKK<}ouS+N23trpha?YicbKARhw_Wmg;n}Z$pL6lvTaRCOR{Sy^ z)`svO_y7Z}EGOaxN-bK>)T7M+6kwIafASX87#jym62t1Q;#IOn8l70P^Np_O$7(Tzo`5H7zYSM99P zkRYEqZKPB+nPoH6ELh@&)BsptJa@NM4c$K7$c9~dtu%b|*)E;_6 z8d3p$noh4*r{$v#@C^;oh6lT9b3=0TGc$5xJc23ifGdW<0a(sWv9y;^0+ z)THOTtC*i2X&x%Fmz@z^wX>iOghsn3N(0)kFmpL_)Q#0NEXr&oN>DO%$vYAaP%3mi zDHkWUt!yc1wMv5($DF?EFGyc7d8`&B;ycAY>&72Sm#*JzJ;5dJHWgh%z6dS&>GCbWefu z_-8BAvix<$sC!9~bIb{7u(R&SOPMaZhS6-mdc1pOfU z6x6YzVjSAR9aljNuC|?ID79LoD`qZvM|nq5^0fTruY$q=#FeSMkFk{H&PaIHwZNn% zC%3@7C7WUr1Xv{|jRXPZS=vmZ)nHl{HE>b7f_&^5X{roKj)!3VOPT>`TTVtF@JTAY zL5oIFZ9z6tynLPBH%{ZENOboL_X{1J5t9*7k`v)oQs6eoVy)m&LxV07x2v2$Ud#Am zWkGXGDg(HWI*_wb^-!l(YcwXa=0gtnjLW75_Xa6omeeR`lcG_zAt^OU5gYMR3iR33 zFyA;ur`Kd?a==+3b>_rEI2^(CV2cM3#Ui=cX#6Dl^k5kR$ANQbR2+Z7!BXBq?n1IB zcSZO97Wb6VSW1~DMKzgMj*-_ygzt-m~w>H`~)7GneL5zw2X78!bFZl+N<3z2Z2 zfZljiV6=jC`Epk@a&Y1d)5R$}v;|@iDK;8J>zXwv7^lTXBMs7|LGI+3HJCCRf^apb zh$xalW2sW5%tg_PGbOJMs1gW|AXY|UEO~J_C+!BujRpA-(4(Om^_@yx3?=D{6L=UE zFr+Ed({G=ifzooLWkovFzf9g*rDotc-L9jkIuj>-trxf5+zwZepOz(ngoo4A5h zLjj&o#G>G)z(8vw>BK+^Ob3GSDhV=N*&qYzG9LbjN`eemGNgl|ub|3YIoe{>Rs8UR zjx!jQL7@e@k1ATWUFmai_(FVL$EO5~>0dfz}zQet>caZyz0klcU* zt$TcoE;!be3NZ*dd0Gr84KSw!z^;Ley7}~-5$M8Div>a^FOgxSE9s4-*ufcc(Ui|* zX-F_dp;K&JN=RgMLVQXvL`T3SRoQ|{;^4+Yh*izl^N9*!(i9wCoJNV%0f3U1c&G&)A5J()ZKxfj|W$%k@$ zjeHn#B=RJAGEkKpdj!%Gz^t0<3FO0#jaxnf+3ensEdjeeSWlW;dl>b7H zqZ8dV$wQQhg?dGb&Ydv}*2t{U_+{>5b&}RGc~_Xx;p@H+{foX=s-QMV)#%JLMM8(x zYjRXdy$S_@cn0O078K?g7?%_g7?i0=2#N^IE7Aw6ODxqjV{*=Ft;PV0E-6K7O-5$E zNJNSHfSF*>{2AZH8)ux0RlVj$g8=l5NyTcA2=i!hwaTQ`ijIVYn#38Bk|TqQyfRe@ ze#uVSq+-1X7ZSOH*d6Ni9Q*LY8RR?^a(1G=IHKAj%y;bT<~Ui5_D$ z4kobpvUD-UxCUpfAw{Xx7347bLsR74*>0rOi)r=4H0f&e4ryLY@4dnWrH}Xt%k^I5as+#AY$?o z>swl;{?(N!#O5_H0jR%9mKs!DM)m4+eTrUJkdav<+A*&16G+h zBQlO*d3#4D_BpJWeU2J(;SOBDMCpIZg$Hl}W5@rLi+;dGv;Q?0Ucd!RqyDF|=nq_= zRpI|F7v8`HEI8Ik7Mz8%YI`=5OJ@-%1#MvYDxBh?+%o;*qP_j~?nz;V@%cH0SuT|> zWjK??ovDT5O18m}Bt$R1hZ4<#knEC$S2bOi$z=_PIBIFfF@jp#5ntkXpI+jybmgog zwV)A4E~LkyCqdPEa;guz)VzpXeA(X@|PUH#)i?v45azVx+g5P8p^Nck>R24oRY=%*>|#jeSzLI#&=T zKtl(f=+j8BKuNDj5su7uuy1r)zNf#|NfGMplIND8ii{uXRuYiPE(X!R2ut;iAx$kF z(=ryli$sz*?nPU+e7&e_38(x8PvH#qAhr|zVA}ihM06!HW3Bc;?8Lf?VM;`uWVVrBiV~! zCcIu0cSIiz=&LYX_?C1;94krl_EwLotmvxWc%e9BDFcO{X^4Y zQt7tQVkJ0=Bd&hcR2;$hgll9whh~IiHrh34R7^;?GBVaBJGP*or!qM(*d?9LQ|HVI z4jq|dmgb1Mi|Y^Tb>&ly3NT=eDqyH^evrR?Ql z+Q`I1ESxv7Kv7@wZ>;zu;sojOsL(4QR~O`xP>>%P8drapBE2#v11(x9 zi<$O{{b2Q%shZtuuyl7+j4%^yA#qaal>RJ-QOc2)<0a8qfc?q?msHK$i1+LX#u-AY-x}{5k^A!!2e zMzeVtwAc?eo2aDVOp67QI{A@HK<8g4k&@~H4NA>cqvJt(-!NBAzl4nbT0hs+%&eGU z+M-d3Vd@f^V|jb>$TF(M($lXT;^+vOHkaI!b`@(IgJT=XRLm+G%JsU1QIK)n_>83s zQ;paUk*cw&!wNP}u$6_VHIq;Em_#phsTz|;5)k+3Mwv*Em6NC*I-*=Waf!!*jTUbT zSbdY!L%BCii5wM&hNMIWyT-dJLPr-wL@M$^3)B6w7zafw1`$iH9Mp8-f$I@r9Z+zI zG#DRdk?~4mUFnPp^BP~7uJlH2;F6&Z7>9?BN{mWL4v6UISrDA$H9AwP3W##2Ib$S% z-l0>s=SjefQn2k*Niq#{bb75ao17eUsj5uqW>R-?)s(lV7f?u@3Ds&-Q!>+1!je<+ zi*!ZVg=t=XMU2FZNRlAiuZ(LmeJ)a`M?R81N6Ac&upZbeI3u>7N<}uiE2^YQs(_={ zfD~0;noEFFNKkZsP)wnhn`@zWT#+dIG*Z|M`5H8A(rqG_j~uu>a?Zp!Rg25$fT6nh ze1HF_>|&kgFcs2ZZE!)gxTFoAd@d~C%3qmP=9|z zWU_ZuT%li5>}XnWqIVP0q)m?GvtcAhzu}pzDs^;QO^$D{ce27Ye)N#YOqD)AB`DpH zIHbQSSCk_fX@WB*8SA2ie7Z)EscL#vu^JEY!OTJ75>#9e6{-x%&C+}1CB-DD^urX)vY;40l`-UO8AIJl z8Y?zfPbb53W+F4pEv&#P+arBQs5ZNQ-Y9jdYN#rW6PnWkCk9s0F8=!yz`mBr&0$gt zfBPxZNV!oI{qN?-M>YSb9EYhQ=S1o-~3OqeHS{y6F`Cp?Z z^Fg}p?1Y#-xx~+Dp~e4BurFe%jeN=-I5c!Jdc3#=(-*((8^ zzn_7vNxqTpH;xuqde)F}@v!YIoOlT|BZbqSzT#tjwKM}A#iXWHA`>gtJZOH5@8BCM z&joU-+U5}>9#g7eYkM|(&)Xi;UM7k!pYEbngP9^#@?Z$H&IenG7U)5*$y);6(38WX ze6ffgSh|zR?RroJTEH|(g52>FUy1matl8Z{Yv#7aRKF22?$k=W@AlArS4FdK#wE=? zp8(TuOFGr2nIiF9a`9Hagcd!Tg-9rmWDp$J}wIVM6LP_uC18ZT(MMfxTN4=Ocefz_;O49bQL?ZibnyEEKGU@ z8(6LqeZucRHJw3=0G&2fgWj#ebqb^^^s`JAdYssab|}cmnOzx{e~L`5{_>40CHc}(rXio4hFr2Gp3 zqa(Rqp*CZ2yy|J80BNr1yLiw?r*iM)yox?9CiEmt#61aJfF;E|PUXlRVuN?=;b)HV z>tEE_CH3gAtFHUohg?Ly%71ulF+n0eVC`JN`& zROCG~M)a(V0=%?|lrWr}aqU69OOW#lF|+RkV%`M=k6@sw0D9fv)HXpru|L=;h@HWQ zs?!J&vZ#}_fu%1f-(x- z!#sx-2c-mz)-x7Q5fFcR1QQTHe#;QR;-dZtM-*-<{1&%H&U#AK=Rg*RSH`^1Ft|6; z_UX5k2yI^?Zu@ir=1~%%0V0{;rSisxGS#QSG!ox(G9K`N($WQ}{cE?`JvT%0H)G_n$g23y9^hgLr+#^#X> zagIIW^EvSm3598Tr*uF6;ygoeiC6NF67+^Wx_;5S2vcxAhwe(MztQUr(bfpLVaFa| zQRV7Ab9Lz+k;#UPq!O>BBv(!UL}szND%H_>sRJxzb>z`0ahsC`P-FPDaAD*eoC`@3uVE2A4-Ps_6TSS_A zkj0oK9?bS5QzM?|`l4J_YIilcv-AlvK6idq<&M{8)G`FKEjLQ!gKReDJe~06X4D>* zKPt)-ns`*S_{>7ZxF`Kde@FV>s2I19q|6M1pLbwJt}4eRG1@OA-%?k{FovH`Ajc26 zBdNauma)`i$9szIvEBj~oE%>emL9H-_D}TA3yCVo)<(MZ3(Vt7$OvKu#yA6tFRWM! z%NWwMU8MMCgm6p|fGQ3}Dtlw9Kw_u;i=9GEqsc;#13v1B^tsTxacfMaa0rdYaSiY4&L;sZ<7 zSmL@6x-Rl=f+cN+Ao!TCl9xz)#h>IQa3sV};)n|<`RSkP3S{=yrxRY&T$+!ek4S$;|05l#@v zPoZiF`Dy%DxUP4G95bT0Y?m|U1!0AIe4k8e>(l7BYX@!| z-+GYqdi&5UZRAk5%%MS{ahg#9`TDf}{zHZ`R*6XSE=KXVU#P?Hv_^Xh_=_$kXP}Ze zI)~^q^cg)3We$4@B}MOA#iF+dqrGZnP8#*j{TRd~)1|QE=-h!g%L5S!yD`SPRDnk8;TIS6*6+eWNAT zZ>T(0gb0f=K~7Gq0h<$kvAOwp7J^m?b8-BalFm!BPYfdWt4;6mn0w4yB&y&tKvzQA zIEaF)rzYMy%YI#t=9RjOH_j*%UY{X-C;b5x;rz3q_+k)Uz~b}xXYfv+ygAuE*i7KH zPdqM6Sn#1Db%8yO3#sDBx zN;J#3*J-$_I#MSQgz!T=L&;CY;6-BC@3%lr7LDEj!Bm$h*Sm zh(0omUSDopxf)BorlXIAn!8^o7GL-_bJzC@0!ke-HUL_R#2qNCp1k;tWG zI&50G|3k&HSZmVd7>|+BMczY$U4wH1Vsmuq>g+66b%~pwn}}8^DfZYUaPAkqlJrgq zxh4IEMyjH8`EhxL+Q58WOseG*Ry@qq(HFfqrd=Qy(RaLP8H(P;oQOTjDMcUC zRx9;cIXZzS!F1SiG+DVTUB5$P`WKzXzq4iOeDbcSP6rtcn##_{8$m2pCc)dnhhHga z;)C1MKlzfXe$)^61pBnYIPsHpz~$f@Hw4Y9X9E9 z(06DLwsoRUmHN-*+|)J}nTcpb?0~*TNPZ~WW*DlKF>f?#*(4!^JPtm{ibk#m6mykr zms7r@X#_1PM_f0Ki;}rDn06rh+Quk?O7CI^BG*v$3T}*${S>lc%O$i=^CYp_+#{r5hmW| z?$DZckQT2QP?N(J>{nXjU>h5)57OVzd;>L!XJF#%2MqPw86hG_JnwhOS;yEk&tJfpnPKFDhJiOi#_qcW~ z-c}^X#glW_&Xs=6oKZFA6rsiM4rFONqa~3Ve5n{#0(mW~%$HdDGCB^2s}P9!6k!%;fYoL*N>GLqoLT!LHie5cDnF zjGP#cq5_v-kG$%;S=`gi_(``1?rbZWK?D0in4ncj|F}ey+s2-SKh5`~yRk1(9jVt{!<25yi~AI_XlxzY&b( zW+I1ONnphEu+b08)q>GXqlZT5wRZZU5qRT4qEeX_mJ{t4)UTheAU)Sbqm7EmX1`^@ z?yu~l*)sD zbc322PO~i2<`EO^6_+3Ofpw4oq6;}w#-B>@4RuZO%1I86jS3Ac91TM$YgstN;e#wGT20lLebMWrD$rH^I;-*h7y1=ew7yYyarNiex~1>#=E{Y$G{LAf z=iM9P#$owb0uydzWXuCVMoj{c9YjPDF-pAQ#<_uga)s2=*G6&VU~)W=1!Tf^e9(#| z`YUQt4lY$55-5{uD`rx$YIxlp9v`w%i!2x)b~M})KwA{M=S4p98{-#Q9GB`|5ZcdB z=%O;Dx%3Z>a}j>m|?ChV|keU2@f>~&*Dh}EpRoz7~zLw81B#WbPT)C!1`u9kkm zh|twq{6-Smg^^G5Cz0K(wXB=RjBKh{`s`0K3?QFwjR+0#h%PilhlUnt;*vvCW5Y+g z7Zx*>utS&dXaM!T3DXCFe8s=}VceTo_SNo9=0j06Ipcy^;Qgth!>F5QR;Xh_ZOBfB zk;ejC?s#L4V)E$#a&)9#A2C`RH##vkK%p9@&CD#y57ZaxijrvMXAOdVmWWmWVkA3X z^Bv}u=I>EZQWDZHDk~&E#WgT6$Pl2i+^XrwnVp(4Qvz-^wRRwYnE^A^aJ#s=8povp zznMX!ZghZh^zvvO+I+?{3+|a99b&)&o$y`k;UDyd4{7+^1qg9j)4k7XhC^3cTi8P! zBJ)P^rQ<4U2xob&>hs22V}Vq*ZUJ4$xy0?Bebb0TXGRd5QtVGwtLS{#c&%ZTzkYgn}~zQpBWoD1s1BOE)q6%KK(;;~QFr^DoCu-uJw zi+WTyPdegjaPL+`D{gd4A=luHk1{FZG9pt;GULKrqx1EOEQM!;UXvlV<)Nfo&PZ6q z3c|e&qhiCeiUSk#wZVQ7{R@WVW)u|H*b3;XId@iLI7r@4@&03DyqX?hu0Pi#*O2F$ zn$Uat*h!PRX+V8pfo>WOZU`7;-IyG*UqSP!T=cdtg;QLVTc%%Jw70+BJt?d(K0l{0 z%Z1mu9DuPvqI+d4S+Ej_Dlj=;X&6POi>SG$RWdnX ziR@8|n;31B?6@Uo_9*EzBB!qR3)NbU!GJbWQ*gZVFU218K8VomRS=+ZZXZtef4ZHC5!5N=d~R>E<}SQ(~w_J1ROkKR-4-){v7L=Mt=D!jtyQ zrjMn&iA+)Uk^3?qau=@N)+S8kP8s7$xOIp}@Kp>p zV1LM{4!W^$XGK5@o^dHd;d!}R?kQ)#tg12)3acmA8G$eF3wSk63Hq|5R1YSsZNUOo^3`p0IcQ&}S+ zf-UrPrZ`6N3U+}sXVpP!_IKI}B87R${@m*Xn#<>{vw)orKCgfx?ptHwn z?C3{pocKI!NHrrnLdseR=@!PKCR$(@gYH%AZ!up&+J*6qOqG+YEAq(Jga@VP1bTSJ zgv7+?4D5`IUG-z1%c$lPlkzu>dxQtGnoX-6esELXotKLS*xWS6_%5y#thLCMbP2jX zBF5~~4wmaSJk=n+ppL6#d8cb8yYp?7%InVm*WPsiMpb3)JISPxgiJ_)Kqx~;gqTnQ zC`uYlAcQKY7$(Ui8JT3_%!Ib???7lODk@lTRY0tZF1l;MvW6mJLBWn|0Yp|^6iA`x64_4;W4~ja0__2!My4mk(NA*)tl4Vp&{UV>hZEr&e$G)t2GT*W+@a?v`yNg1 zRGzS@N_{CyWohf8Uf%r4Gov5av+btj`?O!>6pYx(&bn)NITQ~^HlwT zzd9{OESx|5smYn%R{tJYVzfT+=cxKayvQY;FmO2LlqtiC&pm%!ZT`6Yx`i`qN@}!Q`F~S`3FX>`z}L>>)!ftz<^{go zJfcgth*!PQ8m5+y076w{Da~yx|L84mn3h9kBrFA%G;C!LUd6AHQP0;;smQezI_8fX zmg}B%$&~B@PwA2wx?`wVGBiJ@--7Qw`DTi|*Wv zw)u=#{0d-f2o*mz|Ng>-?i0L;QBNS3*qrAU4?Zt@%Azsuy4kJ?`I9TkCg|oV$`FyI zCaXf=SBna%4T@BMmR_LLREg9!CeaIs^T@4!9{9iT=y9mu-PIpG4$Sf98BaT>R)V`E z6cOoJwed-6TeWUQCAa?41RN}KmbzW_Rn;M!Hdgb;L9(h<_~BnQm?mFH7AJ_CP6JO5 ztiTW>!CQsPAwyCt85kLJ z4|FZc`r`nO3}qfy=$${&TQst;!0pY+Ils_5a)^7rhKFFIn3~pJ!FAQy${pUt>H@Am z|AE#(6u^g!(F)O;x+>kd6zlYQccrb|ZiNzg z;g(V^!)qXOQkni8P-TS^ge^0cLMykofd2(5lqQdO9}tAsGMkZ)Yz)*^=@Y;&?R4miN=9m8IoRLMj1!D?^ zLy;N3D)wr46YR1=g4cjWY96dF(b8)0dtJM+E*dv9eYnaD_mk9kQ+nK04V8d}oi+;?gd(#f#&BJF4djd!?fmzVq&? zJqI-G(Q6@GZ^lm3LY6y$`&rF~!bS#6OsS)`(gm`FRh(3+OCXuyuvhWMSRXBBou@21 zx$LO0*Loe54!aw8W9g@n0V!%0 zV+q{dKjKU+nlkQL>h3Lc+wS!kP%RDy#M^eG*ZD_>U)6lx%jV2sf7nvsfTAt~D=k{6 zO)x(AYin~lY@WeLqTyjJ=1V)&ZM`TXVM_|vMz*J*JQM}hTR;_~d{CmJxEyPg);XSMl?8=!PWue13 zEJl0eNykKdjcRYwy@z72_tV^ab8$B3zZrXA3Ucp(&Y06M1pWeX zqm_udBjTQu7;VQzNT#KYK0&dfRr+5PA1N$#P#bZIvO!5D0^vdC{~BS1q@ofgjQPp?U%^|f+SGAB1lT%Z9!60 zuL=@f(@SJ>szkOwCP)mMet|^xLauAMN+(einM|K1Tq&XHDrrWC^qDG21x%lXq^P^} z^Mosfoh?W#VEXxjMA!D6Br=_9Rb-?62k0ubh3$_@0XnV{Bo?ycYC%%$)QE$S%!6pa zppV~6K0011k(qMtGPS(tfxpZ-!ZVRFM>Ay>W)hM`Q8Hx|XO@bnFjtxLB(kGNChG-> zrE*=6lK78ZZLrHigIuMN393z_W;@GkJmzf1YHGdiY2ym4v*(;|je8r0q8SVhl{025 z_tZ_EK6VDa2R2{7fqgGyuMWbv0{dy!HWA0H&@sWtkL8uuxuG{OjfeN*h2A=RzX{&c z%i%ycyl;i~fo{kkyuXF^W&Hh9e6OPSD0pS*?itexp={~!ZmrV2m+9WitKBu=XDImT zUE{2WJdnYXB-v|cPKV##@Ye+fKhxMaR=})m0$jyd$Z}Z@8wt-`Hi!*|K~fIzE^v4p z^L1gx%mra_I9A6N!*I;Os$ekYWdp&s9%8q{ZzT=W>@f7AD>kqWAS@V-x1P!&d<-gNThkDFnL{SIS|wOV-BM#e_K&^LOmX4m_6kZc&d zJhDr(E4z`gM?vMEaS%5^QPR=yjH-lP75}`Ov1FLCr|pL^@dcc3;D+w(jyD;b0o#Hn z?PKib4UF{#|0x#|aA%;LEjQr<{*;k3*mW2izyYlK^+$UIfXRjtbVtl+Elzi>i9rN( z&lj%bS#CtS7YJAKTqu)8f~2tH1WBI92+|C1-6^{Qm)$1|SMpqpWXFzb?+S!uFoG%E zi)!!h6}?PAFIpNCW%68^#9mqFS!=~9Lt0yl*zZnlt%$kDP$_JWVKSL3k-bEP(2%y* zX~Gp_?lnLrPZuO+qt`$|qUV0?WD@$@l*%hmHqc-t@Mpht;SXc!*Fh#b3X&qt5F~ld z6ePKJk;$$y*>GWM_v(JL!~OUEjM-t% zRiztn#;KnSV(Kvc!XGRrFfOFoAXZ84 zep-jk{5X?vpWsYyHW?~Yn(Jl+MSsFSR>=HE|K1-n&2H)V4#9#lvvk*W#S38B(hFD(@5r-WZn zYT4?YUQ{Z@QwEiwJhPbLm{L4*6a_sq5b>q+?Tcr?rdVbMDxtK}=JBEu4f+9S;4LxW zO(?MNoeX-7DaCZb8xWS*XbR5E(5JhB6=0{+hN4H>cT44~wO2SQl#T{tOZ32%*^25; z|79f+3SS^StpF^ho~Og&JzlrHw#r)_3lYUB8+tKPG_XfB(PLX+FX6WV#(|X?KPm*j zpgjP+P;c~w>{*V#TnR*ag#g%~YWDLC0u%iiN{wiVqdylUqB!~=Drv^>(Vwa$Vce{B zI*2?obvhIkwp->Y=owJAe97TFOarX6kv@@fA3=k1o)t{n{2+#>=&&U=KZGOclTc0pC14e|%A*E_ zN*iCTSSgEQK*haCd<-seeYz$t z7-+gCE=bP#P(+EskoJTiV~~7OCZCeYzX}p1PdAmNs!2dskQ`n4X^a6@yH=|Oj;j%I zV1X023lejesAY#-wd@f2IC>_1g9Uo*I+$x+%t~24o5E&cU@f81gdK zr{*QG5v_zUChia<21wG{DNMYcgaAFCJc+JL;h}|1(olq+lN1!`TJogJF@RK4Nz3Ip zR&XD>mNd|ObQ&ijsox>yAp_deJR&Wvk-YN&)S_uVs5R3_ttF%IK1@(^T`H5Zc(f<+ z&^&8$fKllbNJP#NQDH6XG*t-T%nO7oa$2X0qOckyu+lR%NTBB~Juocc373-20R4!( z#B>IXu4VJC!K)FFT zQUg>SCfO5{q>^JLbm=8X%x;(7g2e20=_8YUkpu+86Ak*Hz8q?vhVfslXJbjz3~)u9m}IgCrDR!$Oy} z!Utlw%e69j9S=bKQ&@OCx5`uOFwi-a4M(QAdxZbj(TOu!^3{vRYb0OfV+aNS zAFvd#5#BJ6HhjbS9`GOZK>^Tpz;}W~`5f>)l2mkOGe!3RuF3#I^?;;Gz!Arw2Yzk9 z5HDa1b&<6&>kf}G1O6>?f|W4f2P6Sr+C*Rg${f$S58jG3Xkkf;30hPzEN8JS^~EwK zia!=nq30=+1&L8j(I!GfJ}A3Bgd`|x4#5u?|6gz|SuBMpSt7`IbbVKlnE2FuL1JoC z@07{ABr>)TNl65z-Y5B(`bU}EDwBVa$%h4rSusH&q=O?^!Etc|-UA$i<2wNdu`oJ1z>l*p;i%cP&{F4=XrAgLyvk;y$0IW0kuK6smk z@YTB5X*GhxB2255$=fAz&Ot%qO3$3PRWb_0&iR{6zN3S`hKeU&8e z&p9MWEa05Og2dd94GcF8r7=wty#2gVM=54BOCg-Og{aVu; zTl~jZ7zYMdRKPxX3$6pQ|&BW*u9~WVX+$obXSQ2HhM1LeaCq*T- zaI@IZ2v#C!H5I1$X*GF0bmRIPMFdJF0Zf*lE|j#|u|h6pkTayzhPg*@D(kk}yk zE04h{;}1e0P?$1q6(kB`hSm=ed7E&>hQutC3S2#GMpqbX9IGUQKKd(G%Ayon^+YSt z4w(VI6zJ6QX6|=k#tIE%%QmhJmm3?IUTJiK`dd~YC&RjIxPgc znlsQ<8ZVhUi2#%WY1p*?e^rg$^;A zW{MJL)7D@B$;#YT#DOCYb29e<-pBxzai;KrWz85RNUZ0KvjvGgea1ORN-1QF7CtQK zBTtaX@QebPEEFUNTgsbK%bW7%U(prj972tn^5!lP03uF#Q)_)8_LPH~{ufKvT@!RH zin+ZAOJrw3V$RH&NJ{Ec^GM-?0(22%1qSFUlidVKoV5xPV^7p_M&xUftNApUJRQl7 z)HcIQ69ca10q6x755V6DZ)i0Ttu)|d36fmTkjYtsq{3?xTk@>cA(2|86G@e>CX{mU zfQX7*r^%!?g(26uvg<_>8TXzbF;{WgupGn2y)9gc6vJ{Ve%w2<>$`%asNNSOg*_yb zhh_4JOnxAfM`iLuK~h4;Wb*GasSOXv^T)!Ka`lNI$@L#H`JEui^QVF&&!5Sp*6fh$ z3E@g%zYrw3{!=Eu6eM~6UXbMZb(wq>Nf1P>f8pyaQlr4vX<&{HYb0n!A00|YrZYW3O6H?w2pOnd`WRgmziaASsT6S%e$sIDeQzo@m zi?SiLS}ak_sHmGy)TEv~YZELYvDwv7p7^{>z95q?%H%$od`Twv3lgh6QHFKmo3g7c z1&JCoDU%vBiPWHpWXC!Y6_G0h34MTm8$4!1w{_Eu)%}k_R4MIQ zdsBPNG|viA+^#(nd(gF)JUm1VCM|DSBfT`>vpwth^!~NKzqZe;SI80`Pk;W=yCYBE z@R%;{XkA`uh!`c-HNtvUA!C-JVC@z?b=AjBAAt z9)6;LQD_|;#i;e9L~30plIF`XCXq?=6*74plCbobAkGVzujf{II;(ZuN_T|7yq;U> z4jP#Mz^x!cGC-N{LMxHDPEkQwyOjsXqjOEfF!SAtm9~&>zOAXRd!%U1_sZmbJb*s8 zf8e64yhA2Q+Jh%fBClna#65TjMf}zA z1Ek2e(1GY$NpeZXXh;c-)uvQ3=!=Gx;7iv$(9lwzT>ce{00DK{9Bo`kQNPT6#k3b>EWGi}dQ!rC!=7x_xoNbW(5ji{)lAbiDGGCG(sI|2>I$B7iBS^T_w6BH6Ly(+3ZA-BG%TjXrwmj~z!3WlukR=UJW` zdmSeG36(+WAo}3FS+IVasO#tUgf;= znn7#DnFLi%)!&YzkNx?^#H`!c)337R+4XB@P-fxbgFnA6Gk_KCd!?=(_4Gffa5wav zqRI)ohMp6oOhCraa|-EzIj*7StD)y)ahfs(TbAbaoaFkx5HunW z$>d>~JR*}H$Yis6PKx=M?D_99`H@V1ER&zeq&z!|`%ZSnk+as4#C;}{$7M1ydQK|f z>mp_xUhuu-FkE|;6qq<@G4z~Fx7i1ol(IT=&SXG?t|HMS40jDZC$dA<58Nej$k1~FgAnd! zL(dsNQMyJF7QfR-X@;INh>rO_nRSMq6M=>X_!492IrCIrL|V@o)$9sl5Dn$Wd`<1? z*S8;dciO9X!4DZ9JPi${u@e)f+r~~zY|pXrGInB$@wK^&=;SKk&De=a`qcmaotP-b zafXIcH&cw$Hmac3AddFA2%T8mmqN~ob>GXRp`j$jVWZ#*kvn8^r%W0eN=5pRSH@0E z&WvBfPR!QRP_nH*XD8+;LqmxJxLo zF}pH$VluO_6B7gi6sEtXotTD(l3IK90%Pm|dJ5p1TmB*7>0%tkqO{1 z&Wa-+a2P<`FwTk_8cHq-hKBNI?xE!8yp>Bk_4j}x*ii4oVHbyQiGoBFvLt-yKK%z2 z+0z4-l_sDCx58~&xYDGv!tuPHmA$DI9YK);RyOs8E1Z6DRmoQPfoNXTLEtsqt7qW= z{1-~F;3wf*e68uQh+b<-A2EZAgHkS;i)^5KXp`XD;u&l_n*#qP5-VD}Ou3OS6H752 z^w3okR3^NJc{*I^%s(c9AC;H_m*HOwS2CBw)%P=icEiQ@_<^}1xT@I$zv^cyN`Nkh z&&|R0_~G$F`X+pe_H8>&@=34*-Ll~K-MZne!5(JmyFgU=C{_Yr8uil05o(zg?!B%7 z_TX1M@Yd=IuqgyLWC=3K3SZ}Q!Mg)OxWZ*i3$HpJ5qTc;0DggtMC?)*VZQJe zD)(8fRPMJ$xd#&py?{c*qP}VSWlKIP@+F5R)6#jOD0vq)6Kb^x+9AB1pCi`VGR#Vi z^tC@{``X#9QtXtc>0A~yEm&Os2^Xz*vcg9PJn#>DE)QP6a~ykdK7eWMUi+NZDe;3B zi;C_KrNhs@VrRrZ1)C3`E(dTs;FAj25uM70dUWK=Yf8y{^ulGLyjBqGI_PfkgFJSC zH-3_bcetmroOzKiCxb6^?0id<6YEv2$1!XX)u=kSCoPV3j3L{ksyc z)Y>`Gk5W7Msc$5Q<0D06VFV|^jxwojD4+?&nVPFov$}{W0C@{x-qAm@>m9@*7@3Zv zD}RA%#62b$UdE*#Sa){ValM$eezu zNP;p!yE>JBu^trfN=2iuOc4lX(5#=flLG~)UmtXp1N3WZ)t6i8ZZGY3MpIvfXeG3o z{p1@&`gK(T+{-^W4^%%*CXiG8G?}2{oh70pa+FNc4T_>!IbGS=mzU0Yi_uLM0Vwuj znVce%B}jJYz(d1V*AxcImpHS%KWAaCF!Bvihm`32Ku5BlK{)F-TshnjR=V zenu$e4m$}m@WF&HTB{P}15!D~>{G^|c#TSw%p%o^?o&pd5(YxWa*@g+3V@F;6n_3s z=LOT^$xx~-yBHg59h$$CySWr&A=Nj<-@oQ$1*k=xw+Mmj2N4@R^QZOwv26tEzv%x_7f*3=g*!}oLOu4X4TjhxoWd2 z9ZtKWBCC98MY+puciSr$n4(Uy+5I&i6*jNUySUDt<*4=A-L>`#N5#NVnlsr}+RE)& zuCj~m<=%6q4b&X9uoVN#TrTHn)Z#;fUYf0-C#p0j3Rn#$s&sI)`Zfgobqy4a$O8E6 zr+Ki|I%;fQ2b9NF?{zJ-JF2R^S!J$TdzI6*kaZ23UAWW-lZ|1PSS3#}rH-09XDocP zQ%eC$QsF4aG;Hq074}M7z0+G}^Vs95Q&U3n4vwNLt#MV@;~2{WD!z2Seen$7A@z6( zrIj|1w+JGPr*^}Vgd9+~Ab!bfOev-d-hdyLvjIsuc^*;4NAZ|U*K9~cN%ENtvsT0H=29u-kSDK zMsxqa)W;jmy%?$K#!}eBOI^It-2WEQE+!%RDJRJWJbdsM!d+Sz$5-Gr3VRORj;UA3 z5Sc_D|Dl})(7mL*LCOo7MR#N8fx^xKGoxx;uVARZ$GK7kDe_=_ohxxdomsbd$}_H+ zDUZHCUq=&R6s%$n3lvqwsW7R;3apgwU`^vM!Q@esOzE#R?Y5xY*`!W!PELg#q z1?ArUl*#)Q@>b-Uk-QYS$@F94Y)Exrb_Q=-*4w{=R_hxTj{ zYWveNE3Qf5s+1d4wpgpUdV#WC3$~}SHYY~Dyylcn+pbtEYSRjf4p@^;!tG+(4TNPC zSoJ~c_b#6=wbN#op2amacCULe=|fJ}X%M_Hv-a5C3ml;G(1w?)_j9Hx156WRBedA` zfpqWXPMgPL_hhM|m3U#ck0zniWA}O;wN)NPIS^M`?y9MA)q=tTbOPXB?gE9#BCkgY z&_=bGP!YpeZ!LK(pRyifP$6U!jZk?oeNmeWrfPa1pc0<3a{{W2no$+03&a~5% z5`tm~a*hTRL!dorR>h%H&8U%ODY0dt=EVtYV|{hirjO zPeO3zPaCfMX@@0ON-us%(ZiKL;0Dn7I| zQr%^slu|b-fZx}1{LY-2yGx$UR+*eClhb6fK_-{Wfy8Z)gJD`nSJ63IWFO`fra z;tmN{A`c^}fg1=iqd&o~Pqx8TU)`0P6yhxrff-%>zAtnTU8MzX^CoVU*PBNT6#t+o z_=2y?=7B9*-KPYEy4a{#(JnWUbMph}LnC*} zLMtuGZAu_*IbZ!|qh1&vYU+!=5hI^Hdzt&9q?FWp$f>zxvZo*^Yc?K!^hZ1fpv+A- z516l2eCcw0T~jM%KY_jwz0nty92Q`e?+6fyn;y(dge&FccSy>`yHxf;(;nH!{|O%^ zcr#zdQPjX+n-3N3BqhA&=S+LT}9g2pZ zRkULD=hfiEr2^#|eQEjPU!ar48udRm>L=kczIK+ zNlHNF1I*~_iKf12(}kR`TJ%L#Nqnzq8i;lib!_&=bxexBpCrfo9FIs>f#w&QT0P&? z>e;4NkD?Xjk&1tpOy1Kpz}@L?{(M>}+tnV{`5BCg@+gr|+5F}D{a%DuURAO|B!hYu(T z3fl=?WtbInE3hYnfySYg7O1lzsiDdgBt^MfkW@X-2oiTQnfC~iT$c)x809A(Zghp* z-B>5GvR?^cI+6!(W-s@l2SmMM^_?~_S-`FGC>AIQ1YD^2`dIVjK`SaH@$q^jSNP!J zr+mfW8-h8m3+A{!nBxt>9B&EcSdNaAc`9CoAgPLG2$I-5OC~Rn$@64#woJ~E$(b^F zzD(lSh3AUW9UzmZ%j7_rL_q4oX369kGI^s+=Ex*UzAmaEGC5QxZ<5JjGMOur!)0=$ zOr9x|XUU|zyU%>H>^eb^RH0KOGVXvNk+E^F2-1q=yDAxvVdLII5}J_bgsk%qACkBr zX7QWqDV0~K8X&ad>j=>&nsG4iYsMk*aSTk8sKE^P1TUstqgGP8F9NkBD(?yz?juD7 zrInotf7$SN2L3|W#3w`q?etFYKA9DSG#4i2Xf`Oe?W__M3^>iilmbjT$yV9!O@+n> za+EP2{P_Xj@ATi)7EhEhZjl_*Ez88fd72*7Vp17G&$BK%+5veKQfSlc=%U$O(anay z+>%&u*=Wz2je^P*G#=IoA`58LW;~!HScd8jn>6{}WXw$LDO}S=Z!uUYN?6dkyPPh? z0%x2FrFAxMH6{i=xJ7-vqXI2J5kEkuW5$SeYXX$cVvpBegUO#$2#o{`d3Bm=@=t33 zS+ZY<7fsv(yW0ah4walIRFo+epq@t4MhgYH^4NONapw+Y8dAt+$Y5|n-`zAWQNoxh zzGu*dv;_tzSitgXhqJ@)Z z#N~99FHYj#%;-(2&v<;#cUO6mm2BI1&f4;vWu$02_GZ@>y66~XulKrPTdAkqUTe>C zyIfu+9jME5#H(&UZLJc(RhQ+e_tw>Wc@|OAs2JV8R&7n9+ynfptr`R}!n&N(G<^}B zg*Q=ZWc=5vlR+&`;xDX&%ty%sW3jRra7B6GnF)V^2xO!=gOzQuTCAd+0X?FtDHTLi zDBkS2Yjf*B2er*eD7@_>QO?NkQ%EQk6XjXOcZhM`}K~0b@Mzr)eye{|Rtg?EC6INyQJP#NLW*PGp zSD*yY$zqcLO2h88Rbi0+gR=(@9h#kO9c0C~(xIg}Ln{WCmyH~08=hTRnlpGv?ugu> z*@H(|vJ}yx9zz_U!s9subs}Bmw$)Yp^-S2Q4JsU1kGshgiM9;nb!r&|Lor?J(0p(O z?PiP&8~xLCEs+ibe8Cu-<}9^)VKJ04h*JhXK?7N4Y?Ln3qKe_bfmU5w)uOv5=z|JH zAvh>4gOZtLpb5t1qDj&fi8lt+H`IHv(}tyYsMHGkA{=L8JBdB18dCjb?G`#JpcgEH zqzjz3^7#d>MSwqAJ{HW!!PR9~DqcCf5X--xdhAwk0qK+RSECoO9 zr-EbIDQIU%EJw$*I-48B4my)^YUCE!%SR0!Sm|`xpxr-5eG#0>gwO!mL0zQN6hY%q zP;XiUVbGWs!%Li!0=H70g_cZ}Zx+R~P%Q=?O z;9xOXQ2lI)vP@SyL(x!LwFFQq2v%4+kKG9;XJDWQf&hCSn#$OedM}C@MiiyShGLd2 zN9ZqR*_xPf2xm8uv;jXA14$Z%?gk0w^jB(HhsdDfB1DZ#fMrl7yP(}!qYtdTws6&h z8wy)eh#dbySk8m2eYi4vbk(Ex6z)H76(8=w$d^7Ec8p#A4d#tX-;QpO!aJhG9x)+5@DkQOgD}pb7h9Xrj`73F zD}0bWMYrUOZnwD_8Un~NKRv8>Wc&F>4taYA+qYy z^Qso`;KKhs9$+u3@gTjIkOuwjl;KOm++U6p>It6XD8aNO;)54;g-aPyka+X6r-%m< z^^}w0Y*tkmMHmSSC>ON`Ygo$D|D4`+_Qd=#O1w$ap^$Vxq0JTKxwbFr zPBGUGzfb#`Mb{vZbhkP1Nc0v|q>u`LyAn;vMQw)&9;GPo=OE&DIr&xYspBIVak8Nw z?X=R!c33GiQ>I~0njkT=y4%x;>?6B&MRJ7?9_mm6UeRFCZrrb$i@r!MZ<0IRF$`>Q z_#RO>p)QUfUl=u0wgZ0dFW6TGT#`&f>jhuSbmlD{;g5(zsY+4&ZzhQzIx{^J+=R* zXn@h<=ASXZ_`Wq{2!{LAnNmNN4nyim|H}gmzdi%|<$m&5p;+7Ejh>5pVAm~1N5wsn z?9kz99DRYw%i_Q2vln;D8^^8mPW`!)x?@%EL}L*c%xo9iv5RSBgd?yh9GVr&!;@k< z2a}T0=3MK!=t{fjr4vrYG#1jvN9ao(0GYnWvkJvxh|>QD?yU> z+BY4j6>B^6X>?RfJLdxp{_{o6o9n60XTh+j7%elQSWAOae7qmM9O z+M#aiMHvY{8i+6u@xOxz-JC=>t`dq+ZG;uyUVIIs!&n${rQjKTmiK+& z)Kq&1LBZZcs>%*{0=W4&V`;s`8!R5UHeXfkbeO1Si}4JKy!kD*2aAvLl~RKj6KL`I zmo*8t(m3e!AFK%li%sM%_3+(z!jmHM&t2@oyk+iL)xqMyTu~JIc>Ht4g2f*AF0ZIt*FFWLY?}-% z)|993Q)Bh#Eyn8qv9REL)sPiQ$Z;JkX%*1ItOs@w;eBTn&=^(1A1+X9qNNCy zR=45RsL2P+Yip}=*_=5nYQxV^CRInCIk5JTvhfkYoGub`tgPu~n()fr!j%?BR& zaLSe1*4LhNf35@Sc|6oN-m2?Ve#M=f;eD&7#mK!l@?{0&ro4i!CLd4?0NIB9@=!*7 zb@FWq>UY#yu;;wRC7vJo6640^D;9_nTcDdkG`DD*MT^VjjC{GUCLix8%9YJ1*O?e1qhk7X8%@w##s=Jp0^2Xv`#Q?N2P2>TY zd|i+&KZ8)y8eHwPX`B!9M(q=RwHvo^)M2}SO4_&DRw>iA%l{_I)JWmR;Y8&4K>jOQlt92lv?H zXQt=F1P1TIhHajdeSCz<$c2$-B<41~2jv7SH$X0FVt*9ej7wXPS^UzL7Rywqdvzcs zT?d2->v|luPw5$;74VyIQ_bQdR2f?oc^NYySl3WaP{u;YHEseY|KT-REgW<&j(Cyr zHs6$+@e~X1XglD(Px!|_S|J8!LW-cc_}%q0;oCyanPzfxZ&%9#P}Fh8DShthhW z>vch!3}4+nCFT2F)Tspri)`;`^9PNF;LI-tE6NHQApioIqWw&WUz{w(O2byOpbTv?Z^D4*h+V1 z^p3pmGm8#}`V2T4aHNqf$Dh$V2I(D9MIDL3YHaSn zHj{>(OG9uCj-+ca;Ap_nfFq5Hesb>kPK4n2!F}_-%-jF&6zmQ)IMOUR95^n=@QNxY zZvwo7K`TX-lZP}Iw9-^LG||>nIbWlzrploKvry$kEkIX8l>=)W$Ox^Wh`cHXqNl1* zR5_q|Al?V=9Y?F*%k%riOj_{~Tu-B@az=kJgv{upL&>OzdoY4uN|n>4$!87!3>0s1`#Na|;He3<^~O zqXAXM{=2lUP;CZO0S)5g4Y(DgoHp1jpap6jNY4ujQv8xjBtUv`S;i+PzIcW@styP% zY9MIwBOH+WFsQLKf?R<8tN3FqjfBykBHIEPWaKZ^pHie9&jkW(ETadE3fOCw+1 z(5<|~zB~e<2FG~hLrdW}`r!Mw#O+=9JB&l^Glw1feb#kXj6y4Icq{jr<{jv&4R2{> zM+|SHOVQOB-WtPOlmimi|E87749OM3jq*$qpdlK^;(n_VJ&9n`j`@dO%0W>&L z)80}z8XE_YnMUvUTk0J@NBl)#MtSK)l}qmWboRM29H|!y8IJkcC6e+7gb@u!srHW5 zjc7ww&Gn9a8l&}&doaA{9S!A=%0EN-b831=y#3#WSK1@eG^V3~Hm~S8j;;}a<;^<> zG~Ac)GU>*&UXu2$g$zq$dmTW=*j|V2hA`NsTJaH*PF4i-d%+0)C+3hRA_T`RU#C3r zzI`kqQG+Ax#%x_UUIwc`$Zer_joF8!(o3qYqL(!0kSM9hA+5nC(pVzFPE%P@WWUsI zS3K7miQX~);=+PgeFuJz6|KRM^zz}rQ5v+)K*`~#($v_@Ts zt^$t6pcNU2EHU7S_yb0?vPx$F8wYEX`28h3QCtVBmF41eC4LNk7<>*MKL}sWY9Ked zVeRcmHoJcLPjrpIuy)Wn3pcxpE}D*<(11(l7Qz9p55pTvBgh3~X#|;ooUr2Kw=t~! z5{eoLIPTqi)&=F8o*RLj)8I($lmSO#14&ja@`uRL_+}j9V%?Ds!Ljxmf+HS>iv%1? z`rdMWn>WjzN9Jm9qyv)%91S??CUAi_;(^!%G$>EMAvk^&Aviw#_?vgc4*qJB498R? z4LBNbG~ft85IUvi=Z;@T2#yCo|Df>kua{gS!;usV1{@7IYNBWyaSo=+`6fbe9OeGg z4LU26# z_MYFpyy45oWH{2TO5wm!TA_pzpus4Eu29OG&W#mHm1)25^;nP%oC>ku_?gN9;sQFn z`xwqq!abrMxVarKqpM-KMk#J@M=WX!$M+&h<4e(;>u&D(9$h1_Lg{;d@2LOVe&O9% zQ<@Hz-ogQ{55pTPl*k2Rg%X*7ocImtVBdoBh73nj^Y_}>3zt9RzVL+?k$oB*35fXqG9BCyZ z960{ek_Z$E4W^#Hu zt>{G1ZxfDiJe#UQEQxU52j1QK>%C?&78XL_D?=a}y5$aJLi01o_s})e<&Ger?0))7 z58t%n;ARv{O}|PN5DqAfa&ka*2{3DpavX&n|-8uW?$(eRWfiNiO>sVP@aB6tB*nNstW^-&ot(|F?X=fWP|f zTBe#gF}r^Vcht4+4Rt|aJN4MP6*qjIa|A_08`Sm=7q&hOV9YU*3&tE1 znSh+Ik`(K1C9n9LxW>jH`E20 zJKpY|J8b)shy3=o(z2idM+1%q97BWS_YvwHuk3U08=d;Dyg(kT(g{KXjs_eJIEDtt zuy>mg}IQ^=|j`RL>Q=AM(I)!h* z(SV}?$I#&Tb~uC8AaHynba;swX5tFj0V;-=Fgyx`uk4N05H9U`IyE z_HR$$h8WTM$stG@!&al8H2O&ZLI}H8-ZefF5F zOCNrGrwqs3u;6IiJqK->aS$1ppcxdP-_X4P$DzEDm^-FzN_exbWYE1b9EXPmN8|1} zkUayA8W!R0HwMQO5rX4oU!6Vt#dVuk$#5Le`fzM|_uL;aQANk9x@tPs&FE_ASPdO3 z$^lL4>GLaezmw3lFuGqxyBLht3N#W{@=}WWV#7%WU;J9pessl}2%&0Le{!(;1>`Cc zz2gSQB`-BzGVD_8!sFxToJT8IETe za6}Fm3nIpX2#LW@4r;@?doBnZ*UU`0=mzIKTV*(oGT>;y(dZpR?;UYLF%ol3$8&{u zZ@7H)voajdHsEN$(ST#vUK+Y56f`OL(+hw0Y?Llp}{e% zyXS(yam2mTE}PzCBpxKu4Oa6FI3foOI2v#a4UXS~xI{vg(@=5Ojnh7KXUK3YFyLsw z(U?1iK6ec3?z!mZ@1Cpt_LJI+*oPh&j)evs4LBNb3>}VP-8~lsj%~KSclD9o7tfX9 zSY*J_fTID&(Bb$th)c+W)gW*@o_*cI&wKnxC$F?ka$^iQ8gMk=7&;vBI-E$r@fGu< zQ*N2;HKFura2#vE(SV}?$I#*UPK4k%{HA|Da^pQ8Tr9(JoB>Azjs_e=R==rECk zaiZfCoalhVf>ad>9DyX>|Mu>l8_V3x#cEse?zteq{A)?WpZZ15E zRv?WPNC1L358eD#kYU|D7X*$ywyfy8AaK0L`QzwI)>XPt`ZPFB3JZ?L z-E+V^ziV1p082YfT(@mZb?nzDD2!y0f59gfF=b&*)|n0(^FJ-@%?3p3V)2FJ-r z8goYtFtJ=xfh%By#5%BK0Md(Ao(QnDaxO-0J zAvFp@LRhO0Rt-23vw!)!=YqiYyQEM5{@AE_3uM@qgbQ0A2FS#R+!-mbHSV4>=9t(D zNd~2*2MJn7$zDgGYfpSMG@vcM^}+|WWdBV!xPA$_io_hV%i4|I{&MgeS{2rM(WzMG zaC%V#j)uwyL`E|xKvt5Xpji90z%i`5=YsT(%hOr+cmH(Zapar^$7u!}kpt8SQ=4q4 ze2n$UVC$1%-8~lsj<(g4jvYAn1)3vjaGY+y(SW0|AQJk5$k!k)kr=Edzq2T|=IWyR zQ2I1D&M@F;z|nwX=y3cdLU8;jZ|F(e{8x9#aGYtt(SV}?$I#*UZG_&;v9#5W(N@B~=1V)& zZM`TX;nJ&0H(&(qb)hJz-hwjn6$GR0l(dOYONqZFG4gZ*JegAx|FM(y5>cMA*(~N@ zcD9h!!Pzx8^Rh8)5j@8O%Fv=HsMzNIC!*>r8jOq)humSq*fC&e@IRPgcGOQ{KPf2N)wnk}L%AfFrA8d4o zzy53nl+g|Cn;ptr1SR&suMJuOYzs<&E{7HncyUU~yR3D}yMK)+Z(mjbZF&V;3}v%H z>ENWYRKH5g7X=2pw5NeZT5r-V5-@3R;{VcAWVL7bkCJ;9tHp=C=`9n!=xqC>Yh~fi zJy$NAo_^+qHKmtsvKD^y$r<0ehQ4d-(Wc)UZ|{5M{MdVMdn*2u+pHJ;+WZp}*qdp6 z7yswg*^j;c+|^SbYFuM_;e+chpP6`a@3j~8x8L%Aub5vSV!7h-g@2iwn*NvP9{Td7 z<)!Y6ohhHK{ikOmRSXQUV_@f#lk;a!DehNm_h!}D z7P)G(D%`e(buPCztJ2}LJ1Vj~)wT+|yWCb@ZLe6gz!Y_oWd-nDVe{I&i|g!?cZb_v z?p+j(X~B@b)KLN3u5eVCR9fz;_1YJCiy%}Kxw81u1$MW`;i?7i?R5#L@j5ENpH=6e zW|MN3Rl(DO1h^R)z-OR5Dj9LqR=SiZVt52`ke}KLm)p$Awxtzr2jp-T&!l3TTv}o57obI(%**$G|6dg+IY;IeP z9m+G_R$Jk;d#JdHJV+|mF4TCw7fM-H@3ni9REPRnN2SYMGt1_7*vcSSvg&{W0<0Ad zZMlP`w5-0e67r-5fT0;?xP7Uk2J%xNJHj*!9U*<2dol1=gi2LIx$3=j_1;34)1^h( zjyonoD5s-h0`SWtI8|kLK`GsfC)7ePs9`iIrK;@|icPUj7uJ()i$HR`j&i$4k&@)n z8oQ@@hN}(|u3D@Fu#hGCv@a@j)z^9z&uL^|;&#+HydZ^orCZ?X- z1oRE=pFGm<a>8e=>G`R4Ar?^RS+GtbY7QxG0zw@c#tuh z+ihD66@!JsQlK4n3mLj-EoWQK_Uh;+ujs*Y_KYU>It%WlN@vsLm3whDvDaB}FEu)w zCa>H}WfOay1@}^}vuX0my|A1BVkPrZw7S*2)m2z&F0HZEbs`li#XU6Kh!9Xp*PO`Z zS1*2MjyE;iDhx0HWPm9FS-_!!oPV3zBVZS-3>9KZYoRAmL@$A`jV*@|?ArR8Lg<-X zwf0&swx{uRE)O(}yyFsSN7XODe5thh8$zCxE{VfIX@X-p!jwG35KKEV0Kh45pM6-oV6`WABs02_WGmfV;ScK=tSUi<~DjEI?&r zg<&JBAD}6HqTj&BQA-`WQn+~NAN8~uBT?MUx@tn5~ZhH*4is*I5{9d zMk;X-!$%lA$t=nSNqAf(jEJD7C=>7yB^n+eB9WwD>rX=ph2=_)Q%fBl6lfS1mGFi# zlQ)Q#Qe_ZS00o4e7yD?;Nn2&CBip7ZBO^c09ZGG`4LHha)L?h}xwheQT?FFM1$x3V zm&>UZFUT*k1KmR{xl&->=|NgB3`Wz5V{9fL11ec7aX~G0Htd^5Gn)ObSW2nWZ~(iWI%G`0OmuCoLt8CUqXX(9E#%- zlWHcxAQny0?1I7vCx3NrS-aB?%rTwT@ukgQjzpnBcjUk*e1|_XSUTa)3uxH}4llyr z^sy7N1)IUogz4jQaQu%VuiCGMch#sZMzs0@4%e!V@Qc=%w%&c6;*Rm58-f*oRkK-n zK{=(c$}d{0Z7!={z^2bColpyPTU&0QP=pj8!ooXaQy|~-;1}ao^F1Ek(Wr6aJ84+) z1&qEY7x{)60~(0EL}~8Xj4`tdr%aiK$*_{?v$8S5YOrKt;TGM$_JIKW=zrfCW&T6(a}(uim34LkTLY5&mX2~ z)uQ>yx9KzTXAFf_(+)9BhRBL$PnZ+~zZg#!urDZVe6I)avIy)Fmsu$-oZQ4M?R9Pb zUh{-l%ixjMnU*K@iHYyj&3t8<&oU;e;JUgSYvI_{cd|}{ z3yzL&yEe7w?vnheOV=E3`0xsIkFl{+fte^XnQIOq+vD=0?nz#L0$-!8kSv=3?pVgO zW%CNG@OwEtFn@!4#wS=SR+!UzZdneG50-CQo8E5H>NSbWmrt?`?Zx`AD?0f`MrGgB zEANV&q8UZAlB}CM?_BD;bmPdV5(uA`?rvP=owq8tde!3UXn0S4prmch{ z_LzudUR!Wl0)F<#e(obziW6m3U z0h|;_u*Ja7dWf)d^rW?8qtbR?o#guTv&l)l_k6f#Ol55BvmeHqpXqz|)|Gcn>KoVB zUTB$QS#4XEXUSZ?^d_t2nb;lK{omR**%DvA%h!9iIiKaS%-9B7u6f@|+w4Ci>@28> zx3-xzz39+;$6op9mG|E18y_6&U&rE)^#GYo3vJJ6+wf<=CxQ@qiXWdXVG{66C2?eH_*=HVqV(Z1M;en{fCe8W% zob}HgSbw-rYR&BIoNpT&?~BcD9563>(i%+8ig19o;^vgEp+Z*NPQpEvW3VHXyzud%%5OU_NI*dN*G&6)46v))s%^6zEm^&NiW*T-`ESU0Ray#1{e4<20q+~kBcXI3myMpZZgcTQin zrCq1n66bciaL(FW{+2m6WA59x{Oz4}jrSHp3^&eNaAQ&Kn2ux5%kAU)1Y%jY{lIe{ z=0A1k_5=GSCmhY^7cT2*IV)#=65wx6zTdpJ9#Haq2m6KzkFv>jsE-Sxd5UEb=^ zb*HUcW%lJ~J=t*9{=$)ei5~T2!>HxaXT5kLeZjoA-&MyAuU_Sbr*Bo=+EsP);uiFe zTkvfRM7z+psx*GpB^j&SH?3NH)6#*TpJ@B}p|)Q&w*53ax@_;JE=MbY;1dYZOZYIlz#!gF0t~qIoA5*)|g4VK0NE1 zr$;neb5n+W_e{+#dBZOJ>zCiYatRcu9Dt6f!U2Yy=CuDbiLO!W=!zwDnI2V9=& z>)C%ryZ#LY)~vjOJoDQR-nJ*+%zTX^4Qy9Sfofs9YLnVk+s(P=7oc_RKYQ|I%j$;s zcm8%^XKWhGn`T3c`sW^t`o@nLl5bs`Trjg{cDMf5^>u9Q_T1)@sz+^|wvFGgWohT$ z^OxoIc0zl2-8;uV+WF3&=dWAcH+}^)#kVD2`N$=&^-jLEuL+yWtlXG^roYi~g^=w$0)a|*X)g|q1 z$!DKE0h;63p{uuC-ua&VhjQbq*2ZS8%r1$Zx2$n|=JHj^x#?H^IW{kIhOPY1^F0%K zo$k5gmUiZudrw^W>GMaAK`(KaxhQS$sE$u$=RPrb`)Nz3u1b2@-geNMt&<-3_^PdI zYR+BzT8~|x2PdBX#KfMrY)-u6^jq2utcTFgzw^=2e{Ov5m3KZmc9*#Zc#m~pw12JN z{@JVy##b*~vTfnBfI1h#D6ybBB4=j>jcALX-VWMtST zoSRd&mRaLt$IhB`efgS<-YkCE(oLPxpXtKA`>JQp%-_kqTRYF1IXRnqU%hhX>(jY+ z(`MVuXEKEMS@}xr*3LJ~+~2N5% zG(VWylgi`==Zl?ui>KXO_43=7y*KRot{c8O568>suAeVjz;Y7yK`@9m!z9)Ue}U$U zPeRy0^F@r4_jvNWOPvpbvo>GUO-61-2QZiyj02tUS2bIe7s!rqSA**sA8p=-mg=eo zpE*wOjCI4};8<7_<8#NMN?hvPvA>c=9{l>30iv5hdLnSc`QQoo&4#}~25SKAVeTER zF9X^k4b6@;4?}x(u14eTX6A?J4~Gra{7_{Ex<}==(N73XQB#v8niW~p@==7k z2lBr;gY&<~VDbg*V-G;$`f zWXIwg=UvTC+hbjxUT{~2&oV7`(;X{cxP9BIwU^f3amD(ZemL?-YR;aF8J~B`x@>2+ zf`1ln?9?GD=8u*uvI}aiw)T(hwPNkKI_ta)TSnG(cdc&MxAaWk($yawxqWT+-)*m? zPu+R6;mR~Nu}u*xoK)1iegBQCM!hn&eN5`6jn_?%9XyWa{{L%p&K^^u&5g)tG=<&b z<9(`xK9q-xPJ_GMl%bo8_Uu#ArWbprWQ4t>ZJ(*uF(ut%rpM1XcXLu3n|WjWxtsc* zyK!inoj|8`KMQXHo3TRS>DjhH5Ipv z82GpBq2~|J-!`hJqD1s$t~Mw2EpOPre8abpdYA%kjB{;^+tuy0{4UIkW5Z)>j=uNKM{9QOc^(FbkJxVh-kS2mq2#aU zz148$Tha5Pd&DhljJvF{;`vXm95Ctgt!rwRl{nMPM-Eyi-LcJF0qt{$`vzKQnf0 zdDds2&cC{LSzb}*+L?Q2Z)$&`ZBfkZdv@-reDm0KV`m*3JG@W)eK>Vq|J*+0PQvQG zFn0!x_yW6odej|lVe;GM{ERs_U-)X~oKAmRXFUz3%stP`{bp9b;j?mw_gOUH+QZxT zf4F0P%=Yb19aw=3-ZwdQ_n{=5NWXRG`gi+WI~lCkPfmDcBg8fF=FD3!?DpCP8#3R0 zXPveEOV(V!&{?XJKFKlnyA<7zI*9|iPxv4_8)dO+Ngc2 zeh$9R^UrJkcEz*1Z&>lfl$50z6R|H_r}S;uf9dKq~-dot0flffzmn|LN zH~z-4^OoiH&s@90af7|im%QW8OF^@HlXc#~m3bSi@hs+^)8vW!&YP|2FRz>j&;902 zFv-8N9yANpFT@HBD0C06n3sZ5!Tjg^ZyV?B8ti@ZhQ$rTowmHH=RZk2H|zdES7q&c zVBqD|&@-hiUk2V^Um26M$CvcHbx6aGq{eyCA3+z^XJ4o1p4xt((~7IsLoYUe*FQIz zI&I0XjBV)TTiS_b#uq2Ol$U%Hc$~W8xm(vCm~6QM)DuH9m%p5xJ1{Zx!kV=;4Uafo z+j{t*gY3N9l%2nKTk_7qahYxIyRKn%+xxaXc<|xvTdrNd{lMgeWdpDm;5+N4M{Q2% zbla74Gj0JD=(@&xpajH;;;e-lU zruN#WN8Rh+^!reaYP9t~Lyy`TZN1IP7;bq`^$nmxW&aw>RG6#?5dn7i#h*? zXhrA1>UTHT_9!|BTb_5$kTn@yK+%vN9lL7dRejBk)yphv`X=pMy~6}riJ=wSGRm?y zSlAs21=$<2;};}-@nHGtYq}O*>YR79ys%gI%r4)(wYKrumF9DDd!Oi(GagP0Mi;ex zXI}Cf^O9DkHkes{cHYrOppwtA?Y5foEJ>D;bq&L2N9V+kO=`!?JJ_|+!)G@P>mS_) z#}R16)UJ bool: + """添加数据到指定根菜单下,根菜单中如果不存在该标题,则创建。 + + 当 forceUpdate 为 True 时,强制更新 Cinema 4D 菜单。 + """ + def doesContain(root: c4d.BaseContainer, title: str) -> bool: + """测试根菜单中是否包含指定标题的子菜单。""" + for _, value in root: + if not isinstance(value, c4d.BaseContainer): + continue + elif value.GetString(c4d.MENURESOURCE_SUBTITLE) == title: + return True + return False + + def insert(root: c4d.BaseContainer, title: str, data: MenuData) -> c4d.BaseContainer: + """递归地将数据插入到根菜单的指定标题下。""" + # 创建一个新的容器并设置其标题 + subMenu: c4d.BaseContainer = c4d.BaseContainer() + subMenu.InsData(c4d.MENURESOURCE_SUBTITLE, title) + + # 遍历数据中的值,插入命令,并对字典进行递归处理 + for key, value in data.items(): + if isinstance(value, dict): + subMenu = insert(subMenu, key, value) + elif isinstance(value, int): + subMenu.InsData(c4d.MENURESOURCE_COMMAND, f"PLUGIN_CMD_{value}") + + root.InsData(c4d.MENURESOURCE_SUBMENU, subMenu) + return root + + # 如果标题已存在,返回 False + if doesContain(root, title): + return False + + # 更新根菜单并根据需要强制更新菜单 + insert(root, title, data) + if forceUpdate and c4d.threading.GeIsMainThreadAndNoDrawThread(): + c4d.gui.UpdateMenus() + + return True + +def PluginMessage(mid: int, data: typing.Any) -> bool: + """在 C4DPL_BUILDMENU 消息时更新菜单。""" + if mid == c4d.C4DPL_BUILDMENU: + # 获取 Cinema 4D 的主菜单并插入 MENU_DATA + menu: c4d.BaseContainer = c4d.gui.GetMenuResource("M_EDITOR") + UpdateMenu(root=menu, title="OmooAsset", data=MENU_DATA) + +def SomeFunction(): + """在任何时候调用以更新菜单,只要调用来自主线程。""" + # 获取 Cinema 4D 的主菜单并插入 MENU_DATA + menu: c4d.BaseContainer = c4d.gui.GetMenuResource("M_EDITOR") + UpdateMenu(root=menu, title="OmooAsset", data=MENU_DATA, forceUpdate=True) + +if __name__ == "__main__": + pass diff --git a/plugins/cinema4d/plugins/omoo_asset_alembic/omoo_assets_abc.pyp b/plugins/cinema4d/plugins/omoo_asset_alembic/omoo_assets_abc.pyp new file mode 100644 index 0000000..cc5be11 --- /dev/null +++ b/plugins/cinema4d/plugins/omoo_asset_alembic/omoo_assets_abc.pyp @@ -0,0 +1,698 @@ +import c4d +from c4d import plugins, gui +import os +import maxon +import re + + +def import_alembic_with_scale(doc, filePath, scale_value): + """导入 Alembic 文件到当前文档""" + # 获取Alembic导入插件的ID + abcImportId = c4d.FORMAT_ABCIMPORT + plug = c4d.plugins.FindPlugin(abcImportId, c4d.PLUGINTYPE_SCENELOADER) + if plug is None: + raise RuntimeError("Failed to retrieve the Alembic importer.") + + # 初始化一个字典以存储插件数据 + data = dict() + # 向Alembic导入插件发送MSG_RETRIEVEPRIVATEDATA消息 + if not plug.Message(c4d.MSG_RETRIEVEPRIVATEDATA, data): + raise RuntimeError("Failed to retrieve private data.") + + # 获取导入设置容器 + abcImport = data.get("imexporter", None) + if abcImport is None: + raise RuntimeError("Failed to retrieve BaseContainer private data.") + + # 设置Alembic导入单位为米 + scaleData = c4d.UnitScaleData() # 创建一个UnitScaleData对象 + scaleData.SetUnitScale(scale_value, c4d.DOCUMENT_UNIT_M) # 设置单位为从厘米到米 + abcImport[c4d.ABCIMPORT_SCALE] = scaleData # 将单位设置为米 + + # 合并文件到当前文档 + if not c4d.documents.MergeDocument(doc, filePath, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS): + raise RuntimeError("Failed to import the Alembic file.") + + c4d.EventAdd() + print(f"Document successfully imported from: {filePath} with scale {scale_value}.") +def search_usda_for_material_and_scales(file_path, scale_value): + if not os.path.isfile(file_path): + print(f"文件 {file_path} 不存在") + return + + with open(file_path, 'r') as file: + lines = file.readlines() + + # 匹配模式 + material_pattern = re.compile(r'def Material \"([^\"]+)\"') # 匹配材质名 + scene_scale_pattern = re.compile(r'inputs:scene_scale\s*=\s*([0-9.]+)') # 匹配场景缩放值 + raw_scene_scale_pattern = re.compile(r'inputs:raw_scene_scale\s*=\s*([0-9.]+)') # 匹配原始缩放值 + detail_scale_pattern = re.compile(r'float inputs:in2\s*=\s*([0-9.]+)') # 匹配细节缩放值 + subsurface_scale_pattern = re.compile(r'float inputs:value\s*=\s*([0-9.]+)') # 匹配SubsurfaceScale + point_color_weight_pattern = re.compile(r'float inputs:value\s*=\s*([0-9.]+)') + + materials = {} # 存储材质名和其相关的字典 + current_material = None # 用于跟踪当前的材质名 + in_subsurface_shader = False # 标记当前是否在 SubsurfaceScale Shader 中 + in_point_color_weight_shader = False + + for line in lines: + # 检查是否是材质定义行 + material_match = material_pattern.search(line) + if material_match: + current_material = material_match.group(1) + materials[current_material] = { + 'scene_scale': 1.0, # 默认场景缩放值 + 'raw_scene_scale': 1.0, # 默认原始缩放值 + 'DetailScale': 1.0, # 默认细节缩放值 + 'SubsurfaceScale': 1.0, # 默认 SubsurfaceScale 值 + 'PointColorWeight': 0.0 # 默认 PointColorWeight 值 + } + print(f"找到材质: {current_material}") + continue + + # 如果找到场景缩放值且当前材质存在,将场景缩放值存入该材质的字典 + if current_material: + scene_scale_match = scene_scale_pattern.search(line) + if scene_scale_match: + scene_scale_value = scene_scale_match.group(1) + print(f"找到场景缩放行: {line.strip()}") + print(f"场景缩放值: {scene_scale_value}") + materials[current_material]['scene_scale'] = float(scene_scale_value)*float(scale_value) + + # 如果找到原始缩放值且当前材质存在,将原始缩放值存入该材质的字典 + raw_scene_scale_match = raw_scene_scale_pattern.search(line) + if raw_scene_scale_match: + raw_scene_scale_value = raw_scene_scale_match.group(1) + print(f"找到原始缩放行: {line.strip()}") + print(f"原始缩放值: {raw_scene_scale_value}") + materials[current_material]['raw_scene_scale'] = float(raw_scene_scale_value)*float(scale_value) + continue + else: + materials[current_material]['raw_scene_scale'] = float(scene_scale_value)*float(scale_value) + + continue + + # 如果找到细节缩放值且当前材质存在,将细节缩放值存入该材质的字典 + detail_scale_match = detail_scale_pattern.search(line) + if detail_scale_match: + detail_scale_value = detail_scale_match.group(1) + print(f"找到细节缩放行: {line.strip()}") + print(f"细节缩放值: {detail_scale_value}") + materials[current_material]['DetailScale'] = detail_scale_value + continue + + # 检查是否进入了 SubsurfaceScale 的 Shader 定义 + if 'def Shader "PointColorWeight"' in line: + in_point_color_weight_shader = True + print(f"进入 PointColorWeight Shader 定义: {line.strip()}") + continue + + # 检查是否匹配 PointColorWeight Shader,提取 value 值 + if in_point_color_weight_shader: + point_color_weight_match = point_color_weight_pattern.search(line) + if point_color_weight_match: + point_color_weight_value = point_color_weight_match.group(1) + print(f"找到 PointColorWeight 的 value 值: {point_color_weight_value}") + materials[current_material]['PointColorWeight'] = point_color_weight_value + in_point_color_weight_shader = False + continue + + # 检查是否进入了 SubsurfaceScale 的 Shader 定义 + if 'def Shader "SubsurfaceScale"' in line: + in_subsurface_shader = True + print(f"进入 SubsurfaceScale Shader 定义: {line.strip()}") + continue + + # 如果在 SubsurfaceScale Shader 中,寻找 SubsurfaceScale 值 + if in_subsurface_shader: + subsurface_scale_match = subsurface_scale_pattern.search(line) + if subsurface_scale_match: + subsurface_scale_value = subsurface_scale_match.group(1) + print(f"找到SubsurfaceScale值: {subsurface_scale_value}") + materials[current_material]['SubsurfaceScale'] = subsurface_scale_value + in_subsurface_shader = False # 离开 SubsurfaceScale 定义 + continue + + return materials + +def load_material_template(doc, templatePath, material_name): + """从模板文件加载材质并重命名""" + # 加载材质模板 + loadedDoc = c4d.documents.LoadDocument(templatePath, c4d.SCENEFILTER_MATERIALS) + if loadedDoc is None: + raise RuntimeError("Could not load the material template document.") + + # 查找目标材质 + mat = loadedDoc.GetFirstMaterial() + while mat: + if mat.GetName() == "RSBaseMaterialNode": # 这里根据原始材质名称进行查找 + clone = mat.GetClone() + clone.SetName(material_name) # 重命名材质 + doc.InsertMaterial(clone) # 插入到当前文档 + c4d.EventAdd() + return clone + mat = mat.GetNext() + + raise RuntimeError(f"Material 'RSBaseMaterialNode' not found in template '{templatePath}'.") + +def FindNodeByName(graphModel, nodeName): + """使用节点名称查找指定的节点。""" + foundNodes = maxon.GraphModelHelper.FindNodesByName( + graphModel, + nodeName, # 要查找的节点名称 + maxon.NODE_KIND.NODE, # 节点类型 + maxon.PORT_DIR.INPUT, # 查找输入端口方向的节点 + True, # 精确匹配节点名称 + None # 不使用回调,返回节点列表 + ) + + + if foundNodes: + print("找到节点:",nodeName) + return foundNodes # 假设只找到一个节点 + else: + return None + +def set_scene_attribute(material, materials_attributes_from_usda): + """设置材质的scene_scale值或其他属性""" + node_material = material.GetNodeMaterialReference() + if not node_material: + return + + redshiftNodeSpaceId = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace") + graph = node_material.GetGraph(redshiftNodeSpaceId) + if graph is None: + print(f"材质 {material.GetName()} 的节点图为空或无效.") + return + + # 设置 raw_scene_scale 值 + raw_scene_scale_value = materials_attributes_from_usda['raw_scene_scale'] + if raw_scene_scale_value: + raw_scene_scale_node = FindNodeByName(graph, "raw_scene_scale")[0] + + if raw_scene_scale_node: + raw_scene_scalePort = raw_scene_scale_node.GetInputs().FindChild("in") + print("raw_scene_scalePort",raw_scene_scalePort) + if raw_scene_scalePort: + with graph.BeginTransaction() as transaction: + # Define the value of the Color's port. + raw_scene_scalePort.SetPortValue(maxon.Float(float(raw_scene_scale_value))) + print(f"已为材质 {material.GetName()} 设置 raw_scene_scale 值为 {raw_scene_scale_value}.") + transaction.Commit() + + # 设置 scene_scale 值 + scene_scale_value = materials_attributes_from_usda['scene_scale'] + if scene_scale_value: + + # 查找所有名为 'scene_scale' 的节点 + scene_scale_nodes = FindNodeByName(graph, "scene_scale") # 假设 FindNodesByName 返回多个节点 + if scene_scale_nodes: + # 遍历所有找到的 'scene_scale' 节点并设置它们的值 + for scene_scale_node in scene_scale_nodes: + scalePort = scene_scale_node.GetInputs().FindChild("in") + if scalePort: + # 创建并使用一个GraphModelInterface事务 + transaction = graph.BeginTransaction() + try: + # 设置每个 scalePort 的值 + scalePort.SetPortValue(maxon.Float(float(scene_scale_value))) + print(f"已为材质 {material.GetName()} 设置 scene_scale 值为 {scene_scale_value}.") + transaction.Commit() # 提交事务 + except Exception as e: + transaction.Rollback() # 回滚事务 + print(f"设置 scene_scale 值时出现错误: {e}") + else: + print(f"没有找到 'scene_scale' 节点。") + + # 设置 DetailScale 值 + detail_scale_value = materials_attributes_from_usda['DetailScale'] + if detail_scale_value: + detail_scale_node = FindNodeByName(graph, "DetailScale")[0] + + if detail_scale_node: + detail_scalePort = detail_scale_node.GetInputs().FindChild("in") + print("detail_scalePort",detail_scalePort) + if detail_scalePort: + with graph.BeginTransaction() as transaction: + # Define the value of the Color's port. + detail_scalePort.SetPortValue(maxon.Float(float(detail_scale_value))) + print(f"已为材质 {material.GetName()} 设置 DetailScale 值为 {detail_scale_value}.") + transaction.Commit() + + # 设置 DetailScale 值 + subsurface_scale_value = materials_attributes_from_usda['SubsurfaceScale'] + if subsurface_scale_value: + subsurface_scale_node = FindNodeByName(graph, "SubsurfaceScale")[0] + + if subsurface_scale_node: + subsurface_scalePort = subsurface_scale_node.GetInputs().FindChild("in") + print("subsurface_scalePort",subsurface_scalePort) + if subsurface_scalePort: + with graph.BeginTransaction() as transaction: + # Define the value of the Color's port. + subsurface_scalePort.SetPortValue(maxon.Float(float(subsurface_scale_value))) + print(f"已为材质 {material.GetName()} 设置 SubsurfaceScale 值为 {subsurface_scale_value}.") + transaction.Commit() + + # 设置 PointColorWeight 值 + point_color_weight_value = materials_attributes_from_usda.get('PointColorWeight') + if point_color_weight_value: + point_color_weight_nodes = FindNodeByName(graph, "PointColorWeight") + if point_color_weight_nodes: + point_color_weight_node = point_color_weight_nodes[0] + point_color_weight_port = point_color_weight_node.GetInputs().FindChild("in") + print("point_color_weight_port", point_color_weight_port) + if point_color_weight_port: + with graph.BeginTransaction() as transaction: + point_color_weight_port.SetPortValue(maxon.Float(float(point_color_weight_value))) + print(f"已为材质 {material.GetName()} 设置 PointColorWeight 值为 {point_color_weight_value}.") + transaction.Commit() + +def apply_material_to_object(material, obj, selection_tag_name, materials_attributes_from_usda): + """为网格对象添加材质标签""" + + print(f"Applying material to object: {obj.GetName()}") # 调试信息:显示应用材质的对象名称 + texture_tag = c4d.TextureTag() + texture_tag[c4d.TEXTURETAG_MATERIAL] = material + obj.InsertTag(texture_tag) + print(f"Material '{material.GetName()}' applied to '{obj.GetName()}'") # 调试信息:显示材质应用情况 + texture_tag[c4d.TEXTURETAG_RESTRICTION] = selection_tag_name + + # Redshift Object Tag的插件ID + REDSHIFT_OBJECT_TAG_ID = 1036222 + + # 检查对象是否已有Redshift Object Tag + tag = obj.GetTag(REDSHIFT_OBJECT_TAG_ID) + if tag: + print(f"对象 {obj.GetName()} 已有Redshift Object Tag,跳过。") # 调试信息 + else: + print(f"对象 {obj.GetName()} 没有Redshift Object Tag,添加标签。") # 调试信息 + tag = obj.MakeTag(REDSHIFT_OBJECT_TAG_ID) + if tag is None: + c4d.gui.MessageDialog(f"无法为对象 {obj.GetName()} 添加Redshift Object Tag。") + print(f"无法为对象 {obj.GetName()} 添加Redshift Object Tag。") # 调试信息 + else: + print(f"成功为对象 {obj.GetName()} 添加了Redshift Object Tag。") # 调试信息 + + # 启用几何覆盖和置换选项 + tag[c4d.REDSHIFT_OBJECT_GEOMETRY_OVERRIDE] = True + tag[c4d.REDSHIFT_OBJECT_GEOMETRY_DISPLACEMENTENABLED] = True + print(f"已启用对象 {obj.GetName()} 的几何覆盖和置换选项。") # 调试信息 + + # 启用曲面细分,设置最大细分为1 + # tag[c4d.REDSHIFT_OBJECT_GEOMETRY_SUBDIVISIONENABLED] = True + # tag[c4d.REDSHIFT_OBJECT_GEOMETRY_MAXTESSELLATIONSUBDIVS] = 1 + # print(f"已启用对象 {obj.GetName()} 的曲面细分,最大细分设置为1。") # 调试信息 + + # 关闭自动凹凸映射,设置最大置换为10 + tag[c4d.REDSHIFT_OBJECT_GEOMETRY_AUTOBUMPENABLED] = False + tag[c4d.REDSHIFT_OBJECT_GEOMETRY_MAXDISPLACEMENT] = 10 + + # 填入点速度 + tag[c4d.REDSHIFT_OBJECT_MOTIONBLUR_MOTIONVECTOR_X] = '.velocities[0]' + tag[c4d.REDSHIFT_OBJECT_MOTIONBLUR_MOTIONVECTOR_Y] = '.velocities[1]' + tag[c4d.REDSHIFT_OBJECT_MOTIONBLUR_MOTIONVECTOR_Z] = '.velocities[2]' + + print(f"已关闭对象 {obj.GetName()} 的自动凹凸映射,最大置换设置为10。") + # 应用 scene_scale 值 + set_scene_attribute(material,materials_attributes_from_usda) + +def set_texture_paths(material, texture_folder, material_name, texture_list): + """设置材质节点的贴图路径,并删除无效的贴图采样器节点""" + node_material = material.GetNodeMaterialReference() + if not node_material: + return + + redshiftNodeSpaceId = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace") + graph = node_material.GetGraph(redshiftNodeSpaceId) + if graph is None: + print(f"材质 {material.GetName()} 的节点图为空或无效.") + return + + print(f"处理材质 {material.GetName()}...") + + # 文件大小阈值 + size_threshold = 400 * 1024 # 400KB + + # 正则表达式,用于匹配 <前缀>..exr 文件名模式 + filename_pattern = re.compile(r'^.*\.\d{4}\.exr$', re.IGNORECASE) + + with graph.BeginTransaction() as transaction: + nodes = graph.GetRoot().GetInnerNodes(maxon.NODE_KIND.NODE, includeThis=False) + + for node in nodes: + nodeName = node.GetValue(maxon.NODE.BASE.NAME) + nodeId = node.GetId().ToString().split("@")[0] + + if nodeId == "texturesampler": + pathPort = node.GetInputs().FindChild("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0").FindChild("path") + + if pathPort: + pathValue = pathPort.GetPortValue() + pathValueStr = str(pathValue) + print(f"检查节点 {nodeId} 的路径: {pathValueStr}") + + if pathValueStr: + # 获取文件夹路径和文件名模式 + folder_path = os.path.dirname(pathValueStr) + file_name_pattern = os.path.basename(pathValueStr).replace('', r'\d{4}').replace('.exr', r'\.exr') + + if os.path.isdir(folder_path): + # 收集所有匹配的文件 + matching_files = [ + os.path.join(folder_path, filename) + for filename in os.listdir(folder_path) + if os.path.isfile(os.path.join(folder_path, filename)) and re.match(file_name_pattern, filename) + ] + + # 如果没有匹配的文件,删除节点 + if not matching_files: + print(f"没有匹配的文件,节点 {nodeId} 将被删除。") + node.Remove() + continue + + # 检查所有匹配文件的大小 + all_files_invalid = True + for file_path in matching_files: + file_size = os.path.getsize(file_path) + print(f"文件 {os.path.basename(file_path)} 大小: {file_size} 字节") + + if file_size > size_threshold: + print(f"文件 {os.path.basename(file_path)} 大于阈值 {size_threshold} 字节,节点保留。") + all_files_invalid = False + break + + # 如果所有文件大小都小于阈值,删除节点 + if all_files_invalid: + print(f"所有匹配文件大小均小于阈值,节点 {nodeId} 将被删除。") + node.Remove() + continue + else: + print(f"至少一个文件大小符合要求,节点 {nodeId} 将保留。") + else: + print(f"文件夹路径 {folder_path} 无效或不存在,节点将被删除。") + node.Remove() + continue + + # 如果贴图路径有效且没有删除,更新其路径 + texture_path = os.path.join(texture_folder, f"{material_name}_{nodeName}..exr") + pathPort.SetPortValue(texture_path) + print(f"材质 '{material_name}' 的节点 '{nodeName}' 的路径已设置为: {texture_path}") + + # 设置 UV 通道 + uvPort = node.GetInputs().FindChild("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tspace_id") + if uvPort: + uvPort.SetPortValue("uv") # 设置 UV 通道为 'uv' + print(f"材质 '{material_name}' 的节点 '{nodeName}' 的 UV 通道已设置为 'uv'。") + + transaction.Commit() + print(f"已更新材质 {material.GetName()} 的所有贴图路径并删除无效节点。") + +def delete_Invalid_texture(material): + """删除无效的贴图采样器节点(纹理节点)""" + # 正则表达式,用于匹配 <前缀>..exr 文件名模式 + filename_pattern = re.compile(r'^.*\.\d{4}\.exr$', re.IGNORECASE) + # 文件大小阈值 + size_threshold = 400 * 1024 # 400KB + + # 确保材质是一个节点材质 + nodeMaterial = material.GetNodeMaterialReference() + if nodeMaterial is None: + print(f"材质 {material.GetName()} 不是一个节点材质.") + return + + # 获取材质的节点图,检查节点空间为 Redshift + redshiftNodeSpaceId = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace") + graph = nodeMaterial.GetGraph(redshiftNodeSpaceId) + if graph is None: + print(f"材质 {material.GetName()} 的节点图为空或无效.") + return + + print(f"处理材质 {material.GetName()} 以删除无效的贴图...") + + # 使用事务来确保操作的原子性 + with graph.BeginTransaction() as transaction: + # 获取材质图的所有内部节点 + nodes = graph.GetRoot().GetInnerNodes(maxon.NODE_KIND.NODE, includeThis=False) + + # 遍历找到的节点并删除符合条件的纹理节点 + for node in nodes: + # 获取节点 ID 并检查它是否为 "texturesampler" + nodeId = node.GetId().ToString().split("@")[0] + if nodeId == "texturesampler": + # 查找 'path' 端口 + pathPort = node.GetInputs().FindChild("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0").FindChild("path") + + # 如果找到端口,且路径值是字符串,且路径有效,则检查文件 + if pathPort: + pathValue = pathPort.GetPortValue() + pathValueStr = str(pathValue) + print(f"检查节点 {nodeId} 的路径: {pathValueStr}") + + if pathValueStr: + # 获取文件夹路径和文件名 + folder_path = os.path.dirname(pathValueStr) + file_name_pattern = os.path.basename(pathValueStr).replace('', r'\d{4}').replace('.exr', r'\.exr') + # 确保文件夹路径存在 + if os.path.isdir(folder_path): + # 收集所有匹配的文件 + matching_files = [ + os.path.join(folder_path, filename) + for filename in os.listdir(folder_path) + if os.path.isfile(os.path.join(folder_path, filename)) and re.match(file_name_pattern, filename) + ] + + # 如果没有匹配的文件,删除节点 + if not matching_files: + print(f"没有匹配的文件,节点 {nodeId} 将被删除。") + node.Remove() + continue + + # 检查所有匹配文件的大小 + all_files_invalid = True + for file_path in matching_files: + file_size = os.path.getsize(file_path) + print(f"文件 {os.path.basename(file_path)} 大小: {file_size} 字节") + + if file_size > size_threshold: + print(f"文件 {os.path.basename(file_path)} 大于阈值 {size_threshold} 字节,节点保留。") + all_files_invalid = False + break + + # 如果所有文件大小都小于阈值,删除节点 + if all_files_invalid: + print(f"所有匹配文件大小均小于阈值,节点 {nodeId} 将被删除。") + node.Remove() + else: + print(f"至少一个文件大小符合要求,节点 {nodeId} 将保留。") + else: + print(f"文件夹路径 {folder_path} 无效或不存在,节点将被删除。") + node.Remove() + continue + + # 提交事务 + transaction.Commit() + +def process_objects(doc, parent, material_template_path, filePath, usda_file_path, scale_value): + """处理所有对象及其子对象,应用材质并从 USDA 中读取 scene_scale""" + document_folder = os.path.dirname(filePath) + texture_folder = os.path.join(document_folder, "Textures") + # print("texture_folder:", texture_folder) + + if not os.path.isdir(texture_folder): + print("Texture folder does not exist.") + texture_folder = document_folder + + # 获取贴图列表 + texture_list = [f for f in os.listdir(texture_folder) if os.path.isfile(os.path.join(texture_folder, f))] + # 从 USDA 文件中读取材质与 scene_scale 对应关系 + materials_attributes_from_usda = search_usda_for_material_and_scales(usda_file_path, scale_value) + print("材质属性列表",materials_attributes_from_usda) + def process_child(obj): + if obj is None: + return + + print("Object Name:", obj.GetName()) + + # 如果是网格模型 + if obj.GetType() == c4d.Oalembicgenerator: + print("这是一个 Alembic 生成器") + tags = obj.GetTags() + for tag in tags: + tag_name = tag.GetName() + if tag_name.startswith("material_"): + material_name = tag_name[len("material_"):] + print("Tag Name:", tag_name) + if material_name: + # 从模板文件中导入材质 + material = load_material_template(doc, material_template_path, material_name) + + material_attributes = materials_attributes_from_usda[material_name] + + # 应用材质并设置属性(例如 scene_scale 和 DetailScale) + apply_material_to_object(material, obj, tag_name, material_attributes) + + # 设置材质的贴图路径 + set_texture_paths(material, texture_folder, material_name, texture_list) + + # 设置贴图路径后,删除无效的纹理节点 + delete_Invalid_texture(material) + + # 递归处理子对象 + for child in obj.GetChildren(): + process_child(child) + + process_child(parent) + +class AlembicImportDialog(gui.GeDialog): + ID_SCALE_INPUT = 1000 + ID_BUTTON_OK = 1001 + + def __init__(self): + super().__init__() + self.scale_value = 1.0 # 默认缩放值 + + def CreateLayout(self): + # 设置窗口标题 + self.SetTitle("Alembic Import Settings") + + # 添加文本标签 + self.AddStaticText(0, c4d.BFH_LEFT, name="Set Scale Value:") + + # 添加一个输入框来输入缩放值 + self.AddEditNumberArrows(self.ID_SCALE_INPUT, c4d.BFH_LEFT, initw=200, inith=10) + self.SetFloat(self.ID_SCALE_INPUT, self.scale_value) # 设置默认值 + + # 添加OK按钮 + self.AddButton(self.ID_BUTTON_OK, c4d.BFH_CENTER, name="OK") + + return True + + def Command(self, id, msg): + # 当点击OK按钮时 + if id == self.ID_BUTTON_OK: + # 获取用户输入的缩放值 + self.scale_value = self.GetFloat(self.ID_SCALE_INPUT) + self.Close() # 关闭对话框 + return True + return False + +def main(doc): + # 弹出文件选择器,选择 Alembic 文件 + filePath = c4d.storage.LoadDialog(c4d.FILESELECTTYPE_ANYTHING, "Select Alembic File", c4d.FILESELECT_LOAD, "abc") + if not filePath: + return + + # 创建并显示自定义对话框 + dialog = AlembicImportDialog() + dialog.Open(c4d.DLG_TYPE_MODAL) # 以模态方式显示对话框,等待用户输入 + + # 获取用户选择的缩放值 + scale_value = dialog.scale_value + + # 调用导入 Alembic 文件的函数,应用用户输入的缩放值 + import_alembic_with_scale(doc, filePath, scale_value) + + # 获取Alembic文件的文件夹路径 + document_folder = os.path.dirname(filePath) + + # 设置usda文件路径 + usda_file_path = os.path.join(document_folder, "temp.usda") + + # 获取当前脚本的绝对路径 + script_path = os.path.abspath(__file__) + # 获取当前脚本所在文件夹的路径 + script_dir = os.path.dirname(script_path) + + # 设置材质模板路径 + material_template_path = os.path.join(script_dir, "RSBaseMaterialNode.c4d") + + # 处理所有对象及其子对象 + root = doc.GetFirstObject() + process_objects(doc, root, material_template_path, filePath, usda_file_path, scale_value) + + +class AlembicImportDialog(gui.GeDialog): + ID_SCALE_INPUT = 1000 + ID_BUTTON_OK = 1001 + + def __init__(self): + super().__init__() + self.scale_value = 1.0 # 默认缩放值 + + def CreateLayout(self): + # 设置窗口标题 + self.SetTitle("Alembic Import Settings") + + # 添加文本标签 + self.AddStaticText(0, c4d.BFH_LEFT, name="Set Scale Value:") + + # 添加一个输入框来输入缩放值 + self.AddEditNumberArrows(self.ID_SCALE_INPUT, c4d.BFH_LEFT, initw=200, inith=10) + self.SetFloat(self.ID_SCALE_INPUT, self.scale_value) # 设置默认值 + + # 添加OK按钮 + self.AddButton(self.ID_BUTTON_OK, c4d.BFH_CENTER, name="OK") + + return True + + def Command(self, id, msg): + # 当点击OK按钮时 + if id == self.ID_BUTTON_OK: + # 获取用户输入的缩放值 + self.scale_value = self.GetFloat(self.ID_SCALE_INPUT) + self.Close() # 关闭对话框 + return True + return False + +def main(doc): + # 弹出文件选择器,选择 Alembic 文件 + filePath = c4d.storage.LoadDialog(c4d.FILESELECTTYPE_ANYTHING, "Select Alembic File", c4d.FILESELECT_LOAD, "abc") + if not filePath: + return + + # 创建并显示自定义对话框 + dialog = AlembicImportDialog() + dialog.Open(c4d.DLG_TYPE_MODAL) # 以模态方式显示对话框,等待用户输入 + + # 获取用户选择的缩放值 + scale_value = dialog.scale_value + + # 调用导入 Alembic 文件的函数,应用用户输入的缩放值 + import_alembic_with_scale(doc, filePath, scale_value) + + # 获取Alembic文件的文件夹路径 + document_folder = os.path.dirname(filePath) + + # 设置usda文件路径 + usda_file_path = os.path.join(document_folder, "temp.usda") + + # 获取当前脚本的绝对路径 + script_path = os.path.abspath(__file__) + # 获取当前脚本所在文件夹的路径 + script_dir = os.path.dirname(script_path) + + # 设置材质模板路径 + material_template_path = os.path.join(script_dir, "RSBaseMaterialNode.c4d") + + # 处理所有对象及其子对象 + root = doc.GetFirstObject() + process_objects(doc, root, material_template_path, filePath, usda_file_path, scale_value) + + +class OmooAssetsMenu(plugins.CommandData): + def Execute(self, doc): + # 调用 OmooAssetsAlembic.py 中的 main 函数 + main(doc) + return True + + def GetState(self, doc): + return c4d.CMD_ENABLED + +if __name__ == "__main__": + PLUGIN_ID = 1064257 + # 注册插件 + if not plugins.RegisterCommandPlugin( + PLUGIN_ID, "Import .abc (OmooAsset)", 0, None, "Import Alembic File", OmooAssetsMenu()): + raise Exception("Failed to register plugin") \ No newline at end of file From f4c511fd9c0adf1e67becec4d74c44b479cc9078 Mon Sep 17 00:00:00 2001 From: KuangZheng Date: Thu, 16 Jan 2025 11:40:00 +0800 Subject: [PATCH 2/2] test 1111 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4256ab6..2519dab 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![alt text](docs/images/overview.png) -# Omoo Asset +# Omoo Asset 111 USD based 3D model storage solution, our goal is **"create once, render anywhere"**. If you're struggling to keep your model assets rendering consistently on different platforms by repeatedly creating material spheres manually, and Houdini is in your workflow, then Omoo Asset is the best choice!