@babylonjs/gui
Version:
Babylon.js GUI module =====================
413 lines • 19.4 kB
JavaScript
import { ContentDisplay3D } from "./contentDisplay3D.js";
import { TouchHolographicButton } from "./touchHolographicButton.js";
import { AdvancedDynamicTexture } from "../../2D/advancedDynamicTexture.js";
import { Control } from "../../2D/controls/control.js";
import { TextBlock } from "../../2D/controls/textBlock.js";
import { DefaultBehavior } from "../behaviors/defaultBehavior.js";
import { SlateGizmo } from "../gizmos/slateGizmo.js";
import { FluentMaterial } from "../materials/fluent/fluentMaterial.js";
import { FluentBackplateMaterial } from "../materials/fluentBackplate/fluentBackplateMaterial.js";
import { PointerDragBehavior } from "@babylonjs/core/Behaviors/Meshes/pointerDragBehavior.js";
import { Vector4 } from "@babylonjs/core/Maths/math.js";
import { Epsilon } from "@babylonjs/core/Maths/math.constants.js";
import { Scalar } from "@babylonjs/core/Maths/math.scalar.js";
import { Quaternion, Vector2, Vector3 } from "@babylonjs/core/Maths/math.vector.js";
import { Viewport } from "@babylonjs/core/Maths/math.viewport.js";
import { CreateBox } from "@babylonjs/core/Meshes/Builders/boxBuilder.js";
import { CreatePlane } from "@babylonjs/core/Meshes/Builders/planeBuilder.js";
import { Mesh } from "@babylonjs/core/Meshes/mesh.js";
import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData.js";
import { Tools } from "@babylonjs/core/Misc/tools.js";
/**
* Class used to create a holographic slate
* @since 5.0.0
*/
export class HolographicSlate extends ContentDisplay3D {
/**
* Regroups all mesh behaviors for the slate
*/
get defaultBehavior() {
return this._defaultBehavior;
}
/**
* 2D dimensions of the slate
*/
get dimensions() {
return this._dimensions;
}
set dimensions(value) {
//clamp, respecting ratios
let scale = 1.0;
if (value.x < this.minDimensions.x || value.y < this.minDimensions.y) {
const newRatio = value.x / value.y;
const minRatio = this.minDimensions.x / this.minDimensions.y;
if (minRatio > newRatio) {
// We just need to make sure the x-val is greater than the min
scale = this.minDimensions.x / value.x;
}
else {
// We just need to make sure the y-val is greater than the min
scale = this.minDimensions.y / value.y;
}
}
this._dimensions.copyFrom(value).scaleInPlace(scale);
this._updatePivot();
this._positionElements();
}
/**
* Height of the title bar component
*/
get titleBarHeight() {
return this._titleBarHeight;
}
set titleBarHeight(value) {
this._titleBarHeight = value;
}
/**
* Rendering ground id of all the meshes
*/
set renderingGroupId(id) {
this._titleBar.renderingGroupId = id;
this._titleBarTitle.renderingGroupId = id;
this._contentPlate.renderingGroupId = id;
this._backPlate.renderingGroupId = id;
}
get renderingGroupId() {
return this._titleBar.renderingGroupId;
}
/**
* The title text displayed at the top of the slate
*/
set title(title) {
this._titleText = title;
if (this._titleTextComponent) {
this._titleTextComponent.text = title;
}
}
get title() {
return this._titleText;
}
/**
* Creates a new slate
* @param name defines the control name
*/
constructor(name) {
super(name);
/**
* Margin between title bar and contentplate
*/
this.titleBarMargin = 0.005;
/**
* Origin in local coordinates (top left corner)
*/
this.origin = new Vector3(0, 0, 0);
this._dimensions = new Vector2(21.875, 12.5);
this._titleBarHeight = 0.625;
this._titleText = "";
/**
* If true, the content will be scaled to fit the dimensions of the slate
*/
this.fitContentToDimensions = false;
this._contentScaleRatio = 1;
/**
* Minimum dimensions of the slate
*/
this.minDimensions = new Vector2(15.625, 6.25);
/**
* Default dimensions of the slate
*/
this.defaultDimensions = this._dimensions.clone();
this._followButton = new TouchHolographicButton("followButton" + this.name);
this._followButton.isToggleButton = true;
this._closeButton = new TouchHolographicButton("closeButton" + this.name);
this._contentViewport = new Viewport(0, 0, 1, 1);
this._contentDragBehavior = new PointerDragBehavior({
dragPlaneNormal: new Vector3(0, 0, -1),
});
}
/**
* Apply the facade texture (created from the content property).
* This function can be overloaded by child classes
* @param facadeTexture defines the AdvancedDynamicTexture to use
*/
_applyFacade(facadeTexture) {
this._contentMaterial.albedoTexture = facadeTexture;
this._resetContentPositionAndZoom();
this._applyContentViewport();
facadeTexture.attachToMesh(this._contentPlate, true);
}
_addControl(control) {
control._host = this._host;
if (this._host.utilityLayer) {
control._prepareNode(this._host.utilityLayer.utilityLayerScene);
}
}
_getTypeName() {
return "HolographicSlate";
}
/**
* @internal
*/
_positionElements() {
const followButton = this._followButton;
const closeButton = this._closeButton;
const titleBar = this._titleBar;
const titleBarTitle = this._titleBarTitle;
const contentPlate = this._contentPlate;
const backPlate = this._backPlate;
if (followButton && closeButton && titleBar) {
closeButton.scaling.setAll(this.titleBarHeight);
followButton.scaling.setAll(this.titleBarHeight);
closeButton.position.copyFromFloats(this.dimensions.x - this.titleBarHeight / 2, -this.titleBarHeight / 2, 0).addInPlace(this.origin);
followButton.position.copyFromFloats(this.dimensions.x - (3 * this.titleBarHeight) / 2, -this.titleBarHeight / 2, 0).addInPlace(this.origin);
const contentPlateHeight = this.dimensions.y - this.titleBarHeight - this.titleBarMargin;
const rightHandScene = contentPlate.getScene().useRightHandedSystem;
titleBar.scaling.set(this.dimensions.x, this.titleBarHeight, Epsilon);
titleBarTitle.scaling.set(this.dimensions.x - 2 * this.titleBarHeight, this.titleBarHeight, Epsilon);
contentPlate.scaling.copyFromFloats(this.dimensions.x, contentPlateHeight, Epsilon);
backPlate.scaling.copyFromFloats(this.dimensions.x, contentPlateHeight, Epsilon);
titleBar.position.copyFromFloats(this.dimensions.x / 2, -(this.titleBarHeight / 2), 0).addInPlace(this.origin);
titleBarTitle.position
.copyFromFloats(this.dimensions.x / 2 - this.titleBarHeight, -(this.titleBarHeight / 2), rightHandScene ? Epsilon : -Epsilon)
.addInPlace(this.origin);
contentPlate.position.copyFromFloats(this.dimensions.x / 2, -(this.titleBarHeight + this.titleBarMargin + contentPlateHeight / 2), 0).addInPlace(this.origin);
backPlate.position
.copyFromFloats(this.dimensions.x / 2, -(this.titleBarHeight + this.titleBarMargin + contentPlateHeight / 2), rightHandScene ? -Epsilon : Epsilon)
.addInPlace(this.origin);
// Update the title's AdvancedDynamicTexture scale to avoid visual stretching
this._titleTextComponent.host.scaleTo((HolographicSlate._DEFAULT_TEXT_RESOLUTION_Y * titleBarTitle.scaling.x) / titleBarTitle.scaling.y, HolographicSlate._DEFAULT_TEXT_RESOLUTION_Y);
const aspectRatio = this.dimensions.x / contentPlateHeight;
this._contentViewport.width = this._contentScaleRatio;
this._contentViewport.height = this._contentScaleRatio / aspectRatio;
this._applyContentViewport();
if (this._gizmo) {
this._gizmo.updateBoundingBox();
}
}
}
_applyContentViewport() {
if (this._contentPlate?.material && this._contentPlate.material.albedoTexture) {
const tex = this._contentPlate.material.albedoTexture;
tex.uScale = this._contentScaleRatio;
tex.vScale = this.fitContentToDimensions ? this._contentScaleRatio : (this._contentScaleRatio / this._contentViewport.width) * this._contentViewport.height;
tex.uOffset = this._contentViewport.x;
tex.vOffset = this._contentViewport.y;
}
}
_resetContentPositionAndZoom() {
this._contentViewport.x = 0;
this._contentViewport.y = 0; // 1 - this._contentViewport.height / this._contentViewport.width;
this._contentScaleRatio = 1;
}
/**
* @internal
*/
_updatePivot() {
if (!this.mesh) {
return;
}
// Update pivot point so it is at the center of geometry
// As origin is topleft corner in 2D, dimensions are calculated towards bottom right corner, thus y axis is downwards
const center = new Vector3(this.dimensions.x * 0.5, -this.dimensions.y * 0.5, Epsilon);
center.addInPlace(this.origin);
center.z = 0;
const origin = new Vector3(0, 0, 0);
Vector3.TransformCoordinatesToRef(origin, this.mesh.computeWorldMatrix(true), origin);
this.mesh.setPivotPoint(center);
const origin2 = new Vector3(0, 0, 0);
Vector3.TransformCoordinatesToRef(origin2, this.mesh.computeWorldMatrix(true), origin2);
this.mesh.position.addInPlace(origin).subtractInPlace(origin2);
}
// Mesh association
_createNode(scene) {
const node = new Mesh("slate_" + this.name, scene);
this._titleBar = CreateBox("titleBar_" + this.name, { size: 1 }, scene);
this._titleBarTitle = CreatePlane("titleText_" + this.name, { size: 1 }, scene);
this._titleBarTitle.parent = node;
this._titleBarTitle.isPickable = false;
const adt = AdvancedDynamicTexture.CreateForMesh(this._titleBarTitle);
this._titleTextComponent = new TextBlock("titleText_" + this.name, this._titleText);
this._titleTextComponent.textWrapping = 2 /* TextWrapping.Ellipsis */;
this._titleTextComponent.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT;
this._titleTextComponent.color = "white";
this._titleTextComponent.fontSize = HolographicSlate._DEFAULT_TEXT_RESOLUTION_Y / 2;
this._titleTextComponent.paddingLeft = HolographicSlate._DEFAULT_TEXT_RESOLUTION_Y / 4;
adt.addControl(this._titleTextComponent);
if (scene.useRightHandedSystem) {
const faceUV = new Vector4(0, 0, 1, 1);
this._contentPlate = CreatePlane("contentPlate_" + this.name, { size: 1, sideOrientation: VertexData.BACKSIDE, frontUVs: faceUV }, scene);
this._backPlate = CreatePlane("backPlate_" + this.name, { size: 1, sideOrientation: VertexData.FRONTSIDE }, scene);
}
else {
const faceUV = new Vector4(0, 0, 1, 1);
this._contentPlate = CreatePlane("contentPlate_" + this.name, { size: 1, sideOrientation: VertexData.FRONTSIDE, frontUVs: faceUV }, scene);
this._backPlate = CreatePlane("backPlate_" + this.name, { size: 1, sideOrientation: VertexData.BACKSIDE }, scene);
}
this._titleBar.parent = node;
this._titleBar.isNearGrabbable = true;
this._contentPlate.parent = node;
this._backPlate.parent = node;
this._attachContentPlateBehavior();
this._addControl(this._followButton);
this._addControl(this._closeButton);
const followButton = this._followButton;
const closeButton = this._closeButton;
followButton.node.parent = node;
closeButton.node.parent = node;
this._positionElements();
const baseUrl = Tools.GetAssetUrl(HolographicSlate.ASSETS_BASE_URL);
this._followButton.imageUrl = baseUrl + HolographicSlate.FOLLOW_ICON_FILENAME;
this._closeButton.imageUrl = baseUrl + HolographicSlate.CLOSE_ICON_FILENAME;
this._followButton.isBackplateVisible = false;
this._closeButton.isBackplateVisible = false;
this._followButton.onToggleObservable.add((isToggled) => {
this._defaultBehavior.followBehaviorEnabled = isToggled;
if (this._defaultBehavior.followBehaviorEnabled) {
this._defaultBehavior.followBehavior.recenter();
}
});
this._closeButton.onPointerClickObservable.add(() => {
this.dispose();
});
node.rotationQuaternion = Quaternion.Identity();
node.isVisible = false;
return node;
}
_attachContentPlateBehavior() {
this._contentDragBehavior.attach(this._contentPlate);
this._contentDragBehavior.moveAttached = false;
this._contentDragBehavior.useObjectOrientationForDragging = true;
this._contentDragBehavior.updateDragPlane = false;
const origin = new Vector3();
const worldDimensions = new Vector3();
const upWorld = new Vector3();
const rightWorld = new Vector3();
const projectedOffset = new Vector2();
let startViewport;
let worldMatrix;
this._contentDragBehavior.onDragStartObservable.add((event) => {
if (!this.node) {
return;
}
startViewport = this._contentViewport.clone();
worldMatrix = this.node.computeWorldMatrix(true);
origin.copyFrom(event.dragPlanePoint);
worldDimensions.set(this.dimensions.x, this.dimensions.y, Epsilon);
worldDimensions.y -= this.titleBarHeight + this.titleBarMargin;
Vector3.TransformNormalToRef(worldDimensions, worldMatrix, worldDimensions);
upWorld.copyFromFloats(0, 1, 0);
Vector3.TransformNormalToRef(upWorld, worldMatrix, upWorld);
rightWorld.copyFromFloats(1, 0, 0);
Vector3.TransformNormalToRef(rightWorld, worldMatrix, rightWorld);
upWorld.normalize();
upWorld.scaleInPlace(1 / Vector3.Dot(upWorld, worldDimensions));
rightWorld.normalize();
rightWorld.scaleInPlace(1 / Vector3.Dot(rightWorld, worldDimensions));
});
const offset = new Vector3();
this._contentDragBehavior.onDragObservable.add((event) => {
if (this.fitContentToDimensions) {
return;
}
offset.copyFrom(event.dragPlanePoint);
offset.subtractInPlace(origin);
projectedOffset.copyFromFloats(Vector3.Dot(offset, rightWorld), Vector3.Dot(offset, upWorld));
// By default, content takes full width available and height is cropped to keep aspect ratio
this._contentViewport.x = Scalar.Clamp(startViewport.x - offset.x, 0, 1 - this._contentViewport.width * this._contentScaleRatio);
this._contentViewport.y = Scalar.Clamp(startViewport.y - offset.y, 0, 1 - this._contentViewport.height * this._contentScaleRatio);
this._applyContentViewport();
});
}
_affectMaterial(mesh) {
// TODO share materials
this._titleBarMaterial = new FluentBackplateMaterial(`${this.name} plateMaterial`, mesh.getScene());
this._contentMaterial = new FluentMaterial(`${this.name} contentMaterial`, mesh.getScene());
this._contentMaterial.renderBorders = true;
this._backMaterial = new FluentBackplateMaterial(`${this.name} backPlate`, mesh.getScene());
this._backMaterial.lineWidth = Epsilon;
this._backMaterial.radius = 0.005;
this._backMaterial.backFaceCulling = true;
this._titleBar.material = this._titleBarMaterial;
this._contentPlate.material = this._contentMaterial;
this._backPlate.material = this._backMaterial;
this._resetContent();
this._applyContentViewport();
}
/**
* @internal
*/
_prepareNode(scene) {
super._prepareNode(scene);
this._gizmo = new SlateGizmo(this._host.utilityLayer);
this._gizmo.attachedSlate = this;
this._defaultBehavior = new DefaultBehavior();
this._defaultBehavior.attach(this.node, [this._titleBar]);
this._defaultBehavior.sixDofDragBehavior.onDragStartObservable.add(() => {
this._followButton.isToggled = false;
});
this._positionChangedObserver = this._defaultBehavior.sixDofDragBehavior.onPositionChangedObservable.add(() => {
this._gizmo.updateBoundingBox();
});
this._updatePivot();
this.resetDefaultAspectAndPose(false);
}
/**
* Resets the aspect and pose of the slate so it is right in front of the active camera, facing towards it.
* @param resetAspect Should the slate's dimensions/aspect ratio be reset as well
*/
resetDefaultAspectAndPose(resetAspect = true) {
if (!this._host || !this._host.utilityLayer || !this.node) {
return;
}
const scene = this._host.utilityLayer.utilityLayerScene;
const camera = scene.activeCamera;
if (camera) {
const worldMatrix = camera.getWorldMatrix();
const backward = Vector3.TransformNormal(Vector3.Backward(scene.useRightHandedSystem), worldMatrix);
this.origin.setAll(0);
this._gizmo.updateBoundingBox();
const pivot = this.node.getAbsolutePivotPoint();
// only if position was not yet set!
if (this.node.position.equalsToFloats(0, 0, 0)) {
this.node.position.copyFrom(camera.position).subtractInPlace(backward).subtractInPlace(pivot);
}
this.node.rotationQuaternion = Quaternion.FromLookDirectionLH(backward, new Vector3(0, 1, 0));
if (resetAspect) {
this.dimensions = this.defaultDimensions;
}
}
}
/**
* Releases all associated resources
*/
dispose() {
super.dispose();
this._titleBarMaterial.dispose();
this._contentMaterial.dispose();
this._titleBar.dispose();
this._titleBarTitle.dispose();
this._contentPlate.dispose();
this._backPlate.dispose();
this._followButton.dispose();
this._closeButton.dispose();
this._host.onPickedPointChangedObservable.remove(this._pickedPointObserver);
this._defaultBehavior.sixDofDragBehavior.onPositionChangedObservable.remove(this._positionChangedObserver);
this._defaultBehavior.detach();
this._gizmo.dispose();
this._contentDragBehavior.detach();
}
}
/**
* Base Url for the assets.
*/
HolographicSlate.ASSETS_BASE_URL = "https://assets.babylonjs.com/core/MRTK/";
/**
* File name for the close icon.
*/
HolographicSlate.CLOSE_ICON_FILENAME = "IconClose.png";
/**
* File name for the close icon.
*/
HolographicSlate.FOLLOW_ICON_FILENAME = "IconFollowMe.png";
HolographicSlate._DEFAULT_TEXT_RESOLUTION_Y = 102.4;
//# sourceMappingURL=holographicSlate.js.map