From 4413de463e504b9b29b925102a00aceaf96b746b Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 16 Apr 2019 19:14:16 +0200 Subject: [PATCH] Remove unsupported line caps --- src/Avalonia.Controls/Shapes/Shape.cs | 72 ++++++++++-------- src/Avalonia.Visuals/Media/BrushExtensions.cs | 10 +-- src/Avalonia.Visuals/Media/Pen.cs | 72 ++++++++---------- src/Avalonia.Visuals/Media/PenLineCap.cs | 3 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 19 ++--- .../Avalonia.Direct2D1/PrimitiveExtensions.cs | 8 +- .../Avalonia.RenderTests/Shapes/PathTests.cs | 8 +- .../Shapes/PolylineTests.cs | 3 +- .../Path/Path_With_PenLineCap.expected.png | Bin 1155 -> 1522 bytes .../Path/Path_With_PenLineCap.expected.png | Bin 1155 -> 1522 bytes 10 files changed, 90 insertions(+), 105 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 57dbeba1cc..499dfb5320 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -21,13 +21,19 @@ namespace Avalonia.Controls.Shapes public static readonly StyledProperty> StrokeDashArrayProperty = AvaloniaProperty.Register>(nameof(StrokeDashArray)); - + public static readonly StyledProperty StrokeDashOffsetProperty = AvaloniaProperty.Register(nameof(StrokeDashOffset)); public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register(nameof(StrokeThickness)); + public static readonly StyledProperty StrokeLineCapProperty = + AvaloniaProperty.Register(nameof(StrokeLineCap), PenLineCap.Flat); + + public static readonly StyledProperty StrokeJoinProperty = + AvaloniaProperty.Register(nameof(StrokeJoin), PenLineJoin.Miter); + private Matrix _transform = Matrix.Identity; private Geometry _definingGeometry; private Geometry _renderedGeometry; @@ -36,7 +42,9 @@ namespace Avalonia.Controls.Shapes static Shape() { AffectsMeasure(StretchProperty, StrokeThicknessProperty); - AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty); + + AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty, StrokeDashOffsetProperty, + StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty); } public Geometry DefiningGeometry @@ -106,7 +114,7 @@ namespace Avalonia.Controls.Shapes get { return GetValue(StrokeDashArrayProperty); } set { SetValue(StrokeDashArrayProperty, value); } } - + public double StrokeDashOffset { get { return GetValue(StrokeDashOffsetProperty); } @@ -119,13 +127,17 @@ namespace Avalonia.Controls.Shapes set { SetValue(StrokeThicknessProperty, value); } } - public PenLineCap StrokeDashCap { get; set; } = PenLineCap.Flat; - - public PenLineCap StrokeStartLineCap { get; set; } = PenLineCap.Flat; - - public PenLineCap StrokeEndLineCap { get; set; } = PenLineCap.Flat; + public PenLineCap StrokeLineCap + { + get { return GetValue(StrokeLineCapProperty); } + set { SetValue(StrokeLineCapProperty, value); } + } - public PenLineJoin StrokeJoin { get; set; } = PenLineJoin.Miter; + public PenLineJoin StrokeJoin + { + get { return GetValue(StrokeJoinProperty); } + set { SetValue(StrokeJoinProperty, value); } + } public override void Render(DrawingContext context) { @@ -133,8 +145,8 @@ namespace Avalonia.Controls.Shapes if (geometry != null) { - var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset), - StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap, StrokeJoin); + var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset), + StrokeLineCap, StrokeJoin); context.DrawGeometry(Fill, pen, geometry); } } @@ -169,11 +181,11 @@ namespace Avalonia.Controls.Shapes protected void InvalidateGeometry() { - this._renderedGeometry = null; - this._definingGeometry = null; + _renderedGeometry = null; + _definingGeometry = null; InvalidateMeasure(); } - + protected override Size MeasureOverride(Size availableSize) { bool deferCalculateTransform; @@ -203,10 +215,10 @@ namespace Avalonia.Controls.Shapes return CalculateShapeSizeAndSetTransform(availableSize); } } - + protected override Size ArrangeOverride(Size finalSize) { - if(_calculateTransformOnArrange) + if (_calculateTransformOnArrange) { _calculateTransformOnArrange = false; CalculateShapeSizeAndSetTransform(finalSize); @@ -312,25 +324,25 @@ namespace Avalonia.Controls.Shapes private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e) { - var control = e.Sender as Shape; + if (!(e.Sender is Shape control)) + { + return; + } - if (control != null) + // If the geometry is invalidated when Bounds changes, only invalidate when the Size + // portion changes. + if (e.Property == BoundsProperty) { - // If the geometry is invalidated when Bounds changes, only invalidate when the Size - // portion changes. - if (e.Property == BoundsProperty) - { - var oldBounds = (Rect)e.OldValue; - var newBounds = (Rect)e.NewValue; + var oldBounds = (Rect)e.OldValue; + var newBounds = (Rect)e.NewValue; - if (oldBounds.Size == newBounds.Size) - { - return; - } + if (oldBounds.Size == newBounds.Size) + { + return; } - - control.InvalidateGeometry(); } + + control.InvalidateGeometry(); } } } diff --git a/src/Avalonia.Visuals/Media/BrushExtensions.cs b/src/Avalonia.Visuals/Media/BrushExtensions.cs index cd351071dd..522953eb04 100644 --- a/src/Avalonia.Visuals/Media/BrushExtensions.cs +++ b/src/Avalonia.Visuals/Media/BrushExtensions.cs @@ -34,16 +34,14 @@ namespace Avalonia.Media { Contract.Requires(pen != null); - var brush = pen?.Brush?.ToImmutable(); - return pen == null || ReferenceEquals(pen?.Brush, brush) ? + var brush = pen.Brush?.ToImmutable(); + return ReferenceEquals(pen.Brush, brush) ? pen : new Pen( brush, thickness: pen.Thickness, - dashStyle: pen.DashStyle, - dashCap: pen.DashCap, - startLineCap: pen.StartLineCap, - endLineCap: pen.EndLineCap, + dashStyle: pen.DashStyle, + lineCap: pen.LineCap, lineJoin: pen.LineJoin, miterLimit: pen.MiterLimit); } diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index ddd8091801..ee427c913b 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -11,63 +11,45 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// - /// The brush used to draw. + /// The stroke color. /// The stroke thickness. /// The dash style. - /// The dash cap. - /// The start line cap. - /// The end line cap. + /// Specifies the type of graphic shape to use on both ends of a line. /// The line join. /// The miter limit. public Pen( - IBrush brush, + uint color, double thickness = 1.0, - DashStyle dashStyle = null, - PenLineCap dashCap = PenLineCap.Flat, - PenLineCap startLineCap = PenLineCap.Flat, - PenLineCap endLineCap = PenLineCap.Flat, - PenLineJoin lineJoin = PenLineJoin.Miter, - double miterLimit = 10.0) + DashStyle dashStyle = null, + PenLineCap lineCap = PenLineCap.Flat, + PenLineJoin lineJoin = PenLineJoin.Miter, + double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) { - Brush = brush; - Thickness = thickness; - DashCap = dashCap; - StartLineCap = startLineCap; - EndLineCap = endLineCap; - LineJoin = lineJoin; - MiterLimit = miterLimit; - DashStyle = dashStyle; } /// /// Initializes a new instance of the class. /// - /// The stroke color. + /// The brush used to draw. /// The stroke thickness. /// The dash style. - /// The dash cap. - /// The start line cap. - /// The end line cap. + /// The line cap. /// The line join. /// The miter limit. public Pen( - uint color, + IBrush brush, double thickness = 1.0, - DashStyle dashStyle = null, - PenLineCap dashCap = PenLineCap.Flat, - PenLineCap startLineCap = PenLineCap.Flat, - PenLineCap endLineCap = PenLineCap.Flat, - PenLineJoin lineJoin = PenLineJoin.Miter, + DashStyle dashStyle = null, + PenLineCap lineCap = PenLineCap.Flat, + PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) { - Brush = new SolidColorBrush(color); + Brush = brush; Thickness = thickness; - StartLineCap = startLineCap; - EndLineCap = endLineCap; + LineCap = lineCap; LineJoin = lineJoin; MiterLimit = miterLimit; DashStyle = dashStyle; - DashCap = dashCap; } /// @@ -78,18 +60,26 @@ namespace Avalonia.Media /// /// Gets the stroke thickness. /// - public double Thickness { get; } = 1.0; + public double Thickness { get; } + /// + /// Specifies the style of dashed lines drawn with a object. + /// public DashStyle DashStyle { get; } - public PenLineCap DashCap { get; } - - public PenLineCap StartLineCap { get; } = PenLineCap.Flat; - - public PenLineCap EndLineCap { get; } = PenLineCap.Flat; + /// + /// Specifies the type of graphic shape to use on both ends of a line. + /// + public PenLineCap LineCap { get; } - public PenLineJoin LineJoin { get; } = PenLineJoin.Miter; + /// + /// Specifies how to join consecutive line or curve segments in a (subpath) contained in a object. + /// + public PenLineJoin LineJoin { get; } - public double MiterLimit { get; } = 10.0; + /// + /// The limit on the ratio of the miter length to half this pen's Thickness. + /// + public double MiterLimit { get; } } } diff --git a/src/Avalonia.Visuals/Media/PenLineCap.cs b/src/Avalonia.Visuals/Media/PenLineCap.cs index 56c5c040eb..83d8f11613 100644 --- a/src/Avalonia.Visuals/Media/PenLineCap.cs +++ b/src/Avalonia.Visuals/Media/PenLineCap.cs @@ -4,7 +4,6 @@ namespace Avalonia.Media { Flat, Round, - Square, - Triangle + Square } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index e69d155305..ef37f69f45 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -573,25 +573,17 @@ namespace Avalonia.Skia // Need to modify dashes due to Skia modifying their lengths // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones. - float dashLengthModifier; - float gapLengthModifier; - switch (pen.StartLineCap) + switch (pen.LineCap) { case PenLineCap.Round: paint.StrokeCap = SKStrokeCap.Round; - dashLengthModifier = -paint.StrokeWidth; - gapLengthModifier = paint.StrokeWidth; break; case PenLineCap.Square: paint.StrokeCap = SKStrokeCap.Square; - dashLengthModifier = -paint.StrokeWidth; - gapLengthModifier = paint.StrokeWidth; break; default: paint.StrokeCap = SKStrokeCap.Butt; - dashLengthModifier = 0.0f; - gapLengthModifier = 0.0f; break; } @@ -617,13 +609,12 @@ namespace Avalonia.Skia for (var i = 0; i < srcDashes.Count; ++i) { - var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier; - - // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale - dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier; + dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth; } - var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset); + var offset = (float)(pen.DashStyle.Offset * pen.Thickness); + + var pe = SKPathEffect.CreateDash(dashesArray, offset); paint.PathEffect = pe; rv.AddDisposable(pe); diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 5209014271..6b0d30f250 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -122,14 +122,16 @@ namespace Avalonia.Direct2D1 /// The Direct2D brush. public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory) { + var d2dLineCap = pen.LineCap.ToDirect2D(); + var properties = new StrokeStyleProperties { DashStyle = DashStyle.Solid, MiterLimit = (float)pen.MiterLimit, LineJoin = pen.LineJoin.ToDirect2D(), - StartCap = pen.StartLineCap.ToDirect2D(), - EndCap = pen.EndLineCap.ToDirect2D(), - DashCap = pen.DashCap.ToDirect2D() + StartCap = d2dLineCap, + EndCap = d2dLineCap, + DashCap = d2dLineCap }; float[] dashes = null; if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs index 4703daca25..2fa93b49e2 100644 --- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs @@ -334,11 +334,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes CompareImages(); } -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else [Fact] -#endif public async Task Path_With_PenLineCap() { Decorator target = new Decorator @@ -351,10 +347,8 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes StrokeThickness = 10, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, - StrokeDashCap = PenLineCap.Triangle, StrokeDashArray = new AvaloniaList(3, 1), - StrokeStartLineCap = PenLineCap.Round, - StrokeEndLineCap = PenLineCap.Square, + StrokeLineCap = PenLineCap.Round, Data = StreamGeometry.Parse("M 20,20 L 180,180"), } }; diff --git a/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs b/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs index 3b586d55ea..8afaeb8838 100644 --- a/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs @@ -61,8 +61,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes Points = polylinePoints, Stretch = Stretch.Uniform, StrokeJoin = PenLineJoin.Round, - StrokeStartLineCap = PenLineCap.Round, - StrokeEndLineCap = PenLineCap.Round, + StrokeLineCap = PenLineCap.Round, StrokeThickness = 10 } }; diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Path_With_PenLineCap.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Path_With_PenLineCap.expected.png index d33068d62c9deb097896fc29422ed4f765f2daf4..a61ba9f080449b976575ff2faaf7d793aac7e5f9 100644 GIT binary patch literal 1522 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZkrl}2EbxddW?Klsp8uxup*tayqEg>Z;J%+FI`*b<}H{$3&;5)~420g%%DmO{F7F9V!V& zIJ#e0p62U0yuJ8YT<`3=pKU&DI(Oyw<9j<_*H|(EU4RZw`yH|CjlCWF@B2!LE#Jd( ztiN49^=U7+VTSg4nZ*{Rz>ESIGr!JbM&6(T5 z!ll&MtUlV6t`f1Dt9DMh z(1yobFwX)#W}XE0*ouXJ>^Qd=s0*o|x_s)2$EV*PE}WX;aqh^1xxPv}rBB!AJ)C*t z=8Z=R))@=CRL(#CmH2dT^u`|6s1xoxe{RXLY}!*4(tb5H=7N*msk6&Ne>U3K7eC3p z`f6&j$;1mmbrj$w1w(XnUv8lm!&~lPd2T z;H$Dl5m%(ZTn~&jVEokZM9mOBS*Ej}|9ovx$#?bN@4a;Vy)CzX&NAjdUe{l{@iUjy zj4IWiHf(EsI#aaS-sVW1wB9&nd-^%XWmB~#NpCFP_bn-q@=*IA^7_Fq!)e>2fL`wUQ@_{d?xNm3 zNtuG?aQ}FA zs(Po*;@5vAwoK?R+TR07cH2WMQzegX+_Q(t(Yvx$!gZt7#0!^}-q7f{2uy^Pr%j>$ z6R(3rm*0_P^EBpLUzlhyd%EvmnbYw<#LMDx&qW^CV)Q&r`*~Tn86MBTyx6*P;+$Sb z?qa2#qQFcf_RM9{{j?UdWo|`kpvZc%^2WXob4$K?*SXv7Up?=BWW4(MKYw+;1-+Si zeZ)rENK0+T*U8~(KKMj zir!n)15A8Xwq{&Oz+`#uCp0zAEMkrHQQ0!FzZj?&lF6Pdz7xoD)x}Br=V?1o?>eZ3&F>ybmVM^c+PM3FOpd!-K)z4*}Q$iB} Dt%9q! literal 1155 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZkrl|*^>lFzsfc@fcVS*?s6^XCKHq>2FBQJ-H3}`=2Odm_kdbq4YOq#d;WQ6u zeaIKUa*%^XsIi&l+Wq2LCU3L$Z#AF0F(MRJMfd&p*>N!&ha? z=hHWI+IFi6Mg7dn5j#{X%$a+|edo*RUv9b_T5M$-7r*uXu{ina_n)rr;(DF)=Fuln zo8nTXGk#qr>YTY9EKCy}6ofcjm>NA41UOlg8XZ&w5P}EqdDw6?nSFElbXa1`MpL22 z3+JYJusC_Ip2*R%Elf#}W9?E8mP7U59##C^zpB6JzU`fRpNihhKKxd>bhT%4r8rp;nX#c1#@*LIshHz5UoAYAzM@Ef!yYdbzy~$*_#d4`KxR(>wfeM zX!mA?G+|ENvq?L$l@`v82*=qvRM@vPM5M&m=>;`3Jffudm8s7>Gy@t zTlY)jdoM6-^85Ez?%4bE`Ns47+b>+cp1gVT)2vb^$7sz7vph1gRY7E?DTqw>Rxq6i z^q@nQ(!yT<<7t9RI$O+MowWnGdW#~?Sc5pRT6yJuTOQpvolE}x<-`?vw^$$D+;b;H z{^>R2`t&;a)#?Av{jvP^_n}zTs^uro`%PPYm6IiRDaih{Q$gg?s~~c&E+_)EqnUvb zX!dG<^K5XGtuX(s(_}L7LL0V}gcNLVdY^O~W+eKn?C*1wj#d`rc$?D+47M#&oen_X z1H&$=>&Zptk2!I2(X~^e`I~2~OlPdG*fF=Na*vaDe(zbLT=k75sCt8kUw?Q-*FOns zY3uzpTi5N|x9>et^0&{sNXV%+9xCf)*MGQEWAyFnL$MvFr%gW6AF*YNP^U%4+kGBK v7!T_R6&^6Xt}Mvz6C&}5fuVv&UHw1P$(b*HrVM%xNb!1@J z*w6hZkrl}2EbxddW?Klsp8uxup*tayqEg>Z;J%+FI`*b<}H{$3&;5)~420g%%DmO{F7F9V!V& zIJ#e0p62U0yuJ8YT<`3=pKU&DI(Oyw<9j<_*H|(EU4RZw`yH|CjlCWF@B2!LE#Jd( ztiN49^=U7+VTSg4nZ*{Rz>ESIGr!JbM&6(T5 z!ll&MtUlV6t`f1Dt9DMh z(1yobFwX)#W}XE0*ouXJ>^Qd=s0*o|x_s)2$EV*PE}WX;aqh^1xxPv}rBB!AJ)C*t z=8Z=R))@=CRL(#CmH2dT^u`|6s1xoxe{RXLY}!*4(tb5H=7N*msk6&Ne>U3K7eC3p z`f6&j$;1mmbrj$w1w(XnUv8lm!&~lPd2T z;H$Dl5m%(ZTn~&jVEokZM9mOBS*Ej}|9ovx$#?bN@4a;Vy)CzX&NAjdUe{l{@iUjy zj4IWiHf(EsI#aaS-sVW1wB9&nd-^%XWmB~#NpCFP_bn-q@=*IA^7_Fq!)e>2fL`wUQ@_{d?xNm3 zNtuG?aQ}FA zs(Po*;@5vAwoK?R+TR07cH2WMQzegX+_Q(t(Yvx$!gZt7#0!^}-q7f{2uy^Pr%j>$ z6R(3rm*0_P^EBpLUzlhyd%EvmnbYw<#LMDx&qW^CV)Q&r`*~Tn86MBTyx6*P;+$Sb z?qa2#qQFcf_RM9{{j?UdWo|`kpvZc%^2WXob4$K?*SXv7Up?=BWW4(MKYw+;1-+Si zeZ)rENK0+T*U8~(KKMj zir!n)15A8Xwq{&Oz+`#uCp0zAEMkrHQQ0!FzZj?&lF6Pdz7xoD)x}Br=V?1o?>eZ3&F>ybmVM^c+PM3FOpd!-K)z4*}Q$iB} Dt%9q! literal 1155 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZkrl|*^>lFzsfc@fcVS*?s6^XCKHq>2FBQJ-H3}`=2Odm_kdbq4YOq#d;WQ6u zeaIKUa*%^XsIi&l+Wq2LCU3L$Z#AF0F(MRJMfd&p*>N!&ha? z=hHWI+IFi6Mg7dn5j#{X%$a+|edo*RUv9b_T5M$-7r*uXu{ina_n)rr;(DF)=Fuln zo8nTXGk#qr>YTY9EKCy}6ofcjm>NA41UOlg8XZ&w5P}EqdDw6?nSFElbXa1`MpL22 z3+JYJusC_Ip2*R%Elf#}W9?E8mP7U59##C^zpB6JzU`fRpNihhKKxd>bhT%4r8rp;nX#c1#@*LIshHz5UoAYAzM@Ef!yYdbzy~$*_#d4`KxR(>wfeM zX!mA?G+|ENvq?L$l@`v82*=qvRM@vPM5M&m=>;`3Jffudm8s7>Gy@t zTlY)jdoM6-^85Ez?%4bE`Ns47+b>+cp1gVT)2vb^$7sz7vph1gRY7E?DTqw>Rxq6i z^q@nQ(!yT<<7t9RI$O+MowWnGdW#~?Sc5pRT6yJuTOQpvolE}x<-`?vw^$$D+;b;H z{^>R2`t&;a)#?Av{jvP^_n}zTs^uro`%PPYm6IiRDaih{Q$gg?s~~c&E+_)EqnUvb zX!dG<^K5XGtuX(s(_}L7LL0V}gcNLVdY^O~W+eKn?C*1wj#d`rc$?D+47M#&oen_X z1H&$=>&Zptk2!I2(X~^e`I~2~OlPdG*fF=Na*vaDe(zbLT=k75sCt8kUw?Q-*FOns zY3uzpTi5N|x9>et^0&{sNXV%+9xCf)*MGQEWAyFnL$MvFr%gW6AF*YNP^U%4+kGBK v7!T_R6&^6Xt}Mvz6C&}5fuVv&UHw1P$(b*Hr