// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. // // Purpose: ChartGraphics3D class is 3D chart rendering engine. // All chart 3D shapes are drawn in specific order so // that correct Z order of all shapes is achieved. 3D // graphics engine do not support shapes intersection. // 3D shapes are transformed into one or more 2D shapes // and then drawn with 2D chart graphics engine. // using System; using System.Collections; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Drawing2D; namespace FastReport.DataVisualization.Charting { #region 3D enumerations /// /// 3D cube surfaces names. /// [Flags] internal enum SurfaceNames { /// /// Front. /// Front = 1, /// /// Back. /// Back = 2, /// /// Left. /// Left = 4, /// /// Right. /// Right = 8, /// /// Top. /// Top = 16, /// /// Bottom. /// Bottom = 32 } /// /// This enumeration defines all significant points in a pie /// slice. Only these points should be transformed for pie /// chart using Matrix object. /// internal enum PiePoints { /// /// Angle 180 Top point on the arc /// Top180, /// /// Angle 180 Bottom point on the arc /// Bottom180, /// /// Angle 0 Top point on the arc /// Top0, /// /// Angle 0 Bottom point on the arc /// Bottom0, /// /// Top Start Angle point on the arc /// TopStart, /// /// Top End Angle point on the arc /// TopEnd, /// /// Bottom Start Angle point on the arc /// BottomStart, /// /// Bottom End Angle point on the arc /// BottomEnd, /// /// Center Top /// TopCenter, /// /// Center Bottom /// BottomCenter, /// /// Top Label Line /// TopLabelLine, /// /// Top Label Line Out /// TopLabelLineout, /// /// Top Label Center /// TopLabelCenter, /// /// Top Rectangle Top Left Point /// TopRectTopLeftPoint, /// /// Top Rectangle Right Bottom Point /// TopRectBottomRightPoint, /// /// Bottom Rectangle Top Left Point /// BottomRectTopLeftPoint, /// /// Bottom Rectangle Right Bottom Point /// BottomRectBottomRightPoint, /// /// Angle 180 Top point on the Doughnut arc /// DoughnutTop180, /// /// Angle 180 Bottom point on the Doughnut arc /// DoughnutBottom180, /// /// Angle 0 Top point on the Doughnut arc /// DoughnutTop0, /// /// Angle 0 Bottom point on the Doughnut arc /// DoughnutBottom0, /// /// Top Start Angle point on the Doughnut arc /// DoughnutTopStart, /// /// Top End Angle point on the Doughnut arc /// DoughnutTopEnd, /// /// Bottom Start Angle point on the Doughnut arc /// DoughnutBottomStart, /// /// Bottom End Angle point on the Doughnut arc /// DoughnutBottomEnd, /// /// Doughnut Top Rectangle Top Left Point /// DoughnutTopRectTopLeftPoint, /// /// Doughnut Top Rectangle Right Bottom Point /// DoughnutTopRectBottomRightPoint, /// /// Doughnut Bottom Rectangle Top Left Point /// DoughnutBottomRectTopLeftPoint, /// /// Doughnut Bottom Rectangle Right Bottom Point /// DoughnutBottomRectBottomRightPoint, } /// /// AxisName of drawing operation. /// [Flags] internal enum DrawingOperationTypes { /// /// Draw element. /// DrawElement = 1, /// /// Calculate element path. (for selection or tooltips) /// CalcElementPath = 2, } /// /// AxisName of line segment. /// internal enum LineSegmentType { /// /// Only one segment exists. /// Single, /// /// First segment. /// First, /// /// Middle segment. /// Middle, /// /// Last segment. /// Last } #endregion /// /// The ChartGraphics class is 3D chart rendering engine. All chart /// 3D shapes are drawn in specific order so that correct Z order /// of all shapes is achieved. 3D graphics engine do not support /// shapes intersection. 3D shapes are transformed into one or /// more 2D shapes and then drawn with 2D chart graphics engine. /// public partial class ChartGraphics { #region Fields /// /// Helper field used to store the index of cylinder left/bottom side coordinate. /// private int _oppLeftBottomPoint = -1; /// /// Helper field used to store the index of cylinder right/top side coordinate. /// private int _oppRigthTopPoint = -1; /// /// Point of the front line from the previous line segment. /// internal PointF frontLinePoint1 = PointF.Empty; /// /// Point of the front line from the previous line segment. /// internal PointF frontLinePoint2 = PointF.Empty; /// /// Previous line segment pen. /// internal Pen frontLinePen = null; #endregion #region 3D Line drawing methods /// /// Draws grid line in 3D space (on two area scene walls) /// /// Chart area. /// Line color. /// Line width. /// Line style. /// First line point. /// Second line point. /// Indicates that grid line is horizontal /// Common Elements /// Selected object internal void Draw3DGridLine( ChartArea area, Color color, int width, ChartDashStyle style, PointF point1, PointF point2, bool horizontal, CommonElements common, object obj ) { float zPositon = area.IsMainSceneWallOnFront() ? area.areaSceneDepth : 0f; ChartElementType chartElementType = obj is StripLine ? ChartElementType.StripLines : ChartElementType.Gridlines; // Draw strip line on the back/front wall ((ChartGraphics)this).Draw3DLine( area.matrix3D, color, width, style, new Point3D(point1.X, point1.Y, zPositon), new Point3D(point2.X, point2.Y, zPositon), common, obj, chartElementType ); if(horizontal) { // Draw strip line on the side wall (left or right) if(area.IsSideSceneWallOnLeft()) { point1.X = Math.Min(point1.X, point2.X); } else { point1.X = Math.Max(point1.X, point2.X); } ((ChartGraphics)this).Draw3DLine( area.matrix3D, color, width, style, new Point3D(point1.X, point1.Y, 0f), new Point3D(point1.X, point1.Y, area.areaSceneDepth), common, obj, chartElementType ); } else if(area.IsBottomSceneWallVisible()) { // Draw strip line on the bottom wall (if visible) point1.Y = Math.Max(point1.Y, point2.Y); ((ChartGraphics)this).Draw3DLine( area.matrix3D, color, width, style, new Point3D(point1.X, point1.Y, 0f), new Point3D(point1.X, point1.Y, area.areaSceneDepth), common, obj, chartElementType ); } } /// /// Draws a line connecting the two specified points. /// /// Coordinates transformation matrix. /// Line color. /// Line width. /// Line style. /// A Point that represents the first point to connect. /// A Point that represents the second point to connect. /// Common elements /// Selected object /// Selected chart element internal void Draw3DLine( Matrix3D matrix, Color color, int width, ChartDashStyle style, Point3D firstPoint, Point3D secondPoint, CommonElements common, object obj, ChartElementType type ) { // Transform coordinates Point3D [] points = new Point3D[] {firstPoint, secondPoint}; matrix.TransformPoints( points ); // Selection mode if (common.ProcessModeRegions && type != ChartElementType.Nothing) { using (GraphicsPath path = new GraphicsPath()) { if (Math.Abs(points[0].X - points[1].X) > Math.Abs(points[0].Y - points[1].Y)) { path.AddLine(points[0].X, points[0].Y - 1, points[1].X, points[1].Y - 1); path.AddLine(points[1].X, points[1].Y + 1, points[0].X, points[0].Y + 1); path.CloseAllFigures(); } else { path.AddLine(points[0].X - 1, points[0].Y, points[1].X - 1, points[1].Y); path.AddLine(points[1].X + 1, points[1].Y, points[0].X + 1, points[0].Y); path.CloseAllFigures(); } common.HotRegionsList.AddHotRegion(path, true, type, obj); } } if( common.ProcessModePaint ) { // Draw 2D line in 3D space ((ChartGraphics)this).DrawLineRel(color, width, style, points[0].PointF, points[1].PointF); } } #endregion #region 3D Pie Drawing methods and enumerations /// /// This method draw and fill four point polygons which /// represents sides of a pie slice. /// /// Chart Area /// X angle rotation /// Start Angle of a pie slice /// Sweep angle of a pie slice /// Significant points of a pie slice /// Brush used for fill /// Pen used for drawing /// Chart AxisName is Doughnut internal void FillPieSides( ChartArea area, float inclination, float startAngle, float sweepAngle, PointF [] points, SolidBrush brush, Pen pen, bool doughnut ) { // Create a graphics path GraphicsPath path = new GraphicsPath(); // Significant Points for Side polygons PointF topCenter = points[(int)PiePoints.TopCenter]; PointF bottomCenter = points[(int)PiePoints.BottomCenter]; PointF topStart = points[(int)PiePoints.TopStart]; PointF bottomStart = points[(int)PiePoints.BottomStart]; PointF topEnd = points[(int)PiePoints.TopEnd]; PointF bottomEnd = points[(int)PiePoints.BottomEnd]; // For Doughnut PointF topDoughnutStart = PointF.Empty; PointF bottomDoughnutStart = PointF.Empty; PointF topDoughnutEnd = PointF.Empty; PointF bottomDoughnutEnd = PointF.Empty; if( doughnut ) { // For Doughnut topDoughnutStart = points[(int)PiePoints.DoughnutTopStart]; bottomDoughnutStart = points[(int)PiePoints.DoughnutBottomStart]; topDoughnutEnd = points[(int)PiePoints.DoughnutTopEnd]; bottomDoughnutEnd = points[(int)PiePoints.DoughnutBottomEnd]; } bool startSide = false; bool endSide = false; float endAngle = startAngle + sweepAngle; // If X angle is negative different side of pie slice is visible. if (inclination > 0) { // Enable start or/and the end side if( startAngle > -90 && startAngle < 90 || startAngle > 270 && startAngle < 450 ) { startSide = true; } if( endAngle >= -180 && endAngle < -90 || endAngle > 90 && endAngle < 270 || endAngle > 450 && endAngle <= 540 ) { endSide = true; } } else { // Enable start or/and the end side if( startAngle >= -180 && startAngle < -90 || startAngle > 90 && startAngle < 270 || startAngle > 450 && startAngle <= 540 ) { startSide = true; } if( endAngle > -90 && endAngle < 90 || endAngle > 270 && endAngle < 450 ) { endSide = true; } } if( startSide ) { // ***************************************** // Draw Start Angle side // ***************************************** using (path = new GraphicsPath()) { if (doughnut) { // Add Line between The Doughnut Arc and Arc path.AddLine(topDoughnutStart, topStart); // Add Line between The Top Start and Bottom Start path.AddLine(topStart, bottomStart); // Add Line between The Bottom Start and Doughnut Start path.AddLine(bottomStart, bottomDoughnutStart); // Add Line between The Bottom Doughnut Start and The Top Doughnut Start path.AddLine(bottomDoughnutStart, topDoughnutStart); } else { // Add Line between The Center and Arc path.AddLine(topCenter, topStart); // Add Line between The Top Start and Bottom Start path.AddLine(topStart, bottomStart); // Add Line between The Bottom Start and The Center Bottom path.AddLine(bottomStart, bottomCenter); // Add Line between The Bottom Center and The Top Center path.AddLine(bottomCenter, topCenter); } // Get surface colors Color frontLightColor, leftLightColor, topLightColor, backLightColor, rightLightColor, bottomLightColor; area.matrix3D.GetLight(brush.Color, out frontLightColor, out backLightColor, out leftLightColor, out rightLightColor, out topLightColor, out bottomLightColor); Color lightColor; if (area.Area3DStyle.Inclination < 0) { lightColor = bottomLightColor; } else { lightColor = topLightColor; } // Draw Path using (Brush lightBrush = new SolidBrush(lightColor)) { FillPath(lightBrush, path); } DrawGraphicsPath(pen, path); } } if( endSide ) { // ***************************************** // Draw End Angle side // ***************************************** using (path = new GraphicsPath()) { if (doughnut) { // Add Line between The Doughnut Arc and Arc path.AddLine(topDoughnutEnd, topEnd); // Add Line between The Top End and Bottom End path.AddLine(topEnd, bottomEnd); // Add Line between The Bottom End and Doughnut End path.AddLine(bottomEnd, bottomDoughnutEnd); // Add Line between The Bottom Doughnut End and The Top Doughnut End path.AddLine(bottomDoughnutEnd, topDoughnutEnd); } else { // Add Line between The Center and Arc path.AddLine(topCenter, topEnd); // Add Line between The Top End and Bottom End path.AddLine(topEnd, bottomEnd); // Add Line between The Bottom End and The Center Bottom path.AddLine(bottomEnd, bottomCenter); // Add Line between The Bottom Center and The Top Center path.AddLine(bottomCenter, topCenter); } // Get surface colors Color frontLightColor, leftLightColor, topLightColor, backLightColor, rightLightColor, bottomLightColor; area.matrix3D.GetLight(brush.Color, out frontLightColor, out backLightColor, out leftLightColor, out rightLightColor, out topLightColor, out bottomLightColor); Color lightColor; if (area.Area3DStyle.Inclination < 0) { lightColor = bottomLightColor; } else { lightColor = topLightColor; } // Draw Path using (Brush lightBrush = new SolidBrush(lightColor)) { FillPath(lightBrush, path); } DrawGraphicsPath(pen, path); } } } /// /// This method Draw and fill pie curves. /// /// Chart area used for drawing /// Data Point /// Graphic Brush used for drawing /// Graphic Pen used for drawing /// Rotated bounded rectangle points /// Rotated bounded rectangle points /// Rotated bounded rectangle points /// Rotated bounded rectangle points /// Significant pie points /// Significant pie points /// Significant pie points /// Significant pie points /// Start pie angle /// End pie angle /// Data Point Index internal void FillPieCurve( ChartArea area, DataPoint point, Brush brush, Pen pen, PointF topFirstRectPoint, PointF topSecondRectPoint, PointF bottomFirstRectPoint, PointF bottomSecondRectPoint, PointF topFirstPoint, PointF topSecondPoint, PointF bottomFirstPoint, PointF bottomSecondPoint, float startAngle, float sweepAngle, int pointIndex ) { // Common Elements CommonElements common = area.Common; // Create a graphics path using (GraphicsPath path = new GraphicsPath()) { // It is enough to transform only two points from // rectangle. This code will create RectangleF from // top left and bottom right points. RectangleF pieTopRectangle = new RectangleF(); pieTopRectangle.X = topFirstRectPoint.X; pieTopRectangle.Y = topFirstRectPoint.Y; pieTopRectangle.Height = topSecondRectPoint.Y - topFirstRectPoint.Y; pieTopRectangle.Width = topSecondRectPoint.X - topFirstRectPoint.X; RectangleF pieBottomRectangle = new RectangleF(); pieBottomRectangle.X = bottomFirstRectPoint.X; pieBottomRectangle.Y = bottomFirstRectPoint.Y; pieBottomRectangle.Height = bottomSecondRectPoint.Y - bottomFirstRectPoint.Y; pieBottomRectangle.Width = bottomSecondRectPoint.X - bottomFirstRectPoint.X; // Angle correction algorithm. After rotation AddArc method should used // different transformed angles. This method transforms angles. double angleCorrection = pieTopRectangle.Height / pieTopRectangle.Width; float endAngle; endAngle = AngleCorrection(startAngle + sweepAngle, angleCorrection); startAngle = AngleCorrection(startAngle, angleCorrection); sweepAngle = endAngle - startAngle; // Add Line between first points path.AddLine(topFirstPoint, bottomFirstPoint); if (pieBottomRectangle.Height <= 0) { // If x angle is 0 this arc will be line in projection. path.AddLine(bottomFirstPoint.X, bottomFirstPoint.Y, bottomSecondPoint.X, bottomSecondPoint.Y); } else { // Add Arc path.AddArc(pieBottomRectangle.X, pieBottomRectangle.Y, pieBottomRectangle.Width, pieBottomRectangle.Height, startAngle, sweepAngle); } // Add Line between second points path.AddLine(bottomSecondPoint, topSecondPoint); if (pieTopRectangle.Height <= 0) { // If x angle is 0 this arc will be line in projection. path.AddLine(topFirstPoint.X, topFirstPoint.Y, topSecondPoint.X, topSecondPoint.Y); } else { path.AddArc(pieTopRectangle.X, pieTopRectangle.Y, pieTopRectangle.Width, pieTopRectangle.Height, startAngle + sweepAngle, -sweepAngle); } if (common.ProcessModePaint) { // Drawing Mode FillPath(brush, path); if (point.BorderColor != Color.Empty && point.BorderWidth > 0 && point.BorderDashStyle != ChartDashStyle.NotSet) { DrawGraphicsPath(pen, path); } } if (common.ProcessModeRegions) { // Check if processing collected data point if (point.IsCustomPropertySet("_COLLECTED_DATA_POINT")) { // Add point to the map area common.HotRegionsList.AddHotRegion( (ChartGraphics)this, path, false, point.ReplaceKeywords(point.ToolTip), string.Empty, string.Empty, string.Empty, point, ChartElementType.DataPoint); return; } common.HotRegionsList.AddHotRegion( path, false, (ChartGraphics)this, point, point.series.Name, pointIndex); } } } /// /// This method draws projection of 3D pie slice. /// /// Chart area used for drawing /// Data point which creates this pie slice /// Graphic Brush used for drawing /// Graphic Pen used for drawing /// The first point of transformed bounding rectangle /// The first arc point of pie slice /// The second point of transformed bounding rectangle /// The second arc point of pie slice /// The center point of pie slice /// Start angle of pie slice /// The end angle of pie slice /// Fill pie slice with brush /// internal void FillPieSlice( ChartArea area, DataPoint point, SolidBrush brush, Pen pen, PointF firstRectPoint, PointF firstPoint, PointF secondRectPoint, PointF secondPoint, PointF center, float startAngle, float sweepAngle, bool fill, int pointIndex ) { // Common elements CommonElements common = area.Common; // Create a graphics path using (GraphicsPath path = new GraphicsPath()) { // It is enough to transform only two points from // rectangle. This code will create RectangleF from // top left and bottom right points. RectangleF pieRectangle = new RectangleF(); pieRectangle.X = firstRectPoint.X; pieRectangle.Y = firstRectPoint.Y; pieRectangle.Height = secondRectPoint.Y - firstRectPoint.Y; pieRectangle.Width = secondRectPoint.X - firstRectPoint.X; // Angle correction algorithm. After rotation AddArc method should used // different transformed angles. This method transforms angles. double angleCorrection = pieRectangle.Height / pieRectangle.Width; float endAngle; endAngle = AngleCorrection(startAngle + sweepAngle, angleCorrection); startAngle = AngleCorrection(startAngle, angleCorrection); sweepAngle = endAngle - startAngle; // Add Line between The Center and Arc path.AddLine(center, firstPoint); // Add Arc if (pieRectangle.Height > 0) { // If x angle is 0 this arc will be line in projection. path.AddArc(pieRectangle.X, pieRectangle.Y, pieRectangle.Width, pieRectangle.Height, startAngle, sweepAngle); } // Add Line between the end of the arc and the centre. path.AddLine(secondPoint, center); if (common.ProcessModePaint) { // Get surface colors Color frontLightColor, leftLightColor, topLightColor, backLightColor, rightLightColor, bottomLightColor; area.matrix3D.GetLight(brush.Color, out frontLightColor, out backLightColor, out leftLightColor, out rightLightColor, out topLightColor, out bottomLightColor); Pen newPen = (Pen)pen.Clone(); if (area.Area3DStyle.LightStyle == LightStyle.Realistic && point.BorderColor == Color.Empty) { newPen.Color = frontLightColor; } // Drawing Mode if (fill) { using (Brush lightBrush = new SolidBrush(frontLightColor)) { FillPath(lightBrush, path); } } if (point.BorderColor != Color.Empty && point.BorderWidth > 0 && point.BorderDashStyle != ChartDashStyle.NotSet) { DrawGraphicsPath(newPen, path); } } if (common.ProcessModeRegions && fill) { // Check if processing collected data point if (point.IsCustomPropertySet("_COLLECTED_DATA_POINT")) { // Add point to the map area common.HotRegionsList.AddHotRegion( (ChartGraphics)this, path, false, point.ReplaceKeywords(point.ToolTip), string.Empty, string.Empty, string.Empty, point, ChartElementType.DataPoint); return; } common.HotRegionsList.AddHotRegion(path, false, (ChartGraphics)this, point, point.series.Name, pointIndex); } } } /// /// This method draws projection of 3D pie slice. /// /// Chart area used for drawing /// Data point which creates this Doughnut slice /// Graphic Brush used for drawing /// Graphic Pen used for drawing /// The first point of transformed bounding rectangle /// The first arc point of Doughnut slice /// The second point of transformed bounding rectangle /// The second arc point of Doughnut slice /// The three point of Doughnut slice /// The four point of Doughnut slice /// Start angle of Doughnut slice /// The end angle of Doughnut slice /// Fill Doughnut slice with brush /// Radius for doughnut chart /// Data Point Index internal void FillDoughnutSlice( ChartArea area, DataPoint point, SolidBrush brush, Pen pen, PointF firstRectPoint, PointF firstPoint, PointF secondRectPoint, PointF secondPoint, PointF threePoint, PointF fourPoint, float startAngle, float sweepAngle, bool fill, float doughnutRadius, int pointIndex ) { // Common Elements CommonElements common = area.Common; doughnutRadius = 1F - doughnutRadius / 100F; // Create a graphics path using (GraphicsPath path = new GraphicsPath()) { // It is enough to transform only two points from // rectangle. This code will create RectangleF from // top left and bottom right points. RectangleF pieRectangle = new RectangleF(); pieRectangle.X = firstRectPoint.X; pieRectangle.Y = firstRectPoint.Y; pieRectangle.Height = secondRectPoint.Y - firstRectPoint.Y; pieRectangle.Width = secondRectPoint.X - firstRectPoint.X; RectangleF pieDoughnutRectangle = new RectangleF(); pieDoughnutRectangle.X = pieRectangle.X + pieRectangle.Width * (1F - doughnutRadius) / 2F; pieDoughnutRectangle.Y = pieRectangle.Y + pieRectangle.Height * (1F - doughnutRadius) / 2F; pieDoughnutRectangle.Height = pieRectangle.Height * doughnutRadius; pieDoughnutRectangle.Width = pieRectangle.Width * doughnutRadius; // Angle correction algorithm. After rotation AddArc method should used // different transformed angles. This method transforms angles. double angleCorrection = pieRectangle.Height / pieRectangle.Width; float endAngle; endAngle = AngleCorrection(startAngle + sweepAngle, angleCorrection); startAngle = AngleCorrection(startAngle, angleCorrection); sweepAngle = endAngle - startAngle; // Add Line between The Doughnut Arc and Arc path.AddLine(fourPoint, firstPoint); // Add Arc if (pieRectangle.Height > 0) { // If x angle is 0 this arc will be line in projection. path.AddArc(pieRectangle.X, pieRectangle.Y, pieRectangle.Width, pieRectangle.Height, startAngle, sweepAngle); } // Add Line between the end of the arc and The Doughnut Arc. path.AddLine(secondPoint, threePoint); // Add Doughnut Arc if (pieDoughnutRectangle.Height > 0) { path.AddArc(pieDoughnutRectangle.X, pieDoughnutRectangle.Y, pieDoughnutRectangle.Width, pieDoughnutRectangle.Height, startAngle + sweepAngle, -sweepAngle); } if (common.ProcessModePaint) { // Get surface colors Color frontLightColor, leftLightColor, topLightColor, backLightColor, rightLightColor, bottomLightColor; area.matrix3D.GetLight(brush.Color, out frontLightColor, out backLightColor, out leftLightColor, out rightLightColor, out topLightColor, out bottomLightColor); Pen newPen = (Pen)pen.Clone(); if (area.Area3DStyle.LightStyle == LightStyle.Realistic && point.BorderColor == Color.Empty) { newPen.Color = frontLightColor; } // Drawing Mode if (fill) { using (Brush lightBrush = new SolidBrush(frontLightColor)) { FillPath(lightBrush, path); } } if (point.BorderColor != Color.Empty && point.BorderWidth > 0 && point.BorderDashStyle != ChartDashStyle.NotSet) { DrawGraphicsPath(newPen, path); } } if (common.ProcessModeRegions && fill) { // Check if processing collected data point if (point.IsCustomPropertySet("_COLLECTED_DATA_POINT")) { // Add point to the map area common.HotRegionsList.AddHotRegion( (ChartGraphics)this, path, false, point.ReplaceKeywords(point.ToolTip), string.Empty, string.Empty, string.Empty, point, ChartElementType.DataPoint); return; } // Add points to the map area common.HotRegionsList.AddHotRegion( path, false, (ChartGraphics)this, point, point.series.Name, pointIndex); } } } /// /// Draw Graphics Path. This method is introduced because of /// bug in DrawPath method when Pen Width is bigger then 1. /// /// Pen /// Graphics Path private void DrawGraphicsPath( Pen pen, GraphicsPath path ) { // Normal case. Very fast Drawing. if( pen.Width < 2 ) { DrawPath( pen, path ); } else { // Converts each curve in this path into a sequence // of connected line segments. Slow Drawing. path.Flatten(); // Set Pen cap pen.EndCap = LineCap.Round; pen.StartCap = LineCap.Round; PointF [] pathPoints; pathPoints = path.PathPoints; // Draw any segment as a line. for( int point = 0; point < path.PathPoints.Length - 1; point++ ) { PointF [] points; points = new PointF[2]; points[0] = pathPoints[point]; points[1] = pathPoints[point+1]; DrawLine( pen, points[0], points[1] ); } } } /// /// Angle correction algorithm. After rotation different /// transformed angle should be used. This method transforms angles. /// /// Not transformed angle /// Correction of bounding rectangle (change between width and height) /// Transformed angle private float AngleCorrection( float angle, double correction ) { // Make all angles to be between -90 and 90. if( angle > -90 && angle < 90 ) { angle = (float)(Math.Atan( Math.Tan( ( angle ) * Math.PI / 180 ) * correction ) * 180 / Math.PI); } else if( angle > -270 && angle < -90 ) { angle = angle + 180; angle = (float)(Math.Atan( Math.Tan( ( angle ) * Math.PI / 180 ) * correction ) * 180 / Math.PI); angle = angle - 180; } else if( angle > 90 && angle < 270 ) { angle = angle - 180; angle = (float)(Math.Atan( Math.Tan( ( angle ) * Math.PI / 180 ) * correction ) * 180 / Math.PI); angle = angle + 180; } else if( angle > 270 && angle < 450 ) { angle = angle - 360; angle = (float)(Math.Atan( Math.Tan( ( angle ) * Math.PI / 180 ) * correction ) * 180 / Math.PI); angle = angle + 360; } else if( angle > 450 ) { angle = angle - 540; angle = (float)(Math.Atan( Math.Tan( ( angle ) * Math.PI / 180 ) * correction ) * 180 / Math.PI); angle = angle + 540; } return angle; } #endregion #region 3D Surface drawing methods (used in Line charts) /// /// Draws a 3D polygon defined by 4 points in 2D space. /// /// Chart area reference. /// Coordinates transformation matrix. /// Name of the surface to draw. /// Z position of the back side of the 3D surface. /// Color of rectangle /// Border Color /// Border Width /// First point. /// Second point. /// Third point. /// Fourth point. /// AxisName of operation Drawing, Calculating Path or Both /// AxisName of line segment. Used for step lines and splines. /// Thin border will be drawn on specified sides. /// Returns elemnt shape path if operationType parameter is set to CalcElementPath, otherwise Null. internal GraphicsPath Draw3DPolygon( ChartArea area, Matrix3D matrix, SurfaceNames surfaceName, float positionZ, Color backColor, Color borderColor, int borderWidth, DataPoint3D firstPoint, DataPoint3D secondPoint, DataPoint3D thirdPoint, DataPoint3D fourthPoint, DrawingOperationTypes operationType, LineSegmentType lineSegmentType, SurfaceNames thinBorders) { // Create graphics path for selection bool drawElements = ((operationType & DrawingOperationTypes.DrawElement) == DrawingOperationTypes.DrawElement); GraphicsPath resultPath = ((operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) ? new GraphicsPath() : null; //********************************************************************** //** Prepare, transform polygon coordinates //********************************************************************** // Define 4 points polygon Point3D [] points3D = new Point3D[4]; points3D[0] = new Point3D((float)firstPoint.xPosition, (float)firstPoint.yPosition, positionZ); points3D[1] = new Point3D((float)secondPoint.xPosition, (float)secondPoint.yPosition, positionZ); points3D[2] = new Point3D((float)thirdPoint.xPosition, (float)thirdPoint.yPosition, positionZ); points3D[3] = new Point3D((float)fourthPoint.xPosition, (float)fourthPoint.yPosition, positionZ); // Transform coordinates matrix.TransformPoints( points3D ); // Get absolute coordinates and create array of PointF PointF[] polygonPoints = new PointF[4]; polygonPoints[0] = GetAbsolutePoint(points3D[0].PointF); polygonPoints[1] = GetAbsolutePoint(points3D[1].PointF); polygonPoints[2] = GetAbsolutePoint(points3D[2].PointF); polygonPoints[3] = GetAbsolutePoint(points3D[3].PointF); //********************************************************************** //** Define drawing colors //********************************************************************** bool topIsVisible = IsSurfaceVisible( points3D[0], points3D[1], points3D[2]); Color polygonColor = matrix.GetPolygonLight( points3D, backColor, topIsVisible, area.Area3DStyle.Rotation, surfaceName, area.ReverseSeriesOrder ); Color surfaceBorderColor = borderColor; if(surfaceBorderColor == Color.Empty) { // If border color is emty use color slightly darker than main back color surfaceBorderColor = ChartGraphics.GetGradientColor( backColor, Color.Black, 0.2 ); } //********************************************************************** //** Draw elements if required. //********************************************************************** Pen thickBorderPen = null; if(drawElements) { // Remember SmoothingMode and turn off anti aliasing SmoothingMode oldSmoothingMode = SmoothingMode; SmoothingMode = SmoothingMode.Default; // Draw the polygon using (Brush brush = new SolidBrush(polygonColor)) { FillPolygon(brush, polygonPoints); } // Return old smoothing mode SmoothingMode = oldSmoothingMode; // Draw thin polygon border of darker color around the whole polygon if(thinBorders != 0) { Pen thinLinePen = new Pen(surfaceBorderColor, 1); if( (thinBorders & SurfaceNames.Left) != 0 ) DrawLine(thinLinePen, polygonPoints[3], polygonPoints[0]); if( (thinBorders & SurfaceNames.Right) != 0 ) DrawLine(thinLinePen, polygonPoints[1], polygonPoints[2]); if( (thinBorders & SurfaceNames.Top) != 0 ) DrawLine(thinLinePen, polygonPoints[0], polygonPoints[1]); if( (thinBorders & SurfaceNames.Bottom) != 0 ) DrawLine(thinLinePen, polygonPoints[2], polygonPoints[3]); } else if(polygonColor.A == 255) { DrawPolygon(new Pen(polygonColor, 1), polygonPoints); } // Create thick border line pen thickBorderPen = new Pen(surfaceBorderColor, borderWidth); thickBorderPen.StartCap = LineCap.Round; thickBorderPen.EndCap = LineCap.Round; // Draw thick Top & Bottom lines DrawLine(thickBorderPen, polygonPoints[0], polygonPoints[1]); DrawLine(thickBorderPen, polygonPoints[2], polygonPoints[3]); // Draw thick Right & Left lines on first & last segments of the line if(lineSegmentType == LineSegmentType.First) { DrawLine(thickBorderPen, polygonPoints[3], polygonPoints[0]); } else if(lineSegmentType == LineSegmentType.Last) { DrawLine(thickBorderPen, polygonPoints[1], polygonPoints[2]); } } //********************************************************************** //** Redraw front line of the previuos line segment. //********************************************************************** if(area.Area3DStyle.Perspective == 0) { if(frontLinePoint1 != PointF.Empty && frontLinePen != null) { if( (frontLinePoint1.X == polygonPoints[0].X && frontLinePoint1.Y == polygonPoints[0].Y || frontLinePoint2.X == polygonPoints[1].X && frontLinePoint2.Y == polygonPoints[1].Y ) || (frontLinePoint1.X == polygonPoints[1].X && frontLinePoint1.Y == polygonPoints[1].Y || frontLinePoint2.X == polygonPoints[0].X && frontLinePoint2.Y == polygonPoints[0].Y ) || (frontLinePoint1.X == polygonPoints[3].X && frontLinePoint1.Y == polygonPoints[3].Y || frontLinePoint2.X == polygonPoints[2].X && frontLinePoint2.Y == polygonPoints[2].Y) || (frontLinePoint1.X == polygonPoints[2].X && frontLinePoint1.Y == polygonPoints[2].Y || frontLinePoint2.X == polygonPoints[3].X && frontLinePoint2.Y == polygonPoints[3].Y) ) { // Do not draw the line if it will be overlapped with current } else { // Draw line !!!! DrawLine( frontLinePen, (float)Math.Round(frontLinePoint1.X), (float)Math.Round(frontLinePoint1.Y), (float)Math.Round(frontLinePoint2.X), (float)Math.Round(frontLinePoint2.Y) ); } // Reset line properties frontLinePen = null; frontLinePoint1 = PointF.Empty; frontLinePoint2 = PointF.Empty; } //********************************************************************** //** Check if front line should be redrawn whith the next segment. //********************************************************************** if(drawElements) { // Add top line frontLinePen = thickBorderPen; frontLinePoint1 = polygonPoints[0]; frontLinePoint2 = polygonPoints[1]; } } // Calculate path for selection if(resultPath != null) { // Add polygon to the path resultPath.AddPolygon(polygonPoints); } return resultPath; } /// /// Helper method which returns the splines flatten path. /// /// Chart area reference. /// Z position of the back side of the 3D surface. /// First point. /// Second point. /// Array of points. /// Line tension. /// Flatten result path. /// Indicates that points coordinates should be translated. /// Index of the Y value to use. /// Spline path. internal GraphicsPath GetSplineFlattenPath( ChartArea area, float positionZ, DataPoint3D firstPoint, DataPoint3D secondPoint, ArrayList points, float tension, bool flatten, bool translateCoordinates, int yValueIndex) { // Find first spline point index int firtsSplinePointIndex = (firstPoint.index < secondPoint.index) ? firstPoint.index : secondPoint.index; --firtsSplinePointIndex; if(firtsSplinePointIndex >= (points.Count - 2) ) { --firtsSplinePointIndex; } if(firtsSplinePointIndex < 1) { firtsSplinePointIndex = 1; } // Find four points which are required to draw the spline int pointArrayIndex = int.MinValue; DataPoint3D [] splineDataPoints = new DataPoint3D[4]; splineDataPoints[0] = FindPointByIndex(points, firtsSplinePointIndex, null, ref pointArrayIndex); splineDataPoints[1] = FindPointByIndex(points, firtsSplinePointIndex + 1, null, ref pointArrayIndex); splineDataPoints[2] = FindPointByIndex(points, firtsSplinePointIndex + 2, null, ref pointArrayIndex); splineDataPoints[3] = FindPointByIndex(points, firtsSplinePointIndex + 3, null, ref pointArrayIndex); // Get offset of spline segment in array int splineSegmentOffset = 0; while(splineSegmentOffset < 4) { if(splineDataPoints[splineSegmentOffset].index == firstPoint.index || splineDataPoints[splineSegmentOffset].index == secondPoint.index) { break; } ++splineSegmentOffset; } // Get number of found points int nonNullPoints = 2; if(splineDataPoints[2] != null) ++nonNullPoints; if(splineDataPoints[3] != null) ++nonNullPoints; // Get coordinates and create array of PointF for the front spline PointF[] polygonPointsFront = new PointF[nonNullPoints]; if(yValueIndex == 0) { polygonPointsFront[0] = new PointF((float)splineDataPoints[0].xPosition, (float)splineDataPoints[0].yPosition); polygonPointsFront[1] = new PointF((float)splineDataPoints[1].xPosition, (float)splineDataPoints[1].yPosition); if(nonNullPoints > 2) polygonPointsFront[2] = new PointF((float)splineDataPoints[2].xPosition, (float)splineDataPoints[2].yPosition); if(nonNullPoints > 3) polygonPointsFront[3] = new PointF((float)splineDataPoints[3].xPosition, (float)splineDataPoints[3].yPosition); } else { // Set active vertical axis Axis vAxis = (firstPoint.dataPoint.series.YAxisType == AxisType.Primary) ? area.AxisY : area.AxisY2; float secondYValue = (float)vAxis.GetPosition(splineDataPoints[0].dataPoint.YValues[yValueIndex]); polygonPointsFront[0] = new PointF((float)splineDataPoints[0].xPosition, secondYValue); secondYValue = (float)vAxis.GetPosition(splineDataPoints[1].dataPoint.YValues[yValueIndex]); polygonPointsFront[1] = new PointF((float)splineDataPoints[1].xPosition, secondYValue); if(nonNullPoints > 2) { secondYValue = (float)vAxis.GetPosition(splineDataPoints[2].dataPoint.YValues[yValueIndex]); polygonPointsFront[2] = new PointF((float)splineDataPoints[2].xPosition, secondYValue); } if(nonNullPoints > 3) { secondYValue = (float)vAxis.GetPosition(splineDataPoints[3].dataPoint.YValues[yValueIndex]); polygonPointsFront[3] = new PointF((float)splineDataPoints[3].xPosition, secondYValue); } } // Translate points coordinates in 3D space and get absolute coordinate if(translateCoordinates) { // Prepare array of points Point3D[] points3D = new Point3D[nonNullPoints]; for(int index = 0; index < nonNullPoints; index++) { points3D[index] = new Point3D(polygonPointsFront[index].X, polygonPointsFront[index].Y, positionZ); } // Make coordinates transformation area.matrix3D.TransformPoints( points3D ); // Get absolute values for(int index = 0; index < nonNullPoints; index++) { polygonPointsFront[index] = GetAbsolutePoint(points3D[index].PointF); } } // Create graphics path for the front spline surface and flatten it. GraphicsPath splineSurfacePath = new GraphicsPath(); splineSurfacePath.AddCurve(polygonPointsFront, splineSegmentOffset, 1, tension); if(flatten) { splineSurfacePath.Flatten(); } // IsReversed points order if(firstPoint.index > secondPoint.index) { splineSurfacePath.Reverse(); } return splineSurfacePath; } /// /// Draws a 3D spline surface connecting the two specified points in 2D space. /// Used to draw Spline based charts. /// /// Chart area reference. /// Coordinates transformation matrix. /// LightStyle style (None, Simplistic, Realistic). /// Name of the surface to draw. /// Z position of the back side of the 3D surface. /// Depth of the 3D surface. /// Color of rectangle /// Border Color /// Border Width /// Border Style /// First point. /// Second point. /// Array of points. /// Index of point to draw. /// Line tension. /// AxisName of operation Drawing, Calculating Path or Both /// Thin border will be drawn on all segments. /// Thick border will be drawn on all segments. /// Series are drawn in reversed order. /// Multiple series are drawn at the same time. /// Index of the Y value to use. /// Surface should be clipped inside plotting area. /// Returns elemnt shape path if operationType parameter is set to CalcElementPath, otherwise Null. internal GraphicsPath Draw3DSplineSurface( ChartArea area, Matrix3D matrix, LightStyle lightStyle, SurfaceNames surfaceName, float positionZ, float depth, Color backColor, Color borderColor, int borderWidth, ChartDashStyle borderDashStyle, DataPoint3D firstPoint, DataPoint3D secondPoint, ArrayList points, int pointIndex, float tension, DrawingOperationTypes operationType, bool forceThinBorder, bool forceThickBorder, bool reversedSeriesOrder, bool multiSeries, int yValueIndex, bool clipInsideArea) { // If zero tension is specified - draw a Line Surface if(tension == 0f) { return Draw3DSurface( area, matrix, lightStyle, surfaceName, positionZ, depth, backColor, borderColor, borderWidth, borderDashStyle, firstPoint, secondPoint, points, pointIndex, tension, operationType, LineSegmentType.Single, forceThinBorder, forceThickBorder, reversedSeriesOrder, multiSeries, yValueIndex, clipInsideArea); } // Create graphics path for selection GraphicsPath resultPath = ((operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) ? new GraphicsPath() : null; // Get spline flatten path GraphicsPath splineSurfacePath = GetSplineFlattenPath( area, positionZ, firstPoint, secondPoint, points, tension, true, false, yValueIndex); // Check if reversed drawing order required bool reversed = false; if((pointIndex + 1) < points.Count) { DataPoint3D p = (DataPoint3D)points[pointIndex + 1]; if(p.index == firstPoint.index) { reversed = true; } } if(reversed) { splineSurfacePath.Reverse(); } // Loop through all segment lines the spline consists off PointF[] splinePathPoints = splineSurfacePath.PathPoints; DataPoint3D dp1 = new DataPoint3D(); DataPoint3D dp2 = new DataPoint3D(); LineSegmentType lineSegmentType = LineSegmentType.Middle; for(int pIndex = 1; pIndex < splinePathPoints.Length; pIndex++) { bool forceSegmentThinBorder = false; bool forceSegmentThickBorder = false; // Calculate surface coordinates if(!reversed) { dp1.index = firstPoint.index; dp1.dataPoint = firstPoint.dataPoint; dp1.xPosition = splinePathPoints[pIndex - 1].X; dp1.yPosition = splinePathPoints[pIndex - 1].Y; dp2.index = secondPoint.index; dp2.index = secondPoint.index; dp2.xPosition = splinePathPoints[pIndex].X; dp2.yPosition = splinePathPoints[pIndex].Y; } else { dp2.index = firstPoint.index; dp2.dataPoint = firstPoint.dataPoint; dp2.xPosition = splinePathPoints[pIndex - 1].X; dp2.yPosition = splinePathPoints[pIndex - 1].Y; dp1.index = secondPoint.index; dp1.dataPoint = secondPoint.dataPoint; dp1.xPosition = splinePathPoints[pIndex].X; dp1.yPosition = splinePathPoints[pIndex].Y; } // Get sefment type lineSegmentType = LineSegmentType.Middle; if(pIndex == 1) { if(!reversed) lineSegmentType = LineSegmentType.First; else lineSegmentType = LineSegmentType.Last; forceSegmentThinBorder = forceThinBorder; forceSegmentThickBorder = forceThickBorder; } else if(pIndex == splinePathPoints.Length - 1) { if(!reversed) lineSegmentType = LineSegmentType.Last; else lineSegmentType = LineSegmentType.First; forceSegmentThinBorder = forceThinBorder; forceSegmentThickBorder = forceThickBorder; } // Draw flat surface GraphicsPath segmentResultPath = Draw3DSurface( area, matrix, lightStyle, surfaceName, positionZ, depth, backColor, borderColor, borderWidth, borderDashStyle, dp1, dp2, points, pointIndex, 0f, operationType, lineSegmentType, forceSegmentThinBorder, forceSegmentThickBorder, reversedSeriesOrder, multiSeries, yValueIndex, clipInsideArea); // Add selection path if(resultPath != null && segmentResultPath != null && segmentResultPath.PointCount > 0) { resultPath.AddPath(segmentResultPath, true); } } return resultPath; } /// /// Draws a 3D surface connecting the two specified points in 2D space. /// Used to draw Line based charts. /// /// Chart area reference. /// Coordinates transformation matrix. /// LightStyle style (None, Simplistic, Realistic). /// Name of the surface to draw. /// Z position of the back side of the 3D surface. /// Depth of the 3D surface. /// Color of rectangle /// Border Color /// Border Width /// Border Style /// First point. /// Second point. /// Array of points. /// Index of point to draw. /// Line tension. /// AxisName of operation Drawing, Calculating Path or Both /// AxisName of line segment. Used for step lines and splines. /// Thin border will be drawn on all segments. /// Thick border will be drawn on all segments. /// Series are drawn in reversed order. /// Multiple series are drawn at the same time. /// Index of the Y value to use. /// Surface should be clipped inside plotting area. /// Returns elemnt shape path if operationType parameter is set to CalcElementPath, otherwise Null. internal GraphicsPath Draw3DSurface( ChartArea area, Matrix3D matrix, LightStyle lightStyle, SurfaceNames surfaceName, float positionZ, float depth, Color backColor, Color borderColor, int borderWidth, ChartDashStyle borderDashStyle, DataPoint3D firstPoint, DataPoint3D secondPoint, ArrayList points, int pointIndex, float tension, DrawingOperationTypes operationType, LineSegmentType lineSegmentType, bool forceThinBorder, bool forceThickBorder, bool reversedSeriesOrder, bool multiSeries, int yValueIndex, bool clipInsideArea) { // If non-zero tension is specified - draw a Spline Surface if(tension != 0f) { return Draw3DSplineSurface( area, matrix, lightStyle, surfaceName, positionZ, depth, backColor, borderColor, borderWidth, borderDashStyle, firstPoint, secondPoint, points, pointIndex, tension, operationType, forceThinBorder, forceThickBorder, reversedSeriesOrder, multiSeries, yValueIndex, clipInsideArea); } //********************************************************************** //** Create graphics path for selection //********************************************************************** bool drawElements = ((operationType & DrawingOperationTypes.DrawElement) == DrawingOperationTypes.DrawElement); GraphicsPath resultPath = ((operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) ? new GraphicsPath() : null; //********************************************************************** //** Check surface coordinates //********************************************************************** if((decimal)firstPoint.xPosition == (decimal)secondPoint.xPosition && (decimal)firstPoint.yPosition == (decimal)secondPoint.yPosition) { return resultPath; } //********************************************************************** //** Clip surface //********************************************************************** // Check if line between the first and second points intersects with // plotting area top or bottom boundary if(clipInsideArea) { //**************************************************************** //** Round plot are position and point coordinates //**************************************************************** int decimals = 3; decimal plotAreaPositionX = Math.Round((decimal)area.PlotAreaPosition.X, decimals); decimal plotAreaPositionY = Math.Round((decimal)area.PlotAreaPosition.Y, decimals); decimal plotAreaPositionRight = Math.Round((decimal)area.PlotAreaPosition.Right, decimals); decimal plotAreaPositionBottom = Math.Round((decimal)area.PlotAreaPosition.Bottom, decimals); // Make area a little bit bigger plotAreaPositionX -= 0.001M; plotAreaPositionY -= 0.001M; plotAreaPositionRight += 0.001M; plotAreaPositionBottom += 0.001M; // Chech data points X values if((decimal)firstPoint.xPosition < plotAreaPositionX || (decimal)firstPoint.xPosition > plotAreaPositionRight || (decimal)secondPoint.xPosition < plotAreaPositionX || (decimal)secondPoint.xPosition > plotAreaPositionRight ) { // Check if surface completly out of the plot area if((decimal)firstPoint.xPosition < plotAreaPositionX && (decimal)secondPoint.xPosition < plotAreaPositionX) { return resultPath; } // Check if surface completly out of the plot area if((decimal)firstPoint.xPosition > plotAreaPositionRight && (decimal)secondPoint.xPosition > plotAreaPositionRight) { return resultPath; } // Only part of the surface is outside - fix X value and adjust Y value if((decimal)firstPoint.xPosition < plotAreaPositionX) { firstPoint.yPosition = ((double)plotAreaPositionX - secondPoint.xPosition) / (firstPoint.xPosition - secondPoint.xPosition) * (firstPoint.yPosition - secondPoint.yPosition) + secondPoint.yPosition; firstPoint.xPosition = (double)plotAreaPositionX; } else if((decimal)firstPoint.xPosition > plotAreaPositionRight) { firstPoint.yPosition = ((double)plotAreaPositionRight - secondPoint.xPosition) / (firstPoint.xPosition - secondPoint.xPosition) * (firstPoint.yPosition - secondPoint.yPosition) + secondPoint.yPosition; firstPoint.xPosition = (double)plotAreaPositionRight; } if((decimal)secondPoint.xPosition < plotAreaPositionX) { secondPoint.yPosition = ((double)plotAreaPositionX - secondPoint.xPosition) / (firstPoint.xPosition - secondPoint.xPosition) * (firstPoint.yPosition - secondPoint.yPosition) + secondPoint.yPosition; secondPoint.xPosition = (double)plotAreaPositionX; } else if((decimal)secondPoint.xPosition > plotAreaPositionRight) { secondPoint.yPosition = ((double)plotAreaPositionRight - secondPoint.xPosition) / (firstPoint.xPosition - secondPoint.xPosition) * (firstPoint.yPosition - secondPoint.yPosition) + secondPoint.yPosition; secondPoint.xPosition = (double)plotAreaPositionRight; } } // Chech data points Y values if((decimal)firstPoint.yPosition < plotAreaPositionY || (decimal)firstPoint.yPosition > plotAreaPositionBottom || (decimal)secondPoint.yPosition < plotAreaPositionY || (decimal)secondPoint.yPosition > plotAreaPositionBottom ) { // Remember previous y positions double prevFirstPointY = firstPoint.yPosition; double prevSecondPointY = secondPoint.yPosition; // Check if whole line is outside plotting region bool surfaceCompletlyOutside = false; if((decimal)firstPoint.yPosition < plotAreaPositionY && (decimal)secondPoint.yPosition < plotAreaPositionY) { surfaceCompletlyOutside = true; firstPoint.yPosition = (double)plotAreaPositionY; secondPoint.yPosition = (double)plotAreaPositionY; } if((decimal)firstPoint.yPosition > plotAreaPositionBottom && (decimal)secondPoint.yPosition > plotAreaPositionBottom) { surfaceCompletlyOutside = true; firstPoint.yPosition = (double)plotAreaPositionBottom; secondPoint.yPosition = (double)plotAreaPositionBottom; } // Calculate color used to draw "cut" surfaces Color cutSurfaceBackColor = ChartGraphics.GetGradientColor(backColor, Color.Black, 0.5); Color cutSurfaceBorderColor = ChartGraphics.GetGradientColor(borderColor, Color.Black, 0.5); // Draw just one surface if(surfaceCompletlyOutside) { resultPath = this.Draw3DSurface( area, matrix, lightStyle, surfaceName, positionZ, depth, cutSurfaceBackColor, cutSurfaceBorderColor, borderWidth, borderDashStyle, firstPoint, secondPoint, points, pointIndex, tension, operationType, lineSegmentType, forceThinBorder, forceThickBorder, reversedSeriesOrder, multiSeries, yValueIndex, clipInsideArea); // Restore previous y positions firstPoint.yPosition = prevFirstPointY; secondPoint.yPosition = prevSecondPointY; return resultPath; } // Get intersection point DataPoint3D intersectionPoint = new DataPoint3D(); intersectionPoint.yPosition = (double)plotAreaPositionY; if((decimal)firstPoint.yPosition > plotAreaPositionBottom || (decimal)secondPoint.yPosition > plotAreaPositionBottom ) { intersectionPoint.yPosition = (double)plotAreaPositionBottom; } intersectionPoint.xPosition = (intersectionPoint.yPosition - secondPoint.yPosition) * (firstPoint.xPosition - secondPoint.xPosition) / (firstPoint.yPosition - secondPoint.yPosition) + secondPoint.xPosition; // Check if there are 2 intersection points (3 segments) int segmentNumber = 2; DataPoint3D intersectionPoint2 = null; if( ((decimal)firstPoint.yPosition < plotAreaPositionY && (decimal)secondPoint.yPosition > plotAreaPositionBottom) || ((decimal)firstPoint.yPosition > plotAreaPositionBottom && (decimal)secondPoint.yPosition < plotAreaPositionY)) { segmentNumber = 3; intersectionPoint2 = new DataPoint3D(); if((decimal)intersectionPoint.yPosition == plotAreaPositionY) { intersectionPoint2.yPosition = (double)plotAreaPositionBottom; } else { intersectionPoint2.yPosition = (double)plotAreaPositionY; } intersectionPoint2.xPosition = (intersectionPoint2.yPosition - secondPoint.yPosition) * (firstPoint.xPosition - secondPoint.xPosition) / (firstPoint.yPosition - secondPoint.yPosition) + secondPoint.xPosition; // Switch intersection points if((decimal)firstPoint.yPosition > plotAreaPositionBottom) { DataPoint3D tempPoint = new DataPoint3D(); tempPoint.xPosition = intersectionPoint.xPosition; tempPoint.yPosition = intersectionPoint.yPosition; intersectionPoint.xPosition = intersectionPoint2.xPosition; intersectionPoint.yPosition = intersectionPoint2.yPosition; intersectionPoint2.xPosition = tempPoint.xPosition; intersectionPoint2.yPosition = tempPoint.yPosition; } } // Adjust points Y values bool firstSegmentVisible = true; if((decimal)firstPoint.yPosition < plotAreaPositionY) { firstSegmentVisible = false; firstPoint.yPosition = (double)plotAreaPositionY; } else if((decimal)firstPoint.yPosition > plotAreaPositionBottom) { firstSegmentVisible = false; firstPoint.yPosition = (double)plotAreaPositionBottom; } if((decimal)secondPoint.yPosition < plotAreaPositionY) { secondPoint.yPosition = (double)plotAreaPositionY; } else if((decimal)secondPoint.yPosition > plotAreaPositionBottom) { secondPoint.yPosition = (double)plotAreaPositionBottom; } // Check if reversed drawing order required bool reversed = false; if((pointIndex + 1) < points.Count) { DataPoint3D p = (DataPoint3D)points[pointIndex + 1]; if(p.index == firstPoint.index) { reversed = true; } } // Draw surfaces in 2 or 3 segments for(int segmentIndex = 0; segmentIndex < 3; segmentIndex++) { GraphicsPath segmentPath = null; if(segmentIndex == 0 && !reversed || segmentIndex == 2 && reversed) { // Draw first segment if(intersectionPoint2 == null) { intersectionPoint2 = intersectionPoint; } intersectionPoint2.dataPoint = secondPoint.dataPoint; intersectionPoint2.index = secondPoint.index; segmentPath = this.Draw3DSurface( area, matrix, lightStyle, surfaceName, positionZ, depth, (firstSegmentVisible && segmentNumber != 3) ? backColor : cutSurfaceBackColor, (firstSegmentVisible && segmentNumber != 3) ? borderColor : cutSurfaceBorderColor, borderWidth, borderDashStyle, firstPoint, intersectionPoint2, points, pointIndex, tension, operationType, lineSegmentType, forceThinBorder, forceThickBorder, reversedSeriesOrder, multiSeries, yValueIndex, clipInsideArea); } if(segmentIndex == 1 && intersectionPoint2 != null && segmentNumber == 3) { // Draw middle segment intersectionPoint2.dataPoint = secondPoint.dataPoint; intersectionPoint2.index = secondPoint.index; segmentPath = this.Draw3DSurface( area, matrix, lightStyle, surfaceName, positionZ, depth, backColor, borderColor, borderWidth, borderDashStyle, intersectionPoint, intersectionPoint2, points, pointIndex, tension, operationType, lineSegmentType, forceThinBorder, forceThickBorder, reversedSeriesOrder, multiSeries, yValueIndex, clipInsideArea); } if(segmentIndex == 2 && !reversed || segmentIndex == 0 && reversed) { // Draw second segment intersectionPoint.dataPoint = firstPoint.dataPoint; intersectionPoint.index = firstPoint.index; segmentPath = this.Draw3DSurface( area, matrix, lightStyle, surfaceName, positionZ, depth, (!firstSegmentVisible && segmentNumber != 3) ? backColor : cutSurfaceBackColor, (!firstSegmentVisible && segmentNumber != 3) ? borderColor : cutSurfaceBorderColor, borderWidth, borderDashStyle, intersectionPoint, secondPoint, points, pointIndex, tension, operationType, lineSegmentType, forceThinBorder, forceThickBorder, reversedSeriesOrder, multiSeries, yValueIndex, clipInsideArea); } // Add segment path if(resultPath != null && segmentPath != null && segmentPath.PointCount > 0) { resultPath.SetMarkers(); resultPath.AddPath(segmentPath, true); } } // Restore previous y positions firstPoint.yPosition = prevFirstPointY; secondPoint.yPosition = prevSecondPointY; return resultPath; } } //********************************************************************** //** Prepare, transform polygon coordinates //********************************************************************** // Define 4 points polygon Point3D [] points3D = new Point3D[4]; points3D[0] = new Point3D((float)firstPoint.xPosition, (float)firstPoint.yPosition, positionZ + depth); points3D[1] = new Point3D((float)secondPoint.xPosition, (float)secondPoint.yPosition, positionZ + depth); points3D[2] = new Point3D((float)secondPoint.xPosition, (float)secondPoint.yPosition, positionZ); points3D[3] = new Point3D((float)firstPoint.xPosition, (float)firstPoint.yPosition, positionZ); // Transform coordinates matrix.TransformPoints( points3D ); // Get absolute coordinates and create array of PointF PointF[] polygonPoints = new PointF[4]; polygonPoints[0] = GetAbsolutePoint(points3D[0].PointF); polygonPoints[1] = GetAbsolutePoint(points3D[1].PointF); polygonPoints[2] = GetAbsolutePoint(points3D[2].PointF); polygonPoints[3] = GetAbsolutePoint(points3D[3].PointF); //********************************************************************** //** Define drawing colors //********************************************************************** bool topIsVisible = IsSurfaceVisible( points3D[0], points3D[1], points3D[2]); Color polygonColor = matrix.GetPolygonLight( points3D, backColor, topIsVisible, area.Area3DStyle.Rotation, surfaceName, area.ReverseSeriesOrder ); Color surfaceBorderColor = borderColor; if(surfaceBorderColor == Color.Empty) { // If border color is emty use color slightly darker than main back color surfaceBorderColor = ChartGraphics.GetGradientColor( backColor, Color.Black, 0.2 ); } //********************************************************************** //** Draw elements if required. //********************************************************************** Pen thinBorderPen = new Pen(surfaceBorderColor, 1); if(drawElements) { // Draw the polygon if(backColor != Color.Transparent) { // Remember SmoothingMode and turn off anti aliasing SmoothingMode oldSmoothingMode = SmoothingMode; SmoothingMode = SmoothingMode.Default; // Draw the polygon using (Brush brush = new SolidBrush(polygonColor)) { FillPolygon(brush, polygonPoints); } // Return old smoothing mode SmoothingMode = oldSmoothingMode; } // Draw thin polygon border of darker color if(forceThinBorder || forceThickBorder) { if(forceThickBorder) { Pen linePen = new Pen(surfaceBorderColor, borderWidth); linePen.StartCap = LineCap.Round; linePen.EndCap = LineCap.Round; DrawLine(linePen, polygonPoints[0], polygonPoints[1]); DrawLine(linePen, polygonPoints[2], polygonPoints[3]); DrawLine(linePen, polygonPoints[3], polygonPoints[0]); DrawLine(linePen, polygonPoints[1], polygonPoints[2]); } else { // Front & Back lines DrawLine(thinBorderPen, polygonPoints[0], polygonPoints[1]); DrawLine(thinBorderPen, polygonPoints[2], polygonPoints[3]); if(lineSegmentType == LineSegmentType.First) { // Left line DrawLine(thinBorderPen, polygonPoints[3], polygonPoints[0]); } else if(lineSegmentType == LineSegmentType.Last) { // Right Line DrawLine(thinBorderPen, polygonPoints[1], polygonPoints[2]); } else { // Left & Right lines DrawLine(thinBorderPen, polygonPoints[3], polygonPoints[0]); DrawLine(thinBorderPen, polygonPoints[1], polygonPoints[2]); } } } else { // Draw thin polygon border of same color (solves anti-aliasing issues) if(polygonColor.A == 255) { DrawPolygon(new Pen(polygonColor, 1), polygonPoints); } // Draw thin Front & Back lines DrawLine(thinBorderPen, polygonPoints[0], polygonPoints[1]); DrawLine(thinBorderPen, polygonPoints[2], polygonPoints[3]); } } //********************************************************************** //** Draw thick border line on visible sides //********************************************************************** Pen thickBorderPen = null; if(borderWidth > 1 && !forceThickBorder) { // Create thick border line pen thickBorderPen = new Pen(surfaceBorderColor, borderWidth); thickBorderPen.StartCap = LineCap.Round; thickBorderPen.EndCap = LineCap.Round; //**************************************************************** //** Switch first and second points. //**************************************************************** if(firstPoint.index > secondPoint.index) { DataPoint3D tempPoint = firstPoint; firstPoint = secondPoint; secondPoint = tempPoint; } //********************************************************************** //** Check if there are visible (non-empty) lines to the left & right //** of the current line. //********************************************************************** // Get visibility of bounding rectangle float minX = (float)Math.Min(points3D[0].X, points3D[1].X); float minY = (float)Math.Min(points3D[0].Y, points3D[1].Y); float maxX = (float)Math.Max(points3D[0].X, points3D[1].X); float maxY = (float)Math.Max(points3D[0].Y, points3D[1].Y); RectangleF position = new RectangleF(minX, minY, maxX - minX, maxY - minY); SurfaceNames visibleSurfaces = GetVisibleSurfaces(position,positionZ,depth,matrix); // Check left line visibility bool thickBorderOnLeft = false; bool thickBorderOnRight = false; if(lineSegmentType != LineSegmentType.Middle) { LineSegmentType tempLineSegmentType = LineSegmentType.Single; // Check left line visibility thickBorderOnLeft = (ChartGraphics.ShouldDrawLineChartSurface( area, reversedSeriesOrder, SurfaceNames.Left, visibleSurfaces, polygonColor, points, firstPoint, secondPoint, multiSeries, ref tempLineSegmentType) == 2); // Check right line visibility thickBorderOnRight = (ChartGraphics.ShouldDrawLineChartSurface( area, reversedSeriesOrder, SurfaceNames.Right, visibleSurfaces, polygonColor, points, firstPoint, secondPoint, multiSeries, ref tempLineSegmentType) == 2); } // Switch left & right border if series is reversed if(reversedSeriesOrder) { bool tempVal = thickBorderOnLeft; thickBorderOnLeft = thickBorderOnRight; thickBorderOnRight = tempVal; } // Draw thick border for single segment lines only // or for the first & last segment if(lineSegmentType != LineSegmentType.First && lineSegmentType != LineSegmentType.Single) { thickBorderOnLeft = false; } if(lineSegmentType != LineSegmentType.Last && lineSegmentType != LineSegmentType.Single) { thickBorderOnRight = false; } //********************************************************************** //** Draw border on the front side of line surface (only when visible) //********************************************************************** if( matrix.Perspective != 0 || (matrix.AngleX != 90 && matrix.AngleX != -90 && matrix.AngleY != 90 && matrix.AngleY != -90 && matrix.AngleY != 180 && matrix.AngleY != -180)) { // Draw thick line on the front side of the line surface if(drawElements) { DrawLine( thickBorderPen, (float)Math.Round(polygonPoints[0].X), (float)Math.Round(polygonPoints[0].Y), (float)Math.Round(polygonPoints[1].X), (float)Math.Round(polygonPoints[1].Y) ); } // Calculate path for selection if(resultPath != null) { // Add front line to the path resultPath.AddLine( (float)Math.Round(polygonPoints[0].X), (float)Math.Round(polygonPoints[0].Y), (float)Math.Round(polygonPoints[1].X), (float)Math.Round(polygonPoints[1].Y)); } } //********************************************************************** //** Draw border on the left side of line surface (only when visible) //********************************************************************** // Use flat end for Right & Left border thickBorderPen.EndCap = LineCap.Flat; // Draw border on the left side if (matrix.Perspective != 0 || (matrix.AngleX != 90 && matrix.AngleX != -90)) { if(thickBorderOnLeft) { if(drawElements) { DrawLine( thickBorderPen, (float)Math.Round(polygonPoints[3].X), (float)Math.Round(polygonPoints[3].Y), (float)Math.Round(polygonPoints[0].X), (float)Math.Round(polygonPoints[0].Y) ); } // Calculate path for selection if(resultPath != null) { // Add left line to the path resultPath.AddLine( (float)Math.Round(polygonPoints[3].X), (float)Math.Round(polygonPoints[3].Y), (float)Math.Round(polygonPoints[0].X), (float)Math.Round(polygonPoints[0].Y)); } } } //********************************************************************** //** Draw border on the right side of the line surface //********************************************************************** if (matrix.Perspective != 0 || (matrix.AngleX != 90 && matrix.AngleX != -90)) { if(thickBorderOnRight) { if(drawElements) { DrawLine( thickBorderPen, (float)Math.Round(polygonPoints[1].X), (float)Math.Round(polygonPoints[1].Y), (float)Math.Round(polygonPoints[2].X), (float)Math.Round(polygonPoints[2].Y) ); } // Calculate path for selection if(resultPath != null) { // Add right line to the path resultPath.AddLine( (float)Math.Round(polygonPoints[1].X), (float)Math.Round(polygonPoints[1].Y), (float)Math.Round(polygonPoints[2].X), (float)Math.Round(polygonPoints[2].Y)); } } } } //********************************************************************** // Redraw front line of the previuos line segment. // Solves 3D visibility problem between wide border line and line surface. //********************************************************************** if( area.Area3DStyle.Perspective == 0 ) { if(frontLinePoint1 != PointF.Empty && frontLinePen != null) { // Draw line DrawLine( frontLinePen, (float)Math.Round(frontLinePoint1.X), (float)Math.Round(frontLinePoint1.Y), (float)Math.Round(frontLinePoint2.X), (float)Math.Round(frontLinePoint2.Y) ); // Reset line properties frontLinePen = null; frontLinePoint1 = PointF.Empty; frontLinePoint2 = PointF.Empty; } //********************************************************************** //** Check if front line should be redrawn whith the next segment. //********************************************************************** if(drawElements) { frontLinePen = (borderWidth > 1) ? thickBorderPen : thinBorderPen; frontLinePoint1 = polygonPoints[0]; frontLinePoint2 = polygonPoints[1]; } } //********************************************************************** //** Calculate path for selection //********************************************************************** if(resultPath != null) { // Widen all the lines currently in the path if(thickBorderPen != null) { try { resultPath.Widen(thickBorderPen); } catch (OutOfMemoryException) { // GraphicsPath.Widen incorrectly throws OutOfMemoryException // catching here and reacting by not widening } catch (ArgumentException) { } } // Add polygon to the path resultPath.AddPolygon(polygonPoints); } return resultPath; } /// /// Helper method, which indicates if area chart surface should be drawn or not. /// /// Chart area object. /// Series are drawn in reversed order. /// Surface name. /// Visible surfaces of the bounding rectangle. /// Point back color. /// Array of all points. /// First point. /// Second point. /// Indicates that multiple series are painted at the same time (stacked or side-by-side). /// Returns line segment type. /// Function retrns 0, 1 or 2. 0 - Do not draw surface, 1 - draw on the back, 2 - draw in front. static internal int ShouldDrawLineChartSurface( ChartArea area, bool reversedSeriesOrder, SurfaceNames surfaceName, SurfaceNames boundaryRectVisibleSurfaces, Color color, ArrayList points, DataPoint3D firstPoint, DataPoint3D secondPoint, bool multiSeries, ref LineSegmentType lineSegmentType) { int result = 0; Series series = firstPoint.dataPoint.series; // Set active horizontal/vertical axis Axis hAxis = (series.XAxisType == AxisType.Primary) ? area.AxisX : area.AxisX2; double hAxisMin = hAxis.ViewMinimum; double hAxisMax = hAxis.ViewMaximum; //**************************************************************** //** Check if data point and it's neigbours have non-transparent //** colors. //**************************************************************** // Check if point main color has transparency bool transparent = color.A != 255; // Check if points on the left and right side exsit and are transparent bool leftPointVisible = false; bool rightPointVisible = false; if( surfaceName == SurfaceNames.Left ) { // Find Left point DataPoint3D leftPoint = null, leftPointAttr = null; int pointArrayIndex = int.MinValue; if(!reversedSeriesOrder) { leftPoint = ChartGraphics.FindPointByIndex(points, Math.Min(firstPoint.index, secondPoint.index) - 1, (multiSeries) ? secondPoint : null, ref pointArrayIndex); leftPointAttr = ChartGraphics.FindPointByIndex(points, Math.Min(firstPoint.index, secondPoint.index), (multiSeries) ? secondPoint : null, ref pointArrayIndex); } else { leftPoint = ChartGraphics.FindPointByIndex(points, Math.Max(firstPoint.index, secondPoint.index) + 1, (multiSeries) ? secondPoint : null, ref pointArrayIndex); leftPointAttr = leftPoint; } if(leftPoint != null) { if(leftPointAttr.dataPoint.IsEmpty) { if(leftPointAttr.dataPoint.series.EmptyPointStyle.Color == color || leftPointAttr.dataPoint.series.EmptyPointStyle.Color.A == 255) { leftPointVisible = true; } } else { if(leftPointAttr.dataPoint.Color == color || leftPointAttr.dataPoint.Color.A == 255) { leftPointVisible = true; } } // Check if found point is outside the scaleView double xValue = (leftPoint.indexedSeries) ? leftPoint.index : leftPoint.dataPoint.XValue; if(xValue > hAxisMax || xValue < hAxisMin) { DataPoint3D currentPoint = null; if(reversedSeriesOrder) { currentPoint = (firstPoint.index > secondPoint.index) ? firstPoint : secondPoint; } else { currentPoint = (firstPoint.index < secondPoint.index) ? firstPoint : secondPoint; } double currentXValue = (currentPoint.indexedSeries) ? currentPoint.index : currentPoint.dataPoint.XValue; if(currentXValue > hAxisMax || currentXValue < hAxisMin) { leftPointVisible = false; } } } } // Find Right point if( surfaceName == SurfaceNames.Right ) { DataPoint3D rightPoint = null, rightPointAttr = null; int pointArrayIndex = int.MinValue; if(!reversedSeriesOrder) { rightPoint = ChartGraphics.FindPointByIndex(points, Math.Max(firstPoint.index, secondPoint.index) + 1, (multiSeries) ? secondPoint : null, ref pointArrayIndex); rightPointAttr = rightPoint; } else { rightPoint = ChartGraphics.FindPointByIndex(points, Math.Min(firstPoint.index, secondPoint.index) - 1, (multiSeries) ? secondPoint : null, ref pointArrayIndex); rightPointAttr = ChartGraphics.FindPointByIndex(points, Math.Min(firstPoint.index, secondPoint.index), (multiSeries) ? secondPoint : null, ref pointArrayIndex); } if(rightPoint != null) { if(rightPointAttr.dataPoint.IsEmpty) { if(rightPointAttr.dataPoint.series.EmptyPointStyle.Color == color || rightPointAttr.dataPoint.series.EmptyPointStyle.Color.A == 255) { rightPointVisible = true; } } else { if(rightPointAttr.dataPoint.Color == color || rightPointAttr.dataPoint.Color.A == 255) { rightPointVisible = true; } } // Check if found point is outside the scaleView double xValue = (rightPoint.indexedSeries) ? rightPoint.index : rightPoint.dataPoint.XValue; if(xValue > hAxisMax || xValue < hAxisMin) { DataPoint3D currentPoint = null; if(reversedSeriesOrder) { currentPoint = (firstPoint.index > secondPoint.index) ? firstPoint : secondPoint; } else { currentPoint = (firstPoint.index < secondPoint.index) ? firstPoint : secondPoint; } double currentXValue = (currentPoint.indexedSeries) ? currentPoint.index : currentPoint.dataPoint.XValue; if(currentXValue > hAxisMax || currentXValue < hAxisMin) { rightPointVisible = false; } } } } //**************************************************************** //** Get line segment //**************************************************************** if( surfaceName == SurfaceNames.Left && !leftPointVisible) { if(lineSegmentType == LineSegmentType.Middle) { lineSegmentType = LineSegmentType.First; } else if(lineSegmentType == LineSegmentType.Last) { lineSegmentType = LineSegmentType.Single; } } if( surfaceName == SurfaceNames.Right && !rightPointVisible) { if(lineSegmentType == LineSegmentType.Middle) { lineSegmentType = LineSegmentType.Last; } else if(lineSegmentType == LineSegmentType.First) { lineSegmentType = LineSegmentType.Single; } } //**************************************************************** //** Check surfaces visibility //**************************************************************** if( surfaceName == SurfaceNames.Top ) { result = ((boundaryRectVisibleSurfaces & SurfaceNames.Top) == SurfaceNames.Top) ? 2 : 1; } if( surfaceName == SurfaceNames.Bottom ) { result = ((boundaryRectVisibleSurfaces & SurfaceNames.Bottom) == SurfaceNames.Bottom) ? 2 : 1; // Draw invisible bottom surface only if chart is transparent if(result == 1 && !transparent) { result = 0; } } if( surfaceName == SurfaceNames.Front ) { result = ((boundaryRectVisibleSurfaces & SurfaceNames.Front) == SurfaceNames.Front) ? 2 : 1; // Draw invisible front surface only if chart is transparent if(result == 1 && !transparent) { result = 0; } } if( surfaceName == SurfaceNames.Back ) { result = ((boundaryRectVisibleSurfaces & SurfaceNames.Back) == SurfaceNames.Back) ? 2 : 1; // Draw invisible back surface only if chart is transparent if(result == 1 && !transparent) { result = 0; } } if( surfaceName == SurfaceNames.Left ) { result = ((boundaryRectVisibleSurfaces & SurfaceNames.Left) == SurfaceNames.Left) ? 2 : 1; // Draw invisible left surface only if point to the left is transparent if(leftPointVisible) { result = 0; } } if( surfaceName == SurfaceNames.Right ) { result = ((boundaryRectVisibleSurfaces & SurfaceNames.Right) == SurfaceNames.Right) ? 2 : 1; // Draw invisible right surface only if point to the right is transparent if(rightPointVisible) { result = 0; } } return result; } /// /// Helper method which finds point in the list by it's real index. /// /// List of points. /// Required index. /// Neighbor point of the same series. /// Neighbor point index in the array list. /// Data point found. internal static DataPoint3D FindPointByIndex(ArrayList points, int index, DataPoint3D neighborDataPoint, ref int neighborPointIndex) { // Try to look around the neighbor point index if(neighborPointIndex != int.MinValue) { // Try getting the next point if(neighborPointIndex < (points.Count - 2)) { DataPoint3D point = (DataPoint3D)points[neighborPointIndex + 1]; // Check required point index for the first point if( point.index == index && (neighborDataPoint == null || String.Compare(neighborDataPoint.dataPoint.series.Name, point.dataPoint.series.Name, StringComparison.Ordinal) == 0)) { ++neighborPointIndex; return point; } } // Try getting the prev point if(neighborPointIndex > 0) { DataPoint3D point = (DataPoint3D)points[neighborPointIndex - 1]; // Check required point index for the first point if( point.index == index && (neighborDataPoint == null || String.Compare(neighborDataPoint.dataPoint.series.Name, point.dataPoint.series.Name, StringComparison.Ordinal) == 0)) { --neighborPointIndex; return point; } } } // Loop through all points neighborPointIndex = 0; foreach(DataPoint3D point3D in points) { // Check required point index for the first point if(point3D.index == index) { // Check if point belongs to the same series if(neighborDataPoint != null) { if (String.Compare(neighborDataPoint.dataPoint.series.Name, point3D.dataPoint.series.Name, StringComparison.Ordinal) != 0) { ++neighborPointIndex; continue; } } // Point found return (DataPoint3D)point3D; } ++neighborPointIndex; } // Data point was not found return null; } #endregion #region 3D Rectangle drawing methods /// /// Function is used to calculate the coordinates of the 2D rectangle in 3D space /// and either draw it or/and calculate the bounding path for selection. /// /// Position of 2D rectangle. /// Z position of the back side of the 3D rectangle. /// Depth of the 3D rectangle. /// Coordinate transformation matrix. /// LightStyle style (None, Simplistic, Realistic). /// Color of rectangle /// Border Color /// Border Width /// Border Style /// AxisName of operation Drawing, Calculating Path or Both /// Returns elemnt shape path if operationType parameter is set to CalcElementPath, otherwise Null. internal GraphicsPath Fill3DRectangle( RectangleF position, float positionZ, float depth, Matrix3D matrix, LightStyle lightStyle, Color backColor, Color borderColor, int borderWidth, ChartDashStyle borderDashStyle, DrawingOperationTypes operationType) { return Fill3DRectangle( position, positionZ, depth, matrix, lightStyle, backColor, 0f, 0f, borderColor, borderWidth, borderDashStyle, BarDrawingStyle.Default, false, operationType); } /// /// Function is used to calculate the coordinates of the 2D rectangle in 3D space /// and either draw it or/and calculate the bounding path for selection. /// /// Position of 2D rectangle. /// Z position of the back side of the 3D rectangle. /// Depth of the 3D rectangle. /// Coordinate transformation matrix. /// LightStyle style (None, Simplistic, Realistic). /// Color of rectangle /// Top (or right in bar chart) darkening effect. /// Bottom (or left in bar chart) darkening effect. /// Border Color /// Border Width /// Border Style /// Bar drawing style. /// Defines if bar is vertical or horizontal. /// AxisName of operation Drawing, Calculating Path or Both /// Returns elemnt shape path if operationType parameter is set to CalcElementPath, otherwise Null. internal GraphicsPath Fill3DRectangle( RectangleF position, float positionZ, float depth, Matrix3D matrix, LightStyle lightStyle, Color backColor, float topRightDarkening, float bottomLeftDarkening, Color borderColor, int borderWidth, ChartDashStyle borderDashStyle, BarDrawingStyle barDrawingStyle, bool veticalOrientation, DrawingOperationTypes operationType) { // Check if special drawing is required if(barDrawingStyle == BarDrawingStyle.Cylinder) { // Draw as 3D cylinder return Fill3DRectangleAsCylinder( position, positionZ, depth, matrix, lightStyle, backColor, topRightDarkening, bottomLeftDarkening, borderColor, borderWidth, borderDashStyle, veticalOrientation, operationType); } // Declare variables Point3D[] cubePoints = new Point3D[8]; GraphicsPath resultPath = ((operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) ? new GraphicsPath() : null; // Front Side cubePoints[0] = new Point3D( position.X, position.Y, positionZ + depth ); cubePoints[1] = new Point3D( position.X, position.Bottom, positionZ + depth ); cubePoints[2] = new Point3D( position.Right, position.Bottom, positionZ + depth ); cubePoints[3] = new Point3D( position.Right, position.Y, positionZ + depth ); // Back Side cubePoints[4] = new Point3D( position.X, position.Y, positionZ ); cubePoints[5] = new Point3D( position.X, position.Bottom, positionZ ); cubePoints[6] = new Point3D( position.Right, position.Bottom, positionZ ); cubePoints[7] = new Point3D( position.Right, position.Y, positionZ ); // Tranform cube coordinates matrix.TransformPoints( cubePoints ); // For lightStyle style Non, Border color always exist. if( lightStyle == LightStyle.None && (borderWidth == 0 || borderDashStyle == ChartDashStyle.NotSet || borderColor == Color.Empty) ) { borderColor = ChartGraphics.GetGradientColor( backColor, Color.Black, 0.5 ); } // Get surface colors Color frontLightColor, leftLightColor, topLightColor, backLightColor, rightLightColor, bottomLightColor; matrix.GetLight( backColor, out frontLightColor, out backLightColor, out leftLightColor, out rightLightColor, out topLightColor, out bottomLightColor ); // Darken colors by specified values if(topRightDarkening != 0f) { if(veticalOrientation) { topLightColor = ChartGraphics.GetGradientColor(topLightColor, Color.Black, topRightDarkening); } else { rightLightColor = ChartGraphics.GetGradientColor(rightLightColor, Color.Black, topRightDarkening); } } if(bottomLeftDarkening != 0f) { if(veticalOrientation) { bottomLightColor = ChartGraphics.GetGradientColor(bottomLightColor, Color.Black, bottomLeftDarkening); } else { leftLightColor = ChartGraphics.GetGradientColor(leftLightColor, Color.Black, bottomLeftDarkening); } } // Check visible surfaces SurfaceNames visibleSurfaces = GetVisibleSurfacesWithPerspective(position,positionZ,depth,matrix); // Draw all invisible surfaces first (if semi-transparent color is used) for(int drawVisible = 0; drawVisible <= 1; drawVisible++) { // Do not draw invisible surfaces for solid colors if(drawVisible == 0 && backColor.A == 255) { continue; } // Check visibility of all surfaces and draw them for(int surfaceIndex = (int)SurfaceNames.Front; surfaceIndex <= (int)SurfaceNames.Bottom; surfaceIndex *= 2) { SurfaceNames currentSurface = (SurfaceNames)surfaceIndex; // If width, height or depth of the cube (3DRectangle) is zero graphical path // should contain only one surface with 4 points. if(depth == 0.0 && currentSurface != SurfaceNames.Front) { continue; } if(position.Width == 0.0 && currentSurface != SurfaceNames.Left && currentSurface != SurfaceNames.Right) { continue; } if(position.Height == 0.0 && currentSurface != SurfaceNames.Top && currentSurface != SurfaceNames.Bottom) { continue; } // Check if surface is visible or semi-transparent color is used bool isVisible = (visibleSurfaces & currentSurface) != 0; if(isVisible && drawVisible == 1 || !isVisible && drawVisible == 0) { // Fill surface coordinates and color PointF [] pointsSurface = new PointF[4]; Color surfaceColor = backColor; switch(currentSurface) { case(SurfaceNames.Front): surfaceColor = frontLightColor; pointsSurface[0] = new PointF(cubePoints[0].X, cubePoints[0].Y); pointsSurface[1] = new PointF(cubePoints[1].X, cubePoints[1].Y); pointsSurface[2] = new PointF(cubePoints[2].X, cubePoints[2].Y); pointsSurface[3] = new PointF(cubePoints[3].X, cubePoints[3].Y); break; case(SurfaceNames.Back): surfaceColor = backLightColor; pointsSurface[0] = new PointF(cubePoints[4].X, cubePoints[4].Y); pointsSurface[1] = new PointF(cubePoints[5].X, cubePoints[5].Y); pointsSurface[2] = new PointF(cubePoints[6].X, cubePoints[6].Y); pointsSurface[3] = new PointF(cubePoints[7].X, cubePoints[7].Y); break; case(SurfaceNames.Left): surfaceColor = leftLightColor; pointsSurface[0] = new PointF(cubePoints[0].X, cubePoints[0].Y); pointsSurface[1] = new PointF(cubePoints[1].X, cubePoints[1].Y); pointsSurface[2] = new PointF(cubePoints[5].X, cubePoints[5].Y); pointsSurface[3] = new PointF(cubePoints[4].X, cubePoints[4].Y); break; case(SurfaceNames.Right): surfaceColor = rightLightColor; pointsSurface[0] = new PointF(cubePoints[3].X, cubePoints[3].Y); pointsSurface[1] = new PointF(cubePoints[2].X, cubePoints[2].Y); pointsSurface[2] = new PointF(cubePoints[6].X, cubePoints[6].Y); pointsSurface[3] = new PointF(cubePoints[7].X, cubePoints[7].Y); break; case(SurfaceNames.Top): surfaceColor = topLightColor; pointsSurface[0] = new PointF(cubePoints[0].X, cubePoints[0].Y); pointsSurface[1] = new PointF(cubePoints[3].X, cubePoints[3].Y); pointsSurface[2] = new PointF(cubePoints[7].X, cubePoints[7].Y); pointsSurface[3] = new PointF(cubePoints[4].X, cubePoints[4].Y); break; case(SurfaceNames.Bottom): surfaceColor = bottomLightColor; pointsSurface[0] = new PointF(cubePoints[1].X, cubePoints[1].Y); pointsSurface[1] = new PointF(cubePoints[2].X, cubePoints[2].Y); pointsSurface[2] = new PointF(cubePoints[6].X, cubePoints[6].Y); pointsSurface[3] = new PointF(cubePoints[5].X, cubePoints[5].Y); break; } // Covert coordinates to absolute for(int pointIndex = 0; pointIndex < pointsSurface.Length; pointIndex++) { pointsSurface[pointIndex] = GetAbsolutePoint(pointsSurface[pointIndex]); } // Draw surface if( (operationType & DrawingOperationTypes.DrawElement) == DrawingOperationTypes.DrawElement) { // Draw only completly visible surfaces if((visibleSurfaces & currentSurface) != 0) { using (Brush brush = new SolidBrush(surfaceColor)) { FillPolygon(brush, pointsSurface); } // Check if any additional drawing should be done if(currentSurface == SurfaceNames.Front && barDrawingStyle != BarDrawingStyle.Default && barDrawingStyle != BarDrawingStyle.Cylinder) { this.DrawBarStyleGradients(matrix, barDrawingStyle, position, positionZ, depth, veticalOrientation); } } // Draw surface border using (Pen pen = new Pen(borderColor, borderWidth)) { pen.DashStyle = GetPenStyle(borderDashStyle); if (lightStyle != LightStyle.None && (borderWidth == 0 || borderDashStyle == ChartDashStyle.NotSet || borderColor == Color.Empty)) { // Draw line of the same color inside the bar pen.Color = surfaceColor; pen.Width = 1; pen.Alignment = PenAlignment.Inset; } pen.StartCap = LineCap.Round; pen.EndCap = LineCap.Round; DrawLine(pen, pointsSurface[0], pointsSurface[1]); DrawLine(pen, pointsSurface[1], pointsSurface[2]); DrawLine(pen, pointsSurface[2], pointsSurface[3]); DrawLine(pen, pointsSurface[3], pointsSurface[0]); } } // Add surface coordinate to the path if( (operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) { // Only if surface is completly visible if((visibleSurfaces & currentSurface) != 0) { resultPath.SetMarkers(); resultPath.AddPolygon(pointsSurface); } } } } } return resultPath; } /// /// Draws special bar style effect on the front surface of the bar. /// /// Drawing matrix. /// Bar drawing style. /// Position in relative coordinates /// Z position. /// Depth. /// Defines if bar is vertical or horizontal. private void DrawBarStyleGradients( Matrix3D matrix, BarDrawingStyle barDrawingStyle, RectangleF position, float positionZ, float depth, bool isVertical) { if(barDrawingStyle == BarDrawingStyle.Wedge) { // Calculate wedge size to fit the rectangle RectangleF positionAbs = GetAbsoluteRectangle(position); float size = (isVertical) ? positionAbs.Width / 2f : positionAbs.Height / 2f; if(isVertical && 2f * size > positionAbs.Height) { size = positionAbs.Height/2f; } if(!isVertical && 2f * size > positionAbs.Width) { size = positionAbs.Width/2f; } SizeF sizeRel = GetRelativeSize(new SizeF(size, size)); // Make 3D convertion of the key points Point3D[] gradientPoints = new Point3D[6]; gradientPoints[0] = new Point3D( position.Left, position.Top, positionZ + depth ); gradientPoints[1] = new Point3D( position.Left, position.Bottom, positionZ + depth ); gradientPoints[2] = new Point3D( position.Right, position.Bottom, positionZ + depth ); gradientPoints[3] = new Point3D( position.Right, position.Top, positionZ + depth ); if(isVertical) { gradientPoints[4] = new Point3D( position.X + position.Width / 2f, position.Top + sizeRel.Height, positionZ + depth ); gradientPoints[5] = new Point3D( position.X + position.Width / 2f, position.Bottom - sizeRel.Height, positionZ + depth ); } else { gradientPoints[4] = new Point3D( position.X + sizeRel.Width, position.Top + position.Height / 2f, positionZ + depth ); gradientPoints[5] = new Point3D( position.Right - sizeRel.Width, position.Top + position.Height / 2f, positionZ + depth ); } // Tranform cube coordinates matrix.TransformPoints( gradientPoints ); // Convert points to absolute PointF [] gradientPointsAbs = new PointF[6]; for(int index = 0; index < gradientPoints.Length; index++) { gradientPointsAbs[index] = GetAbsolutePoint(gradientPoints[index].PointF); } // Draw left/bottom shadow using(GraphicsPath path = new GraphicsPath()) { if(isVertical) { path.AddLine(gradientPointsAbs[4], gradientPointsAbs[5]); path.AddLine(gradientPointsAbs[5], gradientPointsAbs[2]); path.AddLine(gradientPointsAbs[2], gradientPointsAbs[3]); } else { path.AddLine(gradientPointsAbs[4], gradientPointsAbs[5]); path.AddLine(gradientPointsAbs[5], gradientPointsAbs[2]); path.AddLine(gradientPointsAbs[2], gradientPointsAbs[1]); } path.CloseAllFigures(); // Create brush and fill path using(SolidBrush brush = new SolidBrush(Color.FromArgb(90, Color.Black))) { this.FillPath(brush, path); } } // Draw top/right triangle using(GraphicsPath path = new GraphicsPath()) { if(isVertical) { path.AddLine(gradientPointsAbs[0], gradientPointsAbs[4]); path.AddLine(gradientPointsAbs[4], gradientPointsAbs[3]); } else { path.AddLine(gradientPointsAbs[3], gradientPointsAbs[5]); path.AddLine(gradientPointsAbs[5], gradientPointsAbs[2]); } // Create brush and fill path using(SolidBrush brush = new SolidBrush(Color.FromArgb(50, Color.Black))) { // Fill shadow path on the left-bottom side of the bar this.FillPath(brush, path); // Draw Lines using(Pen penDark = new Pen(Color.FromArgb(20, Color.Black), 1)) { this.DrawPath(penDark, path); this.DrawLine( penDark, gradientPointsAbs[4], gradientPointsAbs[5]); } // Draw Lines using(Pen pen = new Pen(Color.FromArgb(40, Color.White), 1)) { this.DrawPath(pen, path); this.DrawLine( pen, gradientPointsAbs[4], gradientPointsAbs[5]); } } } // Draw bottom/left triangle using(GraphicsPath path = new GraphicsPath()) { if(isVertical) { path.AddLine(gradientPointsAbs[1], gradientPointsAbs[5]); path.AddLine(gradientPointsAbs[5], gradientPointsAbs[2]); } else { path.AddLine(gradientPointsAbs[0], gradientPointsAbs[4]); path.AddLine(gradientPointsAbs[4], gradientPointsAbs[1]); } // Create brush using(SolidBrush brush = new SolidBrush(Color.FromArgb(50, Color.Black))) { // Fill shadow path on the left-bottom side of the bar this.FillPath(brush, path); // Draw edges using(Pen penDark = new Pen(Color.FromArgb(20, Color.Black), 1)) { this.DrawPath(penDark, path); } using(Pen pen = new Pen(Color.FromArgb(40, Color.White), 1)) { this.DrawPath(pen, path); } } } } else if(barDrawingStyle == BarDrawingStyle.LightToDark) { // Calculate width of shadows used to create the effect RectangleF positionAbs = GetAbsoluteRectangle(position); float shadowSizeAbs = 5f; if(positionAbs.Width < 6f || positionAbs.Height < 6f) { shadowSizeAbs = 2f; } else if(positionAbs.Width < 15f || positionAbs.Height < 15f) { shadowSizeAbs = 3f; } SizeF shadowSizeRel = GetRelativeSize(new SizeF(shadowSizeAbs, shadowSizeAbs)); // Calculate gradient position RectangleF gradientRect = position; gradientRect.Inflate(-shadowSizeRel.Width, -shadowSizeRel.Height); if(isVertical) { gradientRect.Height = (float)Math.Floor(gradientRect.Height / 3f); } else { gradientRect.X = gradientRect.Right - (float)Math.Floor(gradientRect.Width / 3f); gradientRect.Width = (float)Math.Floor(gradientRect.Width / 3f); } // Top gradient Point3D[] gradientPoints = new Point3D[4]; gradientPoints[0] = new Point3D( gradientRect.Left, gradientRect.Top, positionZ + depth ); gradientPoints[1] = new Point3D( gradientRect.Left, gradientRect.Bottom, positionZ + depth ); gradientPoints[2] = new Point3D( gradientRect.Right, gradientRect.Bottom, positionZ + depth ); gradientPoints[3] = new Point3D( gradientRect.Right, gradientRect.Top, positionZ + depth ); // Tranform cube coordinates matrix.TransformPoints( gradientPoints ); // Convert points to absolute PointF [] gradientPointsAbs = new PointF[4]; for(int index = 0; index < gradientPoints.Length; index++) { gradientPointsAbs[index] = GetAbsolutePoint(gradientPoints[index].PointF); } // Create and draw top path using(GraphicsPath path = new GraphicsPath()) { path.AddPolygon(gradientPointsAbs); RectangleF bounds = path.GetBounds(); bounds.Width += 1f; bounds.Height += 1f; // Create brush if(bounds.Width > 0f && bounds.Height > 0f) { using(LinearGradientBrush topBrush = new LinearGradientBrush( bounds, (!isVertical) ? Color.Transparent : Color.FromArgb(120, Color.White), (!isVertical) ? Color.FromArgb(120, Color.White) : Color.Transparent, (isVertical) ? LinearGradientMode.Vertical : LinearGradientMode.Horizontal)) { // Fill shadow path on the top side of the bar this.FillPath(topBrush, path); } } } // Calculate gradient position for the bottom gradient gradientRect = position; gradientRect.Inflate(-shadowSizeRel.Width, -shadowSizeRel.Height); if(isVertical) { gradientRect.Y = gradientRect.Bottom - (float)Math.Floor(gradientRect.Height / 3f); gradientRect.Height = (float)Math.Floor(gradientRect.Height / 3f); } else { gradientRect.Width = (float)Math.Floor(gradientRect.Width / 3f); } // Top gradient gradientPoints = new Point3D[4]; gradientPoints[0] = new Point3D( gradientRect.Left, gradientRect.Top, positionZ + depth ); gradientPoints[1] = new Point3D( gradientRect.Left, gradientRect.Bottom, positionZ + depth ); gradientPoints[2] = new Point3D( gradientRect.Right, gradientRect.Bottom, positionZ + depth ); gradientPoints[3] = new Point3D( gradientRect.Right, gradientRect.Top, positionZ + depth ); // Tranform cube coordinates matrix.TransformPoints( gradientPoints ); // Convert points to absolute gradientPointsAbs = new PointF[4]; for(int index = 0; index < gradientPoints.Length; index++) { gradientPointsAbs[index] = GetAbsolutePoint(gradientPoints[index].PointF); } // Create and draw top path using(GraphicsPath path = new GraphicsPath()) { path.AddPolygon(gradientPointsAbs); RectangleF bounds = path.GetBounds(); bounds.Width += 1f; bounds.Height += 1f; // Create brush if(bounds.Width > 0f && bounds.Height > 0f) { using(LinearGradientBrush topBrush = new LinearGradientBrush( bounds, (isVertical) ? Color.Transparent : Color.FromArgb(80, Color.Black), (isVertical) ? Color.FromArgb(80, Color.Black) : Color.Transparent, (isVertical) ? LinearGradientMode.Vertical : LinearGradientMode.Horizontal)) { // Fill shadow path on the top side of the bar this.FillPath(topBrush, path); } } } } else if(barDrawingStyle == BarDrawingStyle.Emboss) { // Calculate width of shadows used to create the effect RectangleF positionAbs = GetAbsoluteRectangle(position); float shadowSizeAbs = 4f; if(positionAbs.Width < 6f || positionAbs.Height < 6f) { shadowSizeAbs = 2f; } else if(positionAbs.Width < 15f || positionAbs.Height < 15f) { shadowSizeAbs = 3f; } SizeF shadowSizeRel = GetRelativeSize(new SizeF(shadowSizeAbs, shadowSizeAbs)); // Left/top Side Point3D[] gradientPoints = new Point3D[6]; gradientPoints[0] = new Point3D( position.Left, position.Bottom, positionZ + depth ); gradientPoints[1] = new Point3D( position.Left, position.Top, positionZ + depth ); gradientPoints[2] = new Point3D( position.Right, position.Top, positionZ + depth ); gradientPoints[3] = new Point3D( position.Right - shadowSizeRel.Width, position.Top + shadowSizeRel.Height, positionZ + depth ); gradientPoints[4] = new Point3D( position.Left + shadowSizeRel.Width, position.Top + shadowSizeRel.Height, positionZ + depth ); gradientPoints[5] = new Point3D( position.Left + shadowSizeRel.Width, position.Bottom - shadowSizeRel.Height, positionZ + depth ); // Tranform cube coordinates matrix.TransformPoints( gradientPoints ); // Convert points to absolute PointF [] gradientPointsAbs = new PointF[6]; for(int index = 0; index < gradientPoints.Length; index++) { gradientPointsAbs[index] = GetAbsolutePoint(gradientPoints[index].PointF); } // Create and draw left/top path using(GraphicsPath path = new GraphicsPath()) { path.AddPolygon(gradientPointsAbs); // Create brush using(SolidBrush leftTopBrush = new SolidBrush(Color.FromArgb(100, Color.White))) { // Fill shadow path on the left-bottom side of the bar this.FillPath(leftTopBrush, path); } } // Right/bottom Side gradientPoints[0] = new Point3D( position.Right, position.Top, positionZ + depth ); gradientPoints[1] = new Point3D( position.Right, position.Bottom, positionZ + depth ); gradientPoints[2] = new Point3D( position.Left, position.Bottom, positionZ + depth ); gradientPoints[3] = new Point3D( position.Left + shadowSizeRel.Width, position.Bottom - shadowSizeRel.Height, positionZ + depth ); gradientPoints[4] = new Point3D( position.Right - shadowSizeRel.Width, position.Bottom - shadowSizeRel.Height, positionZ + depth ); gradientPoints[5] = new Point3D( position.Right - shadowSizeRel.Width, position.Top + shadowSizeRel.Height, positionZ + depth ); // Tranform cube coordinates matrix.TransformPoints( gradientPoints ); // Convert points to absolute for(int index = 0; index < gradientPoints.Length; index++) { gradientPointsAbs[index] = GetAbsolutePoint(gradientPoints[index].PointF); } // Create and draw left/top path using(GraphicsPath path = new GraphicsPath()) { path.AddPolygon(gradientPointsAbs); // Create brush using(SolidBrush bottomRightBrush = new SolidBrush(Color.FromArgb(80, Color.Black))) { // Fill shadow path on the left-bottom side of the bar this.FillPath(bottomRightBrush, path); } } } } #endregion #region 3D markers drawing methods /// /// Draw marker using absolute coordinates of the center. /// /// Coordinates transformation matrix. /// LightStyle style (None, Simplistic, Realistic). /// Z position of the 3D marker center. /// Coordinates of the center. /// Marker style. /// Marker size. /// Marker color. /// Marker border color. /// Marker border size. /// Marker image name. /// Marker image transparent color. /// Marker shadow size. /// Marker shadow color. /// Rectangle to which marker image should be scaled. /// AxisName of operation Drawing, Calculating Path or Both /// Returns elemnt shape path if operationType parameter is set to ElementPath, otherwise Null. internal GraphicsPath DrawMarker3D( Matrix3D matrix, LightStyle lightStyle, float positionZ, PointF point, MarkerStyle markerStyle, int markerSize, Color markerColor, Color markerBorderColor, int markerBorderSize, string markerImage, Color markerImageTransparentColor, int shadowSize, Color shadowColor, RectangleF imageScaleRect, DrawingOperationTypes operationType ) { ChartGraphics graph = (ChartGraphics)this; GraphicsPath resultPath = ((operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) ? new GraphicsPath() : null; //************************************************************ //** Transform marker position in 3D space //************************************************************ // Get projection coordinates Point3D[] marker3DPosition = new Point3D[1]; marker3DPosition[0] = new Point3D(point.X, point.Y, positionZ); // Transform coordinates of the marker center matrix.TransformPoints(marker3DPosition); PointF markerRotatedPosition = marker3DPosition[0].PointF; // Translate to absolute coordinates markerRotatedPosition = graph.GetAbsolutePoint(markerRotatedPosition); //************************************************************ //** For those markers that do not have a 3D version - draw the same as in 2D //************************************************************ if(markerImage.Length > 0 || !(markerStyle == MarkerStyle.Circle || markerStyle == MarkerStyle.Square) ) { // Call 2D version of the method if( (operationType & DrawingOperationTypes.DrawElement) == DrawingOperationTypes.DrawElement) { graph.DrawMarkerAbs(markerRotatedPosition, markerStyle, markerSize, markerColor, markerBorderColor, markerBorderSize, markerImage, markerImageTransparentColor, shadowSize, shadowColor, imageScaleRect, false); } // Prepare marker path if( (operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) { RectangleF rect = RectangleF.Empty; rect.X = markerRotatedPosition.X - ((float)markerSize)/2F; rect.Y = markerRotatedPosition.Y - ((float)markerSize)/2F; rect.Width = markerSize; rect.Height = markerSize; resultPath.AddRectangle(rect); } return resultPath; } //************************************************************ //** Draw marker //************************************************************ // Check if marker properties are set if (markerStyle != MarkerStyle.None && markerSize > 0 && markerColor != Color.Empty) { // Create solid color brush using (SolidBrush brush = new SolidBrush(markerColor)) { // Calculate marker rectangle RectangleF rect = RectangleF.Empty; rect.X = markerRotatedPosition.X - ((float)markerSize) / 2F; rect.Y = markerRotatedPosition.Y - ((float)markerSize) / 2F; rect.Width = markerSize; rect.Height = markerSize; // Calculate relative marker size SizeF markerRelativeSize = graph.GetRelativeSize(new SizeF(markerSize, markerSize)); // Draw marker depending on style switch (markerStyle) { case (MarkerStyle.Circle): { if ((operationType & DrawingOperationTypes.DrawElement) == DrawingOperationTypes.DrawElement) { // Draw marker shadow if (shadowSize != 0 && shadowColor != Color.Empty) { if (!graph.softShadows) { using (Brush shadowBrush = new SolidBrush((shadowColor.A != 255) ? shadowColor : Color.FromArgb(markerColor.A / 2, shadowColor))) { RectangleF shadowRect = rect; shadowRect.X += shadowSize; shadowRect.Y += shadowSize; graph.FillEllipse(shadowBrush, shadowRect); } } else { // Add circle to the graphics path using (GraphicsPath path = new GraphicsPath()) { path.AddEllipse(rect.X + shadowSize - 1, rect.Y + shadowSize - 1, rect.Width + 2, rect.Height + 2); // Create path brush using (PathGradientBrush shadowBrush = new PathGradientBrush(path)) { shadowBrush.CenterColor = shadowColor; // Set the color along the entire boundary of the path Color[] colors = { Color.Transparent }; shadowBrush.SurroundColors = colors; shadowBrush.CenterPoint = new PointF(markerRotatedPosition.X, markerRotatedPosition.Y); // Define brush focus scale PointF focusScale = new PointF(1 - 2f * shadowSize / rect.Width, 1 - 2f * shadowSize / rect.Height); if (focusScale.X < 0) { focusScale.X = 0; } if (focusScale.Y < 0) { focusScale.Y = 0; } shadowBrush.FocusScales = focusScale; // Draw shadow graph.FillPath(shadowBrush, path); } } } } // Create path gradient brush using (GraphicsPath brushPath = new GraphicsPath()) { RectangleF rectLightCenter = new RectangleF(rect.Location, rect.Size); rectLightCenter.Inflate(rectLightCenter.Width / 4f, rectLightCenter.Height / 4f); brushPath.AddEllipse(rectLightCenter); using (PathGradientBrush circleBrush = new PathGradientBrush(brushPath)) { circleBrush.CenterColor = ChartGraphics.GetGradientColor(markerColor, Color.White, 0.85); circleBrush.SurroundColors = new Color[] { markerColor }; // Calculate the center point of the gradient Point3D[] centerPoint = new Point3D[] { new Point3D(point.X, point.Y, positionZ + markerRelativeSize.Width) }; matrix.TransformPoints(centerPoint); centerPoint[0].PointF = graph.GetAbsolutePoint(centerPoint[0].PointF); circleBrush.CenterPoint = centerPoint[0].PointF; // Draw circle (sphere) graph.FillEllipse(circleBrush, rect); graph.DrawEllipse(new Pen(markerBorderColor, markerBorderSize), rect); } } } // Prepare marker path if ((operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) { resultPath.AddEllipse(rect); } break; } case (MarkerStyle.Square): { // Calculate marker non-rotated rectangle RectangleF rectNonRotated = RectangleF.Empty; rectNonRotated.X = point.X - ((float)markerRelativeSize.Width) / 2F; rectNonRotated.Y = point.Y - ((float)markerRelativeSize.Height) / 2F; rectNonRotated.Width = markerRelativeSize.Width; rectNonRotated.Height = markerRelativeSize.Height; // Draw 3D bar resultPath = this.Fill3DRectangle( rectNonRotated, positionZ - markerRelativeSize.Width / 2f, markerRelativeSize.Width, matrix, lightStyle, markerColor, markerBorderColor, markerBorderSize, ChartDashStyle.Solid, operationType); break; } default: { throw (new InvalidOperationException(SR.ExceptionGraphics3DMarkerStyleUnknown)); } } } } return resultPath; } #endregion #region 3D cube surface visibility methods /// /// Returns visible surfaces of the 3D cube. /// /// 2D rectangle coordinates. /// Z coordinate of the back side of the cube. /// Cube depth. /// Coordinate transformation matrix. /// Visible surfaces. internal SurfaceNames GetVisibleSurfaces( RectangleF position, float positionZ, float depth, Matrix3D matrix ) { // Check if perspective is used if(matrix.Perspective != 0) { // More sofisticated algorithm must be used for visibility detection. return GetVisibleSurfacesWithPerspective(position, positionZ, depth, matrix); } // Front surface is always visible SurfaceNames result = SurfaceNames.Front; // Left and Right surfaces depend on the Y axis angle if (matrix.AngleY > 0) { result |= SurfaceNames.Right; } else if (matrix.AngleY < 0) { result |= SurfaceNames.Left; } // Top and Bottom surfaces depend on the X axis angle if (matrix.AngleX > 0) { result |= SurfaceNames.Top; } else if (matrix.AngleX < 0) { result |= SurfaceNames.Bottom; } return result; } /// /// Returns visible surfaces of the 3D cube. /// This method takes in consideration the perspective. /// /// 2D rectangle coordinates. /// Z coordinate of the back side of the cube. /// Cube depth. /// Coordinate transformation matrix. /// Visible surfaces. internal SurfaceNames GetVisibleSurfacesWithPerspective( RectangleF position, float positionZ, float depth, Matrix3D matrix) { // Create cube coordinates in 3D space Point3D[] cubePoints = new Point3D[8]; // Front Side cubePoints[0] = new Point3D( position.X, position.Y, positionZ + depth ); cubePoints[1] = new Point3D( position.X, position.Bottom, positionZ + depth ); cubePoints[2] = new Point3D( position.Right, position.Bottom, positionZ + depth ); cubePoints[3] = new Point3D( position.Right, position.Y, positionZ + depth ); // Back Side cubePoints[4] = new Point3D( position.X, position.Y, positionZ ); cubePoints[5] = new Point3D( position.X, position.Bottom, positionZ ); cubePoints[6] = new Point3D( position.Right, position.Bottom, positionZ ); cubePoints[7] = new Point3D( position.Right, position.Y, positionZ ); // Tranform coordinates matrix.TransformPoints( cubePoints ); // Detect surfaces visibility return GetVisibleSurfacesWithPerspective(cubePoints); } /// /// Returns visible surfaces of the 3D cube. /// This method takes in consideration the perspective. /// /// Array of 8 points which define the cube. /// Visible surfaces. internal SurfaceNames GetVisibleSurfacesWithPerspective(Point3D[] cubePoints) { // Check imput array size if(cubePoints.Length != 8) { throw (new ArgumentException(SR.ExceptionGraphics3DCoordinatesInvalid, "cubePoints")); } // Detect surfaces visibility SurfaceNames result = 0; // Check the front side if(IsSurfaceVisible(cubePoints[0],cubePoints[3],cubePoints[2])) { result |= SurfaceNames.Front; } // Check the back side if(IsSurfaceVisible(cubePoints[4],cubePoints[5],cubePoints[6])) { result |= SurfaceNames.Back; } // Check the left side if(IsSurfaceVisible(cubePoints[0],cubePoints[1],cubePoints[5])) { result |= SurfaceNames.Left; } // Check the right side if(IsSurfaceVisible(cubePoints[3],cubePoints[7],cubePoints[6])) { result |= SurfaceNames.Right; } // Check the top side if(IsSurfaceVisible(cubePoints[4],cubePoints[7],cubePoints[3])) { result |= SurfaceNames.Top; } // Check the bottom side if(IsSurfaceVisible(cubePoints[1],cubePoints[2],cubePoints[6])) { result |= SurfaceNames.Bottom; } return result; } /// /// Checks surface visibility using 3 points and clockwise points index rotation. /// /// First point. /// Second point. /// Third point. /// True if surface is visible internal static bool IsSurfaceVisible( Point3D first, Point3D second, Point3D tree ) { // Check if points are oriented clocwise in 2D projection. // If points are clockwise the surface is visible. float a = ( first.Y - second.Y ) / ( first.X - second.X ); float b = first.Y - a * first.X; if( first.X == second.X ) { if( first.Y > second.Y ) { if( tree.X > first.X ) { return true; } else { return false; } } else { if( tree.X > first.X ) { return false; } else { return true; } } } else if ( first.X < second.X ) { if( tree.Y < a * tree.X + b ) { return false; } else { return true; } } else { if( tree.Y <= a * tree.X + b ) { return true; } else { return false; } } } #endregion #region Line intersection helper method /// /// Gets intersection point of two lines /// /// First X value of first line. /// First Y value of first line. /// Second X value of first line. /// Second Y value of first line. /// First X value of second line. /// First Y value of second line. /// Second X value of second line. /// Second Y value of second line. /// Intersection coordinates. internal static PointF GetLinesIntersection(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) { PointF result = PointF.Empty; // Special case for horizontal & vertical lines if(x1 == x2 && y3 == y4) { result.X = x1; result.Y = y3; return result; } else if(y1 == y2 && x3 == x4) { result.X = x3; result.Y = y1; return result; } else if(x1 == x2) { result.X = x1; result.Y = (result.X - x3) * (y4 - y3); result.Y /= x4 - x3; result.Y += y3; return result; } else if(x3 == x4) { result.X = x3; result.Y = (result.X - x1) * (y2 - y1); result.Y /= x2 - x1; result.Y += y1; return result; } // Calculate line eqaution float a1 = ( y1 - y2 ) / ( x1 - x2 ); float b1 = y1 - a1 * x1; float a2 = ( y3 - y4 ) / ( x3 - x4 ); float b2 = y3 - a2 * x3; // Calculate intersection point result.X = (b2 - b1)/(a1 - a2); result.Y = a1*result.X + b1; return result; } #endregion #region 3D Cylinder drawing methods /// /// Function is used to calculate the coordinates of the 2D rectangle in 3D space /// and either draw it or/and calculate the bounding path for selection. /// /// Position of 2D rectangle. /// Z position of the back side of the 3D rectangle. /// Depth of the 3D rectangle. /// Coordinate transformation matrix. /// LightStyle style (None, Simplistic, Realistic). /// Color of rectangle /// Top (or right in bar chart) darkening effect. /// Bottom (or left in bar chart) darkening effect. /// Border Color /// Border Width /// Border Style /// Defines if bar is vertical or horizontal. /// AxisName of operation Drawing, Calculating Path or Both /// Returns elemnt shape path if operationType parameter is set to CalcElementPath, otherwise Null. internal GraphicsPath Fill3DRectangleAsCylinder( RectangleF position, float positionZ, float depth, Matrix3D matrix, LightStyle lightStyle, Color backColor, float topRightDarkening, float bottomLeftDarkening, Color borderColor, int borderWidth, ChartDashStyle borderDashStyle, bool veticalOrientation, DrawingOperationTypes operationType) { Point3D[] cubePoints = new Point3D[8]; GraphicsPath resultPath = ((operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) ? new GraphicsPath() : null; //******************************************************* //** Define coordinates to draw the cylinder //******************************************************* if(veticalOrientation) { cubePoints[0] = new Point3D( position.X, position.Y, positionZ + depth / 2f ); cubePoints[1] = new Point3D( position.X, position.Bottom, positionZ + depth / 2f ); cubePoints[2] = new Point3D( position.Right, position.Bottom, positionZ + depth / 2f ); cubePoints[3] = new Point3D( position.Right, position.Y, positionZ + depth / 2f ); float middleXValue = position.X + position.Width / 2f; cubePoints[4] = new Point3D( middleXValue, position.Y, positionZ + depth ); cubePoints[5] = new Point3D( middleXValue, position.Bottom, positionZ + depth ); cubePoints[6] = new Point3D( middleXValue, position.Bottom, positionZ ); cubePoints[7] = new Point3D( middleXValue, position.Y, positionZ ); } else { cubePoints[0] = new Point3D( position.Right, position.Y, positionZ + depth / 2f ); cubePoints[1] = new Point3D( position.X, position.Y, positionZ + depth / 2f ); cubePoints[2] = new Point3D( position.X, position.Bottom, positionZ + depth / 2f ); cubePoints[3] = new Point3D( position.Right, position.Bottom, positionZ + depth / 2f ); float middleYValue = position.Y + position.Height / 2f; cubePoints[4] = new Point3D( position.Right, middleYValue, positionZ + depth ); cubePoints[5] = new Point3D( position.X, middleYValue, positionZ + depth ); cubePoints[6] = new Point3D( position.X, middleYValue, positionZ ); cubePoints[7] = new Point3D( position.Right, middleYValue, positionZ ); } // Tranform cylinder coordinates matrix.TransformPoints( cubePoints ); // Covert coordinates to absolute for(int pointIndex = 0; pointIndex < cubePoints.Length; pointIndex++) { cubePoints[pointIndex].PointF = GetAbsolutePoint(cubePoints[pointIndex].PointF); } //******************************************************* //** Get cylinder colors. //******************************************************* if( lightStyle == LightStyle.None && (borderWidth == 0 || borderDashStyle == ChartDashStyle.NotSet || borderColor == Color.Empty) ) { borderColor = ChartGraphics.GetGradientColor( backColor, Color.Black, 0.5 ); } // Get surface colors Color frontLightColor, leftLightColor, topLightColor, backLightColor, rightLightColor, bottomLightColor; matrix.GetLight( backColor, out frontLightColor, out backLightColor, out leftLightColor, out rightLightColor, out topLightColor, out bottomLightColor ); // Darken colors by specified values if(topRightDarkening != 0f) { if(veticalOrientation) { topLightColor = ChartGraphics.GetGradientColor(topLightColor, Color.Black, topRightDarkening); } else { rightLightColor = ChartGraphics.GetGradientColor(rightLightColor, Color.Black, topRightDarkening); } } if(bottomLeftDarkening != 0f) { if(veticalOrientation) { bottomLightColor = ChartGraphics.GetGradientColor(bottomLightColor, Color.Black, bottomLeftDarkening); } else { leftLightColor = ChartGraphics.GetGradientColor(leftLightColor, Color.Black, bottomLeftDarkening); } } //******************************************************* //** Check visible surfaces //******************************************************* SurfaceNames visibleSurfaces = GetVisibleSurfacesWithPerspective(position,positionZ,depth,matrix); // Front surface is always visible in cylinder if( (visibleSurfaces & SurfaceNames.Front) != SurfaceNames.Front) { visibleSurfaces |= SurfaceNames.Front; } //******************************************************* //** Create flattened paths for the sides of the //** cylinder (top,bottom/left,rigth) //******************************************************* PointF[] sidePoints = new PointF[4]; sidePoints[0] = cubePoints[6].PointF; sidePoints[1] = cubePoints[1].PointF; sidePoints[2] = cubePoints[5].PointF; sidePoints[3] = cubePoints[2].PointF; GraphicsPath bottomLeftSide = new GraphicsPath(); bottomLeftSide.AddClosedCurve(sidePoints, 0.8f); bottomLeftSide.Flatten(); sidePoints[0] = cubePoints[7].PointF; sidePoints[1] = cubePoints[0].PointF; sidePoints[2] = cubePoints[4].PointF; sidePoints[3] = cubePoints[3].PointF; GraphicsPath topRigthSide = new GraphicsPath(); topRigthSide.AddClosedCurve(sidePoints, 0.8f); topRigthSide.Flatten(); //******************************************************* //** Find cylinder angle //******************************************************* float cylinderAngle = 90f; if(cubePoints[5].PointF.Y != cubePoints[4].PointF.Y) { cylinderAngle = (float)Math.Atan( (cubePoints[4].PointF.X - cubePoints[5].PointF.X) / (cubePoints[5].PointF.Y - cubePoints[4].PointF.Y) ); cylinderAngle = (float)Math.Round(cylinderAngle * 180f / (float)Math.PI); } //******************************************************* //** Draw all invisible surfaces first (if semi-transparent color is used) //******************************************************* for(int drawVisible = 0; drawVisible <= 1; drawVisible++) { // Do not draw invisible surfaces for solid colors if(drawVisible == 0 && backColor.A == 255) { continue; } // Check visibility of all surfaces and draw them for(int surfaceIndex = (int)SurfaceNames.Front; surfaceIndex <= (int)SurfaceNames.Bottom; surfaceIndex *= 2) { SurfaceNames currentSurface = (SurfaceNames)surfaceIndex; // Check if surface is visible or semi-transparent color is used bool isVisible = (visibleSurfaces & currentSurface) != 0; if(isVisible && drawVisible == 1 || !isVisible && drawVisible == 0) { // Fill surface coordinates and color GraphicsPath pathToDraw = null; Color surfaceColor = backColor; // Declare a special brush for the front surface Brush frontSurfaceBrush = null; switch(currentSurface) { case(SurfaceNames.Front): { // Set front surface color surfaceColor = backColor; // Add ellipse segment of the cylinder on top/rigth (reversed) pathToDraw = new GraphicsPath(); PointF leftSideLinePoint = PointF.Empty; PointF rightSideLinePoint = PointF.Empty; AddEllipseSegment( pathToDraw, topRigthSide, bottomLeftSide, (matrix.Perspective == 0) ? veticalOrientation : false, cylinderAngle, out leftSideLinePoint, out rightSideLinePoint); pathToDraw.Reverse(); // Add ellipse segment of the cylinder on bottom/left PointF leftOppSideLinePoint = PointF.Empty; PointF rightOppSideLinePoint = PointF.Empty; AddEllipseSegment( pathToDraw, bottomLeftSide, topRigthSide, (matrix.Perspective == 0) ? veticalOrientation : false, cylinderAngle, out leftOppSideLinePoint, out rightOppSideLinePoint); pathToDraw.CloseAllFigures(); // Reset indexes of opposite side points this._oppLeftBottomPoint = -1; this._oppRigthTopPoint = -1; // Create gradient brush for the front surface if(lightStyle != LightStyle.None) { RectangleF boundsRect = pathToDraw.GetBounds(); if(boundsRect.Height > 0 && boundsRect.Width > 0) { Color lightColor = ChartGraphics.GetGradientColor( backColor, Color.White, 0.3 ); Color darkColor = ChartGraphics.GetGradientColor( backColor, Color.Black, 0.3 ); // Create gradient if(!leftSideLinePoint.IsEmpty && !rightSideLinePoint.IsEmpty && !leftOppSideLinePoint.IsEmpty && !rightOppSideLinePoint.IsEmpty) { PointF boundsRectMiddlePoint = PointF.Empty; boundsRectMiddlePoint.X = boundsRect.X + boundsRect.Width/2f; boundsRectMiddlePoint.Y = boundsRect.Y + boundsRect.Height/2f; PointF centralLinePoint = PointF.Empty; double centralLineAngle = ((cylinderAngle) * Math.PI / 180f); if(cylinderAngle == 0 || cylinderAngle == 180 || cylinderAngle == -180) { centralLinePoint.X = boundsRectMiddlePoint.X + 100f; centralLinePoint.Y = boundsRectMiddlePoint.Y; } else if(cylinderAngle == 90 || cylinderAngle == -90) { centralLinePoint.X = boundsRectMiddlePoint.X; centralLinePoint.Y = boundsRectMiddlePoint.Y + 100f; } else if(cylinderAngle > -45 && cylinderAngle < 45) { centralLinePoint.X = boundsRectMiddlePoint.X + 100f; centralLinePoint.Y = (float)(Math.Tan(centralLineAngle) * centralLinePoint.X); centralLinePoint.Y += (float)(boundsRectMiddlePoint.Y - Math.Tan(centralLineAngle) * boundsRectMiddlePoint.X); } else { centralLinePoint.Y = boundsRectMiddlePoint.Y + 100f; centralLinePoint.X = (float)(centralLinePoint.Y - (boundsRectMiddlePoint.Y - Math.Tan(centralLineAngle) * boundsRectMiddlePoint.X)); centralLinePoint.X /= (float)(Math.Tan(centralLineAngle)); } PointF middlePoint1 = ChartGraphics.GetLinesIntersection( boundsRectMiddlePoint.X, boundsRectMiddlePoint.Y, centralLinePoint.X, centralLinePoint.Y, leftSideLinePoint.X, leftSideLinePoint.Y, leftOppSideLinePoint.X, leftOppSideLinePoint.Y); PointF middlePoint2 = ChartGraphics.GetLinesIntersection( boundsRectMiddlePoint.X, boundsRectMiddlePoint.Y, centralLinePoint.X, centralLinePoint.Y, rightSideLinePoint.X, rightSideLinePoint.Y, rightOppSideLinePoint.X, rightOppSideLinePoint.Y); // Gradient points can not have same coordinates if(middlePoint1.X != middlePoint2.X || middlePoint1.Y != middlePoint2.Y) { frontSurfaceBrush = new LinearGradientBrush( middlePoint1, middlePoint2, lightColor, darkColor); ColorBlend colorBlend = new ColorBlend(5); colorBlend.Colors[0] = darkColor; colorBlend.Colors[1] = darkColor; colorBlend.Colors[2] = lightColor; colorBlend.Colors[3] = darkColor; colorBlend.Colors[4] = darkColor; colorBlend.Positions[0] = 0.0f; colorBlend.Positions[1] = 0.0f; colorBlend.Positions[2] = 0.5f; colorBlend.Positions[3] = 1.0f; colorBlend.Positions[4] = 1.0f; ((LinearGradientBrush)frontSurfaceBrush).InterpolationColors = colorBlend; } } } } break; } case(SurfaceNames.Top): if(veticalOrientation) { surfaceColor = topLightColor; pathToDraw = topRigthSide; } break; case(SurfaceNames.Bottom): if(veticalOrientation) { surfaceColor = bottomLightColor; pathToDraw = bottomLeftSide; } break; case(SurfaceNames.Right): if(!veticalOrientation) { surfaceColor = rightLightColor; pathToDraw = topRigthSide; } break; case(SurfaceNames.Left): if(!veticalOrientation) { surfaceColor = leftLightColor; pathToDraw = bottomLeftSide; } break; } //******************************************************* //** Draw surface //******************************************************* if(pathToDraw != null) { if( (operationType & DrawingOperationTypes.DrawElement) == DrawingOperationTypes.DrawElement) { // Draw only completly visible surfaces if((visibleSurfaces & currentSurface) != 0) { using (Brush brush = new SolidBrush(surfaceColor)) { FillPath( (frontSurfaceBrush == null) ? brush : frontSurfaceBrush, pathToDraw ); } } // Draw surface border using (Pen pen = new Pen(borderColor, borderWidth)) { pen.DashStyle = GetPenStyle(borderDashStyle); if (lightStyle != LightStyle.None && (borderWidth == 0 || borderDashStyle == ChartDashStyle.NotSet || borderColor == Color.Empty)) { // Draw line of the darker color inside the cylinder pen.Color = frontSurfaceBrush == null ? surfaceColor : ChartGraphics.GetGradientColor(backColor, Color.Black, 0.3); pen.Width = 1; pen.Alignment = PenAlignment.Inset; } pen.StartCap = LineCap.Round; pen.EndCap = LineCap.Round; pen.LineJoin = LineJoin.Bevel; DrawPath(pen, pathToDraw); } } // Add surface coordinate to the path if( (operationType & DrawingOperationTypes.CalcElementPath) == DrawingOperationTypes.CalcElementPath) { // Only if surface is completly visible if((visibleSurfaces & currentSurface) != 0) { if(pathToDraw != null && pathToDraw.PointCount > 0) { resultPath.AddPath(pathToDraw, true); resultPath.SetMarkers(); } } } } } } } return resultPath; } /// /// Adds segment of the ellipse to form the front surface of the cylinder /// internal void AddEllipseSegment( GraphicsPath resultPath, GraphicsPath ellipseFlattenPath, GraphicsPath oppositeEllipseFlattenPath, bool veticalOrientation, float cylinderAngle, out PointF leftSideLinePoint, out PointF rightSideLinePoint) { // Initialize return values leftSideLinePoint = PointF.Empty; rightSideLinePoint = PointF.Empty; // Check if input path is empty if(ellipseFlattenPath.PointCount == 0) { return; } // Find the index the left/bottom most and right/top most point in flatten array of ellipse points int leftBottomPoint = 0; int rigthTopPoint = 0; PointF[] ellipsePoints = ellipseFlattenPath.PathPoints; if(veticalOrientation) { for(int pointIndex = 1; pointIndex < ellipsePoints.Length; pointIndex++) { if(ellipsePoints[leftBottomPoint].X > ellipsePoints[pointIndex].X) { leftBottomPoint = pointIndex; } if(ellipsePoints[rigthTopPoint].X < ellipsePoints[pointIndex].X) { rigthTopPoint = pointIndex; } } } else { bool doneFlag = false; leftBottomPoint = -1; rigthTopPoint = -1; if(this._oppLeftBottomPoint != -1 && this._oppRigthTopPoint != -1) { // Get index from previously calculated values leftBottomPoint = this._oppLeftBottomPoint; rigthTopPoint = this._oppRigthTopPoint; } else { // Loop through first ellipse points PointF[] oppositeEllipsePoints = oppositeEllipseFlattenPath.PathPoints; for(int pointIndex = 0; !doneFlag && pointIndex < ellipsePoints.Length; pointIndex++) { // Loop through opposite ellipse points for(int pointOppositeIndex = 0; !doneFlag && pointOppositeIndex < oppositeEllipsePoints.Length; pointOppositeIndex++) { bool closeToVertical = false; bool pointsOnLeft = false; bool pointsOnRight = false; //if(cylinderAngle == 0 || cylinderAngle == 180 || cylinderAngle == -180) if(cylinderAngle > -30 && cylinderAngle < 30) { closeToVertical = true; } if(closeToVertical) { if(oppositeEllipsePoints[pointOppositeIndex].Y == ellipsePoints[pointIndex].Y) { continue; } float linePointX = oppositeEllipsePoints[pointOppositeIndex].X - ellipsePoints[pointIndex].X; linePointX /= oppositeEllipsePoints[pointOppositeIndex].Y - ellipsePoints[pointIndex].Y; // Check if this line has any points to the right/left for(int innerPointIndex = 0; innerPointIndex < ellipsePoints.Length; innerPointIndex++) { // Skip points used to define line function if(innerPointIndex == pointIndex) { continue; } float x = linePointX; x *= ellipsePoints[innerPointIndex].Y - ellipsePoints[pointIndex].Y; x += ellipsePoints[pointIndex].X; if(x > ellipsePoints[innerPointIndex].X) { pointsOnLeft = true; } if(x < ellipsePoints[innerPointIndex].X) { pointsOnRight = true; } if(pointsOnLeft && pointsOnRight) { break; } } if(pointsOnLeft == false || pointsOnRight == false) { for(int innerPointIndex = 0; innerPointIndex < oppositeEllipsePoints.Length; innerPointIndex++) { // Skip points used to define line function if(innerPointIndex == pointOppositeIndex) { continue; } float x = linePointX; x *= oppositeEllipsePoints[innerPointIndex].Y - ellipsePoints[pointIndex].Y; x += ellipsePoints[pointIndex].X; if(x > oppositeEllipsePoints[innerPointIndex].X) { pointsOnLeft = true; } if(x < oppositeEllipsePoints[innerPointIndex].X) { pointsOnRight = true; } if(pointsOnLeft && pointsOnRight) { break; } } } } else { if(oppositeEllipsePoints[pointOppositeIndex].X == ellipsePoints[pointIndex].X) { continue; } float linePointY = oppositeEllipsePoints[pointOppositeIndex].Y - ellipsePoints[pointIndex].Y; linePointY /= oppositeEllipsePoints[pointOppositeIndex].X - ellipsePoints[pointIndex].X; // Check if this line has any points to the right/left for(int innerPointIndex = 0; innerPointIndex < ellipsePoints.Length; innerPointIndex++) { // Skip points used to define line function if(innerPointIndex == pointIndex) { continue; } float y = linePointY; y *= ellipsePoints[innerPointIndex].X - ellipsePoints[pointIndex].X; y += ellipsePoints[pointIndex].Y; if(y > ellipsePoints[innerPointIndex].Y) { pointsOnLeft = true; } if(y < ellipsePoints[innerPointIndex].Y) { pointsOnRight = true; } if(pointsOnLeft && pointsOnRight) { break; } } if(pointsOnLeft == false || pointsOnRight == false) { for(int innerPointIndex = 0; innerPointIndex < oppositeEllipsePoints.Length; innerPointIndex++) { // Skip points used to define line function if(innerPointIndex == pointOppositeIndex) { continue; } float y = linePointY; y *= oppositeEllipsePoints[innerPointIndex].X - ellipsePoints[pointIndex].X; y += ellipsePoints[pointIndex].Y; if(y > oppositeEllipsePoints[innerPointIndex].Y) { pointsOnLeft = true; } if(y < oppositeEllipsePoints[innerPointIndex].Y) { pointsOnRight = true; } if(pointsOnLeft && pointsOnRight) { break; } } } } if(!pointsOnLeft && leftBottomPoint == -1) { leftBottomPoint = pointIndex; this._oppLeftBottomPoint = pointOppositeIndex; } if(!pointsOnRight && rigthTopPoint == -1) { rigthTopPoint = pointIndex; this._oppRigthTopPoint = pointOppositeIndex; } if(leftBottomPoint >= 0 && rigthTopPoint >= 0) { doneFlag = true; if(closeToVertical) { if(ellipsePoints[leftBottomPoint].Y > oppositeEllipsePoints[this._oppLeftBottomPoint].Y) { int temp = leftBottomPoint; leftBottomPoint = rigthTopPoint; rigthTopPoint = temp; temp = this._oppLeftBottomPoint; this._oppLeftBottomPoint = this._oppRigthTopPoint; this._oppRigthTopPoint = temp; } } } } } } } // Point indexes were not found if(leftBottomPoint == rigthTopPoint || rigthTopPoint == -1 || leftBottomPoint == -1) { return; } // Set left\right line coordinates leftSideLinePoint = ellipsePoints[leftBottomPoint]; rightSideLinePoint = ellipsePoints[rigthTopPoint]; // Add required ellipse segment to the result path for(int pointIndex = leftBottomPoint + 1; pointIndex != rigthTopPoint + 1; pointIndex++) { if(pointIndex > ellipsePoints.Length - 1) { resultPath.AddLine(ellipsePoints[ellipsePoints.Length - 1], ellipsePoints[0]); pointIndex = 0; continue; } resultPath.AddLine(ellipsePoints[pointIndex - 1], ellipsePoints[pointIndex]); } } #endregion } /// /// The Point3D class represents point coordinates in 3D space. /// public class Point3D { #region Fields // Point X and Y coordinates private PointF _coordXY = new PointF(0f, 0f); // Point Z coordinate (depth) private float _coordZ = 0; #endregion #region Properties /// /// Gets or sets the X coordinate of the point. /// [ Bindable(true), DefaultValue(0), SRDescription("DescriptionAttributePoint3D_X") ] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "X")] public float X { get { return this._coordXY.X; } set { this._coordXY.X = value; } } /// /// Gets or sets the Y coordinate of the point. /// [ Bindable(true), DefaultValue(0), SRDescription("DescriptionAttributePoint3D_Y") ] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Y")] public float Y { get { return this._coordXY.Y; } set { this._coordXY.Y = value; } } /// /// Gets or sets the Z coordinate of the point. /// [ Bindable(true), DefaultValue(0), SRDescription("DescriptionAttributePoint3D_Z") ] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Z")] public float Z { get { return this._coordZ; } set { this._coordZ = value; } } /// /// Gets or sets a PointF structure, which stores the X and Y coordinates of a 3D point. /// [ Bindable(true), DefaultValue(0), SRDescription("DescriptionAttributePoint3D_PointF") ] public PointF PointF { get { return this._coordXY; } set { this._coordXY = new PointF(value.X, value.Y); } } #endregion #region Constructors /// /// Public constructor. /// [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "X, Y and Z are cartesian coordinates and well understood")] public Point3D(float x, float y, float z) { this._coordXY = new PointF(x, y); this._coordZ = z; } /// /// Public constructor. /// public Point3D( ) { } #endregion // Constructor } }