diff --git a/ACadSharp.Image.Tests/SvgExportTests.cs b/ACadSharp.Image.Tests/SvgExportTests.cs index f07a6e7..dded84a 100644 --- a/ACadSharp.Image.Tests/SvgExportTests.cs +++ b/ACadSharp.Image.Tests/SvgExportTests.cs @@ -46,7 +46,7 @@ public void RenderSvgProducesSvgPage() } [Fact] - public void PointRadiusIsConvertedFromPixelsIntoDrawingUnits() + public void ScalingPointRadiusIsConvertedFromPixelsIntoDrawingUnits() { BlockRecord block = new("points"); block.Entities.Add(new Line(new XYZ(0, 0, 0), new XYZ(100, 50, 0))); @@ -55,6 +55,7 @@ public void PointRadiusIsConvertedFromPixelsIntoDrawingUnits() exporter.Configuration.Width = 1000; exporter.Configuration.Height = 500; exporter.Configuration.SetPadding(0); + exporter.Configuration.Svg.NonScalingStroke = false; exporter.Add(block); using RenderedPage page = Assert.Single(exporter.Render(ImageExportFormat.Svg)); @@ -66,6 +67,32 @@ public void PointRadiusIsConvertedFromPixelsIntoDrawingUnits() Assert.Equal("0.2", (string?)circle.Attribute("r")); } + [Theory] + [InlineData(100)] + [InlineData(216101.36407904)] + public void NonScalingPointMarkerKeepsItsPixelDiameterIndependentOfDrawingExtents(double extent) + { + BlockRecord block = new("point-marker"); + block.Entities.Add(new Line(new XYZ(0, 0, 0), new XYZ(extent, 100, 0))); + block.Entities.Add(new Point { Location = new XYZ(extent / 2, 50, 0) }); + ImageExporter exporter = new(); + exporter.Configuration.Width = 2000; + exporter.Configuration.Height = 2000; + exporter.Configuration.SetPadding(0); + exporter.Add(block); + + using RenderedPage page = Assert.Single(exporter.Render(ImageExportFormat.Svg)); + XElement marker = Assert.Single(XDocument.Parse(((RenderedSvgPage)page).Content).Descendants(), + element => element.Attribute("data-type")?.Value == "POINT"); + Assert.Equal("line", marker.Name.LocalName); + Assert.Equal(marker.Attribute("x1")?.Value, marker.Attribute("x2")?.Value); + Assert.Equal(marker.Attribute("y1")?.Value, marker.Attribute("y2")?.Value); + Assert.Equal("4", marker.Attribute("stroke-width")?.Value); + Assert.Equal("round", marker.Attribute("stroke-linecap")?.Value); + Assert.Equal("none", marker.Attribute("stroke-dasharray")?.Value); + Assert.Equal("non-scaling-stroke", marker.Attribute("vector-effect")?.Value); + } + [Fact] public void YAxisIsFlipped() { diff --git a/ACadSharp.Image/Rendering/EntityRenderDispatcher.cs b/ACadSharp.Image/Rendering/EntityRenderDispatcher.cs index 1f275c8..f697b1f 100644 --- a/ACadSharp.Image/Rendering/EntityRenderDispatcher.cs +++ b/ACadSharp.Image/Rendering/EntityRenderDispatcher.cs @@ -256,7 +256,7 @@ private void DrawPoint(ImageRenderContext context, ImageStyle style, ACadSharp.E { // DotSizePixels is a pixel size; SVG surface units are drawing units, so it has to be converted. float radius = Math.Max(1f, this._configuration.DotSizePixels / 2f); - context.Surface.FillCircle(style, context.ToSurfacePoint(point.Location), context.ToSurfacePixels(radius)); + context.Surface.DrawPointMarker(style, context.ToSurfacePoint(point.Location), context.ToSurfacePixels(radius), radius); } /// diff --git a/ACadSharp.Image/Rendering/IDrawingSurface.cs b/ACadSharp.Image/Rendering/IDrawingSurface.cs index ea81300..34fb033 100644 --- a/ACadSharp.Image/Rendering/IDrawingSurface.cs +++ b/ACadSharp.Image/Rendering/IDrawingSurface.cs @@ -67,6 +67,16 @@ internal interface IDrawingSurface : IDisposable void FillCircle(ImageStyle style, SurfacePoint center, double radius); + /// + /// Draws a point marker, retaining its pixel size on interactive SVG surfaces. + /// + /// Marker colour and opacity. + /// Marker centre in surface coordinates. + /// Radius in surface units for raster and scaling output. + /// Radius in display pixels for non-scaling output. + void DrawPointMarker(ImageStyle style, SurfacePoint center, double radius, double radiusPixels) + => this.FillCircle(style, center, radius); + void DrawText(ImageStyle style, SurfaceText text); /// diff --git a/ACadSharp.Image/Rendering/Svg/SvgDrawingSurface.cs b/ACadSharp.Image/Rendering/Svg/SvgDrawingSurface.cs index f030767..9463718 100644 --- a/ACadSharp.Image/Rendering/Svg/SvgDrawingSurface.cs +++ b/ACadSharp.Image/Rendering/Svg/SvgDrawingSurface.cs @@ -371,6 +371,33 @@ public void FillCircle(ImageStyle style, SurfacePoint center, double radius) new XAttribute("cx", this.N(center.X)), new XAttribute("cy", this.N(center.Y)), new XAttribute("r", this.N(Math.Abs(radius)))), style)); } + public void DrawPointMarker(ImageStyle style, SurfacePoint center, double radius, double radiusPixels) + { + if (!this._options.NonScalingStroke) + { + this.FillCircle(style, center, radius); + return; + } + + if (!IsFinite(center) || !IsFinite(radiusPixels) || radiusPixels <= 0d) + { + this.NotifyNonFinite(); + return; + } + + // A zero-length round-capped stroke is a dot. Unlike a filled circle, + // its non-scaling stroke keeps the configured diameter during zoom. + ImageStyle markerStyle = style with { StrokeWidth = (float)(2d * radiusPixels), DashPattern = null }; + XElement marker = new(Ns + "line", + new XAttribute("x1", this.N(center.X)), new XAttribute("y1", this.N(center.Y)), + new XAttribute("x2", this.N(center.X)), new XAttribute("y2", this.N(center.Y)), + new XAttribute("stroke-linecap", "round")); + marker = this.Stroked(marker, markerStyle); + marker.SetAttributeValue("stroke-width", this.S(markerStyle.StrokeWidth)); + marker.SetAttributeValue("stroke-dasharray", "none"); + this.Append(marker); + } + public void DrawText(ImageStyle style, SurfaceText text) { if (string.IsNullOrWhiteSpace(text.Text))