From 2819bb1bd4dd3e0375fcff84035a5d08d7dbba4b Mon Sep 17 00:00:00 2001
From: Antoine Beyeler
Date: Fri, 20 Oct 2023 18:10:55 +0200
Subject: [PATCH 01/10] Add script to generate RRD vs. screenshots comparison
---
.gitignore | 3 +
.../assets/static/caret-down.svg | 3 +
.../assets/static/caret-right.svg | 3 +
.../assets/static/github-mark-white.svg | 1 +
.../assets/static/index.css | 329 ++++++++++++++++++
.../screenshot_compare/assets/static/index.js | 246 +++++++++++++
.../assets/static/manifest.json | 16 +
.../screenshot_compare/assets/static/sw.js | 27 ++
.../assets/templates/example.html | 83 +++++
.../assets/templates/index.html | 23 ++
.../build_screenshot_compare.py | 294 ++++++++++++++++
11 files changed, 1028 insertions(+)
create mode 100644 scripts/screenshot_compare/assets/static/caret-down.svg
create mode 100644 scripts/screenshot_compare/assets/static/caret-right.svg
create mode 100644 scripts/screenshot_compare/assets/static/github-mark-white.svg
create mode 100644 scripts/screenshot_compare/assets/static/index.css
create mode 100644 scripts/screenshot_compare/assets/static/index.js
create mode 100644 scripts/screenshot_compare/assets/static/manifest.json
create mode 100644 scripts/screenshot_compare/assets/static/sw.js
create mode 100644 scripts/screenshot_compare/assets/templates/example.html
create mode 100644 scripts/screenshot_compare/assets/templates/index.html
create mode 100755 scripts/screenshot_compare/build_screenshot_compare.py
diff --git a/.gitignore b/.gitignore
index 32151af5c0eb..04cd8a798fb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,5 +44,8 @@ screenshot*.png
# Web demo app build
web_demo
+# Screenshot comparison build
+/compare_screenshot
+
.nox/
*.rrd
diff --git a/scripts/screenshot_compare/assets/static/caret-down.svg b/scripts/screenshot_compare/assets/static/caret-down.svg
new file mode 100644
index 000000000000..f14b63e5e4e6
--- /dev/null
+++ b/scripts/screenshot_compare/assets/static/caret-down.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/scripts/screenshot_compare/assets/static/caret-right.svg b/scripts/screenshot_compare/assets/static/caret-right.svg
new file mode 100644
index 000000000000..2f01ce021bd8
--- /dev/null
+++ b/scripts/screenshot_compare/assets/static/caret-right.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/scripts/screenshot_compare/assets/static/github-mark-white.svg b/scripts/screenshot_compare/assets/static/github-mark-white.svg
new file mode 100644
index 000000000000..2cee3ca7accb
--- /dev/null
+++ b/scripts/screenshot_compare/assets/static/github-mark-white.svg
@@ -0,0 +1 @@
+
diff --git a/scripts/screenshot_compare/assets/static/index.css b/scripts/screenshot_compare/assets/static/index.css
new file mode 100644
index 000000000000..fbb6eb05bdf6
--- /dev/null
+++ b/scripts/screenshot_compare/assets/static/index.css
@@ -0,0 +1,329 @@
+html {
+ /* Remove touch delay: */
+ touch-action: manipulation;
+}
+
+body {
+ background: #0d1011;
+}
+
+/* Allow canvas to fill entire web page: */
+html,
+body {
+ overflow: hidden;
+ margin: 0 !important;
+ padding: 0 !important;
+ height: 100%;
+ width: 100%;
+}
+
+/* When used, the screen splitter is a 50/50 split between the canvas and the image. */
+.screen_splitter {
+ position: absolute;
+ height: 100vh;
+ width: 50vw;
+}
+
+.screen_splitter img {
+ position: absolute;
+ top: 0%;
+ left: 50vw;
+ width: 50vw;
+ /*transform: translate(-50%, 0%);*/
+}
+
+/* Position canvas in center-top: */
+canvas {
+ margin-right: auto;
+ margin-left: auto;
+ display: block;
+ position: absolute;
+ box-sizing: border-box;
+ top: 0;
+ left: 0;
+
+ /* canvas must be on top when visible */
+ z-index: 1000;
+}
+
+* {
+ font-family: "Inter", sans-serif;
+ color: #cad8de;
+}
+
+/* Match the Rerun header bar. */
+.header {
+ width: 100%;
+ height: 44px;
+ /* Actual value from Rerun Viewer */
+ background: #141414;
+}
+
+/* Create a container filling the remaining space. */
+.container {
+ position: absolute;
+ height: calc(100vh - 44px);
+ width: 100vw;
+}
+
+/* Centered div inside the container. */
+.centered {
+ margin-right: auto;
+ margin-left: auto;
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+}
+
+/* Rerun styling for normal text. */
+p {
+ /*Gray-775*/
+ color: #cad8de;
+ letter-spacing: -0.15px;
+ font-weight: 500;
+}
+
+/* Rerun styling for strong text. Matches `TextStyle::Heading`. */
+.strong {
+ /*Gray-775*/
+ color: #cad8de;
+ /* Match current rerun header value */
+ font-size: 16px;
+}
+
+/* Rerun styling for subdued text.*/
+.subdued {
+ /*Gray-550*/
+ color: #7d8c92;
+ /* Larger subdued size to match current rerun font */
+ font-size: 12px;
+ letter-spacing: -0.12px;
+}
+
+pre {
+ /*Gray-550*/
+ color: #7d8c92;
+ font-size: 10px;
+ letter-spacing: -0.12px;
+}
+
+a .button {
+ display: inline-block;
+ background: white;
+ color: black;
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ text-decoration: none;
+ font-weight: 500;
+}
+
+/* Transition to hidden */
+.hidden {
+ opacity: 0;
+ transition: opacity 0.2s ease-out;
+ visibility: hidden;
+}
+
+/* Transition to visible */
+.visible {
+ opacity: 100;
+ transition: opacity 0.2s ease-in;
+ visibility: visible;
+}
+
+.demo_header {
+ z-index: 1100;
+ position: absolute;
+ transform: translate(-50%, 0);
+ left: 50%;
+ height: 44px;
+
+ display: flex;
+ flex-direction: row;
+ gap: 4px;
+
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.no-select {
+ user-select: none;
+}
+
+.examples {
+ position: relative;
+ top: 11px;
+}
+
+.example-description {
+ margin-top: 6px;
+ height: 40px;
+ border: none;
+ border-radius: 33%;
+}
+
+.example-description:hover > .example-description-body {
+ display: flex;
+}
+
+.example-description:hover > .example-description-icon {
+ background: #404040;
+ border: none;
+ border-radius: 5px;
+}
+
+.example-description-icon {
+ display: flex;
+ justify-content: center;
+ margin: 4px;
+ padding: 2px;
+ width: 20px;
+ height: 20px;
+ user-select: none;
+}
+
+.example-description-body {
+ display: none;
+
+ flex-direction: column;
+ gap: 8px;
+
+ position: absolute;
+ top: 100%;
+
+ background: #262626;
+ border: none;
+ border-radius: 6px;
+ padding: 12px;
+
+ font-size: 13px;
+ line-height: 1.33;
+ letter-spacing: 0;
+
+ width: 280px;
+}
+
+.example-description-body > p {
+ margin: 0;
+}
+
+a.icon-link {
+ margin: 5px;
+ padding: 5px;
+ width: 24.5px;
+ height: 24px;
+}
+
+a.icon-link:hover {
+ background: #404040;
+ border: none;
+ border-radius: 50%;
+}
+
+img.icon {
+ max-height: 100%;
+}
+
+.dropdown {
+ position: relative;
+}
+
+.dropdown-title {
+ display: flex;
+ gap: 2px;
+ user-select: none;
+ cursor: pointer;
+
+ background: #141414;
+ border: none;
+ border-radius: 6px;
+ padding: 2px 5px 2px 8px;
+
+ font-style: normal;
+ font-weight: 435;
+ font-size: 14px;
+ line-height: 16px;
+}
+
+.dropdown-title > img {
+ width: 16px;
+ height: 16px;
+}
+
+.dropdown-title:hover {
+ background: #404040;
+}
+
+.dropdown-title:active {
+ background: #404040;
+}
+
+.dropdown-body {
+ display: none;
+
+ flex-direction: column;
+ gap: 3px;
+
+ position: absolute;
+ top: 100%;
+ left: 0;
+
+ background: #262626;
+ border: none;
+ border-radius: 6px;
+ padding: 6px;
+
+ user-select: none;
+ font-size: 12px;
+}
+
+.dropdown-body.visible {
+ display: flex;
+}
+
+.dropdown-entry {
+ padding: 2px 5px;
+ border: none;
+ border-radius: 6px;
+ font-size: 13px;
+ display: flex;
+ justify-content: space-between;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.dropdown-entry:hover {
+ background: #404040;
+}
+
+a.flat-link {
+ text-decoration: none;
+}
+
+.centered {
+ margin-right: auto;
+ margin-left: auto;
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: #cad8de;
+ font-size: 24px;
+ font-family: Ubuntu-Light, Helvetica, sans-serif;
+ text-align: center;
+}
+
+a.button {
+ display: inline-block;
+ background: white;
+ color: black;
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ text-decoration: none;
+ font-weight: 500;
+}
+
diff --git a/scripts/screenshot_compare/assets/static/index.js b/scripts/screenshot_compare/assets/static/index.js
new file mode 100644
index 000000000000..acd2c36bcecf
--- /dev/null
+++ b/scripts/screenshot_compare/assets/static/index.js
@@ -0,0 +1,246 @@
+function show_center_html(html) {
+ center_text_elem = document.getElementById("center_text");
+ center_text_elem.innerHTML = html;
+ center_text_elem.classList.remove("hidden");
+ center_text_elem.classList.add("visible");
+}
+function hide_center_html(html) {
+ center_text_elem = document.getElementById("center_text");
+ center_text_elem.innerHTML = html;
+ center_text_elem.classList.remove("visible");
+ center_text_elem.classList.add("hidden");
+}
+function show_canvas(html) {
+ canvas_elem = document.getElementById("the_canvas_id");
+ canvas_elem.classList.remove("hidden");
+ canvas_elem.classList.add("visible");
+ demo_header_elem = document.getElementById("header_bar");
+ demo_header_elem.classList.add("visible");
+ demo_header_elem.classList.remove("hidden");
+}
+function hide_canvas(html) {
+ canvas_elem = document.getElementById("the_canvas_id");
+ canvas_elem.classList.remove("visible");
+ canvas_elem.classList.add("hidden");
+ demo_header_elem = document.getElementById("header_bar");
+ demo_header_elem.classList.add("hidden");
+ demo_header_elem.classList.remove("visible");
+}
+
+// On mobile platforms show a warning, but provide a link to try anyways
+if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
+ show_center_html(`
+
+ Rerun is not yet supported on mobile browsers.
+
+
+ Try anyways
+
`);
+ document.querySelector("#try_anyways").addEventListener("click", function (event) {
+ event.preventDefault();
+ load_wasm();
+ });
+} else {
+ load_wasm();
+}
+
+function load_wasm() {
+ // We'll defer our execution until the wasm is ready to go.
+ // Here we tell bindgen the path to the wasm file so it can start
+ // initialization and return to us a promise when it's done.
+
+ document.getElementById("center_text").innerHTML = `
+
+ Loading Application Bundle…
+
+
+
`;
+
+ const status_element = document.getElementById("status");
+ function progress({ loaded, total_bytes }) {
+ if (total_bytes != null) {
+ status_element.innerHTML = Math.round(Math.min((loaded / total_bytes) * 100, 100)) + "%";
+ } else {
+ status_element.innerHTML = (loaded / (1024 * 1024)).toFixed(1) + "MiB";
+ }
+ }
+
+ var timeoutId = setTimeout(function () {
+ document.getElementById("center_text").classList.remove("hidden");
+ document.getElementById("center_text").classList.add("visible");
+ }, 1500);
+
+ async function wasm_with_progress() {
+ const response = await fetch("./re_viewer_bg.wasm");
+ // Use the uncompressed size
+ var content_length;
+ var content_multiplier = 1;
+ // If the content is gzip encoded, try to get the uncompressed size.
+ if (response.headers.get("content-encoding") == "gzip") {
+ content_length = response.headers.get("x-goog-meta-uncompressed-size");
+
+ // If the uncompressed size wasn't found 3 seems to be a very good approximation
+ if (content_length == null) {
+ content_length = response.headers.get("content-length");
+ content_multiplier = 3;
+ }
+ } else {
+ content_length = response.headers.get("content-length");
+ }
+
+ const total_bytes = parseInt(content_length, 10) * content_multiplier;
+ let loaded = 0;
+
+ const res = new Response(
+ new ReadableStream({
+ async start(controller) {
+ const reader = response.body.getReader();
+ for (;;) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ loaded += value.byteLength;
+ progress({ loaded, total_bytes });
+ controller.enqueue(value);
+ }
+ controller.close();
+ },
+ }),
+ {
+ status: response.status,
+ statusText: response.statusText,
+ }
+ );
+
+ for (const [key, value] of response.headers.entries()) {
+ res.headers.set(key, value);
+ }
+
+ wasm_bindgen(res)
+ .then(() => (clearTimeout(timeoutId), on_wasm_loaded()))
+ .catch(on_wasm_error);
+ }
+
+ wasm_with_progress();
+}
+
+function on_wasm_loaded() {
+ window.set_email = (value) => wasm_bindgen.set_email(value);
+
+ // WebGPU version is currently only supported on browsers with WebGPU support, there is no dynamic fallback to WebGL.
+ if (wasm_bindgen.is_webgpu_build() && typeof navigator.gpu === "undefined") {
+ console.debug("`navigator.gpu` is undefined. This indicates lack of WebGPU support.");
+ show_center_html(`
+
+ Missing WebGPU support.
+
+
+ This version of Rerun requires WebGPU support which is not available in your browser.
+ Either try a different browser or use the WebGL version of Rerun.
+
`);
+ return;
+ }
+
+ console.debug("Wasm loaded. Starting app…");
+
+ let handle = new wasm_bindgen.WebHandle();
+
+ function check_for_panic() {
+ if (handle.has_panicked()) {
+ console.error("Rerun has crashed");
+
+ document.getElementById("the_canvas_id").remove();
+
+ show_center_html(`
+
+ Rerun has crashed.
+
+ ${handle.panic_message()}
+
+ See the console for details.
+
+
+ Reload the page to try again.
+
`);
+ } else {
+ let delay_ms = 1000;
+ setTimeout(check_for_panic, delay_ms);
+ }
+ }
+
+ check_for_panic();
+
+ let url = determine_url();
+ handle
+ .start("the_canvas_id", url)
+ .then(on_app_started)
+ .catch(on_wasm_error);
+}
+
+function on_app_started(handle) {
+ // Call `handle.destroy()` to stop. Uncomment to quick result:
+ // setTimeout(() => { handle.destroy(); handle.free()) }, 2000)
+
+ console.debug("App started.");
+
+ hide_center_html();
+ show_canvas();
+
+ if (window.location !== window.parent.location) {
+ window.parent.postMessage("READY", "*");
+ }
+}
+
+function determine_url() {
+ const base = window.location.pathname.endsWith("/")
+ ? window.location.pathname.slice(0, -1)
+ : window.location.pathname;
+ return base + "/data.rrd";
+}
+
+function on_wasm_error(error) {
+ console.error("Failed to start: " + error);
+
+ let render_backend_name = "WebGPU/WebGL";
+ try {
+ render_backend_name = wasm_bindgen.is_webgpu_build() ? "WebGPU" : "WebGL";
+ } catch (e) {
+ // loading the wasm probably failed.
+ }
+
+ hide_canvas();
+ show_center_html(`
+
+ An error occurred during loading:
+
+
+ ${error}
+
+
+ Make sure you use a modern browser with ${render_backend_name} and Wasm enabled.
+
`);
+}
+
+// open/close dropdown
+document.querySelector("#examples").addEventListener("click", () => {
+ const body = document.querySelector(".dropdown-body");
+ if (!body) return;
+ if (body.classList.contains("visible")) {
+ body.classList.remove("visible");
+ } else {
+ body.classList.add("visible");
+ }
+});
+
+// close dropdowns by clicking outside of it
+document.body.addEventListener("click", (event) => {
+ const body = document.querySelector(".dropdown-body");
+ if (!body) return;
+
+ const is_dropdown = (element) =>
+ element instanceof HTMLElement && element.classList.contains("dropdown");
+
+ if (!event.composedPath().find(is_dropdown)) {
+ body.classList.remove("visible");
+ }
+});
+
diff --git a/scripts/screenshot_compare/assets/static/manifest.json b/scripts/screenshot_compare/assets/static/manifest.json
new file mode 100644
index 000000000000..5f31d7b55c51
--- /dev/null
+++ b/scripts/screenshot_compare/assets/static/manifest.json
@@ -0,0 +1,16 @@
+{
+ "name": "Rerun Viewer",
+ "short_name": "rerun-viewer-pwa",
+ "icons": [
+ {
+ "src": "./icon-256.png",
+ "sizes": "256x256",
+ "type": "image/png"
+ }
+ ],
+ "lang": "en-US",
+ "start_url": "./index.html",
+ "display": "standalone",
+ "background_color": "white",
+ "theme_color": "white"
+}
diff --git a/scripts/screenshot_compare/assets/static/sw.js b/scripts/screenshot_compare/assets/static/sw.js
new file mode 100644
index 000000000000..afdf8339e24a
--- /dev/null
+++ b/scripts/screenshot_compare/assets/static/sw.js
@@ -0,0 +1,27 @@
+var cacheName = 'rerun-viewer-pwa';
+var filesToCache = [
+ './',
+ './index.html',
+ './index.js',
+ './index.css',
+ './re_viewer.js',
+ './re_viewer_bg.wasm',
+];
+
+/* Start the service worker and cache all of the app's content */
+self.addEventListener('install', function (e) {
+ e.waitUntil(
+ caches.open(cacheName).then(function (cache) {
+ return cache.addAll(filesToCache);
+ })
+ );
+});
+
+/* Serve cached content when offline */
+self.addEventListener('fetch', function (e) {
+ e.respondWith(
+ caches.match(e.request).then(function (response) {
+ return response || fetch(e.request);
+ })
+ );
+});
diff --git a/scripts/screenshot_compare/assets/templates/example.html b/scripts/screenshot_compare/assets/templates/example.html
new file mode 100644
index 000000000000..6f285f598f72
--- /dev/null
+++ b/scripts/screenshot_compare/assets/templates/example.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+ Compare Screenshot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/screenshot_compare/assets/templates/index.html b/scripts/screenshot_compare/assets/templates/index.html
new file mode 100644
index 000000000000..e42ab977b613
--- /dev/null
+++ b/scripts/screenshot_compare/assets/templates/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+ Screenshot compare
+
+
+
+
+
+
+
+
diff --git a/scripts/screenshot_compare/build_screenshot_compare.py b/scripts/screenshot_compare/build_screenshot_compare.py
new file mode 100755
index 000000000000..9d34dcb1b053
--- /dev/null
+++ b/scripts/screenshot_compare/build_screenshot_compare.py
@@ -0,0 +1,294 @@
+#!/usr/bin/env python3
+
+"""
+Generate comparison between examples and their related screenshots.
+
+This script builds/gather RRDs and corresponding screenshots and displays
+them side-by-side. It pulls from the following sources:
+
+- The screenshots listed in .fbs files (crates/re_types/definitions/rerun/**/*.fbs),
+ and the corresponding code examples in the docs (docs/code-examples/*.rs)
+- The `demo.rerun.io` examples, as built by the `build_demo_app.py` script.
+"""
+
+from __future__ import annotations
+
+import argparse
+import http.server
+import json
+import os
+import shutil
+import subprocess
+import threading
+from dataclasses import dataclass
+from functools import partial
+from io import BytesIO
+from pathlib import Path
+from typing import Any, Iterable
+
+import requests
+from jinja2 import Template
+from PIL import Image
+
+BASE_PATH = Path("compare_screenshot")
+
+
+SCRIPT_DIR_PATH = Path(__file__).parent
+STATIC_ASSETS = SCRIPT_DIR_PATH / "assets" / "static"
+TEMPLATE_DIR = SCRIPT_DIR_PATH / "assets" / "templates"
+INDEX_TEMPLATE = Template((TEMPLATE_DIR / "index.html").read_text())
+EXAMPLE_TEMPLATE = Template((TEMPLATE_DIR / "example.html").read_text())
+RERUN_DIR = SCRIPT_DIR_PATH.parent.parent
+CODE_EXAMPLE_DIR = RERUN_DIR / "docs" / "code-examples"
+
+
+def measure_thumbnail(url: str) -> Any:
+ """Downloads `url` and returns its width and height."""
+ response = requests.get(url)
+ response.raise_for_status()
+ image = Image.open(BytesIO(response.content))
+ return image.size
+
+
+def run(
+ args: list[str], *, env: dict[str, str] | None = None, timeout: int | None = None, cwd: str | Path | None = None
+) -> None:
+ print(f"> {subprocess.list2cmdline(args)}")
+ result = subprocess.run(args, env=env, cwd=cwd, timeout=timeout, check=False, capture_output=True, text=True)
+ assert (
+ result.returncode == 0
+ ), f"{subprocess.list2cmdline(args)} failed with exit-code {result.returncode}. Output:\n{result.stdout}\n{result.stderr}"
+
+
+@dataclass
+class Example:
+ name: str
+ title: str
+ rrd: Path
+ screenshot_url: str
+ description_html: str = ""
+ source_url: str = ""
+
+
+def copy_static_assets(examples: list[Example]) -> None:
+ # copy root
+ dst = BASE_PATH
+ print(f"\nCopying static assets from {STATIC_ASSETS} to {dst}")
+ shutil.copytree(STATIC_ASSETS, dst, dirs_exist_ok=True)
+
+ # copy examples
+ for example in examples:
+ dst = os.path.join(BASE_PATH, f"examples/{example.name}")
+ shutil.copytree(
+ STATIC_ASSETS,
+ dst,
+ dirs_exist_ok=True,
+ ignore=shutil.ignore_patterns("index.html"),
+ )
+
+
+def build_python_sdk() -> None:
+ print("Building Python SDK…")
+ run(
+ [
+ "maturin",
+ "develop",
+ "--manifest-path",
+ "rerun_py/Cargo.toml",
+ '--extras="tests"',
+ "--quiet",
+ ]
+ )
+
+
+def build_wasm() -> None:
+ print("")
+ run(["cargo", "r", "-p", "re_build_web_viewer", "--", "--release"])
+
+
+def copy_wasm(examples: list[Example]) -> None:
+ files = ["re_viewer_bg.wasm", "re_viewer.js"]
+ for example in examples:
+ for file in files:
+ shutil.copyfile(
+ os.path.join("web_viewer", file),
+ os.path.join(BASE_PATH, f"examples/{example.name}", file),
+ )
+
+
+# ====================================================================================================
+# CODE EXAMPLES
+#
+# We scrape FBS for screenshot URL and generate the corresponding code examples RRD with roundtrips.py
+# ====================================================================================================
+
+
+def extract_code_example_urls_from_fbs() -> dict[str, str]:
+ fbs_path = SCRIPT_DIR_PATH.parent.parent / "crates" / "re_types" / "definitions" / "rerun"
+
+ urls = {}
+ for fbs in fbs_path.glob("**/*.fbs"):
+ for line in fbs.read_text().splitlines():
+ if line.startswith(r"/// \example"):
+ if "!api" in line:
+ continue
+
+ name = line.split()[2]
+
+ idx = line.find('image="')
+ if idx != -1:
+ end_idx = line.find('"', idx + 8)
+ if end_idx == -1:
+ end_idx = len(line)
+ urls[name] = line[idx + 7 : end_idx]
+
+ return urls
+
+
+CODE_EXAMPLE_URLS = extract_code_example_urls_from_fbs()
+
+
+def build_code_examples() -> None:
+ cmd = [
+ str(CODE_EXAMPLE_DIR / "roundtrips.py"),
+ "--no-py",
+ "--no-cpp",
+ "--no-py-build",
+ "--no-cpp-build",
+ ]
+
+ for name in CODE_EXAMPLE_URLS.keys():
+ run(cmd + [name], cwd=RERUN_DIR)
+
+
+def collect_code_examples() -> Iterable[Example]:
+ for name in sorted(CODE_EXAMPLE_URLS.keys()):
+ rrd = CODE_EXAMPLE_DIR / f"{name}_rust.rrd"
+ assert rrd.exists(), f"Missing {rrd} for {name}"
+ yield Example(name=name, title=name, rrd=rrd, screenshot_url=CODE_EXAMPLE_URLS[name])
+
+
+# ====================================================================================================
+# DEMO EXAMPLES
+#
+# We run the `build_demo_app.py` script and scrap the output "web_demo" directory.
+# ====================================================================================================
+
+
+BUILD_DEMO_APP_SCRIPT = RERUN_DIR / "scripts" / "ci" / "build_demo_app.py"
+
+
+def build_demo_examples(skip_example_build: bool = False) -> None:
+ cmd = [
+ str(BUILD_DEMO_APP_SCRIPT),
+ "--skip-build", # we handle that ourselves
+ ]
+
+ if skip_example_build:
+ cmd.append("--skip-example-build")
+
+ run(cmd, cwd=RERUN_DIR)
+
+
+def collect_demo_examples() -> Iterable[Example]:
+ web_demo_example_dir = SCRIPT_DIR_PATH.parent.parent / "web_demo" / "examples"
+ assert web_demo_example_dir.exists(), "Web demos have not been built yet."
+
+ manifest = json.loads((web_demo_example_dir / "manifest.json").read_text())
+
+ for example in manifest:
+ name = example["name"]
+ rrd = web_demo_example_dir / f"{name}" / "data.rrd"
+ assert rrd.exists(), f"Missing {rrd} for {name}"
+
+ yield Example(
+ name=name,
+ title=example["title"],
+ rrd=rrd,
+ screenshot_url=example["thumbnail"]["url"],
+ )
+
+
+def collect_examples() -> Iterable[Example]:
+ yield from collect_code_examples()
+ yield from collect_demo_examples()
+
+
+def render_index(examples: list[Example]) -> None:
+ index_path = BASE_PATH / "index.html"
+ print(f"Rendering index.html -> {index_path}")
+ index_path.write_text(INDEX_TEMPLATE.render(examples=examples))
+
+
+def render_examples(examples: list[Example]) -> None:
+ print("Rendering examples")
+
+ for example in examples:
+ target_path = BASE_PATH / "examples" / example.name
+ target_path.mkdir(parents=True, exist_ok=True)
+ index_path = target_path / "index.html"
+ print(f"{example.name} -> {index_path}")
+ index_path.write_text(EXAMPLE_TEMPLATE.render(example=example, examples=examples))
+
+ shutil.copy(example.rrd, target_path / "data.rrd")
+
+
+def serve_files() -> None:
+ def serve() -> None:
+ print("\nServing examples at http://127.0.0.1:8080/\n")
+ server = http.server.HTTPServer(
+ server_address=("127.0.0.1", 8080),
+ RequestHandlerClass=partial(
+ http.server.SimpleHTTPRequestHandler,
+ directory=BASE_PATH,
+ ),
+ )
+ server.serve_forever()
+
+ threading.Thread(target=serve, daemon=True).start()
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument(
+ "--serve",
+ action="store_true",
+ help="Serve the app on this port after building [default: 8080]",
+ )
+ parser.add_argument("--skip-build", action="store_true", help="Skip building the Python SDK and web viewer Wasm.")
+ parser.add_argument("--skip-example-build", action="store_true", help="Skip building the RRDs.")
+
+ args = parser.parse_args()
+
+ if not args.skip_build:
+ build_python_sdk()
+ build_wasm()
+
+ if not args.skip_example_build:
+ build_code_examples()
+ build_demo_examples()
+
+ examples = list(collect_examples())
+ assert len(examples) > 0, "No examples found"
+
+ render_index(examples)
+ render_examples(examples)
+ copy_static_assets(examples)
+ copy_wasm(examples)
+
+ if args.serve:
+ serve_files()
+
+ while True:
+ try:
+ print("Press enter to reload static files")
+ input()
+ render_examples(examples)
+ copy_static_assets(examples)
+ copy_wasm(examples)
+ except KeyboardInterrupt:
+ break
+
+
+if __name__ == "__main__":
+ main()
From a56257bfd68f0d4a9287dcc6a1bda247ffcbc35d Mon Sep 17 00:00:00 2001
From: Antoine Beyeler
Date: Fri, 20 Oct 2023 18:19:45 +0200
Subject: [PATCH 02/10] Improved docstring
---
scripts/screenshot_compare/build_screenshot_compare.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/scripts/screenshot_compare/build_screenshot_compare.py b/scripts/screenshot_compare/build_screenshot_compare.py
index 9d34dcb1b053..75aabbab5bcf 100755
--- a/scripts/screenshot_compare/build_screenshot_compare.py
+++ b/scripts/screenshot_compare/build_screenshot_compare.py
@@ -9,6 +9,9 @@
- The screenshots listed in .fbs files (crates/re_types/definitions/rerun/**/*.fbs),
and the corresponding code examples in the docs (docs/code-examples/*.rs)
- The `demo.rerun.io` examples, as built by the `build_demo_app.py` script.
+
+The comparison is generated in the `compare_screenshot` directory. Use the `--serve`
+option to show them in a browser.
"""
from __future__ import annotations
From e031f77a9ae192975ddd084d6ab59bae1a8cc902 Mon Sep 17 00:00:00 2001
From: Antoine Beyeler
Date: Fri, 20 Oct 2023 18:20:00 +0200
Subject: [PATCH 03/10] Improved docstring moar
---
scripts/screenshot_compare/build_screenshot_compare.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/screenshot_compare/build_screenshot_compare.py b/scripts/screenshot_compare/build_screenshot_compare.py
index 75aabbab5bcf..4db8e433395d 100755
--- a/scripts/screenshot_compare/build_screenshot_compare.py
+++ b/scripts/screenshot_compare/build_screenshot_compare.py
@@ -10,7 +10,7 @@
and the corresponding code examples in the docs (docs/code-examples/*.rs)
- The `demo.rerun.io` examples, as built by the `build_demo_app.py` script.
-The comparison is generated in the `compare_screenshot` directory. Use the `--serve`
+The comparisons are generated in the `compare_screenshot` directory. Use the `--serve`
option to show them in a browser.
"""
From ee466a0b8c1b71fcae7ec62676d954190ac70a95 Mon Sep 17 00:00:00 2001
From: Antoine Beyeler
Date: Mon, 23 Oct 2023 10:45:08 +0200
Subject: [PATCH 04/10] Don't duplicate the wasm binary everywhere (1GiB ->
250MiB)
The remaining disk size is primarily explained by the demo examples' RRD (50-70MiB each).
---
.../screenshot_compare/assets/static/index.js | 2 +-
.../assets/templates/example.html | 2 +-
.../build_screenshot_compare.py | 19 +++++++++++--------
3 files changed, 13 insertions(+), 10 deletions(-)
diff --git a/scripts/screenshot_compare/assets/static/index.js b/scripts/screenshot_compare/assets/static/index.js
index acd2c36bcecf..fd2937c5a11b 100644
--- a/scripts/screenshot_compare/assets/static/index.js
+++ b/scripts/screenshot_compare/assets/static/index.js
@@ -71,7 +71,7 @@ function load_wasm() {
}, 1500);
async function wasm_with_progress() {
- const response = await fetch("./re_viewer_bg.wasm");
+ const response = await fetch("../re_viewer_bg.wasm");
// Use the uncompressed size
var content_length;
var content_multiplier = 1;
diff --git a/scripts/screenshot_compare/assets/templates/example.html b/scripts/screenshot_compare/assets/templates/example.html
index 6f285f598f72..6a690b47966c 100644
--- a/scripts/screenshot_compare/assets/templates/example.html
+++ b/scripts/screenshot_compare/assets/templates/example.html
@@ -73,7 +73,7 @@
-
+
-
-
-
-
-
-
-
+
-
-
-
-
-
diff --git a/scripts/screenshot_compare/build_screenshot_compare.py b/scripts/screenshot_compare/build_screenshot_compare.py
index 4db8e433395d..5103fd3d1a88 100755
--- a/scripts/screenshot_compare/build_screenshot_compare.py
+++ b/scripts/screenshot_compare/build_screenshot_compare.py
@@ -109,14 +109,15 @@ def build_wasm() -> None:
run(["cargo", "r", "-p", "re_build_web_viewer", "--", "--release"])
-def copy_wasm(examples: list[Example]) -> None:
+def copy_wasm() -> None:
+ """Copies the wasm/js files at the root of the `examples` subdirectory."""
files = ["re_viewer_bg.wasm", "re_viewer.js"]
- for example in examples:
- for file in files:
- shutil.copyfile(
- os.path.join("web_viewer", file),
- os.path.join(BASE_PATH, f"examples/{example.name}", file),
- )
+
+ for file in files:
+ shutil.copyfile(
+ os.path.join("web_viewer", file),
+ os.path.join(BASE_PATH, "examples", file),
+ )
# ====================================================================================================
@@ -218,6 +219,8 @@ def collect_examples() -> Iterable[Example]:
def render_index(examples: list[Example]) -> None:
+ BASE_PATH.mkdir(exist_ok=True)
+
index_path = BASE_PATH / "index.html"
print(f"Rendering index.html -> {index_path}")
index_path.write_text(INDEX_TEMPLATE.render(examples=examples))
@@ -277,7 +280,7 @@ def main() -> None:
render_index(examples)
render_examples(examples)
copy_static_assets(examples)
- copy_wasm(examples)
+ copy_wasm()
if args.serve:
serve_files()
From 7e1c645fb5bf8faa7726152cad40a1fd48e4a2b3 Mon Sep 17 00:00:00 2001
From: Antoine Beyeler
Date: Mon, 23 Oct 2023 11:14:08 +0200
Subject: [PATCH 05/10] Do _not_ skip on `!api`, these are important too.
---
scripts/screenshot_compare/build_screenshot_compare.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/scripts/screenshot_compare/build_screenshot_compare.py b/scripts/screenshot_compare/build_screenshot_compare.py
index 5103fd3d1a88..dbe039db9610 100755
--- a/scripts/screenshot_compare/build_screenshot_compare.py
+++ b/scripts/screenshot_compare/build_screenshot_compare.py
@@ -134,9 +134,6 @@ def extract_code_example_urls_from_fbs() -> dict[str, str]:
for fbs in fbs_path.glob("**/*.fbs"):
for line in fbs.read_text().splitlines():
if line.startswith(r"/// \example"):
- if "!api" in line:
- continue
-
name = line.split()[2]
idx = line.find('image="')
From 394ca4bbd883e9eb32dede322eefaec910b59486 Mon Sep 17 00:00:00 2001
From: Antoine Beyeler
Date: Mon, 23 Oct 2023 14:57:01 +0200
Subject: [PATCH 06/10] Fixed bug with reload
---
scripts/screenshot_compare/build_screenshot_compare.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/screenshot_compare/build_screenshot_compare.py b/scripts/screenshot_compare/build_screenshot_compare.py
index dbe039db9610..c4aa08324f4f 100755
--- a/scripts/screenshot_compare/build_screenshot_compare.py
+++ b/scripts/screenshot_compare/build_screenshot_compare.py
@@ -288,7 +288,7 @@ def main() -> None:
input()
render_examples(examples)
copy_static_assets(examples)
- copy_wasm(examples)
+ copy_wasm()
except KeyboardInterrupt:
break
From 2048a600f84f509f8cfc54c5c780c1cac8aa460a Mon Sep 17 00:00:00 2001
From: Antoine Beyeler
Date: Mon, 23 Oct 2023 17:27:58 +0200
Subject: [PATCH 07/10] Use `rr.serve()` and iframe instead of a canvas
---
.../assets/static/caret-down.svg | 3 -
.../assets/static/caret-right.svg | 3 -
.../assets/static/github-mark-white.svg | 1 -
.../assets/static/index.css | 292 +-----------------
.../screenshot_compare/assets/static/index.js | 4 +-
.../assets/static/manifest.json | 16 -
.../screenshot_compare/assets/static/sw.js | 27 --
.../assets/templates/example.html | 63 +---
.../build_screenshot_compare.py | 49 +--
9 files changed, 30 insertions(+), 428 deletions(-)
delete mode 100644 scripts/screenshot_compare/assets/static/caret-down.svg
delete mode 100644 scripts/screenshot_compare/assets/static/caret-right.svg
delete mode 100644 scripts/screenshot_compare/assets/static/github-mark-white.svg
delete mode 100644 scripts/screenshot_compare/assets/static/manifest.json
delete mode 100644 scripts/screenshot_compare/assets/static/sw.js
diff --git a/scripts/screenshot_compare/assets/static/caret-down.svg b/scripts/screenshot_compare/assets/static/caret-down.svg
deleted file mode 100644
index f14b63e5e4e6..000000000000
--- a/scripts/screenshot_compare/assets/static/caret-down.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/scripts/screenshot_compare/assets/static/caret-right.svg b/scripts/screenshot_compare/assets/static/caret-right.svg
deleted file mode 100644
index 2f01ce021bd8..000000000000
--- a/scripts/screenshot_compare/assets/static/caret-right.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/scripts/screenshot_compare/assets/static/github-mark-white.svg b/scripts/screenshot_compare/assets/static/github-mark-white.svg
deleted file mode 100644
index 2cee3ca7accb..000000000000
--- a/scripts/screenshot_compare/assets/static/github-mark-white.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/scripts/screenshot_compare/assets/static/index.css b/scripts/screenshot_compare/assets/static/index.css
index fbb6eb05bdf6..10bdb96c6722 100644
--- a/scripts/screenshot_compare/assets/static/index.css
+++ b/scripts/screenshot_compare/assets/static/index.css
@@ -7,7 +7,6 @@ body {
background: #0d1011;
}
-/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
@@ -17,7 +16,6 @@ body {
width: 100%;
}
-/* When used, the screen splitter is a 50/50 split between the canvas and the image. */
.screen_splitter {
position: absolute;
height: 100vh;
@@ -26,14 +24,11 @@ body {
.screen_splitter img {
position: absolute;
- top: 0%;
left: 50vw;
width: 50vw;
- /*transform: translate(-50%, 0%);*/
}
-/* Position canvas in center-top: */
-canvas {
+iframe {
margin-right: auto;
margin-left: auto;
display: block;
@@ -41,289 +36,16 @@ canvas {
box-sizing: border-box;
top: 0;
left: 0;
+ width: 50vw;
+ height: 100vh;
+
+ border: none;
/* canvas must be on top when visible */
z-index: 1000;
}
* {
- font-family: "Inter", sans-serif;
- color: #cad8de;
-}
-
-/* Match the Rerun header bar. */
-.header {
- width: 100%;
- height: 44px;
- /* Actual value from Rerun Viewer */
- background: #141414;
-}
-
-/* Create a container filling the remaining space. */
-.container {
- position: absolute;
- height: calc(100vh - 44px);
- width: 100vw;
-}
-
-/* Centered div inside the container. */
-.centered {
- margin-right: auto;
- margin-left: auto;
- display: block;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
-}
-
-/* Rerun styling for normal text. */
-p {
- /*Gray-775*/
- color: #cad8de;
- letter-spacing: -0.15px;
- font-weight: 500;
-}
-
-/* Rerun styling for strong text. Matches `TextStyle::Heading`. */
-.strong {
- /*Gray-775*/
- color: #cad8de;
- /* Match current rerun header value */
- font-size: 16px;
-}
-
-/* Rerun styling for subdued text.*/
-.subdued {
- /*Gray-550*/
- color: #7d8c92;
- /* Larger subdued size to match current rerun font */
- font-size: 12px;
- letter-spacing: -0.12px;
-}
-
-pre {
- /*Gray-550*/
- color: #7d8c92;
- font-size: 10px;
- letter-spacing: -0.12px;
-}
-
-a .button {
- display: inline-block;
- background: white;
- color: black;
- padding: 0.75rem 1rem;
- border-radius: 8px;
- text-decoration: none;
- font-weight: 500;
-}
-
-/* Transition to hidden */
-.hidden {
- opacity: 0;
- transition: opacity 0.2s ease-out;
- visibility: hidden;
-}
-
-/* Transition to visible */
-.visible {
- opacity: 100;
- transition: opacity 0.2s ease-in;
- visibility: visible;
-}
-
-.demo_header {
- z-index: 1100;
- position: absolute;
- transform: translate(-50%, 0);
- left: 50%;
- height: 44px;
-
- display: flex;
- flex-direction: row;
- gap: 4px;
-
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.no-select {
- user-select: none;
-}
-
-.examples {
- position: relative;
- top: 11px;
-}
-
-.example-description {
- margin-top: 6px;
- height: 40px;
- border: none;
- border-radius: 33%;
-}
-
-.example-description:hover > .example-description-body {
- display: flex;
-}
-
-.example-description:hover > .example-description-icon {
- background: #404040;
- border: none;
- border-radius: 5px;
-}
-
-.example-description-icon {
- display: flex;
- justify-content: center;
- margin: 4px;
- padding: 2px;
- width: 20px;
- height: 20px;
- user-select: none;
-}
-
-.example-description-body {
- display: none;
-
- flex-direction: column;
- gap: 8px;
-
- position: absolute;
- top: 100%;
-
- background: #262626;
- border: none;
- border-radius: 6px;
- padding: 12px;
-
- font-size: 13px;
- line-height: 1.33;
- letter-spacing: 0;
-
- width: 280px;
-}
-
-.example-description-body > p {
- margin: 0;
-}
-
-a.icon-link {
- margin: 5px;
- padding: 5px;
- width: 24.5px;
- height: 24px;
-}
-
-a.icon-link:hover {
- background: #404040;
- border: none;
- border-radius: 50%;
-}
-
-img.icon {
- max-height: 100%;
-}
-
-.dropdown {
- position: relative;
-}
-
-.dropdown-title {
- display: flex;
- gap: 2px;
- user-select: none;
- cursor: pointer;
-
- background: #141414;
- border: none;
- border-radius: 6px;
- padding: 2px 5px 2px 8px;
-
- font-style: normal;
- font-weight: 435;
- font-size: 14px;
- line-height: 16px;
-}
-
-.dropdown-title > img {
- width: 16px;
- height: 16px;
-}
-
-.dropdown-title:hover {
- background: #404040;
-}
-
-.dropdown-title:active {
- background: #404040;
-}
-
-.dropdown-body {
- display: none;
-
- flex-direction: column;
- gap: 3px;
-
- position: absolute;
- top: 100%;
- left: 0;
-
- background: #262626;
- border: none;
- border-radius: 6px;
- padding: 6px;
-
- user-select: none;
- font-size: 12px;
-}
-
-.dropdown-body.visible {
- display: flex;
-}
-
-.dropdown-entry {
- padding: 2px 5px;
- border: none;
- border-radius: 6px;
- font-size: 13px;
- display: flex;
- justify-content: space-between;
- white-space: nowrap;
- overflow: hidden;
-}
-
-.dropdown-entry:hover {
- background: #404040;
-}
-
-a.flat-link {
- text-decoration: none;
-}
-
-.centered {
- margin-right: auto;
- margin-left: auto;
- display: block;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
+ font-family: sans-serif;
color: #cad8de;
- font-size: 24px;
- font-family: Ubuntu-Light, Helvetica, sans-serif;
- text-align: center;
-}
-
-a.button {
- display: inline-block;
- background: white;
- color: black;
- padding: 0.75rem 1rem;
- border-radius: 8px;
- text-decoration: none;
- font-weight: 500;
-}
-
+}
\ No newline at end of file
diff --git a/scripts/screenshot_compare/assets/static/index.js b/scripts/screenshot_compare/assets/static/index.js
index fd2937c5a11b..71b494026ba3 100644
--- a/scripts/screenshot_compare/assets/static/index.js
+++ b/scripts/screenshot_compare/assets/static/index.js
@@ -38,10 +38,10 @@ if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(naviga
`);
document.querySelector("#try_anyways").addEventListener("click", function (event) {
event.preventDefault();
- load_wasm();
+ //load_wasm();
});
} else {
- load_wasm();
+ //load_wasm();
}
function load_wasm() {
diff --git a/scripts/screenshot_compare/assets/static/manifest.json b/scripts/screenshot_compare/assets/static/manifest.json
deleted file mode 100644
index 5f31d7b55c51..000000000000
--- a/scripts/screenshot_compare/assets/static/manifest.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "name": "Rerun Viewer",
- "short_name": "rerun-viewer-pwa",
- "icons": [
- {
- "src": "./icon-256.png",
- "sizes": "256x256",
- "type": "image/png"
- }
- ],
- "lang": "en-US",
- "start_url": "./index.html",
- "display": "standalone",
- "background_color": "white",
- "theme_color": "white"
-}
diff --git a/scripts/screenshot_compare/assets/static/sw.js b/scripts/screenshot_compare/assets/static/sw.js
deleted file mode 100644
index afdf8339e24a..000000000000
--- a/scripts/screenshot_compare/assets/static/sw.js
+++ /dev/null
@@ -1,27 +0,0 @@
-var cacheName = 'rerun-viewer-pwa';
-var filesToCache = [
- './',
- './index.html',
- './index.js',
- './index.css',
- './re_viewer.js',
- './re_viewer_bg.wasm',
-];
-
-/* Start the service worker and cache all of the app's content */
-self.addEventListener('install', function (e) {
- e.waitUntil(
- caches.open(cacheName).then(function (cache) {
- return cache.addAll(filesToCache);
- })
- );
-});
-
-/* Serve cached content when offline */
-self.addEventListener('fetch', function (e) {
- e.respondWith(
- caches.match(e.request).then(function (response) {
- return response || fetch(e.request);
- })
- );
-});
diff --git a/scripts/screenshot_compare/assets/templates/example.html b/scripts/screenshot_compare/assets/templates/example.html
index 6a690b47966c..ac434c419f0f 100644
--- a/scripts/screenshot_compare/assets/templates/example.html
+++ b/scripts/screenshot_compare/assets/templates/example.html
@@ -1,5 +1,4 @@
-
@@ -8,76 +7,24 @@
Compare Screenshot
-
-
-