From 0a5b6a41b019e013bce3eb400252240a1accab51 Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Mon, 19 Aug 2024 23:17:26 +0300 Subject: [PATCH] add demo html/css/js --- src/bonsai/bonsai/bim/data/webui/sioserver.py | 14 +- .../bonsai/bim/data/webui/static/css/demo.css | 348 ++++++++++++++++++ .../bonsai/bim/data/webui/static/js/demo.js | 146 ++++++++ .../bonsai/bim/data/webui/templates/demo.html | 230 ++++++++++++ src/bonsai/bonsai/bim/module/demo/operator.py | 1 + 5 files changed, 731 insertions(+), 8 deletions(-) create mode 100644 src/bonsai/bonsai/bim/data/webui/static/css/demo.css create mode 100644 src/bonsai/bonsai/bim/data/webui/static/js/demo.js create mode 100644 src/bonsai/bonsai/bim/data/webui/templates/demo.html diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index d81948d58d..fca2140249 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -20,14 +20,6 @@ sio = socketio.AsyncServer( cors_allowed_origins="*", async_mode="aiohttp", ) - -# sio.instrument( -# auth={ -# "username": "admin", -# "password": "admin", -# } -# ) - app = web.Application() sio.attach(app) @@ -37,6 +29,8 @@ blender_messages = {} blender_theme = {} +# we define two namespaces, one for Bonsai and one for Web UI +# each namespace has its own event handlers that are called when an event is emitted by a client # Web namespace class WebNamespace(socketio.AsyncNamespace): def __init__(self, namespace): @@ -120,6 +114,8 @@ class BlenderNamespace(socketio.AsyncNamespace): blender_theme = data await sio.emit("theme_data", data, namespace="/web") + + # this function will be called when the event demo_data is emitted async def on_demo_data(self, sid, data): print(f"Demo data from Blender client {sid}") blender_messages[sid]["demo_data"] = data @@ -147,6 +143,8 @@ async def documentation(request): return web.Response(text=html_content, content_type="text/html") +# This is a request handler for the /demo URL endpoint. +# It serves the demo HTML file after using pystache to render the variables async def demo(request): with open("templates/demo.html", "r") as f: template = f.read() diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/demo.css b/src/bonsai/bonsai/bim/data/webui/static/css/demo.css new file mode 100644 index 0000000000..9a2460c90f --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/static/css/demo.css @@ -0,0 +1,348 @@ +:root { + --font-family: Arial, sans-serif; + --base-font-size: 16px; + --margin-tiny: 0.125rem; + --margin-small: 0.625rem; + --margin-medium: 1.25rem; + --margin-large: 2.5rem; + --padding-tiny: 0.125rem; + --padding-small: 0.625rem; + --padding-medium: 1rem; + --font-size-large: 1.2rem; + --logo-height: 1.5rem; + --nav-height: 0.625rem; + --border-radius: 0.3rem; +} + +:root.dark { + color-scheme: dark; + --bg-color: #252525; + --text-color: #e0e0e0; + --nav-bg-color: #121212; + --nav-border-color: #25682a; + --nav-link-color: #fff; + --nav-link-hover-color: #3fb449; + --warning-color: #FFDB8F; + --border-color: #464444; +} + +:root.light { + color-scheme: light; + --bg-color: #ffffff; + --text-color: #000000; + --nav-bg-color: #f8f8f8; + --nav-border-color: #cccccc; + --nav-link-color: #000000; + --nav-link-hover-color: #38a63d; + --warning-color: #FF4500; + --border-color: #222; +} + +html { + font-size: var(--base-font-size); +} + +body { + background-color: var(--blender-window-background, var(--bg-color)); + color: var(--blender-text, var(--text-color)); + margin: 0; + font-family: var(--font-family); + display: flex; + flex-direction: column; + min-height: 100vh; +} + +#container { + flex: 1; + margin-top: var(--margin-medium); + margin-left: var(--margin-small); + margin-right: var(--margin-small); + margin-bottom: var(--margin-medium); +} + +h3 { + margin-top: 0; +} + +nav { + background-color: var(--blender-top-bar-header, var(--nav-bg-color)); + height: var(--nav-height); + padding: var(--padding-medium) 0; + display: flex; + align-items: center; + position: relative; + border-bottom: 2px solid var(--blender-tab-outline, var(--nav-border-color)); +} + +nav .logo { + margin-left: var(--margin-large); + height: var(--logo-height); +} + +nav ul { + list-style-type: none; + margin: 0; + display: flex; + flex: 1; + justify-content: center; +} + +nav ul li { + margin-right: var(--margin-large); +} + +nav ul li a { + text-decoration: none; + color: var(--blender-text, var(--nav-link-color)); +} + +nav ul li a:hover, +nav ul li a.active { + color: var(--blender-selected-object, var(--nav-link-hover-color)); +} + +#toggle-theme { + border: none; + background: none; + cursor: pointer; + font-size: var(--font-size-large); + margin-right: var(--margin-medium); +} + +#toggle-theme:focus { + outline: none; +} + +button, +input { + background-color: var(--blender-button-background, var(--bg-color)); + color: var(--blender-button-text, var(--text-color)); + border-color: var(--blender-button-border, var(--border-color)); + transition: filter 0.2s ease; +} + +footer { + background-color: var(--blender-top-bar-header, var(--nav-bg-color)); + text-align: right; + padding: var(--padding-tiny); + border-top: 1px solid var(--blender-tab-outline, var(--nav-border-color)); +} + +footer p { + margin: 0; + color: var(--blender-text, var(--nav-link-color)); + margin-right: var(--margin-small); + font-size: 0.8rem; +} + + +/* =========== styling for code blocks and rendering =========== */ +/* ================== This is auto-generated ================== */ + + +#explanation { + font-family: "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; + font-size: 16px; + padding: 0 26px; + line-height: 22px; + word-wrap: break-word; + margin: 0; +} + +#code-csp-warning { + position: fixed; + top: 0; + right: 0; + color: white; + margin: 16px; + text-align: center; + font-size: 12px; + font-family: sans-serif; + background-color: #444444; + cursor: pointer; + padding: 6px; + box-shadow: 1px 1px 1px rgba(0, 0, 0, .25); +} + +#code-csp-warning:hover { + text-decoration: none; + background-color: #007acc; + box-shadow: 2px 2px 2px rgba(0, 0, 0, .25); +} + +#explanation.scrollBeyondLastLine { + margin-bottom: calc(100vh - 22px); +} + +#explanation.showEditorSelection .code-line { + position: relative; +} + +#explanation.showEditorSelection .code-active-line:before, +#explanation.showEditorSelection .code-line:hover:before { + content: ""; + display: block; + position: absolute; + top: 0; + left: -12px; + height: 100%; +} + +#explanation.showEditorSelection li.code-active-line:before, +#explanation.showEditorSelection li.code-line:hover:before { + left: -30px; +} + +#explanation a:focus, +#explanation input:focus, +#explanation select:focus, +#explanation textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; +} + +#explanation hr { + border: 0; + height: 2px; + border-bottom: 2px solid; +} + +#explanation h1 { + padding-bottom: 0.3em; + line-height: 1.2; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +#explanation h1, +#explanation h2, +#explanation h3 { + font-weight: normal; +} + +#explanation code { + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + font-size: 1em; + line-height: 1.357em; +} + +#explanation .wordWrap pre { + white-space: pre-wrap; +} + +#explanation pre:not(.hljs), +#explanation pre.hljs code>div { + padding: 16px; + border-radius: 3px; + overflow: auto; +} + +#explanation pre code { + tab-size: 4; +} + +#explanation .hljs-comment, +#explanation .hljs-quote { + color: #B6B18B; +} + +#explanation .hljs-variable, +#explanation .hljs-template-variable, +#explanation .hljs-tag, +#explanation .hljs-name, +#explanation .hljs-selector-id, +#explanation .hljs-selector-class, +#explanation .hljs-regexp, +#explanation .hljs-deletion { + color: #EB3C54; +} + +#explanation .hljs-number, +#explanation .hljs-built_in, +#explanation .hljs-builtin-name, +#explanation .hljs-literal, +#explanation .hljs-type, +#explanation .hljs-params, +#explanation .hljs-meta, +#explanation .hljs-link { + color: #E7CE56; +} + +#explanation .hljs-attribute { + color: #EE7C2B; +} + +#explanation .hljs-string, +#explanation .hljs-symbol, +#explanation .hljs-bullet, +#explanation .hljs-addition { + color: #4FB4D7; +} + +#explanation .hljs-title, +#explanation .hljs-section { + color: #78BB65; +} + +#explanation .hljs-keyword, +#explanation .hljs-selector-tag { + color: #B45EA4; +} + +#explanation .hljs { + display: block; + overflow-x: auto; + background: #1C1D21; + color: #c0c5ce; + padding: 0.5em; +} + +#explanation .hljs-emphasis { + font-style: italic; +} + +#explanation .hljs-strong { + font-weight: bold; +} + +#explanation { + font-family: "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif, "Meiryo"; + padding: 0 12px; +} + +#explanation pre { + background-color: #f8f8f8; + border: 1px solid #cccccc; + border-radius: 3px; + overflow-x: auto; + white-space: pre-wrap; + overflow-wrap: break-word; +} + +#explanation pre:not(.hljs) { + padding: 23px; + line-height: 19px; +} + +#explanation blockquote { + background: rgba(127, 127, 127, 0.1); + border-color: rgba(0, 122, 204, 0.5); +} + +#explanation .emoji { + height: 1.4em; +} + +#explanation code { + font-size: 14px; + line-height: 19px; +} + +#explanation :not(pre):not(.hljs)>code { + color: #C9AE75; + font-size: inherit; +} + +#explanation .page { + page-break-after: always; +} \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/demo.js b/src/bonsai/bonsai/bim/data/webui/static/js/demo.js new file mode 100644 index 0000000000..b674ab5014 --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/static/js/demo.js @@ -0,0 +1,146 @@ +// A global variable to hold the WebSocket connection +let socket; + +// Document ready function +// This function is triggered once the DOM is fully loaded. +// It sets the theme for the UI and +// establishes the WebSocket connection by calling connectSocket(). +$(document).ready(function () { + var defaultTheme = "blender"; // Default theme to be applied + var theme = localStorage.getItem("theme") || defaultTheme; // Retrieve the stored theme or use the default + setTheme(theme); // Apply the theme + + connectSocket(); +}); + +// Function to establish WebSocket connection +// This function constructs the WebSocket connection URL using the SOCKET_PORT variable +// and initializes the socket connection. It also registers event handlers to listen +// for various events emitted by the server, such as 'blender_connect', 'blender_disconnect', +// 'theme_data', and 'demo_data'. +function connectSocket() { + const url = "ws://localhost:" + SOCKET_PORT + "/web"; + socket = io(url); + console.log("socket: ", socket); + + // Register socket event handlers + socket.on("blender_connect", handleBlenderConnect); + socket.on("blender_disconnect", handleBlenderDisconnect); + socket.on("theme_data", handleThemeData); + socket.on("demo_data", handleDemoData); +} + +// Function to handle 'blender_connect' event +// This function is triggered when a new Bonsai instance connects to the server. +// The 'blenderId' is a unique identifier assigned by the server to each Bonsai instance. +// It helps distinguish between different instances of Bonsai connected to the server. +// The 'blenderId' is used in various parts of the code to ensure that messages and data +// are correctly associated with the appropriate Bonsai instance. +function handleBlenderConnect(blenderId) { + console.log("blender connected: ", blenderId); +} + +// Function to handle 'blender_disconnect' event +// This function is triggered when a Bonsai instance disconnects from the server. +// The 'blenderId' is used here to log which specific Bonsai instance has disconnected. +function handleBlenderDisconnect(blenderId) { + console.log("blender disconnected: ", blenderId); +} + +// Function to handle 'demo_data' event +// This function processes the demo data received from the server. +// It extracts the message and the Bonsai instance ID ('blenderId'), +// and updates the DOM to display the message. The 'blenderId' is used +// to differentiate between messages from different Bonsai instances. +// If the message for the specific Bonsai instance is already displayed, it updates the content; +// otherwise, it creates a new element to display the message. +function handleDemoData(demoData) { + console.log(demoData); + const message = demoData["data"]["demo_message"]; + const blenderId = demoData["blenderId"]; + const id = "message-" + blenderId; + const messageHeader = $("#" + id); + const messageText = `Bonsai instance with ID: ${blenderId} sent the message: ${message}`; + + if (messageHeader[0] === undefined) { + const newMessageElement = $("

", { id: id }).text(messageText); + $("#message-container").prepend(newMessageElement); + } else { + messageHeader.text(messageText); + messageHeader.prependTo("#message-container"); + } +} + +// Function to send a message to the server +// This function is triggered when the user submits a message. +// It collects the message from the input field, constructs a message object, +// and emits the 'web_operator' event to the server with the message data. +// If 'blenderId' is specified, the message is sent to a specific Bonsai instance. +function sendMessage() { + const inputMessage = $("#input-message").val(); + + const msg = { + sourcePage: "demo", + // blenderId: BlenderId, // specify the target Bonsai instance + operator: { + type: "message", + message: inputMessage, + }, + }; + socket.emit("web_operator", msg); +} + +function handleThemeData(themeData) { + // console.log(themeData); + + function arrayToRgbString(arr) { + const [r, g, b, a] = arr.map((num) => Math.round(num * 255)); + if (a !== undefined) { + return `rgba(${r}, ${g}, ${b}, ${a})`; + } + return `rgb(${r}, ${g}, ${b})`; + } + + function generateCssVariableRule(theme) { + let cssVariables = ":root.blender {\n"; + for (const key in theme) { + const cssVariableName = `--blender-${key.replace(/_/g, "-")}`; + const cssVariableValue = arrayToRgbString(theme[key]); + cssVariables += ` ${cssVariableName}: ${cssVariableValue};\n`; + } + cssVariables += "}"; + return cssVariables; + } + + const cssRule = generateCssVariableRule(themeData.theme); + console.log(cssRule); + + var styleElement = $("#demo-stylesheet")[0]; + if (styleElement) { + var sheet = styleElement.sheet || styleElement.styleSheet; + sheet.insertRule(cssRule, sheet.cssRules.length); + } +} + +function setTheme(theme) { + $("html").removeClass("light dark blender").addClass(theme); + $(":root").css("color-scheme", theme); + if (theme === "light") { + $("#toggle-theme").html(''); + } else if (theme === "dark") { + $("#toggle-theme").html(''); + } else if (theme === "blender") { + $("#toggle-theme").html(''); + } + localStorage.setItem("theme", theme); +} + +function toggleTheme() { + if ($("html").hasClass("light")) { + setTheme("dark"); + } else if ($("html").hasClass("dark")) { + setTheme("blender"); + } else { + setTheme("light"); + } +} diff --git a/src/bonsai/bonsai/bim/data/webui/templates/demo.html b/src/bonsai/bonsai/bim/data/webui/templates/demo.html new file mode 100644 index 0000000000..c68f7692e3 --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/templates/demo.html @@ -0,0 +1,230 @@ + + + + + + + BlenderBIM Web UI + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + +
+

Welcome to the web UI demo page!

+

This demo showcases how Bonsai's Web UI interacts with a WebSocket server. The demo illustrates how the Web UI establishes a connection, exchanges data with the server, and updates the UI dynamically based on incoming data.

+

You can use the above textbox to send a message to Bonsai!

+

If you prefer, you can explore the source code directly in the data/webui directory:

+
    +
  • sioserver.py
  • +
  • templates/demo.html
  • +
  • static/js/demo.js
  • +
+

Each file contains detailed comments to help you understand the code. Alternatively, continue reading this demo page to understand how the Web UI operates

+

WebScoket Server (sioserver.py)

+

In the websocket server, there are two key namespaces.

+
    +
  • Blender Namespace: Connects all Bonsai instances.
  • +
  • Web Namespace: Connects all Web UI clients.
  • +
+

These namespaces are attached to the WebSocket server, allowing event-driven communication.

+ +
class WebNamespace(socketio.AsyncNamespace): + async def on_connect(self, sid, environ): + ... + async def on_disconnect(self, sid): + ... + async def on_web_operator(self, sid, data): + ... + +class BlenderNamespace(socketio.AsyncNamespace): + async def on_connect(self, sid, environ): + ... + async def on_disconnect(self, sid): + ... + async def on_demo_data(self, sid, data): + ... +
+ +

Each namespace defines event handlers that are triggered when a connected client emits specific events. For example, the on_demo_data function is invoked when a client emits the demo_data event.

+

The server also defines routes, such as the /demo URL, which are tied to specific request handlers.

+ +
async def demo(request): + with open("templates/demo.html", "r") as f: + template = f.read() + html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version}) + return web.Response(text=html_content, content_type="text/html") + +app.router.add_get("/demo", demo) +
+ +

In this example, when the /demo URL is accessed, the server reads the demo.html template, renders it with variables like port and version, and returns the fully rendered HTML to the client's browser.

+

HTML File (templates/demo.html)

+

When the Web UI URL is visited, the server follows these steps:

+
    +
  • Retrieves the HTML file from the templates directory.
  • +
  • uses pystache to render variables, such as SOCKET_PORT, into the HTML template.
  • +
  • Sends the fully rendered HTML page to the web browser.
  • +
+

The SOCKET_PORT variable is injected into the HTML and later used by the JavaScript file to establish a WebSocket connection.

+ +
<script> + var SOCKET_PORT = {{port}}; +</script> +
+ +

After the HTML page loads, the browser automatically requests static files, such as CSS and JavaScript, from the static directory specified in the sioserver.py file.

+ +
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" /> +<script defer src="./static/js/demo.js"></script> +
+ +

JavaScript File (static/js/demo.js)

+

The JavaScript file is responsible for establishing and managing the WebSocket connection. It starts by ensuring the DOM is fully loaded before executing any scripts.

+ +
let socket; +$(document).ready(function () { + var defaultTheme = "blender"; // Default theme to be applied + var theme = localStorage.getItem("theme") || defaultTheme; // Retrieve the stored theme or use the default + setTheme(theme); // Apply the theme + + connectSocket(); // Establish WebSocket connection +}); +
+ +

Upon page load, the script sets the theme (either from local storage or the default) and then calls connectSocket function to initiate the WebSocket connection.

+

Establishing WebSocket connection

+

The connectSocket function constructs the WebSocket connection URL using the SOCKET_PORT variable and then initializes the connection.

+ +
function connectSocket() { + const url = "ws://localhost:" + SOCKET_PORT + "/web"; + socket = io(url); + console.log("socket: ", socket); + + // Register socket event handlers + socket.on("blender_connect", handleBlenderConnect); + socket.on("blender_disconnect", handleBlenderDisconnect); + socket.on("theme_data", handleThemeData); + socket.on("demo_data", handleDemoData); +} +
+ +

The socket variable is used to manage the WebSocket connection within the web namespace. After establishing the connection, the script registers event handlers for specific events emitted by the server.

+

Receiving Data from Bonsai

+

Event handlers process the data received from the server and manipulate the DOM to reflect the changes on the webpage. For instance, the handleDemoData function processes the demo_data event and updates the demo content on the page.

+ +
function handleDemoData(demoData) { + console.log(demoData); + const message = demoData["data"]["demo_message"]; + const blenderId = demoData["blenderId"]; + const id = "message-" + blenderId; + const messageHeader = $("#" + id); + const messageText = `Bonsai instance with ID: ${blenderId} sent the message: ${message}`; + + if (messageHeader[0] === undefined) { + const newMessageElement = $("<h3>", { id: id }).text(messageText); + newMessageElement.prependTo("#message-container"); + } else { + messageHeader.text(messageText); + messageHeader.prependTo("#message-container"); + } +} +
+ +

we use the BlenderId, which is a unique identifier given for by the server for each client connedted to it, to differentiate between different Bonsai instances.

+

This functionality is repeated for other event handlers, such as handleBlenderConnect, handleBlenderDisconnect, and handleThemeData, where specific actions are taken based on the event type and the data provided by the server.

+

Sending Data to Bonsai

+

Data is sent to the server in response to DOM events, such as onclick. These events trigger functions that use the socket variable to emit an event to the server, passing the appropriate data for the specific web operator.

+ +
function SendMessage() { + const inputMessage = $("#input-message").val(); + const msg = { + sourcePage: "demo", + // blenderId: BlenderId, + operator: { + type: "message", + message: inputMessage, + }, + }; + socket.emit("web_operator", msg); +} +
+ +

In this example, the blenderId field specifies which Bonsai instance should receive the operator. If blenderId is not set, the operator is broadcast to all instances.

+

The server forwards this operator to the appropriate Bonsai instance, where it is processed by functions like sio_listen_web_operator and check_operator_queue in tool/web.py.

+
+ +
+ + + diff --git a/src/bonsai/bonsai/bim/module/demo/operator.py b/src/bonsai/bonsai/bim/module/demo/operator.py index 7d8c98de3f..d5b34716f1 100644 --- a/src/bonsai/bonsai/bim/module/demo/operator.py +++ b/src/bonsai/bonsai/bim/module/demo/operator.py @@ -90,3 +90,4 @@ class SendWebUiDemoMessage(bpy.types.Operator): if not context.scene.WebProperties.is_connected: bpy.ops.bim.connect_websocket_server(page="demo") core.send_webui_demo_message(tool.Web, message=bpy.context.scene.BIMDemoProperties.webui_message) + return {"FINISHED"}