From 15b8f543cef69838d3c2c8c4ecd61fe6238f337a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Krysi=C5=84ski?= Date: Sun, 1 Jun 2025 16:39:57 +0200 Subject: [PATCH] Added Separate Shapes command --- src/Drawie | 2 +- .../Changes/NodeGraph/NodeOperations.cs | 31 ++ .../Changes/Vectors/SeparateShapes_Change.cs | 156 ++++++++ .../Data/Localization/Languages/en.json | 6 +- .../DocumentModels/ActionAccumulator.cs | 3 +- .../Public/DocumentOperationsModule.cs | 10 + .../Styles/Templates/Timeline.axaml | 4 + .../SubViewModels/LayersViewModel.cs | 14 +- .../PathOverlay/EditableVectorPath.cs | 350 ------------------ .../Views/Overlays/PathOverlay/ShapePoint.cs | 152 -------- .../Views/Overlays/PathOverlay/SubShape.cs | 240 ------------ .../Views/Overlays/PathOverlay/VectorMath.cs | 227 ------------ 12 files changed, 221 insertions(+), 974 deletions(-) create mode 100644 src/PixiEditor.ChangeableDocument/Changes/Vectors/SeparateShapes_Change.cs delete mode 100644 src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs delete mode 100644 src/PixiEditor/Views/Overlays/PathOverlay/ShapePoint.cs delete mode 100644 src/PixiEditor/Views/Overlays/PathOverlay/SubShape.cs delete mode 100644 src/PixiEditor/Views/Overlays/PathOverlay/VectorMath.cs diff --git a/src/Drawie b/src/Drawie index 6a22e1b5a..45f501076 160000 --- a/src/Drawie +++ b/src/Drawie @@ -1 +1 @@ -Subproject commit 6a22e1b5affd0906a949f97b755ae588eda7980a +Subproject commit 45f50107675b9212a28d0e5a8d28bf15765f3eeb diff --git a/src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs b/src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs index 077b01db0..7a2e5f218 100644 --- a/src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs +++ b/src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs @@ -72,6 +72,37 @@ public static class NodeOperations return node; } + public static List AppendMember(Node parent, Node toAppend, out Dictionary originalPositions) + { + InputProperty? parentInput = parent.GetInputProperty(OutputNode.InputPropertyName) as InputProperty; + if (parentInput == null) + { + throw new InvalidOperationException("Parent node does not have an input property for appending members."); + } + + OutputProperty? toAddOutput = toAppend.GetOutputProperty("Output") as OutputProperty; + if (toAddOutput == null) + { + throw new InvalidOperationException("Node to append does not have an output property named 'Output'."); + } + + InputProperty? toAddInput = toAppend.GetInputProperty(OutputNode.InputPropertyName) as InputProperty; + + if (toAddInput == null) + { + throw new InvalidOperationException("Node to append does not have an input property for appending members."); + } + + Guid memberId = toAppend.Id; + + List changes = AppendMember(parentInput, toAddOutput, toAddInput, memberId); + + var adjustedPositions = AdjustPositionsAfterAppend(toAppend, parent, parentInput.Connection?.Node as Node ?? null, out originalPositions); + + changes.AddRange(adjustedPositions); + return changes; + } + public static List AppendMember( InputProperty parentInput, OutputProperty toAddOutput, diff --git a/src/PixiEditor.ChangeableDocument/Changes/Vectors/SeparateShapes_Change.cs b/src/PixiEditor.ChangeableDocument/Changes/Vectors/SeparateShapes_Change.cs new file mode 100644 index 000000000..cc9d9dda8 --- /dev/null +++ b/src/PixiEditor.ChangeableDocument/Changes/Vectors/SeparateShapes_Change.cs @@ -0,0 +1,156 @@ +using ChunkyImageLib.Operations; +using Drawie.Backend.Core.Vector; +using Drawie.Numerics; +using PixiEditor.ChangeableDocument.Changeables.Graph; +using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes; +using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data; +using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph; +using PixiEditor.ChangeableDocument.ChangeInfos.Structure; +using PixiEditor.ChangeableDocument.ChangeInfos.Vectors; +using PixiEditor.ChangeableDocument.Changes.NodeGraph; + +namespace PixiEditor.ChangeableDocument.Changes.Vectors; + +internal class SeparateShapes_Change : Change +{ + private readonly Guid memberId; + private PathVectorData originalData; + private List newMemberIds = new List(); + private Dictionary originalPositions = new Dictionary(); + + [GenerateMakeChangeAction] + public SeparateShapes_Change(Guid memberId) + { + this.memberId = memberId; + } + + public override bool InitializeAndValidate(Document target) + { + if (target.TryFindMember(memberId, out VectorLayerNode? node)) + { + // Check if the node has embedded shape data and is not already a PathVectorData + return node.EmbeddedShapeData is PathVectorData p && GetShapeCount(p) > 1; + } + + return false; + } + + public override OneOf> Apply(Document target, bool firstApply, + out bool ignoreInUndo) + { + VectorLayerNode node = target.FindNodeOrThrow(memberId); + PathVectorData data = node.EmbeddedShapeData as PathVectorData ?? + throw new InvalidOperationException("Node does not contain PathVectorData."); + originalData = data.Clone() as PathVectorData; + + // Separate the shapes into individual PathVectorData instances + List separatedShapes = new List(); + var editablePath = new EditableVectorPath(data.Path); + foreach (var subShape in editablePath.SubShapes) + { + PathVectorData newShape = new PathVectorData(subShape.ToPath()) + { + Fill = data.Fill, + FillPaintable = data.FillPaintable, + Stroke = data.Stroke, + StrokeWidth = data.StrokeWidth, + TransformationMatrix = data.TransformationMatrix, + FillType = data.FillType, + StrokeLineCap = data.StrokeLineCap, + StrokeLineJoin = data.StrokeLineJoin, + }; + + separatedShapes.Add(newShape); + } + + // Replace the original data with the first separated shape + node.EmbeddedShapeData = separatedShapes[0]; + ignoreInUndo = false; + + List changes = new List(); + changes.Add(new VectorShape_ChangeInfo( + memberId, + new AffectedArea(OperationHelper.FindChunksTouchingRectangle( + (RectI)node.EmbeddedShapeData.TransformedVisualAABB, ChunkyImage.FullChunkSize)))); + + var previousNode = node; + + for (int i = 1; i < separatedShapes.Count; i++) + { + // Create a new node for each separated shape + VectorLayerNode newNode = node.Clone(false) as VectorLayerNode; + + if (firstApply) + { + newMemberIds.Add(newNode.Id); + } + else + { + newNode.Id = newMemberIds[i - 1]; + } + + newNode.EmbeddedShapeData = separatedShapes[i]; + + newNode.MemberName = $"{node.MemberName} (Shape {i + 1})"; // Rename to indicate it's a separate shape + + target.NodeGraph.AddNode(newNode); + changes.Add(CreateLayer_ChangeInfo.FromLayer(newNode)); + var appended = NodeOperations.AppendMember(previousNode, newNode, out var positions); + AppendPositions(positions); + changes.AddRange(appended); + + previousNode = newNode; + } + + return changes; + } + + public override OneOf> Revert(Document target) + { + VectorLayerNode node = target.FindNodeOrThrow(memberId); + node.EmbeddedShapeData = originalData.Clone() as PathVectorData; + + List changes = new List(); + + var aabb = node.EmbeddedShapeData.TransformedVisualAABB; + var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle( + (RectI)aabb, ChunkyImage.FullChunkSize)); + + changes.Add(new VectorShape_ChangeInfo(memberId, affected)); + + // Remove the newly created nodes + foreach (var newMemberId in newMemberIds) + { + var createdNode = target.FindNode(newMemberId); + if (createdNode != null) + { + target.NodeGraph.RemoveNode(createdNode); + createdNode?.Dispose(); + changes.Add(new DeleteNode_ChangeInfo(newMemberId)); + } + } + + originalPositions.Clear(); + + return changes; + } + + private int GetShapeCount(PathVectorData data) + { + return new EditableVectorPath(data.Path).SubShapes.Count; + } + + private void AppendPositions(Dictionary positions) + { + foreach (var position in positions) + { + originalPositions[position.Key] = position.Value; + } + } + + public override void Dispose() + { + base.Dispose(); + originalData?.Path?.Dispose(); + } +} diff --git a/src/PixiEditor/Data/Localization/Languages/en.json b/src/PixiEditor/Data/Localization/Languages/en.json index 9f1ca3ba4..deeeeb673 100644 --- a/src/PixiEditor/Data/Localization/Languages/en.json +++ b/src/PixiEditor/Data/Localization/Languages/en.json @@ -1037,7 +1037,7 @@ "SECONDARY_BG_COLOR": "Secondary background color", "RESET": "Reset", "AUTOSAVE_OPEN_FOLDER": "Open autosave folder", - "AUTOSAVE_OPEN_FOLDER_DESCRIPTIVE": "Open the folder where autosaves are stored", + "AUTOSAVE_OPEN_FOLDER_DESCRIPTIVE": "Open the folder where autosaves are stored", "AUTOSAVE_TOGGLE_DESCRIPTIVE": "Enable/disable autosave", "ERROR_GRAPH": "Graph setup produced an error. Fix it in the node graph", "COLOR_MATRIX_FILTER_NODE": "Color Matrix Filter", @@ -1048,5 +1048,7 @@ "RENDER_OUTPUT_SIZE": "Render Output Size", "RENDER_OUTPUT_CENTER": "Render Output Center", "COLOR_PICKER": "Color Picker", - "UNAUTHORIZED_ACCESS": "Unauthorized access" + "UNAUTHORIZED_ACCESS": "Unauthorized access", + "SEPARATE_SHAPES": "Separate Shapes", + "SEPARATE_SHAPES_DESCRIPTIVE": "Separate shapes from current vector into individual layers" } \ No newline at end of file diff --git a/src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs b/src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs index afb4b13df..d9e0d60af 100644 --- a/src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs +++ b/src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs @@ -1,4 +1,5 @@ -using Avalonia.Threading; +using System.Diagnostics; +using Avalonia.Threading; using PixiEditor.ChangeableDocument; using PixiEditor.ChangeableDocument.Actions; using PixiEditor.ChangeableDocument.Actions.Generated; diff --git a/src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs b/src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs index 36759d420..3e7081bc4 100644 --- a/src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs +++ b/src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs @@ -956,4 +956,14 @@ internal class DocumentOperationsModule : IDocumentOperations Internals.ActionAccumulator.AddFinishedActions(new ConvertToCurve_Action(memberId)); } + + public void SeparateShapes(Guid memberId) + { + if (Internals.ChangeController.IsBlockingChangeActive) + return; + + Internals.ChangeController.TryStopActiveExecutor(); + + Internals.ActionAccumulator.AddFinishedActions(new SeparateShapes_Action(memberId)); + } } diff --git a/src/PixiEditor/Styles/Templates/Timeline.axaml b/src/PixiEditor/Styles/Templates/Timeline.axaml index 6f825c391..706b053c8 100644 --- a/src/PixiEditor/Styles/Templates/Timeline.axaml +++ b/src/PixiEditor/Styles/Templates/Timeline.axaml @@ -77,9 +77,13 @@ +