diff --git a/.adr-dir b/.adr-dir new file mode 100644 index 0000000..0d38988 --- /dev/null +++ b/.adr-dir @@ -0,0 +1 @@ +doc/architecture/decisions diff --git a/doc/architecture/decisions/0001-record-architecture-decisions.md b/doc/architecture/decisions/0001-record-architecture-decisions.md new file mode 100644 index 0000000..0c2d747 --- /dev/null +++ b/doc/architecture/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,19 @@ +# 1. Record architecture decisions + +Date: 2024-03-28 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). + +## Consequences + +See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). diff --git a/doc/architecture/decisions/0002-implement-graphics-object-store.md b/doc/architecture/decisions/0002-implement-graphics-object-store.md new file mode 100644 index 0000000..f930f0a --- /dev/null +++ b/doc/architecture/decisions/0002-implement-graphics-object-store.md @@ -0,0 +1,429 @@ +# 2. implement graphics object store + +Date: 2024-03-28 + +## Status + +Proposed + +## Context + +`gromit-mpx` currently follows a "draw-and-forget" strategy, i.e. each +drawing operation, once completed, is drawn on a `cairo_surface_t` +(`GromitData.backbuffer`). + +While this is very straightforward, it prevents the implementation of + +- **dynamic content**, e.g. items that fade after a certain time and + disappear, or otherwise change through time + +- **editing** of past content (e.g. select and move or delete an old item) + +Currently, a work-around exists in the form of an `aux_backbuffer`, +which stores the state prior to the drawing operation in +progress. This allows dynamic content during the ongoing drawing +operation, and is used in e.g. `LINE`, `SMOOTH`, `RECT`. +However, the drawing is split up over various callbacks (`on_press`, +`on_motion`, and `on_release`, which complicates code and maintenance). + +Here, I propose to implement a **graphics object store** that contains +the elements that have been produced so far. By re-executing these +drawing operations, the widget is (re)painted. + +## Implementation + +A graphics object store (`objects`) is added to `GromitData`. This +object store simply is a `GList` of `GfxObject`. Each `GfxObject` has +the following fields: + + typedef struct { + guint id; // unique id + GfxType type; // type of object, e.g. Stroke, or Text + gboolean is_dynamic; // content is dynamically updated + GromitDeviceData *selected; // NULL, or which user selected the item + guint32 capabilities; // bit mask with capabilities (see below) + BoundingBox extent; // the objects rectangular extent + void (*exec)(GromitData *, GfxObjectBase *, + GfxAction, void *); // object-specific methods + } GfxObjectBase; + +This basic set of fields is extended by object-specific fields, which +allows for an inheritance-like pattern, i.e. each specialized object +can be downcast to the above `GfxObjectBase`. + +For a normal stroked path, the specific field could be: + + typedef struct { + GfxObjectBase base; + GList *coordinates; + GdkRGBA color; + gfloat arrowsize; + ArrowType arrowtype; + } GfxObjectPath; + +For a text object, one could have: + + typedef struct { + GfxObjectBase base; + GdkRGBA color; + gchar *text; + gfloat fontsize; + gfloat x, y; + } GfxObjectPath; + +A path that fades away after a certain time could look like so: + + typedef struct { + GfxObjectBase base; + GList *coordinates; + GdkRGBA color; + gfloat arrowsize; + ArrowTyype arrowtype; + guint64 start_fading; // timestamps in + guint64 end_fading; // microseconds + } GfxObjectFadingPath; + +### Capabilities + +Each `gfx_object` contains a bit mask indicating its +"capabilities". These capabilities imply that certain actions are +implemented in the `do_action` function. + + + + + + + + + + + + + + + + + + + + + + + + + + +
Capability Description
draw object can be drawn
+ this would allow to add non-drawable "information" objects
draw_transformed object can be drawn in transformed state;
+ this allows for dynamic representation during operations such as dragging
move object can be moved
scale_isotropic object can be scaled isotropically
scale_anisotropic object can be scaled anisotropically,
+ i.e. differently in X and Y direction isotropic_scale also must be set
rotate object can be rotated
edit object is editable (e.g. TEXT or parts of a path)
+ this probably is hardest to implement, and left for the future
+ + +#### Use of capabilities with selections and during transformations + +When multiple `GfxObject`s are selected, the capability bitmasks could +be `and`ed to obtain the greatest common denominator. For example, +objects may be movable, but not rotateable, and then the rotate handle +is omitted in the GUI. Similarly, isotropic-only scaling would leave +only the corner handles, whereas anisotropic scaling would also +display handles in the middle of the sides (see +[here](#selection-wireframe) for selection wireframe). + +During transform operations, `draw_transformed` is called for each +object, with a 2D affine transformation matrix as argument. + +If an object cannot be drawn dynamically in transformed state, for +example because this is computationally to costly, then the +transformation is just shown by selection wireframe until the +operation completed. + +At the end of the operation, `transform` is called with the final +transformation matrix, which then modifies the object in the object +store. + +The 2D affine transformation functions are already implemented in +`coordlist_ops.c`. + +### Actions + +When calling `exec`, the `action` argument selects the task to be performed. + + + + + + + + + + + + + + + + + + + + + + + + + + + +
draw draw object in "normal" state
draw_selected draw object in "selected" state
draw_transformed draw object in intermediate "transformed" state;
+ this allows to display the object dynamically, e.g. while it is being dragged
update_bounding_box update the bounding box of the base object;
+ trigger recalculation of the bounding box
get_is_clicked detect whether a click at coordinate (x,y) "hits" the object, for example to select it
transform transforms the object, modifying the context in the gfx_store
destroy destroys the object, deallocating memory
clone deep-copies the object
+ + +### Drawing of object store + +Whenever objects need to be re-drawn, the `draw` function simply +iterates through the object store and calls `object->do_action(ptr, +draw_action)` for all objects. Objects with a bounding box not +intersecting the dirty region (`gdk_cairo_get_clip_rectangle()`) are +skipped. + +### Undo/redo system + +Each object that is added or transformed receives a new unique +`id`. This `id` is delivered by a "factory" function and is a simple +`guint32` that is incremented at each request. + +Maintaining a per-user (i.e. per device) undo system is complicated by +the fact that an object can be "taken over" by another user by +manipulating it. This is necessary to enable a truly collaborative +workflow. At the same time, users should only undo their own +operations. + +To solve this conflict, I propose the following system: + +- for each device, the `GdkSeat` is determined, since it links pointer + and keyboard devices that belong together. + +- for each `GdkSeat`, a `GromitSeat` which contains a `GList` of + `undo` and `redo` records. The `GromitSeat`s are stored in a hash + table in `GromitData` that associates the device with the seat. + +- `GromitData` is extended with an undo object store (`undo_objects`). + +- the steps necessary to revert a user's operation are recorded in the + respective device's `undo` records. + +- an undo operation executes the last undo operation according to the + `undo` record of the device, and moves it to the `redo` record. + +- a redo operation does the reverse, and moves the record back to the + end of the `undo` records. + +- any non-undo/redo operation clears the `redo` records. + +The `undo` records contain the `id` of the object to move from +`objects` to `undo_objects`, and the `id` of the object to move from +`undo_objects` to `objects`. An `id` of zero indicates that there is +no such object. + +- when a `GfxObject` is added, the `undo` record contains the + instruction to move the `GfxObject` with the particular `id` from + `object`to `undo_objects`. + +- when a `GfxObject` is deleted, this object is moved from `objects` + to `undo_objects`. The `undo` record consists of the instruction to + add the `GfxObject` with the particular `id` back to `objects`. + +- when a `GfxObject` is transformed, the object is deep-copied (action + `clone`) is created and placed in `undo_objects`. The transformed + object remains in `objects`but receives a new `id`. The `undo` + record for the respective device contains the instruction to swap + the old and new object (which are in the `undo_objects` and the + `object_store`, respectively). + +Handling of conflicts: + +- Conflicts emerge when a user has "taken over" an object from another + user. In this case, the objects referenced in the `undo` records no + longer exist. In this case, this `undo` record is discarded and the + next `undo` record processed. + + Example (here I use letters for the `id`): + + - user 1 adds objects A, B, and C: + +``` + objects -> A -> B -> C -> nil + + undo_objects -> nil + + dev1.undo -> [A->undo] -> [B->undo] -> [C->undo] + dev1.redo -> nil +``` + + - user 2 manipulates B, which becomes D: + +``` + objects -> A -> D -> C -> nil + + undo_objects -> B -> nil + + dev1.undo -> [A->undo] -> [B->undo] -> [C->undo] -> nil + dev1.redo -> nil + + dev2.undo -> [B->store, D->undo] -> nil + dev2.redo -> nil +``` + + - user 1 undos the last step: + + +``` + objects -> A -> D -> nil + + undo_objects -> B -> C -> nil + + dev1.undo -> [A->undo] -> [B->undo] -> nil + dev1.redo -> [C->store] -> nil + + dev2.undo -> [B->store, D->undo] -> nil + dev2.redo -> nil +``` + + - user 1 undos one more step, which fails because B is no longer + there; this step is discarded and the next step executed. + +``` + objects -> D -> nil + + undo_objects -> B -> C -> A -> nil + + dev1.undo -> nil + dev1.redo -> [C->store] -> [A->store] -> nil + + dev2.undo -> [B->store, D->undo] -> nil + dev2.redo -> nil +``` + +Notes: + +- When adding back previously deleted objects from the `undo_objects`, + they are placed at the very end of `objects`. This may change the + Z-order of the objects. + + **Question**: Should we add two more handles to the selection + wireframe? One to move the selection forward, and one to move the + selection backwards in Z-order? + +- Orphans could occur in the undo store. One possibility would be to + reference-count these objects and delete them when the last + reference disappears from the undo/redo buffers, but that would + require pointers or some related mechanism. The other, simpler, + possibility which I prefer is to garbage-collect these once the + `undo_objects` exceeds a maximum size. + +- When `undo_objects` exceeds a maximum size, the oldest items could + be dropped and the `undo`/`redo` records referencing these be deleted. + +## Migration to new drawing system + +### First step + +* **Object store** + + The `objects` store is added to `GromitData`, together with some + housekeeping functions. + +* **Callbacks** + + Then, the callbacks are adapted to add data to the object store + instead of `coordlist`. Direct drawing is replaced by a subsequent + call of `draw`. + + - `on_press` adds a new item of the respective type to the object + store, and "remembers" its `id` (for example, in the hash_table). + + - `on_motion` appends coordinates to the respective `GfxObject`, and + calls `draw`. + + - `on_release` finalizes the operation, for example by smoothing a + path (SMOOTH tool), and then calling `draw`. + + - for each tool, the specific drawing operation is implemented. The + only drawing operation that is required at this first stage is the + drawing of a simple stroke path. + +These are relatively minor changes. With these, the new architecture +already is functional. + +### Second step + +The "undo/redo" functions are implemented. + +### Thirs step + +A "select" tool is implemented. This requires that the `GfxObject` +implement `get_is_clicked` and `draw_selected`. + +The select tool draws a wireframe around the selection, showing the +respective handles (see "capabilities"). + +Then, a "delete" function is implemented. It basically calls the +"destroy" method and removes the selected items from the object store, +and then calls `draw`. + +### Fourth step + +Depending on the capabilities of the selected objects, suitable +transformations are offered. The simplest one is a "move" handle. + +The items that implement "draw_transformed" are dynamically redrawn +during the "drag" operation. Upon releasing the "move" handle, the +tool calls the "transform" method, which modifies the coordinates of +the selecetd items in the object store. + +Similarly, "scaling" and "rotation" operations are implemented by +adding extra handles to the selection wireframe. Everything else +remains the same. + +## Selection wireframe + +The selection wireframe could look like this: + + R R + R R + R +---+ +---+ +---+ R + | S |------------------| A |------------------| S | + +---+ +---+ +---+ + | | + | ^ | | + | Z Z | + | | V | + | | + | ^ | + +---+ | +---+ + | A | <---M---> | A | + +---+ | +---+ + | V | + | | + | BIN | + | ICON | + | | + | | + +---+ +---+ +---+ + | S |------------------| A |------------------| S | + R +---+ +---+ +---+ R + R R + R R + +| Handle | Function | +|--------|---------------------------------------------------------------------------| +| BIN | trash icon, deletes the selected items | +| M | move handle to drag the selection. SHIFT stays H or V | +| S | isotropic scaling handle, opposite corner stays where it is | +| A | anisotropic scaling handle, opposite side stays where it is | +| R | rotate handle, opposite corner is center of rotation, SHIFT for 45° steps | +| Z | move up/down selection in Z-order | +