diff --git a/packages/phoenix-event-display/src/managers/state-manager.ts b/packages/phoenix-event-display/src/managers/state-manager.ts index cc037e083..7a086cae9 100644 --- a/packages/phoenix-event-display/src/managers/state-manager.ts +++ b/packages/phoenix-event-display/src/managers/state-manager.ts @@ -77,13 +77,20 @@ export class StateManager { } /** - * Save the state of the event display as JSON. + * Get the current state of the event display as a JSON object. + * @returns The state object with menu, camera, and clipping data. */ - saveStateAsJSON() { - const state: { [key: string]: any } = { - phoenixMenu: this.phoenixMenuRoot.getNodeState(), + getStateAsJSON(): { [key: string]: any } { + const controls = this.eventDisplay + ?.getThreeManager() + ?.getControlsManager() + ?.getMainControls(); + + return { + phoenixMenu: this.phoenixMenuRoot?.getNodeState(), eventDisplay: { - cameraPosition: this.activeCamera.position.toArray(), + cameraPosition: this.activeCamera?.position.toArray(), + cameraTarget: controls?.target?.toArray(), startClippingAngle: this.clippingEnabled.value ? this.startClippingAngle.value : null, @@ -92,6 +99,13 @@ export class StateManager { : null, }, }; + } + + /** + * Save the state of the event display as JSON. + */ + saveStateAsJSON() { + const state = this.getStateAsJSON(); saveFile( JSON.stringify(state), @@ -115,9 +129,22 @@ export class StateManager { if (jsonData['eventDisplay']) { console.log('StateManager: Processing eventDisplay configuration'); - this.activeCamera.position.fromArray( - jsonData['eventDisplay']?.['cameraPosition'], - ); + if (jsonData['eventDisplay']?.['cameraPosition']) { + this.activeCamera.position.fromArray( + jsonData['eventDisplay']['cameraPosition'], + ); + } + + if (jsonData['eventDisplay']?.['cameraTarget']) { + const controls = this.eventDisplay + ?.getThreeManager() + ?.getControlsManager() + ?.getMainControls(); + if (controls) { + controls.target.fromArray(jsonData['eventDisplay']['cameraTarget']); + controls.update(); + } + } const startAngle = jsonData['eventDisplay']?.['startClippingAngle']; const openingAngle = jsonData['eventDisplay']?.['openingClippingAngle']; diff --git a/packages/phoenix-event-display/src/managers/three-manager/index.ts b/packages/phoenix-event-display/src/managers/three-manager/index.ts index f587a872a..49bc6c4d1 100644 --- a/packages/phoenix-event-display/src/managers/three-manager/index.ts +++ b/packages/phoenix-event-display/src/managers/three-manager/index.ts @@ -319,6 +319,14 @@ export class ThreeManager { return this.sceneManager; } + /** + * Get the controls manager for accessing camera controls. + * @returns The controls manager. + */ + public getControlsManager(): ControlsManager { + return this.controlsManager; + } + /** * Sets controls to auto rotate. * @param autoRotate If the controls are to be automatically rotated or not. diff --git a/packages/phoenix-event-display/src/managers/url-options-manager.ts b/packages/phoenix-event-display/src/managers/url-options-manager.ts index 74cb7e472..e3c6995da 100644 --- a/packages/phoenix-event-display/src/managers/url-options-manager.ts +++ b/packages/phoenix-event-display/src/managers/url-options-manager.ts @@ -12,6 +12,7 @@ export const phoenixURLOptions = { file: '', type: '', config: '', + state: '', hideWidgets: false, embed: false, }; @@ -108,7 +109,10 @@ export class URLOptionsManager { }) .finally(() => { this.eventDisplay.getLoadingManager().itemLoaded('url_config'); + this.applyViewStateOption(); }); + } else { + this.applyViewStateOption(); } }; @@ -224,6 +228,42 @@ export class URLOptionsManager { this.eventDisplay.parsePhoenixEvents(allEventsObject); } + /** + * Apply view state from the URL's "state" parameter. + * Decodes a Base64-encoded JSON state and restores camera, clipping, and menu visibility. + * Uses a load listener to ensure state applies after all other initialization completes. + */ + private applyViewStateOption() { + const stateParam = this.urlOptions.get('state'); + if (!stateParam) { + return; + } + + const applyState = async () => { + try { + const binary = atob(stateParam); + const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)); + // Decompress the deflate-compressed state + const stream = new Blob([bytes]) + .stream() + .pipeThrough(new DecompressionStream('deflate')); + const decompressed = await new Response(stream).arrayBuffer(); + const jsonString = new TextDecoder().decode(decompressed); + const jsonState = JSON.parse(jsonString); + console.log('Applying view state from URL'); + const stateManager = new StateManager(); + stateManager.loadStateFromJSON(jsonState); + } catch (error) { + console.error('Could not parse view state from URL.', error); + } + }; + + this.eventDisplay.getLoadingManager().addLoadListenerWithCheck(() => { + // Small delay to ensure experiment component's load listener runs first + setTimeout(applyState, 200); + }); + } + /** * Hide all overlay widgets if "hideWidgets" option from the URL is true. */ diff --git a/packages/phoenix-ng/projects/phoenix-app/src/app/sections/atlas/atlas.component.ts b/packages/phoenix-ng/projects/phoenix-app/src/app/sections/atlas/atlas.component.ts index 60f730750..d6c6152c1 100644 --- a/packages/phoenix-ng/projects/phoenix-app/src/app/sections/atlas/atlas.component.ts +++ b/packages/phoenix-ng/projects/phoenix-app/src/app/sections/atlas/atlas.component.ts @@ -272,12 +272,12 @@ export class AtlasComponent implements OnInit, OnDestroy { console.log('Loading default configuration.'); this.loaded = true; - const urlConfig = this.eventDisplay - .getURLOptionsManager() - .getURLOptions() - .get('config'); + const urlOptionsManager = this.eventDisplay.getURLOptionsManager(); + const urlConfig = urlOptionsManager.getURLOptions().get('config'); + const urlState = urlOptionsManager.getURLOptions().get('state'); - if (!urlConfig) { + // Skip default config if a full view state is provided via URL + if (!urlConfig && !urlState) { const stateManager = new StateManager(); stateManager.loadStateFromJSON(phoenixMenuConfig); } diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/share-link/share-link-dialog/share-link-dialog.component.html b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/share-link/share-link-dialog/share-link-dialog.component.html index 2021fd363..2ef2d7796 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/share-link/share-link-dialog/share-link-dialog.component.html +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/share-link/share-link-dialog/share-link-dialog.component.html @@ -44,6 +44,11 @@