UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

204 lines (167 loc) • 7.24 kB
// import { Canvas } from './Canvas.js'; import { AxesHelper, Object3D } from 'three'; import * as ThreeMeshUI from 'three-mesh-ui'; import { showGizmos } from '../../engine/engine_default_parameters.js'; import { ComponentInit } from '../../engine/engine_types.js'; import { getParam } from '../../engine/engine_utils.js'; import { Behaviour, GameObject } from "../Component.js"; import { EventSystem } from "./EventSystem.js"; import type { ICanvas } from './Interfaces.js'; import { $shadowDomOwner } from './Symbols.js'; export const includesDir = "./include"; const debug = getParam("debugshadowcomponents"); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy ThreeMeshUI.Block.prototype["interactable"] = { get() { return this.interactive; }, set(value) { this.interactable = value; } } /** * Derive from this class if you want to implement your own UI components. * It provides utility methods and simplifies managing the underlying three-mesh-ui hierarchy. * @category User Interface * @group Components */ export class BaseUIComponent extends Behaviour { /** Is this object on the root of the UI hierarchy ? */ isRoot() { return this.Root?.gameObject === this.gameObject; } /** Access the parent canvas component */ get canvas() { const cv = this.Root as any as ICanvas; if (cv?.isCanvas) return cv; return null; } /** @deprecated use `canvas` */ protected get Canvas() { return this.canvas; } /** Mark the UI dirty which will trigger an THREE-Mesh-UI update */ markDirty() { EventSystem.markUIDirty(this.context); } /** the underlying three-mesh-ui */ get shadowComponent() { return this._shadowComponent } private set shadowComponent(val: Object3D | null) { this._shadowComponent = val; } private _shadowComponent: Object3D | null = null; private _controlsChildLayout = true; get controlsChildLayout(): boolean { return this._controlsChildLayout; } set controlsChildLayout(val: boolean) { this._controlsChildLayout = val; if (this.shadowComponent) { //@ts-ignore (this.shadowComponent as ThreeMeshUI.MeshUIComponent).autoLayout = val; } } private _root?: UIRootComponent | null = undefined; protected get Root(): UIRootComponent | null { if (this._root === undefined) { this._root = GameObject.getComponentInParent(this.gameObject, UIRootComponent); } return this._root; } // private _intermediate?: Object3D; protected _parentComponent?: BaseUIComponent | null = undefined; __internalNewInstanceCreated(args: ComponentInit<this>) { super.__internalNewInstanceCreated(args); this.shadowComponent = null; this._root = undefined; this._parentComponent = undefined; return this; } onEnable() { super.onEnable(); } /** Add a three-mesh-ui object to the UI hierarchy * @param container the three-mesh-ui object to add * @param parent the parent component to add the object to */ protected addShadowComponent(container: any, parent?: BaseUIComponent) { if (!container) return; this.removeShadowComponent(); // instead of inserting here, we attach to the matching shadow hierarchy starting with the Canvas component. const searchFrom = this.isRoot() ? this.gameObject : this.gameObject.parent; this._parentComponent = GameObject.getComponentInParent(searchFrom!, BaseUIComponent); if (!this._parentComponent) { console.warn(`Component \"${this.name}\" doesn't have a UI parent anywhere. Do you have an UI element outside a Canvas? UI components must be a child of a Canvas component`, this); return; } container.name = this.name + " (" + (this.constructor.name ?? "UI") + ")"; container.autoLayout = this._parentComponent.controlsChildLayout; container[$shadowDomOwner] = this; // TODO: raycastTarget doesnt work anymore -> i think we need to set the gameObject layer and then check in the raycaster if the shadowComponentOwner is on the correct layer?! // const raycastTarget = (this as unknown as IGraphic).raycastTarget; // this.gameObject.layers.set(2) this.setShadowComponentOwner(container); let needsUpdate = false; if (this.Root?.gameObject === this.gameObject) { this.gameObject.add(container); } else { const targetShadowComponent = this._parentComponent.shadowComponent; if (targetShadowComponent) { // console.log("ADD", this.name, "to", this._parentComponent.name, targetShadowComponent); targetShadowComponent?.add(container); needsUpdate = true; } } this.shadowComponent = container; if (parent && parent.shadowComponent && this.shadowComponent) { parent.shadowComponent.add(this.shadowComponent); } // this.applyTransform(); if (showGizmos) { container.add(new AxesHelper(.5)); } this.onAfterAddedToScene(); // make sure to update the layout when adding content // otherwise it will fail when object are enabled at runtime if (needsUpdate) ThreeMeshUI.update(); if (debug) console.warn("Added shadow component", this.shadowComponent); } protected setShadowComponentOwner(current: ThreeMeshUI.MeshUIBaseElement | Object3D | null | undefined) { if (!current) return; // TODO: only traverse our own hierarchy, we can stop if we find another owner if (current[$shadowDomOwner] === undefined || current[$shadowDomOwner] === this) { current[$shadowDomOwner] = this; if (current.children) { for (const ch of current.children) { this.setShadowComponentOwner(ch); } } } } private traverseOwnedShadowComponents(current: Object3D, owner: any, callback: (obj: any) => void) { if (!current) return; if (current[$shadowDomOwner] === owner) { callback(current); for (const ch of current.children) { this.traverseOwnedShadowComponents(ch, owner, callback); } } } /** Remove the underlying UI object from the hierarchy */ protected removeShadowComponent() { if (this.shadowComponent) { this.shadowComponent.removeFromParent(); } } protected onAfterAddedToScene() { } setInteractable(value: boolean) { if (this.shadowComponent) { //@ts-ignore this.shadowComponent.interactable = value; } } } export class UIRootComponent extends BaseUIComponent { awake() { super.awake(); } }