diff --git a/docs/support-matrix.md b/docs/support-matrix.md
index a8c1704..7e20b82 100644
--- a/docs/support-matrix.md
+++ b/docs/support-matrix.md
@@ -21,11 +21,11 @@ as with Python Breps.
## Commands
-| Status | Commands |
-| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
-| Included | Basic materials and lights, scene and theme settings, UI controls, text tags, metadata, object actions, removal, and visibility |
-| Included callbacks | Object picking, UI actions, object actions, loaded JSON, and custom host actions |
-| Deferred or limited | Spinner, text geometry fonts, advanced physical-material options, Sky, and RectAreaLight |
+| Status | Commands |
+| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| Included | Basic materials and lights, scene and theme settings, UI controls, text tags, metadata, object actions, removal, visibility, and spinner |
+| Included callbacks | Object picking, UI actions, object actions, loaded JSON, and custom host actions |
+| Deferred or limited | Text geometry fonts, advanced physical-material options, Sky, and RectAreaLight |
Python-to-browser commands use binary protobuf envelopes. Browser-to-Python
callbacks use JSON text.
diff --git a/src/App.vue b/src/App.vue
index 443fb1e..f070c42 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -5,6 +5,8 @@
+
+
@@ -14,6 +16,7 @@ import { onMounted, ref } from "vue";
import RightSidebar from "./components/layout/RightSidebar.vue";
import Sidebar from "@/components/layout/Sidebar.vue";
import ThemeIndicator from "@/components/layout/ThemeIndicator.vue";
+import GlobalSpinner from "@/components/layout/GlobalSpinner.vue";
import type { ViewerRuntime } from "@/viewer/viewer_runtime";
const threeContainer = ref(null);
diff --git a/src/components/layout/GlobalSpinner.vue b/src/components/layout/GlobalSpinner.vue
new file mode 100644
index 0000000..80ccff1
--- /dev/null
+++ b/src/components/layout/GlobalSpinner.vue
@@ -0,0 +1,299 @@
+
+
+
+
+
+ {{ spinnerState.message }}
+
+
+ {{ funnyMessage }}
+
+
+
+
+
+
+
+
diff --git a/src/viewer/viewer_commands.ts b/src/viewer/viewer_commands.ts
index 6903169..d8cabff 100644
--- a/src/viewer/viewer_commands.ts
+++ b/src/viewer/viewer_commands.ts
@@ -365,6 +365,12 @@ export type HandleGeometryCommand =
| SetGeometryVisibilityCommand
| ToggleGeometryVisibilityCommand;
+export interface SpinnerCommand extends CommandRecord {
+ dispatch: "spinner";
+ visible: boolean;
+ message?: string | null;
+}
+
export type ViewerCommand =
| MaterialCommand
| LightCommand
@@ -375,7 +381,8 @@ export type ViewerCommand =
| TextTagCommand
| ObjectInfosCommand
| ObjectActionCommand
- | HandleGeometryCommand;
+ | HandleGeometryCommand
+ | SpinnerCommand;
const SCENE_TYPES = new Set([
"background_color",
@@ -462,6 +469,9 @@ export function parseViewerCommand(record: CommandRecord): ViewerCommand {
case "handle_geometry":
validateHandleGeometry(record);
return record as HandleGeometryCommand;
+ case "spinner":
+ validateSpinner(record);
+ return record as SpinnerCommand;
default:
throw new CompasViewerError(
"unsupported_message",
@@ -750,6 +760,13 @@ function validateHandleGeometry(record: CommandRecord): void {
if (type === "set_visibility") readBoolean(record, "visible");
}
+function validateSpinner(record: CommandRecord): void {
+ readBoolean(record, "visible");
+ if (record.message !== undefined && record.message !== null) {
+ readOptionalString(record, "message");
+ }
+}
+
function readNumberFields(record: CommandRecord, fields: string[]): void {
for (const field of fields) readFiniteNumber(record, field);
}
diff --git a/src/viewer/viewer_runtime.ts b/src/viewer/viewer_runtime.ts
index c7bc5ef..85c7a5c 100644
--- a/src/viewer/viewer_runtime.ts
+++ b/src/viewer/viewer_runtime.ts
@@ -20,6 +20,7 @@ import {
type ObjectActionCommand,
type ObjectInfosCommand,
type SceneCommand,
+ type SpinnerCommand,
type TextCommand,
type TextTagCommand,
type UiCommand,
@@ -487,6 +488,9 @@ export class ViewerRuntime {
case "handle_geometry":
this.handleGeometry(data);
break;
+ case "spinner":
+ this.manageSpinner(data);
+ break;
}
}
@@ -679,6 +683,13 @@ export class ViewerRuntime {
});
}
+ private manageSpinner(data: SpinnerCommand): void {
+ this.store.spinnerState.visible = data.visible;
+ this.store.spinnerState.message = data.visible
+ ? (data.message ?? null)
+ : null;
+ }
+
private pickFromPointer(event: MouseEvent): void {
this.renderer.domElement.focus({ preventScroll: true });
if (
diff --git a/src/viewer/viewer_store.ts b/src/viewer/viewer_store.ts
index bd1384d..1c5f61e 100644
--- a/src/viewer/viewer_store.ts
+++ b/src/viewer/viewer_store.ts
@@ -85,6 +85,7 @@ export interface ViewerStore {
blockPicker: { value: boolean };
showEdges: { value: boolean };
theme: { value: "light" | "dark" };
+ spinnerState: { visible: boolean; message: string | null };
selectedObjectGuid: { value: string | null };
}
@@ -107,6 +108,11 @@ export function createViewerStore(): ViewerStore {
blockPicker: reactive({ value: false }),
showEdges: reactive({ value: false }),
theme: reactive({ value: "light" as const }),
+ spinnerState: reactive({
+ visible: false,
+ message: null as string | null,
+ }),
+
selectedObjectGuid: reactive({ value: null as string | null }),
};
}
diff --git a/tests/viewer_commands.test.ts b/tests/viewer_commands.test.ts
index f938c35..7d40bd0 100644
--- a/tests/viewer_commands.test.ts
+++ b/tests/viewer_commands.test.ts
@@ -41,7 +41,7 @@ describe("viewer command validation", () => {
it("distinguishes unsupported dispatch and variant values", () => {
for (const input of [
- { dispatch: "spinner", type: "show" },
+ { dispatch: "not_a_real_dispatch", type: "show" },
{ dispatch: "scene", type: "unknown_scene_action" },
]) {
expect(() => parseViewerCommand(input)).toThrowError(