kolibril13 / jupyter-tldraw

the very good free whiteboard tldraw in the jupyter output
MIT License
252 stars 19 forks source link

Bidirectional communication between the tldraw and python #12

Open kolibril13 opened 1 year ago

kolibril13 commented 1 year ago

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

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 commented 1 year ago
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 commented 1 year ago

^^ 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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

+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()