Added Separate Shapes command

This commit is contained in:
Krzysztof Krysiński 2025-06-01 16:39:57 +02:00
parent a4e6f475ab
commit 15b8f543ce
12 changed files with 221 additions and 974 deletions

@ -1 +1 @@
Subproject commit 6a22e1b5affd0906a949f97b755ae588eda7980a Subproject commit 45f50107675b9212a28d0e5a8d28bf15765f3eeb

View File

@ -72,6 +72,37 @@ public static class NodeOperations
return node; return node;
} }
public static List<IChangeInfo> AppendMember(Node parent, Node toAppend, out Dictionary<Guid, VecD> originalPositions)
{
InputProperty<Painter?>? parentInput = parent.GetInputProperty(OutputNode.InputPropertyName) as InputProperty<Painter?>;
if (parentInput == null)
{
throw new InvalidOperationException("Parent node does not have an input property for appending members.");
}
OutputProperty<Painter>? toAddOutput = toAppend.GetOutputProperty("Output") as OutputProperty<Painter>;
if (toAddOutput == null)
{
throw new InvalidOperationException("Node to append does not have an output property named 'Output'.");
}
InputProperty<Painter>? toAddInput = toAppend.GetInputProperty(OutputNode.InputPropertyName) as InputProperty<Painter>;
if (toAddInput == null)
{
throw new InvalidOperationException("Node to append does not have an input property for appending members.");
}
Guid memberId = toAppend.Id;
List<IChangeInfo> 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<IChangeInfo> AppendMember( public static List<IChangeInfo> AppendMember(
InputProperty<Painter?> parentInput, InputProperty<Painter?> parentInput,
OutputProperty<Painter> toAddOutput, OutputProperty<Painter> toAddOutput,

View File

@ -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<Guid> newMemberIds = new List<Guid>();
private Dictionary<Guid, VecD> originalPositions = new Dictionary<Guid, VecD>();
[GenerateMakeChangeAction]
public SeparateShapes_Change(Guid memberId)
{
this.memberId = memberId;
}
public override bool InitializeAndValidate(Document target)
{
if (target.TryFindMember<VectorLayerNode>(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<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
out bool ignoreInUndo)
{
VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(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<PathVectorData> separatedShapes = new List<PathVectorData>();
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<IChangeInfo> changes = new List<IChangeInfo>();
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<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
{
VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
node.EmbeddedShapeData = originalData.Clone() as PathVectorData;
List<IChangeInfo> changes = new List<IChangeInfo>();
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<VectorLayerNode>(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<Guid, VecD> positions)
{
foreach (var position in positions)
{
originalPositions[position.Key] = position.Value;
}
}
public override void Dispose()
{
base.Dispose();
originalData?.Path?.Dispose();
}
}

View File

@ -1037,7 +1037,7 @@
"SECONDARY_BG_COLOR": "Secondary background color", "SECONDARY_BG_COLOR": "Secondary background color",
"RESET": "Reset", "RESET": "Reset",
"AUTOSAVE_OPEN_FOLDER": "Open autosave folder", "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", "AUTOSAVE_TOGGLE_DESCRIPTIVE": "Enable/disable autosave",
"ERROR_GRAPH": "Graph setup produced an error. Fix it in the node graph", "ERROR_GRAPH": "Graph setup produced an error. Fix it in the node graph",
"COLOR_MATRIX_FILTER_NODE": "Color Matrix Filter", "COLOR_MATRIX_FILTER_NODE": "Color Matrix Filter",
@ -1048,5 +1048,7 @@
"RENDER_OUTPUT_SIZE": "Render Output Size", "RENDER_OUTPUT_SIZE": "Render Output Size",
"RENDER_OUTPUT_CENTER": "Render Output Center", "RENDER_OUTPUT_CENTER": "Render Output Center",
"COLOR_PICKER": "Color Picker", "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"
} }

View File

@ -1,4 +1,5 @@
using Avalonia.Threading; using System.Diagnostics;
using Avalonia.Threading;
using PixiEditor.ChangeableDocument; using PixiEditor.ChangeableDocument;
using PixiEditor.ChangeableDocument.Actions; using PixiEditor.ChangeableDocument.Actions;
using PixiEditor.ChangeableDocument.Actions.Generated; using PixiEditor.ChangeableDocument.Actions.Generated;

View File

@ -956,4 +956,14 @@ internal class DocumentOperationsModule : IDocumentOperations
Internals.ActionAccumulator.AddFinishedActions(new ConvertToCurve_Action(memberId)); 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));
}
} }

View File

@ -77,9 +77,13 @@
</Border> </Border>
<Border Grid.Row="0" Grid.Column="1"> <Border Grid.Row="0" Grid.Column="1">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
<Button />
<Button />
<ToggleButton Margin="0, 5" Width="24" HorizontalAlignment="Center" Classes="PlayButton" <ToggleButton Margin="0, 5" Width="24" HorizontalAlignment="Center" Classes="PlayButton"
Name="PART_PlayToggle" Name="PART_PlayToggle"
IsChecked="{Binding IsPlaying, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" /> IsChecked="{Binding IsPlaying, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
<Button/>
<Button/>
<TextBlock VerticalAlignment="Center" FontSize="14"> <TextBlock VerticalAlignment="Center" FontSize="14">
<Run> <Run>
<Run.Text> <Run.Text>

View File

@ -116,7 +116,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
using var block = doc.Operations.StartChangeBlock(); using var block = doc.Operations.StartChangeBlock();
Guid? guid = doc.Operations.CreateStructureMember(StructureMemberType.Folder); Guid? guid = doc.Operations.CreateStructureMember(StructureMemberType.Folder);
if(doc.SoftSelectedStructureMembers.Count == 0) if (doc.SoftSelectedStructureMembers.Count == 0)
return; return;
var selectedInOrder = doc.GetSelectedMembersInOrder(); var selectedInOrder = doc.GetSelectedMembersInOrder();
selectedInOrder.Reverse(); selectedInOrder.Reverse();
@ -571,6 +571,18 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
doc!.Operations.ConvertToCurve(member.Id); doc!.Operations.ConvertToCurve(member.Id);
} }
[Command.Basic("PixiEditor.Layer.SeparateShapes", "SEPARATE_SHAPES", "SEPARATE_SHAPES_DESCRIPTIVE",
CanExecute = "PixiEditor.Layer.SelectedMemberIsVectorLayer")]
public void SeparateShapes()
{
var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
var member = doc?.SelectedStructureMember;
if (member is null)
return;
doc!.Operations.SeparateShapes(member.Id);
}
[Evaluator.Icon("PixiEditor.Layer.ToggleReferenceLayerTopMostIcon")] [Evaluator.Icon("PixiEditor.Layer.ToggleReferenceLayerTopMostIcon")]
public IImage GetAboveEverythingReferenceLayerIcon() public IImage GetAboveEverythingReferenceLayerIcon()
{ {

View File

@ -1,350 +0,0 @@
using Drawie.Backend.Core.Vector;
using Drawie.Numerics;
namespace PixiEditor.Views.Overlays.PathOverlay;
public class EditableVectorPath
{
private VectorPath? path;
public VectorPath? Path
{
get => path;
set
{
UpdatePathFrom(value);
path = value;
}
}
private List<SubShape> subShapes = new List<SubShape>();
public IReadOnlyList<SubShape> SubShapes => subShapes;
public int TotalPoints => subShapes.Sum(x => x.Points.Count);
public PathFillType FillType { get; set; }
public int ControlPointsCount
{
get
{
// count verbs with control points
return subShapes.Sum(x => CountControlPoints(x.Points));
}
}
public EditableVectorPath(IEnumerable<SubShape> subShapes, PathFillType fillType)
{
this.subShapes = new List<SubShape>(subShapes);
FillType = fillType;
}
public EditableVectorPath(VectorPath path)
{
if (path != null)
{
Path = new VectorPath(path);
UpdatePathFrom(Path);
}
else
{
this.path = null;
}
}
public VectorPath ToVectorPath()
{
VectorPath newPath;
if (Path != null)
{
newPath = new VectorPath(Path);
newPath.Reset(); // preserve fill type and other properties
}
else
{
newPath = new VectorPath();
}
newPath.FillType = FillType;
foreach (var subShape in subShapes)
{
AddVerbToPath(CreateMoveToVerb(subShape), newPath);
for (int i = 0; i < subShape.Points.Count; i++)
{
AddVerbToPath(subShape.Points[i].Verb, newPath);
}
if (subShape.IsClosed)
{
newPath.Close();
}
}
return newPath;
}
private static Verb CreateMoveToVerb(SubShape subShape)
{
VecF[] points = new VecF[4];
points[0] = subShape.Points[0].Position;
return new Verb((PathVerb.Move, points, 0));
}
private void UpdatePathFrom(VectorPath from)
{
subShapes.Clear();
if (from == null)
{
path = null;
return;
}
int currentSubShapeStartingIndex = 0;
bool isSubShapeClosed = false;
int globalVerbIndex = 0;
List<ShapePoint> currentSubShapePoints = new List<ShapePoint>();
foreach (var data in from)
{
if (data.verb == PathVerb.Done)
{
if (!isSubShapeClosed)
{
subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
}
}
else if (data.verb == PathVerb.Close)
{
isSubShapeClosed = true;
VecF[] verbData = data.points.ToArray();
if (currentSubShapePoints[^1].Verb.IsEmptyVerb())
{
verbData[0] = currentSubShapePoints[^2].Verb.To;
verbData[1] = currentSubShapePoints[0].Verb.From;
if (verbData[0] != verbData[1])
{
AddVerb((PathVerb.Line, verbData, 0), currentSubShapePoints);
}
currentSubShapePoints.RemoveAt(currentSubShapePoints.Count - 1);
}
subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
currentSubShapePoints.Clear();
}
else
{
isSubShapeClosed = false;
if (data.verb == PathVerb.Move)
{
if (currentSubShapePoints.Count > 0)
{
subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
currentSubShapePoints.Clear();
currentSubShapePoints.Add(new ShapePoint(data.points[0], 0, new Verb()));
}
else
{
currentSubShapePoints.Add(new ShapePoint(data.points[0], 0, new Verb()));
}
}
else
{
AddVerb(data, currentSubShapePoints);
}
}
globalVerbIndex++;
}
FillType = from.FillType;
}
private void AddVerbToPath(Verb verb, VectorPath newPath)
{
if (verb.IsEmptyVerb())
{
return;
}
switch (verb.VerbType)
{
case PathVerb.Move:
newPath.MoveTo(verb.From);
break;
case PathVerb.Line:
newPath.LineTo(verb.To);
break;
case PathVerb.Quad:
newPath.QuadTo(verb.ControlPoint1.Value, verb.To);
break;
case PathVerb.Cubic:
newPath.CubicTo(verb.ControlPoint1.Value, verb.ControlPoint2.Value, verb.To);
break;
case PathVerb.Conic:
newPath.ConicTo(verb.ControlPoint1.Value, verb.To, verb.ConicWeight);
break;
case PathVerb.Close:
newPath.Close();
break;
}
}
private static void AddVerb((PathVerb verb, VecF[] points, float conicWeight) data,
List<ShapePoint> currentSubShapePoints)
{
VecF point = data.points[0];
int atIndex = Math.Max(0, currentSubShapePoints.Count - 1);
bool indexExists = currentSubShapePoints.Count > atIndex;
ShapePoint toAdd = new ShapePoint(point, atIndex, new Verb(data));
if (!indexExists)
{
currentSubShapePoints.Add(toAdd);
}
else
{
currentSubShapePoints[atIndex] = toAdd;
}
VecF to = Verb.GetPointFromVerb(data);
currentSubShapePoints.Add(new ShapePoint(to, atIndex + 1, new Verb()));
}
public SubShape GetSubShapeContainingIndex(int index)
{
int currentIndex = 0;
foreach (var subShape in subShapes)
{
if (currentIndex + subShape.Points.Count > index)
{
return subShape;
}
currentIndex += subShape.Points.Count;
}
return null;
}
private int CountControlPoints(IReadOnlyList<ShapePoint> points)
{
int count = 0;
foreach (var point in points)
{
if (point.Verb.VerbType != PathVerb.Cubic)
continue; // temporarily only cubic is supported for control points
if (point.Verb.ControlPoint1 != null)
{
count++;
}
if (point.Verb.ControlPoint2 != null)
{
count++;
}
}
return count;
}
public int GetSubShapePointIndex(int globalIndex, SubShape subShapeContainingIndex)
{
int currentIndex = 0;
foreach (var subShape in subShapes)
{
if (subShape == subShapeContainingIndex)
{
return globalIndex - currentIndex;
}
currentIndex += subShape.Points.Count;
}
return -1;
}
public int GetGlobalIndex(SubShape subShape, int pointIndex)
{
int currentIndex = 0;
foreach (var shape in subShapes)
{
if (shape == subShape)
{
return currentIndex + pointIndex;
}
currentIndex += shape.Points.Count;
}
return -1;
}
public VecD? GetClosestPointOnPath(VecD point, float maxDistanceInPixels)
{
VecD? closest = null;
foreach (var subShape in subShapes)
{
VecD? closestInSubShape = subShape.GetClosestPointOnPath(point, maxDistanceInPixels);
if (closestInSubShape != null)
{
if (closest == null ||
VecD.Distance(closestInSubShape.Value, point) < VecD.Distance(closest.Value, point))
{
closest = closestInSubShape;
}
}
}
return closest;
}
public int? AddPointAt(VecD point)
{
SubShape targetSubShape = null;
Verb verb = null;
foreach (var subShape in subShapes)
{
verb = subShape.FindVerbContainingPoint(point);
if (verb != null && !verb.IsEmptyVerb())
{
targetSubShape = subShape;
break;
}
}
if (targetSubShape != null)
{
int localIndex = targetSubShape.InsertPointAt((VecF)point, verb);
int globalIndex = GetGlobalIndex(targetSubShape, localIndex);
return globalIndex;
}
return null;
}
/*
public void NewSubShape(VecD point)
{
VecF pointF = (VecF)point;
ShapePoint newPoint = new ShapePoint(pointF, 0, new Verb(PathVerb.Move, pointF, pointF, null, null, 0));
var newSubShape = new SubShape(new List<ShapePoint>() { newPoint }, false);
subShapes.Add(newSubShape);
}*/
public void RemoveSubShape(SubShape subShapeContainingIndex)
{
if (subShapes.Contains(subShapeContainingIndex))
{
subShapes.Remove(subShapeContainingIndex);
}
}
}

View File

@ -1,152 +0,0 @@
using System.Diagnostics;
using Drawie.Backend.Core.Vector;
using Drawie.Numerics;
namespace PixiEditor.Views.Overlays.PathOverlay;
[DebuggerDisplay($"Position: {{{nameof(Position)}}}, Index: {{{nameof(Index)}}}")]
public class ShapePoint
{
public VecF Position { get; set; }
public int Index { get; set; }
public Verb Verb { get; set; }
public ShapePoint(VecF position, int index, Verb verb)
{
Position = position;
Index = index;
Verb = verb;
}
public void ConvertVerbToCubic()
{
if(Verb.IsEmptyVerb()) return;
VecF[] points = ConvertVerbToCubicPoints();
Verb = new Verb((PathVerb.Cubic, points, Verb.ConicWeight));
}
private VecF[] ConvertVerbToCubicPoints()
{
if (Verb.VerbType == PathVerb.Line)
{
return [Verb.From, Verb.ControlPoint1 ?? Verb.From, Verb.ControlPoint2 ?? Verb.To, Verb.To];
}
if (Verb.VerbType == PathVerb.Conic)
{
VecF mid1 = Verb.ControlPoint1 ?? Verb.From;
float fixedConic = 1 - Verb.ConicWeight;
// TODO: Make sure it is adjusted/works for other cases
// 0.77 works for oval case, it probably will need to be adjusted for other cases
// we don't have a case for any other shape than oval so right now it's hardcoded like so.
float factor = 2 * fixedConic / (0.77f + fixedConic);
VecF from1 = (mid1 - Verb.From);
from1 = new VecF(from1.X * factor, from1.Y * factor);
VecF from2 = (mid1 - Verb.To);
from2 = new VecF(from2.X * factor, from2.Y * factor);
VecF control1 = Verb.From + from1;
VecF control2 = Verb.To + from2;
return [Verb.From, control1, control2, Verb.To];
}
//TODO: Implement Quad to Cubic conversion
return [Verb.From, Verb.ControlPoint1 ?? Verb.From, Verb.ControlPoint2 ?? Verb.To, Verb.To];
}
}
[DebuggerDisplay($"{{{nameof(VerbType)}}}")]
public class Verb
{
public PathVerb? VerbType { get; }
public VecF From { get; set; }
public VecF To { get; set; }
public VecF? ControlPoint1 { get; set; }
public VecF? ControlPoint2 { get; set; }
public float ConicWeight { get; set; }
public Verb()
{
VerbType = null;
}
public Verb(PathVerb verb, VecF from, VecF to, VecF? controlPoint1, VecF? controlPoint2, float conicWeight)
{
VerbType = verb;
From = from;
To = to;
ControlPoint1 = controlPoint1;
ControlPoint2 = controlPoint2;
ConicWeight = conicWeight;
}
public Verb((PathVerb verb, VecF[] points, float conicWeight) verbData)
{
VerbType = verbData.verb;
From = verbData.points[0];
To = GetPointFromVerb(verbData);
ControlPoint1 = GetControlPoint(verbData, true);
ControlPoint2 = GetControlPoint(verbData, false);
ConicWeight = verbData.conicWeight;
}
public bool IsEmptyVerb()
{
return VerbType == null;
}
public static VecF GetPointFromVerb((PathVerb verb, VecF[] points, float conicWeight) data)
{
switch (data.verb)
{
case PathVerb.Move:
return data.points[0];
case PathVerb.Line:
return data.points[1];
case PathVerb.Quad:
return data.points[2];
case PathVerb.Cubic:
return data.points[3];
case PathVerb.Conic:
return data.points[2];
case PathVerb.Close:
return data.points[0];
case PathVerb.Done:
return new VecF();
default:
throw new ArgumentOutOfRangeException();
}
}
public static VecF? GetControlPoint((PathVerb verb, VecF[] points, float conicWeight) data, bool first)
{
int index = first ? 1 : 2;
switch (data.verb)
{
case PathVerb.Move:
return null;
case PathVerb.Line:
return null;
case PathVerb.Quad:
return data.points[index];
case PathVerb.Cubic:
return data.points[index];
case PathVerb.Conic:
return data.points[index];
case PathVerb.Close:
return null;
case PathVerb.Done:
return null;
default:
throw new ArgumentOutOfRangeException();
}
}
}

View File

@ -1,240 +0,0 @@
using System.Diagnostics;
using Drawie.Backend.Core.Vector;
using Drawie.Numerics;
namespace PixiEditor.Views.Overlays.PathOverlay;
[DebuggerDisplay($"Points: {{{nameof(Points)}}}, Closed: {{{nameof(IsClosed)}}}")]
public class SubShape
{
private List<ShapePoint> points;
public IReadOnlyList<ShapePoint> Points => points;
public bool IsClosed { get; private set; }
public ShapePoint? GetNextPoint(int nextToIndex)
{
if (nextToIndex + 1 < points.Count)
{
return points[nextToIndex + 1];
}
return IsClosed ? points[0] : null;
}
public ShapePoint? GetPreviousPoint(int previousToIndex)
{
if (previousToIndex - 1 >= 0)
{
return points[previousToIndex - 1];
}
return IsClosed ? points[^1] : null;
}
public SubShape(List<ShapePoint> points, bool isClosed)
{
this.points = new List<ShapePoint>(points);
IsClosed = isClosed;
}
public void RemovePoint(int i)
{
bool isFirst = i == 0;
bool isLast = i == points.Count - 1;
if (!isFirst)
{
var previousPoint = GetPreviousPoint(i);
var nextPoint = GetNextPoint(i);
if (previousPoint?.Verb != null && nextPoint?.Verb != null)
{
previousPoint.Verb.To = nextPoint.Position;
}
for (int j = i + 1; j < points.Count; j++)
{
points[j].Index--;
}
}
if (isLast)
{
points[^2].Verb = new Verb();
}
if (isFirst && points.Count > 2)
{
points[^1].Verb.To = points[1].Position;
}
points.RemoveAt(i);
if (points.Count < 3)
{
IsClosed = false;
}
}
public void SetPointPosition(int i, VecF newPos, bool updateControlPoints)
{
var shapePoint = points[i];
var oldPos = shapePoint.Position;
VecF delta = newPos - oldPos;
shapePoint.Position = newPos;
shapePoint.Verb.From = newPos;
if (updateControlPoints)
{
if (shapePoint.Verb.ControlPoint1 != null)
{
shapePoint.Verb.ControlPoint1 = shapePoint.Verb.ControlPoint1.Value + delta;
}
}
var previousPoint = GetPreviousPoint(i);
if (previousPoint?.Verb != null && previousPoint.Verb.To == oldPos)
{
previousPoint.Verb.To = newPos;
if (updateControlPoints)
{
if (previousPoint.Verb.ControlPoint2 != null)
{
previousPoint.Verb.ControlPoint2 = previousPoint.Verb.ControlPoint2.Value + delta;
}
}
}
}
public void AppendPoint(VecF point)
{
if (points.Count == 0)
{
VecF[] data = new VecF[4];
data[0] = VecF.Zero;
data[1] = point;
points.Add(new ShapePoint(point, 0, new Verb((PathVerb.Move, data, 0))));
}
else
{
var lastPoint = points[^1];
VecF[] data = new VecF[4];
data[0] = lastPoint.Position;
data[1] = point;
points.Add(new ShapePoint(point, lastPoint.Index + 1, new Verb((PathVerb.Line, data, 0))));
}
}
public int InsertPointAt(VecF point, Verb pointVerb)
{
int indexOfVerb = this.points.FirstOrDefault(x => x.Verb == pointVerb)?.Index ?? -1;
if (indexOfVerb == -1)
{
throw new ArgumentException("Verb not found in points list");
}
Verb onVerb = pointVerb;
if (onVerb.VerbType is PathVerb.Quad or PathVerb.Conic)
{
this.points[indexOfVerb].ConvertVerbToCubic();
onVerb = this.points[indexOfVerb].Verb;
}
var oldTo = onVerb.To;
VecF[] data = new VecF[4];
VecF insertPoint = point;
if (onVerb.VerbType == PathVerb.Line)
{
onVerb.To = point;
data = [onVerb.To, oldTo, VecF.Zero, VecF.Zero];
}
else
{
float t = VectorMath.GetNormalizedSegmentPosition(onVerb, point);
VecD oldControlPoint1 = (VecD)onVerb.ControlPoint1.Value;
VecD oldControlPoint2 = (VecD)onVerb.ControlPoint2.Value;
// de Casteljau's algorithm
var q0 = ((VecD)onVerb.From).Lerp(oldControlPoint1, t);
var q1 = oldControlPoint1.Lerp(oldControlPoint2, t);
var q2 = oldControlPoint2.Lerp((VecD)oldTo, t);
var r0 = q0.Lerp(q1, t);
var r1 = q1.Lerp(q2, t);
var s0 = r0.Lerp(r1, t);
onVerb.ControlPoint1 = (VecF)q0;
onVerb.ControlPoint2 = (VecF)r0;
onVerb.To = (VecF)s0;
data = [(VecF)s0, (VecF)r1, (VecF)q2, oldTo];
insertPoint = (VecF)s0;
}
this.points.Insert(indexOfVerb + 1,
new ShapePoint(insertPoint, indexOfVerb + 1, new Verb((onVerb.VerbType.Value, data, 0))));
for (int i = indexOfVerb + 2; i < this.points.Count; i++)
{
this.points[i].Index++;
}
return indexOfVerb + 1;
}
public VecD? GetClosestPointOnPath(VecD point, float maxDistanceInPixels)
{
for (int i = 0; i < points.Count; i++)
{
var currentPoint = points[i];
VecD? closest = VectorMath.GetClosestPointOnSegment(point, currentPoint.Verb);
if (closest != null && VecD.Distance(closest.Value, point) < maxDistanceInPixels)
{
return closest;
}
}
return null;
}
public Verb? FindVerbContainingPoint(VecD point)
{
foreach (var shapePoint in points)
{
if (VectorMath.IsPointOnSegment(point, shapePoint.Verb))
{
return shapePoint.Verb;
}
}
return null;
}
public void Close()
{
if (IsClosed)
{
return;
}
IsClosed = true;
if (points.Count > 1)
{
VecF[] data = new VecF[4];
data[0] = points[^1].Position;
data[1] = points[0].Position;
points.Add(new ShapePoint(points[0].Position, points[^1].Index + 1, new Verb((PathVerb.Line, data, 0))));
}
}
}

View File

@ -1,227 +0,0 @@
using Drawie.Backend.Core.Vector;
using Drawie.Numerics;
namespace PixiEditor.Views.Overlays.PathOverlay;
internal static class VectorMath
{
public static VecD? GetClosestPointOnSegment(VecD point, Verb verb)
{
if (verb == null || verb.IsEmptyVerb()) return null;
switch (verb.VerbType)
{
case PathVerb.Move:
return (VecD)verb.From;
case PathVerb.Line:
return ClosestPointOnLine((VecD)verb.From, (VecD)verb.To, point);
case PathVerb.Quad:
return GetClosestPointOnQuad(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
(VecD)verb.To);
case PathVerb.Conic:
return GetClosestPointOnConic(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
(VecD)verb.To,
verb.ConicWeight);
case PathVerb.Cubic:
return GetClosestPointOnCubic(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
(VecD)(verb.ControlPoint2 ?? verb.To), (VecD)verb.To);
case PathVerb.Close:
return (VecD)verb.From;
case PathVerb.Done:
break;
case null:
break;
default:
throw new ArgumentOutOfRangeException();
}
return null;
}
public static bool IsPointOnSegment(VecD point, Verb shapePointVerb)
{
if (shapePointVerb.IsEmptyVerb()) return false;
switch (shapePointVerb.VerbType)
{
case PathVerb.Move:
return Math.Abs(point.X - shapePointVerb.From.X) < 0.0001 &&
Math.Abs(point.Y - shapePointVerb.From.Y) < 0.0001;
case PathVerb.Line:
return IsPointOnLine(point, (VecD)shapePointVerb.From, (VecD)shapePointVerb.To);
case PathVerb.Quad:
return IsPointOnQuad(point, (VecD)shapePointVerb.From,
(VecD)(shapePointVerb.ControlPoint1 ?? shapePointVerb.From),
(VecD)shapePointVerb.To);
case PathVerb.Conic:
return IsPointOnConic(point, (VecD)shapePointVerb.From,
(VecD)(shapePointVerb.ControlPoint1 ?? shapePointVerb.From),
(VecD)shapePointVerb.To, shapePointVerb.ConicWeight);
case PathVerb.Cubic:
return IsPointOnCubic(point, (VecD)shapePointVerb.From,
(VecD)(shapePointVerb.ControlPoint1 ?? shapePointVerb.From),
(VecD)(shapePointVerb.ControlPoint2 ?? shapePointVerb.To), (VecD)shapePointVerb.To);
case PathVerb.Close:
break;
case PathVerb.Done:
break;
case null:
break;
default:
throw new ArgumentOutOfRangeException();
}
return false;
}
public static VecD ClosestPointOnLine(VecD start, VecD end, VecD point)
{
VecD startToPoint = point - start;
VecD startToEnd = end - start;
double sqrtMagnitudeToEnd = Math.Pow(startToEnd.X, 2) + Math.Pow(startToEnd.Y, 2);
double dot = startToPoint.X * startToEnd.X + startToPoint.Y * startToEnd.Y;
var t = dot / sqrtMagnitudeToEnd;
if (t < 0) return start;
if (t > 1) return end;
return start + startToEnd * t;
}
public static bool IsPointOnLine(VecD point, VecD start, VecD end)
{
return Math.Abs(VecD.Distance(start, point) + VecD.Distance(end, point) - VecD.Distance(start, end)) < 0.001f;
}
public static VecD GetClosestPointOnQuad(VecD point, VecD start, VecD controlPoint, VecD end)
{
return FindClosestPointBruteForce(point, (t) => QuadraticBezier(start, controlPoint, end, t));
}
public static VecD GetClosestPointOnCubic(VecD point, VecD start, VecD controlPoint1, VecD controlPoint2, VecD end)
{
return FindClosestPointBruteForce(point, (t) => CubicBezier(start, controlPoint1, controlPoint2, end, t));
}
public static VecD GetClosestPointOnConic(VecD point, VecD start, VecD controlPoint, VecD end, float weight)
{
return FindClosestPointBruteForce(point, (t) => ConicBezier(start, controlPoint, end, weight, t));
}
public static bool IsPointOnQuad(VecD point, VecD start, VecD controlPoint, VecD end)
{
return IsPointOnPath(point, (t) => QuadraticBezier(start, controlPoint, end, t));
}
public static bool IsPointOnCubic(VecD point, VecD start, VecD controlPoint1, VecD controlPoint2, VecD end)
{
return IsPointOnPath(point, (t) => CubicBezier(start, controlPoint1, controlPoint2, end, t));
}
public static bool IsPointOnConic(VecD point, VecD start, VecD controlPoint, VecD end, float weight)
{
return IsPointOnPath(point, (t) => ConicBezier(start, controlPoint, end, weight, t));
}
/// <summary>
/// Finds value from 0 to 1 that represents the position of point on the segment.
/// </summary>
/// <param name="onVerb">Verb that represents the segment.</param>
/// <param name="point">Point that is on the segment.</param>
/// <returns>Value from 0 to 1 that represents the position of point on the segment.</returns>
public static float GetNormalizedSegmentPosition(Verb onVerb, VecF point)
{
if (onVerb.IsEmptyVerb()) return 0;
if (onVerb.VerbType == PathVerb.Cubic)
{
return (float)FindNormalizedSegmentPositionBruteForce(point, (t) =>
CubicBezier((VecD)onVerb.From, (VecD)(onVerb.ControlPoint1 ?? onVerb.From),
(VecD)(onVerb.ControlPoint2 ?? onVerb.To), (VecD)onVerb.To, t));
}
throw new NotImplementedException();
}
private static VecD FindClosestPointBruteForce(VecD point, Func<double, VecD> func, double step = 0.001)
{
double minDistance = double.MaxValue;
VecD closestPoint = new VecD();
for (double t = 0; t <= 1; t += step)
{
VecD currentPoint = func(t);
double distance = VecD.Distance(point, currentPoint);
if (distance < minDistance)
{
minDistance = distance;
closestPoint = currentPoint;
}
}
return closestPoint;
}
private static double FindNormalizedSegmentPositionBruteForce(VecF point, Func<double, VecD> func,
double step = 0.001)
{
double minDistance = float.MaxValue;
double closestT = 0;
for (double t = 0; t <= 1; t += step)
{
VecD currentPoint = func(t);
float distance = (point - currentPoint).Length;
if (distance < minDistance)
{
minDistance = distance;
closestT = t;
}
}
return closestT;
}
private static bool IsPointOnPath(VecD point, Func<double, VecD> func, double step = 0.001)
{
for (double t = 0; t <= 1; t += step)
{
VecD currentPoint = func(t);
if (VecD.Distance(point, currentPoint) < 0.1)
{
return true;
}
}
return false;
}
private static VecD QuadraticBezier(VecD start, VecD control, VecD end, double t)
{
double x = Math.Pow(1 - t, 2) * start.X + 2 * (1 - t) * t * control.X + Math.Pow(t, 2) * end.X;
double y = Math.Pow(1 - t, 2) * start.Y + 2 * (1 - t) * t * control.Y + Math.Pow(t, 2) * end.Y;
return new VecD(x, y);
}
private static VecD CubicBezier(VecD start, VecD control1, VecD control2, VecD end, double t)
{
double x = Math.Pow(1 - t, 3) * start.X + 3 * Math.Pow(1 - t, 2) * t * control1.X +
3 * (1 - t) * Math.Pow(t, 2) * control2.X + Math.Pow(t, 3) * end.X;
double y = Math.Pow(1 - t, 3) * start.Y + 3 * Math.Pow(1 - t, 2) * t * control1.Y +
3 * (1 - t) * Math.Pow(t, 2) * control2.Y + Math.Pow(t, 3) * end.Y;
return new VecD(x, y);
}
private static VecD ConicBezier(VecD start, VecD control, VecD end, float weight, double t)
{
double b0 = (1 - t) * (1 - t);
double b1 = 2 * t * (1 - t);
double b2 = t * t;
VecD numerator = (start * b0) + (control * b1 * weight) + (end * b2);
double denominator = b0 + (b1 * weight) + b2;
return numerator / denominator;
}
}