import { Label } from './label.js';
import { ManipulationHandles } from "./manipulation-handles.js";
import { VRSPACEUI } from "../vrspace-ui.js";
import { BaseArea } from './base-area.js';
/**
* Text area somewhere in space, like a screen.
* Provides methods for writing the text, movement, resizing.
*/
export class TextArea extends BaseArea {
/**
* Creates the area with default values.
* By default, it's sized and positioned to be attached to the camera, is nicely transparent, font size 16 on 512x512 texture,
* and includes manipulation handles.
* @param scene babylon scene, mandatory
* @param name optional, defaults to TextArea
* @param titleText optional title to display above the area
*/
constructor(scene, name = "TextArea", titleText = null) {
super(scene, name);
this.titleText = titleText;
this.position = new BABYLON.Vector3(-.08, 0, .5);
this.alpha = 0.7;
this.fontSize = 16;
this.width = 512;
this.height = 512;
this.capacity = this.width * this.height / this.fontSize;
this.textWrapping = true;
this.addBackground = true;
this.autoScale = false; // experimental, unstable
this.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
this.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
this.text = "";
/** @type {Label} */
this.title = null;
this.visible = false;
/**
* Makes this area scrollable when required, i.e. text does not fit on the area.
* Must be set before show() is called.
*/
this.scrollable = false;
this.scrollViewer = null;
}
/**
* As the name says. Optionally also creates manipulation handles.
*/
show() {
if ( this.visible ) {
return;
}
this.visible = true;
this.group.position = this.position;
this.textBlock = new BABYLON.GUI.TextBlock();
this.textBlock.widthInPixels = this.width;
this.textBlock.textWrapping = this.textWrapping;
this.textBlock.color = "white";
this.textBlock.fontSize = this.fontSize;
this.textBlock.fontFamily = "monospace";
this.textBlock.textHorizontalAlignment = this.textHorizontalAlignment;
this.textBlock.textVerticalAlignment = this.textVerticalAlignment;
this.textBlock.text = "text is required to compute fontOffset used for font rendering";
this.textBlock.computeExpectedHeight(); // and now we have textBlock.fontOffset
this.textBlock.text = this.text;
if (this.autoScale) {
if (!this.text) {
//throw new Error( "Text has to be set before autoscaling");
return;
}
// so we scale height depending on text size and width
// i.e. add as many rows as we need
let rowsNeeded = Math.ceil(this.text.length * this.textBlock.fontOffset.height / this.width);
this.height = rowsNeeded * this.textBlock.fontOffset.height;
this.size = rowsNeeded * this.size;
}
this.ratio = this.width / this.height;
this.areaPlane = BABYLON.MeshBuilder.CreatePlane("TextAreaPlane", { width: this.size * this.ratio, height: this.size }, this.scene);
this.areaPlane.parent = this.group;
if (this.addBackground) {
/*
this.material = new BABYLON.StandardMaterial("TextAreaMaterial", this.scene);
this.material.alpha = this.alpha;
this.material.diffuseColor = new BABYLON.Color3(.2,.2,.3);
*/
this.material = VRSPACEUI.uiMaterial;
this.backgroundPlane = BABYLON.MeshBuilder.CreatePlane("BackgroundPlane", { width: this.size * this.ratio * 1.05, height: this.size * 1.05 }, this.scene);
this.backgroundPlane.position = new BABYLON.Vector3(0, 0, this.size / 100);
this.backgroundPlane.parent = this.group;
this.backgroundPlane.material = this.material;
}
if (this.addHandles) {
this.createHandles();
}
this.texture = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(
this.areaPlane,
this.width,
this.height,
this.scrollable // CHECKME: handle pointer move events ? Kinda required for scroll viewer
);
this.areaPlane.material.transparencyMode = BABYLON.Material.MATERIAL_ALPHATEST;
this.texture.addControl(this.textBlock);
this.showTitle();
}
/**
* Show title text on top of the area. Title can be changed and displayed any time after show().
*/
showTitle() {
if (this.titleText) {
if (this.title) {
this.title.dispose();
}
let titleHeight = this.size / this.getMaxRows() * 2; // twice as high as a text row
this.title = new Label(this.titleText, new BABYLON.Vector3(0, 1.2 * this.size / 2 + titleHeight / 2, 0), this.group);
this.title.text = this.titleText;
this.title.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
this.title.height = titleHeight;
this.title.display();
} else if (this.title) {
this.title.dispose();
this.title = null;
}
}
/**
* Remove the title, if any.
*/
removeTitle() {
if (this.title) {
this.title.dispose();
this.title = null;
}
}
/**
* Creates manipulation handles. Left and right handle resize, and top and bottom move it.
*/
createHandles() {
this.handles = new ManipulationHandles(this.backgroundPlane, this.size * this.ratio, this.size, this.scene);
this.handles.canMinimize = this.canMinimize;
this.handles.canClose = this.canClose;
this.handles.onClose = this.onClose;
this.handles.show();
}
/**
* Hide/show (requires manipulation handles)
* @param flag boolean, hide/show
*/
hide(flag) {
if (this.handles) {
this.handles.hide(flag);
}
}
/** Clean up. */
dispose() {
super.dispose();
this.removeTitle();
if (this.backgroundPlane) {
this.backgroundPlane.dispose();
}
if (this.texture) {
this.textBlock.dispose();
this.texture.dispose();
}
}
/** Attach both textPlane and backgroundPlane to the HUD, and optionally also handles. */
attachToHud() {
super.attachToHud();
}
/**
* Attach it to the camera. It does not resize automatically, just sets the parent.
* It does not automatically switch to another camera if active camera changes.
* @param camera currently active camera
*/
attachToCamera(camera = this.scene.activeCamera) {
super.attachToCamera(camera);
}
/**
* Detach from whatever attached to, i.e. drop it where you stand.
*/
detach(offset) {
super.detach(offset);
}
/**
* Check if current text length exceeds the capacity and truncate as required.
* For scrollable area, check number of lines and turn on scrollbars if required.
*/
checkCapacity() {
//console.log("Test capacity: length=" + this.text.length + " capacity=" + this.capacity + " maxRows=" + this.getMaxRows() + " curRows=" + this.getCurRows());
if (this.capacity < this.text.length) {
this.text = this.text.substring(this.text.length - this.capacity);
}
if (this.scrollable) {
if (this.scrollViewer == null && this.getMaxRows() <= this.getCurRows()) {
this.texture.removeControl(this.textBlock);
this.scrollViewer = new BABYLON.GUI.ScrollViewer(this.name);
this.scrollViewer.thickness = 1;
this.scrollViewer.color = "black";
this.scrollViewer.width = 1;
this.scrollViewer.height = 1;
//this.scrollViewer.background = "black";
this.textBlock.resizeToFit = true;
this.scrollViewer.addControl(this.textBlock);
this.texture.addControl(this.scrollViewer);
this.scrollViewer.verticalBar.value = 1;
}
}
}
/** Same as write */
print(string) {
this.write(string);
}
/** Write a string */
write(string) {
this.text += string;
this.checkCapacity();
this.textBlock.text = this.text;
}
/** Same as writeln */
println(string) {
this.writeln(string);
}
/** Print a string into a new line */
writeln(string = "") {
this.write("\n" + string);
}
/** Print a number of lines */
writeArray(text) {
text.forEach(line => this.writeln(line));
}
/** Remove the text */
clear() {
this.text = "";
this.textBlock.text = this.text;
if (this.scrollable && this.scrollViewer) {
this.scrollViewer.removeControl(this.textBlock);
this.texture.removeControl(this.scrollViewer);
this.scrollViewer.dispose();
this.scrollViewer = null;
this.textBlock.resizeToFit = false;
this.texture.addControl(this.textBlock);
}
}
/** Calculates and returns maximum text rows available */
getMaxRows() {
return Math.floor(this.height / (this.textBlock.fontOffset.height));
}
/** Calculates and returns current number of rows */
getCurRows() {
let rows = 0;
let maxCols = this.getMaxCols();
this.text?.split("\n").forEach(row => {
rows += Math.ceil(row.length / maxCols);
});
return rows;
}
/** Calculates and returns maximum number text columns available */
getMaxCols() {
// font offset on android is not integer
return Math.floor(this.height * this.ratio / (Math.ceil(this.textBlock.fontOffset.height) / 2));
}
/**
* Set click event handler here
* @param callback executed on pointer click, passed Control argument
*/
onClick(callback) {
this.texture.onControlPickedObservable.add(callback);
}
/** Clean up allocated resources */
dispose() {
super.dispose();
if (this.scrollViewer) {
this.scrollViewer.dispose();
}
}
}