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.

276 lines (239 loc) • 10.3 kB
import { Color, LinearSRGBColorSpace, Object3D, SRGBColorSpace, Texture } from 'three'; import * as ThreeMeshUI from 'three-mesh-ui' import type { Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js'; import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior.js" import { serializable } from '../../engine/engine_serialization_decorator.js'; import { ComponentInit } from '../../engine/engine_types.js'; import { NEEDLE_progressive } from '../../engine/extensions/NEEDLE_progressive.js'; import { RGBAColor } from "../../engine/js-extensions/index.js" import { GameObject } from '../Component.js'; import { BaseUIComponent } from "./BaseUIComponent.js"; import { type IGraphic, type IRectTransformChangedReceiver } from './Interfaces.js'; import { Outline } from './Outline.js'; import { RectTransform } from './RectTransform.js'; import { onChange, scheduleAction } from "./Utils.js" const _colorStateObject: { backgroundColor: Color, backgroundOpacity: number, borderColor: Color, borderOpacity: number } = { backgroundColor: new Color(1, 1, 1), backgroundOpacity: 1, borderColor: new Color(1, 1, 1), borderOpacity: 1, }; /** * @category User Interface * @group Components */ export class Graphic extends BaseUIComponent implements IGraphic, IRectTransformChangedReceiver { get isGraphic() { return true; } @serializable(RGBAColor) get color(): RGBAColor { if (!this._color) this._color = new RGBAColor(1, 1, 1, 1); return this._color; } set color(col: RGBAColor) { const changed = !this._color || this._color.r !== col.r || this._color.g !== col.g || this._color.b !== col.b || this._color.alpha !== col.alpha; if (!changed) return; if (!this._color) { this._color = new RGBAColor(1, 1, 1, 1); } this._color.copy(col); this.onColorChanged(); } private _alphaFactor: number = 1; setAlphaFactor(factor: number) { this._alphaFactor = factor; this.onColorChanged(); } get alphaFactor() { return this._alphaFactor; } private sRGBColor: Color = new Color(1, 0, 1); protected onColorChanged() { if (this.uiObject) { this.sRGBColor.copy(this._color); this.sRGBColor.convertLinearToSRGB(); _colorStateObject.backgroundColor = this.sRGBColor; _colorStateObject.backgroundOpacity = this._color.alpha * this._alphaFactor; this.applyEffects(_colorStateObject, this._alphaFactor); this.uiObject.set(_colorStateObject); this.markDirty(); } } // used via animations private get m_Color() { return this._color; } @serializable() raycastTarget: boolean = true; protected uiObject: ThreeMeshUI.Block | null = null; private _color: RGBAColor = null!; private _rect: RectTransform | null = null; private _stateManager: SimpleStateBehavior | null = null; protected get rectTransform(): RectTransform { if (!this._rect) { this._rect = GameObject.getComponent(this.gameObject, RectTransform); } if (!this._rect) throw new Error("Not Supported: Make sure to add a RectTransform component before adding a UI Graphic component."); return this._rect!; } onParentRectTransformChanged() { this.uiObject?.set({ width: this.rectTransform.width, height: this.rectTransform.height }) this.markDirty(); } __internalNewInstanceCreated(init: ComponentInit<this>): this { super.__internalNewInstanceCreated(init); this._rect = null; this.uiObject = null; this._stateManager = null; if (this._color) this._color = this._color.clone(); return this; } setState(state: string) { this.makePanel(); if (this.uiObject) { //@ts-ignore this.uiObject.setState(state); this?.markDirty(); } } setupState(state: object) { this.makePanel(); if (this.uiObject) { // @marwie : v7.x now have a concurrent state management in core mimicking html/css // ie : (::firstChild::hover::disabled) where firstchild, hover and disabled are all on different channels // In order to keep needle Raycaster and EventSystem intact, I added in v7 a SimpleStateBehavior, which acts as previously if (!this._stateManager) this._stateManager = new SimpleStateBehavior(this.uiObject); //@ts-ignore this.uiObject.setupState(state.state, state.attributes); } } setOptions(opts: Options) { this.makePanel(); if (this.uiObject) { //@ts-ignore this.uiObject.set(opts); // if (opts["backgroundColor"] !== undefined || opts["backgroundOpacity"] !== undefined) // this.uiObject["updateBackgroundMaterial"]?.call(this.uiObject); } } awake() { super.awake(); this.makePanel(); // when _color is written to onChange(this, "_color", () => scheduleAction(this, this.onColorChanged)); } onEnable(): void { super.onEnable(); if (this.uiObject) { this.rectTransform.shadowComponent?.add(this.uiObject as unknown as Object3D); this.addShadowComponent(this.uiObject, this.rectTransform); } } onDisable(): void { super.onDisable(); if (this.uiObject) this.removeShadowComponent(); } private _currentlyCreatingPanel: boolean = false; protected makePanel() { if (this.uiObject) return; if (this._currentlyCreatingPanel) return; this._currentlyCreatingPanel = true; const offset = .015; // if (this.Root) offset = .02 * (1 / this.Root.gameObject.scale.z); const opts = { backgroundColor: this.color, backgroundOpacity: this.color.alpha, offset: offset, // without a tiny offset we get z fighting }; this.onBeforeCreate(opts); this.applyEffects(opts); this.onCreate(opts); this.controlsChildLayout = false; this._currentlyCreatingPanel = false; this.onAfterCreated(); this.onColorChanged(); } protected onBeforeCreate(_opts: any) { } protected onCreate(opts: any) { this.uiObject = this.rectTransform.createNewBlock(opts); this.uiObject.name = this.name; } protected onAfterCreated() { } private applyEffects(opts, alpha: number = 1) { const outline = this.gameObject?.getComponent(Outline); if (outline) { if (outline.effectDistance) opts.borderWidth = Math.max(Math.abs(outline.effectDistance.x), Math.abs(outline.effectDistance.y)); if (outline.effectColor) { opts.borderColor = outline.effectColor; opts.borderOpacity = outline.effectColor.alpha * alpha; } } } /** used internally to ensure textures assigned to UI use linear encoding */ static textureCache: Map<Texture, Texture> = new Map(); protected async setTexture(tex: Texture | null | undefined) { this.setOptions({ backgroundOpacity: 0 }); if (tex) { // workaround for https://github.com/needle-tools/needle-engine-support/issues/109 // if (tex.colorSpace === SRGBColorSpace || !tex.colorSpace || true) { if (Graphic.textureCache.has(tex)) { tex = Graphic.textureCache.get(tex)!; } else { if (tex.isRenderTargetTexture) { // we can not clone the texture if it's a render target // otherwise it won't be updated anymore in the UI // TODO: below maskable graphic is flipped but settings a rendertexture results in the texture being upside down. // we should remove the flip below (scale.y *= -1) but this needs to be tested with all UI components } else { const clone = tex.clone(); clone.colorSpace = LinearSRGBColorSpace; Graphic.textureCache.set(tex, clone); tex = clone; } } // } this.setOptions({ backgroundImage: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" }); NEEDLE_progressive.assignTextureLOD(tex, 0).then(res => { if (res instanceof Texture) { if (tex) Graphic.textureCache.set(tex, res); this.setOptions({ backgroundImage: res }); this.markDirty(); } }); } else { this.setOptions({ backgroundImage: undefined, borderRadius: 0, backgroundOpacity: this.color.alpha }); } this.markDirty(); } protected onAfterAddedToScene(): void { super.onAfterAddedToScene(); if (this.shadowComponent) { // @TODO: I think we dont even need this anymore and this leads to the offset being applied twice //@ts-ignore this.shadowComponent.offset = this.shadowComponent.position.z; // console.log(this.shadowComponent); // setTimeout(()=>{ // this.shadowComponent?.traverse(c => { // console.log(c); // if(c.material) c.material.depthTest = false; // }); // },1000); } } } /** * @category User Interface * @group Components */ export class MaskableGraphic extends Graphic { private _flippedObject = false; protected onAfterCreated() { // flip image if (this.uiObject && !this._flippedObject) { this._flippedObject = true; this.uiObject.scale.y *= -1; } } }