diff --git a/src/Perspex.SceneGraph/Media/ArcSegment.cs b/src/Perspex.SceneGraph/Media/ArcSegment.cs
new file mode 100644
index 0000000000..35a1bb87c1
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/ArcSegment.cs
@@ -0,0 +1,103 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Perspex.Media
+{
+ public sealed class ArcSegment : PathSegment
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsLargeArcProperty
+ = PerspexProperty.Register(nameof(IsLargeArc), false);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PointProperty
+ = PerspexProperty.Register(nameof(Point));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty RotationAngleProperty
+ = PerspexProperty.Register(nameof(RotationAngle), 0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty SizeProperty
+ = PerspexProperty.Register(nameof(Size));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty SweepDirectionProperty
+ = PerspexProperty.Register(nameof(SweepDirection), SweepDirection.Clockwise);
+
+ ///
+ /// Gets or sets a value indicating whether this instance is large arc.
+ ///
+ ///
+ /// true if this instance is large arc; otherwise, false.
+ ///
+ public bool IsLargeArc
+ {
+ get { return GetValue(IsLargeArcProperty); }
+ set { SetValue(IsLargeArcProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the point.
+ ///
+ ///
+ /// The point.
+ ///
+ public Point Point
+ {
+ get { return GetValue(PointProperty); }
+ set { SetValue(PointProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the rotation angle.
+ ///
+ ///
+ /// The rotation angle.
+ ///
+ public double RotationAngle
+ {
+ get { return GetValue(RotationAngleProperty); }
+ set { SetValue(RotationAngleProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the size.
+ ///
+ ///
+ /// The size.
+ ///
+ public Size Size
+ {
+ get { return GetValue(SizeProperty); }
+ set { SetValue(SizeProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the sweep direction.
+ ///
+ ///
+ /// The sweep direction.
+ ///
+ public SweepDirection SweepDirection
+ {
+ get { return GetValue(SweepDirectionProperty); }
+ set { SetValue(SweepDirectionProperty, value); }
+ }
+
+ protected internal override void ApplyTo(StreamGeometryContext ctx)
+ {
+ ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Media/BezierSegment .cs b/src/Perspex.SceneGraph/Media/BezierSegment .cs
new file mode 100644
index 0000000000..7a7ad9827a
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/BezierSegment .cs
@@ -0,0 +1,65 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Perspex.Media
+{
+ public sealed class BezierSegment : PathSegment
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty Point1Property
+ = PerspexProperty.Register(nameof(Point1));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty Point2Property
+ = PerspexProperty.Register(nameof(Point2));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty Point3Property
+ = PerspexProperty.Register(nameof(Point3));
+
+ ///
+ /// Gets or sets the point1.
+ ///
+ ///
+ /// The point1.
+ ///
+ public Point Point1
+ {
+ get { return GetValue(Point1Property); }
+ set { SetValue(Point1Property, value); }
+ }
+
+ ///
+ /// Gets or sets the point2.
+ ///
+ ///
+ /// The point2.
+ ///
+ public Point Point2
+ {
+ get { return GetValue(Point2Property); }
+ set { SetValue(Point2Property, value); }
+ }
+
+ ///
+ /// Gets or sets the point3.
+ ///
+ ///
+ /// The point3.
+ ///
+ public Point Point3
+ {
+ get { return GetValue(Point3Property); }
+ set { SetValue(Point3Property, value); }
+ }
+
+ protected internal override void ApplyTo(StreamGeometryContext ctx)
+ {
+ ctx.CubicBezierTo(Point1, Point2, Point3);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Media/Geometry.cs b/src/Perspex.SceneGraph/Media/Geometry.cs
index 806d5321a5..7e48169efe 100644
--- a/src/Perspex.SceneGraph/Media/Geometry.cs
+++ b/src/Perspex.SceneGraph/Media/Geometry.cs
@@ -36,7 +36,7 @@ namespace Perspex.Media
///
/// Gets the platform-specific implementation of the geometry.
///
- public IGeometryImpl PlatformImpl
+ public virtual IGeometryImpl PlatformImpl
{
get;
protected set;
diff --git a/src/Perspex.SceneGraph/Media/LineSegment.cs b/src/Perspex.SceneGraph/Media/LineSegment.cs
new file mode 100644
index 0000000000..716697b02c
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/LineSegment.cs
@@ -0,0 +1,31 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Perspex.Media
+{
+ public sealed class LineSegment : PathSegment
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PointProperty
+ = PerspexProperty.Register(nameof(Point));
+
+ ///
+ /// Gets or sets the point.
+ ///
+ ///
+ /// The point.
+ ///
+ public Point Point
+ {
+ get { return GetValue(PointProperty); }
+ set { SetValue(PointProperty, value); }
+ }
+
+ protected internal override void ApplyTo(StreamGeometryContext ctx)
+ {
+ ctx.LineTo(Point);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Media/PathFigure.cs b/src/Perspex.SceneGraph/Media/PathFigure.cs
new file mode 100644
index 0000000000..8ce54d4bd8
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/PathFigure.cs
@@ -0,0 +1,102 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Perspex.Metadata;
+
+namespace Perspex.Media
+{
+ public sealed class PathFigure : PerspexObject
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsClosedProperty
+ = PerspexProperty.Register(nameof(IsClosed), true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsFilledProperty
+ = PerspexProperty.Register(nameof(IsFilled), true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty SegmentsProperty
+ = PerspexProperty.RegisterDirect(nameof(Segments), f => f.Segments, (f, s) => f.Segments = s);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty StartPointProperty
+ = PerspexProperty.Register(nameof(StartPoint));
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PathFigure()
+ {
+ Segments = new PathSegments();
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this instance is closed.
+ ///
+ ///
+ /// true if this instance is closed; otherwise, false.
+ ///
+ public bool IsClosed
+ {
+ get { return GetValue(IsClosedProperty); }
+ set { SetValue(IsClosedProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this instance is filled.
+ ///
+ ///
+ /// true if this instance is filled; otherwise, false.
+ ///
+ public bool IsFilled
+ {
+ get { return GetValue(IsFilledProperty); }
+ set { SetValue(IsFilledProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the segments.
+ ///
+ ///
+ /// The segments.
+ ///
+ [Content]
+ public PathSegments Segments
+ {
+ get { return _segments; }
+ set { SetAndRaise(SegmentsProperty, ref _segments, value); }
+ }
+
+ ///
+ /// Gets or sets the start point.
+ ///
+ ///
+ /// The start point.
+ ///
+ public Point StartPoint
+ {
+ get { return GetValue(StartPointProperty); }
+ set { SetValue(StartPointProperty, value); }
+ }
+
+ internal void ApplyTo(StreamGeometryContext ctx)
+ {
+ ctx.BeginFigure(StartPoint, IsFilled);
+
+ foreach (var segment in Segments)
+ {
+ segment.ApplyTo(ctx);
+ }
+
+ ctx.EndFigure(IsClosed);
+ }
+
+ private PathSegments _segments;
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Media/PathGeometry.cs b/src/Perspex.SceneGraph/Media/PathGeometry.cs
new file mode 100644
index 0000000000..245885d4b0
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/PathGeometry.cs
@@ -0,0 +1,123 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Perspex.Collections;
+using Perspex.Metadata;
+using Perspex.Platform;
+using System;
+
+namespace Perspex.Media
+{
+ public class PathGeometry : StreamGeometry
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty FiguresProperty =
+ PerspexProperty.RegisterDirect(nameof(Figures), g => g.Figures, (g, f) => g.Figures = f);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty FillRuleProperty =
+ PerspexProperty.Register(nameof(FillRule));
+
+ static PathGeometry()
+ {
+ FiguresProperty.Changed.Subscribe(onNext: v =>
+ {
+ (v.Sender as PathGeometry)?.OnFiguresChanged(v.OldValue as PathFigures, v.NewValue as PathFigures);
+ });
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PathGeometry()
+ {
+ Figures = new PathFigures();
+ }
+
+ ///
+ /// Gets or sets the figures.
+ ///
+ ///
+ /// The figures.
+ ///
+ [Content]
+ public PathFigures Figures
+ {
+ get { return _figures; }
+ set { SetAndRaise(FiguresProperty, ref _figures, value); }
+ }
+
+ ///
+ /// Gets or sets the fill rule.
+ ///
+ ///
+ /// The fill rule.
+ ///
+ public FillRule FillRule
+ {
+ get { return GetValue(FillRuleProperty); }
+ set { SetValue(FillRuleProperty, value); }
+ }
+
+ public override IGeometryImpl PlatformImpl
+ {
+ get
+ {
+ PrepareIfNeeded();
+ return base.PlatformImpl;
+ }
+
+ protected set
+ {
+ base.PlatformImpl = value;
+ }
+ }
+
+ public override Geometry Clone()
+ {
+ PrepareIfNeeded();
+
+ return base.Clone();
+ }
+
+ public void PrepareIfNeeded()
+ {
+ if (_isDirty)
+ {
+ _isDirty = false;
+
+ using (var ctx = Open())
+ {
+ ctx.SetFillRule(FillRule);
+ foreach (var f in Figures)
+ {
+ f.ApplyTo(ctx);
+ }
+ }
+ }
+ }
+
+ internal void NotifyChanged()
+ {
+ _isDirty = true;
+ }
+
+ private PathFigures _figures;
+ private IDisposable _figuresObserver = null;
+ private IDisposable _figuresPropertiesObserver = null;
+ private bool _isDirty = true;
+
+ private void OnFiguresChanged(PathFigures oldValue, PathFigures newValue)
+ {
+ _figuresObserver?.Dispose();
+ _figuresPropertiesObserver?.Dispose();
+
+ _figuresObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged());
+ _figuresPropertiesObserver = newValue?.TrackItemPropertyChanged(t => NotifyChanged());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Media/PathGeometryCollections.cs b/src/Perspex.SceneGraph/Media/PathGeometryCollections.cs
new file mode 100644
index 0000000000..d93cedaede
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/PathGeometryCollections.cs
@@ -0,0 +1,15 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Perspex.Collections;
+
+namespace Perspex.Media
+{
+ public sealed class PathFigures : PerspexList
+ {
+ }
+
+ public sealed class PathSegments : PerspexList
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Media/PathSegment.cs b/src/Perspex.SceneGraph/Media/PathSegment.cs
new file mode 100644
index 0000000000..523494644b
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/PathSegment.cs
@@ -0,0 +1,10 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Perspex.Media
+{
+ public abstract class PathSegment : PerspexObject
+ {
+ protected internal abstract void ApplyTo(StreamGeometryContext ctx);
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Media/QuadraticBezierSegment .cs b/src/Perspex.SceneGraph/Media/QuadraticBezierSegment .cs
new file mode 100644
index 0000000000..f347a8ee26
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/QuadraticBezierSegment .cs
@@ -0,0 +1,46 @@
+namespace Perspex.Media
+{
+ public sealed class QuadraticBezierSegment : PathSegment
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty Point1Property
+ = PerspexProperty.Register(nameof(Point1));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty Point2Property
+ = PerspexProperty.Register(nameof(Point2));
+
+ ///
+ /// Gets or sets the point1.
+ ///
+ ///
+ /// The point1.
+ ///
+ public Point Point1
+ {
+ get { return GetValue(Point1Property); }
+ set { SetValue(Point1Property, value); }
+ }
+
+ ///
+ /// Gets or sets the point2.
+ ///
+ ///
+ /// The point2.
+ ///
+ public Point Point2
+ {
+ get { return GetValue(Point2Property); }
+ set { SetValue(Point2Property, value); }
+ }
+
+ protected internal override void ApplyTo(StreamGeometryContext ctx)
+ {
+ ctx.QuadraticBezierTo(Point1, Point2);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Media/TransformGroup.cs b/src/Perspex.SceneGraph/Media/TransformGroup.cs
new file mode 100644
index 0000000000..79694bbfc7
--- /dev/null
+++ b/src/Perspex.SceneGraph/Media/TransformGroup.cs
@@ -0,0 +1,57 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Perspex.Collections;
+using Perspex.Metadata;
+
+namespace Perspex.Media
+{
+ public class TransformGroup : Transform
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly PerspexProperty ChildrenProperty =
+ PerspexProperty.Register(nameof(Children));
+
+ public TransformGroup()
+ {
+ Children = new Transforms();
+ }
+
+ ///
+ /// Gets or sets the children.
+ ///
+ ///
+ /// The children.
+ ///
+ [Content]
+ public Transforms Children
+ {
+ get { return GetValue(ChildrenProperty); }
+ set { SetValue(ChildrenProperty, value); }
+ }
+
+ ///
+ /// Gets the tranform's .
+ ///
+ public override Matrix Value
+ {
+ get
+ {
+ Matrix result = Matrix.Identity;
+
+ foreach (var t in Children)
+ {
+ result *= t.Value;
+ }
+
+ return result;
+ }
+ }
+ }
+
+ public sealed class Transforms : PerspexList
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj
index ea84ae710e..9c2867c5e9 100644
--- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj
+++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj
@@ -61,11 +61,14 @@
+
+
+
@@ -75,9 +78,15 @@
+
+
+
+
+
+