PixiEditor's Core
PixiEditor’s Core
Section titled “PixiEditor’s Core”ChangeableDocument
Section titled “ChangeableDocument”ChangeableDocument is a system used to keep track of all changes done to a document.
The system is implemented in the ChangeableDocument project and it is the main part of PixiEditor that enables undo and redo functionality.
The Document class stores most of the data of a single document. This includes document dimensions, all layers with their bitmaps, blend modes,
all folders, selection, reference layer, and other stuff. The data can be freely read from the UI, but the only way to write into it
is by using the change pipeline implemented in the DocumentChangeTracker class.
DocumentChangeTracker class implements a pipeline which accepts IAction instances, modifies the Document based on them,
and returns IChangeInfo instances. IAction instances describe user’s actions. For example, ResizeCanvas_Action is created
and passed down the pipeline when the user resizes the canvas. It contains the new canvas size.
After accepting the ResizeCanvas_Action DocumentChangeTracker creates a new instances of the change that corresponds to it, in this case ResizeCanvas_Change.
The GUI passes actions to DocumentChangeTracker from the ActionAccumulator class.
Changes
Section titled “Changes”Regular Change
Section titled “Regular Change”The “change” is an object that describes some modification made to the
document, like changing it’s size. Changes have two main methods: Apply and Revert, used for applying and reverting the change.
These methods directly write into the contents of the Document class.
If we take changing document size as an example, Apply method applies the change, in this case,
it resizes the document to the new requested size, cropping the layer bitmaps. Revert goes the other way:
it restores the original size of the canvas, resizes the layer bitmaps back to the original size, and draws the areas on the edges that
were previously cut away. All data required to apply and revert the change is saved inside the change itself.
Apply and Revert functions return zero, one, or multiple instances of IChangeInfo. Each instance of IChangeInfo
concretely describes a change made to one or multiple properties of Document, for example “Document size property changed from 64x64
to 128x128”. These ChangeInfos are passed to the UI, and they are utilized to update the state of the UI.
UpdateableChange
Section titled “UpdateableChange”Some changes can’t be applied right away. Let’s take using a lasso tool to select an area as an example. When using a lasso tool, you first hold down left mouse button, then select the area, then release the button, and only then the final area is known. This means that the lasso change needs to get updates continiously while you are selecting.
This is what UpdateableChange is for. It has an ApplyTemporarily function that accepts updated data and returns
IChangeInfo’s. ApplyTemporarily is very similar to Apply, but it can be called multiple times while you are selecting
the area, and once you are done Apply is still called. If an updateable change gets reverted and then re-applied
ApplyTemporarily is not called.
UpdateableChange’s have two corresponding IAction’s: the first one implements IStartUpdateChangeAction
and is used to start and update the change; the second one implements IEndChangeAction and it indicates the end of
the change.
Here is an example of how you’d use actions to execute updateable changes. This example changes layer/folder opacity, like you would while dragging the opacity slider.
// user started dragging the slider, start changing opacity of layer/folder with guid memberGuid// initial value - 1.0f// this will create an updateable change and call it's `ApplyTemporarily`Helpers.ActionAccumulator.AddActions(new StructureMemberOpacity_Action(memberGuid, 1.0f));...// some time later, user dragged the slider to 0.5, update opacity to 0.5f// this will call `ApplyTemporarily` of the active updateable changeHelpers.ActionAccumulator.AddActions(new StructureMemberOpacity_Action(memberGuid, 0.5f));...// some time later, when the user let go of the slider, end the change// this calls `Apply` of the active updateable changeHelpers.ActionAccumulator.AddFinishedActions(new EndStructureMemberOpacity_Action());LayerImageChunks_ChangeInfo
Section titled “LayerImageChunks_ChangeInfo”Whenever something is drawn on one of the layer bitmaps, the GUI needs to know what area was updated.
ChangeableDocument uses LayerImageChunks_ChangeInfo to pass the updated areas to the GUI.
ChunkyImage
Section titled “ChunkyImage”ChunkyImageLib provides a bitmap class. ChunkyImage’s are used to store the contents of a layer and it’s mask.
You can draw on ChunkyImage by calling methods that enqueue various drawing operations.
The drawing operations can be cancelled, allowing you to preview the changes before committing them. For example, cancelling is used when transforming an area of the canvas, since the app needs to display the transformed image while it is being moved around. On every position change the old image drawing operation is cancelled, and a new one is enqueued in the new position. Other examples include drawing shapes, using brushes, and in general to anything that requires continious input from the user with immediate visual feedback.
Inside ChunkyImage, the image is stored as a set of 256x256 segments, or chunks. The drawing operations are applied individually and
independently to each chunk. They are applied in a lazy fashion, chunks are updated to the latest version only if you
try to access them. Moreover, there is an option to render chunks in low resolution. All of this enables PixiEditor to
display the above mentioned “continuous” operations in a very efficient way: parts of the image not currently on display
aren’t being rendered, and if you zoom far enough the rendering system switches to using a low resolution.
How ChangeableDocument uses ChunkyImages
Section titled “How ChangeableDocument uses ChunkyImages”In general, we draw on ChunkyImages from various Changes and UpdateableChanges. As an example, let’s take the Apply
function of ReplaceColor_Change. ReplaceColor_Change operates on all layers, replacing one color with another, e.g.
all red #FF0000 pixels are replaced by green #00FF00 pixels.
public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo){ ... // initialize a dictionary where old versions of the chunks will be stored // they are needed cause the change can get reverted in the future savedChunks = new();
List<IChangeInfo> infos = new(); // for each layer target.ForEveryMember(member => { if (member is not Layer layer) return;
// enqueue a color replacing operation in layer's ChunkyImage layer.LayerImage.EnqueueReplaceColor(oldColor, newColor);
// find all chunks affected by currently enqueued operations HashSet<VecI>? chunks = layer.LayerImage.FindAffectedChunks();
// save these chunks CommittedChunkStorage storage = new(layer.LayerImage, chunks); savedChunks[layer.GuidValue] = storage;
// fully commit changes layer.LayerImage.CommitChanges();
// create a new changeinfo for this layer infos.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, chunks)); }); // if no layers were modified, don't save this change in the undo/redo history ignoreInUndo = !savedChunks.Any();
// return all created infos return infos;}Notice how we first enqueue changes in ChunkyImage, then get affected chunks, save them in CommittedChunkStorage,
then commit them. This is a very common pattern, shared by many changes. ApplyTemporarily functions of
UpdateableChanges usually don’t commit the changes, as they may need to be cancelled on the next update (next
call to ApplyTemporarily). Instead the changes are committed in the Apply function.
All of our content is carefully written by hand, no AI was involved during the process.