-
-
Notifications
You must be signed in to change notification settings - Fork 84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
propose architectural change with graphics object store #203
base: master
Are you sure you want to change the base?
Conversation
Few thoughts: Implementation
Migration
Selection wireframe
|
I think with the latter it would be easier to maintain the "Z order" of items. This then would mean that in a multi user environment, user can only manipulate their own objects? Similarly, for the undo/redo part? This would be different from the current system, right?
The idea is that, for example while an object is rotated, This would allow to deal with objects that are computationally too "heavy" to transform and display dynamically
I don't know enough about this.
Agree.
My idea would be to still draw the objects into
So basically after each change to the object store, one would check what the The same would be true if for example a dynamic object "expires"
when C disappears, one has:
I have started to code a simple implementation to see how this might look like, and in this one I use the object's Thinking about it, I now realize that also the individual transformations need to be undo/redoable, not just the addition of a new object. Maybe this would require to make a deep copy of the object (or group of objects), and place it in the redo buffer, somehow "flagging" that undo would replace the existing objects (or their contents). Maybe the id could be used for this, but I need to think more about this.
Yes. |
OK!
Good question! I think we should align with what makes sense. Proposal:
AFAICS undo/redo is global now as is erasing. Only drawing is multi-user (LINE/RECT might not be as per their use of aux_backbuffer)
So no "hard" reasons IMO ;-) We can go with the current idea, I just want to challenge the current reasoning.
Mh why not dynamically redraw in transient transformed state always? Wouldn't that be what users expect from a "modern" UI? Drawing transient wireframes seems a bit like a 90's performance excuse that we don't really need anymore to me.
See above. Does this really apply? I again want to point to figma (figjam there) who are doing it all in a browser without being slow.
Me neither, it justed sounded suitable from the back of my head. Might need some reading. We can go without it if too much up-front effort.
Likewise, will provide feedback on this in a later edit.
|
bed8d6d
to
99614d5
Compare
99614d5
to
2ec524a
Compare
I have now refined the proposal and added it as ADR, under doc/architecture/decisions/0002. In particular, it now contains an undo/redo mechanism that should work with multiple users. Please comment. |
2ec524a
to
7f745ff
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2nd round of thoughts.
doc/architecture/decisions/0002-implement-graphics-object-store.md
Outdated
Show resolved
Hide resolved
doc/architecture/decisions/0002-implement-graphics-object-store.md
Outdated
Show resolved
Hide resolved
doc/architecture/decisions/0002-implement-graphics-object-store.md
Outdated
Show resolved
Hide resolved
doc/architecture/decisions/0002-implement-graphics-object-store.md
Outdated
Show resolved
Hide resolved
doc/architecture/decisions/0002-implement-graphics-object-store.md
Outdated
Show resolved
Hide resolved
|
||
- each device (i.e. user) contains an `GList` with `undo` and `redo` records. | ||
|
||
- `GromitData` is extended with an undo object store (`undo_objects`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefix w/ "The global " to make this clearer please.
- 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make it clear that objects
is in GromitData and "global" please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is written on line 35 already...
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like https://docs.gtk.org/gobject/method.Object.unref.html ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but then pointers are required. I prefer to have the object ids. And since the objects are part of a list, they should not be deallocated, but the list link needs to be deleted.
534e7e9
to
974b2c7
Compare
Some more thoughts: RECOLOR will have to go, I think. ERASER should go as well, I think. One could of course draw with a corresponding pen, but maybe it is better do delete entire objects with the new mechanism, or just "undo" whenever possible.
|
RECOLOR probably does not matter, but i do know that people use ERASER, so we need to find a solution for this one. It could
|
There is no backbuffer anymore, objects are drawn directly...
Yes, that would work, but see above (Z-order, destructive). As an intermediate fix, this certainly would be ok and meet some user's demands. However, people currently use ERASER because there is no other possibility to delete (parts of) an object. IMO the best solution would be to find another, intuitive use of e.g. the back of a tablet pencil. One could think about selecting an object by tapping on it, and then select the part of the path one wants to delete. This could either split the object into multiple new objects, or add sections with width = 0 that are skipped during drawing of the object-. What I also want to do is to think about "on the fly" ways to correct drawings. Here is an example with which I am experimenting in the "orthogonal" tool, where "back-tracking" can be used to correct the drawing. This is already with the new object store system, so direct drawing without any backbuffer. |
Hmm, there was a buffer mentioned on the very top of this PR at "Drawing of object store". In case the design in the ADR departs from that (and recreates the to-be-drawn-buffer from objects each time?), why not get back to this? OTOH, I probably have to get my head around the fact that the new design changes the program from a raster drawing tool to a kind of vector drawing tool with objects. Would this describe the new UX?
I think while this might be neat technically, it's not really useful UX and kinda goes over the top of the functionality of an annotation tool :-) |
Yes, the buffer was more to cache the drawing. But I think you said that this was rather "90s" or something like that, and I think you were right. Only the objects that are in the area that is invalidated are redrawn. The code called from "on_expose" looks like this:
|
This PR does not yet contain code but contains a design document outlining an architectural change.
The idea is to discuss it, and once refined and agreed on, I would add actual code to this PR.
For ease of reading, I paste the content of the design document into this description:
Background and Rationale
gromit
currently follows a "draw-and-forget" strategy, i.e. eachdrawing operation, once completed, is drawn on a
cairo_surface_t
(
GromitData.back_buffer
).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
, andon_release
, which complicates 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 (function
redraw
), the concent ofbackbuffer
could be re-created at any time.
Implementation
A graphics object store is added to
GromitData
. This object storesimple is a
GList
ofGfxObject
. EachGfxObject
has thefollowing fields:
The object specific fields allow for an inheritance-like structure,
i.e. each specialized object can be downcast to the above
GfxObject
.For a normal stroked path, the specific field could be:
For a text object, one could have:
A path that fades away after a certain time could look like so:
Capabilities
Each
gfx_object
has a bit mask indicating its "capabilities". Thesecapabilities imply that certain actions are implemented in the
do
function.
this allows for dynamic representation during
operations such as dragging
i.e. differently in X and Y direction
isotropic_scale also must be set
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 couldbe
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 for selection wireframe).
During transform operations,
draw_transformed
is called for eachobject, with a 2D affine transformation matrix as argument.
At the end of the operation,
transform
is called with the finaltransformation 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
do
, theaction
argument selects the task to be performed.this allows to display the object dynamically,
e.g. while it is being dragged
"hits" the object, for example to select it
in the gfx_store
Drawing of object store
Whenever objects need to be re-drawn, the
redraw
function simply iteratesthrough the object store and calls
object->do(ptr, draw_action)
forall objects.
To improve update speed, a cached
cairo_surface_t
(similar tobackbuffer
) is maintained. This cache contains all drawingoperations up to but not including the first that is flagged as
dynamic
.In practice, most of the time only the last object would need to be
redrawn atop the cached image. This is no different from the current
code which often draws over
aux_backbuffer
.Migration to new drawing system
First step
Object store
The object store is added to
GromitData
, together with somehousekeeping functions.
Callbacks
Then, the callbacks are adapted to add data to the object store
instead of
GromitData->coordlist
. Direct drawing is replaced by asubsequent call of
redraw
.on_press
adds a new item of the respective type to the objectstore, and "remembers" its
id
(for example, in the hash_table).on_motion
appends coordinates to the respectiveGfxObject
, andcalls
redraw
.on_release
finalizes the operation, for example by smoothing apath (SMOOTH tool), and then calling
redraw
.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.
Undo/redo
"Undo" is implemented by simply moving the last
GfxObjects
from the object store to the head of a separate "redo" list,
followed by a call to
redraw
."Redo" moves the first element from the "redo" list to the last
position in the object store, followed by a call of
redraw
.These are relatively minor changes. With these, the new architecture
already is functional.
Second step
In a next step, a "select" tool is implemented. This requires that the
GfxObject
implementis_clicked
anddraw_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
redraw
.Third 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: