CanvasId for WebAssembly

I will push for that you get involved. The wrapping and selecting of what to include in the different .wasm files is a little invasive on our VTK branch. Trying to establish a runtime containing VTK/Common, but without windows. I imagine having some .wasm modules compatible with your data model without knowing anything about rendering.

My current solution for an interactor that can be used for multiple windows, I have made using vtkGenericRenderWindowInteractor and is shown below.

export class JSVTKCanvasHandler {
    // Timer to control responses
    Timer = class {
        constructor(callback, interval) {
            this.callback = callback;
            this.interval = interval;
            this.timerId = null;
        }
        start() {
            if (this.timerId === null) {
                this.timerId = setTimeout(() => {
                    this.callback();
                    this.timerId = null; // Reset the timerId after the callback is executed
                }, this.interval);
            }
        }
        stop() {
            if (this.timerId !== null) {
                clearTimeout(this.timerId);
                this.timerId = null;
            }
        }
        restart() {
            this.stop();
            this.start();
        }
    }
    
    static MouseButton = {
        LEFT: 0,
        MIDDLE: 1,
        RIGHT: 2
    };
    static mouseButtonEvents = {
        LEFT: 'LeftButtonReleaseEvent',
        RIGHT: 'RightButtonReleaseEvent',
        MIDDLE: 'MiddleButtonReleaseEvent'
    };
    static vtkToJsCursorMap = {
        0:  'default',      // VTK_CURSOR_DEFAULT
        1:  'default',      // VTK_CURSOR_ARROW
        2:  'default',      // VTK_CURSOR_SIZENE
        3:  'default',      // VTK_CURSOR_SIZENWSE
        4:  'default',      // VTK_CURSOR_SIZESW
        5:  'default',      // VTK_CURSOR_SIZESE
        6:  'ns-resize',    // VTK_CURSOR_SIZENS
        7:  'ew-resize',    // VTK_CURSOR_SIZEWE
        8:  'pointer',      // VTK_CURSOR_SIZEALL
        9:  'pointer',      // VTK_CURSOR_HAND
        10: 'crosshair'     // VTK_CURSOR_CROSSHAIR
    }
    

    constructor(canvas, iren, wasmExports) {
        this.canvas = canvas;
        // The VTK interactor. TODO: Figure out which interactors that require a timer
        this.iren = iren;
        this.handleMouseDown = this.handleMouseDown.bind(this);
        this.handleMouseMove = this.handleMouseMove.bind(this);
        this.handleMouseUp = this.handleMouseUp.bind(this);
        this.handleDoubleClick = this.handleDoubleClick.bind(this);
        this.handleKeyDown = this.handleKeyDown.bind(this);
        this.handleEnter = this.handleEnter.bind(this);
        this.handleLeave = this.handleLeave.bind(this);
        this.handleTimer = this.handleTimer.bind(this);
        this.handleWheel = this.handleWheel.bind(this);
        this.handleResize = this.handleResize.bind(this);

        this.changedCursor = this.changedCursor.bind(this);
        this.showCursor = this.showCursor.bind(this);
        
        this.addEventListeners();
        this.vtk = wasmExports;

        // TODO: Verify if lastPosition = (0,0) is an issue
        this.lastPosition = {x: 0, y: 0};
        this.lastModifiers = {ctrlPressed: false, shiftPressed: false};
        this.wheelData = 0;
        this.mouseState = {LEFT: false,
                           RIGHT: false,
                           MIDDLE: false};
        this.mouseEnabled = true;

        // Callback for creating timer
        this.createTimerCommand = new this.vtk.vtkCallbackCommand();
        this.createTimerCommand.SetCallback((caller, evId, clientData, callData) => {
            this.createTimer();
        });

        // Callback for destroying timer
        this.destroyTimerCommand = new this.vtk.vtkCallbackCommand();
        this.destroyTimerCommand.SetCallback((caller, evId, clientData, callData) => {
            this.destroyTimer();
        });

        this.cursorChangedCommand = new this.vtk.vtkCallbackCommand();
        this.cursorChangedCommand.SetCallback((caller, evId, clientData, callData) => {
            this.changedCursor();
        });
        
        this.iren.AddObserver('CreateTimerEvent', this.createTimerCommand, 0.0)
        this.iren.AddObserver('DestroyTimerEvent', this.destroyTimerCommand, 0.0)
        this.iren.GetRenderWindow().AddObserver('CursorChangedEvent',
                                                this.cursorChangedCommand, 0.0)

        // Timer, we allow mouse events every 10 ms
        this.timer = new this.Timer(this.handleTimer, 10);
        // For debugging purposes. To see how many mouse event we have
        this.counter = 0;
    }
    addEventListeners() {
        this.canvas.addEventListener('mousedown', this.handleMouseDown);
        this.canvas.addEventListener('mousemove', this.handleMouseMove);
        this.canvas.addEventListener('mouseup', this.handleMouseUp);
        this.canvas.addEventListener("mouseenter", this.handleEnter, false);
        this.canvas.addEventListener("mouseleave", this.handleLeave, false);
        this.canvas.addEventListener('dblclick', this.handleDoubleClick);
        this.canvas.addEventListener('wheel', this.handleWheel);
        window.addEventListener('resize', this.handleResize);
        document.addEventListener('keydown', this.handleKeyDown);

        // Hide the pop-up menu
        document.addEventListener('contextmenu', function(event) {
            event.preventDefault();
        });    
    }
    createTimer() {
      this.mouseEnabled = false;
      this.timer.start();
    }
    destroyTimer() {
      this.mouseEnabled = true;
      this.timer.stop();
    }
    changedCursor() {
        // Called when the CursorChangedEvent fires on the render window.
        // This indirection is needed since when the event fires, the current
        // cursor is not yet set so we defer this by which time the current
        // cursor should have been set.
        //
        // ISSUE: Needs to be in-sync with the other timer, I guess.
        setTimeout(this.showCursor, 10);
    }
    showCursor() {
        const vtk_cursor = this.iren.GetRenderWindow().GetCurrentCursor()
        const js_cursor = JSVTKCanvasHandler.vtkToJsCursorMap[vtk_cursor];
        console.log('show cursor');
        this.setCursor(js_cursor)       
    }
    setCursor(cursorStyle) {
        document.body.style.cursor = cursorStyle;
    }
    
    handleTimer() {
      this.iren.TimerEvent();
    }
    getPixelRatio() {
        return window.devicePixelRatio || 1;
    }
    getMouseButton(event) {
        return event.button;
    }
    getMousePosition(event) {
        // TODO: Do we need to flip up/down
        const rect = this.canvas.getBoundingClientRect();
        return {
            x: (event.clientX - rect.left) * (this.canvas.width / rect.width),
            y: (event.clientY - rect.top) * (this.canvas.height / rect.height)
        };
    }    
    areModifiersPressed(event) {
        return {
            ctrlPressed: event.ctrlKey | this.lastModifiers.ctrlPressed,
            shiftPressed: event.shiftKey | this.lastModifiers.shiftPressed
        };
    }
    setEventInformation(x, y, ctrl=false, shift=false, key=0, repeat=0, keysym='') {
        const devicePixelRatio = this.getPixelRatio();
        const scale = devicePixelRatio;
        const flipUpDown = false;
        if (flipUpDown) {
            this.iren.SetEventInformation(Math.round(x*scale),
                                          Math.round((this.canvas.height - y - 1)*scale), ctrl, shift, key, repeat, keysym);
        } else {
            this.iren.SetEventInformation(Math.round(x*scale),
                                          Math.round(y*scale), ctrl, shift, key, repeat, keysym);
        }
    }
    handleEnter(event) {
        const modifiers = this.areModifiersPressed(event);
        this.setEventInformation(this.lastPosition.x,
                                 this.lastPosition.y,
                                 modifiers.ctrlPressed,
                                 modifiers.shiftPressed);
        this.iren.EnterEvent();
    }
    handleLeave(event) {
        const modifiers = this.areModifiersPressed(event);
        this.setEventInformation(this.lastPosition.x,
                                 this.lastPosition.y,
                                 modifiers.ctrlPressed,
                                 modifiers.shiftPressed, 0, 0, '');
        for (const button in JSVTKCanvasHandler.mouseButtonEvents) {
            if (this.mouseState[button]) {
                this.iren[JSVTKCanvasHandler.mouseButtonEvents[button]]();
                this.mouseState[button] = false;
            }
        }
        this.currentCanvas = null;
        this.iren.LeaveEvent();
    }
    handleResize() {
        const containerElem = this.canvas.parentElement;
        const body = document.querySelector('body');
        const applyStyle = true;
        if (applyStyle) {
            if (body === containerElem) {
                body.style.margin = 0;
                body.style.width = '100vw';
                body.style.height = '100vh';
            } else {
                containerElem.style.position = 'relative';
                containerElem.style.width = '100%';
                containerElem.style.height = '100%';
            }
            this.canvas.style.position = 'absolute';
            this.canvas.style.top = 0;
            this.canvas.style.left = 0;
            this.canvas.style.width = '100%';
            this.canvas.style.height = '100%';
        }

        const scale = devicePixelRatio;
        this.iren.GetRenderWindow().SetDPI(Math.round(72*scale));
        this.canvas.width = containerElem.clientWidth;
        this.canvas.height = containerElem.clientHeight;
        this.iren.GetRenderWindow().SetSize(this.canvas.width, this.canvas.height);
        this.iren.ConfigureEvent();
        this.iren.GetRenderWindow().Render();
    }
    
    handleWheel(event) {
        event.preventDefault(); // Prevent the default scrolling behavior
        this.wheelData += event.deltaY;
        if (this.wheelData >= 120) {
            this.iren.MouseWheelForwardEvent();
            this.wheelData = 0;
        }
        else if (this.wheelData <= -120) {
            this.iren.MouseWheelBackwardEvent();
            this.wheelData = 0;
        }
    }
    handleMouseMove(event) {
        const rect = this.canvas.getBoundingClientRect();
        if (
            event.clientX >= rect.left &&
            event.clientX <= rect.right &&
            event.clientY >= rect.top &&
            event.clientY <= rect.bottom
        ) {
            this.currentCanvas = this.canvas;
            this.counter = this.counter + 1;
            // One could update every counter % 200 == 0 for styles
            // which do not use timers for smooth motion
            if (this.iren.GetInteractorStyle().GetUseTimers() == 0 || this.mouseEnabled) {
                // Handle movement
                const position = this.getMousePosition(event);
                const modifiers = this.areModifiersPressed(event);
                this.lastModifiers = modifiers;
                this.lastButtons = this.getMouseButton(event);
                this.lastPosition = position;
                this.setEventInformation(position.x, position.y,
                                         modifiers.ctrlPressed,
                                         modifiers.shiftPressed,
                                         0, 0, '');
                this.iren.MouseMoveEvent();
            }
        } else {
            console.log("canvas is null");
            this.currentCanvas = null;
        }
    }
    handleDoubleClick(event) {
        if (this.currentCanvas) {
            const mouseButton = this.getMouseButton(event);
            const modifiers = this.areModifiersPressed(event);
            const position = this.getMousePosition(event);
            const repeat = 1;
            this.iren.SetEventInformation(position.x, position.y, modifiers.ctrlPressed, modifiers.shiftPressed, 0, repeat, '');
            switch (mouseButton) {
            case JSVTKCanvasHandler.MouseButton.LEFT:
                this.iren.LeftButtonPressEvent();
                // Otherwise, it spins like mad
                this.mouseState.LEFT = true;
                break;
            case JSVTKCanvasHandler.MouseButton.MIDDLE:
                this.iren.MiddleButtonPressEvent();
                this.mouseState.MIDDLE = true;
                break;
            case JSVTKCanvasHandler.MouseButton.RIGHT:
                this.iren.RightButtonPressEvent();
                this.mouseState.RIGHT = true;
                break;
            default:
                break;
            }
        }
    }
    handleKeyDown(event) {
        if (this.currentCanvas) {
            const key = event.key;
            const asciiCode = key.length === 1 ? key.charCodeAt(0) : null;
            const modifiers = this.areModifiersPressed(event);
            if (asciiCode !== null) {
                this.iren.SetEventInformation(this.lastPosition.x,
                                              this.lastPosition.y,
                                              modifiers.ctrlPressed, modifiers.shiftPressed, asciiCode, 0, key);
                this.iren.KeyPressEvent();
                this.iren.CharEvent();
            } else {
                console.log(`No ASCII code for this key`);
            }
        }
    }
    handleKeyUp(event) {
        if (this.currentCanvas) {
            // Create a getCharAndKeySym(event) function
            const key = event.key;
            const asciiCode = key.length === 1 ? key.charCodeAt(0) : null;
            const modifiers = this.areModifiersPressed(event);
            this.iren.SetEventInformation(this.lastPosition.x,
                                          this.lastPosition.y,
                                          modifiers.ctrlPressed, modifiers.shiftPressed, asciiCode, 0, key);
            this.iren.KeyReleaseEvent();
        }
    }
    
    handleMouseDown(event) {
        if (this.currentCanvas) {
            const mouseButton = this.getMouseButton(event);
            const modifiers = this.areModifiersPressed(event);
            const position = this.getMousePosition(event);
            const repeat = 0; // Equal to 1 for double-click
            this.iren.SetEventInformation(position.x, (this.canvas.height - 1 - position.y), modifiers.ctrlPressed, modifiers.shiftPressed, 0, repeat, '');
            switch (mouseButton) {
            case JSVTKCanvasHandler.MouseButton.LEFT:
                this.iren.LeftButtonPressEvent();
                this.mouseState.LEFT = true;
                break;
            case JSVTKCanvasHandler.MouseButton.MIDDLE:
                this.iren.MiddleButtonPressEvent();
                this.mouseState.MIDDLE = true;
                break;
            case JSVTKCanvasHandler.MouseButton.RIGHT:
                this.iren.RightButtonPressEvent();
                this.mouseState.RIGHT = true;
                break;
            default:
                break;
            }
        }
    }
    handleMouseUp(event) {
        if (this.currentCanvas) {
            const mouseButton = this.getMouseButton(event);
            const modifiers = this.areModifiersPressed(event);
            const position = this.getMousePosition(event);
            const repeat = 0; // Equal to 1 for double-click
            this.iren.SetEventInformation(position.x, position.y, modifiers.ctrlPressed, modifiers.shiftPressed, 0, repeat, '');
            switch (mouseButton) {
            case JSVTKCanvasHandler.MouseButton.LEFT:
                this.iren.LeftButtonReleaseEvent();
                this.mouseState.LEFT = false;
                break;
            case JSVTKCanvasHandler.MouseButton.MIDDLE:
                this.iren.MiddleButtonReleaseEvent();
                this.mouseState.MIDDLE = false;
                break;
            case JSVTKCanvasHandler.MouseButton.RIGHT:
                this.iren.RightButtonReleaseEvent();
                this.mouseState.RIGHT = false;
                break;
            default:
                break;
            }
        }
    }
}

When it is a bit more tested, it may be a candidate for Wrapping/JavaScript/