Skip to content

Commit aa39b9f

Browse files
committed
Implement native clipping for SvgRenderContext (#1564)
1 parent 699e9dd commit aa39b9f

6 files changed

Lines changed: 181 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file.
3030
- Added title clipping to PlotModel (#1510)
3131
- Added LabelStep and LabelSpacing to contour series (#1511)
3232
- Example for Issue #1481 showing text rendering with emoji
33+
- Native Clipping for OxyPlot.SvgRenderContext (#1564)
3334

3435
### Changed
3536
- Legends model (#644)

Source/Examples/ExampleLibrary/Examples/RenderingCapabilities.cs

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ namespace ExampleLibrary
1616
using OxyPlot;
1717
using OxyPlot.Annotations;
1818
using System.Linq;
19-
using System.Collections.Generic;
2019

2120
/// <summary>
2221
/// Provides rendering capability examples.
@@ -458,6 +457,59 @@ public static PlotModel DrawTextWithMetrics(string text, string font, double fon
458457
return model;
459458
}
460459

460+
[Example("Clipping")]
461+
public static PlotModel Clipping()
462+
{
463+
var model = new PlotModel();
464+
model.Annotations.Add(new DelegateAnnotation(rc =>
465+
{
466+
// reset a couple of times without any clipping being set to see that nothing explodes
467+
rc.ResetClip();
468+
rc.ResetClip();
469+
rc.ResetClip();
470+
471+
var p1 = new ScreenPoint(150, 150);
472+
var clip = 100d;
473+
var radiusFactor = 1.1;
474+
var clipShrink = .15 * clip;
475+
476+
var rect = new OxyRect(p1.X - clip, p1.Y - clip, clip * 2, clip * 2);
477+
rc.DrawRectangle(rect, OxyColors.Undefined, OxyColors.Red, 1, EdgeRenderingMode.Automatic);
478+
rc.SetClip(rect);
479+
rc.DrawCircle(p1, clip * radiusFactor, OxyColors.Red, OxyColors.Undefined, 0, EdgeRenderingMode.Automatic);
480+
481+
clip -= clipShrink;
482+
rect = new OxyRect(p1.X - clip, p1.Y - clip, clip * 2, clip * 2);
483+
rc.DrawRectangle(rect, OxyColors.Undefined, OxyColors.Green, 1, EdgeRenderingMode.Automatic);
484+
rc.SetClip(rect);
485+
rc.DrawCircle(p1, clip * radiusFactor, OxyColors.Green, OxyColors.Undefined, 0, EdgeRenderingMode.Automatic);
486+
487+
clip -= clipShrink;
488+
rect = new OxyRect(p1.X - clip, p1.Y - clip, clip * 2, clip * 2);
489+
rc.DrawRectangle(rect, OxyColors.Undefined, OxyColors.Blue, 1, EdgeRenderingMode.Automatic);
490+
rc.SetClip(rect);
491+
rc.DrawCircle(p1, clip * radiusFactor, OxyColors.Blue, OxyColors.Undefined, 0, EdgeRenderingMode.Automatic);
492+
rc.ResetClip();
493+
494+
rc.DrawText(p1, "Clipped Circles", OxyColors.White, fontSize: 12, horizontalAlignment: HorizontalAlignment.Center, verticalAlignment: VerticalAlignment.Middle);
495+
496+
rc.DrawText(new ScreenPoint(p1.X * 2, 50), "Not clipped", OxyColors.Black, fontSize: 40);
497+
498+
rect = new OxyRect(p1.X * 2 + 10, 100, 80, 60);
499+
rc.DrawRectangle(rect, OxyColors.Undefined, OxyColors.Black, 1, EdgeRenderingMode.Automatic);
500+
501+
// set the same clipping a couple of times to see that nothing explodes
502+
rc.SetClip(rect);
503+
rc.SetClip(rect);
504+
using (rc.AutoResetClip(rect))
505+
{
506+
rc.DrawText(new ScreenPoint(p1.X * 2, 100), "Clipped", OxyColors.Black, fontSize: 40);
507+
}
508+
}));
509+
510+
return model;
511+
}
512+
461513
private const double GRID_SIZE = 40;
462514
private const double TILE_SIZE = 30;
463515
private const int THICKNESS_STEPS = 10;
@@ -466,10 +518,6 @@ public static PlotModel DrawTextWithMetrics(string text, string font, double fon
466518
private const double OFFSET_TOP = 20;
467519
private static readonly OxyColor FILL_COLOR = OxyColors.LightBlue;
468520

469-
/// <summary>
470-
/// Shows capabilities for the MeasureText method.
471-
/// </summary>
472-
/// <returns>A plot model.</returns>
473521
[Example("Rectangles - EdgeRenderingMode")]
474522
public static PlotModel Rectangles()
475523
{
@@ -500,10 +548,6 @@ public static PlotModel Rectangles()
500548
return model;
501549
}
502550

503-
/// <summary>
504-
/// Shows capabilities for the MeasureText method.
505-
/// </summary>
506-
/// <returns>A plot model.</returns>
507551
[Example("Lines - EdgeRenderingMode")]
508552
public static PlotModel Lines()
509553
{
@@ -540,10 +584,6 @@ public static PlotModel Lines()
540584
return model;
541585
}
542586

543-
/// <summary>
544-
/// Shows capabilities for the MeasureText method.
545-
/// </summary>
546-
/// <returns>A plot model.</returns>
547587
[Example("Polygons - EdgeRenderingMode")]
548588
public static PlotModel Polygons()
549589
{
@@ -582,10 +622,6 @@ public static PlotModel Polygons()
582622
return model;
583623
}
584624

585-
/// <summary>
586-
/// Shows capabilities for the MeasureText method.
587-
/// </summary>
588-
/// <returns>A plot model.</returns>
589625
[Example("Ellipses - EdgeRenderingMode")]
590626
public static PlotModel Ellipses()
591627
{
@@ -619,7 +655,7 @@ public static PlotModel Ellipses()
619655
/// <summary>
620656
/// Represents an annotation that renders by a delegate.
621657
/// </summary>
622-
private class DelegateAnnotation : Annotation
658+
public class DelegateAnnotation : Annotation
623659
{
624660
/// <summary>
625661
/// Initializes a new instance of the <see cref="DelegateAnnotation"/> class.

Source/OxyPlot.Tests/Svg/SvgExporterTests.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace OxyPlot.Tests
99
using System;
1010
using System.Diagnostics.CodeAnalysis;
1111
using System.IO;
12-
12+
using System.Linq;
1313
using ExampleLibrary;
1414
using NUnit.Framework;
1515

@@ -122,5 +122,54 @@ public void Export_BoundedTest()
122122
rc.Flush();
123123
}
124124
}
125+
126+
[Test]
127+
public void Test_Clipping()
128+
{
129+
var plotModel = new PlotModel { Title = "Clipping" };
130+
131+
var clipRect = new OxyRect(100, 100, 100, 100);
132+
var center = new ScreenPoint(150, 150);
133+
var radius = 60;
134+
135+
plotModel.Annotations.Add(new RenderingCapabilities.DelegateAnnotation(rc =>
136+
{
137+
using (rc.AutoResetClip(clipRect))
138+
{
139+
rc.DrawCircle(center, radius, OxyColors.Black, OxyColors.Undefined, 0, EdgeRenderingMode.Automatic);
140+
}
141+
}));
142+
143+
byte[] baseline;
144+
using (var stream = new MemoryStream())
145+
{
146+
SvgExporter.Export(plotModel, stream, 400, 400, false);
147+
baseline = stream.ToArray();
148+
}
149+
150+
plotModel.Annotations.Clear();
151+
plotModel.Annotations.Add(new RenderingCapabilities.DelegateAnnotation(rc =>
152+
{
153+
// reset without a clipping rect being set
154+
rc.ResetClip();
155+
rc.ResetClip();
156+
157+
// set clipping multiple times
158+
rc.SetClip(clipRect);
159+
rc.SetClip(clipRect);
160+
rc.SetClip(clipRect);
161+
rc.DrawCircle(center, radius, OxyColors.Black, OxyColors.Undefined, 0, EdgeRenderingMode.Automatic);
162+
rc.ResetClip();
163+
}));
164+
165+
byte[] setMultiple;
166+
using (var stream = new MemoryStream())
167+
{
168+
SvgExporter.Export(plotModel, stream, 400, 400, false);
169+
setMultiple = stream.ToArray();
170+
}
171+
172+
Assert.IsTrue(baseline.SequenceEqual(setMultiple));
173+
}
125174
}
126175
}

Source/OxyPlot/Rendering/RenderContext/RenderingExtensions.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,17 @@ public static OxySize MeasureText(this IRenderContext rc, string text, string fo
896896
return MeasureRotatedRectangleBound(bounds, angle);
897897
}
898898

899+
/// <summary>
900+
/// Applies the specified clipping rectangle the the render context and returns a reset token. The clipping is reset once this token is disposed.
901+
/// </summary>
902+
/// <param name="rc">The render context.</param>
903+
/// <param name="clippingRectangle">The clipping rectangle.</param>
904+
/// <returns>The reset token. Clipping is reset once this is disposed.</returns>
905+
public static IDisposable AutoResetClip(this IRenderContext rc, OxyRect clippingRectangle)
906+
{
907+
return new AutoResetClipToken(rc, clippingRectangle);
908+
}
909+
899910
/// <summary>
900911
/// Adds a marker geometry to the specified collections.
901912
/// </summary>
@@ -1125,5 +1136,24 @@ private static void ReducePoints(IList<ScreenPoint> points, double minDistSquare
11251136
}
11261137
}
11271138
}
1139+
1140+
/// <summary>
1141+
/// Represents the token that is used to automatically reset the clipping in the <see cref="AutoResetClip(IRenderContext, OxyRect)"/> method.
1142+
/// </summary>
1143+
private class AutoResetClipToken : IDisposable
1144+
{
1145+
private readonly IRenderContext renderContext;
1146+
1147+
public AutoResetClipToken(IRenderContext renderContext, OxyRect clippingRectangle)
1148+
{
1149+
this.renderContext = renderContext;
1150+
renderContext.SetClip(clippingRectangle);
1151+
}
1152+
1153+
void IDisposable.Dispose()
1154+
{
1155+
this.renderContext.ResetClip();
1156+
}
1157+
}
11281158
}
11291159
}

Source/OxyPlot/Svg/SvgRenderContext.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public class SvgRenderContext : RenderContextBase, IDisposable
2828
/// </summary>
2929
private bool disposed;
3030

31+
/// <summary>
32+
/// The current clipping rectangle.
33+
/// </summary>
34+
private OxyRect? clipRect;
35+
3136
/// <summary>
3237
/// Initializes a new instance of the <see cref="SvgRenderContext" /> class.
3338
/// </summary>
@@ -262,11 +267,43 @@ public override void DrawImage(
262267
this.w.WriteImage(srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight, source);
263268
}
264269

270+
/// <inheritdoc/>
271+
public override bool SetClip(OxyRect rect)
272+
{
273+
if (this.clipRect != null)
274+
{
275+
if (rect.Equals(this.clipRect.Value))
276+
{
277+
return true;
278+
}
279+
else
280+
{
281+
this.ResetClip();
282+
}
283+
}
284+
285+
this.clipRect = rect;
286+
this.w.BeginClip(rect.Left, rect.Top, rect.Width, rect.Height);
287+
return true;
288+
}
289+
290+
/// <inheritdoc/>
291+
public override void ResetClip()
292+
{
293+
if (this.clipRect == null)
294+
{
295+
return;
296+
}
297+
298+
this.clipRect = null;
299+
this.w.EndClip();
300+
}
301+
265302
/// <summary>
266303
/// Releases unmanaged and - optionally - managed resources
267304
/// </summary>
268305
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
269-
private void Dispose(bool disposing)
306+
protected virtual void Dispose(bool disposing)
270307
{
271308
if (!this.disposed)
272309
{

0 commit comments

Comments
 (0)