From fd3914b00c9d6a4b26fc9674d081ef50e0e3a1b6 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 21 Sep 2026 14:50:03 +0200 Subject: [PATCH 1/3] Add configurable browser tab title to App and Workspace.open_in_browser App now accepts a title parameter (default "COMPAS ThreeJS"), passed to the frontend as a `title` query param on the viewer URL so each App instance/page can show its own tab title. Workspace.open_in_browser accepts an optional title override, falling back to the App's own title, encoded into the opened URL's `title` query param that the frontend's createViewer() already reads (compas_threejs_ts#36fd78a). Co-Authored-By: Claude Sonnet 5 --- src/compas_threejs/viewer/app.py | 16 +++++++++++++--- src/compas_threejs/viewer/workspace.py | 17 ++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/compas_threejs/viewer/app.py b/src/compas_threejs/viewer/app.py index 55f08c7..325a745 100644 --- a/src/compas_threejs/viewer/app.py +++ b/src/compas_threejs/viewer/app.py @@ -4,6 +4,7 @@ import webbrowser from typing import Optional from typing import Union +from urllib.parse import quote from compas.geometry import Point from rich.console import Console @@ -81,7 +82,13 @@ class App: """ - def __init__(self, host: str = "127.0.0.1", websocket_port: int = 9001, frontend_dir=None): + def __init__( + self, + host: str = "127.0.0.1", + websocket_port: int = 9001, + frontend_dir=None, + title: str = "COMPAS ThreeJS", + ): """ Parameters ---------- @@ -91,11 +98,14 @@ def __init__(self, host: str = "127.0.0.1", websocket_port: int = 9001, frontend frontend to its own copy - e.g. one it rebuilds itself from a branch or fork of the frontend source - independent of whatever happens to be bundled in this installed package. + title : str, optional + Browser tab title for the viewer page. Default is "COMPAS ThreeJS". """ # Server self.host = host self.websocket_port = websocket_port self.websocket_server_thread = None + self.title = title self.server = AppServer(frontend_dir=frontend_dir) self.outbox = Outbox(self.server) @@ -253,7 +263,7 @@ def toolbar(self): def url(self) -> str: """Get the URL to access the viewer in a web browser.""" display_host = self.host if self.host != "0.0.0.0" else get_local_ip() - return f"http://{display_host}:{self.websocket_port}/?ws_host={display_host}&ws_port={self.websocket_port}" + return f"http://{display_host}:{self.websocket_port}/?ws_host={display_host}&ws_port={self.websocket_port}&title={quote(self.title)}" def set_view(self, view: Union[CameraView, Point, tuple, list], target=None): """Set the main workspace's camera view from a preset or explicit point. @@ -348,7 +358,7 @@ def show(self): else: connect_host = self.host - shareable_url = f"http://{connect_host}:{self.websocket_port}/?ws_host={connect_host}&ws_port={self.websocket_port}" + shareable_url = f"http://{connect_host}:{self.websocket_port}/?ws_host={connect_host}&ws_port={self.websocket_port}&title={quote(self.title)}" console.log(f"[underline yellow]{shareable_url}[/underline yellow]") webbrowser.open(shareable_url) diff --git a/src/compas_threejs/viewer/workspace.py b/src/compas_threejs/viewer/workspace.py index e979dc6..9f983cc 100644 --- a/src/compas_threejs/viewer/workspace.py +++ b/src/compas_threejs/viewer/workspace.py @@ -2,6 +2,7 @@ from enum import IntEnum from typing import Optional from typing import Union +from urllib.parse import quote import compas_pb from compas.colors import Color @@ -607,9 +608,19 @@ def stop_spinner(self): workspace_id=self.workspace_id, ) - def open_in_browser(self): - """Launches a browser window tracking only this workspace.""" + def open_in_browser(self, title: Optional[str] = None): + """Launches a browser window tracking only this workspace. + + Parameters + ---------- + title : str, optional + Browser tab title for this workspace's page. Defaults to the App's own `title`. + """ connect_host = self.app.host if self.app.host != "0.0.0.0" else "127.0.0.1" - url = f"http://{connect_host}:{self.app.websocket_port}/?ws_host={connect_host}&ws_port={self.app.websocket_port}&workspace={self.workspace_id}" + url = ( + f"http://{connect_host}:{self.app.websocket_port}/" + f"?ws_host={connect_host}&ws_port={self.app.websocket_port}" + f"&workspace={self.workspace_id}&title={quote(title or self.app.title)}" + ) webbrowser.open(url) From 71d7059807dfa990399cedc179aa3a4728eb8f7b Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 21 Sep 2026 14:54:11 +0200 Subject: [PATCH 2/3] docs(examples): add a configurable tab title example Shows App(title=...) for the main workspace and Workspace.open_in_browser(title=...) for a per-workspace override, mirroring compas_threejs_ts's examples/embedded_tab_title.html. Co-Authored-By: Claude Sonnet 5 --- examples/tab_title.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/tab_title.py diff --git a/examples/tab_title.py b/examples/tab_title.py new file mode 100644 index 0000000..bad6371 --- /dev/null +++ b/examples/tab_title.py @@ -0,0 +1,36 @@ +from compas.colors import Color +from compas.geometry import Box +from compas.geometry import Point +from compas.geometry import Sphere + +from compas_threejs.materials import Material +from compas_threejs.tag import TextTag +from compas_threejs.ui import Button +from compas_threejs.viewer import App + +# `title` sets the browser tab title (and the toolbar heading, replacing the +# default "COMPAS ThreeJS") for the main workspace's page. +viz = App(title="Bridge Inspector") + +box = Box(1, 1, 1) +viz.add_geometry(box, material=Material(Color.blue())) +viz.add_tag(TextTag(point=Point(0, 0, 0.5), text="The Box", color=Color.blue())) + +# A second workspace opened in its own browser tab. Without a `title` it falls +# back to the App's own title; pass one to give it a distinct tab title instead. +detail_workspace = viz.get_workspace("sphere") +detail_workspace.add_default_lighting() + +sphere = Sphere(1) +detail_workspace.add_geometry(sphere, material=Material(Color.red())) +detail_workspace.add_tag(TextTag(point=Point(0, 0, 1), text="The Sphere", color=Color.red())) + + +def open_detail_view(): + detail_workspace.open_in_browser(title="Bridge Inspector - Sphere Detail") + + +open_btn = Button("Open Sphere Detail View", action=open_detail_view) +viz.add_ui_element(open_btn) + +viz.start(show=True) From 0085cd3b5856257491aed19990a125f5a81c7aa8 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 23 Sep 2026 08:01:24 +0200 Subject: [PATCH 3/3] docs: add changelog entry for the configurable browser tab title Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d486b..87ef1a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `Toolbar`: lets the backend show/hide or enable/disable a frontend-defined toolbar button by id (`app.toolbar.set_visible(id, bool)`, `app.toolbar.set_enabled(id, bool)`). `App.toolbar`/`Workspace.toolbar` expose it, delegated the same way as `background_color`, `world_axis`, etc. +- `App(title=...)`: sets the browser tab title of the viewer page (default `"COMPAS ThreeJS"`), passed to the frontend as a `title` query param. `Workspace.open_in_browser(title=...)` overrides it per workspace, falling back to the App's title. ### Changed