UNPKG

@chronosai/three-mesh-ui

Version:

a library on top of three.js to help in creating 3D user interfaces

684 lines (415 loc) 14.5 kB
import { Plane } from 'three'; import { Vector3 } from 'three'; import FontLibrary from './FontLibrary.js'; import UpdateManager from './UpdateManager.js'; import DEFAULTS from '../../utils/Defaults.js'; import { warnAboutDeprecatedAlignItems } from '../../utils/block-layout/AlignItems'; /** Job: - Set this component attributes and call updates accordingly - Getting this component attribute, from itself or from its parents - Managing this component's states This is the core module of three-mesh-ui. Every component is composed with it. It owns the principal public methods of a component : set, setupState and setState. */ export default function MeshUIComponent( Base ) { return class MeshUIComponent extends Base { constructor( options ) { super( options ); this.states = {}; this.currentState = undefined; this.isUI = true; this.autoLayout = true; // children this.childrenUIs = []; this.childrenBoxes = []; this.childrenTexts = []; this.childrenInlines = []; // parents this.parentUI = null; // update parentUI when this component will be added or removed this.addEventListener( 'added', this._rebuildParentUI ); this.addEventListener( 'removed', this._rebuildParentUI ); } ///////////// /// GETTERS ///////////// getClippingPlanes() { const planes = []; if ( this.parentUI ) { if ( this.isBlock && this.parentUI.getHiddenOverflow() ) { const yLimit = ( this.parentUI.getHeight() / 2 ) - ( this.parentUI.padding || 0 ); const xLimit = ( this.parentUI.getWidth() / 2 ) - ( this.parentUI.padding || 0 ); const newPlanes = [ new Plane( new Vector3( 0, 1, 0 ), yLimit ), new Plane( new Vector3( 0, -1, 0 ), yLimit ), new Plane( new Vector3( 1, 0, 0 ), xLimit ), new Plane( new Vector3( -1, 0, 0 ), xLimit ) ]; newPlanes.forEach( plane => { plane.applyMatrix4( this.parent.matrixWorld ); } ); planes.push( ...newPlanes ); } if ( this.parentUI.parentUI ) { planes.push( ...this.parentUI.getClippingPlanes() ); } } return planes; } /** Get the highest parent of this component (the parent that has no parent on top of it) */ getHighestParent() { if ( !this.parentUI ) { return this; } return this.parent.getHighestParent(); } /** * look for a property in this object, and if does not find it, find in parents or return default value * @private */ _getProperty( propName ) { if ( this[ propName ] === undefined && this.parentUI ) { return this.parent._getProperty( propName ); } else if ( this[ propName ] !== undefined ) { return this[ propName ]; } return DEFAULTS[ propName ]; } // getFontSize() { return this._getProperty( 'fontSize' ); } getFontKerning() { return this._getProperty( 'fontKerning' ); } getLetterSpacing() { return this._getProperty( 'letterSpacing' ); } getFontTexture() { if ( this[ 'fontTexture' ] === undefined && this.parentUI ) { return this.parent._getProperty( 'fontTexture' ); } else if ( this[ 'fontTexture' ] !== undefined ) { return this[ 'fontTexture' ]; } return DEFAULTS.getDefaultTexture(); } getFontFamily() { return this._getProperty( 'fontFamily' ); } getBreakOn() { return this._getProperty( 'breakOn' ); } getWhiteSpace() { return this._getProperty( 'whiteSpace' ); } getTextAlign() { return this._getProperty( 'textAlign' ); } getTextType() { return this._getProperty( 'textType' ); } getFontColor() { return this._getProperty( 'fontColor' ); } getFontSupersampling() { return this._getProperty( 'fontSupersampling' ); } getFontOpacity() { return this._getProperty( 'fontOpacity' ); } getFontPXRange() { return this._getProperty( 'fontPXRange' ); } getBorderRadius() { return this._getProperty( 'borderRadius' ); } getBorderWidth() { return this._getProperty( 'borderWidth' ); } getBorderColor() { return this._getProperty( 'borderColor' ); } getBorderOpacity() { return this._getProperty( 'borderOpacity' ); } /// SPECIALS /** return the first parent with a 'threeOBJ' property */ getContainer() { if ( !this.threeOBJ && this.parent ) { return this.parent.getContainer(); } else if ( this.threeOBJ ) { return this; } return DEFAULTS.container; } /** Get the number of UI parents above this elements (0 if no parent) */ getParentsNumber( i ) { i = i || 0; if ( this.parentUI ) { return this.parentUI.getParentsNumber( i + 1 ); } return i; } //////////////////////////////////// /// GETTERS WITH NO PARENTS LOOKUP //////////////////////////////////// getBackgroundOpacity() { return ( !this.backgroundOpacity && this.backgroundOpacity !== 0 ) ? DEFAULTS.backgroundOpacity : this.backgroundOpacity; } getBackgroundColor() { return this.backgroundColor || DEFAULTS.backgroundColor; } getBackgroundTexture() { return this.backgroundTexture || DEFAULTS.getDefaultTexture(); } /** * @deprecated * @returns {string} */ getAlignContent() { return this.alignContent || DEFAULTS.alignContent; } getAlignItems() { return this.alignItems || DEFAULTS.alignItems; } getContentDirection() { return this.contentDirection || DEFAULTS.contentDirection; } getJustifyContent() { return this.justifyContent || DEFAULTS.justifyContent; } getInterLine() { return ( this.interLine === undefined ) ? DEFAULTS.interLine : this.interLine; } getOffset() { return ( this.offset === undefined ) ? DEFAULTS.offset : this.offset; } getBackgroundSize() { return ( this.backgroundSize === undefined ) ? DEFAULTS.backgroundSize : this.backgroundSize; } getHiddenOverflow() { return ( this.hiddenOverflow === undefined ) ? DEFAULTS.hiddenOverflow : this.hiddenOverflow; } getBestFit() { return ( this.bestFit === undefined ) ? DEFAULTS.bestFit : this.bestFit; } /////////////// /// UPDATE /////////////// /** * Filters children in order to compute only one times children lists * @private */ _rebuildChildrenLists() { // Stores all children that are ui this.childrenUIs = this.children.filter( child => child.isUI ); // Stores all children that are box this.childrenBoxes = this.children.filter( child => child.isBoxComponent ); // Stores all children that are inline this.childrenInlines = this.children.filter( child => child.isInline ); // Stores all children that are text this.childrenTexts = this.children.filter( child => child.isText ); } /** * Try to retrieve parentUI after each structural change * @private */ _rebuildParentUI = ( ) => { if ( this.parent && this.parent.isUI ) { this.parentUI = this.parent; } else { this.parentUI = null; } }; /** * When the user calls component.add, it registers for updates, * then call THREE.Object3D.add. */ add() { for ( const id of Object.keys( arguments ) ) { // An inline component relies on its parent for positioning if ( arguments[ id ].isInline ) this.update( null, true ); } const result = super.add( ...arguments ); this._rebuildChildrenLists(); return result; } /** * When the user calls component.remove, it registers for updates, * then call THREE.Object3D.remove. */ remove() { for ( const id of Object.keys( arguments ) ) { // An inline component relies on its parent for positioning if ( arguments[ id ].isInline ) this.update( null, true ); } const result = super.remove( ...arguments ); this._rebuildChildrenLists(); return result; } // update( updateParsing, updateLayout, updateInner ) { UpdateManager.requestUpdate( this, updateParsing, updateLayout, updateInner ); } onAfterUpdate() { } /** * Called by FontLibrary when the font requested for the current component is ready. * Trigger an update for the component whose font is now available. * @private - "package protected" */ _updateFontFamily( font ) { this.fontFamily = font; this.traverse( ( child ) => { if ( child.isUI ) child.update( true, true, false ); } ); this.getHighestParent().update( false, true, false ); } /** @private - "package protected" */ _updateFontTexture( texture ) { this.fontTexture = texture; this.getHighestParent().update( false, true, false ); } /** * Set this component's passed parameters. * If necessary, take special actions. * Update this component unless otherwise specified. */ set( options ) { let parsingNeedsUpdate, layoutNeedsUpdate, innerNeedsUpdate; // Register to the update manager, so that it knows when to update UpdateManager.register( this ); // Abort if no option passed if ( !options || JSON.stringify( options ) === JSON.stringify( {} ) ) return; // DEPRECATION Warnings until -------------------------------------- 7.x.x --------------------------------------- // Align content has been removed if( options["alignContent"] ){ options["alignItems"] = options["alignContent"]; if( !options["textAlign"] ){ options["textAlign"] = options["alignContent"]; } console.warn("`alignContent` property has been deprecated, please rely on `alignItems` and `textAlign` instead.") delete options["alignContent"]; } // Align items left top bottom right will be removed if( options['alignItems'] ){ warnAboutDeprecatedAlignItems( options['alignItems'] ); } // Set this component parameters according to options, and trigger updates accordingly // The benefit of having two types of updates, is to put everthing that takes time // in one batch, and the rest in the other. This way, efficient animation is possible with // attribute from the light batch. for ( const prop of Object.keys( options ) ) { if ( this[ prop ] != options[ prop ] ) { switch ( prop ) { case 'content' : case 'fontSize' : case 'fontKerning' : case 'breakOn': case 'whiteSpace': if ( this.isText ) parsingNeedsUpdate = true; layoutNeedsUpdate = true; this[ prop ] = options[ prop ]; break; case 'bestFit' : if ( this.isBlock ) { parsingNeedsUpdate = true; layoutNeedsUpdate = true; } this[ prop ] = options[ prop ]; break; case 'width' : case 'height' : case 'padding' : if ( this.isInlineBlock || ( this.isBlock && this.getBestFit() != 'none' ) ) parsingNeedsUpdate = true; layoutNeedsUpdate = true; this[ prop ] = options[ prop ]; break; case 'letterSpacing' : case 'interLine' : if ( this.isBlock && this.getBestFit() != 'none' ) parsingNeedsUpdate = true; layoutNeedsUpdate = true; this[ prop ] = options[ prop ]; break; case 'margin' : case 'contentDirection' : case 'justifyContent' : case 'alignContent' : case 'alignItems' : case 'textAlign' : case 'textType' : layoutNeedsUpdate = true; this[ prop ] = options[ prop ]; break; case 'fontColor' : case 'fontOpacity' : case 'fontSupersampling' : case 'offset' : case 'backgroundColor' : case 'backgroundOpacity' : case 'backgroundTexture' : case 'backgroundSize' : case 'borderRadius' : case 'borderWidth' : case 'borderColor' : case 'borderOpacity' : innerNeedsUpdate = true; this[ prop ] = options[ prop ]; break; case 'hiddenOverflow' : this[ prop ] = options[ prop ]; break; case 'billboard' : case 'sizeAttenuation' : this[ prop ] = options[ prop ]; break; } } } // special cases, this.update() must be called only when some files finished loading if ( options.fontFamily ) { FontLibrary.setFontFamily( this, options.fontFamily ); } if ( options.fontTexture ) { FontLibrary.setFontTexture( this, options.fontTexture ); } // if font kerning changes for a child of a block with Best Fit enabled, we need to trigger parsing for the parent as well. if ( this.parentUI && this.parentUI.getBestFit() != 'none' ) this.parentUI.update( true, true, false ); // Call component update this.update( parsingNeedsUpdate, layoutNeedsUpdate, innerNeedsUpdate ); if ( layoutNeedsUpdate ) this.getHighestParent().update( false, true, false ); } ///////////////////// // STATES MANAGEMENT ///////////////////// /** Store a new state in this component, with linked attributes */ setupState( options ) { this.states[ options.state ] = { attributes: options.attributes, onSet: options.onSet }; } /** Set the attributes of a stored state of this component */ setState( state ) { const savedState = this.states[ state ]; if ( !savedState ) { console.warn( `state "${state}" does not exist within this component:`, this.name ); return; } if ( state === this.currentState ) return; this.currentState = state; if ( savedState.onSet ) savedState.onSet(); if ( savedState.attributes ) this.set( savedState.attributes ); } /** Get completely rid of this component and its children, also unregister it for updates */ clear() { this.traverse( ( obj ) => { UpdateManager.disposeOf( obj ); if ( obj.material ) obj.material.dispose(); if ( obj.geometry ) obj.geometry.dispose(); } ); } }; }