From 186d556bc8be93890b8f65436382f5336c21ba1e Mon Sep 17 00:00:00 2001 From: Lars Brubaker Date: Tue, 24 Oct 2023 17:53:43 -0700 Subject: [PATCH] Working on a new radial pinch --- .../ApplicationView/SceneOperations.cs | 23 +- .../DesignTools/Operations/IRadiusProvider.cs | 36 ++ .../Operations/ObjectCircleExtensions.cs | 62 ++++ .../Operations/RadialPinchObject3D.cs | 309 ++++++++++++++++++ .../DesignTools/Operations/TwistObject3D.cs | 32 +- StaticData/Icons/radial-pinch.png | Bin 0 -> 5245 bytes StaticData/Translations/Master.txt | 6 + 7 files changed, 435 insertions(+), 33 deletions(-) create mode 100644 MatterControlLib/DesignTools/Operations/IRadiusProvider.cs create mode 100644 MatterControlLib/DesignTools/Operations/ObjectCircleExtensions.cs create mode 100644 MatterControlLib/DesignTools/Operations/RadialPinchObject3D.cs create mode 100644 StaticData/Icons/radial-pinch.png diff --git a/MatterControlLib/ApplicationView/SceneOperations.cs b/MatterControlLib/ApplicationView/SceneOperations.cs index 14b0ce5cb..a785e3480 100644 --- a/MatterControlLib/ApplicationView/SceneOperations.cs +++ b/MatterControlLib/ApplicationView/SceneOperations.cs @@ -763,7 +763,8 @@ namespace MatterHackers.MatterControl { CurveOperation(), PinchOperation(), - TwistOperation(), + RadialPinchOperation(), + TwistOperation(), PlaneCutOperation(), #if DEBUG FindSliceOperation(), @@ -1451,7 +1452,25 @@ namespace MatterHackers.MatterControl }; } - private static SceneOperation UngroupOperation() + private static SceneOperation RadialPinchOperation() + { + return new SceneOperation("Radial Pinch") + { + ResultType = typeof(RadialPinchObject3D), + TitleGetter = () => "Radial Pinch".Localize(), + Action = (sceneContext) => + { + var radialPinch = new RadialPinchObject3D(); + radialPinch.WrapSelectedItemAndSelect(sceneContext.Scene); + }, + Icon = (theme) => StaticData.Instance.LoadIcon("radial-pinch.png", 16, 16).GrayToColor(theme.TextColor), + HelpTextGetter = () => "At least 1 part must be selected".Localize().Stars(), + IsEnabled = (sceneContext) => IsMeshObject(sceneContext.Scene.SelectedItem), + }; + } + + + private static SceneOperation UngroupOperation() { return new SceneOperation("Ungroup") { diff --git a/MatterControlLib/DesignTools/Operations/IRadiusProvider.cs b/MatterControlLib/DesignTools/Operations/IRadiusProvider.cs new file mode 100644 index 000000000..d4f1d3022 --- /dev/null +++ b/MatterControlLib/DesignTools/Operations/IRadiusProvider.cs @@ -0,0 +1,36 @@ +/* +Copyright (c) 2018, Lars Brubaker, John Lewin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +namespace MatterHackers.MatterControl.DesignTools +{ + public interface IRadiusProvider + { + double Radius { get; } + } +} \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/ObjectCircleExtensions.cs b/MatterControlLib/DesignTools/Operations/ObjectCircleExtensions.cs new file mode 100644 index 000000000..ae8c32ff7 --- /dev/null +++ b/MatterControlLib/DesignTools/Operations/ObjectCircleExtensions.cs @@ -0,0 +1,62 @@ +/* +Copyright (c) 2018, Lars Brubaker, John Lewin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +using System.Collections.Generic; +using System.Linq; +using MatterHackers.DataConverters3D; +using MatterHackers.PolygonMesh; +using MatterHackers.VectorMath; + +namespace MatterHackers.MatterControl.DesignTools +{ + public static class ObjectCircleExtensions + { + public static Circle GetSmallestEnclosingCircleAlongZ(this IObject3D object3D) + { + var visibleMeshes = object3D.VisibleMeshes().Select(vm => (source: vm, convexHull: vm.Mesh.GetConvexHull(false))).ToList(); + + IEnumerable GetVertices() + { + foreach (var visibleMesh in visibleMeshes) + { + var matrix = visibleMesh.source.WorldMatrix(object3D); + foreach (var positon in visibleMesh.convexHull.Vertices) + { + var transformed = positon.Transform(matrix); + yield return new Vector2(transformed.X, transformed.Y); + } + } + } + + var circle = SmallestEnclosingCircle.MakeCircle(GetVertices()); + + return circle; + } + } +} \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/RadialPinchObject3D.cs b/MatterControlLib/DesignTools/Operations/RadialPinchObject3D.cs new file mode 100644 index 000000000..88d6b927d --- /dev/null +++ b/MatterControlLib/DesignTools/Operations/RadialPinchObject3D.cs @@ -0,0 +1,309 @@ +/* +Copyright (c) 2018, Lars Brubaker, John Lewin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MatterHackers.Agg; +using MatterHackers.Agg.UI; +using MatterHackers.Agg.VertexSource; +using MatterHackers.DataConverters3D; +using MatterHackers.Localizations; +using MatterHackers.MatterControl.DesignTools.Operations; +using MatterHackers.MatterControl.PartPreviewWindow; +using MatterHackers.PolygonMesh; +using MatterHackers.RenderOpenGl; +using MatterHackers.RenderOpenGl.OpenGl; +using MatterHackers.VectorMath; +using static MatterHackers.MatterControl.DesignTools.LinearHorizontalOffset2D; + +namespace MatterHackers.MatterControl.DesignTools +{ + public class LinearHorizontalOffset2D + { + public class ControlPoint + { + public double WidthRatio { get; set; } = 1.0; + + public double TopControlRatio { get; set; } + + public double BottomControlRatio { get; set; } + } + + public List ControlPoints { get; set; } = new List() + { + new ControlPoint() { WidthRatio = 1.0, TopControlRatio = .5, BottomControlRatio = 0 }, + new ControlPoint() { WidthRatio = 1.5, TopControlRatio = .5, BottomControlRatio = .5 }, + new ControlPoint() { WidthRatio = 1.0, TopControlRatio = 0, BottomControlRatio = .5 }, + }; + + public VertexStorage GetOffsetPath(double radius, double totalHeight) + { + double segmentHeight = totalHeight / (ControlPoints.Count - 1); + var vertexStorage = new VertexStorage(); + for (int i = 0; i < ControlPoints.Count; i++) + { + if (i == 0) + { + vertexStorage.MoveTo(radius * ControlPoints[0].WidthRatio, 0); + } + else + { + var curPoint = ControlPoints[i]; + var x = radius * curPoint.WidthRatio; + var y = i * segmentHeight; + vertexStorage.curve4(x, y - curPoint.BottomControlRatio * segmentHeight, x, y - curPoint.TopControlRatio * segmentHeight, x, y); + } + } + + return vertexStorage; + } + } + + public class RadialPinchObject3D : OperationSourceContainerObject3D, IPropertyGridModifier, IEditorDraw + { + public RadialPinchObject3D() + { + Name = "Radial Pinch".Localize(); + } + + public LinearHorizontalOffset2D LinearHorizontalOffset { get; set; } = new LinearHorizontalOffset2D(); + + [Description("Specifies the number of vertical cuts required to ensure the part can be pinched well.")] + [Slider(0, 50, snapDistance: 1)] + public IntOrExpression PinchSlices { get; set; } = 5; + + [Description("Enable advanced features.")] + public bool Advanced { get; set; } = false; + + [Description("Allows for the repositioning of the rotation origin")] + public Vector2 RotationOffset { get; set; } + + [Description("The percentage up from the bottom to end the pinch")] + [Slider(0, 100, Easing.EaseType.Quadratic, snapDistance: 1)] + public DoubleOrExpression EndHeightPercent { get; set; } = 100; + + [Description("The percentage up from the bottom to start the pinch")] + [Slider(0, 100, Easing.EaseType.Quadratic, snapDistance: 1)] + public DoubleOrExpression StartHeightPercent { get; set; } = 0; + + public IRadiusProvider RadiusProvider + { + get + { + if (this.SourceContainer.Children.Count == 1 + && this.SourceContainer.Children.First() is IRadiusProvider radiusProvider) + { + return radiusProvider; + } + + return null; + } + } + + public void DrawEditor(Object3DControlsLayer layer, DrawEventArgs e) + { + var sourceAabb = this.SourceContainer.GetAxisAlignedBoundingBox(); + var rotationCenter = SourceContainer.GetSmallestEnclosingCircleAlongZ().Center + RotationOffset; + + var center = new Vector3(rotationCenter.X, rotationCenter.Y, sourceAabb.Center.Z); + + // render the top and bottom rings + layer.World.RenderCylinderOutline(this.WorldMatrix(), center, 1, sourceAabb.ZSize, 15, Color.Red, Color.Red, 5); + + // turn the lighting back on + GL.Enable(EnableCap.Lighting); + } + + public AxisAlignedBoundingBox GetEditorWorldspaceAABB(Object3DControlsLayer layer) + { + var sourceAabb = this.SourceContainer.GetAxisAlignedBoundingBox(); + var rotationCenter = SourceContainer.GetSmallestEnclosingCircleAlongZ().Center + RotationOffset; + var center = new Vector3(rotationCenter.X, rotationCenter.Y, sourceAabb.Center.Z); + return AxisAlignedBoundingBox.CenteredBox(new Vector3(1, 1, sourceAabb.ZSize), center).NewTransformed(this.WorldMatrix()); + } + + public override Task Rebuild() + { + this.DebugDepth("Rebuild"); + + bool valuesChanged = false; + + var aabb = this.GetAxisAlignedBoundingBox(); + + var pinchSlices = PinchSlices.ClampIfNotCalculated(this, 0, 300, ref valuesChanged); + var endHeightPercent = EndHeightPercent.ClampIfNotCalculated(this, 0, 100, ref valuesChanged); + endHeightPercent = EndHeightPercent.ClampIfNotCalculated(this, 1, 100, ref valuesChanged); + var startHeightPercent = StartHeightPercent.ClampIfNotCalculated(this, 0, endHeightPercent - 1, ref valuesChanged); + startHeightPercent = Math.Min(endHeightPercent - 1, startHeightPercent); + + var rebuildLocks = this.RebuilLockAll(); + + return ApplicationController.Instance.Tasks.Execute( + "Pinch".Localize(), + null, + (reporter, cancellationToken) => + { + var sourceAabb = this.SourceContainer.GetAxisAlignedBoundingBox(); + + var bottom = sourceAabb.MinXYZ.Z; + var top = sourceAabb.MaxXYZ.Z; + var size = sourceAabb.ZSize; + if (Advanced) + { + top -= sourceAabb.ZSize * endHeightPercent / 100.0; + bottom += sourceAabb.ZSize * startHeightPercent / 100.0; + size = top - bottom; + } + + double numberOfCuts = pinchSlices; + + double cutSize = size / numberOfCuts; + var cuts = new List(); + for (int i = 0; i < numberOfCuts + 1; i++) + { + var ratio = i / numberOfCuts; + cuts.Add(bottom - cutSize + (size * ratio)); + } + + // get the rotation from the center of the circumscribed circle of the convex hull + var enclosingCircle = SourceContainer.GetSmallestEnclosingCircleAlongZ(); + var rotationCenter = enclosingCircle.Center + RotationOffset; + + var horizontalOffest = LinearHorizontalOffset.GetOffsetPath(enclosingCircle.Radius + RotationOffset.Length, size); + + var pinchedChildren = new List(); + + foreach (var sourceItem in SourceContainer.VisibleMeshes()) + { + var originalMesh = sourceItem.Mesh; + var status = "Copy Mesh".Localize(); + reporter?.Invoke(0, status); + var transformedMesh = originalMesh.Copy(CancellationToken.None); + var itemMatrix = sourceItem.WorldMatrix(SourceContainer); + + // transform into this space + transformedMesh.Transform(itemMatrix); + + status = "Split Mesh".Localize(); + reporter?.Invoke(0, status); + + // split the mesh along the z axis + transformedMesh.SplitOnPlanes(Vector3.UnitZ, cuts, cutSize / 8); + + for (int i = 0; i < transformedMesh.Vertices.Count; i++) + { + var position = transformedMesh.Vertices[i]; + + var ratio = 1.0; + + if (position.Z >= bottom + && position.Z <= top) + { + ratio = (position.Z - bottom) / size; + } + + var positionXy = new Vector2(position) - rotationCenter; + if (true) + { + positionXy *= Easing.Quadratic.InOut(ratio); + } + else + { + //positionXy *= horizontalOffest.GetXAtY(positionXy.Y - bottom); + } + positionXy += rotationCenter; + transformedMesh.Vertices[i] = new Vector3Float(positionXy.X, positionXy.Y, position.Z); + } + + // transform back into item local space + transformedMesh.Transform(itemMatrix.Inverted); + + //transformedMesh.MergeVertices(.1); + transformedMesh.CalculateNormals(); + + var pinchedChild = new Object3D() + { + Mesh = transformedMesh + }; + pinchedChild.CopyWorldProperties(sourceItem, SourceContainer, Object3DPropertyFlags.All, false); + pinchedChild.Visible = true; + + pinchedChildren.Add(pinchedChild); + } + + RemoveAllButSource(); + this.SourceContainer.Visible = false; + + this.Children.Modify((list) => + { + list.AddRange(pinchedChildren); + }); + + ApplyHoles(reporter, cancellationToken.Token); + + UiThread.RunOnIdle(() => + { + rebuildLocks.Dispose(); + this.CancelAllParentBuilding(); + Parent?.Invalidate(new InvalidateArgs(this, InvalidateType.Children)); + Invalidate(InvalidateType.DisplayValues); + }); + + return Task.CompletedTask; + }); + } + + private Dictionary changeSet = new Dictionary(); + + public void UpdateControls(PublicPropertyChange change) + { + changeSet.Clear(); + + changeSet.Add(nameof(RotationOffset), Advanced); + changeSet.Add(nameof(StartHeightPercent), Advanced); + changeSet.Add(nameof(EndHeightPercent), Advanced); + + // first turn on all the settings we want to see + foreach (var kvp in changeSet.Where(c => c.Value)) + { + change.SetRowVisible(kvp.Key, () => kvp.Value); + } + + // then turn off all the settings we want to hide + foreach (var kvp in changeSet.Where(c => !c.Value)) + { + change.SetRowVisible(kvp.Key, () => kvp.Value); + } + } + } +} \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Operations/TwistObject3D.cs b/MatterControlLib/DesignTools/Operations/TwistObject3D.cs index 17c667970..fab13ed56 100644 --- a/MatterControlLib/DesignTools/Operations/TwistObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/TwistObject3D.cs @@ -48,7 +48,7 @@ using Newtonsoft.Json; namespace MatterHackers.MatterControl.DesignTools { - public class TwistObject3D : OperationSourceContainerObject3D, IPropertyGridModifier, IEditorDraw + public class TwistObject3D : OperationSourceContainerObject3D, IPropertyGridModifier, IEditorDraw { public TwistObject3D() { @@ -374,34 +374,4 @@ namespace MatterHackers.MatterControl.DesignTools } } } - - public interface IRadiusProvider - { - double Radius { get; } - } - - public static class ObjectCircleExtensions - { - public static Circle GetSmallestEnclosingCircleAlongZ(this IObject3D object3D) - { - var visibleMeshes = object3D.VisibleMeshes().Select(vm => (source: vm, convexHull: vm.Mesh.GetConvexHull(false))).ToList(); - - IEnumerable GetVertices() - { - foreach (var visibleMesh in visibleMeshes) - { - var matrix = visibleMesh.source.WorldMatrix(object3D); - foreach (var positon in visibleMesh.convexHull.Vertices) - { - var transformed = positon.Transform(matrix); - yield return new Vector2(transformed.X, transformed.Y); - } - } - } - - var circle = SmallestEnclosingCircle.MakeCircle(GetVertices()); - - return circle; - } - } } \ No newline at end of file diff --git a/StaticData/Icons/radial-pinch.png b/StaticData/Icons/radial-pinch.png new file mode 100644 index 0000000000000000000000000000000000000000..cbb7d31146b55387e0c488141448d205f5cd0cf9 GIT binary patch literal 5245 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE^$GjU`@Bmw9rwnI-cWJ#N{tAI2I zx9fW~?R3uUZMeZ8!QNuV(#vzPT`==mGvgxvMchBPFK7P3Y(F*fRPMU??Fzcz{&vnmbEj@SZ`xAYhCtIhQT+i%N>Qhyj(83{EIwjDNS1`&n$Y)80 z{j*2%8~=J1oqVFzJTaLubj5@m`ycXu-d)r2@wfDBl(_IaDN_D%L%w*1sm|-)H#SA7 zH++}eamngmmhg<kcyrS9WdRxt@6*5dS|+>@ z`>@_}@7C{yzZgTBRz z;GCL~=}}db8eHWUl3bOYY?-2DZ^va*VO5b^kegbPs8ErclUHn2VXFi-*D9~r3M8zr zqySb@l5MLL;TxdfoL`ixV4`QDXQ1m^ky&P>WXGjoQ-9F z1v$^t#a0Pqzg0?pa%PGZm}!(`WNcw%V61DHoNB6Tl5A?GYiVL)q-&OJk!ER>oM>)j zkb-2CXI^nhVqS78$f%0k0=>-46swfvR5OD#Qv+QyQ*%pQ6C=|k-9$5U16@-K10y4I zQw!tdRAVF~{EITvGxHL2kX;2bDkU?;%F@g%)hIR5ST`-zG+Ecg%*aC5(%9Tk*UZ>B zEz!Ur(b7CM6>L;WvXxtYQEp<1tx{%gVtT56L0&po0uG_W!<2{AOYGBU6-HqxF@7d^sN(`89ZS*n1 z1EK(_h_T~RfQSXTxY=>p=!45SP?-lY5LD*S5<}yGmR2Yzj9Nlc_>KnGXmF7f0wgIO zOU*z5?$H2g%=IP=XQgQ3;ZSRbj zP?_T&&(C$AWs#7wl_>7`(8-p%EJ;bzoOIbn0) ziL!{9;5)T7&cc&a873T8xu|e)zDiy4)&3GW<(aF~_!qx=!YG`}^ImcP>Tk0sZJPjJ5xHa= z|J(khR?KeK4gN4kt&-SudCS*Jm8SI6N_I#UI%lVc&V|m`^2DPH|#+!OC z#(T_Dsht+mcEm){cU$wDs!!hoD>@Dx-fX?UMtEV`p|YaeClgnR2z^@ma$kyM=BZ1O zi{yenRv3#guUOYR(Z-`>)0QW9dwd1ws(s|t(<$ejvFLfK5UbvH zi(k9^-CDruYBDxI?ntzw_7WpJRT;KEW<3>bY2! z5)(vD}@Z1Z;qvK*df>9S$V)7+|QFYih5-b$S{UsJiMa?yP6 zN42gj`xD-#r!u%Smbw=_xux^rzVK&@{r&IWzkI*;?vb}veX*Y%JTClFn!9}UO!k<< zy3S1p*FE0y=1>5uX~MP8^p+R5e--VMX?Y=$o36D*yn`wB_v>=wE2VQ%?)T1K_~~~8 z>l?P&#wPMB?jK$ASl=qH<255Vp)0TnFoWVDiJ_E5!Lx4Sd3kQmC98QC+~Rk)wkV%+ zMRC@CO}jNaW;zEY-#%&QyQ5f=vE$tNI@=tz-7_aLiU0N%oqJSn^6cevUB6W=m7lU^ z0o#^&>VH=~dbvI;Lg8ze>RkQK5Y_Ws+vSoUnf%lhoyRG8Y5j%I2Oq8aX3E(()9m80 z4|$)e!yXj=d>y*?4Y%+@5wq==xP?FWuIhLh`b7H9uk~lnu*pa*n{w`S+;R0)h4qg@ fEWj>alk=N@>C8xFmTv;OpfMd!S3j3^P6