/**
* 3D Foundation Project
* Copyright 2025 Smithsonian Institution
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Subscriber from "@ff/core/Subscriber";
import CFullscreen from "@ff/scene/components/CFullscreen";
import CVAnalytics from "../../components/CVAnalytics";
import CVToolProvider from "../../components/CVToolProvider";
import CVDocument from "../../components/CVDocument";
import CVARManager from "../../components/CVARManager";
import CVScene from "../../components/CVScene";
import CVModel2 from "../../components/CVModel2";
import { EDerivativeQuality } from "../../schema/model";
import DocumentView, { customElement, html } from "./DocumentView";
import ShareMenu from "./ShareMenu";
import CVAnnotationView from "client/components/CVAnnotationView";
import ARCode from "./ARCode";
////////////////////////////////////////////////////////////////////////////////
@customElement("sv-main-menu")
export default class MainMenu extends DocumentView
{
protected documentProps = new Subscriber("value", this.onUpdate, this);
protected shareButtonSelected = false;
protected resizeObserver: ResizeObserver = null;
protected isClipped: boolean = false;
protected get fullscreen() {
return this.system.getMainComponent(CFullscreen);
}
protected get toolProvider() {
return this.system.getMainComponent(CVToolProvider);
}
protected get analytics() {
return this.system.getMainComponent(CVAnalytics);
}
protected get arManager() {
return this.system.getMainComponent(CVARManager);
}
protected get sceneNode() {
return this.system.getComponent(CVScene);
}
protected firstConnected()
{
super.firstConnected();
this.classList.add("sv-main-menu");
}
protected connected()
{
super.connected();
this.fullscreen.outs.fullscreenActive.on("value", this.onUpdate, this);
this.toolProvider.ins.visible.on("value", this.onUpdate, this);
this.activeDocument.setup.language.outs.activeLanguage.on("value", this.onUpdate, this);
this.activeDocument.setup.audio.outs.narrationEnabled.on("value", this.onUpdate, this);
this.activeDocument.setup.tours.ins.closed.on("value", this.setTourFocus, this);
this.activeDocument.setup.reader.ins.closed.on("value", this.setReaderFocus, this);
this.activeDocument.setup.viewer.ins.annotationExit.on("value", this.setAnnotationFocus, this);
this.toolProvider.ins.closed.on("value", this.setToolsFocus, this);
if(!this.resizeObserver) {
this.resizeObserver = new ResizeObserver(() => this.onResize());
}
this.resizeObserver.observe(this);
}
protected disconnected()
{
this.resizeObserver.disconnect();
this.toolProvider.ins.closed.off("value", this.setToolsFocus, this);
this.activeDocument.setup.viewer.ins.annotationExit.off("value", this.setAnnotationFocus, this);
this.activeDocument.setup.reader.ins.closed.off("value", this.setReaderFocus, this);
this.activeDocument.setup.tours.ins.closed.off("value", this.setTourFocus, this);
this.activeDocument.setup.audio.outs.narrationEnabled.off("value", this.onUpdate, this);
this.activeDocument.setup.language.outs.activeLanguage.off("value", this.onUpdate, this);
this.toolProvider.ins.visible.off("value", this.onUpdate, this);
this.fullscreen.outs.fullscreenActive.off("value", this.onUpdate, this);
super.disconnected();
}
protected render()
{
const document = this.activeDocument;
if (!document) {
return html``;
}
const isEditing = !!this.system.getComponent("CVStoryApplication", true);
const setup = document.setup;
const scene = this.sceneNode;
const tourButtonVisible = setup.tours.outs.count.value > 0;
const toursActive = setup.tours.ins.enabled.value;
const modeButtonsDisabled = toursActive && !isEditing;
const readerButtonVisible = setup.reader.articles.length > 0; // && !isEditing;
const readerActive = setup.reader.ins.enabled.value;
const views = scene.getGraphComponents(CVAnnotationView);
const annotationsButtonVisible = views.some(view => {return view.hasAnnotations;}); //true;
const annotationsActive = setup.viewer.ins.annotationsVisible.value;
const fullscreen = this.fullscreen;
const fullscreenButtonVisible = fullscreen.outs.fullscreenAvailable.value;
const fullscreenActive = fullscreen.outs.fullscreenActive.value;
const toolButtonVisible = setup.interface.ins.tools.value;
const toolsActive = this.toolProvider.ins.visible.value;
const narrationButtonVisible = setup.audio.outs.narrationEnabled.value;
const narrationActive = setup.audio.outs.narrationPlaying.value;
const language = setup.language;
// TODO - push to ARManager?
const models = scene.getGraphComponents(CVModel2);
let hasARderivatives = false;
models.forEach(model => {
hasARderivatives = model.derivatives.getByQuality(EDerivativeQuality.AR).length > 0 ? true : hasARderivatives;
});
const arButtonVisible = (this.arManager.outs.available.value || this.arManager.arCodeImage ) && hasARderivatives && models.length >= 1;
return html`
${arButtonVisible ? html`` : null}
${narrationButtonVisible ? html`` : null}
${tourButtonVisible ? html`` : null}
${readerButtonVisible ? html`` : null}
${annotationsButtonVisible ? html`` : null}
${fullscreenButtonVisible ? html`` : null}
${toolButtonVisible ? html`` : null}`;
}
protected onToggleReader()
{
const reader = this.activeDocument.setup.reader;
const readerIns = reader.ins;
readerIns.enabled.setValue(!readerIns.enabled.value);
readerIns.focus.setValue(readerIns.enabled.value);
if(readerIns.enabled.value) {
readerIns.articleId.setValue(reader.articles.length === 1 ? reader.articles[0].article.id : "");
}
this.analytics.sendProperty("Reader_Enabled", readerIns.enabled.value);
}
protected onToggleTours()
{
const tourIns = this.activeDocument.setup.tours.ins;
const readerIns = this.activeDocument.setup.reader.ins;
if (tourIns.enabled.value) {
tourIns.enabled.setValue(false);
}
else {
if (readerIns.enabled.value) {
readerIns.enabled.setValue(false); // disable reader
}
tourIns.enabled.setValue(true); // enable tours
tourIns.tourIndex.setValue(-1); // show tour menu
}
this.analytics.sendProperty("Tours_Enabled", tourIns.enabled.value);
}
protected onToggleAnnotations()
{
const toolIns = this.toolProvider.ins;
const viewerIns = this.activeDocument.setup.viewer.ins;
if (toolIns.visible.value) {
toolIns.visible.setValue(false);
}
viewerIns.annotationsVisible.setValue(!viewerIns.annotationsVisible.value);
viewerIns.annotationFocus.setValue(true);
this.analytics.sendProperty("Annotations_Visible", viewerIns.annotationsVisible.value);
}
protected onToggleShare()
{
if (!this.shareButtonSelected) {
this.shareButtonSelected = true;
this.requestUpdate();
const container = this.closest("sv-chrome-view") as HTMLElement;
ShareMenu.show(container, this.activeDocument.setup.language).then(() => {
this.shareButtonSelected = false;
this.requestUpdate();
this.setElementFocus("share-btn");
});
this.analytics.sendProperty("Menu_Share");
}
}
protected onToggleFullscreen()
{
this.fullscreen.toggle();
this.analytics.sendProperty("Menu_Fullscreen");
}
protected onToggleTools()
{
const toolIns = this.toolProvider.ins;
const viewerIns = this.activeDocument.setup.viewer.ins;
if (viewerIns.annotationsVisible.value) {
viewerIns.annotationsVisible.setValue(false);
}
toolIns.visible.setValue(!toolIns.visible.value);
this.analytics.sendProperty("Tools_Visible", toolIns.visible.value);
}
protected onEnterAR()
{
const ar = this.arManager;
const arIns = ar.ins;
if(ar.outs.available.value) {
arIns.enabled.setValue(true);
}
else {
const container = this.closest("sv-chrome-view") as HTMLElement;
ARCode.show(container, this.activeDocument.setup.language, ar.arCodeImage).then(() => {
//this.shareButtonSelected = false;
//this.requestUpdate();
this.setElementFocus("ar-btn");
});
}
}
protected onToggleNarration()
{
const audio = this.activeDocument.setup.audio;
audio.setupAudio(); // required for Safari compatibility
audio.ins.playNarration.set();
}
// TODO: More elegant way to handle focus
protected setTourFocus()
{
this.setElementFocus("tour-btn");
}
protected setReaderFocus()
{
this.setElementFocus("reader-btn");
}
protected setToolsFocus()
{
this.setElementFocus("tools-btn");
}
protected setAnnotationFocus()
{
this.setElementFocus("anno-btn");
}
protected setElementFocus(elementID: string)
{
const buttons = this.getElementsByTagName("ff-button");
const buttonArray = Array.from(buttons);
const buttonToFocus = buttonArray.find(element => element.id === elementID);
if(buttonToFocus !== undefined) {
(buttonToFocus as HTMLElement).focus();
}
else {
console.warn("Can't focus. Element [" + elementID + "] not found.");
}
}
protected onActiveDocument(previous: CVDocument, next: CVDocument)
{
if (previous) {
this.documentProps.off();
}
if (next) {
const setup = next.setup;
this.documentProps.on(
setup.interface.ins.tools,
setup.reader.ins.enabled,
setup.reader.outs.count,
setup.tours.ins.enabled,
setup.tours.outs.count,
setup.viewer.ins.annotationsVisible,
setup.audio.outs.narrationPlaying,
this.toolProvider.ins.visible
);
}
this.requestUpdate();
}
protected onResize() {
const clipped = this.scrollHeight > this.clientHeight;
if(this.isClipped !== clipped) {
if(clipped) {
this.scrollTo({top: this.scrollTop + this.scrollHeight, behavior: "smooth"});
}
this.isClipped = clipped;
}
}
}