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.

540 lines • 22.5 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Color } from 'three'; import * as ThreeMeshUI from 'three-mesh-ui'; import { serializable } from '../../engine/engine_serialization_decorator.js'; import { getParam } from '../../engine/engine_utils.js'; import { Graphic } from './Graphic.js'; import { updateRenderSettings } from './Utils.js'; const debug = getParam("debugtext"); export var TextAnchor; (function (TextAnchor) { TextAnchor[TextAnchor["UpperLeft"] = 0] = "UpperLeft"; TextAnchor[TextAnchor["UpperCenter"] = 1] = "UpperCenter"; TextAnchor[TextAnchor["UpperRight"] = 2] = "UpperRight"; TextAnchor[TextAnchor["MiddleLeft"] = 3] = "MiddleLeft"; TextAnchor[TextAnchor["MiddleCenter"] = 4] = "MiddleCenter"; TextAnchor[TextAnchor["MiddleRight"] = 5] = "MiddleRight"; TextAnchor[TextAnchor["LowerLeft"] = 6] = "LowerLeft"; TextAnchor[TextAnchor["LowerCenter"] = 7] = "LowerCenter"; TextAnchor[TextAnchor["LowerRight"] = 8] = "LowerRight"; })(TextAnchor || (TextAnchor = {})); export var VerticalWrapMode; (function (VerticalWrapMode) { VerticalWrapMode[VerticalWrapMode["Truncate"] = 0] = "Truncate"; VerticalWrapMode[VerticalWrapMode["Overflow"] = 1] = "Overflow"; })(VerticalWrapMode || (VerticalWrapMode = {})); var HorizontalWrapMode; (function (HorizontalWrapMode) { HorizontalWrapMode[HorizontalWrapMode["Wrap"] = 0] = "Wrap"; HorizontalWrapMode[HorizontalWrapMode["Overflow"] = 1] = "Overflow"; })(HorizontalWrapMode || (HorizontalWrapMode = {})); export var FontStyle; (function (FontStyle) { FontStyle[FontStyle["Normal"] = 0] = "Normal"; FontStyle[FontStyle["Bold"] = 1] = "Bold"; FontStyle[FontStyle["Italic"] = 2] = "Italic"; FontStyle[FontStyle["BoldAndItalic"] = 3] = "BoldAndItalic"; })(FontStyle || (FontStyle = {})); /** * @category User Interface * @group Components */ export class Text extends Graphic { alignment = TextAnchor.UpperLeft; verticalOverflow = VerticalWrapMode.Truncate; horizontalOverflow = HorizontalWrapMode.Wrap; lineSpacing = 1; supportRichText = false; font; fontStyle = FontStyle.Normal; // private _alphaFactor : number = 1; setAlphaFactor(factor) { super.setAlphaFactor(factor); this.uiObject?.set({ fontOpacity: this.color.alpha * this.alphaFactor }); this.markDirty(); } get text() { return this._text; } set text(val) { if (val !== this._text) { this._text = val; this.feedText(this.text, this.supportRichText); this.markDirty(); } } set_text(val) { this.text = val; } get fontSize() { return this._fontSize; } set fontSize(val) { // Setting that kind of property in a parent, would cascade to each 'non-overrided' children. this._fontSize = val; this.uiObject?.set({ fontSize: val }); } sRGBTextColor = new Color(1, 0, 1); onColorChanged() { this.sRGBTextColor.copy(this.color); this.sRGBTextColor.convertLinearToSRGB(); this.uiObject?.set({ color: this.sRGBTextColor, fontOpacity: this.color.alpha }); } onParentRectTransformChanged() { super.onParentRectTransformChanged(); if (this.uiObject) { this.updateOverflow(); } } // onBeforeRender(): void { // // TODO TMUI @swingingtom this is so we don't have text clipping // if (this.uiObject && (this.Canvas?.screenspace || this.context.isInVR)) { // this.updateOverflow(); // } // } onBeforeCanvasRender(_canvas) { // ensure the text clipping matrix is updated (this was a problem with multiple screenspace canvases due to canvas reparenting) this.updateOverflow(); } updateOverflow() { // HACK: force the text overflow to update const overflow = this.uiObject?._overflow; if (overflow) { overflow._needsUpdate = true; // the screenspace canvas does force an update, no need to mark dirty here } } onCreate(_opts) { if (debug) console.log(this); if (this.horizontalOverflow == HorizontalWrapMode.Overflow) { // Only line characters in the textContent (\n,\r\t) would be able to multiline the text _opts.whiteSpace = 'pre'; } if (this.verticalOverflow == VerticalWrapMode.Truncate) { this.context.renderer.localClippingEnabled = true; _opts.overflow = 'hidden'; } // @marwie : this combination is currently KO. See sample "Overflow Overview" if (this.horizontalOverflow == HorizontalWrapMode.Overflow && this.verticalOverflow == VerticalWrapMode.Truncate) { // This could fix this combination, but would require anchors updates to replace element // _opts.width = 'auto'; } _opts.lineHeight = this.lineSpacing; // @marwie : Should be fixed. Currently _opts are always fed with : // backgroundOpacity : color.opacity // backgroundColor : color delete _opts.backgroundOpacity; delete _opts.backgroundColor; // helper to show bounds of text element if (debug) { _opts.backgroundColor = 0xff9900; _opts.backgroundOpacity = 0.5; } const rt = this.rectTransform; // Texts now support both options, block and inline, and inline has all default to inherit _opts = { ..._opts, ...this.getTextOpts() }; this.getAlignment(_opts); if (debug) { _opts.backgroundColor = Math.random() * 0xffffff; _opts.backgroundOpacity = 0.1; } this.uiObject = rt.createNewText(_opts); this.feedText(this.text, this.supportRichText); } onAfterAddedToScene() { super.onAfterAddedToScene(); this.handleTextRenderOnTop(); } _text = ""; _fontSize = 12; _textMeshUi = null; getTextOpts() { const fontSize = this.fontSize; // if (this.canvas) { // fontSize /= this.canvas?.scaleFactor; // } const textOpts = { color: this.color, fontOpacity: this.color.alpha, fontSize: fontSize, fontKerning: "normal", }; this.setFont(textOpts, this.fontStyle); return textOpts; } onEnable() { super.onEnable(); this._didHandleTextRenderOnTop = false; if (this.uiObject) { // @ts-ignore // @TODO : Evaluate the need of keeping it anonymous. // From v7.x afterUpdate can be removed but requires a reference this.uiObject.addAfterUpdate(() => { // We need to update the shadow owner when the text updates // because once the font has loaded we get new children (a new mesh) // which is the text, it needs to be linked back to this component // to be properly handled by the EventSystem // since the EventSystem looks for shadow component owners to handle events this.setShadowComponentOwner(this.uiObject); this.markDirty(); }); } setTimeout(() => this.markDirty(), 10); this.canvas?.registerEventReceiver(this); } onDisable() { super.onDisable(); this.canvas?.unregisterEventReceiver(this); } getAlignment(opts) { opts.flexDirection = "column"; switch (this.alignment) { case TextAnchor.UpperLeft: case TextAnchor.MiddleLeft: case TextAnchor.LowerLeft: opts.textAlign = "left"; break; case TextAnchor.UpperCenter: case TextAnchor.MiddleCenter: case TextAnchor.LowerCenter: opts.textAlign = "center"; break; case TextAnchor.UpperRight: case TextAnchor.MiddleRight: case TextAnchor.LowerRight: opts.textAlign = "right"; break; } switch (this.alignment) { default: case TextAnchor.UpperLeft: case TextAnchor.UpperCenter: case TextAnchor.UpperRight: opts.alignItems = "start"; break; case TextAnchor.MiddleLeft: case TextAnchor.MiddleCenter: case TextAnchor.MiddleRight: opts.alignItems = "center"; break; case TextAnchor.LowerLeft: case TextAnchor.LowerCenter: case TextAnchor.LowerRight: opts.alignItems = "end"; break; } return opts; } feedText(text, richText) { // if (!text || text.length <= 0) return; // if (!text ) return; if (debug) console.log("feedText", this.uiObject, text, richText); if (!this.uiObject) return; if (!this._textMeshUi) this._textMeshUi = []; // this doesnt work and produces errors when length is 0: // this.uiObject.textContent = " "; // reset the current text (e.g. when switching from "Hello" to "Hello <b>World</b>") // @TODO swingingtom: this is a hack to reset the text content, not sure how to do that right this.uiObject.children.length = 0; if (!richText || text.length === 0) { //@TODO: @swingingtom how would the text content be set? //@ts-ignore this.uiObject.textContent = text; } else { let currentTag = this.getNextTag(text); if (!currentTag) { //@ts-ignore // we have to set it to empty string, otherwise TMUI won't update it @swingingtom this.uiObject.textContent = ""; // < this.setOptions({ textContent: text }); return; } else if (currentTag.startIndex > 0) { // First segment should also clear children inlines for (let i = this.uiObject.children.length - 1; i >= 0; i--) { const child = this.uiObject.children[i]; // @ts-ignore if (child.isUI) { this.uiObject.remove(child); child.clear(); } } const el = new ThreeMeshUI.Inline({ textContent: text.substring(0, currentTag.startIndex), color: 'inherit' }); this.uiObject.add(el); } const stackArray = []; while (currentTag) { const next = this.getNextTag(text, currentTag.endIndex); const opts = { fontFamily: this.uiObject?.get('fontFamily'), color: 'inherit', textContent: "" }; if (next) { opts.textContent = this.getText(text, currentTag, next); this.handleTag(currentTag, opts, stackArray); const el = new ThreeMeshUI.Inline(opts); this.uiObject?.add(el); } else { opts.textContent = text.substring(currentTag.endIndex); this.handleTag(currentTag, opts, stackArray); const el = new ThreeMeshUI.Inline(opts); this.uiObject?.add(el); } currentTag = next; } } } _didHandleTextRenderOnTop = false; handleTextRenderOnTop() { if (this._didHandleTextRenderOnTop) return; this._didHandleTextRenderOnTop = true; this.startCoroutine(this.renderOnTopCoroutine()); } // waits for all the text objects to be ready to set the render on top setting // @THH : this isn't true anymore. We can set mesh and material properties before their counterparts are created. // Values would automatically be passed when created. Not sure for depthWrite but it can be added; *renderOnTopCoroutine() { if (!this.canvas) return; const updatedRendering = []; const canvas = this.canvas; const settings = { renderOnTop: canvas.renderOnTop, depthWrite: canvas.depthWrite, doubleSided: canvas.doubleSided }; while (true) { let isWaitingForElementToUpdate = false; if (this._textMeshUi) { for (let i = 0; i < this._textMeshUi.length; i++) { if (updatedRendering[i] === true) continue; isWaitingForElementToUpdate = true; const textMeshObject = this._textMeshUi[i]; // text objects have this textContent which is the mesh // it is not ready immediately so we have to check if it exists // and only then setting the render on top property works if (!textMeshObject["textContent"]) continue; updateRenderSettings(textMeshObject, settings); updatedRendering[i] = true; // console.log(textMeshObject); } } if (!isWaitingForElementToUpdate) break; yield; } } handleTag(tag, opts, stackArray) { // console.log(tag); if (!tag.isEndTag) { if (tag.type.includes("color")) { const stackEntry = new TagStackEntry(tag, { color: opts.color }); stackArray.push(stackEntry); if (tag.type.length > 6) // color= { const col = parseInt("0x" + tag.type.substring(7)); opts.color = col; } else { // if it does not contain a color it is white opts.color = new Color(1, 1, 1); } } else if (tag.type == "b") { this.setFont(opts, FontStyle.Bold); const stackEntry = new TagStackEntry(tag, { fontWeight: 700, }); stackArray.push(stackEntry); } else if (tag.type == "i") { this.setFont(opts, FontStyle.Italic); const stackEntry = new TagStackEntry(tag, { fontStyle: 'italic' }); stackArray.push(stackEntry); } } } getText(text, start, end) { return text.substring(start.endIndex, end.startIndex); } getNextTag(text, startIndex = 0) { const start = text.indexOf("<", startIndex); const end = text.indexOf(">", start); if (start >= 0 && end >= 0) { const tag = text.substring(start + 1, end); return { type: tag, startIndex: start, endIndex: end + 1, isEndTag: tag.startsWith("/") }; } return null; } /** * Update provided opts to have a proper fontDefinition : family+weight+style * Ensure Family and Variant are registered in FontLibrary * * @param opts * @param fontStyle * @private */ setFont(opts, fontStyle) { // @TODO : THH could be useful to uniformize font family name : // This would ease possible html/vr matching // - Arial instead of assets/arial // - Arial should stay Arial instead of arial if (!this.font) return; const fontName = this.font; const familyName = this.getFamilyNameWithCorrectSuffix(fontName, fontStyle); if (debug) console.log("Selected font family:" + familyName); // ensure a font family is register under this name let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(familyName); if (!fontFamily) fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(familyName); // @TODO: @swingingtom how should the font be set? //@ts-ignore opts.fontFamily = fontFamily; switch (fontStyle) { default: case FontStyle.Normal: opts.fontWeight = 400; opts.fontStyle = "normal"; break; case FontStyle.Bold: opts.fontWeight = 700; opts.fontStyle = "normal"; break; case FontStyle.Italic: opts.fontWeight = 400; opts.fontStyle = "italic"; break; case FontStyle.BoldAndItalic: opts.fontStyle = 'italic'; opts.fontWeight = 400; } // Ensure a fontVariant is registered //@TODO: @swingingtom add type for fontWeight let fontVariant = fontFamily.getVariant(opts.fontWeight, opts.fontStyle); if (!fontVariant) { let jsonPath = familyName; if (!jsonPath?.endsWith("-msdf.json")) jsonPath += "-msdf.json"; let texturePath = familyName; if (!texturePath?.endsWith(".png")) texturePath += ".png"; //@TODO: @swingingtom add type for fontWeight //@TODO: @swingingtom addVariant return type is wrong (should be FontVariant) fontVariant = fontFamily.addVariant(opts.fontWeight, opts.fontStyle, jsonPath, texturePath); /** @ts-ignore */ fontVariant?.addEventListener('ready', () => { this.markDirty(); }); } } getFamilyNameWithCorrectSuffix(familyName, style) { // we can only change the style for the family if the name has a suffix (e.g. Arial-Bold) const styleSeparator = familyName.lastIndexOf('-'); if (styleSeparator < 0) return familyName; // Check if the font name contains a style that we don't support in the enum // e.g. -Medium, -Black, -Thin... const styleName = familyName.substring(styleSeparator + 1)?.toLowerCase(); if (unsupportedStyleNames.includes(styleName)) { if (debug) console.warn("Unsupported font style: " + styleName); return familyName; } // Try find a suffix that matches the style // We assume that if the font name is "Arial-Regular" then the bold version is "Arial-Bold" // and if the font name is "arial-regular" then the bold version is "arial-bold" const pathSeparatorIndex = familyName.lastIndexOf("/"); let fontBaseName = familyName; if (pathSeparatorIndex >= 0) { fontBaseName = fontBaseName.substring(pathSeparatorIndex + 1); } const isUpperCase = fontBaseName[0] === fontBaseName[0].toUpperCase(); const fontNameWithoutSuffix = familyName.substring(0, styleSeparator); if (debug) console.log("Select font: ", familyName, FontStyle[style], fontBaseName, isUpperCase, fontNameWithoutSuffix); switch (style) { case FontStyle.Normal: if (isUpperCase) return fontNameWithoutSuffix + "-Regular"; else return fontNameWithoutSuffix + "-regular"; case FontStyle.Bold: if (isUpperCase) return fontNameWithoutSuffix + "-Bold"; else return fontNameWithoutSuffix + "-bold"; case FontStyle.Italic: if (isUpperCase) return fontNameWithoutSuffix + "-Italic"; else return fontNameWithoutSuffix + "-italic"; case FontStyle.BoldAndItalic: if (isUpperCase) return fontNameWithoutSuffix + "-BoldItalic"; else return fontNameWithoutSuffix + "-bolditalic"; default: return familyName; } } } __decorate([ serializable() ], Text.prototype, "alignment", void 0); __decorate([ serializable() ], Text.prototype, "verticalOverflow", void 0); __decorate([ serializable() ], Text.prototype, "horizontalOverflow", void 0); __decorate([ serializable() ], Text.prototype, "lineSpacing", void 0); __decorate([ serializable() ], Text.prototype, "supportRichText", void 0); __decorate([ serializable(URL) ], Text.prototype, "font", void 0); __decorate([ serializable() ], Text.prototype, "fontStyle", void 0); __decorate([ serializable() ], Text.prototype, "text", null); __decorate([ serializable() ], Text.prototype, "fontSize", null); class TagStackEntry { tag; previousValues; constructor(tag, previousValues) { this.tag = tag; this.previousValues = previousValues; } } // const anyTag = new RegExp('<.+?>', 'g'); // const regex = new RegExp('<(?<type>.+?)>(?<text>.+?)<\/.+?>', 'g'); const unsupportedStyleNames = [ "medium", "mediumitalic", "black", "blackitalic", "thin", "thinitalic", "extrabold", "light", "lightitalic", "semibold" ]; //# sourceMappingURL=Text.js.map