Added Separate Shapes command
This commit is contained in:
parent
a4e6f475ab
commit
15b8f543ce
@ -1 +1 @@
|
|||||||
Subproject commit 6a22e1b5affd0906a949f97b755ae588eda7980a
|
Subproject commit 45f50107675b9212a28d0e5a8d28bf15765f3eeb
|
@ -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,
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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;
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user