Jump to content

[WIP] Floating, colored, changeable text in KSP Flight: a brief tutorial.


Fengist

Recommended Posts

This brief tutorial is primarily for plugin developers. HOWEVER, as I get time I plan to make this a standalone plugin for those modders who can't code.  Once I get a plugin working, I'll start a release thread and you'll find the link here.

So let's get started with what this tutorial shows:

NhAV5YD.png

 

The Problem - font shaders

In the past, there have been numerous questions on the forums about how to add text with fonts to parts in KSP.  And in the past, it was difficult if not impossible to do in KSP.  Unity has a TextMesh component that works in all Unity games. And it even works in KSP.  The problem with the TextMesh component is that it requires it's own unique shader.  Without that shader, the text will be visible even if it's inside a part or behind a part.  This made it almost useless in KSP.  I have not tried, but I'm assuming that since no one has accomplished this yet that creating a shader for TextMesh is either too difficult or they can't be brought into KSP because of it's unique shader system.

The Solution - TMPro

With the addition of localization, KSP has added a new component that has recently been incorporated into Unity, TextMeshPro (TMP).  After playing with it a good bit, I've discovered that it's a HUGELY powerful text/font utility.  And the good news, it includes it's own shaders by default.  And because KSP has added TMP as one of it's assets, those shaders were included in the package.  This means, KSP modders now have access to not only the TMP component, but the shaders and all of the fonts loaded into KSP as well.

The Next Problem - PartTools

You can take any part you're creating in Unity and add a TMP component to it.  And you can even get the text to display. The problem arises when you try to export that part using PartTools.  Squad probably didn't think that anyone would attempt to export a part to KSP with the TMP component on it and didn't include it in the latest release of PartTools. When you try to export the part, it politely gives you an error in the Unity console and refuses to export it.

The Next Solution - Dynamic TMPro

While PartTools chokes and dies trying to export a TMP component, KSP has no problem creating one on the fly.  The basic technique I used is to create a flat panel in my modelling software (and I'll explain why I do this in a moment). In unity, in the hierarchy, I give this panel a unique name that will match a variable in my part.cfg, like "screen".  In order to have it displayed as floating text I give the screen a completely transparent .png texture and set the shader to KSP/Alpha/Translucent.

In the screenshot below I have a semi-transparent texture so that you can see the screen dimensions.  Were I to use the completely transparent texture, it would appear invisible.

u4o7mOh.png

So, as you can see, this is a very, very simple part.  I've added a stock Unity cube and the screen I created in my modeling software. I created it in the modeling software so that I could set it's dimensions.  As of this moment, I haven't found a satisfactory way to create or resize this screen in Unity. Reason being, Unity expects a part to be of a particular dimension and you really can't adjust it. You can 'scale' the part but that doesn't truly 'resize' it.  The dimension of this 'screen' will play a role in the size of the TMP component and I'm still working on a way to take the unity 'scale' factor and resize the TMP component to match without stretching and deforming the text.  So, for now, I use a fixed size screen.  One thing to take note of if you decide to play around with this, the rotation of the screen in the modeling software is pretty critical, and I'll explain this in a moment.  Just realize that if you do get this working and your fonts are in the wrong location or not visible, there's a good chance the rotation of the screen is off.

The Config File

I created a PartModule for this little experiment so that, once I get all the details worked out, it can be easily incorporated into any part you create.  And for this experiment, the Module in the .cfg file is very, very simple

    MODULE
    {
        name = ModuleKETestFont        
        ScreenName = screen
        screenText = Now is the time for all good men to come to the aid of their country
    }

Naturally, the name of the Plugin. Next is the name of the part I labeled in Unity that I want to be my display screen for the text.  Finally, the text I want to show.  In my other mod, which I'll link to below, this text is also dynamic and changes based on user interaction.  For this simple demo, it's fixed in the part.cfg file.

The Code

Eventually, once I get a true working plugin, I'll put this code up on GitHub.  Because this is still highly experimental, I'm going to paste it here so you get an idea of how it works.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;

namespace ModuleKELights
{
    class ModuleKETestFont : PartModule
    {
        Color lastColor;
        float lastFontSize = 0;

        [KSPField(isPersistant = true)]
        Color currentColor;

        TextMeshPro LCDTextMesh = null;

        [KSPField]
        public string screenText = "None";

        [KSPField]
        public string ScreenName;

        TMP_FontAsset[] loadedFonts;

        bool autoFont = true;

        [KSPField(guiName = "#autoLOC_6001402", isPersistant = true, guiActive = true, guiActiveEditor = true), UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.05f)]
        public float fontColorR = 1f;

        [KSPField(guiName = "#autoLOC_6001403", isPersistant = true, guiActive = true, guiActiveEditor = true), UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.05f)]
        public float fontColorG = 1f;

        [KSPField(guiName = "#autoLOC_6001404", isPersistant = true, guiActive = true, guiActiveEditor = true), UI_FloatRange(minValue = 0f, maxValue = 1f, stepIncrement = 0.05f)]
        public float fontColorB = 1f;

        [KSPField(guiName = "Font Size", isPersistant = true, guiActive = true, guiActiveEditor = true), UI_FloatRange(minValue = 0.1f, maxValue = 10f, stepIncrement = 0.05f)]
        public float fontSize = 1f;

        [KSPField(guiActiveEditor = true, guiActive = true, guiName = "Current Font")]
        public string fontName = string.Empty;

        [KSPField(guiActiveEditor = true, guiActive = true, guiName = "Current Font Index", isPersistant = true)]
        public int loadedFontIndex = 0;

        [KSPEvent(guiActive = true, guiActiveEditor = true, guiActiveUnfocused = false, guiName = "Next Font")]
        public void nextFont()
        {
            loadedFontIndex++;
            if (loadedFontIndex > loadedFonts.Length - 1)
            {
                loadedFontIndex = 0;
            }
            SetFontName(loadedFontIndex);
        }

        [KSPEvent(guiActive = true, guiActiveEditor = true, guiActiveUnfocused = false, guiName = "Prev Font")]
        public void previousFont()
        {
            loadedFontIndex--;
            if (loadedFontIndex < 0)
            {
                loadedFontIndex = loadedFonts.Length - 1;
            }
            SetFontName(loadedFontIndex);
        }

        public override void OnCopy(PartModule fromModule)
        {
            base.OnCopy(fromModule);
            TextMeshPro thisMesh = GetComponentInChildren<TextMeshPro>();
            if (thisMesh != null)
            {
                thisMesh.gameObject.DestroyGameObject();
            }
        }

        public void ShowText()
        {
            GameObject LCDScreen = new GameObject();
            Transform screenTransform = this.part.FindModelTransform(ScreenName);
            LCDScreen.transform.parent = screenTransform;
            LCDScreen.transform.localRotation = screenTransform.localRotation;
            LCDScreen.transform.localRotation = Quaternion.Euler(0, 180f, 0);
            LCDTextMesh = LCDScreen.AddComponent<TextMeshPro>();

            Mesh M = screenTransform.GetComponent<MeshFilter>().mesh;
            RectTransform T = LCDTextMesh.gameObject.GetComponent<RectTransform>();
            T.sizeDelta = new Vector2(M.bounds.size.x, M.bounds.size.y);
            LCDScreen.transform.localPosition = new Vector3(0, 0, (M.bounds.size.z / 2) + 0.01f);

            Debug.Log("TM Created " +T.sizeDelta.x + " " + T.sizeDelta.y);

            LCDTextMesh.enableAutoSizing = autoFont;
            LCDTextMesh.fontSizeMin = 0.1f;
            LCDTextMesh.overflowMode = TextOverflowModes.Truncate;
            LCDTextMesh.alignment = TextAlignmentOptions.Center;

            LCDTextMesh.text = screenText;
        }

        public void SetFontName(int thisFontIndex)
        {
            fontName = loadedFonts[loadedFontIndex].name;
            LCDTextMesh.font = loadedFonts[thisFontIndex];
        }

        public void SetFontColor(Color thisColor)
        {
            LCDTextMesh.color = thisColor;
            lastColor = thisColor;
        }

        public void SetFontSize(float thisFontSize)
        {
            LCDTextMesh.fontSize = thisFontSize;
            lastFontSize = thisFontSize;
        }

        public override void OnAwake()
        {
            loadedFonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
            //example of how to load a Unity font. Not needed for TMP.
            UnityEngine.Object[] fonts = UnityEngine.Resources.FindObjectsOfTypeAll(typeof(UnityEngine.Font));
        }

        public override void OnStart(StartState state)
        {
            ShowText();
        }

        public void FixedUpdate()
        {
            if (LCDTextMesh == null)
            {
                return;
            }
            currentColor = new Color(fontColorR, fontColorG, fontColorB, 1);
            if (currentColor != lastColor)
            {
                SetFontColor(currentColor);
            }
            if (!autoFont && fontSize != lastFontSize)
            {
                SetFontSize(fontSize);
            }
        }
    }
}

OnAwake

First, the lesser important function, fonts.  TMP comes with a number of very basic fonts installed. And Squad has loaded a couple of their own.  In order to get these fonts, they need to be loaded from the resources. The OnAwake event handles that.  All of the fonts are loaded into a list of <TMP_FontAsset>.  The user can change these in-game by clicking on a KSPEvent button to cycle through the fonts.

OnStart

The important function, creating the TMP component.  The reason I create a screen in Unity is so that I have something of a fixed dimension to 'stick' the TMP component onto.  When OnStart runs it calls the ShowText function.  First, I create a new GameObject that I just call LCDScreen that's going to hold the TMP component.  I set it's parent to the 'screen' I created in Unity and then, adjust the rotation.  You'll notice I make two changes to the rotation, this is just me experimenting.  The LCDScreen location is, by default, assigned to the Unity 'screen' location.  I assume because of part rotation, I have a hell of a time getting the all of the parts rotated and pointed in the same direction, hence the Quaternian.Euler rotation.

Next I take the TMP component and stick it to the LCDScreen GameObject.  The next 4 lines define the size of the TMP component and create a RectTransform that it's going to need. Because text meshes are 2D, they use the RectTransform rather than a normal transform.  The fourth line moves the local z position so that it hovers just above the Unity 'screen'.  This is only necessary if you're using an opaque background that you're displaying the text on. Without this little step, the text would end up buried in the middle of the screen.

The debug logging tells me the size of the TMP component it creates. I have this there because if you have the rotation incorrect, you can still see that it's creating the component but it's dimensions won't be proportional to the Unity 'screen' and it may end up on the thin side.  If the proportions look wrong, the rotation is wrong.  The last lines merely define some basics for the TMP component.  TMP has one really cool feature called autosizing.  It will look at the size of it's component, look at the amount of text you want to display and attempt to set a font size so that all of the text gets on the screen.  The .fontSizeMin determines the smallest font size It'll use when autosizing.

And finally, for this little experiment, it pulls the screenText from the .cfg file and sets the TMP text to it.  And, like magic, you have text on your screen.

OnCopy

This event fires if you're in the hangar and you hit that x button to duplicate a part.  I've tried several methods to avoid doing it this way but this is one that works so I stuck with it.  When you create a copy of a part that has a dynamic TMP component on it, it duplicates it as well.  The problem there is, the duplicated part, for some reason I haven't figured out, doesn't recognize that the TMP component exists so it creates another in the OnStart for the duplicated part.  So, in the event, I look to see if a TMP component already exists on the part and if so, get rid of it.  After that, the OnStart fires for the duplicated part and creates a new one.

The final proof

Here's an image with the TMP component created on an opaque screen.

yUpKTst.png

And here's the back side of that screen showing that the text does not 'bleed' through the part like it would if it were a normal Unity TextMesh

wUVVBf6.png

What's possible?
When I stumbled onto this looking for a solution to creating one part that could be set up to monitor any resource, I rather quickly realized it's potential when it comes to modding, like naming your ships and having that name appear on the side of them in any color you want, holographic text projectors that display the amount of resources in floating text above a part, floating text above a cockpit to show any... ANY in-flight data.  While I would have loved to keep this little secret all to myself, I don't nearly have the time to exploit it's potential.  I will have time in the coming weeks to create a standalone plugin to help part developers display static text on any part as well as a plugin that other plugin developers can use to quickly call functions and display text of their chosing.

jeVF1Gm.png

Fonts

In the future, it should be possible to add your own custom fonts to the game and have them displayed. I have accomplished this feat once but can't seem to reproduce it.  The reason is a convoluted story.  TMP was a paid asset that you could purchase and add onto Unity.  Squad did just that for it's localization efforts.  Since then, TMP has been purchased by Unity and made free, with a few exceptions.  First, the free version does not include the source.  Next, the free version and previous paid versions are incompatible with each other.  The dev who created TMP has decided to continue to support those who purchased TMP but keep it, for whatever reason, separate from the free version.  Because the TMP paid version is embedded in KSP, modders aren't currently able to successfully import fonts using the free version.  While there is a supposed workaround that allows the font to be brought into KSP, I can't get it to work with any consistency.  Here's the link to @JPLRepo's post about how to load fonts and a link to the workaround. If you get this working, LET ME KNOW!

So there you have it.  The basis for displaying changing, floating, colored text in KSP.

A Working Mod

I currently have one WIP mod that uses this exact technique so that I created exactly one part, yet it can be configured to monitor any resource loaded into the game and display that resource name on a 'screen'.  If you'd like to see this technique in action, download the mod linked below and add a "Idiot Light' to your ship.

rkXcnBh.png

Question & comments welcomed.

____________________________________________________________________________________

thanks to @InfiniteDice who tolerates an inordinate amount of coding related sarcasm via Skype whilst I bash my skull against my desk

Edited by Fengist
Link to comment
Share on other sites

Thanks man for this very valuable information.
With very little changes (mostly removing some options for the players), the code you posted is ideal to add localization support to (nearly) all texts displayed on models in mods without the need for us modders to create an new alternative texture for every language.

Link to comment
Share on other sites

1 hour ago, Nils277 said:

Thanks man for this very valuable information.
With very little changes (mostly removing some options for the players), the code you posted is ideal to add localization support to (nearly) all texts displayed on models in mods without the need for us modders to create an new alternative texture for every language.

I hadn't actually considered that but, yes it could do that very easily.  Here's the link I meant to put in the OP with instructions on how to create and load fonts (that I can't get to work well).  

I believe you should be able to find out from the fontloader or the localizer used in that link's examples, which language is currently set.  From that you should be able to get the language specific font from the fontloader which you can set any TMP component to.   A plugin for this could have an option to automatically switch to the current language font.  The only problem then becomes translating your text which you could do with a Localizer .cfg file.  Setting the TMP text to an #autoLOC_XXXXX should also work.

One thing to kinda keep in the back of your mind is that this MAY become a good bit easier to do if Squad can sort out the issues with TMP and PartTools.  Should they get it so that you can write a .mu with a TMP component on it, this all becomes rather moot.  You'll just be able to add your component in Unity.  So, for now this method is a patch to get text attached to parts.  The good news, even if they do get PartTools to work with TMP,  this method should still function and you shouldn't be forced to rework any parts that use it.  And, if a plugin were made robust enough, it might even give modders more options for creating dynamic text rather than hard coding the TMP component into the part.

Link to comment
Share on other sites

Played around with this a bit too and at least for english the "NotoSans-Regular SDF" stock game font works pretty fine.
But using e.g. chinese is much more tricky, because KSP seems to have separated the fonts for Korean, Chinese and Japanes into multiple fonts:

  • NotoSansCJK-J-Regular SDF
  • NotoSansCJK-Jext-Regular SDF
  • NotoSansCJK-K01-Regular SDF
  • NotoSansCJK-K02-Regular SDF
  • NotoSansCJK01-Regular SDF
  • NotoSansCJK02-Regular SDF
  • NotoSansCJK03-Regular SDF
  • NotoSansCJK04-Regular SDF
  • NotoSans-Regular SDF
  • NotoSansCJKjp-Regular SDF

Every font with "CKJ" in it is for "Chinese, Korean, Japanese"....From the naming alone it cannot be decided which font contains which glyph. And it may not be unlikely that a text contains glyphs from more than one font.
This might get quite difficult :/

Edited by Nils277
Link to comment
Share on other sites

Yay, found a way to use all fallback fonts for NotoSans

pU7T9HU.png

Code:

using System.Collections.Generic;
using TMPro;
using UnityEngine;

namespace KSPModLocalizer
{
    class ModuleTextLocalizer : PartModule
    {
        //--------------------Public fields---------------------

        /// <summary>
        /// The color of the text
        /// </summary>
        [KSPField]
        public Vector3 color = new Vector3(1, 1, 1);

        /// <summary>
        /// The transparency of the text
        /// </summary>
        [KSPField]
        public float alpha = 1f;

        /// <summary>
        /// Sets wheter the text should be bold
        /// </summary>
        [KSPField]
        public bool bold = false;

        /// <summary>
        /// Sets whether the text should be italic
        /// </summary>
        [KSPField]
        public bool italic = false;

        /// <summary>
        /// Sets wheter the text should be underlined
        /// </summary>
        [KSPField]
        public bool underlined = false;

        /// <summary>
        /// Sets wheter the text should be striked 
        /// </summary>
        [KSPField]
        public bool strikeThrough = false;

        /// <summary>
        /// The displayed text
        /// </summary>
        [KSPField]
        public string text = string.Empty;

        /// <summary>
        /// The name of the transform to display the text on
        /// </summary>
        [KSPField]
        public string textQuadName = string.Empty;

        /// <summary>
        /// The Size of the font. Font will be resized automatically when set to 0
        /// </summary>
        [KSPField]
        public float fontSize = 0.0f;

        //-----------------Internal fields-------------------

        //The textmesh to display the text on
        TextMeshPro TextMesh = null;

        //The used font
        TMP_FontAsset font;

        //----------------Life Cycle------------------

        //Delete the textmesh when the part is copied
        public override void OnCopy(PartModule fromModule)
        {
            base.OnCopy(fromModule);
            TextMeshPro thisMesh = GetComponentInChildren<TextMeshPro>();
            if (thisMesh != null)
            {
                thisMesh.gameObject.DestroyGameObject();
            }
        }

        //Load and set up the fonts when the module is awoken
        public override void OnAwake()
        {
            TMP_FontAsset[] loadedFonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
            List<TMP_FontAsset> fonts = new List<TMP_FontAsset>();

            //get the default font
            for (int i = 0; i < loadedFonts.Length; i++)
            {
                if (loadedFonts[i].name == "NotoSans-Regular SDF")
                {
                    font = Instantiate(loadedFonts[i]);
                }
            }

            //add the fallback fonts
            for (int i = 0; i < loadedFonts.Length; i++)
            {
                if (loadedFonts[i].name.StartsWith("NotoSans") && !(loadedFonts[i].name == "NotoSans-Regular SDF"))
                {
                    font.fallbackFontAssets.Add(loadedFonts[i]);
                }
            }
        }

        //Init and show texts
        public override void OnStart(StartState state)
        {
            //get 
            TMP_FontAsset[] loadedFonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();

            
            Transform screenTransform = part.FindModelTransform(textQuadName);

            GameObject LCDScreen = new GameObject();
            LCDScreen.transform.parent = screenTransform;
            LCDScreen.transform.localRotation = screenTransform.localRotation;
            LCDScreen.transform.localRotation = Quaternion.Euler(0, 180f, 0);

            //set the properties of the used mesh for the LCDScreen
            TextMesh = LCDScreen.AddComponent<TextMeshPro>();
            Mesh M = screenTransform.GetComponent<MeshFilter>().mesh;
            RectTransform T = TextMesh.gameObject.GetComponent<RectTransform>();
            T.sizeDelta = new Vector2(M.bounds.size.x, M.bounds.size.y);
            LCDScreen.transform.localPosition = new Vector3(0, 0, (M.bounds.size.z / 2) + 0.002f);

            //set the parameters of the textmesh
            TextMesh.enableAutoSizing = (fontSize == 0.0f);
            TextMesh.fontSizeMin = 0.1f;
            TextMesh.overflowMode = TextOverflowModes.Overflow;
            TextMesh.alignment = TextAlignmentOptions.Center;
            TextMesh.color = new Color(color.x, color.y, color.z, alpha);
            TextMesh.font = font;

            //"effects" of the displayed text
            string displayedText = text;
            displayedText = bold ? "<b>" + displayedText + "</b>" : displayedText;
            displayedText = italic? "<i>" + displayedText + "</i>": displayedText;
            displayedText = underlined ? "<u>" + displayedText + "</u>" : displayedText;
            displayedText = strikeThrough ? "<s>" + displayedText + "</s>" : displayedText;

            TextMesh.text = displayedText;
        }
    }
}

Config:

    MODULE
    {
        name = ModuleTextLocalizer
        textQuadName = Text
        text = #autoLOC_135145
    }   

 

Edited by Nils277
Link to comment
Share on other sites

@Nils277 Is the purpose of getting the fallback fonts so that KSP can display all of the symbols used in the current language? 

UISkinManager.TMPFont should provide you with the font KSP is currently using.

I'm not sure if it uses different fonts based on which language KSP is using, or if just has all of the fallback fonts assigned, it should be easy enough to check. And it might be simpler than running through all of the loaded fonts.

Link to comment
Share on other sites

 

@Nils277 Is the purpose of getting the fallback fonts so that KSP can display all of the symbols used in the current language? 

UISkinManager.TMPFont should provide you with the font KSP is currently using.

I'm not sure if it uses different fonts based on which language KSP is using, or if just has all of the fallback fonts assigned, it should be easy enough to check. And it might be simpler than running through all of the loaded fonts.

This works perfectly. Thanks!

 

Looks great!  That mean you're gonna write the plugin? :cool:

Hmm, thought about maybe adding it to:

But this is your decision. If you are okay with it, i would add two modules, one for external parts and one for  the IVAs to allow easier localization for modders.
Credits for this fine piece would of course go to you. I can also add you as collaborator in the github repository if you want.

(Maybe the mods name would have to be changed, as it would not be for static files only anymore.)

Edited by Nils277
Link to comment
Share on other sites

11 hours ago, Nils277 said:

This works perfectly. Thanks!

Hmm, thought about maybe adding it to:

But this is your decision. If you are okay with it, i would add two modules, one for external parts and one for  the IVAs to allow easier localization for modders.
Credits for this fine piece would of course go to you. I can also add you as collaborator in the github repository if you want.

(Maybe the mods name would have to be changed, as it would not be for static files only anymore.)

My good man, I was merely walking down the road and stubbed my toe on a rock.  When I started digging, this is what I unearthed.  If you would like this rock for a door stop, it is yours.  Thanks for the offer to be a collaborator but I can barely figure out how to upload my own source to Git.  A minor note saying I was the one who tripped over this will be most adequate.  That way, should someone look at your code and then mine, I won't be accused of being a clumsy oaf and a thief.

Also, @InfiniteDice, who doesn't do much modding any more, had a fair sized hand in helping me figure this out.  Noting that he helped me unearth this thing would be appreciated.

Edited by Fengist
Link to comment
Share on other sites

@Fengist i'm kinda stuck at the moment with adding this ability to the plugin.

The problem i have is, that the shader used for the texts is an unlit shader, which means that i will look pretty bad on parts when they are in the dark because their text will always be glowing. I tried applying a standard shader from KSP to it but this does not work because TextMeshPro needs some special shaders to work. And unfortunatele the only shaders for TMP that are available in KSP are the unlit ones.... :( 

Do you have achieved something while experimenting with the texts that allows the text to be shaded?

Link to comment
Share on other sites

13 hours ago, Nils277 said:

@Fengist i'm kinda stuck at the moment with adding this ability to the plugin.

The problem i have is, that the shader used for the texts is an unlit shader, which means that i will look pretty bad on parts when they are in the dark because their text will always be glowing. I tried applying a standard shader from KSP to it but this does not work because TextMeshPro needs some special shaders to work. And unfortunatele the only shaders for TMP that are available in KSP are the unlit ones.... :( 

Do you have achieved something while experimenting with the texts that allows the text to be shaded?

Sorry Nils, I didn't even mess with the shader.  It worked so I didn't even look to see what options were available.

Link to comment
Share on other sites

  • 2 years later...
This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...