diff --git a/Elements.Serialization.SVG/src/BaseDrawing.cs b/Elements.Serialization.SVG/src/BaseDrawing.cs new file mode 100644 index 000000000..f2fe35586 --- /dev/null +++ b/Elements.Serialization.SVG/src/BaseDrawing.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Linq; +using Elements.Geometry; + +namespace Elements.Serialization.SVG +{ + /// + /// Base class for SVG documents + /// + public abstract class SvgBaseDrawing + { + /// + /// Scale applied to the model. + /// Usually it's a PageHeight to a ViewBoxHeight ratio (or a PageWidth to a ViewBoxWidth) + /// + public double Scale { get; protected set; } + /// + /// The width of the model bounding box. + /// + public float ViewBoxWidth + { + get { return _viewBoxWidth; } + protected set { _viewBoxWidth = value; } + } + /// + /// The height of the model bounding box. + /// + public float ViewBoxHeight + { + get { return _viewBoxHeight; } + protected set { _viewBoxHeight = value; } + } + + /// + /// Get the scene bounds. + /// + public BBox3 SceneBounds + { + get { return _sceneBounds; } + protected set { _sceneBounds = value; } + } + + /// + /// Computes scene bounds + /// + /// The set of the models + /// Returns the boundinf box around the input models + public static BBox3 ComputeSceneBounds(IList models) + { + var bounds = new BBox3(Vector3.Max, Vector3.Min); + foreach (var model in models) + { + foreach (var element in model.Elements) + { + if (element.Value is GeometricElement geo) + { + geo.UpdateRepresentations(); + if ((geo.Representation == null || geo.Representation.SolidOperations.All(v => v.IsVoid)) && + (geo.RepresentationInstances == null || !geo.RepresentationInstances.Any())) + { + continue; + } + geo.UpdateBoundsAndComputeSolid(); + + if (geo.Bounds.Volume.ApproximatelyEquals(0)) + { + continue; + } + + var bbMax = geo.Transform.OfPoint(geo.Bounds.Max); + var bbMin = geo.Transform.OfPoint(geo.Bounds.Min); + bounds.Extend(new[] { bbMax, bbMin }); + } + } + } + + return bounds; + } + + /// + /// Gets the rotation angle that must be applied to the plan. + /// + /// The set of the models. + /// The orientation for a plan relative to the page. + /// The angle value that must be applied if rotation is Angle + /// The angle in degrees + protected static double GetRotationValueForPlan(IList models, PlanRotation rotation, double angle) + { + if (rotation == PlanRotation.Angle) + { + return angle; + } + + var grids = models.SelectMany(m => m.AllElementsOfType()).Select(gl => gl.Curve).Where(gl => gl is Line).ToList(); + if (!grids.Any()) + { + return 0.0; + } + + var longest = (Line)grids.OrderBy(g => g.Length()).First(); + + return rotation switch + { + PlanRotation.LongestGridHorizontal => -longest.Direction().PlaneAngleTo(Vector3.YAxis), + PlanRotation.LongestGridVertical => -longest.Direction().PlaneAngleTo(Vector3.XAxis), + PlanRotation.Angle => angle, + PlanRotation.None => 0.0, + _ => 0.0, + }; + } + + private BBox3 _sceneBounds; + private float _viewBoxHeight; + private float _viewBoxWidth; + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/DrawingActions/DrawPolygon.cs b/Elements.Serialization.SVG/src/DrawingActions/DrawPolygon.cs new file mode 100644 index 000000000..689534f89 --- /dev/null +++ b/Elements.Serialization.SVG/src/DrawingActions/DrawPolygon.cs @@ -0,0 +1,36 @@ +using Elements.Geometry; + +namespace Elements.Serialization.SVG +{ + /// + /// Draw polygon action + /// + public class DrawPolygon : DrawingAction + { + /// + /// Initializes a new instance for DrawPolygon class. + /// + /// The polygon. + /// The svg context that can be used to draw the polygon (color, line thickness, etc.) + /// The text inside the polygon. + public DrawPolygon(Polygon polygon, SvgContext context, string text = "") + { + _polygon = polygon; + _text = text; + _context = context; + } + + /// + /// Draws the polygon. + /// + /// The canvas where the polygon will be added. + public override void Draw(BaseSvgCanvas canvas) + { + canvas.DrawPolygon(_polygon, _context); + } + + private Polygon _polygon; + private string _text; + private SvgContext _context; + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/DrawingActions/DrawText.cs b/Elements.Serialization.SVG/src/DrawingActions/DrawText.cs new file mode 100644 index 000000000..b70d027fb --- /dev/null +++ b/Elements.Serialization.SVG/src/DrawingActions/DrawText.cs @@ -0,0 +1,36 @@ +using Elements.Geometry; + +namespace Elements.Serialization.SVG +{ + /// + /// Draw text action + /// + public class DrawText : DrawingAction + { + /// + /// Initializes a new instance for DrawText class. + /// + /// The text. + /// The transformation of the text. + /// The svg context that can be used to draw the text (color, line thickness, etc.) + public DrawText(string text, Transform transform, SvgContext context) + { + _text = text; + _transform = transform; + _context = context; + } + + /// + /// Draws the text. + /// + /// The canvas where the text will be added. + public override void Draw(BaseSvgCanvas canvas) + { + canvas.DrawText(_text, _transform, _context); + } + + private string _text; + private Transform _transform; + private SvgContext _context; + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/DrawingActions/DrawingAction.cs b/Elements.Serialization.SVG/src/DrawingActions/DrawingAction.cs new file mode 100644 index 000000000..cbc65ae9b --- /dev/null +++ b/Elements.Serialization.SVG/src/DrawingActions/DrawingAction.cs @@ -0,0 +1,14 @@ +namespace Elements.Serialization.SVG +{ + /// + /// The drawing action (e.g. draw polygon, text) + /// + public abstract class DrawingAction + { + /// + /// Draw something using input drawing tool + /// + /// The canvas where the element will be added + public abstract void Draw(BaseSvgCanvas canvas); + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/DrawingTools/BaseCanvas.cs b/Elements.Serialization.SVG/src/DrawingTools/BaseCanvas.cs new file mode 100644 index 000000000..6c814416e --- /dev/null +++ b/Elements.Serialization.SVG/src/DrawingTools/BaseCanvas.cs @@ -0,0 +1,91 @@ +using System.Linq; +using Elements.Geometry; + +namespace Elements.Serialization.SVG +{ + /// + /// The SVG drawing tool + /// + public abstract class BaseSvgCanvas + { + /// + /// The bounds of the scene. + /// + protected BBox3 SceneBounds { get; set; } + /// + /// The SVG document + /// + protected SvgBaseDrawing Document { get; } + + /// + /// The height of the page. + /// + public double PageHeight { get; set; } + /// + /// The width of the page. + /// + public double PageWidth { get; set; } + + /// + /// Initializes a new instance of BaseDrawingTool. + /// + /// The SVG document. + public BaseSvgCanvas(SvgBaseDrawing document) + { + Document = document; + } + + /// + /// Sets the scene bounds + /// + /// The scene bounding box. + /// The plan rotation angle (in degrees). + public virtual void SetBounds(BBox3 sceneBounds, double rotation) + { + SceneBounds = sceneBounds; + var transform = new Transform(Vector3.Origin); + transform.Rotate(rotation); + var bounds = new BBox3(sceneBounds.Corners().Select(v => transform.OfPoint(v))); + var viewBoxWidth = (float)(bounds.Max.X - bounds.Min.X); + var viewBoxHeight = (float)(bounds.Max.Y - bounds.Min.Y); + + PageHeight = viewBoxHeight; + PageWidth = viewBoxWidth; + } + + /// + /// Draw polygon logic. + /// + /// The polygon. + /// The svg context that can be used to draw the polygon (color, line thickness, etc.) + public abstract void DrawPolygon(Polygon polygon, SvgContext context); + + /// + /// Draw text logic. + /// + /// The text. + /// The text transformation matrix. + /// The svg context that can be used to draw the text (color, line thickness, etc.) + public abstract void DrawText(string text, Transform transform, SvgContext context); + + /// + /// Draw line logic. + /// + /// The line. + /// + public abstract void DrawLine(Line line, SvgContext context); + + /// + /// Draw circle logic. + /// + /// The circle center. + /// The circle radius. + /// The svg context that can be used to draw the circle (color, line thickness, etc.) + public abstract void DrawCircle(Vector3 center, double radius, SvgContext context); + + /// + /// Close the document. + /// + public abstract void Close(); + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/DrawingTools/SkiaCanvas.cs b/Elements.Serialization.SVG/src/DrawingTools/SkiaCanvas.cs new file mode 100644 index 000000000..d5559a3af --- /dev/null +++ b/Elements.Serialization.SVG/src/DrawingTools/SkiaCanvas.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using Elements.Geometry; +using SkiaSharp; +using System.Linq; +using System; +using System.IO; + +namespace Elements.Serialization.SVG +{ + /// + /// The adapter for Skia library. + /// + public class SkiaCanvas : BaseSvgCanvas + { + private SKCanvas _canvas; + private SKMatrix _matrix = new SKMatrix(); + + /// + /// Initializes a new instance of SkDrawingTool + /// + /// The canvas. + /// The SVG document. + /// + public SkiaCanvas(SKCanvas canvas, SvgBaseDrawing document) : base(document) + { + _canvas = canvas; + } + + /// + /// Initializes a new instance of SkDrawingTool. + /// + /// The document stream. + /// The height of the model bounding box. + /// The width of the model bounding box. + /// The SVG document + public SkiaCanvas(Stream stream, double viewBoxHeight, double viewBoxWidth, SvgBaseDrawing document) + : base(document) + { + _canvas = SKSvgCanvas.Create(SKRect.Create((float)(viewBoxWidth * document.Scale), + (float)(viewBoxHeight * document.Scale)), stream); + } + + /// + /// Sets the scene bounds + /// + /// The scene bounding box. + /// The plan rotation angle (in degrees). + public override void SetBounds(BBox3 sceneBounds, double rotation) + { + var wOld = (float)(sceneBounds.Max.X - sceneBounds.Min.X); + var hOld = (float)(sceneBounds.Max.Y - sceneBounds.Min.Y); + + base.SetBounds(sceneBounds, rotation); + double max = Math.Max(PageWidth, PageHeight); + _matrix = SKMatrix.CreateRotationDegrees((float)rotation, (float)(PageWidth * Document.Scale / 2.0), + (float)(PageHeight * Document.Scale / 2.0)); + _matrix.TransX += (float)((wOld - PageWidth) * Document.Scale / 2.0f); + _matrix.TransY += (float)((PageHeight - hOld) * Document.Scale / 2.0f); + } + + /// + /// Draws polygon on canvas. + /// + /// The polygon. + /// The svg context that can be used to draw the polygon (color, line thickness, etc.) + public override void DrawPolygon(Polygon polygon, SvgContext context) + { + var path = new SKPath(); + var points = new List(); + foreach (var point in polygon.Vertices) + { + points.Add(ToSkPoint(point)); + } + + path.AddPoly(points.ToArray(), true); + var paint = new SKPaint() { Style = SKPaintStyle.Fill, StrokeWidth = (float)(context.ElementStroke.Width * Document.Scale) }; + if (context.Color.HasValue) + { + paint.Color = ToSkColor(context.Color.Value); + _canvas.DrawPath(path, paint); + } + + paint.Color = ToSkColor(context.ElementStroke.Color); + paint.Style = SKPaintStyle.Stroke; + _canvas.DrawPath(path, paint); + } + + /// + /// Draws text on canvas. + /// + /// The text. + /// The text transformation matrix. + /// The svg context that can be used to draw the text (color, line thickness, etc.) + public override void DrawText(string text, Transform transform, SvgContext context) + { + var r = default(SKRect); + var t2 = SKTypeface.FromFamilyName(context.Font.FamilyName, SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); + var font2 = new SKFont(t2, (float)(context.Font.Size * Document.Scale)); + var p2 = new SKPaint(font2); + + double textLength = p2.MeasureText(text, ref r); + + var heightVectot = transform.OfVector(Vector3.YAxis); + var top = ToSkPoint(transform.Origin + heightVectot * textLength / 2.0); + var bottom = ToSkPoint(transform.Origin - heightVectot * textLength / 2.0); + + var path = new SKPath(); + path.AddPoly(new SKPoint[] { top, bottom }); + + _canvas.DrawTextOnPath(text, path, (float)(textLength * Document.Scale / 2.0 - r.MidX) / 2f, r.Height / 2.0f, p2); + } + + /// + /// Draws line on canvas. + /// + /// The line. + /// + public override void DrawLine(Line line, SvgContext context) + { + var paint = new SKPaint() { Style = SKPaintStyle.Fill, StrokeWidth = (float)(context.ElementStroke.Width * Document.Scale) }; + if (context.Color.HasValue) + { + paint.Color = ToSkColor(context.Color.Value); + } + + paint.Color = ToSkColor(context.ElementStroke.Color); + paint.Style = SKPaintStyle.Stroke; + if (context.ElementStroke.HasDash) + { + paint.PathEffect = SKPathEffect.CreateDash(context.ElementStroke.DashIntervals.Select(i => (float)(i * Document.Scale)).ToArray(), (float)context.ElementStroke.DashPhase); + } + + _canvas.DrawLine(ToSkPoint(line.Start), ToSkPoint(line.End), paint); + } + + /// + /// Draws circle on canvas. + /// + /// The circle center. + /// The circle radius. + /// The svg context that can be used to draw the circle (color, line thickness, etc.) + public override void DrawCircle(Vector3 center, double radius, SvgContext context) + { + var location = ToSkPoint(center); + var paint = new SKPaint() { Style = SKPaintStyle.Fill, StrokeWidth = (float)(context.ElementStroke.Width * Document.Scale) }; + if (context.Color.HasValue) + { + paint.Color = ToSkColor(context.Color.Value); + _canvas.DrawCircle(location, (float)(radius * Document.Scale), paint); + } + + paint.Color = ToSkColor(context.ElementStroke.Color); + paint.Style = SKPaintStyle.Stroke; + + _canvas.DrawCircle(location, (float)(radius * Document.Scale), paint); + } + + /// + /// Disposes the canvas + /// + public override void Close() + { + _canvas.Dispose(); + } + + private SKPoint ToSkPoint(Vector3 point) + { + var viewBoxHeight = (float)(SceneBounds.Max.Y - SceneBounds.Min.Y); + var skPoint = new SKPoint((float)((point.X - SceneBounds.Min.X) * Document.Scale), + (float)((viewBoxHeight + SceneBounds.Min.Y - point.Y) * Document.Scale)); + return _matrix.MapPoint(skPoint); + } + + private static SKColor ToSkColor(Color color) + { + return new SKColor((byte)(color.Red * 255), (byte)(color.Green * 255), (byte)(color.Blue * 255), (byte)(color.Alpha * 255)); + } + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/Elements.Serialization.SVG.csproj b/Elements.Serialization.SVG/src/Elements.Serialization.SVG.csproj index 45e0298cd..c180a63da 100644 --- a/Elements.Serialization.SVG/src/Elements.Serialization.SVG.csproj +++ b/Elements.Serialization.SVG/src/Elements.Serialization.SVG.csproj @@ -19,15 +19,10 @@ - + - - - - - \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/EventArgs/ElementSerializationEventArgs.cs b/Elements.Serialization.SVG/src/EventArgs/ElementSerializationEventArgs.cs index 0e37468b5..70c2c6a44 100644 --- a/Elements.Serialization.SVG/src/EventArgs/ElementSerializationEventArgs.cs +++ b/Elements.Serialization.SVG/src/EventArgs/ElementSerializationEventArgs.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Svg; namespace Elements.Serialization.SVG { @@ -35,10 +34,12 @@ public enum CreationSequences /// It can be used to create SvgElements /// (e.g. polygon.ToSvgPolygon(e.DrawingPlan, e.DrawingPlan.FrontContext) or e.DrawingPlan.CreateText(..)) /// - public ElementSerializationEventArgs(SvgSection drawingPlan, Element element) + /// + public ElementSerializationEventArgs(SvgSection drawingPlan, Element element, double scale) { DrawingPlan = drawingPlan; Element = element; + Scale = scale; } /// @@ -55,7 +56,7 @@ public ElementSerializationEventArgs(SvgSection drawingPlan, Element element) /// The set of elements that will be added to the drawing plan if IsProcessed == true /// Please add there all new element that you created from the Element /// - public List SvgElements { get; } = new List(); + public List Actions { get; } = new List(); /// /// The element which is processed before being added to the drawing plan @@ -71,5 +72,10 @@ public ElementSerializationEventArgs(SvgSection drawingPlan, Element element) /// 5. Elements that are marked as AfterGridLines /// public CreationSequences CreationSequence { get; set; } + + /// + /// The drawing scale + /// + public double Scale { get; } } } \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/Models/Paint/SvgContext.cs b/Elements.Serialization.SVG/src/Models/Paint/SvgContext.cs new file mode 100644 index 000000000..f0f20f8bf --- /dev/null +++ b/Elements.Serialization.SVG/src/Models/Paint/SvgContext.cs @@ -0,0 +1,203 @@ +using Elements.Geometry; +using System; +using Svg; +using System.Linq; + +namespace Elements.Serialization.SVG +{ + /// + /// The style and color information about how to draw geometries and text. + /// + public class SvgContext + { + /// + /// Initializes a new instance of SvgContext class. + /// + public SvgContext() : this(Colors.White, Colors.Black, 0.01) + { + } + + /// + /// Initializes a new instance of SvgContext class. + /// + /// The paint's stroke color. + /// The paint's stroke width. + /// The definition of the dash pattern via an even number of entries. + public SvgContext(Color strokeColor, double strokeWidth, double[] dashIntervals) : this(Colors.White, strokeColor, strokeWidth) + { + ElementStroke.CreateDash(dashIntervals, 0); + // TODO: delete + _strokeDashArray = new SvgUnitCollection(); + foreach (var interval in dashIntervals) + { + _strokeDashArray.Add(new SvgUnit(SvgUnitType.User, (float)interval)); + } + } + + /// + /// Initializes a new instance of SvgContext class. + /// + /// The paint's stroke color. + /// The paint's stroke width. + public SvgContext(Color strokeColor, double strokeWidth) : this(Colors.White, strokeColor, strokeWidth) { } + + /// + /// Initializes a new instance of SvgContext class. + /// + /// The paint's foreground color. + /// The paint's stroke color. + /// The paint's stroke width. + public SvgContext(Color color, Color strokeColor, double strokeWidth) + { + _elementStroke = new SvgStroke(); + _font = new SvgFont("Arial"); + + Color = color; + ElementStroke.Color = strokeColor; + ElementStroke.Width = strokeWidth; + // TODO: delete + _stroke = new SvgColourServer(ToDrawingColor(strokeColor)); + _strokeWidth = new SvgUnit(SvgUnitType.User, (float)strokeWidth); + } + + /// + /// Initializes a new instance of SvgContext class. + /// + /// The font family name. + /// The font size. + public SvgContext(string fontFamily, double fontSize) : this(Colors.White, Colors.Black, 0) + { + Font.FamilyName = fontFamily; + Font.Size = fontSize; + } + + /// + /// Initializes a new instance of SvgContext class. + /// + /// The paint's foreground color. + /// The paint's stroke color. + /// The paint's stroke width. + public SvgContext(System.Drawing.Color color, System.Drawing.Color strokeColor, double strokeWidth) : + this(new Color(color), new Color(strokeColor), strokeWidth) + { } + + /// + /// Initializes a new instance of SvgContext class. + /// + /// The paint's stroke color. + /// The paint's stroke width. + public SvgContext(System.Drawing.Color strokeColor, double strokeWidth) : + this(new Color(strokeColor), strokeWidth) + { } + + /// + /// Initializes a new instance of SvgContext class. + /// + /// The paint's stroke color. + /// The paint's stroke width. + /// The definition of the dash pattern via an even number of entries. + public SvgContext(System.Drawing.Color strokeColor, double strokeWidth, double[] dashIntervals) : + this(new Color(strokeColor), strokeWidth, dashIntervals) + { } + + + [Obsolete("Fill is deprecated, please use Color instead.")] + public SvgColourServer? Fill + { + get { return _colourServer; } + set + { + _colourServer = value; + _color = value == null ? null : new Color(value.Colour); + } + } + + [Obsolete("Stroke is deprecated, please use ElementStroke.Color instead.")] + public SvgColourServer? Stroke + { + get { return _stroke; } + set + { + if (value == null) + { + _stroke = null; + _elementStroke.Color = null; + } + else + { + _stroke = value; + _elementStroke.Color = new Color(value.Colour); + } + } + } + + [Obsolete("StrokeWidth is deprecated, please use ElementStroke.Width instead.")] + public SvgUnit StrokeWidth + { + get { return _strokeWidth; } + set + { + _strokeWidth = value; + _elementStroke.Width = value.Value; + } + } + + [Obsolete("StrokeDashArray is deprecated, please use ElementStroke.CreateDash instead.")] + public SvgUnitCollection? StrokeDashArray + { + get { return _strokeDashArray; } + set + { + if (value == null) + { + _strokeDashArray = null; + _elementStroke.DeleteDash(); + } + else + { + _strokeDashArray = value; + var intervals = _strokeDashArray.Select(ar => (double)ar.Value); + _elementStroke.CreateDash(intervals.ToArray(), 0); + } + } + } + + /// + /// The paint's foreground color. + /// + public Color? Color + { + get { return _color; } + set + { + _color = value; + _colourServer = _color.HasValue ? new SvgColourServer(ToDrawingColor(_color.Value)) : null; + } + } + + /// + /// The stroke style information. + /// + public SvgStroke ElementStroke => _elementStroke; + + /// + /// The font style information. + /// + public SvgFont Font => _font; + + private static System.Drawing.Color ToDrawingColor(Geometry.Color color) + { + return System.Drawing.Color.FromArgb((int)(color.Alpha * 255), (int)(color.Red * 255), (int)(color.Green * 255), (int)(color.Blue * 255)); + } + + private Color? _color; + private readonly SvgStroke _elementStroke; + private SvgFont _font; + + // TODO: delete + private SvgColourServer? _colourServer; + private SvgColourServer? _stroke; + private SvgUnit _strokeWidth = new SvgUnit(SvgUnitType.User, 0.01f); + private SvgUnitCollection? _strokeDashArray; + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/Models/Paint/SvgFont.cs b/Elements.Serialization.SVG/src/Models/Paint/SvgFont.cs new file mode 100644 index 000000000..6eb87e7c7 --- /dev/null +++ b/Elements.Serialization.SVG/src/Models/Paint/SvgFont.cs @@ -0,0 +1,27 @@ +namespace Elements.Serialization.SVG +{ + /// + /// The font family and size information + /// + public class SvgFont + { + /// + /// Initializes a new instance of SvgFont. + /// + /// The family name. + public SvgFont(string familyName) + { + FamilyName = familyName; + } + + /// + /// The font family name. + /// + public string FamilyName { get; set; } + + /// + /// The font size + /// + public double Size { get; set; } = 0.7; + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/Models/Paint/SvgStroke.cs b/Elements.Serialization.SVG/src/Models/Paint/SvgStroke.cs new file mode 100644 index 000000000..65e6a4135 --- /dev/null +++ b/Elements.Serialization.SVG/src/Models/Paint/SvgStroke.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Elements.Geometry; + +namespace Elements.Serialization.SVG +{ + /// + /// The stroke style and size information + /// + public class SvgStroke + { + /// + /// The stroke width. + /// + public double Width { get; set; } = 0.01; + + /// + /// The stroke color. + /// + public Color Color { get; set; } + + /// + /// The definition of the dash pattern via an even number of entries. + /// + public IEnumerable DashIntervals => _dashIntervals; + + /// + /// The offset into the intervals array. (mod the sum of all of the intervals). + /// + public double DashPhase => _dashPhase; + + /// + /// A value indicating whether paint has a dash pattern. + /// + public bool HasDash { get; private set; } + + /// + /// Creates a dash path effect by specifying the dash intervals. + /// + /// The definition of the dash pattern via an even number of entries. + /// The offset into the intervals array. (mod the sum of all of the intervals). + public void CreateDash(double[] intervals, float phase) + { + _dashIntervals.Clear(); + _dashIntervals.AddRange(intervals); + _dashPhase = phase; + HasDash = true; + } + + /// + /// Deletes dash pattern + /// + public void DeleteDash() + { + _dashIntervals.Clear(); + _dashPhase = 0; + HasDash = false; + } + + private readonly List _dashIntervals = new List(); + private double _dashPhase; + } +} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/SvgContext.cs b/Elements.Serialization.SVG/src/SvgContext.cs deleted file mode 100644 index 515d9be99..000000000 --- a/Elements.Serialization.SVG/src/SvgContext.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Svg; - -namespace Elements.Serialization.SVG -{ - /// - /// Contextual data for the SVG serialization process. - /// - public class SvgContext - { - /// - /// The fill color. - /// - public SvgColourServer? Fill { get; set; } - - /// - /// The stroke color. - /// - public SvgColourServer? Stroke { get; set; } - - /// - /// The stroke width. - /// - public SvgUnit StrokeWidth { get; set; } = new SvgUnit(SvgUnitType.User, 0.01f); - - /// - /// The line end cap style. - /// - public SvgStrokeLineCap? StrokeLineCap { get; set; } = SvgStrokeLineCap.Butt; - - /// - /// The pattern of dashes and gaps used to stroke paths. - /// - public SvgUnitCollection? StrokeDashArray { get; set; } - } -} \ No newline at end of file diff --git a/Elements.Serialization.SVG/src/SvgDrawingExtensions.cs b/Elements.Serialization.SVG/src/SvgDrawingExtensions.cs index c9f9803ff..d38394fc9 100644 --- a/Elements.Serialization.SVG/src/SvgDrawingExtensions.cs +++ b/Elements.Serialization.SVG/src/SvgDrawingExtensions.cs @@ -34,7 +34,7 @@ public static SvgLine ToSvgLine(this Line line, Vector3 sceneBoundsMin, float vi string style = string.Empty; if (context.Fill != null) { - style += $"fill:{context.Fill.Colour.Name.ToLower()}"; + style = $"fill:{SvgColourServerToString(context.Fill)}"; } if (context.Stroke != null) @@ -43,7 +43,7 @@ public static SvgLine ToSvgLine(this Line line, Vector3 sceneBoundsMin, float vi { style += "; "; } - style += $"stroke:{context.Stroke.Colour.Name.ToLower()}"; + style += $"stroke:{SvgColourServerToString(context.Stroke)}"; } if (!string.IsNullOrEmpty(style)) @@ -53,6 +53,10 @@ public static SvgLine ToSvgLine(this Line line, Vector3 sceneBoundsMin, float vi return svgLine; } + private static string SvgColourServerToString(SvgColourServer colorServer) + { + return $"rgb({colorServer.Colour.R}, {colorServer.Colour.G}, {colorServer.Colour.B})"; + } /// /// Convert a geometric line to an SVG using a section and a context. /// @@ -62,7 +66,7 @@ public static SvgLine ToSvgLine(this Line line, Vector3 sceneBoundsMin, float vi /// An SVG line. public static SvgLine ToSvgLine(this Line line, SvgSection drawingPlan, SvgContext context) { - return ToSvgLine(line, drawingPlan.GetSceneBounds().Min, drawingPlan.ViewBoxHeight, context); + return ToSvgLine(line, drawingPlan.SceneBounds.Min, drawingPlan.ViewBoxHeight, context); } /// @@ -75,14 +79,34 @@ public static SvgLine ToSvgLine(this Line line, SvgSection drawingPlan, SvgConte /// An SVG polygon. public static SvgPolygon ToSvgPolygon(this Polygon polygon, Vector3 sceneBoundsMin, float viewBoxHeight, SvgContext context) { - return new SvgPolygon() + var svgPolygon = new SvgPolygon() { - Fill = context.Fill, - Stroke = context.Stroke, StrokeWidth = context.StrokeWidth, StrokeDashArray = context.StrokeDashArray, Points = polygon.Vertices.ToSvgPointCollection(sceneBoundsMin, viewBoxHeight) }; + + string style = string.Empty; + if (context.Fill != null) + { + style += $"fill:{SvgColourServerToString(context.Fill)}"; + } + + if (context.Stroke != null) + { + if (!string.IsNullOrEmpty(style)) + { + style += "; "; + } + style += $"stroke:{SvgColourServerToString(context.Stroke)}"; + } + + if (!string.IsNullOrEmpty(style)) + { + svgPolygon.CustomAttributes.Add("style", style); + } + + return svgPolygon; } /// @@ -94,7 +118,7 @@ public static SvgPolygon ToSvgPolygon(this Polygon polygon, Vector3 sceneBoundsM /// An SVG polygon. public static SvgPolygon ToSvgPolygon(this Polygon polygon, SvgSection drawingPlan, SvgContext context) { - return ToSvgPolygon(polygon, drawingPlan.GetSceneBounds().Min, drawingPlan.ViewBoxHeight, context); + return ToSvgPolygon(polygon, drawingPlan.SceneBounds.Min, drawingPlan.ViewBoxHeight, context); } /// @@ -116,7 +140,7 @@ public static SvgUnit ToXUserUnit(this double x, Vector3 sceneBoundsMin) /// An SVG unit. public static SvgUnit ToXUserUnit(this double x, SvgSection drawingPlan) { - return ToXUserUnit(x, drawingPlan.GetSceneBounds().Min); + return ToXUserUnit(x, drawingPlan.SceneBounds.Min); } /// @@ -140,7 +164,7 @@ public static SvgUnit ToYUserUnit(this double y, float viewBoxHeight, Vector3 sc /// An SVG unit. public static SvgUnit ToYUserUnit(this double y, SvgSection drawingPlan) { - return ToYUserUnit(y, drawingPlan.ViewBoxHeight, drawingPlan.GetSceneBounds().Min); + return ToYUserUnit(y, drawingPlan.ViewBoxHeight, drawingPlan.SceneBounds.Min); } /// @@ -169,7 +193,7 @@ public static SvgPointCollection ToSvgPointCollection(this IList points /// An collection of SVG points. public static SvgPointCollection ToSvgPointCollection(this IList points, SvgSection drawingPlan) { - return ToSvgPointCollection(points, drawingPlan.GetSceneBounds().Min, drawingPlan.ViewBoxHeight); + return ToSvgPointCollection(points, drawingPlan.SceneBounds.Min, drawingPlan.ViewBoxHeight); } } -} \ No newline at end of file +} diff --git a/Elements.Serialization.SVG/src/SvgFaceElevation.cs b/Elements.Serialization.SVG/src/SvgFaceElevation.cs index c0df28971..eb4e9d8ee 100644 --- a/Elements.Serialization.SVG/src/SvgFaceElevation.cs +++ b/Elements.Serialization.SVG/src/SvgFaceElevation.cs @@ -65,20 +65,12 @@ public SvgFaceElevation(Face face, Vector3 up) /// /// The svg context which defines settings for element lines. /// - public SvgContext ElementLinesContext { get; set; } = new SvgContext() - { - Stroke = new SvgColourServer(Colors.Black), - StrokeWidth = new SvgUnit(SvgUnitType.User, 0.01f) - }; + public SvgContext ElementLinesContext { get; set; } = new SvgContext(Colors.Black, 0.01); /// /// The svg context which defines settings for leader lines . /// - public SvgContext DimensionLinesContext { get; set; } = new SvgContext() - { - Stroke = new SvgColourServer(System.Drawing.Color.DarkBlue), - StrokeWidth = new SvgUnit(SvgUnitType.User, 0.005f) - }; + public SvgContext DimensionLinesContext { get; set; } = new SvgContext(System.Drawing.Color.DarkBlue, 0.005); /// /// An additional amount to rotate the drawing. diff --git a/Elements.Serialization.SVG/src/SvgSection.cs b/Elements.Serialization.SVG/src/SvgSection.cs index 017a2e23d..241371f23 100644 --- a/Elements.Serialization.SVG/src/SvgSection.cs +++ b/Elements.Serialization.SVG/src/SvgSection.cs @@ -1,8 +1,5 @@ using Elements.Geometry; -using SkiaSharp; using Svg; -using Svg.Skia; -using Svg.Transforms; using System; using System.Collections.Generic; using System.IO; @@ -37,14 +34,20 @@ public enum PlanRotation /// /// A section of a model serialized to SVG. /// - public class SvgSection + public class SvgSection : SvgBaseDrawing { + #region Constants + + private const double DEFAULT_SCALE = 10; + + #endregion + #region Private fields private readonly List _models = new List(); private Dictionary _gridLines = new Dictionary(); private readonly double _elevation; - private BBox3 _sceneBounds; + private BaseSvgCanvas _canvas = null!; #endregion @@ -78,35 +81,23 @@ public SvgSection(IList models, double elevation) /// /// The svg context which defines settings for elements cut by the cut plane. /// - public SvgContext FrontContext { get; set; } = new SvgContext() - { - Fill = new SvgColourServer(System.Drawing.Color.Black), - StrokeWidth = new SvgUnit(SvgUnitType.User, 0.01f) - }; + public SvgContext FrontContext { get; set; } = new SvgContext(Colors.Black, Colors.Black, 0.01); /// /// The svg context which defines settings for elements behind the cut plane. /// - public SvgContext BackContext { get; set; } = new SvgContext() - { - Stroke = new SvgColourServer(System.Drawing.Color.Black), - StrokeWidth = new SvgUnit(SvgUnitType.User, 0.01f) - }; + public SvgContext BackContext { get; set; } = new SvgContext(Colors.Black, 0.01); /// /// The svg context which defines settings for the grid elements. /// - public SvgContext GridContext { get; set; } = new SvgContext() - { - Stroke = new SvgColourServer(Colors.Black), - StrokeWidth = new SvgUnit(SvgUnitType.User, 0.01f), - StrokeDashArray = new SvgUnitCollection(){ - new SvgUnit(SvgUnitType.User, 0.3f), - new SvgUnit(SvgUnitType.User, 0.025f), - new SvgUnit(SvgUnitType.User, 0.05f), - new SvgUnit(SvgUnitType.User, 0.025f), - } - }; + public SvgContext GridContext { get; set; } = new SvgContext(Colors.Black, 0.01, new double[] { 0.3 * 5, 0.025 * 5, 0.05 * 5, 0.025 * 5 }); + + /// + /// The svg context which defines settings for the grid text elements. + /// + /// + public SvgContext GridTextContext { get; set; } = new SvgContext("Arial", 0.7); /// /// Should grid lines be shown in the section? @@ -133,9 +124,6 @@ public SvgSection(IList models, double elevation) /// public double PlanRotationDegrees { get; set; } = 0.0; - internal float ViewBoxWidth { get; private set; } - internal float ViewBoxHeight { get; private set; } - #endregion #region Public logic @@ -220,7 +208,7 @@ public static SvgDocument CreatePlanFromModels(IList models, PlanRotation planRotation = PlanRotation.Angle, double planRotationDegrees = 0.0) { - var drawingPlan = new SvgSection(models, elevation); + var drawingPlan = new SvgSectionOld(models, elevation); drawingPlan.BackContext = backContext; drawingPlan.FrontContext = frontContext; drawingPlan.ShowGrid = showGrid; @@ -237,11 +225,14 @@ public static SvgDocument CreatePlanFromModels(IList models, /// /// Create a plan of a model and save the resulting section to the provided stream. /// - /// The stream to write the SVG data + /// The stream to write the SVG data. public void SaveAsSvg(Stream stream) { - var doc = CreateSvgDocument(); - doc.Write(stream); + var rotation = CreateSceneViewBox(); + _canvas = new SkiaCanvas(stream, ViewBoxHeight, ViewBoxWidth, this); + _canvas.SetBounds(SceneBounds, rotation); + Draw(_canvas); + _canvas.Close(); } /// @@ -258,187 +249,20 @@ public void SaveAsSvg(string path) } /// - /// Create a plan of a model and save the resulting section to the provided path. - /// - /// The location on disk to write the PDF file - /// The pdf save options: page size, mergin - public void SaveAsPdf(string path, PdfSaveOptions saveOptions) - { - var pdfPath = System.IO.Path.ChangeExtension(path, ".pdf"); - using (var stream = new FileStream(pdfPath, FileMode.Create, FileAccess.Write)) - { - SaveAsPdf(stream, saveOptions); - } - } - - /// - /// Create a plan of a model and save the resulting section to the provided stream. - /// - /// The stream to write the PDF data - /// The pdf save options: page size, mergin - public void SaveAsPdf(Stream stream, PdfSaveOptions saveOptions) - { - var pageHeight = saveOptions.PageHeight; - var pageWidth = saveOptions.PageWidth; - var margin = saveOptions.Margin; - - using var document = SKDocument.CreatePdf(stream); - var path = System.IO.Path.GetTempFileName(); - path = System.IO.Path.ChangeExtension(path, ".svg"); - - using (var svg = new SKSvg()) - { - var svgStream = new MemoryStream(); - SaveAsSvg(path); - if (svg.Load(path) is { } && svg.Picture != null) - { - if (svg.Picture.CullRect.Width > svg.Picture.CullRect.Height) - { - var tmp = pageHeight; - pageHeight = pageWidth; - pageWidth = tmp; - } - - using (var pdfCanvas = document.BeginPage(pageWidth, pageHeight)) - { - float scale = Math.Min((pageHeight - margin * 2) / svg.Picture.CullRect.Height, - (pageWidth - margin * 2) / svg.Picture.CullRect.Width); - var matrix = SKMatrix.CreateScale(scale, scale); - matrix = SKMatrix.Concat(SKMatrix.CreateTranslation(margin, margin), matrix); - pdfCanvas.DrawPicture(svg.Picture, ref matrix); - } - - document.EndPage(); - } - } - - document.Close(); - stream.Seek(0, SeekOrigin.Begin); - } - - /// - /// Create SvgText element. + /// Draws the models on the canvas from the input. /// - /// The content. - /// The element location at the model. - /// The angle of the content. + /// The drawing tool (adapter for Skia, SVG.Net, etc.) + /// The height of the page. + /// The width of the page. + /// The margin from the left and right of the page. /// - public SvgText CreateText(string text, Vector3 location, double angle) - { - var x = location.X.ToXUserUnit(this); - var y = location.Y.ToYUserUnit(this); - - var svgText = new SvgText(text) - { - X = new SvgUnitCollection() { x }, - Y = new SvgUnitCollection() { y }, - TextAnchor = SvgTextAnchor.Middle, - Transforms = new Svg.Transforms.SvgTransformCollection() { new SvgRotate((float)angle, x.Value, y.Value), new SvgTranslate(0, 0.2f) } - }; - - svgText.CustomAttributes.Add("style", "font-family: Arial; font-size: 0.5; fill:black"); - return svgText; - } - - /// - /// Generate a plan of a model. - /// - public SvgDocument CreateSvgDocument() - { - _sceneBounds = SvgSection.ComputeSceneBounds(_models); - - var doc = new SvgDocument - { - Fill = SvgPaintServer.None - }; - - var rotation = SvgSection.GetRotationValueForPlan(_models, PlanRotation, PlanRotationDegrees); - - _gridLines.Clear(); - if (ShowGrid) - { - _gridLines = ExtendSceneWithGridLines(); - } - - CreateViewBox(doc, rotation); - Draw(doc, rotation); - return doc; - } - - /// - /// Get the scene bounds. - /// - public BBox3 GetSceneBounds() - { - return _sceneBounds; - } - - #endregion - - #region Private logic - - private static double GetRotationValueForPlan(IList models, PlanRotation rotation, double angle) - { - if (rotation == PlanRotation.Angle) - { - return angle; - } - - var grids = models.SelectMany(m => m.AllElementsOfType()).Select(gl => gl.Curve).Where(gl => gl is Line).ToList(); - if (!grids.Any()) - { - return 0.0; - } - - var longest = (Line)grids.OrderBy(g => g.Length()).First(); - - return rotation switch - { - PlanRotation.LongestGridHorizontal => -longest.Direction().PlaneAngleTo(Vector3.YAxis), - PlanRotation.LongestGridVertical => -longest.Direction().PlaneAngleTo(Vector3.XAxis), - PlanRotation.Angle => angle, - PlanRotation.None => 0.0, - _ => 0.0, - }; - } - - private static BBox3 ComputeSceneBounds(IList models) - { - var bounds = new BBox3(Vector3.Max, Vector3.Min); - foreach (var model in models) - { - foreach (var element in model.Elements) - { - if (element.Value is GeometricElement geo) - { - geo.UpdateRepresentations(); - if ((geo.Representation == null || geo.Representation.SolidOperations.All(v => v.IsVoid)) && - (geo.RepresentationInstances == null || !geo.RepresentationInstances.Any())) - { - continue; - } - geo.UpdateBoundsAndComputeSolid(); - - if (geo.Bounds.Volume.ApproximatelyEquals(0)) - { - continue; - } - - var bbMax = geo.Transform.OfPoint(geo.Bounds.Max); - var bbMin = geo.Transform.OfPoint(geo.Bounds.Min); - bounds.Extend(new[] { bbMax, bbMin }); - } - } - } - - return bounds; - } - - private void Draw(SvgDocument doc, double rotation) + public double Draw(BaseSvgCanvas canvas, float pageHeight = -1, float pageWidth = -1, float margin = 0) { + _canvas = canvas; + var rotation = CreateSceneViewBox(pageHeight, pageWidth, margin); var plane = new Plane(new Vector3(0, 0, _elevation), Vector3.ZAxis); - var customElementsBeforeGrid = new List(); - var customElementsAfterGrid = new List(); + var customElementsBeforeGrid = new List(); + var customElementsAfterGrid = new List(); foreach (var model in _models) { @@ -448,20 +272,20 @@ private void Draw(SvgDocument doc, double rotation) var elements = new Dictionary(); foreach (var element in model.Elements) { - var e = new ElementSerializationEventArgs(this, element.Value); + var e = new ElementSerializationEventArgs(this, element.Value, Scale); OnElementDrawing.Invoke(this, e); if (e.IsProcessed) { switch (e.CreationSequence) { case ElementSerializationEventArgs.CreationSequences.AfterGridLines: - customElementsAfterGrid.AddRange(e.SvgElements); + customElementsAfterGrid.AddRange(e.Actions); break; case ElementSerializationEventArgs.CreationSequences.BeforeGridLines: - customElementsBeforeGrid.AddRange(e.SvgElements); + customElementsBeforeGrid.AddRange(e.Actions); break; case ElementSerializationEventArgs.CreationSequences.Immediately: - e.SvgElements.ForEach(el => doc.Children.Add(el)); + e.Actions.ForEach(a => a.Draw(_canvas)); break; } } @@ -483,7 +307,7 @@ private void Draw(SvgDocument doc, double rotation) { foreach (var p in intersectingPolygon.Value) { - doc.Children.Add(p.ToSvgPolygon(_sceneBounds.Min, ViewBoxHeight, FrontContext)); + _canvas.DrawPolygon(p, FrontContext); } } @@ -491,7 +315,7 @@ private void Draw(SvgDocument doc, double rotation) { foreach (var p in backPolygon.Value) { - doc.Children.Add(p.ToSvgPolygon(_sceneBounds.Min, ViewBoxHeight, BackContext)); + _canvas.DrawPolygon(p, BackContext); } } @@ -499,74 +323,76 @@ private void Draw(SvgDocument doc, double rotation) { foreach (var l in line.Value) { - doc.Children.Add(l.ToSvgLine(_sceneBounds.Min, ViewBoxHeight, FrontContext)); + _canvas.DrawLine(l, FrontContext); } } } - customElementsBeforeGrid.ForEach(el => doc.Children.Add(el)); - DrawGridLines(doc, rotation, _gridLines); - customElementsAfterGrid.ForEach(el => doc.Children.Add(el)); + customElementsBeforeGrid.ForEach(el => el.Draw(_canvas)); + DrawGridLines(rotation, _gridLines); + customElementsAfterGrid.ForEach(el => el.Draw(_canvas)); + return rotation; } - private void CreateViewBox(SvgDocument doc, double rotation) + /// + /// Creates scene view box + /// + /// The page height. + /// The pae widthh. + /// The margin from the left and right of the page. + /// Returns plan rotation in degrees. + public double CreateSceneViewBox(float pageHeight = -1, float pageWidth = -1, float margin = 0) { - ViewBoxWidth = (float)(_sceneBounds.Max.X - _sceneBounds.Min.X); - ViewBoxHeight = (float)(_sceneBounds.Max.Y - _sceneBounds.Min.Y); + SceneBounds = SvgBaseDrawing.ComputeSceneBounds(_models); + var rotation = SvgBaseDrawing.GetRotationValueForPlan(_models, PlanRotation, PlanRotationDegrees); + _gridLines.Clear(); + if (ShowGrid) + { + _gridLines = ExtendSceneWithGridLines(); + } - if (rotation == 0.0) + ViewBoxWidth = (float)(SceneBounds.Max.X - SceneBounds.Min.X); + ViewBoxHeight = (float)(SceneBounds.Max.Y - SceneBounds.Min.Y); + var transform = new Transform(Vector3.Origin); + transform.Rotate(rotation); + var bounds = new BBox3(SceneBounds.Corners().Select(v => transform.OfPoint(v))); + var wOld = ViewBoxWidth; + var hOld = ViewBoxHeight; + ViewBoxWidth = (float)(bounds.Max.X - bounds.Min.X); + ViewBoxHeight = (float)(bounds.Max.Y - bounds.Min.Y); + + if (pageHeight == -1 || pageWidth == -1) { - doc.ViewBox = new SvgViewBox(0, 0, ViewBoxWidth, ViewBoxHeight); + Scale = DEFAULT_SCALE; } else { - // Compute a new bounding box around the rotated - // bounding box, to ensure that we our viewbox isn't too small. - var t = new Transform(Vector3.Origin); - t.Rotate(rotation); - var bounds = new BBox3(_sceneBounds.Corners().Select(v => t.OfPoint(v))); - var wOld = ViewBoxWidth; - var hOld = ViewBoxHeight; - ViewBoxWidth = (float)(bounds.Max.X - bounds.Min.X); - ViewBoxHeight = (float)(bounds.Max.Y - bounds.Min.Y); - float max = Math.Max(ViewBoxWidth, ViewBoxHeight); - doc.ViewBox = new SvgViewBox((wOld - ViewBoxWidth) / 2.0f, (ViewBoxHeight - hOld) / 2.0f, ViewBoxWidth, ViewBoxHeight); - doc.CustomAttributes.Add("transform", $"rotate({rotation} {ViewBoxWidth / 2.0} {ViewBoxHeight / 2.0})"); + Scale = Math.Min((pageHeight - margin * 4) / ViewBoxHeight, + (pageWidth - margin * 4) / ViewBoxWidth); + } + + float max = Math.Max(ViewBoxWidth, ViewBoxHeight); + + if (_canvas != null) + { + _canvas.SetBounds(SceneBounds, rotation); } + return rotation; } - private void DrawGridLines(SvgDocument doc, double rotation, Dictionary gridLines) + #endregion + + #region Private logic + + private void DrawGridLines(double rotation, Dictionary gridLines) { foreach (var line in gridLines) { - doc.Children.Add(line.Value.ToSvgLine(_sceneBounds.Min, ViewBoxHeight, GridContext)); - doc.Children.Add(new SvgCircle() - { - CenterX = line.Value.Start.X.ToXUserUnit(_sceneBounds.Min), - CenterY = line.Value.Start.Y.ToYUserUnit(ViewBoxHeight, _sceneBounds.Min), - Radius = new SvgUnit(SvgUnitType.User, (float)GridHeadRadius), - Stroke = new SvgColourServer(Colors.Black), - Fill = new SvgColourServer(Colors.White), - StrokeWidth = GridContext.StrokeWidth - }); - - var x = line.Value.Start.X.ToXUserUnit(_sceneBounds.Min); - var y = line.Value.Start.Y.ToYUserUnit(ViewBoxHeight, _sceneBounds.Min); - - var text = new SvgText(line.Key.ToString()) - { - X = new SvgUnitCollection() { x }, - Y = new SvgUnitCollection() { y }, - FontStyle = SvgFontStyle.Normal, - FontSize = new SvgUnit(SvgUnitType.User, 0.5f), - FontFamily = "Arial", - Fill = new SvgColourServer(Colors.Black), - TextAnchor = SvgTextAnchor.Middle, - }; - text.CustomAttributes.Add("transform", $"rotate({-1 * rotation} {x} {y}), translate(0, 0.2)"); - doc.Children.Add(text); + _canvas.DrawLine(line.Value, GridContext); + _canvas.DrawCircle(line.Value.Start, GridHeadRadius, GridContext); + _canvas.DrawText(line.Key.ToString(), new Transform(line.Value.Start), GridTextContext); } } @@ -589,7 +415,7 @@ private Dictionary ExtendSceneWithGridLines() var t = start + new Vector3(0, GridHeadRadius); var b = start + new Vector3(0, -GridHeadRadius); - _sceneBounds.Extend(start, end, l, r, t, b); + SceneBounds.Extend(start, end, l, r, t, b); } return gridLines; } diff --git a/Elements.Serialization.SVG/src/SvgSectionOld.cs b/Elements.Serialization.SVG/src/SvgSectionOld.cs new file mode 100644 index 000000000..0eecb4f12 --- /dev/null +++ b/Elements.Serialization.SVG/src/SvgSectionOld.cs @@ -0,0 +1,403 @@ +using Elements.Geometry; +using Svg; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Colors = System.Drawing.Color; + +namespace Elements.Serialization.SVG +{ + /// + /// A section of a model serialized to SVG. + /// + internal class SvgSectionOld + { + #region Private fields + + private readonly List _models = new List(); + private Dictionary _gridLines = new Dictionary(); + private readonly double _elevation; + private BBox3 _sceneBounds; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of SvgDrawingPlan class. + /// + /// A collection of models to include in the plan. + /// The elevation at which the plan will be cut. + public SvgSectionOld(IList models, double elevation) + { + this._models.AddRange(models); + this._elevation = elevation; + } + + #endregion + + #region Properties + + /// + /// The svg context which defines settings for elements cut by the cut plane. + /// + public SvgContext FrontContext { get; set; } = new SvgContext(Colors.Black, System.Drawing.Color.Black, 0.01); + + /// + /// The svg context which defines settings for elements behind the cut plane. + /// + public SvgContext BackContext { get; set; } = new SvgContext(System.Drawing.Color.Black, 0.01); + + /// + /// The svg context which defines settings for the grid elements. + /// + public SvgContext GridContext { get; set; } = new SvgContext(Colors.Black, 0.01, new double[] { 0.3, 0.025, 0.05, 0.025 }); + + /// + /// Should grid lines be shown in the section? + /// + public bool ShowGrid { get; set; } = true; + + /// + /// The extension of the grid head past the bounds of the drawing in the created plan. + /// + public double GridHeadExtension { get; set; } = 2.0; + + /// + /// The radius of grid heads in the created plan. + /// + public double GridHeadRadius { get; set; } = 0.5; + + /// + /// How should the plan be rotated relative to the page. + /// + public PlanRotation PlanRotation { get; set; } = PlanRotation.Angle; + + /// + /// An additional amount to rotate the plan. + /// + public double PlanRotationDegrees { get; set; } = 0.0; + + internal float ViewBoxWidth { get; private set; } + internal float ViewBoxHeight { get; private set; } + + #endregion + + #region Public logic + + /// + /// Create a plan of a model at the provided elevation and save the + /// resulting section to the provided path. + /// + /// A collection of models to include in the plan. + /// The elevation at which the plan will be cut. + /// An svg context which defines settings for elements cut by the cut plane. + /// An svg context which defines settings for elements behind the cut plane. + /// The location on disk to write the SVG file. + /// If gridlines exist, should they be shown in the plan? + /// The extension of the grid head past the bounds of the drawing in the created plan. + /// The radius of grid heads in the created plan. + /// How should the plan be rotated relative to the page? + /// An additional amount to rotate the plan. + public static void CreateAndSavePlanFromModels(IList models, + double elevation, + SvgContext frontContext, + SvgContext backContext, + string path, + bool showGrid = true, + double gridHeadExtension = 2.0, + double gridHeadRadius = 0.5, + PlanRotation planRotation = PlanRotation.Angle, + double planRotationDegrees = 0.0) + { + var doc = CreatePlanFromModels(models, elevation, frontContext, backContext, showGrid, gridHeadExtension, gridHeadRadius, planRotation, planRotationDegrees); + using var stream = new FileStream(path, FileMode.Create, FileAccess.Write); + doc.Write(stream); + } + + /// + /// Generate a plan of a model at the provided elevation. + /// + /// A collection of models to include in the plan. + /// The elevation at which the plan will be cut. + /// An svg context which defines settings for elements cut by the cut plane. + /// An svg context which defines settings for elements behind the cut plane. + /// If gridlines exist, should they be shown in the plan? + /// The extension of the grid head past the bounds of the drawing in the created plan. + /// The radius of grid heads in the created plan. + /// How should the plan be rotated relative to the page? + /// An additional amount to rotate the plan. + public static SvgDocument CreatePlanFromModels(IList models, + double elevation, + SvgContext frontContext, + SvgContext backContext, + bool showGrid = true, + double gridHeadExtension = 2.0, + double gridHeadRadius = 0.5, + PlanRotation planRotation = PlanRotation.Angle, + double planRotationDegrees = 0.0) + { + return CreatePlanFromModels(models, elevation, frontContext, backContext, out _, showGrid, gridHeadExtension, gridHeadRadius, + planRotation, planRotationDegrees); + } + + /// + /// Generate a plan of a model at the provided elevation. + /// + /// A collection of models to include in the plan. + /// The elevation at which the plan will be cut. + /// An svg context which defines settings for elements cut by the cut plane. + /// An svg context which defines settings for elements behind the cut plane. + /// The scene bounds. It can be used to add elements to the output document + /// If gridlines exist, should they be shown in the plan? + /// The extension of the grid head past the bounds of the drawing in the created plan. + /// The radius of grid heads in the created plan. + /// How should the plan be rotated relative to the page? + /// An additional amount to rotate the plan. + public static SvgDocument CreatePlanFromModels(IList models, + double elevation, + SvgContext frontContext, + SvgContext backContext, + out BBox3 sceneBounds, + bool showGrid = true, + double gridHeadExtension = 2.0, + double gridHeadRadius = 0.5, + PlanRotation planRotation = PlanRotation.Angle, + double planRotationDegrees = 0.0) + { + var drawingPlan = new SvgSectionOld(models, elevation); + drawingPlan.BackContext = backContext; + drawingPlan.FrontContext = frontContext; + drawingPlan.ShowGrid = showGrid; + drawingPlan.GridHeadExtension = gridHeadExtension; + drawingPlan.GridHeadRadius = gridHeadRadius; + drawingPlan.PlanRotation = planRotation; + drawingPlan.PlanRotationDegrees = planRotationDegrees; + + var doc = drawingPlan.CreateSvgDocument(); + sceneBounds = drawingPlan.GetSceneBounds(); + return doc; + } + + /// + /// Generate a plan of a model. + /// + public SvgDocument CreateSvgDocument() + { + _sceneBounds = SvgSectionOld.ComputeSceneBounds(_models); + + var doc = new SvgDocument + { + Fill = SvgPaintServer.None + }; + + var rotation = SvgSectionOld.GetRotationValueForPlan(_models, PlanRotation, PlanRotationDegrees); + + _gridLines.Clear(); + if (ShowGrid) + { + _gridLines = ExtendSceneWithGridLines(); + } + + CreateViewBox(doc, rotation); + Draw(doc, rotation); + return doc; + } + + /// + /// Get the scene bounds. + /// + public BBox3 GetSceneBounds() + { + return _sceneBounds; + } + + #endregion + + #region Private logic + + private static double GetRotationValueForPlan(IList models, PlanRotation rotation, double angle) + { + if (rotation == PlanRotation.Angle) + { + return angle; + } + + var grids = models.SelectMany(m => m.AllElementsOfType()).Select(gl => gl.Curve).Where(gl => gl is Line).ToList(); + if (!grids.Any()) + { + return 0.0; + } + + var longest = (Line)grids.OrderBy(g => g.Length()).First(); + + return rotation switch + { + PlanRotation.LongestGridHorizontal => -longest.Direction().PlaneAngleTo(Vector3.YAxis), + PlanRotation.LongestGridVertical => -longest.Direction().PlaneAngleTo(Vector3.XAxis), + PlanRotation.Angle => angle, + PlanRotation.None => 0.0, + _ => 0.0, + }; + } + + private static BBox3 ComputeSceneBounds(IList models) + { + var bounds = new BBox3(Vector3.Max, Vector3.Min); + foreach (var model in models) + { + foreach (var element in model.Elements) + { + if (element.Value is GeometricElement geo) + { + geo.UpdateRepresentations(); + if (geo.Representation == null || geo.Representation.SolidOperations.All(v => v.IsVoid)) + { + continue; + } + geo.UpdateBoundsAndComputeSolid(); + + var bbMax = geo.Transform.OfPoint(geo.Bounds.Max); + var bbMin = geo.Transform.OfPoint(geo.Bounds.Min); + bounds.Extend(new[] { bbMax, bbMin }); + } + } + } + + return bounds; + } + + private void Draw(SvgDocument doc, double rotation) + { + var plane = new Plane(new Vector3(0, 0, _elevation), Vector3.ZAxis); + var customElementsBeforeGrid = new List(); + var customElementsAfterGrid = new List(); + + foreach (var model in _models) + { + var modelWithoutCustomElements = model; + + modelWithoutCustomElements.Intersect(plane, + out Dictionary> intersecting, + out Dictionary> back, + out Dictionary> lines); + + foreach (var intersectingPolygon in intersecting) + { + foreach (var p in intersectingPolygon.Value) + { + doc.Children.Add(p.ToSvgPolygon(_sceneBounds.Min, ViewBoxHeight, FrontContext)); + } + } + + foreach (var backPolygon in back) + { + foreach (var p in backPolygon.Value) + { + doc.Children.Add(p.ToSvgPolygon(_sceneBounds.Min, ViewBoxHeight, BackContext)); + } + } + + foreach (var line in lines) + { + foreach (var l in line.Value) + { + doc.Children.Add(l.ToSvgLine(_sceneBounds.Min, ViewBoxHeight, FrontContext)); + } + } + + } + + customElementsBeforeGrid.ForEach(el => doc.Children.Add(el)); + DrawGridLines(doc, rotation, _gridLines); + customElementsAfterGrid.ForEach(el => doc.Children.Add(el)); + } + + private void CreateViewBox(SvgDocument doc, double rotation) + { + ViewBoxWidth = (float)(_sceneBounds.Max.X - _sceneBounds.Min.X); + ViewBoxHeight = (float)(_sceneBounds.Max.Y - _sceneBounds.Min.Y); + + + if (rotation == 0.0) + { + doc.ViewBox = new SvgViewBox(0, 0, ViewBoxWidth, ViewBoxHeight); + } + else + { + // Compute a new bounding box around the rotated + // bounding box, to ensure that we our viewbox isn't too small. + var t = new Transform(Vector3.Origin); + t.Rotate(rotation); + var bounds = new BBox3(_sceneBounds.Corners().Select(v => t.OfPoint(v))); + var wOld = ViewBoxWidth; + var hOld = ViewBoxHeight; + ViewBoxWidth = (float)(bounds.Max.X - bounds.Min.X); + ViewBoxHeight = (float)(bounds.Max.Y - bounds.Min.Y); + float max = Math.Max(ViewBoxWidth, ViewBoxHeight); + doc.ViewBox = new SvgViewBox((wOld - ViewBoxWidth) / 2.0f, (ViewBoxHeight - hOld) / 2.0f, ViewBoxWidth, ViewBoxHeight); + doc.CustomAttributes.Add("transform", $"rotate({rotation} {ViewBoxWidth / 2.0} {ViewBoxHeight / 2.0})"); + } + } + + private void DrawGridLines(SvgDocument doc, double rotation, Dictionary gridLines) + { + foreach (var line in gridLines) + { + doc.Children.Add(line.Value.ToSvgLine(_sceneBounds.Min, ViewBoxHeight, GridContext)); + doc.Children.Add(new SvgCircle() + { + CenterX = line.Value.Start.X.ToXUserUnit(_sceneBounds.Min), + CenterY = line.Value.Start.Y.ToYUserUnit(ViewBoxHeight, _sceneBounds.Min), + Radius = new SvgUnit(SvgUnitType.User, (float)GridHeadRadius), + Stroke = new SvgColourServer(Colors.Black), + Fill = new SvgColourServer(Colors.White), + StrokeWidth = GridContext.StrokeWidth + }); + + var x = line.Value.Start.X.ToXUserUnit(_sceneBounds.Min); + var y = line.Value.Start.Y.ToYUserUnit(ViewBoxHeight, _sceneBounds.Min); + + var text = new SvgText(line.Key.ToString()) + { + X = new SvgUnitCollection() { x }, + Y = new SvgUnitCollection() { y }, + FontStyle = SvgFontStyle.Normal, + FontSize = new SvgUnit(SvgUnitType.User, 0.5f), + FontFamily = "Arial", + Fill = new SvgColourServer(Colors.Black), + TextAnchor = SvgTextAnchor.Middle, + }; + text.CustomAttributes.Add("transform", $"rotate({-1 * rotation} {x} {y}), translate(0, 0.2)"); + doc.Children.Add(text); + } + } + + /// + /// Grow the bounds by the size of the grid and grid heads + /// + private Dictionary ExtendSceneWithGridLines() + { + var gridLines = new Dictionary(); + foreach (var g in _models.SelectMany(m => m.AllElementsOfType()).ToList()) + { + var gl = (Line)g.Curve; + var d = gl.Direction(); + var start = gl.Start + d.Negate() * GridHeadExtension; + var end = gl.End + d * GridHeadExtension; + gridLines.Add(g.Name, new Line(start, end)); + + var l = start + new Vector3(-GridHeadRadius, 0); + var r = start + new Vector3(GridHeadRadius, 0); + var t = start + new Vector3(0, GridHeadRadius); + var b = start + new Vector3(0, -GridHeadRadius); + + _sceneBounds.Extend(start, end, l, r, t, b); + } + return gridLines; + } + + #endregion + } +} diff --git a/Elements/src/GeometricElement.cs b/Elements/src/GeometricElement.cs index d986a9761..d05676ad2 100644 --- a/Elements/src/GeometricElement.cs +++ b/Elements/src/GeometricElement.cs @@ -51,7 +51,7 @@ public class GeometricElement : Element public Representation Representation { get; set; } /// - /// The list of element representations. + /// The list of element representations. /// [JsonIgnore] public List RepresentationInstances { get; set; } = new List(); @@ -196,7 +196,7 @@ public bool HasGeometry() /// The plane of intersection. /// A collection of polygons representing /// the intersections of the plane and the element's solid geometry. - /// A collection of polygons representing coplanar + /// A collection of polygons representing coplanar /// faces beyond the plane of intersection. /// A collection of lines representing intersections /// of zero-thickness elements with the plane.