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 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) 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)