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 @@ + + + + + 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(