UNPKG

@babylonjs/gui

Version:

Babylon.js GUI module =====================

1,150 lines 67.9 kB
import { Observable } from "@babylonjs/core/Misc/observable.js"; import { Vector2, Vector3, TmpVectors } from "@babylonjs/core/Maths/math.vector.js"; import { Tools } from "@babylonjs/core/Misc/tools.js"; import { PointerEventTypes } from "@babylonjs/core/Events/pointerEvents.js"; import { ClipboardEventTypes, ClipboardInfo } from "@babylonjs/core/Events/clipboardEvents.js"; import { KeyboardEventTypes } from "@babylonjs/core/Events/keyboardEvents.js"; import { Texture } from "@babylonjs/core/Materials/Textures/texture.js"; import { DynamicTexture } from "@babylonjs/core/Materials/Textures/dynamicTexture.js"; import { Layer } from "@babylonjs/core/Layers/layer.js"; import { Container } from "./controls/container.js"; import { Control } from "./controls/control.js"; import { Style } from "./style.js"; import { Measure } from "./measure.js"; import { Constants } from "@babylonjs/core/Engines/constants.js"; import { Viewport } from "@babylonjs/core/Maths/math.viewport.js"; import { Color3 } from "@babylonjs/core/Maths/math.color.js"; import { WebRequest } from "@babylonjs/core/Misc/webRequest.js"; import { RandomGUID } from "@babylonjs/core/Misc/guid.js"; import { GetClass } from "@babylonjs/core/Misc/typeStore.js"; import { DecodeBase64ToBinary } from "@babylonjs/core/Misc/stringTools.js"; /** * Class used to create texture to support 2D GUI elements * @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui */ export class AdvancedDynamicTexture extends DynamicTexture { /** Gets the number of layout calls made the last time the ADT has been rendered */ get numLayoutCalls() { return this._numLayoutCalls; } /** Gets the number of render calls made the last time the ADT has been rendered */ get numRenderCalls() { return this._numRenderCalls; } /** * If set to true, the renderScale will be adjusted automatically to the engine's hardware scaling * If this is set to true, manually setting the renderScale will be ignored * This is useful when the engine's hardware scaling is set to a value other than 1 */ get adjustToEngineHardwareScalingLevel() { return this._adjustToEngineHardwareScalingLevel; } set adjustToEngineHardwareScalingLevel(value) { if (this._adjustToEngineHardwareScalingLevel === value) { return; } this._adjustToEngineHardwareScalingLevel = value; this._onResize(); } /** * Gets or sets a number used to scale rendering size (2 means that the texture will be twice bigger). * Useful when you want more antialiasing */ get renderScale() { return this._renderScale; } set renderScale(value) { if (value === this._renderScale) { return; } this._renderScale = value; this._onResize(); } /** Gets or sets the background color */ get background() { return this._background; } set background(value) { if (this._background === value) { return; } this._background = value; this.markAsDirty(); } /** * Gets or sets the ideal width used to design controls. * The GUI will then rescale everything accordingly * @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui#adaptive-scaling */ get idealWidth() { return this._idealWidth; } set idealWidth(value) { if (this._idealWidth === value) { return; } this._idealWidth = value; this.markAsDirty(); this._rootContainer._markAllAsDirty(); } /** * Gets or sets the ideal height used to design controls. * The GUI will then rescale everything accordingly * @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui#adaptive-scaling */ get idealHeight() { return this._idealHeight; } set idealHeight(value) { if (this._idealHeight === value) { return; } this._idealHeight = value; this.markAsDirty(); this._rootContainer._markAllAsDirty(); } /** * Gets or sets a boolean indicating if the smallest ideal value must be used if idealWidth and idealHeight are both set * @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui#adaptive-scaling */ get useSmallestIdeal() { return this._useSmallestIdeal; } set useSmallestIdeal(value) { if (this._useSmallestIdeal === value) { return; } this._useSmallestIdeal = value; this.markAsDirty(); this._rootContainer._markAllAsDirty(); } /** * Gets or sets a boolean indicating if adaptive scaling must be used * @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui#adaptive-scaling */ get renderAtIdealSize() { return this._renderAtIdealSize; } set renderAtIdealSize(value) { if (this._renderAtIdealSize === value) { return; } this._renderAtIdealSize = value; this._onResize(); } /** * Gets the ratio used when in "ideal mode" * @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui#adaptive-scaling * */ get idealRatio() { let rwidth = 0; let rheight = 0; if (this._idealWidth) { rwidth = this.getSize().width / this._idealWidth; } if (this._idealHeight) { rheight = this.getSize().height / this._idealHeight; } if (this._useSmallestIdeal && this._idealWidth && this._idealHeight) { return window.innerWidth < window.innerHeight ? rwidth : rheight; } if (this._idealWidth) { // horizontal return rwidth; } if (this._idealHeight) { // vertical return rheight; } return 1; } /** * Gets the underlying layer used to render the texture when in fullscreen mode */ get layer() { return this._layerToDispose; } /** * Gets the root container control */ get rootContainer() { return this._rootContainer; } /** * Returns an array containing the root container. * This is mostly used to let the Inspector introspects the ADT * @returns an array containing the rootContainer */ getChildren() { return [this._rootContainer]; } /** * Will return all controls that are inside this texture * @param directDescendantsOnly defines if true only direct descendants of 'this' will be considered, if false direct and also indirect (children of children, an so on in a recursive manner) descendants of 'this' will be considered * @param predicate defines an optional predicate that will be called on every evaluated child, the predicate must return true for a given child to be part of the result, otherwise it will be ignored * @returns all child controls */ getDescendants(directDescendantsOnly, predicate) { return this._rootContainer.getDescendants(directDescendantsOnly, predicate); } /** * Will return all controls with the given type name * @param typeName defines the type name to search for * @returns an array of all controls found */ getControlsByType(typeName) { return this._rootContainer.getDescendants(false, (control) => control.typeName === typeName); } /** * Will return the first control with the given name * @param name defines the name to search for * @returns the first control found or null */ getControlByName(name) { return this._getControlByKey("name", name); } _getControlByKey(key, value) { return this._rootContainer.getDescendants().find((control) => control[key] === value) || null; } /** * Gets or sets the current focused control */ get focusedControl() { return this._focusedControl; } set focusedControl(control) { if (this._focusedControl == control) { return; } if (this._focusedControl) { this._focusedControl.onBlur(); } if (control) { control.onFocus(); } this._focusedControl = control; } /** * Gets or sets a boolean indicating if the texture must be rendered in background or foreground when in fullscreen mode */ get isForeground() { if (!this.layer) { return true; } return !this.layer.isBackground; } set isForeground(value) { if (!this.layer) { return; } if (this.layer.isBackground === !value) { return; } this.layer.isBackground = !value; } /** * Gets or set information about clipboardData */ get clipboardData() { return this._clipboardData; } set clipboardData(value) { this._clipboardData = value; } /** @internal */ constructor(name, widthOrOptions, _height = 0, scene, generateMipMaps = false, samplingMode = Texture.NEAREST_SAMPLINGMODE, invertY = true) { widthOrOptions = widthOrOptions ?? 0; const width = typeof widthOrOptions === "object" && widthOrOptions !== undefined ? (widthOrOptions.width ?? 0) : (widthOrOptions ?? 0); const height = typeof widthOrOptions === "object" && widthOrOptions !== undefined ? (widthOrOptions.height ?? 0) : _height; super(name, { width, height }, typeof widthOrOptions === "object" && widthOrOptions !== undefined ? widthOrOptions : scene, generateMipMaps, samplingMode, Constants.TEXTUREFORMAT_RGBA, invertY); /** Indicates whether the ADT is used autonomously */ this.useStandalone = false; /** Observable that fires when the GUI is ready */ this.onGuiReadyObservable = new Observable(); this._isDirty = false; /** @internal */ this._rootContainer = new Container("root"); /** @internal */ this._lastControlOver = {}; /** @internal */ this._lastControlDown = {}; /** @internal */ this._capturingControl = {}; /** @internal */ this._linkedControls = new Array(); /** @internal */ this._isFullscreen = false; this._fullscreenViewport = new Viewport(0, 0, 1, 1); this._idealWidth = 0; this._idealHeight = 0; this._useSmallestIdeal = false; this._renderAtIdealSize = false; this._blockNextFocusCheck = false; this._renderScale = 1; this._cursorChanged = false; this._defaultMousePointerId = 0; this._rootChildrenHaveChanged = false; this._adjustToEngineHardwareScalingLevel = false; /** @internal */ this._capturedPointerIds = new Set(); /** @internal */ this._numLayoutCalls = 0; /** @internal */ this._numRenderCalls = 0; /** * Define type to string to ensure compatibility across browsers * Safari doesn't support DataTransfer constructor */ this._clipboardData = ""; /** * Observable event triggered each time an clipboard event is received from the rendering canvas */ this.onClipboardObservable = new Observable(); /** * Observable event triggered each time a pointer down is intercepted by a control */ this.onControlPickedObservable = new Observable(); /** * Observable event triggered before layout is evaluated */ this.onBeginLayoutObservable = new Observable(); /** * Observable event triggered after the layout was evaluated */ this.onEndLayoutObservable = new Observable(); /** * Observable event triggered before the texture is rendered */ this.onBeginRenderObservable = new Observable(); /** * Observable event triggered after the texture was rendered */ this.onEndRenderObservable = new Observable(); /** * Gets or sets a boolean defining if alpha is stored as premultiplied */ this.premulAlpha = false; /** * Gets or sets a boolean indicating that the canvas must be reverted on Y when updating the texture */ // eslint-disable-next-line @typescript-eslint/naming-convention this.applyYInversionOnUpdate = true; /** * A boolean indicating whether or not the elements can be navigated to using the tab key. * Defaults to false. */ this.disableTabNavigation = false; /** * A boolean indicating whether controls can be picked/clicked on or not. Defaults to false. */ this.disablePicking = false; /** * If set to true, the POINTERTAP event type will be used for "click", instead of POINTERUP */ this.usePointerTapForClickEvent = false; /** * If this is set, even when a control is pointer blocker, some events can still be passed through to the scene. * Options from values are PointerEventTypes * POINTERDOWN, POINTERUP, POINTERMOVE, POINTERWHEEL, POINTERPICK, POINTERTAP, POINTERDOUBLETAP */ this.skipBlockEvents = 0; /** * If set to true, every scene render will trigger a pointer event for the GUI * if it is linked to a mesh or has controls linked to a mesh. This will allow * you to catch the pointer moving around the GUI due to camera or mesh movements, * but it has a performance cost. */ this.checkPointerEveryFrame = false; this._useInvalidateRectOptimization = true; // Invalidated rectangle which is the combination of all invalidated controls after they have been rotated into absolute position this._invalidatedRectangle = null; this._alreadyRegisteredForRender = false; this._clearMeasure = new Measure(0, 0, 0, 0); this._focusProperties = { index: 0, total: -1 }; /** * @internal */ this._onClipboardCopy = (rawEvt) => { const evt = rawEvt; const ev = new ClipboardInfo(ClipboardEventTypes.COPY, evt); this.onClipboardObservable.notifyObservers(ev); evt.preventDefault(); }; /** * @internal */ this._onClipboardCut = (rawEvt) => { const evt = rawEvt; const ev = new ClipboardInfo(ClipboardEventTypes.CUT, evt); this.onClipboardObservable.notifyObservers(ev); evt.preventDefault(); }; /** * @internal */ this._onClipboardPaste = (rawEvt) => { const evt = rawEvt; const ev = new ClipboardInfo(ClipboardEventTypes.PASTE, evt); this.onClipboardObservable.notifyObservers(ev); evt.preventDefault(); }; /** * Recreate the content of the ADT from a JSON object * @param serializedObject define the JSON serialized object to restore from * @param scaleToSize defines whether to scale to texture to the saved size * @param urlRewriter defines an url rewriter to update urls before sending them to the controls * @deprecated Please use parseSerializedObject instead */ this.parseContent = this.parseSerializedObject; scene = this.getScene(); if (!scene || !this._texture) { return; } this.applyYInversionOnUpdate = invertY; this._rootElement = scene.getEngine().getInputElement(); const adtOptions = widthOrOptions; this.useStandalone = !!adtOptions?.useStandalone; if (!this.useStandalone) { this._renderObserver = scene.onBeforeCameraRenderObservable.add((camera) => this._checkUpdate(camera)); } /** Whenever a control is added or removed to the root, we have to recheck the camera projection as it can have changed */ this._controlAddedObserver = this._rootContainer.onControlAddedObservable.add((control) => { if (control) { this._rootChildrenHaveChanged = true; } }); this._controlRemovedObserver = this._rootContainer.onControlRemovedObservable.add((control) => { if (control) { this._rootChildrenHaveChanged = true; } }); this._preKeyboardObserver = scene.onPreKeyboardObservable.add((info) => { // check if tab is pressed if (!this.disableTabNavigation && info.type === KeyboardEventTypes.KEYDOWN && info.event.code === "Tab") { const forward = !info.event.shiftKey; if ((forward && this._focusProperties.index === this._focusProperties.total - 1) || (!forward && this._focusProperties.index === 0 && this._focusProperties.total > 0)) { this.focusedControl = null; this._focusProperties.index = 0; this._focusProperties.total = -1; return; } this._focusNextElement(forward); info.event.preventDefault(); return; } if (!this._focusedControl) { return; } if (info.type === KeyboardEventTypes.KEYDOWN) { this._focusedControl.processKeyboard(info.event); } info.skipOnPointerObservable = true; }); this._rootContainer._link(this); this.hasAlpha = true; if (!width || !height) { this._resizeObserver = scene.getEngine().onResizeObservable.add(() => this._onResize()); this._onResize(); } this._texture.isReady = true; } /** * Get the current class name of the texture useful for serialization or dynamic coding. * @returns "AdvancedDynamicTexture" */ getClassName() { return "AdvancedDynamicTexture"; } /** * Function used to execute a function on all controls * @param func defines the function to execute * @param container defines the container where controls belong. If null the root container will be used */ executeOnAllControls(func, container) { if (!container) { container = this._rootContainer; } func(container); for (const child of container.children) { if (child.children) { this.executeOnAllControls(func, child); continue; } func(child); } } /** * Gets or sets a boolean indicating if the InvalidateRect optimization should be turned on */ get useInvalidateRectOptimization() { return this._useInvalidateRectOptimization; } set useInvalidateRectOptimization(value) { this._useInvalidateRectOptimization = value; } /** * Invalidates a rectangle area on the gui texture * @param invalidMinX left most position of the rectangle to invalidate in the texture * @param invalidMinY top most position of the rectangle to invalidate in the texture * @param invalidMaxX right most position of the rectangle to invalidate in the texture * @param invalidMaxY bottom most position of the rectangle to invalidate in the texture */ invalidateRect(invalidMinX, invalidMinY, invalidMaxX, invalidMaxY) { if (!this._useInvalidateRectOptimization) { return; } if (!this._invalidatedRectangle) { this._invalidatedRectangle = new Measure(invalidMinX, invalidMinY, invalidMaxX - invalidMinX + 1, invalidMaxY - invalidMinY + 1); } else { // Compute intersection const maxX = Math.ceil(Math.max(this._invalidatedRectangle.left + this._invalidatedRectangle.width - 1, invalidMaxX)); const maxY = Math.ceil(Math.max(this._invalidatedRectangle.top + this._invalidatedRectangle.height - 1, invalidMaxY)); this._invalidatedRectangle.left = Math.floor(Math.min(this._invalidatedRectangle.left, invalidMinX)); this._invalidatedRectangle.top = Math.floor(Math.min(this._invalidatedRectangle.top, invalidMinY)); this._invalidatedRectangle.width = maxX - this._invalidatedRectangle.left + 1; this._invalidatedRectangle.height = maxY - this._invalidatedRectangle.top + 1; } } /** * Marks the texture as dirty forcing a complete update */ markAsDirty() { this._isDirty = true; } /** * Helper function used to create a new style * @returns a new style * @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui#styles */ createStyle() { return new Style(this); } /** * Adds a new control to the root container * @param control defines the control to add * @returns the current texture */ addControl(control) { this._rootContainer.addControl(control); return this; } /** * Removes a control from the root container * @param control defines the control to remove * @returns the current texture */ removeControl(control) { this._rootContainer.removeControl(control); return this; } /** * Moves overlapped controls towards a position where it is not overlapping anymore. * Please note that this method alters linkOffsetXInPixels and linkOffsetYInPixels. * @param overlapGroup the overlap group which will be processed or undefined to process all overlap groups * @param deltaStep the step size (speed) to reach the target non overlapping position (default 0.1) * @param repelFactor how much is the control repelled by other controls */ moveToNonOverlappedPosition(overlapGroup, deltaStep = 1, repelFactor = 1) { let controlsForGroup; if (Array.isArray(overlapGroup)) { controlsForGroup = overlapGroup; } else { const descendants = this.getDescendants(true); // get only the controls with an overlapGroup property set // if the overlapGroup parameter is set, filter the controls and get only the controls belonging to that overlapGroup controlsForGroup = overlapGroup === undefined ? descendants.filter((c) => c.overlapGroup !== undefined) : descendants.filter((c) => c.overlapGroup === overlapGroup); } for (const control1 of controlsForGroup) { let velocity = Vector2.Zero(); const center = new Vector2(control1.centerX, control1.centerY); for (const control2 of controlsForGroup) { if (control1 !== control2 && AdvancedDynamicTexture._Overlaps(control1, control2)) { // if the two controls overlaps get a direction vector from one control's center to another control's center const diff = center.subtract(new Vector2(control2.centerX, control2.centerY)); const diffLength = diff.length(); if (diffLength > 0) { // calculate the velocity velocity = velocity.add(diff.normalize().scale(repelFactor / diffLength)); } } } if (velocity.length() > 0) { // move the control along the direction vector away from the overlapping control velocity = velocity.normalize().scale(deltaStep * (control1.overlapDeltaMultiplier ?? 1)); control1.linkOffsetXInPixels += velocity.x; control1.linkOffsetYInPixels += velocity.y; } } } /** * Release all resources */ dispose() { const scene = this.getScene(); if (!scene) { return; } this._rootElement = null; scene.onBeforeCameraRenderObservable.remove(this._renderObserver); if (this._resizeObserver) { scene.getEngine().onResizeObservable.remove(this._resizeObserver); } if (this._prePointerObserver) { scene.onPrePointerObservable.remove(this._prePointerObserver); } if (this._sceneRenderObserver) { scene.onBeforeRenderObservable.remove(this._sceneRenderObserver); } if (this._pointerObserver) { scene.onPointerObservable.remove(this._pointerObserver); } if (this._preKeyboardObserver) { scene.onPreKeyboardObservable.remove(this._preKeyboardObserver); } if (this._canvasPointerOutObserver) { scene.getEngine().onCanvasPointerOutObservable.remove(this._canvasPointerOutObserver); } if (this._canvasBlurObserver) { scene.getEngine().onCanvasBlurObservable.remove(this._canvasBlurObserver); } if (this._controlAddedObserver) { this._rootContainer.onControlAddedObservable.remove(this._controlAddedObserver); } if (this._controlRemovedObserver) { this._rootContainer.onControlRemovedObservable.remove(this._controlRemovedObserver); } if (this._layerToDispose) { this._layerToDispose.texture = null; this._layerToDispose.dispose(); this._layerToDispose = null; } this._rootContainer.dispose(); this.onClipboardObservable.clear(); this.onControlPickedObservable.clear(); this.onBeginRenderObservable.clear(); this.onEndRenderObservable.clear(); this.onBeginLayoutObservable.clear(); this.onEndLayoutObservable.clear(); this.onGuiReadyObservable.clear(); super.dispose(); } _onResize() { const scene = this.getScene(); if (!scene) { return; } // Check size const engine = scene.getEngine(); if (this.adjustToEngineHardwareScalingLevel) { // force the renderScale to the engine's hardware scaling level this._renderScale = engine.getHardwareScalingLevel(); // calculate the max renderScale, based on the max texture size of engine.getCaps().maxTextureSize (enforced by some mobile devices) this._renderScale = 1 / Math.max(this._renderScale, engine.getRenderWidth() / engine.getCaps().maxTextureSize, engine.getRenderHeight() / engine.getCaps().maxTextureSize); } const textureSize = this.getSize(); let renderWidth = engine.getRenderWidth() * this._renderScale; let renderHeight = engine.getRenderHeight() * this._renderScale; if (this._renderAtIdealSize) { if (this._idealWidth) { renderHeight = (renderHeight * this._idealWidth) / renderWidth; renderWidth = this._idealWidth; } else if (this._idealHeight) { renderWidth = (renderWidth * this._idealHeight) / renderHeight; renderHeight = this._idealHeight; } } if (textureSize.width !== renderWidth || textureSize.height !== renderHeight) { this.scaleTo(renderWidth, renderHeight); if (this.adjustToEngineHardwareScalingLevel) { const engineRenderScale = 1 / engine.getHardwareScalingLevel(); const scale = this._renderScale * engineRenderScale; this._rootContainer.scaleX = scale; this._rootContainer.scaleY = scale; this._rootContainer.widthInPixels = renderWidth / scale; this._rootContainer.heightInPixels = renderHeight / scale; } this.markAsDirty(); if (this._idealWidth || this._idealHeight) { this._rootContainer._markAllAsDirty(); } if (!this._alreadyRegisteredForRender) { this._alreadyRegisteredForRender = true; Tools.SetImmediate(() => { // We want to force an update so the texture can be set as ready this.update(this.applyYInversionOnUpdate, this.premulAlpha, AdvancedDynamicTexture.AllowGPUOptimizations); this._alreadyRegisteredForRender = false; }); } } this.invalidateRect(0, 0, textureSize.width - 1, textureSize.height - 1); } /** @internal */ _getGlobalViewport() { const size = this.getSize(); const globalViewPort = this._fullscreenViewport.toGlobal(size.width, size.height); const targetX = Math.round(globalViewPort.width / this._rootContainer.scaleX); const targetY = Math.round(globalViewPort.height / this._rootContainer.scaleY); const scale = this._adjustToEngineHardwareScalingLevel ? this._renderScale / (this.getScene()?.getEngine().getHardwareScalingLevel() || 1) : 1; globalViewPort.x += (globalViewPort.width / scale - targetX) / 2; globalViewPort.y += (globalViewPort.height / scale - targetY) / 2; globalViewPort.width = targetX; globalViewPort.height = targetY; return globalViewPort; } /** * Get screen coordinates for a vector3 * @param position defines the position to project * @param worldMatrix defines the world matrix to use * @returns the projected position */ getProjectedPosition(position, worldMatrix) { const result = this.getProjectedPositionWithZ(position, worldMatrix); return new Vector2(result.x, result.y); } /** * Get screen coordinates for a vector3 * @param position defines the position to project * @param worldMatrix defines the world matrix to use * @returns the projected position with Z */ getProjectedPositionWithZ(position, worldMatrix) { const scene = this.getScene(); if (!scene) { return Vector3.Zero(); } const globalViewport = this._getGlobalViewport(); const projectedPosition = Vector3.Project(position, worldMatrix, scene.getTransformMatrix(), globalViewport); return new Vector3(projectedPosition.x, projectedPosition.y, projectedPosition.z); } /** @internal */ _checkUpdate(camera, skipUpdate) { if (this._layerToDispose && camera) { if ((camera.layerMask & this._layerToDispose.layerMask) === 0) { return; } } if (this._isFullscreen && this._linkedControls.length) { const scene = this.getScene(); if (!scene) { return; } const globalViewport = this._getGlobalViewport(); for (const control of this._linkedControls) { if (!control.isVisible) { continue; } const mesh = control._linkedMesh; if (!mesh || mesh.isDisposed()) { Tools.SetImmediate(() => { control.linkWithMesh(null); }); continue; } const position = mesh.getBoundingInfo ? mesh.getBoundingInfo().boundingSphere.center : Vector3.ZeroReadOnly; const projectedPosition = Vector3.Project(position, mesh.getWorldMatrix(), scene.getTransformMatrix(), globalViewport); if (projectedPosition.z < 0 || projectedPosition.z > 1) { control.notRenderable = true; continue; } control.notRenderable = false; if (this.useInvalidateRectOptimization) { control.invalidateRect(); } control._moveToProjectedPosition(projectedPosition); } } if (!this._isDirty && !this._rootContainer.isDirty) { return; } this._isDirty = false; this._render(skipUpdate); if (!skipUpdate) { this.update(this.applyYInversionOnUpdate, this.premulAlpha, AdvancedDynamicTexture.AllowGPUOptimizations); } } _render(skipRender) { const textureSize = this.getSize(); const renderWidth = textureSize.width; const renderHeight = textureSize.height; const context = this.getContext(); context.font = "18px Arial"; context.strokeStyle = "white"; if (this.onGuiReadyObservable.hasObservers()) { this._checkGuiIsReady(); } /** We have to recheck the camera projection in the case the root control's children have changed */ if (this._rootChildrenHaveChanged) { const camera = this.getScene()?.activeCamera; if (camera) { this._rootChildrenHaveChanged = false; this._checkUpdate(camera, true); } } // Layout this.onBeginLayoutObservable.notifyObservers(this); const measure = new Measure(0, 0, renderWidth, renderHeight); this._numLayoutCalls = 0; this._rootContainer._layout(measure, context); this.onEndLayoutObservable.notifyObservers(this); this._isDirty = false; // Restoring the dirty state that could have been set by controls during layout processing if (skipRender) { return; } // Clear if (this._invalidatedRectangle) { this._clearMeasure.copyFrom(this._invalidatedRectangle); } else { this._clearMeasure.copyFromFloats(0, 0, renderWidth, renderHeight); } context.clearRect(this._clearMeasure.left, this._clearMeasure.top, this._clearMeasure.width, this._clearMeasure.height); if (this._background) { context.save(); context.fillStyle = this._background; context.fillRect(this._clearMeasure.left, this._clearMeasure.top, this._clearMeasure.width, this._clearMeasure.height); context.restore(); } // Render this.onBeginRenderObservable.notifyObservers(this); this._numRenderCalls = 0; this._rootContainer._render(context, this._invalidatedRectangle); this.onEndRenderObservable.notifyObservers(this); this._invalidatedRectangle = null; } /** * @internal */ _changeCursor(cursor) { if (this._rootElement) { this._rootElement.style.cursor = cursor; this._cursorChanged = true; } } /** * @internal */ _registerLastControlDown(control, pointerId) { this._lastControlDown[pointerId] = control; this.onControlPickedObservable.notifyObservers(control); } _doPicking(x, y, pi, type, pointerId, buttonIndex, deltaX, deltaY) { const scene = this.getScene(); if (!scene || this.disablePicking) { return; } const engine = scene.getEngine(); const textureSize = this.getSize(); if (this._isFullscreen) { const camera = scene.cameraToUseForPointers || scene.activeCamera; if (!camera) { return; } const viewport = camera.viewport; x = x * (textureSize.width / (engine.getRenderWidth() * viewport.width)); y = y * (textureSize.height / (engine.getRenderHeight() * viewport.height)); } if (this._capturingControl[pointerId]) { if (this._capturingControl[pointerId].isPointerBlocker) { this._shouldBlockPointer = true; } this._capturingControl[pointerId]._processObservables(type, x, y, pi, pointerId, buttonIndex); return; } this._cursorChanged = false; if (!this._rootContainer._processPicking(x, y, pi, type, pointerId, buttonIndex, deltaX, deltaY)) { if (!scene.doNotHandleCursors) { this._changeCursor(""); } if (type === PointerEventTypes.POINTERMOVE) { if (this._lastControlOver[pointerId]) { this._lastControlOver[pointerId]._onPointerOut(this._lastControlOver[pointerId], pi); delete this._lastControlOver[pointerId]; } } } if (!this._cursorChanged && !scene.doNotHandleCursors) { this._changeCursor(""); } this._manageFocus(); } /** * @internal */ _cleanControlAfterRemovalFromList(list, control) { for (const pointerId in list) { if (!Object.prototype.hasOwnProperty.call(list, pointerId)) { continue; } const lastControlOver = list[pointerId]; if (lastControlOver === control) { delete list[pointerId]; } } } /** * @internal */ _cleanControlAfterRemoval(control) { this._cleanControlAfterRemovalFromList(this._lastControlDown, control); this._cleanControlAfterRemovalFromList(this._lastControlOver, control); } /** * This function will run a pointer event on this ADT and will trigger any pointer events on any controls * This will work on a fullscreen ADT only. For mesh based ADT, simulate pointer events using the scene directly. * @param x pointer X on the canvas for the picking * @param y pointer Y on the canvas for the picking * @param pi optional pointer information */ pick(x, y, pi = null) { if (this._isFullscreen && this._scene) { this._translateToPicking(this._scene, new Viewport(0, 0, 0, 0), pi, x, y); } } _translateToPicking(scene, tempViewport, pi, x = scene.pointerX, y = scene.pointerY) { const camera = scene.cameraToUseForPointers || scene.activeCamera; const engine = scene.getEngine(); const originalCameraToUseForPointers = scene.cameraToUseForPointers; if (!camera) { tempViewport.x = 0; tempViewport.y = 0; tempViewport.width = engine.getRenderWidth(); tempViewport.height = engine.getRenderHeight(); } else { if (camera.rigCameras.length) { // rig camera - we need to find the camera to use for this event const rigViewport = new Viewport(0, 0, 1, 1); for (const rigCamera of camera.rigCameras) { // generate the viewport of this camera rigCamera.viewport.toGlobalToRef(engine.getRenderWidth(), engine.getRenderHeight(), rigViewport); const transformedX = x / engine.getHardwareScalingLevel() - rigViewport.x; const transformedY = y / engine.getHardwareScalingLevel() - (engine.getRenderHeight() - rigViewport.y - rigViewport.height); // check if the pointer is in the camera's viewport if (transformedX < 0 || transformedY < 0 || x > rigViewport.width || y > rigViewport.height) { // out of viewport - don't use this camera return; } // set the camera to use for pointers until this pointer loop is over scene.cameraToUseForPointers = rigCamera; // set the viewport tempViewport.x = rigViewport.x; tempViewport.y = rigViewport.y; tempViewport.width = rigViewport.width; tempViewport.height = rigViewport.height; } } else { camera.viewport.toGlobalToRef(engine.getRenderWidth(), engine.getRenderHeight(), tempViewport); } } const transformedX = x / engine.getHardwareScalingLevel() - tempViewport.x; const transformedY = y / engine.getHardwareScalingLevel() - (engine.getRenderHeight() - tempViewport.y - tempViewport.height); this._shouldBlockPointer = false; // Do picking modifies _shouldBlockPointer if (pi) { const pointerId = pi.event.pointerId || this._defaultMousePointerId; this._doPicking(transformedX, transformedY, pi, pi.type, pointerId, pi.event.button, pi.event.deltaX, pi.event.deltaY); // Avoid overwriting a true skipOnPointerObservable to false if ((this._shouldBlockPointer && !(pi.type & this.skipBlockEvents)) || this._capturingControl[pointerId]) { pi.skipOnPointerObservable = true; } } else { this._doPicking(transformedX, transformedY, null, PointerEventTypes.POINTERMOVE, this._defaultMousePointerId, 0); } // if overridden by a rig camera - reset back to the original value scene.cameraToUseForPointers = originalCameraToUseForPointers; } /** Attach to all scene events required to support pointer events */ attach() { const scene = this.getScene(); if (!scene) { return; } const tempViewport = new Viewport(0, 0, 0, 0); this._prePointerObserver = scene.onPrePointerObservable.add((pi) => { if (scene.isPointerCaptured(pi.event.pointerId) && pi.type === PointerEventTypes.POINTERUP && !this._capturedPointerIds.has(pi.event.pointerId)) { return; } if (pi.type !== PointerEventTypes.POINTERMOVE && pi.type !== PointerEventTypes.POINTERUP && pi.type !== PointerEventTypes.POINTERDOWN && pi.type !== PointerEventTypes.POINTERWHEEL && pi.type !== PointerEventTypes.POINTERTAP) { return; } if (pi.type === PointerEventTypes.POINTERMOVE) { // Avoid pointerMove events firing while the pointer is captured by the scene if (scene.isPointerCaptured(pi.event.pointerId)) { return; } if (pi.event.pointerId) { this._defaultMousePointerId = pi.event.pointerId; // This is required to make sure we have the correct pointer ID for wheel } } this._translateToPicking(scene, tempViewport, pi); }); this._attachPickingToSceneRender(scene, () => this._translateToPicking(scene, tempViewport, null), false); this._attachToOnPointerOut(scene); this._attachToOnBlur(scene); } _focusNextElement(forward = true) { // generate the order of tab-able controls const sortedTabbableControls = []; this.executeOnAllControls((control) => { if (control.isFocusInvisible || !control.isVisible || control.tabIndex < 0) { return; } sortedTabbableControls.push(control); }); // if no control is tab-able, return if (sortedTabbableControls.length === 0) { return; } sortedTabbableControls.sort((a, b) => { // if tabIndex is 0, put it in the end of the list, otherwise sort by tabIndex return a.tabIndex === 0 ? 1 : b.tabIndex === 0 ? -1 : a.tabIndex - b.tabIndex; }); this._focusProperties.total = sortedTabbableControls.length; // if no control is focused, focus the first one let nextIndex = -1; if (!this._focusedControl) { nextIndex = forward ? 0 : sortedTabbableControls.length - 1; } else { const currentIndex = sortedTabbableControls.indexOf(this._focusedControl); nextIndex = currentIndex + (forward ? 1 : -1); if (nextIndex < 0) { nextIndex = sortedTabbableControls.length - 1; } else if (nextIndex >= sortedTabbableControls.length) { nextIndex = 0; } } sortedTabbableControls[nextIndex].focus(); this._focusProperties.index = nextIndex; } /** * Register the clipboard Events onto the canvas */ registerClipboardEvents() { self.addEventListener("copy", this._onClipboardCopy, false); self.addEventListener("cut", this._onClipboardCut, false); self.addEventListener("paste", this._onClipboardPaste, false); } /** * Unregister the clipboard Events from the canvas */ unRegisterClipboardEvents() { self.removeEventListener("copy", this._onClipboardCopy); self.removeEventListener("cut", this._onClipboardCut); self.removeEventListener("paste", this._onClipboardPaste); } /** * Transform uvs from mesh space to texture space, taking the texture into account * @param uv the uvs in mesh space * @returns the uvs in texture space */ _transformUvs(uv) { const textureMatrix = this.getTextureMatrix(); let result; if (textureMatrix.isIdentityAs3x2()) { result = uv; } else { const homogeneousTextureMatrix = TmpVectors.Matrix[0]; textureMatrix.getRowToRef(0, TmpVectors.Vector4[0]); textureMatrix.getRowToRef(1, TmpVectors.Vector4[1]); textureMatrix.getRowToRef(2, TmpVectors.Vector4[2]); const r0 = TmpVectors.Vector4[0]; const r1 = TmpVectors.Vector4[1]; const r2 = TmpVectors.Vector4[2]; homogeneousTextureMatrix.setRowFromFloats(0, r0.x, r0.y, 0, 0); homogeneousTextureMatrix.setRowFromFloats(1, r1.x, r1.y, 0, 0); homogeneousTextureMatrix.setRowFromFloats(2, 0, 0, 1, 0); homogeneousTextureMatrix.setRowFromFloats(3, r2.x, r2.y, 0, 1); result = TmpVectors.Vector2[0]; Vector2.TransformToRef(uv, homogeneousTextureMatrix, result); } // In wrap and mirror mode, the texture coordinate for coordinates more than 1 is the fractional part of the coordinate if (this.wrapU === Texture.WRAP_ADDRESSMODE || this.wrapU === Texture.MIRROR_ADDRESSMODE) { if (result.x > 1) { let fX = result.x - Math.trunc(result.x); // In mirror mode, the sign of the texture coordinate depends on the integer part - // odd integers means it is mirrored from the original coordinate if (this.wrapU === Texture.MIRROR_ADDRESSMODE && Math.trunc(result.x) % 2 === 1) { fX = 1 - fX; } result.x = fX; } } if (this.wrapV === Texture.WRAP_ADDRESSMODE || this.wrapV === Texture.MIRROR_ADDRESSMODE) { if (result.y > 1) { let fY = result.y - Math.trunc(result.y); if (this.wrapV === Texture.MIRROR_ADDRESSMODE && Math.trunc(result.x) % 2 === 1) { fY = 1 - fY; } result.y = fY; } } return result; } /** * Connect the texture to a hosting mesh to enable interactions * @param mesh defines the mesh to attach to * @param supportPointerMove defines a boolean indicating if pointer move events must be catched as well */ attachToMesh(mesh, supportPointerMove = true) { const scene = this.getScene(); if (!scene) { return; } if (this._pointerObserver) { scene.onPointerObservable.remove(this._pointerObserver); } this._pointerObserver = scene.onPointerObservable.add((pi) => { if (pi.type !== PointerEventTypes.POINTERMOVE && pi.type !== PointerEventTypes.POINTERUP && pi.type !== PointerEventTypes.POINTERDOWN && pi.type !== PointerEventTypes.POINTERWHEEL) { return; } if (pi.type === PointerEventTypes.POINTERMOVE && pi.event.pointerId) { this._defaultMousePointerId = pi.event.pointerId; // This is required to make sure we have the correct pointer ID for wheel } const pointerId = pi.event.pointerId || this._defaultMousePointerId; if (pi.pickInfo && pi.pickInfo.hit && pi.pickInfo.pickedMesh === mesh) { let uv = pi.pickInfo.getTextureCoordinates(); if (uv) { uv = this._transformUvs(uv); const size = this.getSize(); this._doPicking(uv.x * size.width, (this.applyYInversionOnUpdate ? 1.0 - uv.y : uv.y) * size.height, pi, pi.type, pointerId, pi.event.button, pi.event.deltaX, pi.event.deltaY); } } else if (pi.type === PointerEventTypes.POINTERUP) { if (this._lastControlDown[pointerId]) { this._lastControlDown[pointerId]._forcePointerUp(pointerId); } delete this._lastControlDown[pointerId]; if (this.focusedControl) { const friendlyControls = this.focusedControl.keepsFocusWith(); let canMoveFocus = true; if (friendlyControls) { for (const control of friendlyControls) { // Same host, no need to keep the focus if (this === control._host) { continue; } // Different hosts const otherHost = control._host; if (otherHost._lastControlOver[pointerId] && otherHost._lastControlOver[pointerId].isAscendant(control)) { canMoveFocus = false; break; }