using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using ChunkyImageLib;
using ChunkyImageLib.DataHolders;
using PixiEditor.AvaloniaUI.Helpers.Extensions;
using PixiEditor.AvaloniaUI.Models.Clipboard;
using PixiEditor.AvaloniaUI.Models.DocumentModels.UpdateableChangeExecutors;
using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
using PixiEditor.AvaloniaUI.Models.Handlers;
using PixiEditor.AvaloniaUI.Models.Layers;
using PixiEditor.AvaloniaUI.Models.Position;
using PixiEditor.AvaloniaUI.Models.Tools;
using PixiEditor.ChangeableDocument.Actions.Generated;
using PixiEditor.ChangeableDocument.Actions.Undo;
using PixiEditor.ChangeableDocument.Enums;
using PixiEditor.DrawingApi.Core.Numerics;
using PixiEditor.DrawingApi.Core.Surface.Vector;
using PixiEditor.Extensions.CommonApi.Palettes;
using PixiEditor.Numerics;
namespace PixiEditor.AvaloniaUI.Models.DocumentModels.Public;
#nullable enable
internal class DocumentOperationsModule : IDocumentOperations
{
private IDocument Document { get; }
private DocumentInternalParts Internals { get; }
public DocumentOperationsModule(IDocument document, DocumentInternalParts internals)
{
Document = document;
Internals = internals;
}
///
/// Creates a new selection with the size of the document
///
public void SelectAll() => Select(new RectI(VecI.Zero, Document.SizeBindable), SelectionMode.Add);
///
/// Creates a new selection with the size of the document
///
public void Select(RectI rect, SelectionMode mode = SelectionMode.New)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(
new SelectRectangle_Action(rect, mode),
new EndSelectRectangle_Action());
}
///
/// Clears the current selection
///
public void ClearSelection()
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new ClearSelection_Action());
}
///
/// Deletes selected pixels
///
/// Should the selection be cleared
public void DeleteSelectedPixels(bool clearSelection = false)
{
var member = Document.SelectedStructureMember;
if (Internals.ChangeController.IsChangeActive || member is null)
return;
bool drawOnMask = member is ILayerHandler layer ? layer.ShouldDrawOnMask : true;
if (drawOnMask && !member.HasMaskBindable)
return;
Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.GuidValue, drawOnMask));
if (clearSelection)
Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
Internals.ActionAccumulator.AddFinishedActions();
}
///
/// Sets the opacity of the member with the guid
///
/// The Guid of the member
/// A value between 0 and 1
public void SetMemberOpacity(Guid memberGuid, float value)
{
if (Internals.ChangeController.IsChangeActive || value is > 1 or < 0)
return;
Internals.ActionAccumulator.AddFinishedActions(
new StructureMemberOpacity_Action(memberGuid, value),
new EndStructureMemberOpacity_Action());
}
///
/// Adds a new viewport or updates a existing one
///
public void AddOrUpdateViewport(ViewportInfo info) => Internals.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(info));
///
/// Deletes the viewport with the
///
/// The Guid of the viewport to remove
public void RemoveViewport(Guid viewportGuid) => Internals.ActionAccumulator.AddActions(new RemoveViewport_PassthroughAction(viewportGuid));
///
/// Delete the whole undo stack
///
public void ClearUndo()
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddActions(new DeleteRecordedChanges_Action());
}
///
/// Pastes the as new layers
///
/// The images to paste
public void PasteImagesAsLayers(List images)
{
if (Internals.ChangeController.IsChangeActive)
return;
RectI maxSize = new RectI(VecI.Zero, Document.SizeBindable);
foreach (var imageWithName in images)
{
maxSize = maxSize.Union(new RectI(imageWithName.position, imageWithName.image.Size));
}
if (maxSize.Size != Document.SizeBindable)
Internals.ActionAccumulator.AddActions(new ResizeCanvas_Action(maxSize.Size, ResizeAnchor.TopLeft));
foreach (var imageWithName in images)
{
var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer, Path.GetFileName(imageWithName.name));
DrawImage(imageWithName.image, new ShapeCorners(new RectD(imageWithName.position, imageWithName.image.Size)), layerGuid, true, false, false);
}
Internals.ActionAccumulator.AddFinishedActions();
}
///
/// Creates a new structure member of type with the name
///
/// The type of the member
/// The name of the member
/// The Guid of the new structure member or null if there is already an active change
public Guid? CreateStructureMember(StructureMemberType type, string? name = null, bool finish = true)
{
if (Internals.ChangeController.IsChangeActive)
return null;
return Internals.StructureHelper.CreateNewStructureMember(type, name, finish);
}
///
/// Duplicates the layer with the
///
/// The Guid of the layer
public void DuplicateLayer(Guid guidValue)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
}
///
/// Delete the member with the
///
/// The Guid of the layer
public void DeleteStructureMember(Guid guidValue)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMember_Action(guidValue));
}
///
/// Deletes all members with the
///
/// The Guids of the layers to delete
public void DeleteStructureMembers(IReadOnlyList guids)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(guids.Select(static guid => new DeleteStructureMember_Action(guid)).ToArray());
}
///
/// Resizes the canvas (Does not upscale the content of the image)
///
/// The size the canvas should be resized to
/// Where the existing content should be put
public void ResizeCanvas(VecI newSize, ResizeAnchor anchor)
{
if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
return;
if (Document.ReferenceLayerHandler.ReferenceBitmap is not null)
{
VecI offset = anchor.FindOffsetFor(Document.SizeBindable, newSize);
ShapeCorners curShape = Document.ReferenceLayerHandler.ReferenceShapeBindable;
ShapeCorners offsetCorners = new ShapeCorners()
{
TopLeft = curShape.TopLeft + offset,
TopRight = curShape.TopRight + offset,
BottomLeft = curShape.BottomLeft + offset,
BottomRight = curShape.BottomRight + offset,
};
Internals.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(offsetCorners), new EndTransformReferenceLayer_Action());
}
Internals.ActionAccumulator.AddFinishedActions(new ResizeCanvas_Action(newSize, anchor));
}
///
/// Resizes the image (Upscales the content of the image)
///
/// The size the image should be resized to
/// The resampling method to use
public void ResizeImage(VecI newSize, ResamplingMethod resampling)
{
if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
return;
if (Document.ReferenceLayerHandler.ReferenceBitmap is not null)
{
VecD scale = ((VecD)newSize).Divide(Document.SizeBindable);
ShapeCorners curShape = Document.ReferenceLayerHandler.ReferenceShapeBindable;
ShapeCorners offsetCorners = new ShapeCorners()
{
TopLeft = curShape.TopLeft.Multiply(scale),
TopRight = curShape.TopRight.Multiply(scale),
BottomLeft = curShape.BottomLeft.Multiply(scale),
BottomRight = curShape.BottomRight.Multiply(scale),
};
Internals.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(offsetCorners), new EndTransformReferenceLayer_Action());
}
Internals.ActionAccumulator.AddFinishedActions(new ResizeImage_Action(newSize, resampling));
}
///
/// Replaces all with
///
/// The color to replace
/// The new color
public void ReplaceColor(PaletteColor oldColor, PaletteColor newColor)
{
if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
return;
Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor.ToColor(), newColor.ToColor()));
ReplaceInPalette(oldColor, newColor);
}
private void ReplaceInPalette(PaletteColor oldColor, PaletteColor newColor)
{
int indexOfOldColor = Document.Palette.IndexOf(oldColor);
if(indexOfOldColor == -1)
return;
Document.Palette.RemoveAt(indexOfOldColor);
Document.Palette.Insert(indexOfOldColor, newColor);
}
///
/// Creates a new mask on the
///
public void CreateMask(IStructureMemberHandler member)
{
if (Internals.ChangeController.IsChangeActive)
return;
if (!member.MaskIsVisibleBindable)
Internals.ActionAccumulator.AddActions(new StructureMemberMaskIsVisible_Action(true, member.GuidValue));
Internals.ActionAccumulator.AddFinishedActions(new CreateStructureMemberMask_Action(member.GuidValue));
}
///
/// Deletes the mask of the
///
public void DeleteMask(IStructureMemberHandler member)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMemberMask_Action(member.GuidValue));
}
///
/// Applies the mask to the image
///
public void ApplyMask(IStructureMemberHandler member)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.GuidValue), new DeleteStructureMemberMask_Action(member.GuidValue));
}
///
/// Sets the selected structure memeber
///
/// The Guid of the member to select
public void SetSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new SetSelectedMember_PassthroughAction(memberGuid));
///
/// Adds a member to the soft selection
///
/// The Guid of the member to add
public void AddSoftSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new AddSoftSelectedMember_PassthroughAction(memberGuid));
///
/// Removes a member from the soft selection
///
/// The Guid of the member to remove
public void RemoveSoftSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new RemoveSoftSelectedMember_PassthroughAction(memberGuid));
///
/// Clears the soft selection
///
public void ClearSoftSelectedMembers() => Internals.ActionAccumulator.AddActions(new ClearSoftSelectedMembers_PassthroughAction());
public void AddSelectedKeyFrame(Guid keyFrameGuid) => Internals.ActionAccumulator.AddActions(new AddSelectedKeyFrame_PassthroughAction(keyFrameGuid));
public void RemoveSelectedKeyFrame(Guid keyFrameGuid) => Internals.ActionAccumulator.AddActions(new RemoveSelectedKeyFrame_PassthroughAction(keyFrameGuid));
public void ClearSelectedKeyFrames() => Internals.ActionAccumulator.AddActions(new ClearSelectedKeyFrames_PassthroughAction());
///
/// Undo last change
///
public void Undo()
{
if (Internals.ChangeController.IsChangeActive)
{
Internals.ChangeController.MidChangeUndoInlet();
return;
}
Internals.ActionAccumulator.AddActions(new Undo_Action());
}
///
/// Redo previously undone change
///
public void Redo()
{
if (Internals.ChangeController.IsChangeActive)
{
Internals.ChangeController.MidChangeRedoInlet();
return;
}
Internals.ActionAccumulator.AddActions(new Redo_Action());
}
public void NudgeSelectedObject(VecI distance)
{
if (Internals.ChangeController.IsChangeActive)
{
Internals.ChangeController.SelectedObjectNudgedInlet(distance);
}
}
///
/// Moves a member next to or inside another structure member
///
/// The member to move
/// The target member
/// Where to place the
public void MoveStructureMember(Guid memberToMove, Guid memberToMoveIntoOrNextTo, StructureMemberPlacement placement)
{
if (Internals.ChangeController.IsChangeActive || memberToMove == memberToMoveIntoOrNextTo)
return;
Internals.StructureHelper.TryMoveStructureMember(memberToMove, memberToMoveIntoOrNextTo, placement);
}
///
/// Merge all structure members with the Guids inside
///
public void MergeStructureMembers(IReadOnlyList members)
{
if (Internals.ChangeController.IsChangeActive || members.Count < 2)
return;
var (child, parent) = Document.StructureHelper.FindChildAndParent(members[0]);
if (child is null || parent is null)
return;
int index = parent.Children.IndexOf(child);
Guid newGuid = Guid.NewGuid();
//make a new layer, put combined image onto it, delete layers that were merged
Internals.ActionAccumulator.AddActions(
new CreateStructureMember_Action(parent.GuidValue, newGuid, index, StructureMemberType.Layer),
new StructureMemberName_Action(newGuid, child.NameBindable),
new CombineStructureMembersOnto_Action(members.ToHashSet(), newGuid));
foreach (var member in members)
Internals.ActionAccumulator.AddActions(new DeleteStructureMember_Action(member));
Internals.ActionAccumulator.AddActions(new ChangeBoundary_Action());
}
///
/// Starts a image transform and pastes the transformed image on the currently selected layer
///
/// The image to paste
/// Where the transform should start
public void PasteImageWithTransform(Surface image, VecI startPos)
{
if (Document.SelectedStructureMember is null)
return;
Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos));
}
///
/// Starts a image transform and pastes the transformed image on the currently selected layer
///
/// The image to paste
/// Where the transform should start
public void PasteImageWithTransform(Surface image, VecI startPos, Guid memberGuid, bool drawOnMask)
{
Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos, memberGuid, drawOnMask));
}
///
/// Starts a transform on the selected area
///
/// Is this transform started by a tool
public void TransformSelectedArea(bool toolLinked)
{
if (Document.SelectedStructureMember is null ||
Internals.ChangeController.IsChangeActive && !toolLinked ||
Document.SelectionPathBindable.IsEmpty)
return;
Internals.ChangeController.TryStartExecutor(new TransformSelectedAreaExecutor(toolLinked));
}
///
/// Ties stopping the currently executing tool linked executor
///
public void TryStopToolLinkedExecutor()
{
if (Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked)
Internals.ChangeController.TryStopActiveExecutor();
}
public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask) =>
DrawImage(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, true);
///
/// Draws a image on the member with the
///
/// The image to draw onto the layer
/// The shape the image should fit into
/// The Guid of the member to paste on
/// Ignore selection clipping and symmetry (See DrawingChangeHelper.ApplyClipsSymmetriesEtc of UpdateableDocument)
/// Draw on the mask or on the image
/// Is this a finished action
private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, bool finish)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddActions(
new PasteImage_Action(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask),
new EndPasteImage_Action());
if (finish)
Internals.ActionAccumulator.AddFinishedActions();
}
///
/// Resizes the canvas to fit the content
///
public void ClipCanvas()
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action());
}
///
/// Flips the image on the axis
///
public void FlipImage(FlipType flipType) => FlipImage(flipType, null);
///
/// Flips the members with the Guids of on the axis
///
public void FlipImage(FlipType flipType, List membersToFlip)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, membersToFlip));
}
///
/// Rotates the image
///
/// The degrees to rotate the image by
public void RotateImage(RotationAngle rotation) => RotateImage(rotation, null);
///
/// Rotates the members with the Guids of
///
/// The degrees to rotate the members by
public void RotateImage(RotationAngle rotation, List membersToRotate)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate));
}
///
/// Puts the content of the image in the middle of the canvas
///
public void CenterContent(IReadOnlyList structureMembers)
{
if (Internals.ChangeController.IsChangeActive)
return;
Internals.ActionAccumulator.AddFinishedActions(new CenterContent_Action(structureMembers.ToList()));
}
///
/// Imports a reference layer from a Pbgra Int32 array
///
/// The size of the image
public void ImportReferenceLayer(ImmutableArray imageBgra8888Bytes, VecI imageSize)
{
if (Internals.ChangeController.IsChangeActive)
return;
RectD referenceImageRect = new RectD(VecD.Zero, Document.SizeBindable).AspectFit(new RectD(VecD.Zero, imageSize));
ShapeCorners corners = new ShapeCorners(referenceImageRect);
Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imageBgra8888Bytes, imageSize));
}
///
/// Deletes the reference layer
///
public void DeleteReferenceLayer()
{
if (Internals.ChangeController.IsChangeActive || Document.ReferenceLayerHandler.ReferenceBitmap is null)
return;
Internals.ActionAccumulator.AddFinishedActions(new DeleteReferenceLayer_Action());
}
///
/// Starts a transform on the reference layer
///
public void TransformReferenceLayer()
{
if (Document.ReferenceLayerHandler.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)
return;
Internals.ChangeController.TryStartExecutor(new TransformReferenceLayerExecutor());
}
///
/// Resets the reference layer transform
///
public void ResetReferenceLayerPosition()
{
if (Document.ReferenceLayerHandler.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)
return;
VecD size = new(Document.ReferenceLayerHandler.ReferenceBitmap.Size.X, Document.ReferenceLayerHandler.ReferenceBitmap.Size.Y);
RectD referenceImageRect = new RectD(VecD.Zero, Document.SizeBindable).AspectFit(new RectD(VecD.Zero, size));
ShapeCorners corners = new ShapeCorners(referenceImageRect);
Internals.ActionAccumulator.AddFinishedActions(
new TransformReferenceLayer_Action(corners),
new EndTransformReferenceLayer_Action()
);
}
public void SelectionToMask(SelectionMode mode)
{
if (Document.SelectedStructureMember is not { } member || Document.SelectionPathBindable.IsEmpty)
return;
if (!Document.SelectedStructureMember.HasMaskBindable)
{
Internals.ActionAccumulator.AddActions(new CreateStructureMemberMask_Action(member.GuidValue));
}
Internals.ActionAccumulator.AddFinishedActions(new SelectionToMask_Action(member.GuidValue, mode));
}
public void CropToSelection(bool clearSelection = true)
{
var bounds = Document.SelectionPathBindable.TightBounds;
if (Document.SelectionPathBindable.IsEmpty || bounds.Width <= 0 || bounds.Height <= 0)
return;
Internals.ActionAccumulator.AddActions(new Crop_Action((RectI)bounds));
if (clearSelection)
{
Internals.ActionAccumulator.AddFinishedActions(new ClearSelection_Action());
}
else
{
Internals.ActionAccumulator.AddFinishedActions();
}
}
public void InvertSelection()
{
var selection = Document.SelectionPathBindable;
var inverse = new VectorPath();
inverse.AddRect(new RectI(new(0, 0), Document.SizeBindable));
Internals.ActionAccumulator.AddFinishedActions(new SetSelection_Action(inverse.Op(selection, VectorPathOp.Difference)));
}
public void SetActiveFrame(int value)
{
if (Internals.ChangeController.IsChangeActive || value is < 0)
return;
Internals.ActionAccumulator.AddFinishedActions(
new ActiveFrame_Action(value),
new EndActiveFrame_Action());
}
}