The Tool Loop Jun 6, 2015

Probably, the most craziest part of source code inside Aseprite is the “tool loop.” And as it is the craziest one, it is the one I’m most proud of (even when the code is ugly as hell). So, what is it?

Here is a “tool loop” in action:

It’s the whole process that begins when a mouse button is pressed, continues when the mouse is moved, and ends when the same mouse button is released.

Aseprite is distributed with a little gui.xml file. In this file there is a definition for each tool in the <tool> section. For example, the pencil tool is defined as:

<tool id="pencil"
      text="Pencil Tool"
      ink="paint"
      controller="freehand"
      pointshape="brush"
      intertwine="as_lines"
      tracepolicy="accumulate"
      />

What do those ink/controller/pointshape/intertwine/tracepolicy attributes mean?

Almost all tools are controlled by the ToolLoopManager class. This class receives UI events (mouse press/release, key down/up) from the sprite Editor (from DrawingState really). (E.g. when the user press a mouse button, the ToolLoopManager::pressButton is called.) This means that the ToolLoopManager could be used in isolation (e.g. by unit tests) to draw on the sprite simulating mouse buttons and keys.

Anyway the ToolLoopManager receives a delegate: a ToolLoop implementation. This class has several sub-delegates like an ink/controller/pointshape/intertwine/tracepolicy (these came from the gui.xml file configuration).

Each of these sub-delegates are used to control specific parts of the drawing process:

  1. The Controller creates the list of points to be connected/intertwined. For example, the FreehandController adds one point for each mouse movement, or the TwoPointsController is used to add just two points, the first one and the last one. (Used in line-like tools, e.g. line, rectangle, ellipse, etc.)
  2. The Intertwine joins those points generated by the Controller. The most common one is IntertwineAsLines, to join each sequential point with Bresenham’s lines. It’s used in freehand tools and line tool. Other interesting “intertwiner” is the IntertwineAsPixelPerfect dedicated to the pixel-perfect algorithm.
  3. For each generated pixel/point by the intertwiner, a PointShape is used. Its goal is to convert a pixel into scanlines for the current brush shape (or other shapes). There are three main point shapes:
  4. Each PointShape’s scanline is drawn using the Ink, specifically Ink::inkHline member function. Generally, ink implementations call a function defined in ink_processing.h. But those are a lot of details we prefer to avoid talking about.

The general idea here is that each tool is controlled by the union of these “mini” delegates: points from the mouse are pre-processed by the Controller, joined by the Intertwiner, converted to scanlines by the PointShape, and finally drawn by the Ink. And those elements can be combined in different ways. Does it mean that we can create our own tools in gui.xml? Yes, we could:

There are other (nasty) details like the trace policy: there are tools like the freehand that accumulate pixels, and other tools like the line that use the last line. Or the scroll tool that is handled by a specific Editor state, or the eyedropper tool that bypass the whole process using a dummy ink.

Tips: Sometimes you can separate a huge task in smaller classes (or functions) that can be dedicated to one step or specific goal in the whole algorithm. We are used to separate UI from business logic, but remember that you can separate UI (e.g. sprite Editor) from the same UI logic (ToolLoopManager). It’s a way to create UI logic without widgets.