Code/PolygonMeshBuilder.SVG.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Sandbox.Utility.Svg;
namespace Sandbox.Polygons;
/// <summary>
/// Options for <see cref="PolygonMeshBuilder.AddSvg"/>.
/// </summary>
public class AddSvgOptions
{
public static AddSvgOptions Default { get; } = new();
/// <summary>
/// If true, any unsupported path types will throw an exception. Defaults to false.
/// </summary>
public bool ThrowIfNotSupported { get; set; }
/// <summary>
/// Maximum distance between vertices on curved paths. Defaults to 1.
/// </summary>
public float CurveResolution { get; set; } = 1f;
public bool KeepAspectRatio { get; set; } = true;
}
partial class PolygonMeshBuilder
{
/// <summary>
/// Add all supported paths from the given SVG document.
/// </summary>
/// <param name="contents">SVG document contents.</param>
/// <param name="options">Options for generating vertices from paths.</param>
/// <param name="targetBounds">Rescale and translate the imported SVG to fill the given bounds</param>
public PolygonMeshBuilder AddSvg( string contents, AddSvgOptions options = null, Rect? targetBounds = null )
{
options ??= AddSvgOptions.Default;
var svg = SvgDocument.FromString( contents );
if ( svg.Paths.Count == 0 )
{
return this;
}
if ( targetBounds == null )
{
foreach ( var path in svg.Paths )
{
AddPath( path, options );
}
return this;
}
var bounds = svg.Paths[0].Bounds;
foreach ( var path in svg.Paths )
{
bounds.Add( path.Bounds );
}
var scale = targetBounds.Value.Size / bounds.Size;
var aspectOffset = Vector2.Zero;
if ( options.KeepAspectRatio )
{
var oldScale = scale;
scale = Math.Min( scale.x, scale.y );
aspectOffset = (oldScale - scale) * targetBounds.Value.Size * 0.25f;
}
var offset = targetBounds.Value.Position - bounds.Position * scale + aspectOffset;
foreach ( var path in svg.Paths )
{
AddPath( path, options, offset, scale );
}
return this;
}
private static void ThrowNotSupported( AddSvgOptions options, string message )
{
if ( !options.ThrowIfNotSupported )
{
return;
}
throw new NotImplementedException( $"SVG path element not supported: {message}" );
}
/// <summary>
/// Add an individual path from an SVG document, if supported.
/// </summary>
/// <param name="path">SVG path element.</param>
/// <param name="options">Options for generating vertices from paths.</param>
/// <param name="targetBounds">Rescale and translate the imported SVG to fill the given bounds</param>
public PolygonMeshBuilder AddPath( SvgPath path, AddSvgOptions options = null )
{
options ??= AddSvgOptions.Default;
return AddPath( path, options, Vector2.Zero, Vector2.One );
}
private PolygonMeshBuilder AddPath( SvgPath path, AddSvgOptions options, Vector2 offset, Vector2 scale )
{
if ( path.IsEmpty )
{
return this;
}
if ( path.FillColor == null )
{
return this;
}
if ( path.FillType != PathFillType.Winding )
{
if ( options.ThrowIfNotSupported )
{
//throw new NotImplementedException( "Only fill-type: winding is supported." );
}
//return this;
}
var openPath = new List<Vector2>();
var last = Vector2.Zero;
foreach ( var cmd in path.Commands )
{
switch ( cmd )
{
case AddPolyPathCommand addPolyPathCommand:
AddPolyPath( addPolyPathCommand, options, offset, scale );
break;
case AddCirclePathCommand addCirclePathCommand:
AddCirclePath( addCirclePathCommand, options, openPath, offset, scale );
break;
case MoveToPathCommand moveToPathCommand:
openPath.Clear();
openPath.Add( new Vector2( moveToPathCommand.X, moveToPathCommand.Y ) );
break;
case LineToPathCommand lineToPathCommand:
openPath.Add( new Vector2( lineToPathCommand.X, lineToPathCommand.Y ) );
break;
case CubicToPathCommand cubicToPathCommand:
CubicToPath( cubicToPathCommand, options, openPath, last );
break;
case ClosePathCommand:
if ( openPath.Count >= 3 )
{
AddEdgeLoop( openPath, 0, openPath.Count, offset, scale );
}
openPath.Clear();
break;
default:
ThrowNotSupported( options, $"{cmd.GetType()}" );
break;
}
if ( openPath.Count > 0 )
{
last = openPath[^1];
}
}
return this;
}
private void AddPolyPath( AddPolyPathCommand cmd, AddSvgOptions options, Vector2 offset, Vector2 scale )
{
if ( !cmd.Close )
{
return;
}
AddEdgeLoop( cmd.Points, 0, cmd.Points.Count, offset, scale );
}
private void AddCirclePath( AddCirclePathCommand cmd, AddSvgOptions options, List<Vector2> openPath, Vector2 offset, Vector2 scale )
{
openPath.Clear();
var center = new Vector2( cmd.X, cmd.Y );
for ( var i = 23; i >= 0; i-- )
{
var r = i * (MathF.PI * 2f / 24f);
var cos = MathF.Cos( r );
var sin = MathF.Sin( r );
openPath.Add( new Vector2( cos, sin ) * cmd.Radius + center );
}
AddEdgeLoop( openPath, 0, openPath.Count, offset, scale );
}
private void CubicToPath( CubicToPathCommand cmd, AddSvgOptions options, List<Vector2> openPath, Vector2 last )
{
var pointCount = 6;
var tScale = 1f / pointCount;
for ( var i = 0; i < pointCount; i++ )
{
var t = (i + 1) * tScale;
var s = 1f - t;
var a = s * s * s;
var b = 3f * s * s * t;
var c = 3f * s * t * t;
var d = t * t * t;
var p0 = last;
var p1 = new Vector2( cmd.X0, cmd.Y0 );
var p2 = new Vector2( cmd.X1, cmd.Y1 );
var p3 = new Vector2( cmd.X2, cmd.Y2 );
openPath.Add( p0 * a + p1 * b + p2 * c + p3 * d );
}
}
public string ToSvg()
{
var openEdges = new HashSet<int>( _activeEdges );
var writer = new StringWriter();
writer.WriteLine( "<svg xmlns=\"http://www.w3.org/2000/svg\">" );
while ( openEdges.Count > 0 )
{
var firstIndex = openEdges.First();
var edge = _allEdges[firstIndex];
writer.Write( " <polygon points=\"" );
while ( true )
{
writer.Write( $"{edge.Origin.x:R},{edge.Origin.y:R} " );
openEdges.Remove( edge.Index );
if ( edge.NextEdge == firstIndex )
{
break;
}
edge = _allEdges[edge.NextEdge];
}
writer.WriteLine("\" fill=\"black\" stroke=\"red\" />");
}
writer.WriteLine( @"</svg>" );
return writer.ToString();
}
}