Browse Source

Merge pull request #544 from jkoritzinsky/Path-Clipping

Add support for clipping to a Geometry. Fixes #359.
pull/549/head
Jeremy Koritzinsky 10 years ago
parent
commit
01766e2710
  1. 21
      src/Avalonia.SceneGraph/Media/DrawingContext.cs
  2. 10
      src/Avalonia.SceneGraph/Media/IDrawingContext.cs
  3. 27
      src/Avalonia.SceneGraph/Media/PathMarkupParser.cs
  4. 1
      src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
  5. 15
      src/Avalonia.SceneGraph/Visual.cs
  6. 5
      src/Avalonia.SceneGraph/VisualTree/IVisual.cs
  7. 12
      src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs
  8. 6
      src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs
  9. 11
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  10. 26
      src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs
  11. 5
      tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
  12. 51
      tests/Avalonia.RenderTests/GeometryClippingTests.cs
  13. BIN
      tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png
  14. BIN
      tests/TestFiles/Direct2D1/GeometryClipping/Geometry_Clip_Clips_Path.expected.png
  15. BIN
      tests/TestFiles/Skia/GeometryClipping/Geometry_Clip_Clips_Path.expected.png

21
src/Avalonia.SceneGraph/Media/DrawingContext.cs

@ -127,7 +127,8 @@ namespace Avalonia.Media
Matrix,
Opacity,
Clip,
MatrixContainer
MatrixContainer,
GeometryClip
}
public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix))
@ -149,10 +150,12 @@ namespace Avalonia.Media
_context._states.Pop();
if (_type == PushedStateType.Matrix)
_context.CurrentTransform = _matrix;
else if(_type == PushedStateType.Clip)
else if (_type == PushedStateType.Clip)
_context._impl.PopClip();
else if(_type == PushedStateType.Opacity)
else if (_type == PushedStateType.Opacity)
_context._impl.PopOpacity();
else if (_type == PushedStateType.GeometryClip)
_context._impl.PopGeometryClip();
else if (_type == PushedStateType.MatrixContainer)
{
var cont = _context._transformContainers.Pop();
@ -174,6 +177,18 @@ namespace Avalonia.Media
return new PushedState(this, PushedState.PushedStateType.Clip);
}
/// <summary>
/// Pushes a clip geometry.
/// </summary>
/// <param name="clip">The clip geometry.</param>
/// <returns>A disposable used to undo the clip geometry.</returns>
public PushedState PushGeometryClip(Geometry clip)
{
Contract.Requires<ArgumentNullException>(clip != null);
_impl.PushGeometryClip(clip);
return new PushedState(this, PushedState.PushedStateType.GeometryClip);
}
/// <summary>
/// Pushes an opacity value.
/// </summary>

10
src/Avalonia.SceneGraph/Media/IDrawingContext.cs

@ -69,7 +69,6 @@ namespace Avalonia.Media
/// Pushes a clip rectange.
/// </summary>
/// <param name="clip">The clip rectangle.</param>
/// <returns>A disposable used to undo the clip rectangle.</returns>
void PushClip(Rect clip);
void PopClip();
@ -78,9 +77,16 @@ namespace Avalonia.Media
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <returns>A disposable used to undo the opacity.</returns>
void PushOpacity(double opacity);
void PopOpacity();
/// <summary>
/// Pushes a clip geometry.
/// </summary>
/// <param name="clip">The clip geometry.</param>
void PushGeometryClip(Geometry clip);
void PopGeometryClip();
}
}

27
src/Avalonia.SceneGraph/Media/PathMarkupParser.cs

@ -34,6 +34,12 @@ namespace Avalonia.Media
{ 'z', Command.Close },
};
private static readonly Dictionary<char, FillRule> FillRules = new Dictionary<char, FillRule>
{
{'0', FillRule.EvenOdd },
{'1', FillRule.NonZero }
};
private StreamGeometry _geometry;
private readonly StreamGeometryContext _context;
@ -90,8 +96,7 @@ namespace Avalonia.Media
switch (command)
{
case Command.FillRule:
// TODO: Implement.
reader.Read();
_context.SetFillRule(ReadFillRule(reader));
break;
case Command.Move:
@ -226,6 +231,24 @@ namespace Avalonia.Media
}
}
private static FillRule ReadFillRule(StringReader reader)
{
int i = reader.Read();
if (i == -1)
{
throw new InvalidDataException("Invalid fill rule");
}
char c = (char)i;
FillRule rule;
if (!FillRules.TryGetValue(c, out rule))
{
throw new InvalidDataException("Invalid fill rule");
}
return rule;
}
private static double ReadDouble(StringReader reader)
{
ReadWhitespace(reader);

1
src/Avalonia.SceneGraph/Rendering/RendererMixin.cs

@ -119,6 +119,7 @@ namespace Avalonia.Rendering
using (context.PushPostTransform(m))
using (context.PushOpacity(opacity))
using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState))
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState))
using (context.PushTransformContainer())
{
visual.Render(context);

15
src/Avalonia.SceneGraph/Visual.cs

@ -39,6 +39,12 @@ namespace Avalonia
public static readonly StyledProperty<bool> ClipToBoundsProperty =
AvaloniaProperty.Register<Visual, bool>(nameof(ClipToBounds));
/// <summary>
/// Defines the <see cref="Clip"/> property.
/// </summary>
public static readonly StyledProperty<Geometry> ClipProperty =
AvaloniaProperty.Register<Visual, Geometry>(nameof(Clip));
/// <summary>
/// Defines the <see cref="IsVisibleProperty"/> property.
/// </summary>
@ -127,6 +133,15 @@ namespace Avalonia
set { SetValue(ClipToBoundsProperty, value); }
}
/// <summary>
/// Gets or sets the geometry clip for this visual.
/// </summary>
public Geometry Clip
{
get { return GetValue(ClipProperty); }
set { SetValue(ClipProperty, value); }
}
/// <summary>
/// Gets a value indicating whether this scene graph node and all its parents are visible.
/// </summary>

5
src/Avalonia.SceneGraph/VisualTree/IVisual.cs

@ -41,6 +41,11 @@ namespace Avalonia.VisualTree
/// </summary>
bool ClipToBounds { get; set; }
/// <summary>
/// Gets or sets the geometry clip for this visual.
/// </summary>
Geometry Clip { get; set; }
/// <summary>
/// Gets a value indicating whether this scene graph node is attached to a visual root.
/// </summary>

12
src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs

@ -340,5 +340,17 @@ namespace Avalonia.Cairo.Media
return SetBrush(pen.Brush, destinationSize);
}
public void PushGeometryClip(Geometry clip)
{
_context.Save();
_context.AppendPath(((StreamGeometryImpl)clip.PlatformImpl).Path);
_context.Clip();
}
public void PopGeometryClip()
{
_context.Restore();
}
}
}

6
src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs

@ -64,7 +64,11 @@ namespace Avalonia.Cairo.Media
internal bool FillContains(Point point)
{
return _context.InFill(point.X, point.Y);
using (var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, 0, 0)))
{
context.AppendPath(Path);
return context.InFill(point.X, point.Y);
}
}
public void LineTo(Point point)

11
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -288,6 +288,17 @@ namespace Avalonia.Skia
{
}
public void PushGeometryClip(Geometry clip)
{
Canvas.Save();
Canvas.ClipPath(((StreamGeometryImpl)clip.PlatformImpl).EffectivePath);
}
public void PopGeometryClip()
{
Canvas.Restore();
}
private Matrix _currentTransform = Matrix.Identity;
public Matrix Transform

26
src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs

@ -276,6 +276,11 @@ namespace Avalonia.Direct2D1.Media
}
public void PopOpacity()
{
PopLayer();
}
private void PopLayer()
{
var layer = _layers.Pop();
if (layer != null)
@ -324,5 +329,26 @@ namespace Avalonia.Direct2D1.Media
return new SolidColorBrushImpl((Avalonia.Media.SolidColorBrush)null, _renderTarget);
}
}
public void PushGeometryClip(Avalonia.Media.Geometry clip)
{
var parameters = new LayerParameters
{
ContentBounds = PrimitiveExtensions.RectangleInfinite,
MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
Opacity = 1,
GeometricMask = ((GeometryImpl)clip.PlatformImpl).Geometry
};
var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_renderTarget);
_renderTarget.PushLayer(ref parameters, layer);
_layers.Push(layer);
}
public void PopGeometryClip()
{
PopLayer();
}
}
}

5
tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems

@ -8,7 +8,7 @@
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Avalonia.RenderTests</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<ItemGroup>
<Compile Include="Controls\ImageTests.cs" />
<Compile Include="Controls\BorderTests.cs" />
<Compile Include="Media\ImageBrushTests.cs" />
@ -22,5 +22,6 @@
<Compile Include="Shapes\RectangleTests.cs" />
<Compile Include="TestBase.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="GeometryClippingTests.cs" />
</ItemGroup>
</Project>
</Project>

51
tests/Avalonia.RenderTests/GeometryClippingTests.cs

@ -0,0 +1,51 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
#if AVALONIA_CAIRO
namespace Avalonia.Cairo.RenderTests
#elif AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
namespace Avalonia.Direct2D1.RenderTests
#endif
{
public class GeometryClippingTests : TestBase
{
public GeometryClippingTests()
:base("GeometryClipping")
{
}
[Fact]
public void Geometry_Clip_Clips_Path()
{
var target = new Canvas
{
Clip = StreamGeometry.Parse("F1 M 0,0 H 76 V 76 Z"),
Width = 76,
Height = 76,
Children = new Avalonia.Controls.Controls
{
new Path
{
Width = 32,
Height = 40,
[Canvas.LeftProperty] = 23,
[Canvas.TopProperty] = 18,
Stretch = Stretch.Fill,
Fill = Brushes.Black,
Data = StreamGeometry.Parse("F1 M 27,18L 23,26L 33,30L 24,38L 33,46L 23,50L 27,58L 45,58L 55,38L 45,18L 27,18 Z")
}
}
};
RenderToFile(target);
CompareImages();
}
}
}

BIN
tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

BIN
tests/TestFiles/Direct2D1/GeometryClipping/Geometry_Clip_Clips_Path.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

BIN
tests/TestFiles/Skia/GeometryClipping/Geometry_Clip_Clips_Path.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Loading…
Cancel
Save