UNPKG

aframe-mesh-ui-components

Version:

A simple port of felixmariotto's three-mesh-ui package for use in A-Frame's environment

1,891 lines (1,275 loc) 176 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("THREE")); else if(typeof define === 'function' && define.amd) define(["THREE"], factory); else { var a = typeof exports === 'object' ? factory(require("THREE")) : factory(root["THREE"]); for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; } })(self, (__WEBPACK_EXTERNAL_MODULE_three__) => { return /******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ "./node_modules/three-mesh-ui/build/three-mesh-ui.module.js": /*!******************************************************************!*\ !*** ./node_modules/three-mesh-ui/build/three-mesh-ui.module.js ***! \******************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ AlignItems: () => (/* binding */ __webpack_exports__AlignItems), /* harmony export */ Block: () => (/* binding */ __webpack_exports__Block), /* harmony export */ ContentDirection: () => (/* binding */ __webpack_exports__ContentDirection), /* harmony export */ FontLibrary: () => (/* binding */ __webpack_exports__FontLibrary), /* harmony export */ InlineBlock: () => (/* binding */ __webpack_exports__InlineBlock), /* harmony export */ JustifyContent: () => (/* binding */ __webpack_exports__JustifyContent), /* harmony export */ Keyboard: () => (/* binding */ __webpack_exports__Keyboard), /* harmony export */ Text: () => (/* binding */ __webpack_exports__Text), /* harmony export */ TextAlign: () => (/* binding */ __webpack_exports__TextAlign), /* harmony export */ Whitespace: () => (/* binding */ __webpack_exports__Whitespace), /* harmony export */ "default": () => (/* binding */ __webpack_exports__default), /* harmony export */ update: () => (/* binding */ __webpack_exports__update) /* harmony export */ }); /* harmony import */ var three__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! three */ "three"); /* harmony import */ var three__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(three__WEBPACK_IMPORTED_MODULE_0__); /******/ // The require scope /******/ var __nested_webpack_require_103__ = {}; /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __nested_webpack_require_103__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__nested_webpack_require_103__.o(definition, key) && !__nested_webpack_require_103__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __nested_webpack_require_103__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports /******/ __nested_webpack_require_103__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ })(); /******/ /************************************************************************/ var __nested_webpack_exports__ = {}; // EXPORTS __nested_webpack_require_103__.d(__nested_webpack_exports__, { "g1": () => (/* reexport */ AlignItems_namespaceObject), "gO": () => (/* reexport */ Block), "km": () => (/* reexport */ ContentDirection_namespaceObject), "zV": () => (/* reexport */ core_FontLibrary), "ol": () => (/* reexport */ InlineBlock), "uM": () => (/* reexport */ JustifyContent_namespaceObject), "N1": () => (/* reexport */ Keyboard), "xv": () => (/* reexport */ Text), "PH": () => (/* reexport */ TextAlign_namespaceObject), "UH": () => (/* reexport */ Whitespace_namespaceObject), "ZP": () => (/* binding */ three_mesh_ui), "Vx": () => (/* binding */ update) }); // NAMESPACE OBJECT: ./src/utils/block-layout/ContentDirection.js var ContentDirection_namespaceObject = {}; __nested_webpack_require_103__.r(ContentDirection_namespaceObject); __nested_webpack_require_103__.d(ContentDirection_namespaceObject, { "COLUMN": () => (COLUMN), "COLUMN_REVERSE": () => (COLUMN_REVERSE), "ROW": () => (ROW), "ROW_REVERSE": () => (ROW_REVERSE), "contentDirection": () => (contentDirection) }); // NAMESPACE OBJECT: ./src/utils/block-layout/AlignItems.js var AlignItems_namespaceObject = {}; __nested_webpack_require_103__.r(AlignItems_namespaceObject); __nested_webpack_require_103__.d(AlignItems_namespaceObject, { "CENTER": () => (CENTER), "END": () => (END), "START": () => (START), "STRETCH": () => (STRETCH), "alignItems": () => (alignItems), "warnAboutDeprecatedAlignItems": () => (warnAboutDeprecatedAlignItems) }); // NAMESPACE OBJECT: ./src/utils/block-layout/JustifyContent.js var JustifyContent_namespaceObject = {}; __nested_webpack_require_103__.r(JustifyContent_namespaceObject); __nested_webpack_require_103__.d(JustifyContent_namespaceObject, { "CENTER": () => (JustifyContent_CENTER), "END": () => (JustifyContent_END), "SPACE_AROUND": () => (SPACE_AROUND), "SPACE_BETWEEN": () => (SPACE_BETWEEN), "SPACE_EVENLY": () => (SPACE_EVENLY), "START": () => (JustifyContent_START), "justifyContent": () => (justifyContent) }); // NAMESPACE OBJECT: ./src/utils/inline-layout/Whitespace.js var Whitespace_namespaceObject = {}; __nested_webpack_require_103__.r(Whitespace_namespaceObject); __nested_webpack_require_103__.d(Whitespace_namespaceObject, { "NORMAL": () => (NORMAL), "NOWRAP": () => (NOWRAP), "PRE": () => (PRE), "PRE_LINE": () => (PRE_LINE), "PRE_WRAP": () => (PRE_WRAP), "WHITE_CHARS": () => (WHITE_CHARS), "collapseWhitespaceOnInlines": () => (collapseWhitespaceOnInlines), "collapseWhitespaceOnString": () => (collapseWhitespaceOnString), "newlineBreakability": () => (newlineBreakability), "shouldBreak": () => (Whitespace_shouldBreak) }); // NAMESPACE OBJECT: ./src/utils/inline-layout/TextAlign.js var TextAlign_namespaceObject = {}; __nested_webpack_require_103__.r(TextAlign_namespaceObject); __nested_webpack_require_103__.d(TextAlign_namespaceObject, { "CENTER": () => (TextAlign_CENTER), "JUSTIFY": () => (JUSTIFY), "JUSTIFY_CENTER": () => (JUSTIFY_CENTER), "JUSTIFY_LEFT": () => (JUSTIFY_LEFT), "JUSTIFY_RIGHT": () => (JUSTIFY_RIGHT), "LEFT": () => (LEFT), "RIGHT": () => (RIGHT), "textAlign": () => (textAlign) }); ;// CONCATENATED MODULE: external "three" var x = y => { var x = {}; __nested_webpack_require_103__.d(x, y); return x; } var y = x => () => x const external_three_namespaceObject = x({ ["BufferAttribute"]: () => three__WEBPACK_IMPORTED_MODULE_0__.BufferAttribute, ["BufferGeometry"]: () => three__WEBPACK_IMPORTED_MODULE_0__.BufferGeometry, ["CanvasTexture"]: () => three__WEBPACK_IMPORTED_MODULE_0__.CanvasTexture, ["Color"]: () => three__WEBPACK_IMPORTED_MODULE_0__.Color, ["FileLoader"]: () => three__WEBPACK_IMPORTED_MODULE_0__.FileLoader, ["LinearFilter"]: () => three__WEBPACK_IMPORTED_MODULE_0__.LinearFilter, ["Mesh"]: () => three__WEBPACK_IMPORTED_MODULE_0__.Mesh, ["Object3D"]: () => three__WEBPACK_IMPORTED_MODULE_0__.Object3D, ["Plane"]: () => three__WEBPACK_IMPORTED_MODULE_0__.Plane, ["PlaneGeometry"]: () => three__WEBPACK_IMPORTED_MODULE_0__.PlaneGeometry, ["ShaderMaterial"]: () => three__WEBPACK_IMPORTED_MODULE_0__.ShaderMaterial, ["TextureLoader"]: () => three__WEBPACK_IMPORTED_MODULE_0__.TextureLoader, ["Vector2"]: () => three__WEBPACK_IMPORTED_MODULE_0__.Vector2, ["Vector3"]: () => three__WEBPACK_IMPORTED_MODULE_0__.Vector3 }); ;// CONCATENATED MODULE: ./src/utils/block-layout/ContentDirection.js const ROW = "row"; const ROW_REVERSE = "row-reverse"; const COLUMN = "column"; const COLUMN_REVERSE = "column-reverse"; function contentDirection( container, DIRECTION, startPos, REVERSE ){ // end to end children let accu = startPos; let childGetSize = "getWidth"; let axisPrimary = "x"; let axisSecondary = "y"; if( DIRECTION.indexOf( COLUMN ) === 0 ){ childGetSize = "getHeight"; axisPrimary = "y"; axisSecondary = "x"; } // Refactor reduce into fori in order to get rid of this keyword for ( let i = 0; i < container.childrenBoxes.length; i++ ) { const child = container.childrenBoxes[ i ]; const CHILD_ID = child.id; const CHILD_SIZE = child[childGetSize](); const CHILD_MARGIN = child.margin || 0; accu += CHILD_MARGIN * REVERSE; container.childrenPos[ CHILD_ID ] = { [axisPrimary]: accu + ( ( CHILD_SIZE / 2 ) * REVERSE ), [axisSecondary]: 0 }; // update accu for next children accu += ( REVERSE * ( CHILD_SIZE + CHILD_MARGIN ) ); } } ;// CONCATENATED MODULE: ./src/utils/block-layout/AlignItems.js const START = "start"; const CENTER = "center"; const END = "end"; const STRETCH = "stretch"; // Still bit experimental function alignItems( boxComponent, DIRECTION){ const ALIGNMENT = boxComponent.getAlignItems(); if( AVAILABLE_ALIGN_ITEMS.indexOf(ALIGNMENT) === -1 ){ console.warn( `alignItems === '${ALIGNMENT}' is not supported` ); } let getSizeMethod = "getWidth"; let axis = "x"; if( DIRECTION.indexOf( ROW ) === 0 ){ getSizeMethod = "getHeight"; axis = "y"; } const AXIS_TARGET = ( boxComponent[getSizeMethod]() / 2 ) - ( boxComponent.padding || 0 ); boxComponent.childrenBoxes.forEach( ( child ) => { let offset; switch ( ALIGNMENT ){ case END: case 'right': // @TODO : Deprecated and will be remove upon 7.x.x case 'bottom': // @TODO : Deprecated and will be remove upon 7.x.x if( DIRECTION.indexOf( ROW ) === 0 ){ offset = - AXIS_TARGET + ( child[getSizeMethod]() / 2 ) + ( child.margin || 0 ); }else{ offset = AXIS_TARGET - ( child[getSizeMethod]() / 2 ) - ( child.margin || 0 ); } break; case START: case 'left': // @TODO : Deprecated and will be remove upon 7.x.x case 'top': // @TODO : Deprecated and will be remove upon 7.x.x if( DIRECTION.indexOf( ROW ) === 0 ){ offset = AXIS_TARGET - ( child[getSizeMethod]() / 2 ) - ( child.margin || 0 ); }else{ offset = - AXIS_TARGET + ( child[getSizeMethod]() / 2 ) + ( child.margin || 0 ); } break; } boxComponent.childrenPos[ child.id ][axis] = offset || 0; } ); } /** * @deprecated * // @TODO: Be remove upon 7.x.x * @param alignment */ function warnAboutDeprecatedAlignItems( alignment ){ if( DEPRECATED_ALIGN_ITEMS.indexOf(alignment) !== - 1){ console.warn(`alignItems === '${alignment}' is deprecated and will be remove in 7.x.x. Fallback are 'start'|'end'`) } } const AVAILABLE_ALIGN_ITEMS = [ START, CENTER, END, STRETCH, 'top', // @TODO: Be remove upon 7.x.x 'right', // @TODO: Be remove upon 7.x.x 'bottom', // @TODO: Be remove upon 7.x.x 'left' // @TODO: Be remove upon 7.x.x ]; // @TODO: Be remove upon 7.x.x const DEPRECATED_ALIGN_ITEMS = [ 'top', 'right', 'bottom', 'left' ]; ;// CONCATENATED MODULE: ./src/utils/block-layout/JustifyContent.js const JustifyContent_START = "start"; const JustifyContent_CENTER = "center"; const JustifyContent_END = "end"; const SPACE_AROUND = 'space-around'; const SPACE_BETWEEN = 'space-between'; const SPACE_EVENLY = 'space-evenly'; function justifyContent( boxComponent, direction, startPos, REVERSE){ const JUSTIFICATION = boxComponent.getJustifyContent(); if ( AVAILABLE_JUSTIFICATIONS.indexOf( JUSTIFICATION ) === -1 ) { console.warn( `justifyContent === '${ JUSTIFICATION }' is not supported` ); } const side = direction.indexOf('row') === 0 ? 'width' : 'height' const usedDirectionSpace = boxComponent.getChildrenSideSum( side ); const INNER_SIZE = side === 'width' ? boxComponent.getInnerWidth() : boxComponent.getInnerHeight(); const remainingSpace = INNER_SIZE - usedDirectionSpace; // Items Offset const axisOffset = ( startPos * 2 ) - ( usedDirectionSpace * Math.sign( startPos ) ); // const axisOffset = ( startPos * 2 ) - ( usedDirectionSpace * REVERSE ); const justificationOffset = _getJustificationOffset( JUSTIFICATION, axisOffset ); // Items margin const justificationMargins = _getJustificationMargin( boxComponent.childrenBoxes, remainingSpace, JUSTIFICATION, REVERSE ); // Apply const axis = direction.indexOf( 'row' ) === 0 ? "x" : "y" boxComponent.childrenBoxes.forEach( ( child , childIndex ) => { boxComponent.childrenPos[ child.id ][axis] -= justificationOffset - justificationMargins[childIndex]; } ); } const AVAILABLE_JUSTIFICATIONS = [ JustifyContent_START, JustifyContent_CENTER, JustifyContent_END, SPACE_AROUND, SPACE_BETWEEN, SPACE_EVENLY ]; /** * * @param {string} justification * @param {number} axisOffset * @returns {number} */ function _getJustificationOffset( justification, axisOffset ){ // Only end and center have justification offset switch ( justification ){ case JustifyContent_END: return axisOffset; case JustifyContent_CENTER: return axisOffset / 2; } return 0; } /** * * @param items * @param spaceToDistribute * @param justification * @param reverse * @returns {any[]} */ function _getJustificationMargin( items, spaceToDistribute, justification, reverse ){ const justificationMargins = Array( items.length ).fill( 0 ); if ( spaceToDistribute > 0 ) { // Only space-* have justification margin betweem items switch ( justification ) { case SPACE_BETWEEN: // only one children would act as start if ( items.length > 1 ) { const margin = spaceToDistribute / ( items.length - 1 ) * reverse; // set this margin for any children // except for first child justificationMargins[ 0 ] = 0; for ( let i = 1; i < items.length; i++ ) { justificationMargins[ i ] = margin * i; } } break; case SPACE_EVENLY: // only one children would act as start if ( items.length > 1 ) { const margin = spaceToDistribute / ( items.length + 1 ) * reverse; // set this margin for any children for ( let i = 0; i < items.length; i++ ) { justificationMargins[ i ] = margin * ( i + 1 ); } } break; case SPACE_AROUND: // only one children would act as start if ( items.length > 1 ) { const margin = spaceToDistribute / ( items.length ) * reverse; const start = margin / 2; justificationMargins[ 0 ] = start; // set this margin for any children for ( let i = 1; i < items.length; i++ ) { justificationMargins[ i ] = start + margin * i; } } break; } } return justificationMargins; } ;// CONCATENATED MODULE: ./src/components/core/BoxComponent.js /** Job: Handle everything related to a BoxComponent element dimensioning and positioning Knows: Parents and children dimensions and positions It's worth noting that in three-mesh-ui, it's the parent Block that computes its children position. A Block can only have either only box components (Block) as children, or only inline components (Text, InlineBlock). */ function BoxComponent( Base ) { return class BoxComponent extends Base { constructor( options ) { super( options ); this.isBoxComponent = true; this.childrenPos = {}; } /** Get width of this component minus its padding */ getInnerWidth() { const DIRECTION = this.getContentDirection(); switch ( DIRECTION ) { case 'row' : case 'row-reverse' : return this.width - ( this.padding * 2 || 0 ) || this.getChildrenSideSum( 'width' ); case 'column' : case 'column-reverse' : return this.getHighestChildSizeOn( 'width' ); default : console.error( `Invalid contentDirection : ${DIRECTION}` ); break; } } /** Get height of this component minus its padding */ getInnerHeight() { const DIRECTION = this.getContentDirection(); switch ( DIRECTION ) { case 'row' : case 'row-reverse' : return this.getHighestChildSizeOn( 'height' ); case 'column' : case 'column-reverse' : return this.height - ( this.padding * 2 || 0 ) || this.getChildrenSideSum( 'height' ); default : console.error( `Invalid contentDirection : ${DIRECTION}` ); break; } } /** Return the sum of all this component's children sides + their margin */ getChildrenSideSum( dimension ) { return this.childrenBoxes.reduce( ( accu, child ) => { const margin = ( child.margin * 2 ) || 0; const CHILD_SIZE = ( dimension === 'width' ) ? ( child.getWidth() + margin ) : ( child.getHeight() + margin ); return accu + CHILD_SIZE; }, 0 ); } /** Look in parent record what is the instructed position for this component, then set its position */ setPosFromParentRecords() { if ( this.parentUI && this.parentUI.childrenPos[ this.id ] ) { this.position.x = ( this.parentUI.childrenPos[ this.id ].x ); this.position.y = ( this.parentUI.childrenPos[ this.id ].y ); } } /** Position inner elements according to dimensions and layout parameters. */ computeChildrenPosition() { if ( this.children.length > 0 ) { const DIRECTION = this.getContentDirection(); let directionalOffset; switch ( DIRECTION ) { case ROW : directionalOffset = - this.getInnerWidth() / 2; break; case ROW_REVERSE : directionalOffset = this.getInnerWidth() / 2; break; case COLUMN : directionalOffset = this.getInnerHeight() / 2; break; case COLUMN_REVERSE : directionalOffset = - this.getInnerHeight() / 2; break; } const REVERSE = - Math.sign( directionalOffset ); contentDirection(this, DIRECTION, directionalOffset, REVERSE ); justifyContent(this, DIRECTION, directionalOffset, REVERSE ); alignItems( this, DIRECTION ); } } /** * Returns the highest linear dimension among all the children of the passed component * MARGIN INCLUDED */ getHighestChildSizeOn( direction ) { return this.childrenBoxes.reduce( ( accu, child ) => { const margin = child.margin || 0; const maxSize = direction === 'width' ? child.getWidth() + ( margin * 2 ) : child.getHeight() + ( margin * 2 ); return Math.max( accu, maxSize ); }, 0 ); } /** * Get width of this element * With padding, without margin */ getWidth() { // This is for stretch alignment // @TODO : Conceive a better performant way if( this.parentUI && this.parentUI.getAlignItems() === 'stretch' ){ if( this.parentUI.getContentDirection().indexOf('column') !== -1 ){ return this.parentUI.getWidth() - ( this.parentUI.padding * 2 || 0 ); } } return this.width || this.getInnerWidth() + ( this.padding * 2 || 0 ); } /** * Get height of this element * With padding, without margin */ getHeight() { // This is for stretch alignment // @TODO : Conceive a better performant way if( this.parentUI && this.parentUI.getAlignItems() === 'stretch' ){ if( this.parentUI.getContentDirection().indexOf('row') !== -1 ){ return this.parentUI.getHeight() - ( this.parentUI.padding * 2 || 0 ); } } return this.height || this.getInnerHeight() + ( this.padding * 2 || 0 ); } }; } ;// CONCATENATED MODULE: ./src/utils/inline-layout/Whitespace.js /** * @see https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#whitespace_helper_functions * * Throughout, whitespace is defined as one of the characters * "\t" TAB \u0009 * "\n" LF \u000A * "\r" CR \u000D * " " SPC \u0020 * * This does not use Javascript's "\s" because that includes non-breaking * spaces (and also some other characters). **/ const WHITE_CHARS = { '\t': '\u0009', '\n': '\u000A', '\r': '\u000D', ' ': '\u0020' }; const NORMAL = 'normal'; const NOWRAP = 'nowrap'; const PRE = 'pre'; const PRE_LINE = 'pre-line'; const PRE_WRAP = 'pre-wrap'; /** * Collapse whitespaces and sequence of whitespaces on string * * @param textContent * @param whiteSpace * @returns {*} */ const collapseWhitespaceOnString = function ( textContent, whiteSpace ) { switch ( whiteSpace ) { case NOWRAP: case NORMAL: // newlines are treated as other whitespace characters textContent = textContent.replace( /\n/g, ' ' ); //falls through case PRE_LINE: // collapsed white spaces sequences textContent = textContent.replace( /[ ]{2,}/g, ' ' ); break; default: } return textContent; }; /** * Get the breakability of a newline character according to white-space property * * @param whiteSpace * @returns {string} */ const newlineBreakability = function ( whiteSpace ) { switch ( whiteSpace ) { case PRE: case PRE_WRAP: case PRE_LINE: return 'mandatory'; case NOWRAP: case NORMAL: default: // do not automatically break on newline } }; /** * Check for breaks in inlines according to whiteSpace value * * @param inlines * @param i * @param lastInlineOffset * @param options * @returns {boolean} */ const Whitespace_shouldBreak = function( inlines, i, lastInlineOffset, options){ const inline = inlines[i]; switch ( options.WHITESPACE ){ case NORMAL: case PRE_LINE: case PRE_WRAP: // prevent additional computation if line break is mandatory if( inline.lineBreak === 'mandatory' ) return true; const kerning = inline.kerning ? inline.kerning : 0; const xoffset = inline.xoffset ? inline.xoffset : 0; const xadvance = inline.xadvance ? inline.xadvance : inline.width; // prevent additional computation if this character already exceed the available size if( lastInlineOffset + xadvance + xoffset + kerning > options.INNER_WIDTH ) return true; const nextBreak = _distanceToNextBreak( inlines, i, options ); return _shouldFriendlyBreak( inlines[ i - 1 ], lastInlineOffset, nextBreak, options ); case PRE: return inline.lineBreak === 'mandatory'; case NOWRAP: default: return false; } } /** * Alter a line of inlines according to white-space property * @param line * @param {('normal'|'pre-wrap'|'pre-line')} whiteSpace */ const collapseWhitespaceOnInlines = function ( line, whiteSpace ) { const firstInline = line[ 0 ]; const lastInline = line[ line.length - 1 ]; // @see https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace // // current implementation is 'pre-line' // if the breaking character is a space, get the previous one switch ( whiteSpace ) { // trim/collapse first and last whitespace characters of a line case PRE_WRAP: // only process whiteChars glyphs inlines // if( firstInline.glyph && whiteChars[firstInline.glyph] && line.length > 1 ){ if ( firstInline.glyph && firstInline.glyph === '\n' && line.length > 1 ) { _collapseLeftInlines( [ firstInline ], line[ 1 ] ); } // if( lastInline.glyph && whiteChars[lastInline.glyph] && line.length > 1 ){ if ( lastInline.glyph && lastInline.glyph === '\n' && line.length > 1 ) { _collapseRightInlines( [ lastInline ], line[ line.length - 2 ] ); } break; case PRE_LINE: case NOWRAP: case NORMAL: let inlinesToCollapse = []; let collapsingTarget; // collect starting whitespaces to collapse for ( let i = 0; i < line.length; i++ ) { const inline = line[ i ]; if ( inline.glyph && WHITE_CHARS[ inline.glyph ] && line.length > i ) { inlinesToCollapse.push( inline ); collapsingTarget = line[ i + 1 ]; continue; } break; } _collapseLeftInlines( inlinesToCollapse, collapsingTarget ); inlinesToCollapse = []; collapsingTarget = null; // collect ending whitespace to collapse for ( let i = line.length - 1; i > 0; i-- ) { const inline = line[ i ]; if ( inline.glyph && WHITE_CHARS[ inline.glyph ] && i > 0 ) { inlinesToCollapse.push( inline ); collapsingTarget = line[ i - 1 ]; continue; } break; } _collapseRightInlines( inlinesToCollapse, collapsingTarget ); break; case PRE: break; default: console.warn( `whiteSpace: '${whiteSpace}' is not valid` ); return 0; } return firstInline.offsetX; }; /*********************************************************************************************************************** * Internal logics **********************************************************************************************************************/ /** * Visually collapse inlines from right to left ( endtrim ) * @param {Array} inlines * @param targetInline * @private */ function _collapseRightInlines( inlines, targetInline ) { if ( !targetInline ) return; for ( let i = 0; i < inlines.length; i++ ) { const inline = inlines[ i ]; inline.width = 0; inline.height = 0; inline.offsetX = targetInline.offsetX + targetInline.width; } } /** * Visually collapse inlines from left to right (starttrim) * @param {Array} inlines * @param targetInline * @private */ function _collapseLeftInlines( inlines, targetInline ) { if ( !targetInline ) return; for ( let i = 0; i < inlines.length; i++ ) { const inline = inlines[ i ]; inline.width = 0; inline.height = 0; inline.offsetX = targetInline.offsetX; } } /** * get the distance in world coord to the next glyph defined * as break-line-safe ( like whitespace for instance ) * @private */ function _distanceToNextBreak( inlines, currentIdx, options, accu ) { accu = accu || 0; // end of the text if ( !inlines[ currentIdx ] ) return accu; const inline = inlines[ currentIdx ]; const kerning = inline.kerning ? inline.kerning : 0; const xoffset = inline.xoffset ? inline.xoffset : 0; const xadvance = inline.xadvance ? inline.xadvance : inline.width; // if inline.lineBreak is set, it is 'mandatory' or 'possible' if ( inline.lineBreak ) return accu + xadvance; // no line break is possible on this character return _distanceToNextBreak( inlines, currentIdx + 1, options, accu + xadvance + options.LETTERSPACING + xoffset + kerning ); } /** * Test if we should line break here even if the current glyph is not out of boundary. * It might be necessary if the last glyph was break-line-friendly (whitespace, hyphen..) * and the distance to the next friendly glyph is out of boundary. */ function _shouldFriendlyBreak( prevChar, lastInlineOffset, nextBreak, options ) { // We can't check if last glyph is break-line-friendly it does not exist if ( !prevChar || !prevChar.glyph ) return false; // Next break-line-friendly glyph is inside boundary if ( lastInlineOffset + nextBreak < options.INNER_WIDTH ) return false; // Previous glyph was break-line-friendly return options.BREAKON.indexOf( prevChar.glyph ) > -1; } ;// CONCATENATED MODULE: ./src/utils/inline-layout/TextAlign.js const LEFT = 'left'; const RIGHT = 'right'; const TextAlign_CENTER = 'center'; const JUSTIFY = 'justify'; const JUSTIFY_LEFT = 'justify-left'; const JUSTIFY_RIGHT = 'justify-right'; const JUSTIFY_CENTER = 'justify-center'; function textAlign( lines, ALIGNMENT, INNER_WIDTH ) { // Start the alignment by sticking to directions : left, right, center for ( let i = 0; i < lines.length; i++ ) { const line = lines[ i ]; // compute the alignment offset of the line const offsetX = _computeLineOffset( line, ALIGNMENT, INNER_WIDTH, i === lines.length - 1 ); // apply the offset to each characters of the line for ( let j = 0; j < line.length; j++ ) { line[ j ].offsetX += offsetX; } line.x = offsetX; } // last operations for justifications alignments if ( ALIGNMENT.indexOf( JUSTIFY ) === 0 ) { for ( let i = 0; i < lines.length; i++ ) { const line = lines[ i ]; // do not process last line for justify-left or justify-right if ( ALIGNMENT.indexOf( '-' ) !== -1 && i === lines.length - 1 ) return; // can only justify is space is remaining const REMAINING_SPACE = INNER_WIDTH - line.width; if ( REMAINING_SPACE <= 0 ) return; // count the valid spaces to extend // Do not take the first nor the last space into account let validSpaces = 0; for ( let j = 1; j < line.length - 1; j++ ) { validSpaces += line[ j ].glyph === ' ' ? 1 : 0; } const additionalSpace = REMAINING_SPACE / validSpaces; // for right justification, process the loop in reverse let inverter = 1; if ( ALIGNMENT === JUSTIFY_RIGHT ) { line.reverse(); inverter = -1; } let incrementalOffsetX = 0; // start at ONE to avoid first space for ( let j = 1; j <= line.length - 1; j++ ) { // apply offset on each char const char = line[ j ]; char.offsetX += incrementalOffsetX * inverter; // and increase it when space incrementalOffsetX += char.glyph === ' ' ? additionalSpace : 0; } // for right justification, the loop was processed in reverse if ( ALIGNMENT === JUSTIFY_RIGHT ) { line.reverse(); } } } } const _computeLineOffset = ( line, ALIGNMENT, INNER_WIDTH, lastLine ) => { switch ( ALIGNMENT ) { case JUSTIFY_LEFT: case JUSTIFY: case LEFT: return -INNER_WIDTH / 2; case JUSTIFY_RIGHT: case RIGHT: return -line.width + ( INNER_WIDTH / 2 ); case TextAlign_CENTER: return -line.width / 2; case JUSTIFY_CENTER: if ( lastLine ) { // center alignement return -line.width / 2; } // left alignment return -INNER_WIDTH / 2; default: console.warn( `textAlign: '${ALIGNMENT}' is not valid` ); } }; ;// CONCATENATED MODULE: ./src/components/core/InlineManager.js /** Job: Positioning inline elements according to their dimensions inside this component Knows: This component dimensions, and its children dimensions This module is used for Block composition (Object.assign). A Block is responsible for the positioning of its inline elements. In order for it to know what is the size of these inline components, parseParams must be called on its children first. It's worth noting that a Text is not positioned as a whole, but letter per letter, in order to create a line break when necessary. It's Text that merge the various letters in its own updateLayout function. */ function InlineManager( Base ) { return class InlineManager extends Base { /** Compute children .inlines objects position, according to their pre-computed dimensions */ computeInlinesPosition() { // computed by BoxComponent const INNER_WIDTH = this.getWidth() - ( this.padding * 2 || 0 ); const INNER_HEIGHT = this.getHeight() - ( this.padding * 2 || 0 ); // got by MeshUIComponent const JUSTIFICATION = this.getJustifyContent(); const ALIGNMENT = this.getTextAlign(); const INTERLINE = this.getInterLine(); // Compute lines const lines = this.computeLines(); lines.interLine = INTERLINE; ///////////////////////////////////////////////////////////////// // Position lines according to justifyContent and contentAlign ///////////////////////////////////////////////////////////////// const textHeight = Math.abs( lines.height ); // Line vertical positioning const justificationOffset = ( () => { switch ( JUSTIFICATION ) { case 'start': return (INNER_HEIGHT/2); case 'end': return textHeight - ( INNER_HEIGHT / 2 ); case 'center': return ( textHeight / 2 ); default: console.warn( `justifyContent: '${JUSTIFICATION}' is not valid` ); } } )(); // lines.forEach( ( line ) => { line.y += justificationOffset; line.forEach( ( inline ) => { inline.offsetY += justificationOffset; } ); } ); // Horizontal positioning textAlign( lines, ALIGNMENT, INNER_WIDTH ); // Make lines accessible to provide helpful informations this.lines = lines; } calculateBestFit( bestFit ) { if ( this.childrenInlines.length === 0 ) return; switch ( bestFit ) { case 'grow': this.calculateGrowFit(); break; case 'shrink': this.calculateShrinkFit(); break; case 'auto': this.calculateAutoFit(); break; } } calculateGrowFit() { const INNER_HEIGHT = this.getHeight() - ( this.padding * 2 || 0 ); //Iterative method to find a fontSize of text children that text will fit into container let iterations = 1; const heightTolerance = 0.075; const firstText = this.childrenInlines.find( inlineComponent => inlineComponent.isText ); let minFontMultiplier = 1; let maxFontMultiplier = 2; let fontMultiplier = firstText._fitFontSize ? firstText._fitFontSize / firstText.getFontSize() : 1; let textHeight; do { textHeight = this.calculateHeight( fontMultiplier ); if ( textHeight > INNER_HEIGHT ) { if ( fontMultiplier <= minFontMultiplier ) { // can't shrink text this.childrenInlines.forEach( inlineComponent => { if ( inlineComponent.isInlineBlock ) return; // ensure fontSize does not shrink inlineComponent._fitFontSize = inlineComponent.getFontSize(); } ); break; } maxFontMultiplier = fontMultiplier; fontMultiplier -= ( maxFontMultiplier - minFontMultiplier ) / 2; } else { if ( Math.abs( INNER_HEIGHT - textHeight ) < heightTolerance ) break; if ( Math.abs( fontMultiplier - maxFontMultiplier ) < 5e-10 ) maxFontMultiplier *= 2; minFontMultiplier = fontMultiplier; fontMultiplier += ( maxFontMultiplier - minFontMultiplier ) / 2; } } while ( ++iterations <= 10 ); } calculateShrinkFit() { const INNER_HEIGHT = this.getHeight() - ( this.padding * 2 || 0 ); // Iterative method to find a fontSize of text children that text will fit into container let iterations = 1; const heightTolerance = 0.075; const firstText = this.childrenInlines.find( inlineComponent => inlineComponent.isText ); let minFontMultiplier = 0; let maxFontMultiplier = 1; let fontMultiplier = firstText._fitFontSize ? firstText._fitFontSize / firstText.getFontSize() : 1; let textHeight; do { textHeight = this.calculateHeight( fontMultiplier ); if ( textHeight > INNER_HEIGHT ) { maxFontMultiplier = fontMultiplier; fontMultiplier -= ( maxFontMultiplier - minFontMultiplier ) / 2; } else { if ( fontMultiplier >= maxFontMultiplier ) { // can't grow text this.childrenInlines.forEach( inlineComponent => { if ( inlineComponent.isInlineBlock ) return; // ensure fontSize does not grow inlineComponent._fitFontSize = inlineComponent.getFontSize(); } ); break; } if ( Math.abs( INNER_HEIGHT - textHeight ) < heightTolerance ) break; minFontMultiplier = fontMultiplier; fontMultiplier += ( maxFontMultiplier - minFontMultiplier ) / 2; } } while ( ++iterations <= 10 ); } calculateAutoFit() { const INNER_HEIGHT = this.getHeight() - ( this.padding * 2 || 0 ); //Iterative method to find a fontSize of text children that text will fit into container let iterations = 1; const heightTolerance = 0.075; const firstText = this.childrenInlines.find( inlineComponent => inlineComponent.isText ); let minFontMultiplier = 0; let maxFontMultiplier = 2; let fontMultiplier = firstText._fitFontSize ? firstText._fitFontSize / firstText.getFontSize() : 1; let textHeight; do { textHeight = this.calculateHeight( fontMultiplier ); if ( textHeight > INNER_HEIGHT ) { maxFontMultiplier = fontMultiplier; fontMultiplier -= ( maxFontMultiplier - minFontMultiplier ) / 2; } else { if ( Math.abs( INNER_HEIGHT - textHeight ) < heightTolerance ) break; if ( Math.abs( fontMultiplier - maxFontMultiplier ) < 5e-10 ) maxFontMultiplier *= 2; minFontMultiplier = fontMultiplier; fontMultiplier += ( maxFontMultiplier - minFontMultiplier ) / 2; } } while ( ++iterations <= 10 ); } /** * computes lines based on children's inlines array. * @private */ computeLines() { // computed by BoxComponent const INNER_WIDTH = this.getWidth() - ( this.padding * 2 || 0 ); // Will stock the characters of each line, so that we can // correct lines position before to merge const lines = [ [] ]; lines.height = 0; const INTERLINE = this.getInterLine(); this.childrenInlines.reduce( ( lastInlineOffset, inlineComponent ) => { // Abort condition if ( !inlineComponent.inlines ) return; ////////////////////////////////////////////////////////////// // Compute offset of each children according to its dimensions ////////////////////////////////////////////////////////////// const FONTSIZE = inlineComponent._fitFontSize || inlineComponent.getFontSize(); const LETTERSPACING = inlineComponent.isText ? inlineComponent.getLetterSpacing() * FONTSIZE : 0; const WHITESPACE = inlineComponent.getWhiteSpace(); const BREAKON = inlineComponent.getBreakOn(); const whiteSpaceOptions = { WHITESPACE, LETTERSPACING, BREAKON, INNER_WIDTH } const currentInlineInfo = inlineComponent.inlines.reduce( ( lastInlineOffset, inline, i, inlines ) => { const kerning = inline.kerning ? inline.kerning : 0; const xoffset = inline.xoffset ? inline.xoffset : 0; const xadvance = inline.xadvance ? inline.xadvance : inline.width; // Line break const shouldBreak = Whitespace_shouldBreak(inlines,i,lastInlineOffset, whiteSpaceOptions ); if ( shouldBreak ) { lines.push( [ inline ] ); inline.offsetX = xoffset; // restart the lastInlineOffset as zero. if ( inline.width === 0 ) return 0; // compute lastInlineOffset normally // except for kerning which won't apply // as there is visually no lefthanded glyph to kern with return xadvance + LETTERSPACING; } lines[ lines.length - 1 ].push( inline ); inline.offsetX = lastInlineOffset + xoffset + kerning; return lastInlineOffset + xadvance + kerning + LETTERSPACING; }, lastInlineOffset ); // return currentInlineInfo; }, 0 ); // Compute lines dimensions let width = 0, height =0, lineOffsetY = -INTERLINE/2; lines.forEach( ( line ) => { // line.lineHeight = line.reduce( ( height, inline ) => { const charHeight = inline.lineHeight !== undefined ? inline.lineHeight : inline.height; return Math.max( height, charHeight ); }, 0 ); // line.lineBase = line.reduce( ( lineBase, inline ) => { const newLineBase = inline.lineBase !== undefined ? inline.lineBase : inline.height; return Math.max( lineBase, newLineBase ); }, 0 ); // line.width = 0; line.height = line.lineHeight; const lineHasInlines = line[ 0 ]; if ( lineHasInlines ) { // starts by processing whitespace, it will return a collapsed left offset const WHITESPACE = this.getWhiteSpace(); const whiteSpaceOffset = collapseWhitespaceOnInlines( line, WHITESPACE ); // apply the collapsed left offset to ensure the starting offset is 0 line.forEach( ( inline ) => { inline.offsetX -= whiteSpaceOffset; } ); // compute its width: length from firstInline:LEFT to lastInline:RIGHT line.width = this.computeLineWidth( line ); if( line.width > width ){ width = line.width; } line.forEach( ( inline ) => { inline.offsetY = (lineOffsetY - inline.height) - inline.anchor; if( inline.lineHeight < line.lineHeight ){ inline.offsetY -= line.lineBase- inline.lineBase; } } ); line.y = lineOffsetY; // line.x will be set by textAlign height += ( line.lineHeight + INTERLINE ); lineOffsetY = lineOffsetY - (line.lineHeight + INTERLINE ); } } ); lines.height = height; lines.width = width; return lines; } calculateHeight( fontMultiplier ) { this.childrenInlines.forEach( inlineComponent => { if ( inlineComponent.isInlineBlock ) return; // Set font size and recalculate dimensions inlineComponent._fitFontSize = inlineComponent.getFontSize() * fontMultiplier; inlineComponent.calculateInlines( inlineComponent._fitFontSize ); } ); const lines = this.computeLines(); return Math.abs( lines.height ); } /** * Compute the width of a line * @param line * @returns {number} */ computeLineWidth( line ) { // only by the length of its extremities const firstInline = line[ 0 ]; const lastInline = line[ line.length - 1 ]; // Right + Left ( left is negative ) return (lastInline.offsetX + lastInline.width) + firstInline.offsetX; } }; } ;// CONCATENATED MODULE: ./src/components/core/FontLibrary.js /* Job: Keeping record of all the loaded fonts, which component use which font, and load new fonts if necessary Knows: Which component use which font, loaded fonts This is one of the only modules in the 'component' folder that is not used for composition (Object.assign). MeshUIComponent is the only module with a reference to it, it uses FontLibrary for recording fonts accross components. This way, if a component uses the same font as another, FontLibrary will skip loading it twice, even if the two component are not in the same parent/child hierarchy */ const fileLoader = new external_three_namespaceObject.FileLoader(); const requiredFontFamilies = []; const fontFamilies = {}; const textureLoader = new external_three_namespaceObject.TextureLoader(); const requiredFontTextures = []; const fontTextures = {}; const records = {}; /** Called by MeshUIComponent after fontFamily was set When done, it calls MeshUIComponent.update, to actually display the text with the loaded font. */ function setFontFamily( component, fontFamily ) { if ( typeof fontFamily === 'string' ) { loadFontJSON( component, fontFamily ); } else { // keep record of the font that this component use if ( !records[ component.id ] ) records[ component.id ] = { component }; // Ensure the font json is processed _buildFriendlyKerningValues( fontFamily ); records[ component.id ].json = fontFamily; component._updateFontFamily( fontFamily ); } } /** Called by MeshUIComponent after fontTexture was set When done, it calls MeshUIComponent.update, to actually display the text with the loaded font. */ function setFontTexture( component, url ) { // if this font was never asked for, we load it if ( requiredFontTextures.indexOf( url ) === -1 ) { requiredFontTextures.push( url ); textureLoader.load( url, ( texture ) => { texture.generateMipmaps = false; texture.minFilter = external_three_namespaceObject.LinearFilter; texture.magFilter = external_three_namespaceObject.LinearFilter; fontTextures[ url ] = texture; for ( const recordID of Object.keys( records ) ) { if ( url === records[ recordID ].textureURL ) { // update all the components that were waiting for this font for an update records[ recordID ].component._updateFontTexture( texture ); } } } ); } // keep record of the font that this component use if ( !records[ component.id ] ) records[ component.id ] = { component }; records[ component.id ].textureURL = url; // update the component, only if the font is already requested and loaded if ( fontTextures[ url ] ) { component._updateFontTexture( fontTextures[ url ] ); } } /** used by Text to know if a warning must be thrown */ function getFontOf( component ) { const record = records[ component.id ]; if ( !record && component.parentUI ) { return getFontOf( component.parentUI ); } return record; } /** Load JSON file at the url provided by the user at the component attribute 'fontFamily' */ function loadFontJSON( component, url ) { // if this font was never asked for, we load it if ( requiredFontFamilies.indexOf( url ) === -1 ) { requiredFontFamilies.push( url ); fileLoader.load( url, ( text ) => { // FileLoader import as a JSON string const font = JSON.parse( text ); // Ensure the font json is processed _buildFriendlyKerningValues( font ); fontFamilies[ url ] = font; for ( const recordID of Object.keys( records ) ) { if ( url === records[ recordID ].jsonURL ) { // update all the components that were waiting for this font for an update records[ recordID ].component._updateFontFamily( font ); } } } ); } // keep record of the font that this component use if ( !records[ component.id ] ) records[ component.id ] = { component }; records[ component.id ].jsonURL = url; // update the component, only if the font is already requested and loaded if ( fontFamilies[ url ] ) { component._updateFontFamily( fontFamilies[ url ] ); } } /** * From the original json font kernings array * First : Reduce the number of values by ignoring any kerning defining an amount of 0 * Second : Update the data structure of kernings from * {Array} : [{first: 97, second: 121, amount: 0},{first: 97, second: 122, amount: -1},...] * to * {Object}: {"ij":-2,"WA":-3,...}} * * @private */ function _buildFriendlyKerningValues( font ) { // As "font registering" can comes from different paths : addFont, loadFontJSON, setFontFamily // Be sure we don't repeat this operation if ( font._kernings ) return; const friendlyKernings = {}; for ( let i = 0; i < font.kernings.length; i++ ) { const kerning = font.kernings[ i ]; // ignore zero kerned glyph pair if ( kerning.amount === 0 ) continue; // Build and store the glyph paired characters "ij","WA", ... as keys, referecing their kerning amount const glyphPair = String.fromCharCode( kerning.first, kerning.second ); friendlyKernings[ glyphPair ] = kerning.amount; } // update the font to keep it font._kernings = friendlyKernings; } /* This method is intended for adding manually loaded fonts. Method assumes font hasn't been loaded or requested yet. If it was, font with specified name will be overwritten, but components using it won't be updated. */ function addFont( name, json, texture ) { texture.generateMipmaps = false; texture.minFilter = external_three_namespaceObject.LinearFilter; texture.magFilter = external_three_namespaceObject.LinearFilter; requiredFontFamilies.push( name ); fontFamilies[ name ] = json; // Ensure the font json is processed _buildFriendlyKerningValues( json ); if ( texture ) { requiredFontTextures.push( name ); fontTextures[ name ] = texture; } } // const FontLibrary = { setFontFamily, setFontTexture, getFontOf, addFont }; /* harmony default export */ const core_FontLibrary = (FontLibrary); ;// CONCATENATED MODULE: ./src/components/core/UpdateManager.js /** * Job: * - recording components required updates * - trigger those updates when 'update' is called * * This module is a bit special. It is, with FontLibrary, one of the only modules in the 'component' * directory not to be used in component composition (Object.assign). * * When MeshUIComponent is instanciated, it calls UpdateManager.register(). * * Then when MeshUIComponent receives new attributes, it doesn't update the component right away. * Instead, it calls UpdateManager.requestUpdate(), so that the component is updated when the user * decides it (usually in the render loop). * * This is best for performance, because when a UI is created, thousands of componants can * potentially be instantiated. If they called updates function on their ancestors right away, * a given component could be updated thousands of times in one frame, which is very ineficient. * * Instead, redundant update request are moot, the component will update once when the use calls * update() in their render loop. */ class UpdateManager { /* * get called by MeshUIComponent when component.set has been used. * It registers this component and all its descendants for the different types of updates that were required. */ static requestUpdate( component, updateParsing, updateLayout, updateInner ) { component.traverse( ( child ) => { if ( !child.isUI ) return; // request updates for all descendants of the passed components if ( !this.requestedUpdates[ child.id ] ) { this.requestedUpdates[ child.id ] = { updateParsing, updateLayout, updateInner, needCallback: ( updateParsing || updateLayout || updateInner ) }; } else { if ( updateParsing ) this.requestedUpdates[ child.id ].updateParsing = true; if ( updateLayout ) this.requestedUpdates[ child.id ].updateLayout = true; if ( updateInner ) this.requestedUpdates[ child.id ].updateInner = true; } } ); } /** Register a passed component for later updates */ static register( component ) { if ( !this.components.includes( component ) ) { this.components.push( component ); } } /** Unregister a component (when it's deleted for instance) */ static disposeOf( component ) { const idx = this.components.indexOf( component ); if ( idx > -1 ) { this.components.splice( idx, 1 ); } } /** Trigger all requested updates of registered components */ static update() { if ( Object.keys( this.requestedUpdates ).length > 0 ) { const roots = this.components.filter( ( component ) => { return !component.parentUI; } ); roots.forEach( root => this.traverseParsing( root ) ); roots.forEach( root => this.traverseUpdates( root