@vrspace/babylonjs
Version:
vrspace.org babylonjs client
246 lines (228 loc) • 7.63 kB
JavaScript
import { Label } from './label.js';
import { RemoteBrowser } from './remote-browser.js';
import { VRSpaceAPI } from './../../client/rest-api.js'
import { VRSPACEUI } from '../vrspace-ui.js';
import { ServerCapabilities } from '../../client/openapi/model/ServerCapabilities.js';
class Attachment {
constructor(fileName, callback) {
this.fileName = fileName;
this.label = null;
this.buttons = [];
this.callback = callback;
}
dispose() {
this.label.dispose();
this.buttons.forEach(b=>b.dispose());
}
getText() {
return this.fileName;
}
}
class Link {
constructor( text, enterWorld=false ) {
this.url = text;
this.enterWorld = enterWorld;
let pos = text.indexOf("://");
if ( pos > 0 ) {
text = text.substring(pos+3);
} else {
this.url = "https://"+this.url;
}
pos = text.indexOf("/");
if ( pos > 0 ) {
this.site = text.substring(0,pos);
} else {
this.site = text;
}
console.log('new link: '+this.url+" "+this.site);
this.label = null;
this.buttons = [];
}
openHere(local) {
if ( local ) {
console.log("TODO: Enter the world like AvatarSelection.enterWorld does");
let dest = URL.parse(this.url);
window.location.href = window.location.pathname + dest.search;
} else {
window.location.href = this.url;
}
}
openTab() {
window.open(this.url, '_blank').focus();
}
dispose() {
this.label.dispose();
this.buttons.forEach(b=>b.dispose());
}
getText() {
return this.site;
}
}
export class ButtonStack {
/** @type {ServerCapabilities} */
static serverCapablities = null;
constructor(scene, parent, position, scaling = new BABYLON.Vector3(.02,.02,.02)) {
this.scene = scene;
this.parent = parent;
this.scaling = scaling;
this.position = position;
this.capacity = 5;
this.links = [];
this.meshes = []; // XR selectables
this.clickHandler = this.scene.onPointerObservable.add((pointerInfo) => {
if ( pointerInfo.type == BABYLON.PointerEventTypes.POINTERUP
&& pointerInfo.pickInfo.hit
) {
for ( let i = 0; i < this.links.length; i++ ) {
if ( this.links[i].label.textPlane == pointerInfo.pickInfo.pickedMesh ) {
this.clicked(this.links[i]);
break;
}
}
}
});
if ( ButtonStack.serverCapablities == null ) {
VRSpaceAPI.getInstance().endpoint.server.getServerCapabilities().then(c=>ButtonStack.serverCapablities=c);
}
}
/**
* Add a link button to the stack, called when ChatLog finds a link in the chat.
* Also creates buttons to enter world, open link in new tab, internal browser if available.
* @param {string} word A single word to display on the button, e.g. web site name
* @param {boolean} enterWorld Whether the link points to another world
*/
addLink(word, enterWorld){
let link = new Link(word, enterWorld);
this.scroll();
// add buttons to open in new tab, this tab, optionally internal browser
if ( enterWorld ) {
this.addButton(link, "enter", () => link.openHere(enterWorld));
} else {
this.addButton(link, "external-link", () => link.openTab());
if (ButtonStack.serverCapablities && ButtonStack.serverCapablities.remoteBrowser) {
this.addButton(link, "play", () => this.openBrowser(link.url));
}
}
return this.addLabel(link);
}
/**
* Add a label with the link/attachment text
* @private
*/
addLabel(link) {
let x = this.scaling.x*link.buttons.length+this.position.x+link.getText().length/(Label.fontRatio*2)*this.scaling.x;
let pos = new BABYLON.Vector3(x,this.position.y,this.position.z);
let label = new Label(link.getText(),pos,this.parent);
//label.background = "black";
label.display();
label.textPlane.scaling = this.scaling;
link.label = label;
this.links.push(link);
this.meshes.push(label.textPlane);
return link;
}
/**
* @param {Link} link world/web link
* @param {string} name Icon under /content/icons to display
* @param {*} callback function
* @private
*/
addButton(link, name, callback) {
let button = BABYLON.GUI.Button.CreateImageOnlyButton(name+"-"+link.getText(), VRSPACEUI.contentBase+"/content/icons/"+name+".png");
let buttonPlane = BABYLON.MeshBuilder.CreatePlane(name+"-"+link.getText(), {height:1,width:1});
buttonPlane.parent = this.parent;
buttonPlane.position = new BABYLON.Vector3(this.position.x + this.scaling.x*link.buttons.length,this.position.y,this.position.z);
buttonPlane.scaling = this.scaling;
let buttonTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(
buttonPlane,
128,
128,
false // mouse events disabled
);
buttonTexture.addControl(button);
button.onPointerUpObservable.add( () => callback());
link.buttons.push(buttonPlane);
this.meshes.push(buttonPlane);
}
addAttachment(fileName, callback) {
let attachment = new Attachment(fileName, callback);
this.scroll();
this.addButton(attachment, "attachment", () => callback());
return this.addLabel(attachment);
}
/**
* Called on click on a link, opens the link either in a new tab, or internal browser if available.
* This only handles click on the link itself, click on a button behavior is defined in addLink().
* @private
* @param {Link|Attachment} link
*/
async clicked(link) {
// process invitations
console.log("Clicked "+link.getText());
if ( link.fileName ) {
if ( link.callback ) {
link.callback();
}
} else if ( link.enterWorld ) {
// CHECKME
link.openHere(true);
} else if (ButtonStack.serverCapablities.remoteBrowser) {
this.openBrowser(link.url);
} else {
link.openTab();
}
}
/**
* Opens the URL in internal browser
* @private
* @param {string} url link to open
*/
openBrowser(url) {
if ( this.browser ) {
this.browser.dispose();
}
this.browser = new RemoteBrowser(this.scene);
this.browser.show();
//this.browser.attachToCamera();
this.browser.attachToHud();
if ( url.toLowerCase().endsWith(".jpg") || url.toLowerCase().endsWith(".png") ) {
this.browser.loadUrl(url);
} else {
this.browser.get(url);
}
}
/**
* Scroll all buttons up to make space for a new one, keeps only this.capacity buttons.
* @private
*/
scroll() {
if ( this.links.length == this.capacity ) {
this.links[0].dispose();
let link = this.links.splice(0,1)[0];
link.dispose();
}
for ( let i = 0; i < this.links.length; i++ ) {
let label = this.links[i].label;
let y = label.textPlane.position.y + label.textPlane.scaling.y*1.5;
label.textPlane.position = new BABYLON.Vector3( label.textPlane.position.x, y, label.textPlane.position.z );
this.links[i].buttons.forEach(b=>b.position.y = y );
}
}
/**
* Clean up all resources
*/
dispose() {
this.scene.onPointerObservable.remove(this.clickHandler);
this.links.forEach(l=>l.dispose());
this.meshes = [];
if ( this.browser ) {
this.browser.dispose();
}
}
/**
* XR stuff
*/
isSelectableMesh(mesh) {
return this.meshes.includes(mesh);
}
}