@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
text/typescript
// 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();
}
}