Files
2026-07-13 13:13:17 +08:00

375 lines
14 KiB
C#

#nullable enable
using System.Drawing;
using System.Drawing.Drawing2D;
using Lib.Utils;
using Svg;
using Svg.Pathing;
using Svg.Transforms;
using T3.Core.Utils;
namespace Lib.point.io;
[Guid("e8d94dd7-eb54-42fe-a7b1-b43543dd457e")]
internal sealed class LoadSvg : Instance<LoadSvg>, IDescriptiveFilename
{
[Output(Guid = "e21e3843-7d63-4db2-9234-77664e872a0f")]
public readonly Slot<StructuredList> ResultList = new();
public LoadSvg()
{
_svgResource = new Resource<SvgDocument>(FilePath, SvgLoader.TryLoad);
_svgResource.AddDependentSlots(ResultList);
ResultList.UpdateAction += Update;
_pointListWithSeparator.TypedElements[_pointListWithSeparator.NumElements - 1] = Point.Separator();
}
private struct GraphicsPathEntry
{
public GraphicsPath GraphicsPath;
public bool NeedsClosing;
}
private void Update(EvaluationContext context)
{
if (!_svgResource.TryGetValue(context, out var svgDoc) && !Scale.IsDirty && !CenterToBounds.IsDirty && !ScaleToBounds.IsDirty && !ImportAs.IsDirty && !ReduceFactor.IsDirty)
{
// Nothing changed, keep existing data
return;
}
if (svgDoc == null)
{
_pointListWithSeparator.SetLength(0);
ResultList.Value = _pointListWithSeparator;
return;
}
var centerToBounds = CenterToBounds.GetValue(context);
var scaleToBounds = ScaleToBounds.GetValue(context);
var bounds = new Vector3(svgDoc.Bounds.Size.Width, svgDoc.Bounds.Size.Height, 0);
var fitBoundsFactor = scaleToBounds ? (2f / bounds.Y) : 1;
var scale = Scale.GetValue(context) * fitBoundsFactor;
var importMode = ImportAs.GetValue(context);
var importAsLines = importMode == 0;
var importAsShape = importMode == 2;
var reduceFactor = ReduceFactor.GetValue(context).Clamp(0.001f, 1f);
var selectedShapeIndex = SelectSingleShape.GetValue(context); // Get the selected shape index
var svgElements = svgDoc.Descendants();
var pathElements = importAsShape
? GetSelectedShapePathElements(svgElements, selectedShapeIndex)
: ConvertAllNodesIntoGraphicPaths(svgElements, importAsLines);
// Calculate actual bounds and center offset based on import mode
Vector3 centerOffset;
if (importAsShape && pathElements.Count > 0)
{
// Get actual bounds from the shape path
var minX = float.MaxValue;
var minY = float.MaxValue;
var maxX = float.MinValue;
var maxY = float.MinValue;
foreach (var pathElement in pathElements)
{
var pathBounds = pathElement.GraphicsPath.GetBounds();
if (pathBounds.Left < minX) minX = pathBounds.Left;
if (pathBounds.Right > maxX) maxX = pathBounds.Right;
if (pathBounds.Bottom < minY) minY = pathBounds.Bottom;
if (pathBounds.Top > maxY) maxY = pathBounds.Top;
}
var shapeWidth = maxX - minX;
var shapeHeight = maxY - minY;
// Center offset should account for the actual position of the shape
centerOffset = centerToBounds
? new Vector3(-(minX + shapeWidth / 2), (minY + shapeHeight / 2), 0)
: Vector3.Zero;
}
else
{
centerOffset = centerToBounds ? new Vector3(-bounds.X / 2, bounds.Y / 2, 0) : Vector3.Zero;
}
// Flatten and sum total point count including separators
var totalPointCount = 0;
foreach (var p in pathElements)
{
p.GraphicsPath.Flatten(null, reduceFactor);
var closePoint = p.NeedsClosing ? 1 : 0;
totalPointCount += p.GraphicsPath.PointCount + 1 + closePoint;
}
if (totalPointCount != _pointListWithSeparator.NumElements)
{
_pointListWithSeparator.SetLength(totalPointCount);
}
var pointIndex = 0;
foreach (var pathElement in pathElements)
{
var startIndex = pointIndex;
var path = pathElement.GraphicsPath;
var pathPointCount = path.PathPoints.Length;
for (var pathPointIndex = 0; pathPointIndex < pathPointCount; pathPointIndex++)
{
var point = path.PathPoints[pathPointIndex];
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].Position
= (new Vector3(point.X, 1 - point.Y, 0) + centerOffset) * scale;
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].F1 = 1;
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].Orientation = Quaternion.Identity;
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].Color = new Vector4(1.0f); // We need a better fix, maybe with the colors from the SVG file
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].F2 = 1;
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].Scale = Vector3.One;
}
// Calculate normals
if (pathPointCount > 1)
{
for (var pathPointIndex = 0; pathPointIndex < pathPointCount; pathPointIndex++)
{
if (pathPointIndex == 0)
{
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].Orientation =
RotationFromTwoPositions(_pointListWithSeparator.TypedElements[0].Position,
_pointListWithSeparator.TypedElements[1].Position);
}
else if (pathPointIndex == pathPointCount - 1)
{
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].Orientation =
RotationFromTwoPositions(_pointListWithSeparator.TypedElements[pathPointCount - 2].Position,
_pointListWithSeparator.TypedElements[pathPointCount - 1].Position);
}
else
{
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].Orientation =
RotationFromTwoPositions(_pointListWithSeparator.TypedElements[startIndex + pathPointIndex].Position,
_pointListWithSeparator.TypedElements[startIndex + pathPointIndex + 1].Position);
}
}
}
// Close loop?
if (pathElement.NeedsClosing)
{
_pointListWithSeparator.TypedElements[startIndex + pathPointCount] = _pointListWithSeparator.TypedElements[startIndex];
pointIndex++;
}
pointIndex += path.PathPoints.Length;
_pointListWithSeparator.TypedElements[pointIndex] = Point.Separator();
pointIndex++;
}
ResultList.Value = _pointListWithSeparator;
}
private static Quaternion RotationFromTwoPositions(Vector3 p1, Vector3 p2)
{
return Quaternion.CreateFromAxisAngle(new Vector3(0, 0, 1), (float)(Math.Atan2(p1.X - p2.X, -(p1.Y - p2.Y)) + Math.PI / 2));
}
/// <summary>
/// Gets a specific path element by index from all SVG paths
/// </summary>
private static List<GraphicsPathEntry> GetSelectedShapePathElements(IEnumerable<SvgElement> nodes, int selectedIndex)
{
var paths = new List<GraphicsPathEntry>();
_svgRenderer ??= SvgRenderer.FromImage(new Bitmap(1, 1));
// Collect all SvgPath elements
var allSvgPaths = nodes.OfType<SvgPath>().ToList();
if (allSvgPaths.Count == 0)
return paths;
// Clamp the selected index to valid range
var clampedIndex = selectedIndex;
if (clampedIndex < 0)
clampedIndex = 0;
if (clampedIndex >= allSvgPaths.Count)
clampedIndex = allSvgPaths.Count - 1;
var targetPath = allSvgPaths[clampedIndex];
// Let the library build the full path (handles the new AddToPath API internally)
var fullPath = targetPath.Path(_svgRenderer);
SplitGraphicsPathIntoSubPaths(fullPath, paths);
return paths;
}
private static List<GraphicsPathEntry> ConvertAllNodesIntoGraphicPaths(IEnumerable<SvgElement> nodes, bool importAsLines)
{
var paths = new List<GraphicsPathEntry>();
_svgRenderer ??= SvgRenderer.FromImage(new Bitmap(1, 1));
foreach (var node in nodes)
{
switch (node)
{
case SvgPath svgPath:
{
// Let the library build the full path (handles the new AddToPath API internally)
var fullPath = svgPath.Path(_svgRenderer);
SplitGraphicsPathIntoSubPaths(fullPath, paths);
break;
}
case SvgGroup:
break;
case SvgPathBasedElement element:
{
if (element is SvgRectangle rect)
{
if (rect.Transforms != null)
{
foreach (var t in rect.Transforms)
{
if (t is not SvgTranslate tr)
continue;
rect.X += tr.X;
rect.Y += tr.Y;
}
}
}
var needsClosing = element is SvgRectangle or SvgCircle or SvgEllipse;
var graphicsPath = element.Path(_svgRenderer);
paths.Add(new GraphicsPathEntry
{
GraphicsPath = graphicsPath,
NeedsClosing = needsClosing && importAsLines
});
break;
}
}
}
return paths;
}
public InputSlot<string> SourcePathSlot => FilePath;
private readonly Resource<SvgDocument> _svgResource;
private readonly StructuredList<Point> _pointListWithSeparator = new(101);
[Input(Guid = "EF2A461D-C66D-44D8-8B0E-E48A57EC991F")]
public readonly InputSlot<string> FilePath = new();
[Input(Guid = "C6692E97-E7F8-4B3F-95BC-5F86C2B399A5")]
public readonly InputSlot<float> Scale = new();
[Input(Guid = "4DFCE92E-9282-486F-A274-E59402696BBB")]
public readonly InputSlot<bool> CenterToBounds = new();
[Input(Guid = "221BF10C-B13E-40CF-80AF-769C10A21C5B")]
public readonly InputSlot<bool> ScaleToBounds = new();
[Input(Guid = "8D63C134-1257-4331-AE84-F5EB6DD66C13", MappedType = typeof(ImportModes))]
public readonly InputSlot<int> ImportAs = new();
[Input(Guid = "2BB64740-ED2F-4295-923D-D585D70197E7")]
public readonly InputSlot<float> ReduceFactor = new();
[Input(Guid = "05E5AEC4-35A7-48DD-8F79-91EF754D20E8")]
public readonly InputSlot<int> SelectSingleShape = new();
[SuppressMessage("ReSharper", "UnusedMember.Local")]
private enum ImportModes
{
Lines,
Points,
Shape
}
/// <summary>
/// Splits a GraphicsPath into sub-paths at each StartPoint marker.
/// This avoids manual segment iteration with AddToPath, letting the SVG library
/// handle path construction internally via SvgPath.Path(renderer).
/// </summary>
private static void SplitGraphicsPathIntoSubPaths(GraphicsPath? fullPath, List<GraphicsPathEntry> paths)
{
if (fullPath == null || fullPath.PointCount == 0)
return;
var points = fullPath.PathPoints;
var types = fullPath.PathTypes;
GraphicsPath? currentSubPath = null;
var subPathStart = PointF.Empty;
for (var i = 0; i < points.Length; i++)
{
var pathType = types[i];
var pointType = (PathPointType)(pathType & 0x07); // mask off flags
var isStartPoint = pointType == PathPointType.Start;
var isCloseSubPath = (pathType & (byte)PathPointType.CloseSubpath) != 0;
if (isStartPoint)
{
// Flush previous sub-path
if (currentSubPath != null && currentSubPath.PointCount > 0)
{
paths.Add(new GraphicsPathEntry
{
GraphicsPath = currentSubPath,
NeedsClosing = false
});
}
currentSubPath = new GraphicsPath();
subPathStart = points[i];
}
currentSubPath ??= new GraphicsPath();
if (pointType == PathPointType.Line)
{
var prev = i > 0 ? points[i - 1] : subPathStart;
currentSubPath.AddLine(prev, points[i]);
}
else if (pointType == PathPointType.Bezier3 && i + 2 < points.Length)
{
// Cubic bezier uses 3 consecutive points (control1, control2, endpoint)
var prev = i > 0 ? points[i - 1] : subPathStart;
currentSubPath.AddBezier(prev, points[i], points[i + 1], points[i + 2]);
i += 2; // skip the two extra bezier points
}
if (isCloseSubPath && currentSubPath.PointCount > 0)
{
currentSubPath.CloseFigure();
paths.Add(new GraphicsPathEntry
{
GraphicsPath = currentSubPath,
NeedsClosing = false
});
currentSubPath = null;
}
}
// Flush last sub-path
if (currentSubPath != null && currentSubPath.PointCount > 0)
{
paths.Add(new GraphicsPathEntry
{
GraphicsPath = currentSubPath,
NeedsClosing = false
});
}
}
private static ISvgRenderer? _svgRenderer;
}