#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, IDescriptiveFilename { [Output(Guid = "e21e3843-7d63-4db2-9234-77664e872a0f")] public readonly Slot ResultList = new(); public LoadSvg() { _svgResource = new Resource(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)); } /// /// Gets a specific path element by index from all SVG paths /// private static List GetSelectedShapePathElements(IEnumerable nodes, int selectedIndex) { var paths = new List(); _svgRenderer ??= SvgRenderer.FromImage(new Bitmap(1, 1)); // Collect all SvgPath elements var allSvgPaths = nodes.OfType().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 ConvertAllNodesIntoGraphicPaths(IEnumerable nodes, bool importAsLines) { var paths = new List(); _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 SourcePathSlot => FilePath; private readonly Resource _svgResource; private readonly StructuredList _pointListWithSeparator = new(101); [Input(Guid = "EF2A461D-C66D-44D8-8B0E-E48A57EC991F")] public readonly InputSlot FilePath = new(); [Input(Guid = "C6692E97-E7F8-4B3F-95BC-5F86C2B399A5")] public readonly InputSlot Scale = new(); [Input(Guid = "4DFCE92E-9282-486F-A274-E59402696BBB")] public readonly InputSlot CenterToBounds = new(); [Input(Guid = "221BF10C-B13E-40CF-80AF-769C10A21C5B")] public readonly InputSlot ScaleToBounds = new(); [Input(Guid = "8D63C134-1257-4331-AE84-F5EB6DD66C13", MappedType = typeof(ImportModes))] public readonly InputSlot ImportAs = new(); [Input(Guid = "2BB64740-ED2F-4295-923D-D585D70197E7")] public readonly InputSlot ReduceFactor = new(); [Input(Guid = "05E5AEC4-35A7-48DD-8F79-91EF754D20E8")] public readonly InputSlot SelectSingleShape = new(); [SuppressMessage("ReSharper", "UnusedMember.Local")] private enum ImportModes { Lines, Points, Shape } /// /// 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). /// private static void SplitGraphicsPathIntoSubPaths(GraphicsPath? fullPath, List 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; }