Application context and focus
Application tags tell Rotatrix what the application is doing, so the user’s profile can select suitable input behavior. A change of workspace, tool or edit operation can change those tags. This applies to Navigation and Axis Streaming.
The demos add object-interaction tags during
an edit and remove them when it ends. Their integrations publish application
state through OpenAxisConnectionManager.
Tags and metadata
Section titled “Tags and metadata”Tags travel in a metadata snapshot alongside capabilities and focus. Tags describe available interactions; capabilities identify supported protocol features; focus helps Rotatrix select the active recipient. Each update replaces the complete tag set, so persistent tags remain in every snapshot.
The demos use these tag sets:
| Application state | Complete tag set |
|---|---|
| Camera navigation, including a selected object | demo-3d-services |
| Active object edit | demo-3d-services, interaction.object.rotate, interaction.object.translate |
| Edit accepted or cancelled | demo-3d-services |
Press F in any viewer to add navigation.hint.free_camera to the current
tag set, or remove it to return to orbit. The demo-3d-services tag and any edit
tags remain present; both camera modes share the same object bindings. See
Free camera movement.
The interaction tags authorize rotation and translation; the user’s profile chooses between them. Selection alone does not start an edit. See Object manipulation for native operation ownership.
The metadata provider supplies the latest snapshot on connection and refresh. Application state is read on the thread where the application’s API is valid.
The manager calls MyOpenAxisIntegration._metadata() on startup/reconnect and
refresh. It uses self.app to inspect the active edit and focus; _tags() builds
the complete tag list for that snapshot:
def _tags(self): camera_tags = ['navigation.hint.free_camera'] if self.app.free_camera else [] return ['demo-3d-services'] + camera_tags + (['interaction.object.rotate', 'interaction.object.translate'] if self.app.operation else [])
def _metadata(self): # This viewer runs its GUI and networking on the same asyncio thread. return ConnectionMetadata(tags=tuple(self._tags()), capabilities=('navigation',), focused=self.app.has_focus())View full implementation: MyOpenAxisIntegration._tags →
The Python demo’s GUI and networking share one asyncio thread, so the provider reads application state directly.
Application events call MyOpenAxisIntegration.PublishMetadata(). It reads
app on the UI thread and replaces the snapshot that the manager will read:
void PublishMetadata(){ var tags = new List<string> { "demo-3d-services" }; if (app.FreeCamera) tags.Add("navigation.hint.free_camera"); if (app.Operation != null) tags.AddRange(new[] { "interaction.object.rotate", "interaction.object.translate" }); Volatile.Write(ref metadata, new ConnectionMetadata { Tags = tags.ToArray(), Capabilities = new[] { "navigation" }, Focused = app.IsActive, }); if (stopping || connection.State != ConnectionManagerState.Ready) return; var update = connection.RefreshMetadataAsync(); updates.Add(update); _ = ObserveUpdate(update);}View full implementation: MyOpenAxisIntegration.PublishMetadata →
The C# manager reads metadata on a networking thread. The integration publishes
an immutable snapshot with Volatile.Write; its provider uses Volatile.Read
so networking never needs to access scene objects.
metadata() reads current edit and focus state on the browser event loop. The manager calls it on connection and refresh. refresh observes failures and tracks pending updates.
metadata(): ConnectionMetadata { return { capabilities: ["navigation"], tags: [ "demo-3d-services", ...(this.app.freeCamera ? ["navigation.hint.free_camera"] : []), ...(this.app.edit ? ["interaction.object.rotate", "interaction.object.translate"] : []), ], focused: !this.hidden && !document.hidden && document.hasFocus(), };}
refresh = (): void => { if (this.stopped || this.connection.state !== "ready") return; const update = this.connection.refreshMetadata().catch((error) => { if (!this.stopped) console.debug("Metadata refresh interrupted", error); }); this.updates.add(update); void update.then(() => this.updates.delete(update));};MyOpenAxisIntegration::update updates its metadata snapshot after native events and calls
connection.refresh_metadata() when tags or focus change. Edits add both object-interaction tags; free-camera preference adds its own tag. GLFW waits for events or the next scheduled deadline when idle.
void update(bool focused) { collector.set_enabled(app.diagnostics); collector.set_context(context_key());
std::vector<std::string> tags = {"demo-3d-services"}; if (app.free_camera) tags.push_back("navigation.hint.free_camera"); if (app.editing >= 0) { tags.push_back("interaction.object.translate"); tags.push_back("interaction.object.rotate"); } if (metadata.focused != focused || metadata.tags != tags) { metadata.focused = focused; metadata.tags = std::move(tags); connection.refresh_metadata(); } scheduler.drain();}Connect the application events
Section titled “Connect the application events”An operation-change event first invalidates navigation bound to the previous edit, then requests a metadata refresh. Focus changes use the same refresh path without changing the operation identity.
During MyOpenAxisIntegration.start(), the existing client and session are
retained and update-task tracking is initialized as described in the
connection recipe. These hooks are registered
before networking starts:
app.on_navigation_changed = self._metadata_changedapp.on_focus_changed = self._metadata_changedapp.on_operation_changed = self._operation_changedView full implementation: MyOpenAxisIntegration.start →
An edit transition calls _operation_changed; a focus transition calls
_metadata_changed. Both are bound methods on that integration instance.
_metadata_sent is a task completion callback that observes any send error:
def _operation_changed(self): self.session.context_changed() self._metadata_changed()
def _metadata_changed(self): if self.connection.state != ConnectionManagerState.READY: return # Startup/reconnect reads the latest application state itself. task = asyncio.create_task(self.connection.refresh_metadata()) self._metadata_tasks.add(task) task.add_done_callback(self._metadata_sent)
def _metadata_sent(self, task): self._metadata_tasks.discard(task) if not task.cancelled() and task.exception() is not None: # The connection may close during a send; reconnect replays current facts. logging.getLogger('openaxis.viewer').debug('Metadata refresh interrupted: %s', task.exception())View full implementation: MyOpenAxisIntegration._operation_changed →
The MyOpenAxisIntegration constructor registers equivalent hooks. The manager’s
provider reads the snapshot, while native events call the integration’s methods:
app.ContextChanged = () => { session.ContextChanged(); PublishMetadata(); };app.NavigationChanged = PublishMetadata;app.FocusChanged = PublishMetadata;View full implementation: MyOpenAxisIntegration.MyOpenAxisIntegration →
PublishMetadata schedules a refresh only while ready. Every update task is tracked and its failure observed:
async Task ObserveUpdate(Task update){ try { await update; } catch (Exception error) { Console.WriteLine($"Metadata refresh interrupted: {error.Message}"); } finally { updates.Remove(update); }}View full implementation: MyOpenAxisIntegration.ObserveUpdate →
start() registers native notifications before starting the connection supervisor. The host implements on and returns an unsubscribe callback. Operation changes invalidate the previous binding before refreshing metadata.
this.detach.push( this.app.on("navigation", this.refresh), this.app.on("operation", () => { this.session.contextChanged(); this.diagnostics.clear(); this.refresh(); }),);The GLFW loop calls MyOpenAxisIntegration::update after native input. Edit transitions increment MyApplication::generation and notify session.context_changed(); captured contexts prevent old output from targeting the replacement edit. Camera and object input send their own native-change notifications. The scheduler executes deferred drains and deadlines.
Application events request refreshes. While offline, the latest application state
is retained for the next connection. While connected, OpenAxisConnectionManager
serializes announcements. The integration observes refresh failures so a
disconnect during an update is reported; reconnect publishes current state.
Report focus through window events
Section titled “Report focus through window events”Focus identifies whether the application is receiving user interaction. It is a separate metadata field, updated when window focus changes. Unrelated events, such as a resize, do not require a metadata refresh.
The application emits on_focus_changed when its window gains or loses focus.
The integration routes that event to _metadata_changed(); the provider reads
app.has_focus() when producing the snapshot.
The application emits FocusChanged when its window gains or loses focus.
PublishMetadata() reads app.IsActive on the UI thread and publishes a new
snapshot for the connection manager.
start() listens to window focus and blur; both invoke refresh. Its metadata provider reads document.hasFocus() at send time, including initial connection and reconnect. The returned cleanup removes both listeners. For an embedded canvas, adapt focus policy to your application.
The demo reads GLFW window focus after native events and stores it in the manager’s metadata snapshot. The scheduler continues handling transport and deadlines while unfocused. Adapt the focus predicate for embedded viewports or modal tools.
Reconnect and shutdown
Section titled “Reconnect and shutdown”OpenAxisConnectionManager reads a fresh snapshot after each successful connection.
If the user changes tools or ends an edit while offline, reconnect announces that
current state without replaying obsolete transitions.
In C++, setters update desired metadata and the scheduler delivers connection changes.
The other SDKs’ refresh APIs are refresh_metadata() in Python, RefreshMetadataAsync() in C#, and
refreshMetadata() in TypeScript. They publish the complete snapshot when ready
and do nothing offline. Refresh failures return to the caller.
The connection and shutdown recipe shows how event callbacks and pending updates are cleaned up before the application closes.
Verify
Section titled “Verify”- Start an edit and confirm both interaction tags appear; accept or cancel and confirm they disappear while the profile tag remains.
- Switch focus and confirm only actual focus changes send a focus update.
- Leave the application idle and confirm no metadata updates are scheduled.
- Disconnect, change state, and reconnect. The first snapshot must describe new state.
- Shut down with an update pending. It must finish or be cancelled before native resources disappear, and detached events must schedule no work.
For automated commands and coverage, use the demo READMEs: Python, C#, TypeScript, and C++.