Skip to content
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

Bidirectional communication between the tldraw and python #12

Open
kolibril13 opened this issue Jun 2, 2023 · 5 comments
Open

Bidirectional communication between the tldraw and python #12

kolibril13 opened this issue Jun 2, 2023 · 5 comments

Comments

@kolibril13
Copy link
Owner

kolibril13 commented Jun 2, 2023

Bidirectional communication between python and tldraw would be amazing to have!
Here are three use cases that I can see:

  • updating background Matplotlib figures without losing the overlay drawings
  • image segmentation by getting the coordinates of the hand drawn strokes on the canvas
  • programmatic shape generation, e.g. with functions and sliders from the python side.

Here is an example of how I imagine bidirectional communication could look like:
image
I've set up the JupyterLite notebook https://kolibril13.github.io/jupyter-tldraw/lab/?path=idea_bidirectional_communication.ipynb for investigation.
WARNING: All Code of this JupyterLite instance will be deleted on reload, so code should be copy+pasted to a save place before closing that page.

Brainstorming on how to get there:

@steveruizok
Copy link

steveruizok commented Jun 2, 2023

import ipyreact
from traitlets import Unicode
from IPython.display import display


class TldrawWidget(ipyreact.ReactWidget):
    my_text = Unicode("Hello World").tag(sync=True)
    my_text_out = Unicode("Hello World").tag(sync=True)

    _esm = """
    import { TDShapeType, Tldraw } from "@tldraw/tldraw";
    import * as React from "react";

    export default function App({ my_text, my_text_out }) {
    
        const [app, setApp] = React.useState()

         const handleMount = React.useCallback((app: Tldraw) => {
             setApp(app)
         }, []);

         React.useEffect(() => {
             if (app) {
             app.createShapes({
                 id: "text1",
                 type: TDShapeType.Text,
                 point: [100, 100],
                 text: my_text,
             });
             }
         }, [my_text, app])

        return (
            <div
            style={{
                position: "relative",
                width: "800px",
                height: "350px",
            }}
            >
            <Tldraw onMount={handleMount} onChange={e => my_text_out = "updated" }/>
            </div>
        );
    }
    """


tldraw = TldrawWidget()
tldraw.my_text = "This is Tldrawesome!!🎉"
tldraw.my_text_out = "This is Tldrawesome!!🎉"
display(tldraw)

@steveruizok
Copy link

steveruizok commented Jun 2, 2023

^^ pasted during our call, but that should be enough to get you going! Basically you want to run a side effect whenever those props change, and in that side effect you'd want to interface with the tldraw imperative API (app), which has methods for creating shapes, updating shapes, etc.

@kolibril13
Copy link
Owner Author

Thanks so much for this gold nugget, that will be a great starting point!
The direction "python -> Tldraw" is now covered.
If you have a little bit of bandwidth, do you have an idea for "Tldraw -> python" side as well?
image
So that tldraw.my_text will give me 'My text that I wrote in tldraw' insead of 'Hey there!'

@kolibril13
Copy link
Owner Author

I experimented a bit more, and it seems that the onChange method in
<Tldraw onMount={handleMount} onChange={e => console.log("My log") }/>
works fine every time the canvas is updated, so I just have to find a way to update the my_text traitlet.
I think that I will be able to solve this on my own :)

@MarcSkovMadsen
Copy link

MarcSkovMadsen commented Nov 25, 2023

+1.

I was hoping to find bidirectional communication. I was playing around with Panel and AnyWidget examples. TlDraw with bidirectional communication would be amazing to have access to.

image

# pip install panel ipywidgets_bokeh tldraw
from tldraw import TldrawWidget

widget = TldrawWidget()

# HELP FUNCTIONALITY to convert Traitlets Classes/ Events to Param Classes/ Events
import anywidget
import param

_ipywidget_classes = {}
_any_widget_traits = set(anywidget.AnyWidget().traits())

def create_observer(obj, traits=None)->param.Parameterized:
    """Returns a Parameterized class with parameters corresponding to the traits of the obj
    
    Args:
        traits: A list of traits to observe. If None all traits not on the base AnyWidget will be
        observed.
    """
    if not traits:
        traits = list(set(obj.traits())-_any_widget_traits)
    print(traits)
    name = type(obj).__name__
    if name in _ipywidget_classes:
        observer_class = _ipywidget_classes[name]
    else:
        observer_class = param.parameterized_class(name, {trait: param.Parameter() for trait in traits})
        _ipywidget_classes[name] = observer_class
    
    values = {trait: getattr(obj, trait) for trait in traits}
    observer = observer_class(**values)
    obj.observe(lambda event: setattr(observer, event["name"], event["new"]), names=traits)
    return observer

# THE PANEL APP

import panel as pn
pn.extension("ipywidgets")

observer = create_observer(widget)

def some_output(value):
    print(value)
    return value

component = pn.Column(widget, pn.bind(some_output, observer.param.value))

pn.template.FastListTemplate(
    site="Panel",
    title="Works with Jupyter-TlDraw",
    main=[component],
).servable()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants