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()); } }