From f0066f12d440f660386a2362fd058384c8e03d29 Mon Sep 17 00:00:00 2001
From: Jaswant Panchumarti <jaswant.panchumarti@kitware.com>
Date: Fri, 14 Feb 2025 13:28:34 -0500
Subject: [PATCH] Add MultipleCanvases in emscripten examples

---
 .../Cxx/MultipleCanvases/CMakeLists.txt       |  70 ++++++++
 .../Cxx/MultipleCanvases/MultipleCanvases.cxx | 130 ++++++++++++++
 .../Emscripten/Cxx/MultipleCanvases/README.md |  27 +++
 .../Cxx/MultipleCanvases/index.html           | 158 ++++++++++++++++++
 4 files changed, 385 insertions(+)
 create mode 100644 Examples/Emscripten/Cxx/MultipleCanvases/CMakeLists.txt
 create mode 100644 Examples/Emscripten/Cxx/MultipleCanvases/MultipleCanvases.cxx
 create mode 100644 Examples/Emscripten/Cxx/MultipleCanvases/README.md
 create mode 100644 Examples/Emscripten/Cxx/MultipleCanvases/index.html

diff --git a/Examples/Emscripten/Cxx/MultipleCanvases/CMakeLists.txt b/Examples/Emscripten/Cxx/MultipleCanvases/CMakeLists.txt
new file mode 100644
index 00000000000..d9177663b78
--- /dev/null
+++ b/Examples/Emscripten/Cxx/MultipleCanvases/CMakeLists.txt
@@ -0,0 +1,70 @@
+cmake_minimum_required(VERSION 3.13)
+project(MultipleCanvases)
+
+# -----------------------------------------------------------------------------
+# EMSCRIPTEN only
+# -----------------------------------------------------------------------------
+
+if (NOT EMSCRIPTEN)
+  message("Skipping example: This needs to run inside an Emscripten build environment")
+  return ()
+endif ()
+
+# -----------------------------------------------------------------------------
+# Handle VTK dependency
+# -----------------------------------------------------------------------------
+
+find_package(VTK
+  COMPONENTS
+    FiltersSources      # VTK pipeline
+    InteractionStyle    # Mouse handling
+    RenderingOpenGL2    # For Rendering
+    RenderingUI         # For window
+)
+
+if (NOT VTK_FOUND)
+  message("Skipping example: ${VTK_NOT_FOUND_MESSAGE}")
+  return ()
+endif ()
+
+# -----------------------------------------------------------------------------
+# Compile example code
+# -----------------------------------------------------------------------------
+
+add_executable(MultipleCanvases MultipleCanvases.cxx)
+target_link_libraries(MultipleCanvases PRIVATE ${VTK_LIBRARIES})
+
+# -----------------------------------------------------------------------------
+# WebAssembly build options
+# -----------------------------------------------------------------------------
+set(emscripten_link_options)
+list(APPEND emscripten_link_options
+  "-sALLOW_MEMORY_GROWTH=1"
+  "-sSINGLE_FILE=1"
+  "-sEXPORTED_RUNTIME_METHODS=['ENV']" # ENV holds the environment variables accessible by C getenv
+  "--shell-file=${CMAKE_CURRENT_SOURCE_DIR}/index.html"
+)
+if (CMAKE_SIZEOF_VOID_P EQUAL "8")
+  list(APPEND emscripten_link_options
+    "-sMAXIMUM_MEMORY=16GB")
+else ()
+  list(APPEND emscripten_link_options
+    "-sMAXIMUM_MEMORY=4GB")
+endif ()
+target_link_options(MultipleCanvases
+  PUBLIC
+    ${emscripten_link_options}
+)
+set_target_properties(MultipleCanvases
+PROPERTIES 
+  OUTPUT_NAME "index"
+  SUFFIX ".html")
+
+# -----------------------------------------------------------------------------
+# VTK modules initialization
+# -----------------------------------------------------------------------------
+
+vtk_module_autoinit(
+  TARGETS  MultipleCanvases
+  MODULES  ${VTK_LIBRARIES}
+)
diff --git a/Examples/Emscripten/Cxx/MultipleCanvases/MultipleCanvases.cxx b/Examples/Emscripten/Cxx/MultipleCanvases/MultipleCanvases.cxx
new file mode 100644
index 00000000000..0c224a7810d
--- /dev/null
+++ b/Examples/Emscripten/Cxx/MultipleCanvases/MultipleCanvases.cxx
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "vtkActor.h"
+#include "vtkArrowSource.h"
+#include "vtkCompositePolyDataMapper.h"
+#include "vtkConeSource.h"
+#include "vtkCubeSource.h"
+#include "vtkCylinderSource.h"
+#include "vtkDiskSource.h"
+#include "vtkPartitionedDataSetCollectionSource.h"
+#include "vtkPlatonicSolidSource.h"
+#include "vtkRegularPolygonSource.h"
+#include "vtkRenderWindow.h"
+#include "vtkRenderWindowInteractor.h"
+#include "vtkRenderer.h"
+#include "vtkSmartPointer.h"
+#include "vtkSphereSource.h"
+#include "vtkSuperquadricSource.h"
+#include "vtkWebAssemblyOpenGLRenderWindow.h"
+#include "vtkWebAssemblyRenderWindowInteractor.h"
+
+#include <emscripten/emscripten.h>
+
+#include <array>
+#include <cstdlib>
+#include <iostream>
+#include <random>
+#include <vector>
+
+namespace
+{
+std::vector<vtkSmartPointer<vtkRenderWindowInteractor>> interactors;
+}
+
+int main(int argc, char* argv[])
+{
+  vtkRenderWindowInteractor::InteractorManagesTheEventLoop = false;
+  if (argc < 2)
+  {
+    std::cerr << "Usage: MultipleCanvases <numCanvases>\n";
+  }
+  const int numCanvases = std::atoi(argv[1]);
+  std::array<vtkSmartPointer<vtkAlgorithm>, 10> sources;
+  std::size_t i = 0;
+  sources[i++] = { vtk::TakeSmartPointer(vtkArrowSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkConeSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkCubeSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkCylinderSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkDiskSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkPartitionedDataSetCollectionSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkPlatonicSolidSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkRegularPolygonSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkSphereSource::New()) };
+  sources[i++] = { vtk::TakeSmartPointer(vtkSuperquadricSource::New()) };
+
+  for (int iCanvas = 0; iCanvas < numCanvases; ++iCanvas)
+  {
+    const std::string canvasId = "canvas" + std::to_string(iCanvas);
+    const std::string canvasSelector = "#" + canvasId;
+    vtkNew<vtkRenderWindow> renderWindow;
+    vtkNew<vtkRenderWindowInteractor> interactor;
+    vtkNew<vtkRenderer> renderer;
+    vtkNew<vtkActor> actor;
+    vtkNew<vtkCompositePolyDataMapper> mapper;
+
+    auto& source = sources[iCanvas % sources.size()];
+    mapper->SetInputConnection(source->GetOutputPort());
+    mapper->ScalarVisibilityOff();
+    actor->SetMapper(mapper);
+    renderer->AddActor(actor);
+
+    if (auto* wasmInteractor = vtkWebAssemblyRenderWindowInteractor::SafeDownCast(interactor))
+    {
+      wasmInteractor->SetCanvasSelector(canvasSelector.c_str());
+    }
+    if (auto* wasmGLWindow = vtkWebAssemblyOpenGLRenderWindow::SafeDownCast(renderWindow))
+    {
+      wasmGLWindow->SetCanvasSelector(canvasSelector.c_str());
+    }
+    renderer->SetBackground(0.3, 0.3, 0.3);
+    renderWindow->AddRenderer(renderer);
+    interactor->SetRenderWindow(renderWindow);
+    interactors.emplace_back(interactor);
+    // clang-format off
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
+    MAIN_THREAD_EM_ASM({addCanvas($0, $1, $2); }, canvasId.c_str(), source->GetClassName(), iCanvas);
+#pragma clang diagnostic pop
+    // clang-format on
+    renderer->ResetCamera();
+    renderWindow->Render();
+  }
+  return 0;
+}
+
+extern "C"
+{
+  EMSCRIPTEN_KEEPALIVE void startEventLoop(int iCanvas)
+  {
+    if (static_cast<std::size_t>(iCanvas) < interactors.size())
+    {
+      interactors[iCanvas]->Start();
+    }
+  }
+
+  EMSCRIPTEN_KEEPALIVE void updateSize(int iCanvas, int width, int height)
+  {
+    if (static_cast<std::size_t>(iCanvas) < interactors.size())
+    {
+      interactors[iCanvas]->UpdateSize(width, height);
+    }
+  }
+
+  EMSCRIPTEN_KEEPALIVE void render(int iCanvas, int width, int height)
+  {
+    if (static_cast<std::size_t>(iCanvas) < interactors.size())
+    {
+      interactors[iCanvas]->GetRenderWindow()->Render();
+    }
+  }
+
+  EMSCRIPTEN_KEEPALIVE void stopEventLoop(int iCanvas)
+  {
+    if (static_cast<std::size_t>(iCanvas) < interactors.size())
+    {
+      interactors[iCanvas]->TerminateApp();
+    }
+  }
+}
diff --git a/Examples/Emscripten/Cxx/MultipleCanvases/README.md b/Examples/Emscripten/Cxx/MultipleCanvases/README.md
new file mode 100644
index 00000000000..9e55c93c1b0
--- /dev/null
+++ b/Examples/Emscripten/Cxx/MultipleCanvases/README.md
@@ -0,0 +1,27 @@
+# Build
+This example illustrates how to utilize multiple canvases in a single webassembly module. The key functions are:
+1. `vtkWebAssemblyRenderWindowInteractor::SetCanvasSelector()`
+2. `vtkWebAssemblyOpenGLRenderWindow::SetCanvasSelector()`
+
+In WebGL, there can be atmost 16 contexts. As a result, the max.
+number of canvases are limited. This example does not yet support webgpu, it will need slight
+refactoring to make it compatible.
+
+```
+emcmake cmake \
+  -G Ninja \
+  -S /path/to/vtk/Examples/Emscripten/Cxx/MultipleCanvases \
+  -B out/build \
+  -DVTK_DIR=/path/to/where/vtk/wasm/was/built
+
+cmake --build out/build
+```
+
+# Serve and test generated code
+
+```
+cd out/build
+python3 -m http.server 8000
+```
+
+Open your browser to http://localhost:8000
diff --git a/Examples/Emscripten/Cxx/MultipleCanvases/index.html b/Examples/Emscripten/Cxx/MultipleCanvases/index.html
new file mode 100644
index 00000000000..fcf78c90855
--- /dev/null
+++ b/Examples/Emscripten/Cxx/MultipleCanvases/index.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html lang="en-us">
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+  <style>
+    :root {
+      color-scheme: light dark;
+    }
+
+    html,
+    body {
+      /*remove default margin*/
+      margin: 0;
+      /* make body fill the browser */
+      height: 100%;
+    }
+
+    #outer {
+      width: 100%;
+      height: 100%;
+      display: block;
+      overflow: auto;
+    }
+
+    .canvas {
+      display: inline-block;
+      padding: 1em;
+      background: #888;
+      margin: 1em;
+    }
+
+    .size0>canvas {
+      width: 300px;
+      height: 300px;
+    }
+
+    .size1>canvas {
+      width: 400px;
+      height: 300px;
+    }
+
+    .size2>canvas {
+      width: 300px;
+      height: 400px;
+    }
+
+    .size3>canvas {
+      width: 400px;
+      height: 400px;
+    }
+  </style>
+</head>
+
+<body>
+  <div id="outer">
+  </div>
+  <script>
+    const numCanvases = 16;
+    /**
+     * Lock canvas size for VTK rendering unit tests. These settings make the canvas ignore
+     * resize events from the parent HTML element.
+     */
+    function lockCanvasSize(wasmRuntime) {
+      try {
+        if (typeof wasmRuntime._setDefaultExpandVTKCanvasToContainer !== 'undefined') {
+          wasmRuntime._setDefaultExpandVTKCanvasToContainer(false);
+        }
+        if (typeof wasmRuntime._setDefaultInstallHTMLResizeObserver !== 'undefined') {
+          wasmRuntime._setDefaultInstallHTMLResizeObserver(false);
+        }
+      } catch (_e) {}
+    }
+
+    const resizeObserver = new ResizeObserver((entries) => {
+      for (const entry of entries) {
+        const canvas = entry.target;
+        const width = entry.contentBoxSize[0].inlineSize;
+        const height = entry.contentBoxSize[0].blockSize;
+        const scaledWidth = Math.max(
+          1,
+          width * window.devicePixelRatio + 0.5
+        );
+        const scaledHeight = Math.max(
+          1,
+          height * window.devicePixelRatio + 0.5
+        );
+        Module._updateSize(canvas.internalIndex, scaledWidth, scaledHeight);
+        Module._render(canvas.internalIndex);
+      }
+    });
+
+    function rand(min, max) {
+      if (min === undefined) {
+        max = 1;
+        min = 0;
+      } else if (max === undefined) {
+        max = min;
+        min = 0;
+      }
+      return Math.random() * (max - min) + min;
+    }
+
+    function addCanvas(canvasId, sourceName, iCanvas) {
+      canvasId = maybeCStringToJsString(canvasId);
+      sourceName = maybeCStringToJsString(sourceName);
+      const outerElem = document.body.querySelector("#outer");
+      // making this
+      // <div class="product size?">
+      //   <canvas></canvas>
+      //   <div>Canvas#: ?</div>
+      // </div>
+      const newCanvas = document.createElement('canvas');
+      newCanvas.id = canvasId;
+      newCanvas.oncontextmenu = (event) => event.preventDefault();
+      newCanvas.tabIndex = -1;
+      newCanvas.internalIndex = iCanvas;
+      resizeObserver.observe(newCanvas);
+
+      const container = document.createElement('div');
+      container.className = `canvas size${Math.floor(rand(1, 4))}`;
+
+      const description = document.createElement('div');
+      description.textContent = `Canvas #: ${iCanvas + 1} ${sourceName}`;
+
+      container.onmouseenter = () => {
+        Module._startEventLoop(iCanvas);
+        newCanvas.focus();
+      };
+      container.onmouseleave = () => {
+        Module._stopEventLoop(iCanvas);
+      };
+      container.appendChild(newCanvas);
+      container.appendChild(description);
+      outerElem.appendChild(container);
+      setTimeout((i) => Module._render(i), 1000, iCanvas);
+    }
+
+    var vtkWasmRuntime = null;
+    var Module = {
+      arguments: [`${numCanvases}`],
+      preRun: (runtime) => {
+        // cache the VTK wasm runtime instance.
+        vtkWasmRuntime = runtime;
+        vtkWasmRuntime.ENV.VTK_GRAPHICS_BACKEND = "WEBGPU";
+      },
+      onRuntimeInitialized: () => {
+        if (vtkWasmRuntime !== null) {
+          lockCanvasSize(vtkWasmRuntime);
+        }
+      },
+    };
+  </script>
+  {{{ SCRIPT }}}
+</body>
+
+</html>
-- 
GitLab