polotno-project / polotno-board

Roadmap and bug-tracker for the Polotno project.
https://polotno.dev/
9 stars 1 forks source link

Better Events #64

Closed iamsterdam800 closed 1 year ago

iamsterdam800 commented 1 year ago

Hi Anton,

Trying to extend your editor with custom UX logic, and I find it quite challenging to add logic due to missing information about user actions. Would it be feasible to extend the event API in the following directions?:

lavrton commented 1 year ago

Hello. I would like to have additional information about your use cases. The current store and elements are "event-less" intentionally.

store.on('change') to include { pageId(s), elementId(s) } that were changed? also including creation and deletion as a change.

I will need to find if I can give a patch object. As workaround you can always compare previous JSON with the new JSON and change what is changed.

store.on('load')

Load of what? Right now, it is designed as:

// import data
store.loadJSON(json);
// wait for loading
await store.waitLoading();
// do export
const url = await store.toDataURL();

page.on('change'), page.on('focus'), page.on('blur')

Why do you need to listen to change on a specific page? For focus event you can technically use this:

import { reaction } from 'mobx';

reaction(() => store.activePage, () => {
  console.log('active pages is changed');
});

But still, I am not certain why you may need that. If you want to highlight something in UI, it is better to use observer and reactive nature of the store:

import { observer } from 'mobx-react-lite';

const App = observer(({ store }) => {
  return (
    <div>
      {store.pages.map(page => {
        cosnt isActive = page === store.activePage;
        return <div style={{ border: isActive ? 'solid 1px red' : 'none' }} />
      })}
    </div>
  );
});

element.on('select'), element.on('unselect'), store.on('selectionChanged')

Similar to page, you can react on any change on store.selectedElements property.

element.on('hover'), element.on('dbl-click')

Why do you need it? By design an SDK user shouldn't listen to UI events. Only "data" events, such as "change" and react to any changes in model with observer.

iamsterdam800 commented 1 year ago

Hi Anton, thanks for the hints. I'll try them.

In my use case I have a complex design, that I want to break down into parts, each designed on it's own page. For example, consider a box, where each side can be designed:

image

If you do it all on one page, problems are:

I solve these by providing extra pages and custom PageView elements on the main page, that are rendered from contents of other pages.

For example, I have a page with the Side design, and when it's changed (that's why it's important for me to know which page/elements changed), I render two corresponding PageView elements on the main page:

image

Similarly, I solve the "continuous design" problem by having a page, where Top and Front are connected. And on the main page I have two PageView elements, that render that page with different cropping (image mask).

Now I would like that user can jump to the page with Side design, when they (double-)click on the PageView element, that renders the Side. For that I need element click events.. But maybe because those are custom elements, I can somehow add click event listeners myself?

When user hovers over PageView elements or selects them, I might have some UI logic to emphasize the element shape (in my case it's not a rectangle).

I also don't want my main page elements to be accidentially deleted by user, therefore want to detect deletions (preferably prevent them too).

store.on('load'), that fires after all changes are processed, would be more convenient to have, than the current store.on('change'). Because (1) within the onChange listener, you always need to await for store.waitLoading() and (2) while you're awaiting, there might be concurrent change events. So I have to create quite complex logic to detect those concurrent changes and only execute the rendering logic when it's the final change and it's processed. Btw, maybe the name is a bit confusing and a proper name would be not 'load', but 'ready' or 'afterChangesProcessed'.

Apart from rendering PageView contents, I also render the main page for the 3D preview, so knowing what changed where helps me a lot to optimize rendering of only the changed parts.

lavrton commented 1 year ago

Interesting. That is a very edge case, and Polotno is not really designed for "keep in sync" use cases. But let's think what we can do.

Now I would like that user can jump to the page with Side design, when they (double-)click on the PageView element, that renders the Side. For that I need element click events.. But maybe because those are custom elements, I can somehow add click event listeners myself?

Yes. If you have a custom element, you can add add your own event listener. As alternative, you can add a button into UI (like in the toolbar). Lets call it "open full view". On its click you will jump to the side design.

When user hovers over PageView elements or selects them, I might have some UI logic to emphasize the element shape (in my case it's not a rectangle).

What do you mean by "emphasize the element shape"? Like highlight? If it is a custom element, you can listen mousenter/mouseleave events and redraw a shape differently.

I also don't want my main page elements to be accidentially deleted by user, therefore want to detect deletions (preferably prevent them too).

You can use removable: false property. It will block the delete button and deleting with the keyboard.

w, maybe the name is a bit confusing and a proper name would be not 'load', but 'ready' or 'afterChangesProcessed'.

I still don't get it. Changes are processed instantly. Why do you need to wait them? The only things that you may need to wait is assets loading. Like fonts or images. And you only have to wait for them if you want to export into an image. If you don't need to export, then you do any post-process right after loading of JSON.

Apart from rendering PageView contents, I also render the main page for the 3D preview, so knowing what changed where helps me a lot to optimize rendering of only the changed parts.

If you have a defined upfront number of pages, you can probably use mobx-state-tree API directly to listen to events on some data in the store. I am not showing any public examples of that because it may be an antipattern. But it should work.

import { onSnapshot } from 'mobx-state-tree';

// let's think we always have a second page, and it is made for the side view
const sidePage = store.pages[1];
// listen to any changes on the page
onSnapshot(sidePage, (snapshot) => {
  // react somehow to page changes
}):

You can even use onPatch method.

iamsterdam800 commented 1 year ago

Thank you Anton! My solution is mostly working and with mobx I think I can optimize it further.

As alternative, you can add a button into UI (like in the toolbar). Lets call it "open full view". On its click you will jump to the side design.

That's what I do now, but it's non-intuitive for the user.

What do you mean by "emphasize the element shape"? Like highlight? If it is a custom element, you can listen mousenter/mouseleave events and redraw a shape differently.

Yes, I want to highlight the shape/contour. But I don't want that highlight to appear on the preview. So I guess I can use one more element for the highlight on top of PageView, that is not exportable, so will not appear on the preview.

You can use removable: false property. It will block the delete button and deleting with the keyboard.

Yes, but there is a bug that allows a user to overrule it. I'll submit it separately. Probably easy to fix.

...you only have to wait for them if you want to export into an image...

That's exactly my usecase. On every change I export/render secondary pages as images for PageViews and the main page for 3D preview.

Anyways, I think I can progress now. So, closing the issue.