NexiiText3

== Non-License ==
<lsl>
//===================================================//
// NexiiText3 //
// "26 September 2011", "16:26:00 GMT-0" //
// By Nexii Malthus //
// Public Domain //
//===================================================//
</lsl>
== Intro ==
Second generation of my prim text renderer. It's meant to be easy, compact, efficient and high performance.
This improved version over NexiiText2 that is meant to be slightly more bloated but with richer options, with Icons and Unit measurements. It is extended to move away from static reading display and towards a very rich interactive medium.
While you do not have the precise manual control of prim positioning that static text displays have, on the other hand you can very easily scale font size on an equation and create text boxes.
This library is excellent for use in the dynamic environment of HUDs. You can allocate a reserve of prims for use of the text library and use them up as you wish.
The renderer is efficient in prim usage as empty areas are completely void of prims. Prims are only used where text is visible.
The text rendering operations also support newlines (\n) and tabs (\t).
=== Note on Colour ===
It makes more sense for a rotation type to transport colours in a compact format (rotation RGBA), rather than multiple vars (vector RGB + float Alpha).
The compact format is <Red [0,1], Green [0,1], Blue [0,1], Alpha [0,1]>
== Text Prim ==
<lsl>
default {
 state_entry() {
 llSetPrimitiveParams([
 PRIM_TYPE,
 PRIM_TYPE_PRISM,
 PRIM_HOLE_SQUARE,
 <0.199, 0.8, 0>,
 0.306,
 <0, 0, 0>,
 <1, 1, 0>,
 <0, 0, 0>,
 PRIM_SIZE,
 <0.01, 0.58, 0.18>,
 PRIM_TEXTURE,
 ALL_SIDES,
 "180d23df-62b6-8608-0535-03413ddf3805",
 <1, 1, 0>,
 <0, 0, 0>,
 0.0
 ]);
 llSetObjectName("Text");
 llRemoveInventory(llGetScriptName());
 }
}
</lsl>
== Fonts ==
Some fonts I created.
<lsl>
key Normal = "0967db80-5132-d6dc-f37c-4f9cc1196983";// Normal Font
key Bold = "48de7a07-1641-3d26-2bca-9773a20d2adf";// Be Bold!
key Lined = "35399d2f-e35f-5100-179a-f3a119b1cdf7";// URLy Underline!
key Italic = "1bb8a1f9-87cb-4946-afc6-b1856bc9b752";// Iffy Italics!
key Stroked = "a2d5149e-d018-3ddd-202d-3432605c8084";// Edgy Edge!
key Shadowed = "347ff828-9cef-31e5-a792-1bfdd2a1cea0";// Silly Shadow!
</lsl>
And Icons.
<lsl>
key Icons = "65f49eb3-9532-0f3a-c1e6-d533c4cd5ae4";// Intense Icons!
</lsl>
== Colours ==
Here's an extra snippet for use alongside, instead of writing down the colours all the time. For quick reference and convenience.
<lsl>
rotation White = <.99,.99,.99, 1.0>;
rotation Black = <.00,.00,.00, 1.0>;
rotation Red = <.99,.00,.00, 1.0>;
rotation Green = <.00,.99,.50, 1.0>;
rotation Blue = <.00,.40,.90, 1.0>;
rotation Orange = <.99,.50,.00, 1.0>;
rotation LSL = <.00,.99,.50, 1.0>;
rotation URL = <.20,.40,.70, 1.0>;
</lsl>
== Script ==
<lsl>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// NexiiText3
list TextLinkset;
integer TextPrim;
integer TextPrims;
list TextRender;
integer TextRenders;
integer TextRenderBatches = 10;
integer TextLastCleared;
integer TextLength;
vector TextPos;
rotation TextRot;
float TextHeight;
integer TextLineX;
integer TextLineY;
float TextRatio = 3.2222;
float TextOffset = 0.558;
string TextChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
string TextChars0 = " !\"#$%&'()*+,-./";
string TextChars1 = "0123456789";
string TextChars2 = ":;<=>?@";
string TextChars3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string TextChars4 = "[\\]^_`";
string TextChars5 = "abcdefghijklmnopqrstuvwxyz";
string TextChars6 = "{|}~";
float TextGlyphW = 0.60;
float TextGlyphH = 0.98;
float TextGlyphX = 0.00;
vector TextFace0R;
float TextFace0O;
vector TextFace1R;
vector TextFace2R;
float TextFace2O;
TextInit() {
 integer Prims = llGetNumberOfPrims()+1; TextLinkset = [];
 while(--Prims) if(llGetLinkName(Prims) == "Text") TextLinkset += Prims;
 TextPrims = llGetListLength(TextLinkset);
 TextLastCleared = 4096;
 TextFace0R = <.25*TextGlyphW, .1*TextGlyphH, 0>;
 TextFace0O = .0745*TextGlyphW;
 TextFace1R = <.1*TextGlyphW,.1*TextGlyphH,0>;
 TextFace2R = <-1.635*TextGlyphW,.1*TextGlyphH,0>;
 TextFace2O = .69*TextGlyphW;
 TextGlyphX *= TextGlyphW;
}
TextCursor(vector Position, vector Direction, float FontHeight, integer MaxCharsPerLine) {
 TextPos = Position;
 TextRot = llRotBetween(<1,0,0>, Direction);
 TextLength = MaxCharsPerLine;
 TextHeight = FontHeight;
 TextLineX = TextLineY = 0;
}
vector TextCursorPos(float x, float y) {
 return TextPos + <0.025,
 (TextHeight * TextOffset * TextLineX) + (TextHeight * TextOffset * (x+.5)),
 TextHeight*-.5 - (TextHeight * TextLineY) - (TextHeight * -y)> * TextRot;
}
Text(string Text, rotation Colour, key Font) {
 integer x; integer y = llStringLength(Text);
 for(; x < y && TextPrim < TextPrims; ++TextPrim) {
 @whitespace;
 string Char = llGetSubString(Text,x,x);
 if(Char == "\n") { ++x; ++TextLineY; TextLineX = 0; jump whitespace; }
 if(Char == " ") { ++x; ++TextLineX; jump whitespace; }
 if(TextLineX >= TextLength) { TextLineX = 0; ++TextLineY; } // Line Wrap
 
 integer Link = llList2Integer(TextLinkset,TextPrim);
 
 list Render = [
 PRIM_LINK_TARGET, Link,
 PRIM_SIZE, <0.01, TextHeight*TextRatio, TextHeight>,
 PRIM_ROT_LOCAL, TextRot,
 PRIM_POS_LOCAL, TextCursorPos(2.0, 0.0),
 PRIM_COLOR, ALL_SIDES, <Colour.x, Colour.y, Colour.z>, Colour.s,
 PRIM_TEXTURE, ALL_SIDES, "180d23df-62b6-8608-0535-03413ddf3805", <1,1,0>, <0,0,0>, 0.0 // Invis
 ];
 
 integer Face;
 while(Char != "\n" && x < y && Face < 5 && TextLineX < TextLength) {
 integer i;
 if(~(i=llSubStringIndex(TextChars5, Char))) i += 65; else
 if(~(i=llSubStringIndex(TextChars3, Char))) i += 33; else
 if(~(i=llSubStringIndex(TextChars1, Char))) i += 16; else
 if(~(i=llSubStringIndex(TextChars0, Char))) ; else
 if(~(i=llSubStringIndex(TextChars2, Char))) i += 26; else
 if(~(i=llSubStringIndex(TextChars4, Char))) i += 59; else
 if(~(i=llSubStringIndex(TextChars6, Char))) i += 91;
 
 integer Side;
 vector Repeat;
 vector Offset = <(-.45+.1*(i%10)) + TextGlyphX, .45-(.1*(i/10)), 0>;
 
 if(Face == 0) {
 Side = 3;
 Repeat = TextFace0R;
 Offset.x += TextFace0O;
 } else if(Face == 1) {
 Side = 7;
 Repeat = TextFace1R;
 } else if(Face == 2) {
 Side = 4;
 Repeat = TextFace2R;
 Offset.x -= TextFace2O;
 if(Offset.x < -1) Offset.x = 2 + Offset.x;
 } else if(Face == 3) {
 Side = 6;
 Repeat = TextFace1R;
 } else {//Face == 4
 Side = 1;
 Repeat = TextFace0R;
 Offset.x -= TextFace0O;
 }
 
 Render += [PRIM_TEXTURE, Side, Font, Repeat, Offset, 0.0];
 
 ++x; ++Face; ++TextLineX;
 Char = llGetSubString(Text,x,x);
 }
 TextRender += Render;
 
 if(TextLineX >= TextLength) { TextLineX = 0; ++TextLineY; }
 if(++TextRenders >= TextRenderBatches) {
 llSetLinkPrimitiveParamsFast(0, TextRender);
 TextRender = []; TextRenders = 0;
 }
 }
}
Icon(integer Index, rotation Colour) {
 if(TextPrim >= TextPrims) return;
 if(TextLineX >= TextLength-1) { TextLineX = 0; ++TextLineY; } // Line Wrap
 
 TextRender += [
 PRIM_LINK_TARGET, llList2Integer(TextLinkset,TextPrim++),
 PRIM_SIZE, <0.01, TextHeight*TextRatio, TextHeight>,
 PRIM_ROT_LOCAL, TextRot,
 PRIM_POS_LOCAL, TextCursorPos(2.0, 0.0),
 PRIM_COLOR, ALL_SIDES, <Colour.x, Colour.y, Colour.z>, Colour.s,
 PRIM_TEXTURE, ALL_SIDES, "180d23df-62b6-8608-0535-03413ddf3805", <1,1,0>, <0,0,0>, 0.0, // Invis
 PRIM_TEXTURE, 3, Icons, <.25*.475,.1*.98,0>,
 <((-.45+.1*(Index%10)) + (.0745*.475)) + (-.05*.475), .45-(.1*(Index/10)),0>, 0,
 PRIM_TEXTURE, 7, Icons, <.1*.475,.1*.98,0>,
 <((-.45+.1*(Index%10))) + (.05*.475), .45-(.1*(Index/10)),0>, 0
 ];
 TextLineX += 2;
 if(TextLineX >= TextLength) { TextLineX = 0; ++TextLineY; }
 if(++TextRenders >= TextRenderBatches) {
 llSetLinkPrimitiveParamsFast(0, TextRender);
 TextRender = []; TextRenders = 0;
 }
}
Units(list Value, string Unit, rotation Colour, key Font) {
 string Stringified;
 if(llGetListEntryType(Value,0) == TYPE_FLOAT)
 Stringified = llGetSubString((string)llList2Float(Value,0), 0,-6);
 else Stringified = (string)Value;
 
 integer Index;
 Unit = llToLower(Unit);
 llOwnerSay(Unit);
 Unit = llDumpList2String(llParseStringKeepNulls(Unit, ["^2"], []), "²");
 Unit = llDumpList2String(llParseStringKeepNulls(Unit, ["sec"], []), "s");
 if(Unit == "points") Index = 18; else
 if(Unit == "experience") Index = 19; else
 if(Unit == "count") Index == 20; else
 if(Unit == "percent") Index = 21; else
 if(Unit == "mm") Index = 22; else
 if(Unit == "cm") Index = 23; else
 if(Unit == "m") Index = 24; else
 if(Unit == "km") Index = 25; else
 if(Unit == "mm/s") Index = 26; else
 if(Unit == "cm/s") Index = 27; else
 if(Unit == "m/s") Index = 28; else
 if(Unit == "km/s") Index = 29; else
 if(Unit == "deg/s") Index = 30; else
 if(Unit == "rad/s") Index = 31; else
 if(Unit == "mm/s²") Index = 32; else
 if(Unit == "cm/s²") Index = 33; else
 if(Unit == "m/s²") Index = 34; else
 if(Unit == "km/s²") Index = 35; else
 if(Unit == "deg/s²") Index = 36; else
 if(Unit == "rad/s²") Index = 37; else
 if(Unit == "mg") Index = 38; else
 if(Unit == "g") Index = 39; else
 if(Unit == "kg") Index = 40; else
 if(Unit == "t") Index = 41; else
 if(Unit == "b") Index = 42; else
 if(Unit == "kb") Index = 43; else
 if(Unit == "mb") Index = 44; else
 if(Unit == "gb") Index = 45; else
 if(Unit == "tb") Index = 46;
 
 Text(Stringified, Colour, Font);
 Icon(Index, Colour);
}
TextFrame() {
 if(TextRender) {
 llSetLinkPrimitiveParamsFast(0, TextRender);
 TextRender = [];
 }
}
TextFrameEnd() {
 integer TextStartClearing = TextPrim;
 for(; TextPrim < TextPrims && TextPrim < TextLastCleared; ++TextPrim) {
 TextRender += [
 PRIM_LINK_TARGET, llList2Integer(TextLinkset, TextPrim),
 PRIM_POS_LOCAL, ZERO_VECTOR,
 PRIM_SIZE, <0.01, 0.01, 0.01>,
 PRIM_COLOR, ALL_SIDES, ZERO_VECTOR, 0.0,
 PRIM_TEXTURE, ALL_SIDES, "180d23df-62b6-8608-0535-03413ddf3805", <1,1,0>, <0,0,0>, 0.0 // Invis
 ];
 if(++TextRenders >= TextRenderBatches) {
 llSetLinkPrimitiveParamsFast(0, TextRender);
 TextRender = []; TextRenders = 0;
 }
 }
 if(TextRender) {
 llSetLinkPrimitiveParamsFast(0, TextRender);
 TextRender = [];
 }
 TextLastCleared = TextStartClearing;
 TextRenders = TextPrim = 0;
}
// NexiiText3
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Styles
key Normal = "0967db80-5132-d6dc-f37c-4f9cc1196983";// Normal Font
key Bold = "48de7a07-1641-3d26-2bca-9773a20d2adf";// Be Bold!
key Lined = "35399d2f-e35f-5100-179a-f3a119b1cdf7";// URLy Underline!
key Italic = "1bb8a1f9-87cb-4946-afc6-b1856bc9b752";// Iffy Italics!
key Stroked = "a2d5149e-d018-3ddd-202d-3432605c8084";// Edgy Edge!
key Shadowed = "347ff828-9cef-31e5-a792-1bfdd2a1cea0";// Silly Shadow!
key Icons = "65f49eb3-9532-0f3a-c1e6-d533c4cd5ae4";// Intense Icons!
integer ICON_RIGHT_ARROW = 1;
integer ICON_RIGHT_SHIFT = 2;
integer ICON_HOME = 3;
integer ICON_INFO = 4;
integer ICON_QUESTION = 5;
integer ICON_L$ = 6;
integer ICON_RSS = 7;
integer ICON_EXTERNAL_LINK = 8;
integer ICON_AVATAR = 9;
integer RATING_GENERAL = 10;
integer RATING_MATURE = 11;
integer RATING_ADULT = 12;
integer GAME_HEADSHOT = 13;
integer GAME_WEAPON_C7 = 14;
integer GAME_WEAPON_FORBODA = 15;
integer GAME_EXPLOSION = 16;
integer GAME_WEAPON_KNIFE = 17;
rotation White = <.99,.99,.99, 1.0>;
rotation Black = <.00,.00,.00, 1.0>;
rotation Red = <.99,.00,.00, 1.0>;
rotation Green = <.00,.99,.50, 1.0>;
rotation Blue = <.00,.40,.90, 1.0>;
rotation Orange = <.99,.50,.00, 1.0>;
rotation LSL = <.00,.99,.50, 1.0>;
rotation URL = <.20,.40,.70, 1.0>;
// Styles
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Linkset Indexing
integer Linked(string Needle) {
 integer Prims = llGetNumberOfPrims()+1;
 while(--Prims) if(llGetLinkName(Prims) == Needle) return Prims;
 return 0;
}
// Linkset Indexing
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utility function, for code racing or timing
integer Millisec(string Stamp) {
 return (integer)llGetSubString(Stamp, 8, 9) * 86400000 + // Days
 (integer)llGetSubString(Stamp, 11, 12) * 3600000 + // Hours
 (integer)llGetSubString(Stamp, 14, 15) * 60000 + // Minutes
 llRound(((float)llGetSubString(Stamp, 17, -2) * 1000.0)) // Seconds.Milliseconds
 - 617316353; // Offset to fit between [-617316353,2147483547]
}
string FancyFloat(float F, integer Precision) { return llGetSubString((string)F, 0, -7+Precision); }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Application
vector vURLmin;
vector vURLmax;
default {
 state_entry() {
 integer Bytecode = 65536-llGetFreeMemory();// Semi-accurate if left at very top of default state_entry
 string Debuggage = "Debug Profiling";
 Debuggage += "\n~- [Bytecode] "+FancyFloat(Bytecode/1024., 1)+"kb";
 
 integer primA = Linked("A");
 vector vA = llList2Vector(llGetLinkPrimitiveParams(primA, [PRIM_POS_LOCAL]),0);
 integer primB = Linked("B");
 llSetLinkPrimitiveParamsFast(primB, [PRIM_POS_LOCAL, ZERO_VECTOR]);
 integer primC = Linked("C");
 llSetLinkPrimitiveParamsFast(primC, [PRIM_POS_LOCAL, ZERO_VECTOR]);
 
 
 
 
 
 
 
 // We have to initialise the script engine once, so that text prims are identified.
 TextInit();
 
 // We can optionally clear all text prims by drawing an empty frame at the start.
 // This isn't hardcoded into TextInit, because it may introduce unwanted lag
 TextFrameEnd();
 
 
 
 
 
 
 // This is how we draw text.
 
 // We set a cursor position, a bit like the classic pipe
 // character that guides our text entry in this script editor
 TextCursor(vA, <1,0,0>, 0.1, 25);
 
 // The Text() function accepts three arguments
 // A string of arbritary size
 // An rot input that describes colour format
 // <Red, Green, Blue, Alpha>, Values are 0.0 to 1.0
 // Lastly, we give a desired font to use, which is a 10x10 texture grid.
 Text("Awesome!", Red, Bold);
 
 // The Text() also correctly consumes \n as a newline
 Text("\n\nNexiiText3", White, Lined);
 
 // \t are tabs, which are 4 space chars, and whitespace is optimised out
 // by moving text prims dynamically around, where possible.
 Text("\n\tA ", White, Normal);
 
 Text("Prim ", Orange, Normal);
 
 Text("Text Renderer", White, Normal);
 
 Text("\n\n...with", White, Normal);
 
 Text("\n\tdynamic prim\n\t\tallocation", Green, Italic);
 
 // After we have drawn everything, we have to end the frame.
 // This cleans up leftovers from a previous drawing operation.
 TextFrameEnd();
 
 
 
 
 
 
 
 
 
 
 
 // Below is the same set of calls but I have thrown in some profiling, for working on optimizing.
 
 llSleep(2.5);
 TextFrameEnd();
 
 string FrameStart = llGetTimestamp();
 
 TextCursor(vA, <1,0,0>, 0.1, 25); string t1 = llGetTimestamp();
 Units([24.4],"deg/s^2", Red, Normal);
 Text(" of Awesome!\n\n", Red, Normal); string t2 = llGetTimestamp();
 
 vURLmin = TextCursorPos(-0.5,-0.5);
 Icon(ICON_EXTERNAL_LINK, URL);
 Text("NexiiText3", URL, Lined); string t3 = llGetTimestamp();
 vURLmax = TextCursorPos(-0.5, 0.5);
 
 Text("\n\tA ", White, Normal); string t4 = llGetTimestamp();
 Text("Prim ", Orange, Normal); string t5 = llGetTimestamp();
 Text("Text Renderer", White, Normal); string t6 = llGetTimestamp();
 Text("\n\n...with", White, Normal); string t7 = llGetTimestamp();
 Text("\n\tdynamic prim\n\t\tallocation", Green, Italic); string t8 = llGetTimestamp();
 TextFrameEnd();
 
 string FrameEnd = llGetTimestamp();
 
 
 
 
 
 Debuggage += "\n~- [Timing] Frame "+(string)(Millisec(FrameEnd)-Millisec(FrameStart))+"ms"
 +"\n\t\t\t- TextCursor() "+(string)(Millisec(t1)-Millisec(FrameStart))+"ms"
 +"\n\t\t\t- Text() "+(string)(Millisec(t2)-Millisec(t1))+"ms"
 +"\n\t\t\t- Text() "+(string)(Millisec(t3)-Millisec(t2))+"ms"
 +"\n\t\t\t- Text() "+(string)(Millisec(t4)-Millisec(t3))+"ms"
 +"\n\t\t\t- Text() "+(string)(Millisec(t5)-Millisec(t4))+"ms"
 +"\n\t\t\t- Text() "+(string)(Millisec(t6)-Millisec(t5))+"ms"
 +"\n\t\t\t- Text() "+(string)(Millisec(t7)-Millisec(t6))+"ms"
 +"\n\t\t\t- Text() "+(string)(Millisec(t8)-Millisec(t7))+"ms"
 +"\n\t\t\t- TextFrameEnd() "+(string)(Millisec(FrameEnd)-Millisec(t6))+"ms";
 
 /*
 Times reported are rough estimators,
 as they may be re-aligned with simulator frames,
 but aren't bad for testing optimisation efforts.
 
 A script that was forcefully split into several frames
 by a sim along code chunks can still be used as a verifiable metric.
 */
 
 
 integer FreeMem = llGetFreeMemory();
 Debuggage += "\n~- [Memory] Peak Use " + FancyFloat((65536-FreeMem-Bytecode)/1024., 1) + "kb";
 Debuggage += "\n~- [Memory] Available " + FancyFloat(FreeMem/1024., 1) + "kb";
 
 llOwnerSay(Debuggage);
 
 
 
 
 
 
 /*
 vURL is used to create a clickable URL in the below code.
 If you look above, you'll see how vURLmin and vURLmax are created.
 (The text is also styled with a HTML-like underline and blue colour.)
 
 By getting the bounding box corners of a piece of text,
 we can do useful and very cool things with it.
 Such as:
 Drawing a background
 Highlighting text
 Interactive text
 
 In this example of touch_end and vURL, we have created a clickable URL that is loaded.
 But we could do other fun things, like menu navigation, using wiki-like markup and parsing.
 */
 
 
 // Uncomment below to see the edges of the bounding box
 //llSetLinkPrimitiveParamsFast(primB, [PRIM_POS_LOCAL, vURLmin]);
 //llSetLinkPrimitiveParamsFast(primC, [PRIM_POS_LOCAL, vURLmax]);
 }
 
 touch_end(integer d) {
 vector Pos = (llDetectedTouchPos(0)-llGetPos()) / llGetRot();
 key Av = llDetectedKey(0);
 
 if(Pos.y > vURLmin.y && Pos.z > vURLmin.z && Pos.y < vURLmax.y && Pos.z < vURLmax.z) {
 llLoadURL(Av, "NexiiText3 on the wiki", "https://wiki.secondlife.com/wiki/NexiiText3");
 }
 }
}
</lsl>