UNPKG

@vrspace/babylonjs

Version:

vrspace.org babylonjs client

457 lines (445 loc) 16.5 kB
import {SpeechInput} from '../../core/speech-input.js'; import {VRSPACEUI} from '../vrspace-ui.js'; /** * Base form helper class contains utility methods for creation of UI elements - text blocks, checkboxes, text input etc. * All elements share the same style defined in constructor. * Every property of the form or element can be overriden with properties of passed params object. * UI elements need to be named; this name is used to create a speech command that activates the element. * E.g. if the element is checkbox named 'rigged', it can be (de)selected by speaking 'rigged on' or 'rigged off'. * HUD delegates gamepad events to appropriate form methods, making form elements usable. */ export class Form { constructor(params) { this.fontSize = 42; this.heightInPixels = 48; this.resizeToFit = true; this.color = "white"; this.background = "black"; this.selected = "yellow"; this.submitColor = "green"; this.cancelColor = "red"; this.verticalPanel = false; this.inputWidth = 500; this.padding = 0; this.paddingLeftInPixels = 0; this.paddingRightInPixels = 0; this.keyboardRows = null; this.virtualKeyboardEnabled = true; this.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; this.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER; this.speechInput = new SpeechInput(); this.speechInput.addNoMatch((phrases)=>this.noMatch(phrases)); this.inputFocusListener = null; this.elements = []; this.controls = []; this.activeControl = null; this.activeBackground = null; this.plane = null; this.texture = null; if ( params ) { for(var c of Object.keys(params)) { this[c] = params[c]; } } VRSPACEUI.addSelectable(this); } /** Returns Form, required by HUD*/ getClassName() { return "Form"; } /** Called by default on speech recognition mismatch */ noMatch(phrases) { console.log('no match:',phrases) } /** * Returns new StackPanel with 1 height and width and aligned to center both vertically and horizontally */ createPanel() { this.panel = new BABYLON.GUI.StackPanel('StackPanel:'+(this.verticalPanel?'vertical':'horizontal')); this.panel.isVertical = this.verticalPanel; this.panel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; this.panel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER; this.panel.width = 1; this.panel.height = 1; return this.panel; } /** Add control to the panel */ addControl(control) { this.panel.addControl(control); } /** * Creates and returns a textblock with given text */ textBlock(text, params) { var block = new BABYLON.GUI.TextBlock('TextBlock:'+text); block.horizontalAlignment = this.horizontalAlignment; block.verticalAlignment = this.verticalAlignment; block.text = text; block.color = this.color; block.fontSize = this.fontSize; block.heightInPixels = this.heightInPixels; block.resizeToFit = this.resizeToFit; block.paddingLeftInPixels = this.paddingLeftInPixels; block.paddingRightInPixels = this.paddingRightInPixels; if ( params ) { for(var c of Object.keys(params)) { block[c] = params[c]; } } this.elements.push(block); return block; } // common code for checkbox and radio button _common(obj, name, params) { obj.heightInPixels = this.heightInPixels; obj.widthInPixels = this.heightInPixels; obj.color = this.color; obj.background = this.background; // makes box not square: //obj.paddingLeftInPixels = this.paddingLeftInPixels; //obj.paddingRightInPixels = this.paddingRightInPixels; if ( params ) { for(var c of Object.keys(params)) { obj[c] = params[c]; } } let command = this.nameToCommand(name); if ( command ) { this.speechInput.addCommand(command, (text) => { if ( text == 'on' || text == 'true') { obj.isChecked = true; } else if ( text == 'off' || text == 'false') { obj.isChecked = false; } else { console.log("Can't set "+name+" to "+text); } }, "*onoff"); } this.elements.push(obj); this.controls.push(obj); return obj; } /** * Creates and returns a named Checkbox */ checkbox(name, params) { var checkbox = new BABYLON.GUI.Checkbox('Checkbox:'+name); return this._common(checkbox, name, params); } /** * Creates and returns a named RadioButton */ radio(name, params) { var radioButton= new BABYLON.GUI.RadioButton('radio:'+name); return this._common(radioButton, name, params); } /** * Creates and returns a named InputText, registers this.inputFocus() as focus/blur listener * @param name identifier used for speech input * @param params optional object to override InputText field values */ inputText(name, params) { let input = new BABYLON.GUI.InputText('InputText:'+name); input.widthInPixels = this.inputWidth; input.heightInPixels = this.heightInPixels; input.fontSizeInPixels = this.fontSize; input.paddingLeftInPixels = this.paddingLeftInPixels; input.paddingRightInPixels = this.paddingRightInPixels; input.color = this.color; input.background = this.background; input.focusedBackground = this.background; input.onFocusObservable.add(i=>this.inputFocused(i,true)); input.onBlurObservable.add(i=>this.inputFocused(i,false)); input.disableMobilePrompt = VRSPACEUI.hud.inXR(); if ( params ) { for(let c of Object.keys(params)) { input[c] = params[c]; } } let command = this.nameToCommand(name); if ( command ) { this.speechInput.addCommand(command, (text) => { this.input.text = text; this.input.onTextChangedObservable.notifyObservers(text); this.input.onBlurObservable.notifyObservers(); }, "*text"); } this.elements.push(input); this.controls.push(input); return input; } /** Common code for submitButton() and textButton() */ setupButton(button, callback) { button.horizontalAlignment = this.horizontalAlignment; button.verticalAlignment = this.verticalAlignment; button.heightInPixels = this.heightInPixels; // CHECKME: hardcoded padding? button.paddingLeft = "10px"; if ( this.padding ) { button.setPaddingInPixels(this.padding); } let command = this.nameToCommand(button.name); if ( callback ) { if ( command ) { this.speechInput.addCommand(command, () => callback(this)); } button.onPointerDownObservable.add( () => callback(this)); } this.elements.push(button); this.controls.push(button); } /** * Ceates and returns a named submit image-only Button. */ submitButton(name, callback, icon=VRSPACEUI.contentBase+"/content/icons/play.png") { let button = BABYLON.GUI.Button.CreateImageOnlyButton(name, icon); button.background = this.submitColor; button.widthInPixels = this.heightInPixels+10; this.setupButton(button, callback); return button; } /** Creates and returns button showing both text and image */ textButton(name, callback, icon=VRSPACEUI.contentBase+"/content/icons/play.png", color=this.submitColor) { let button = BABYLON.GUI.Button.CreateImageButton(name.toLowerCase(), name, icon); button.background = color; button.widthInPixels = this.heightInPixels/2*name.length+10; this.setupButton(button, callback); return button; } makeIcon(name, url) { let ret = new BABYLON.GUI.Image(name, url); ret.stretch = BABYLON.GUI.Image.STRETCH_UNIFORM; return ret; } /** * Creates and returns a VirtualKeyboard, bound to given AdvancedDynamicTexture. * A form can only have one keyboard, shared by all InputText elements. * Currently selected InputText takes keyboard input. */ keyboard(advancedTexture = this.texture) { var keyboard = BABYLON.GUI.VirtualKeyboard.CreateDefaultLayout('form-keyboard'); keyboard.fontSizeInPixels = 36; keyboard.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; if (this.keyboardRows) { this.keyboardRows.forEach(row=>keyboard.addKeysRow(row)); } advancedTexture.addControl(keyboard); keyboard.isVisible = false; this.vKeyboard = keyboard; return keyboard; } /** * Creates a plane and advanced dynamic texture to hold the panel and all controlls. * At this point all UI elements should be created. * TODO Form should estimate required texture width/height from elements */ createPlane(size, textureWidth, textureHeight) { this.planeSize = size; this.plane = BABYLON.MeshBuilder.CreatePlane("FormPlane", {width: size*textureWidth/textureHeight, height: size}); this.texture = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(this.plane,textureWidth,textureHeight); // advancedTexture creates material and attaches it to the plane this.plane.material.transparencyMode = BABYLON.Material.MATERIAL_ALPHATEST; this.texture.addControl(this.panel); return this.plane; } /** * Connects the keyboard to given input, or hides it * @param input InputText to (dis)connect * @param focused true = connect the keyboard, false = disconnect and hide */ inputFocused(input, focused) { if ( this.vKeyboard && this.virtualKeyboardEnabled ) { if ( focused ) { this.vKeyboard.connect(input); // makes keyboard invisible if input has no focus } else { this.vKeyboard.disconnect(input); } this.vKeyboard.isVisible=focused; } if ( this.inputFocusListener ) { this.inputFocusListener(input,focused); } } /** * Dispose of all created elements. */ dispose() { VRSPACEUI.removeSelectable(this); if ( this.vKeyboard ) { this.vKeyboard.dispose(); delete this.vKeyboard; } this.elements.forEach(e=>e.dispose()); this.speechInput.dispose(); if ( this.panel ) { this.panel.dispose(); } if ( this.plane ) { this.plane.dispose(); } if ( this.texture ) { this.texture.dispose(); } } /** Internal voice input method: converts a control (button,checkbox...) name (label) to voice command */ nameToCommand(name) { let ret = null; if ( name ) { // split words and remove all punctuation let tokens = name.split(/\s+/).map(word => word.replace(/^[^\w]+|[^\w]+$/g, '')); if ( tokens ) { // first word is mandatory let command = tokens[0].toLowerCase(); if ( tokens.length > 1 ) { // all other words are optional for ( let i = 1; i < tokens.length; i++ ) { command += ' ('+tokens[i].toLowerCase()+')'; } } ret = command; } } return ret; } /** * Input delegate method used for gamepad input, or programatic control of the form. * @return all controls in this form, or in the keyboard if it's active */ getControls() { if ( this.activeControl && this.activeControl.getClassName() == "VirtualKeyboard") { return this.vKeyboard.children[this.keyboardRow].children; } return this.controls; } /** * Input delegate method used for gamepad input, or programatic control of the form. * @return currently active control, or in the keyboard if it's active */ getActiveControl() { if ( this.activeControl && this.activeControl.getClassName() == "VirtualKeyboard") { return this.vKeyboard.children[this.keyboardRow].children[this.keyboardCol]; } return this.activeControl; } /** * Input delegate method used for gamepad input, or programatic control of the form. * Sets currently active control. */ setActiveControl(control) { if ( this.activeControl && this.activeControl.getClassName() == "VirtualKeyboard") { return; } this.activeControl = control; } /** * Input delegate method used for gamepad input, or programatic control of the form. * Activates currently selected control, equivalent to clicking/tapping it. * E.g. (de)select a checkbox, press a button, etc. */ activateCurrent() { if ( this.activeControl ) { //console.log('activate '+this.activeControl.getClassName()); if ( this.activeControl.getClassName() == "Checkbox") { this.activeControl.isChecked = !this.activeControl.isChecked; } else if ( this.activeControl.getClassName() == "Button") { this.activeControl.onPointerDownObservable.observers.forEach(observer=>observer.callback(this)); } else if ( this.activeControl.getClassName() == "InputText") { console.log("activating keyboard"); this.activeControl.disableMobilePrompt = true; // keyboard has 5 children, each with own children; this.getActiveControl().background = this.activeBackground; this.activeControl = this.vKeyboard; this.keyboardRow = 0; this.keyboardCol = 0; this.selectCurrent(0); } else if ( this.activeControl.getClassName() == "VirtualKeyboard") { let input = this.activeControl.connectedInputText; let button = this.vKeyboard.children[this.keyboardRow].children[this.keyboardCol]; button.onPointerUpObservable.observers.forEach(o=>{ o.callback(); }) if(!this.vKeyboard.isVisible) { // enter key pressed this.activeControl = input; this.getActiveControl().background = this.selected; } } } } /** * Internal virtual keyboard method, selects current row at given index */ selectCurrent(index) { if (this.activeControl) { //console.log('select '+index+' '+this.getActiveControl().getClassName()); this.keyboardCol = index; this.activeBackground = this.getActiveControl().background; this.getActiveControl().background = this.selected; if ( this.getActiveControl().getClassName() == "InputText") { this.inputFocused(this.getActiveControl(),true); } } } /** * Deselects current control, i.e. changes the background color */ unselectCurrent() { if (this.activeControl) { this.getActiveControl().background = this.activeBackground; if ( this.getActiveControl().getClassName() == "InputText") { this.inputFocused(this.activeControl,false); } } } /** * Internal virtual keyboard method, keeps column index in range */ adjustKeyboardColumn() { if (this.keyboardCol >= this.vKeyboard.children[this.keyboardRow].children.length-1) { this.keyboardCol = this.vKeyboard.children[this.keyboardRow].children.length-1; } this.selectCurrent(this.keyboardCol); } /** * Input delegate method used for gamepad input, or programatic control of the form. * Processes up key: activate current element, or move up a row in virtual keyboard */ up() { if ( this.activeControl && this.activeControl.getClassName() == "VirtualKeyboard") { this.unselectCurrent(); if ( this.keyboardRow > 0 ) { this.keyboardRow--; } else { this.keyboardRow = this.vKeyboard.children.length-1; } this.adjustKeyboardColumn(); } else { this.activateCurrent(); } } /** * Input delegate method used for gamepad input, or programatic control of the form. * Processes down key: move down a row in virtual keyboard */ down() { if ( this.activeControl && this.activeControl.getClassName() == "VirtualKeyboard") { this.unselectCurrent(); if ( this.keyboardRow + 1 < this.vKeyboard.children.length ) { this.keyboardRow++; } else { this.keyboardRow = 0; } this.adjustKeyboardColumn(); return false; } return true; } /** * XR selection support */ isSelectableMesh(mesh) { return mesh.isEnabled() && this.plane && this.plane == mesh; } }