Recent experiences with Paper.js serialization

713 views
Skip to first unread message

Mikko Mononen

unread,
Dec 30, 2013, 12:39:45 PM12/30/13
to pap...@googlegroups.com
Hello,

I have been recently done quite a bit of work around Paper.js serialization, and I'd thought I share few things I encountered along the way.

I'm currently using serialization to:
- store temporary state of items being edited for translate/rotate/scale
- store items to clip board for cut/copy/paste
- store the state of the document for undo/redo

Temporary State
There are generally two ways to edit an item based on mouse movement. The first simpler kind is that you apply the delta movement on mousedrag. The second kind is that you store the original position (state) of the item and calculate the delta all the way from mousedown and the new position is the original position plus the delta. The second kind is generally more robust, and is needed if you want to do snapping or constraining.

The first problem I ran into implementing this was that Paper.js does not retain the IDs over serialization. I fixed that simply by storing the item id, then calling importJSON and then restore the id back. It works, but I'm not sure if that is a good way to handle things. The reason the ids are need to be the same is that we will capture the state once, but restore it multiple times, each time the mouse moves. If the ids change, then it is not possible to know which item states to restore.

You can find the captureSelectionState()/restoreSelectionState() here:

The second snag I ran into was that item.exportJSON() returns a string, so I used paper.Base.serialize(item) to get an non-strigified json.

Copy & Paste
I used the same captureSelectionState() here for copy, but the difference between restoring state and paste is that you'd actually want to create new items so this was more straight forward. It took me a while to figure out how the API should be used to create new items based on json. It seems like that I could have called importJSON() on a layer, but feels really weird API choice. I ended up using paper.Base.importJSON(). I wish the API would be more clear in this, like paper.Item.createFromJSON().

Paste can be found here:

Undo/Redo
Undo was the most involved of the all. I stumbled into the same problem with IDs again. This time I solved it by storing the IDs in the data of the items, and then restoring the ids from there. I used paper.Base.serialize() again to store the state as a js object instead of string.

One thing I could not get working was selection. In order to get expected results after undo, I'd store the selection separately. After the project has been restore, I would deselect all, and apply my stored selection.

No matter what I did, I always ended up in a situation where an item or few were selected after undo or redo and I could not deselect them. So I had to manually call paper.project._selectedItems = {} after paper.project.deselectAll() to make this work.

I did not end up using project._changes array for undo, as it would require a way to store the state of an object before modification too.

Summary
I think these are quite usual cases for serialization apart from actually storing the data to disk.

It would be nice if it was possible to control if the ids were retained in serialization. I think they should be always serialized, and there should be control over if they were to be deserialized. In addition it would be nice if the API regarding serialization would be a bit more clear, especially in the context of restoring state vs. creating new item. It is quite error prone if the same function does both.

Finally, I wish there was project.getItemById(), it would be useful for many serialization things, like storing selection state separately. Also including cases where you are referring to an item, but the item may change because of undo/redo. For example a pen tool might have an 'active path' reference, and the item it points to can change after undo/redo.


--mikko

Jürg Lehni

unread,
Dec 30, 2013, 1:54:32 PM12/30/13
to pap...@googlegroups.com
Hi Mikko,

This sounds really great! Can't wait to try it all out.

Here my responses:

> Temporary State
> There are generally two ways to edit an item based on mouse movement. The first simpler kind is that you apply the delta movement on mousedrag. The second kind is that you store the original position (state) of the item and calculate the delta all the way from mousedown and the new position is the original position plus the delta. The second kind is generally more robust, and is needed if you want to do snapping or constraining.

Instead of serializing, couldn't you simply use clone for this? Maybe we should add a Item#swap() method which would swap one item with another.

> The first problem I ran into implementing this was that Paper.js does not retain the IDs over serialization. I fixed that simply by storing the item id, then calling importJSON and then restore the id back. It works, but I'm not sure if that is a good way to handle things.

Yeah that's a tricky one... Rather than preserving IDs in serialization, which doesn't seem like such a good idea. ids in Paper.js really are unique ids, and should be treated as such. I was considering renaming them to #uid to be more clear about that.

> The reason the ids are need to be the same is that we will capture the state once, but restore it multiple times, each time the mouse moves. If the ids change, then it is not possible to know which item states to restore.

I don't understand this problem. Can you explain better why a changing id is an issue here?

> The second snag I ran into was that item.exportJSON() returns a string, so I used paper.Base.serialize(item) to get an non-strigified json.

Yes I will fix this for you. There will be a `asString` property in the options object, defaulting to true. `item.exportJSON({ asString: false })` will produce the desired result then.

> Copy & Paste
> I used the same captureSelectionState() here for copy, but the difference between restoring state and paste is that you'd actually want to create new items so this was more straight forward. It took me a while to figure out how the API should be used to create new items based on json. It seems like that I could have called importJSON() on a layer, but feels really weird API choice.

Let me explain the current design:

item#importJSON() tries to import the JSON into the actual item. There are multiple scenarios:

- Importing a JSON describing a Path into a Path: The item does not get replaced, and preserves the ID and all. It will read its new state from the JSON, replacing all its segments, style, closed state, matrix, etc.

- Importing a JSON describing a Path, Raster, CompoundPath, etc. into a Layer or Group: A new item will be deserialized from the JSON and added as a child to the Layer, using addChild (I think).

- Importing a JSON describing a Group into a Group: The same happens asa for the Path above, the Group replaces its children with the ones deserialized from the JSON. The Group keeps its ID, but the children receive new ones, since they get replaced.

I think that's a pretty clean API choice, but perhaps the docs are out of sync. They are in quite a few places, I'm simply too swamped to keep track of all the docs at the same time, and am hoping for other people to join the effort there.

> I ended up using paper.Base.importJSON(). I wish the API would be more clear in this, like paper.Item.createFromJSON().

I'm considering Base.importJSON() kind of private... Ideally you wouldn't have to use it from outside. Does what I outlined above make it more clear? Does the current API facilitate what you need to do?

> Undo/Redo
> Undo was the most involved of the all. I stumbled into the same problem with IDs again. This time I solved it by storing the IDs in the data of the items, and then restoring the ids from there. I used paper.Base.serialize() again to store the state as a js object instead of string.
>
> One thing I could not get working was selection. In order to get expected results after undo, I'd store the selection separately. After the project has been restore, I would deselect all, and apply my stored selection.
>
> No matter what I did, I always ended up in a situation where an item or few were selected after undo or redo and I could not deselect them. So I had to manually call paper.project._selectedItems = {} after paper.project.deselectAll() to make this work.

That's very strange. It most definitely sounds like a bug. Do you think you could create a simple isolated test case for me to replicate and debug this? Perhaps on sketch.paperjs.org?

> I did not end up using project._changes array for undo, as it would require a way to store the state of an object before modification too.

That makes sense! So how do you store the state?

> Summary
> I think these are quite usual cases for serialization apart from actually storing the data to disk.
>
> It would be nice if it was possible to control if the ids were retained in serialization. I think they should be always serialized, and there should be control over if they were to be deserialized. In addition it would be nice if the API regarding serialization would be a bit more clear, especially in the context of restoring state vs. creating new item. It is quite error prone if the same function does both.

I don't agree, for the reason stated above (uids). I think ids shouldn't play a role at all except internally... Couldn't you do what you're doing without ids, but with direct variable references to these items?

Let's figure this out and find something that helps you and makes your life easier. It's great to have such a project that highlights issues in the current design, and I'm happy to reconsider choices and fix such issues. It's also a good time now since we'll hopefully soon stabilize the API and go v1.0

Best,

Jürg

Jürg Lehni

unread,
Dec 30, 2013, 5:35:35 PM12/30/13
to pap...@googlegroups.com
PS: This is implemented now:

Item#exportJSON({ asString: false })

https://github.com/paperjs/paper.js/commit/c197f531a407c76f3bbdbe5ab992c2198f8fa01b

J

Mikko Mononen

unread,
Dec 31, 2013, 2:43:52 AM12/31/13
to pap...@googlegroups.com
Hi,

The problem I'm trying to solve with the IDs is how to match a serialized to an existing item in paper.js.

Here's an example case, of capturing and restoring a state of the selection:

// Returns serialized contents of selected items.
function captureSelectionState() {
        var originalContent = [];
        var selected = paper.project.selectedItems;
        for (var i = 0; i < selected.length; i++) {
                var item = selected[i];
                if (item.guide) continue;
                var orig = {
                        id: item.id,
                        json: paper.Base.serialize(item) // item.exportJSON();
                };
                originalContent.push(orig);
        }
        return originalContent;
}

// Restore the state of selected items.
function restoreSelectionState(originalContent) {
        // TODO: could use findItemById() instead.
        for (var i = 0; i < originalContent.length; i++) {
                var orig = originalContent[i];
                var item = findItemById(orig.id);
                if (!item) continue;
                // HACK: paper does not retain item IDs after importJSON,
                // store the ID here, and restore after deserialization.
                var id = item.id;
                item.importJSON(orig.json); // This changes item.id
                item._id = id;
        }
}

And how it is being used in a tool:

mousedown: function(event) {
this.mouseStartPos = event.point.clone();
     this.originalContent = captureSelectionState();
}
  
mousedrag: function(event) {
    var delta = event.point.subtract(this.mouseStartPos);

if (event.modifiers.shift)
        delta = snapDeltaToAngle(delta, Math.PI*2/8);

    restoreSelectionState(this.originalContent);

    var selected = paper.project.selectedItems;
    for (var i = 0; i < selected.length; i++)
        selected[i].position = selected[i].position.add(delta);
}

How would you implement restoreSelectionState() without using the IDs? Since we're restoring multiple times, the IDs must stay the same.

I have a second similar use case for the ids too. I wanted to store the selection separate from the actual serialized state of the document. In this case I have the same problem, how to refer to the item that is selected without and id.

Thanks for clarifying the serialization API, it makes sense now. Doesn't the whole API to work in a way that if the type of the serialized data does not match the type of the item it will create new children?

I'd like to use the public API to achieve what I'm trying to do, I'm currently just hacking around and try to get things working. I wish there are 2 APIs for serialization, one which is aimed for capturing and restoring the state of items, it would for example ensure that the types match and tries it's best to keep the ids consistent, and another one which is aimed at creating new items from serialized data. I have observed over the years that ReplaceOrCreate() (or even worse, FindOrCreate()) type of functions will create hard to find bugs over time.


Another thing that I'm trying to think a head is to how to merge two documents created using paper.js. I think this is why I'm so fixated on keeping the ids consistent (in addition to some programming patterns carried voer from c++). It is far easier to do a json diff if we can trust the ids.


--mikko
Reply all
Reply to author
Forward
0 new messages