Compare commits

...

7 Commits

Author SHA1 Message Date
Ziad-I 773605330b fix first demo message not showing 2024-08-23 12:18:49 +03:00
Ziad-I a9a45f886b add more details in demo page 2024-08-23 12:09:19 +03:00
Ziad-I 6b6f70694e Bonsai web ui templates 2024-08-19 23:24:58 +03:00
Ziad-I 9a5e37c82b handle operators from demo page 2024-08-19 23:23:37 +03:00
Ziad-I 5625f62bf5 add demo html/css/js 2024-08-19 23:17:26 +03:00
Ziad-I d12f59fa72 add demo endpoint and event listener 2024-08-18 22:48:12 +03:00
Ziad-I 13fcc62406 add new webui demo panel in demo module 2024-08-18 19:27:13 +03:00
13 changed files with 840 additions and 16 deletions
+21 -10
View File
@@ -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):
@@ -74,7 +68,8 @@ class WebNamespace(socketio.AsyncNamespace):
await self.emit("csv_data", {"blenderId": blenderId, "data": messages["csv_data"]}, room=sid)
if "gantt_data" in messages:
await self.emit("gantt_data", {"blenderId": blenderId, "data": messages["gantt_data"]}, room=sid)
if "demo_data" in messages:
await self.emit("demo_data", {"blenderId": blenderId, "data": messages["demo_data"]}, room=sid)
# Blender namespace
class BlenderNamespace(socketio.AsyncNamespace):
@@ -117,11 +112,17 @@ class BlenderNamespace(socketio.AsyncNamespace):
async def on_theme_data(self, sid, data):
global blender_theme
print(f"Blender theme data")
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
await sio.emit("demo_data", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request):
with open("templates/index.html", "r") as f:
template = f.read()
@@ -143,6 +144,15 @@ 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()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def on_startup(app):
pid_file = "running_pid.json"
@@ -169,6 +179,7 @@ sio.register_namespace(BlenderNamespace("/blender"))
app.router.add_get("/", schedules)
app.router.add_get("/documentation", documentation)
app.router.add_get("/sequencing", sequencing)
app.router.add_get("/demo", demo)
# Add static files
app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt")
@@ -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;
}
@@ -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 = $("<h3>", { 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('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
} else if (theme === "blender") {
$("#toggle-theme").html('<i class="fas fa-adjust"></i>');
}
localStorage.setItem("theme", theme);
}
function toggleTheme() {
if ($("html").hasClass("light")) {
setTheme("dark");
} else if ($("html").hasClass("dark")) {
setTheme("blender");
} else {
setTheme("light");
}
}
@@ -0,0 +1,268 @@
<!-- this is a demo web UI page -->
<!DOCTYPE html>
<html lang="en" class="blender">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bonsai Web UI</title>
<!-- here we request the CSS file from the server, -->
<!-- using registered static path in the server -->
<link rel="stylesheet" href="/static/css/demo.css" id="demo-stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
></script>
<script>
// here we define the port that the server is listening on.
// so that we can connect to the websocket server and request static files
// this is set when the HTML file is served by the server when opening the web UI URL.
var SOCKET_PORT = {{port}};
</script>
<!-- here we request the JS file from the server-->
<!-- using registered static path in the server -->
<script defer src="./static/js/demo.js"></script>
</head>
<body>
<!-- the navigation bar at the top of the page. -->
<nav>
<img
src="https://bonsaibim.org/assets/images/blender/blender-logo.png"
alt="Logo"
class="logo"
/>
<ul>
<li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
<li>
<a><i class="fa-solid fa-camera"></i> BCF Topics</a>
</li>
<li>
<a><i class="fa-solid fa-square-check"></i> IDS Audits</a>
</li>
<li>
<a><i class="fa-solid fa-hotel"></i> Facility Management</a>
</li>
</ul>
<button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</nav>
<!-- the main page container. -->
<div id="container">
<div id="message-container">
<!-- Here we have a text input and a button that calls the SendMessage() function-->
<input type="text" id="input-message" placeholder="Send a Message" />
<button onclick="sendMessage()">Send Message to Bonsai</button>
</div>
<!-- The following is the explanation part of the demo page. -->
<!-- I suggest reading it from the demo page iteself :) -->
<!-- we need to stop formating because formatiing messes up pre tag spacing -->
<!-- which in turn messes up code blocks display-->
<!-- prettier-ignore-start -->
<div id="explanation">
<h1 id="welcome-to-the-web-ui-demo-page">Welcome to the web UI demo page!</h1>
<p>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.</p>
<p>You can use the above textbox to send a message to Bonsai which will be printed to the console!</p>
<p>If you prefer, you can explore the source code directly in the data/webui directory:</p>
<ul>
<li>sioserver.py</li>
<li>templates/demo.html</li>
<li>static/js/demo.js</li>
</ul>
<p>Each file contains detailed comments to help you understand the code. Alternatively, continue reading this demo page to understand how the Web UI operates</p>
<h2 id="webscoket-server-sioserverpy">WebScoket Server (sioserver.py)</h2>
<p>In the websocket server, there are two key namespaces.</p>
<ul>
<li>Blender Namespace: Connects all Bonsai instances.</li>
<li>Web Namespace: Connects all Web UI clients.</li>
</ul>
<p>These namespaces are attached to the WebSocket server, allowing event-driven communication.</p>
<pre class="hljs"><code><div><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WebNamespace</span><span class="hljs-params">(socketio.AsyncNamespace)</span>:</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_connect</span><span class="hljs-params">(self, sid, environ)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_disconnect</span><span class="hljs-params">(self, sid)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_web_operator</span><span class="hljs-params">(self, sid, data)</span>:</span>
...
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlenderNamespace</span><span class="hljs-params">(socketio.AsyncNamespace)</span>:</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_connect</span><span class="hljs-params">(self, sid, environ)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_disconnect</span><span class="hljs-params">(self, sid)</span>:</span>
...
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_demo_data</span><span class="hljs-params">(self, sid, data)</span>:</span>
...
</div></code></pre>
<p>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.</p>
<p>The server also defines routes, such as the /demo URL, which are tied to specific request handlers.</p>
<pre class="hljs"><code><div><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">demo</span><span class="hljs-params">(request)</span>:</span>
<span class="hljs-keyword">with</span> open(<span class="hljs-string">"templates/demo.html"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> f:
template = f.read()
html_content = pystache.render(template, {<span class="hljs-string">"port"</span>: sio_port, <span class="hljs-string">"version"</span>: bonsai_version})
<span class="hljs-keyword">return</span> web.Response(text=html_content, content_type=<span class="hljs-string">"text/html"</span>)
app.router.add_get(<span class="hljs-string">"/demo"</span>, demo)
</div></code></pre>
<p>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.</p>
<h2 id="html-file-templatesdemohtml">HTML File (templates/demo.html)</h2>
<p>When the Web UI URL is visited, the server follows these steps:</p>
<ul>
<li>Retrieves the HTML file from the templates directory.</li>
<li>uses pystache to render variables, such as SOCKET_PORT, into the HTML template.</li>
<li>Sends the fully rendered HTML page to the web browser.</li>
</ul>
<p>The SOCKET_PORT variable is injected into the HTML and later used by the JavaScript file to establish a WebSocket connection.</p>
<pre class="hljs"><code><div><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="actionscript">
<span class="hljs-keyword">var</span> SOCKET_PORT = {{port}};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</div></code></pre>
<p>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.</p>
<pre class="hljs"><code><div><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/static/css/demo.css"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"demo-stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">defer</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./static/js/demo.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</div></code></pre>
<h2 id="javascript-file-staticjsdemojs">JavaScript File (static/js/demo.js)</h2>
<p>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.</p>
<pre class="hljs"><code><div><span class="hljs-keyword">let</span> socket;
$(<span class="hljs-built_in">document</span>).ready(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">var</span> defaultTheme = <span class="hljs-string">"blender"</span>; <span class="hljs-comment">// Default theme to be applied</span>
<span class="hljs-keyword">var</span> theme = localStorage.getItem(<span class="hljs-string">"theme"</span>) || defaultTheme; <span class="hljs-comment">// Retrieve the stored theme or use the default</span>
setTheme(theme); <span class="hljs-comment">// Apply the theme</span>
connectSocket(); <span class="hljs-comment">// Establish WebSocket connection</span>
});
</div></code></pre>
<p>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.</p>
<h3 id="establishing-websocket-connection">Establishing WebSocket connection</h3>
<p>The connectSocket function constructs the WebSocket connection URL using the SOCKET_PORT variable and then initializes the connection.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">connectSocket</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> url = <span class="hljs-string">"ws://localhost:"</span> + SOCKET_PORT + <span class="hljs-string">"/web"</span>;
socket = io(url);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"socket: "</span>, socket);
<span class="hljs-comment">// Register socket event handlers</span>
socket.on(<span class="hljs-string">"blender_connect"</span>, handleBlenderConnect);
socket.on(<span class="hljs-string">"blender_disconnect"</span>, handleBlenderDisconnect);
socket.on(<span class="hljs-string">"theme_data"</span>, handleThemeData);
socket.on(<span class="hljs-string">"demo_data"</span>, handleDemoData);
}
</div></code></pre>
<p>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.</p>
<h3 id="receiving-data-from-bonsai">Receiving Data from Bonsai</h3>
<p>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.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleDemoData</span>(<span class="hljs-params">demoData</span>) </span>{
<span class="hljs-built_in">console</span>.log(demoData);
<span class="hljs-keyword">const</span> message = demoData[<span class="hljs-string">"data"</span>][<span class="hljs-string">"demo_message"</span>];
<span class="hljs-keyword">const</span> blenderId = demoData[<span class="hljs-string">"blenderId"</span>];
<span class="hljs-keyword">const</span> id = <span class="hljs-string">"message-"</span> + blenderId;
<span class="hljs-keyword">const</span> messageHeader = $(<span class="hljs-string">"#"</span> + id);
<span class="hljs-keyword">const</span> messageText = <span class="hljs-string">`Bonsai instance with ID: <span class="hljs-subst">${blenderId}</span> sent the message: <span class="hljs-subst">${message}</span>`</span>;
<span class="hljs-keyword">if</span> (messageHeader[<span class="hljs-number">0</span>] === <span class="hljs-literal">undefined</span>) {
<span class="hljs-keyword">const</span> newMessageElement = $(<span class="hljs-string">"&lt;h3&gt;"</span>, { <span class="hljs-attr">id</span>: id }).text(messageText);
newMessageElement.prependTo(<span class="hljs-string">"#message-container"</span>);
} <span class="hljs-keyword">else</span> {
messageHeader.text(messageText);
messageHeader.prependTo(<span class="hljs-string">"#message-container"</span>);
}
}
</div></code></pre>
<p>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.</p>
<p>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.</p>
<h3 id="sending-data-to-bonsai">Sending Data to Bonsai</h3>
<p>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.</p>
<pre class="hljs"><code><div><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SendMessage</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> inputMessage = $(<span class="hljs-string">"#input-message"</span>).val();
<span class="hljs-keyword">const</span> msg = {
<span class="hljs-attr">sourcePage</span>: <span class="hljs-string">"demo"</span>,
<span class="hljs-comment">// blenderId: BlenderId,</span>
<span class="hljs-attr">operator</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"message"</span>,
<span class="hljs-attr">message</span>: inputMessage,
},
};
socket.emit(<span class="hljs-string">"web_operator"</span>, msg);
}
</div></code></pre>
<p>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.</p>
<p>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.</p>
<h2 id="web-tool-toolwebpy">Web Tool (tool/web.py)</h2>
<p>the sio_listen_web_operator function is automatically called when the event web_operator is emitted to Bonsai. It takes the web operator and attempts to put it in a web operators queue.</p>
<pre class="hljs"><code><div><span class="hljs-meta">@classmethod</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sio_listen_web_operator</span><span class="hljs-params">(cls, data)</span>:</span>
<span class="hljs-keyword">try</span>:
web_operator_queue.put_nowait(data)
<span class="hljs-keyword">except</span> queue.Full:
<span class="hljs-keyword">pass</span>
</div></code></pre>
<p>then the check_operator_queue function is called by a timer that is run every second to retrieve operators from the queue and handle them</p>
<pre class="hljs"><code><div><span class="hljs-meta">@classmethod</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">check_operator_queue</span><span class="hljs-params">(cls)</span>:</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> bpy.context.scene.WebProperties.is_connected:
<span class="hljs-keyword">with</span> web_operator_queue.mutex:
web_operator_queue.queue.clear()
<span class="hljs-keyword">return</span> <span class="hljs-literal">None</span> <span class="hljs-comment"># unregister timer if not connected</span>
<span class="hljs-keyword">while</span> <span class="hljs-keyword">not</span> web_operator_queue.empty():
operator = web_operator_queue.get_nowait()
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> operator:
<span class="hljs-keyword">continue</span>
<span class="hljs-keyword">if</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"csv"</span>:
cls.handle_csv_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"gantt"</span>:
cls.handle_gantt_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"drawings"</span>:
cls.handle_drawings_operator(operator[<span class="hljs-string">"operator"</span>])
<span class="hljs-keyword">elif</span> operator[<span class="hljs-string">"sourcePage"</span>] == <span class="hljs-string">"demo"</span>:
message = operator[<span class="hljs-string">"operator"</span>][<span class="hljs-string">"message"</span>]
print(<span class="hljs-string">f"Message from demo page: <span class="hljs-subst">{message}</span>"</span>)
<span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span>
</div></code></pre>
<p>Here we check the source page of the operator and call the appropriate handler for that page.</p>
</div>
<!-- prettier-ignore-end -->
</div>
<footer>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
@@ -149,7 +149,7 @@
</div>
</div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link rel="stylesheet" type="text/css" href="/jsgantt/jsgantt.css" />
<link rel="stylesheet" href="/static/css/gantt.css" id="gantt-stylesheet" />
<link
@@ -73,7 +73,7 @@
</div>
<div id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<title>Bonsai Web UI</title>
<link rel="stylesheet" href="/static/css/index.css" id="index-stylesheet" />
<link
rel="stylesheet"
@@ -78,7 +78,7 @@
</div>
<div id="container"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
<p>Bonsai Version: {{version}}</p>
</footer>
</body>
</html>
@@ -36,8 +36,10 @@ from . import ui, prop, operator
classes = (
operator.DemonstrateHelloWorld,
operator.DemonstrateRenameProject,
operator.SendWebUiDemoMessage,
prop.BIMDemoProperties,
ui.BIM_PT_demo,
ui.BIM_PT_webui_demo,
)
@@ -76,3 +76,18 @@ class DemonstrateRenameProject(bpy.types.Operator, tool.Ifc.Operator):
# properties (such as an input field) or data from the scene (like the
# actively selected object).
core.demonstrate_rename_project(tool.Ifc, tool.Demo, name=bpy.context.scene.BIMDemoProperties.name)
class SendWebUiDemoMessage(bpy.types.Operator):
bl_idname = "bim.send_webui_demo_message"
bl_label = "Send Web UI Demo Message"
bl_description = "Sends a demo message to the currently connected Web UI"
def execute(self, context):
# First, we need to make sure that we are connected to the Web UI
# We do that by checking the Web module properties' is_connected prop
# and calling the operator connect_websocket_server if we aren't connected to a Web UI
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"}
@@ -59,3 +59,4 @@ class BIMDemoProperties(PropertyGroup):
# an input text field in the ui.py.
message: StringProperty(name="Message")
show_hints: BoolProperty(name="Show Hints", default=False)
webui_message: StringProperty(name="Web UI Message", default="Hello, Web UI!")
+24
View File
@@ -130,3 +130,27 @@ class BIM_PT_demo(bpy.types.Panel):
if self.props.show_hints:
row = self.layout.row()
row.label(text="Name cannot be blank!")
class BIM_PT_webui_demo(bpy.types.Panel):
bl_label = "Web UI Demo"
bl_idname = "BIM_PT_webui_demo"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
# we specify that this panel is closed by default
bl_options = {"DEFAULT_CLOSED"}
# here we reference the demo panel to create a sub panel
# for the web UI demo
bl_parent_id = "BIM_PT_demo"
def draw(self, context):
self.props = context.scene.BIMDemoProperties
row = self.layout.row()
# we create an input field for the property webui_message
row.prop(self.props, "webui_message")
row = self.layout.row()
# here we create a button referencing the operator send_webui_message
row.operator("bim.send_webui_demo_message")
+6
View File
@@ -72,3 +72,9 @@ def demonstrate_rename_project(ifc, demo, name=None):
demo.hide_user_hints()
else:
demo.show_user_hints()
# here this core function uses the web tool to send a message to the web UI
# we call the send_webui_data method to send any kind of data to the web UI
def send_webui_demo_message(web, message="Hello"):
web.send_webui_data(data=message, data_key="demo_message", event="demo_data")
+3
View File
@@ -307,6 +307,9 @@ class Web(bonsai.core.tool.Web):
cls.handle_gantt_operator(operator["operator"])
elif operator["sourcePage"] == "drawings":
cls.handle_drawings_operator(operator["operator"])
elif operator["sourcePage"] == "demo":
message = operator["operator"]["message"]
print(f"Message from demo page: {message}")
return 1.0
@classmethod