Remember a while back, when I was processing a list of positions and normals to create a list of positions and angles, for a roller coaster. Well, we’re back. Last time, in Y-Face-Normal Rotation, I talked about how I worked out the angles I needed. A script I’ve not written about, took Dizzi’s input data and produced a web page from which we can cut and paste the data that goes into our roller coaster scripts. Now, because reasons, we need to do it a different way.
We want to process very long routes and store them in notecards. The current script is limited in how many points it can handle, and we need lots more. So the new plan is to store the input data in two notecards, since it comes from Blender broken out into the list of points and the list of normals, and to read those cards and produce the desired output format as a web page that we can cut and paste back to notecards. (If LL would let us create notecards directly, we’d do that, of course. But they don’t.
Now, it turns out that to produce one line of output, we need to look at three consecutive pairs of data (position, normal). To produce the angle of rotation, we have to factor out the flat rotation of the roller coaster car. To get that rotation, we need to consider the current point, the preceding point, and the next point. That’s covered in the preceding referenced article.
In this article, we’ll talk about how to process our two notecards, one “normals” and one “offsets”. In another article, I’ll probably talk about the web interface part of this. Now the code I have right now has all the data in two lists, one for offsets and one for normals, and it just loops from beginning to end, like this:
convert_items() { Result = "string TData =\n"; integer N = llGetListLength(offsets); integer i; for (i = 0; i < N; i++) { add_item(convert_item(i)); } if ( llStringLength(Buffer) > 0 ) { Result += PREFIX + Buffer + SUFFIX; } Result += "\"\";"; } string convert_item(integer index) { vector from = get_offset(index-1); vector to = get_offset(index); vector norm = get_normal(index); rotation flat_rot = toLookSoberlyAt(to, from); rotation roll = llRotBetween(<0,1,0>, norm/flat_rot); rotation final_rot = roll*flat_rot; vector axis = llRot2Axis(roll); integer sign = 1; if ( axis.x > 0 ) sign = -1; float angle = sign*llRot2Angle(roll)*RAD_TO_DEG; return pack_float(to.x,5)+pack_float(to.y,5)+pack_float(to.z,6) + pack_integer((integer) llRound(angle), 3); // return <to.x, to.y, to.z, (integer) llRound(angle)>; } vector get_normal(integer i) { integer length = llGetListLength(normals); if ( i < 0 ) i += length; i = i%length; return llList2Vector(normals, i); } vector get_offset(integer i) { integer length = llGetListLength(offsets); if ( i < 0 ) i += length; i = i%length; return llList2Vector(offsets, i); }
We don’t need to go through this in detail. The main point is that convert_items
loops over all the items, and convert_item
looks at three of them, the previous, current, and next. We need to replicate this behavior in our new script, but the data has to come from notecards. And did I mention that there is so much data that we can’t read it all into lists at once?
So we’ve got two issues at least: we need to have at least three each of normals and offsets … and we can’t have them all. So my cunning plan is to read three of them, process those three, throw one away, read the next, process those three, and so on. As you probably know, you can’t just read a line from a notecard in LSL. No, you have to request a read, then in a data server
event, process the read. Since we have two cards, it’ll get even more weird.
Now this morning, I wrote a script to process two notecards three pairs at a time. It looks like this:
// read two cards parallel // JR 2018-09-27 key NormalKey; key OffsetKey; key NormalData; key OffsetData; integer NormalSize; integer OffsetSize; integer NormalsLine; integer OffsetsLine; list Normals; list Offsets; check_sizes(key queryid, string data) { if (queryid == NormalKey ) { NormalSize = (integer) data; } else if ( queryid == OffsetKey ) { OffsetSize = (integer) data; } if ( NormalSize >= 0 && OffsetSize >= 0 ) { string m = llDumpList2String(["Normals:", NormalSize, "Offsets", OffsetSize], " "); llOwnerSay(m); NormalsLine = 0; OffsetsLine = 0; NormalData = llGetNotecardLine("normals", NormalsLine); OffsetData = llGetNotecardLine("offsets", OffsetsLine); } } process_card(key queryid, string data) { if ( queryid == NormalData ) { Normals += (vector) data; if ( llGetListLength(Normals) < 3 ) NormalData = llGetNotecardLine("normals", ++NormalsLine); } else if ( queryid == OffsetData ) { Offsets += (vector) data; if ( llGetListLength(Offsets) < 3 ) OffsetData = llGetNotecardLine("offsets", ++OffsetsLine); } if ( llGetListLength(Normals) == 3 && llGetListLength(Offsets) == 3 ) { process_three_cards(); } } process_three_cards() { string n = llDumpList2String(Offsets, ", "); string o = llDumpList2String(Normals, ", "); llOwnerSay((string) NormalsLine + " " + n + "-" + o); Normals = llDeleteSubList(Normals, 0, 0); Offsets = llDeleteSubList(Offsets, 0, 0); NormalData = llGetNotecardLine("normals", ++NormalsLine); OffsetData = llGetNotecardLine("offsets", ++OffsetsLine); } default { state_entry() { NormalKey = llGetNumberOfNotecardLines("normals"); OffsetKey = llGetNumberOfNotecardLines("offsets"); NormalSize = -1; OffsetSize = -1; } dataserver(key queryid, string data) { if ( data == EOF ) { llOwnerSay("EOF"); return; } if ( queryid == NormalKey || queryid == OffsetKey) check_sizes(queryid, data); else process_card(queryid, data); } }
I often do this when faced with a new problem. I write a free-standing separate script that works some part of the problem all by itself, so that I learn a bit, or refresh my memory and ideas a bit, about how I might solve the problem. The script above checks to be sure the cards have equal size, and then reads until it has three elements in each list. Then it processes those three. After processing, it removes the first elements, and reads two more, which puts us back where we get two card lines, discover that our lists have three items, and process again. This runs until the cards run out.
Now the basic scheme there is to initiate two reads, then whenever a read finishes, process its data and if the corresponding list doesn’t have three elements yet, do another read. When both of them get to three, we process them, and the process function deletes the obsolete one and issues two more reads. And the process repeats.
Now my current plan is to follow the style of the above script, processing each three until done. We’ll have to do something about wrap-around, since we need the -1st and the N+1st items, which wrap around to the last and the first respectively. No problem, that’s modular arithmetic or something.
Now there is another possibility for an approach which might be different enough to consider. Suppose we think of ourselves as reading “randomly” … calling something like get_me_the_three_for_doing_number(n)
and so on. Then we’d read those ones and when they’re all in, carry on. This might make for a different shape to the program, and it might be “better”. How do we know and what do we do? Sometimes, when it’s important, I’d advise doing it both ways and assessing which is best. In this case, whichever way we pick, when it works, we’ll be done and if the other is a little bit better, it probably won’t matter enough to make it worth our while to try the other way.
Thinking about it, it seems to me that what I have is pretty close, and doing it the other way would be similar. One difference would be that we’d be tempted to issue all six reads at once, and while I assume we can count on them coming back in order, that’s not defined as far as I know, so we could get them out of order. If we did, that would be bad. So we might have to use six read keys N0, N1, N2, O0, O1, O2, to be sure they’re sorted. With the scheme shown above, we know we have the right order. Plus if we did the three at a time thing, we’d want to “optimize it” so it didn’t keep reading each record three times. And we’d probably wind up with something much like the existing approach.
Netting it all out, I think I’ll go with the existing approach. I do think I’ll go to a common index for both tables where right now I have a separate line number for each.
Why are you telling me this?
Well, because I want to. It’s my blog and no one reads it anyway. But if you are reading it, I’m telling you this because it’s the kind of thinking one does when deciding how to do things. Break down the problem, maybe solve little pieces to learn how to do it, then based on that learning, and maybe on the actual solutions, build up a real solution to the real problem. So while you may never work on this particular kind of thing, you may find the ideas useful. If you’re not going to find them useful, I hope you stopped reading a long time back.
Sketching the solution
Based on what we know now, I’m envisioning something like this: Processing will be driven, as in my sample, by the data server
event deciding that it has three items. After those three are done, it’ll do three more. We’ll have some special code to kick it off, much like the code we have now. And there will be a little bit of messing about to deal with the wrap-around.
I think I’ll try just to edit my current program to do the job, by essentially extending the process_three_cards
function. I’ll start by letting it process items 0 1 2 and item 1, and deal with getting -1 as a second step. Here goes …
My First Cut
Here’s the first set of changes I made. The bite was a bit too large but not bad:
// read two cards parallel // JR 2018-09-27 key NormalKey; key OffsetKey; key NormalData; key OffsetData; integer NormalSize; integer OffsetSize; integer ProcessLine; list Normals; list Offsets; check_sizes(key queryid, string data) { if (queryid == NormalKey ) { NormalSize = (integer) data; } else if ( queryid == OffsetKey ) { OffsetSize = (integer) data; } if ( NormalSize >= 0 && OffsetSize >= 0 ) { string m = llDumpList2String(["Normals:", NormalSize, "Offsets", OffsetSize], " "); llOwnerSay(m); ProcessLine = 1; // should be zero NormalData = llGetNotecardLine("normals", ProcessLine-1); OffsetData = llGetNotecardLine("offsets", ProcessLine-1); } } process_card(key queryid, string data) { integer n; if ( queryid == NormalData ) { Normals += (vector) data; n = llGetListLength(Normals); if ( n < 3 ) NormalData = llGetNotecardLine("normals", ProcessLine + n - 1); } else if ( queryid == OffsetData ) { Offsets += (vector) data; n = llGetListLength(Offsets); if ( n < 3 ) OffsetData = llGetNotecardLine("offsets", ProcessLine + n - 1); } if ( llGetListLength(Normals) == 3 && llGetListLength(Offsets) == 3 ) { process_three_cards(); } } process_three_cards() { string n = llDumpList2String(Offsets, ", "); string o = llDumpList2String(Normals, ", "); llOwnerSay((string) ProcessLine + " " + n + "-" + o); Normals = llDeleteSubList(Normals, 0, 0); Offsets = llDeleteSubList(Offsets, 0, 0); ProcessLine++; NormalData = llGetNotecardLine("normals", ProcessLine + 1); OffsetData = llGetNotecardLine("offsets", ProcessLine + 1); } default { state_entry() { llOwnerSay("ready"); } touch_start(integer num_detected) { NormalKey = llGetNumberOfNotecardLines("normals"); OffsetKey = llGetNumberOfNotecardLines("offsets"); NormalSize = -1; OffsetSize = -1; } dataserver(key queryid, string data) { if ( data == EOF ) { llOwnerSay("EOF"); return; } if ( queryid == NormalKey || queryid == OffsetKey) check_sizes(queryid, data); else process_card(queryid, data); } }
I changed the code to be driven by ProcessLine
rather than the two line numbers that were always in sync anyway. And I created two sample notecards with just six vectors in them, the offsets going (0,0,), (1,0,0), (2,0,0) and so on, and the normals going (0,0,0), (0,1,0), (0,2,0) and so on. My little print in process_three_cards
tells me I’m processing items 1, 2, 3, and 4 correctly. I don’t ask for number zero, since I started with #1, and I don’t process #5 because I hit an EOF reading card #6 and stop processing. Now I just need to fix it so that it’ll handle reads of less than zero and more than the number of cards I have. Then it should process all six sets of three pairs.
That should be “simple”. Wherever I do a read, instead of passing in the index directly, I’ll adjust it to deal with being off the ends:
integer adjust_index(integer card_number) { if ( card_number < 0 ) return card_number + NormalSize; if ( card_number > NormalSize ) return card_number - NormalSize;
And of course I call that function in all the llGetNoteCardLine calls. I even remembered to start with zero:
check_sizes(key queryid, string data) { if (queryid == NormalKey ) { NormalSize = (integer) data; } else if ( queryid == OffsetKey ) { OffsetSize = (integer) data; } if ( NormalSize >= 0 && OffsetSize >= 0 ) { string m = llDumpList2String(["Normals:", NormalSize, "Offsets", OffsetSize], " "); llOwnerSay(m); ProcessLine = 0; NormalData = llGetNotecardLine("normals", adjust_index(ProcessLine-1)); OffsetData = llGetNotecardLine("offsets", adjust_index(ProcessLine-1)); } }
Full disclosure: I originally tested index
in adjust_index, despite that the parameter was named card_number. Can you see why? Right! It’s because I changed my ideas on the name in the middle. I should rename the function to adjust_cardnumber, or the parameter to index. I’ll do the former, because I think it’s a better name. We’ll see that when I next print everything.
By the way, a renaming like that would be a pain if you use the internal editor. I use an external editor, Sublime Text. In that one, I just clicked into adjust_index
, typed command-D until I had them all selected, then command-right arrow, backspace over index, type card number, and changed them all at once. Nearly easy. A rename refactoring would be easier still but I don’t have that. Anyway now to test …
Ha! This is why we test frequently. We do correctly get the zero item, but we don’t get #5. Do you see why not? Look again at adjust_cardnumber:
integer adjust_cardnumber(integer card_number) { if ( card_number < 0 ) return card_number + NormalSize; if ( card_number > NormalSize ) return card_number - NormalSize; return card_number; }
Right. It says > NormalSize and should say >= NormalSize, because we’re zero-based and so do not process number NormalSize
. Try again with that fixed … and Ha again!
It doesn’t stop until finally it gets an EOF again. Why not? Well, our only stopping point is to hit an EOF. Fortunately, our adjust_cardnumber
will let you run off the end, because it doesn’t use the modulus function. I meant to mention that and forgot. I’ll get to it in a minute. Anyway, since now this thing can process off the end of the list we need to stop before we try to process the NormalSize
item.
process_three_cards() { string n = llDumpList2String(Offsets, ", "); string o = llDumpList2String(Normals, ", "); llOwnerSay((string) ProcessLine + " " + n + "-" + o); Normals = llDeleteSubList(Normals, 0, 0); Offsets = llDeleteSubList(Offsets, 0, 0); ProcessLine++; if ( ProcessLine < NormalSize ) { NormalData = llGetNotecardLine("normals", adjust_cardnumber(ProcessLine + 1)); OffsetData = llGetNotecardLine("offsets", adjust_cardnumber(ProcessLine + 1)); } else { llOwnerSay("done"); } }
OK, now we should stop as soon as we get to NormalSize
. (I had to stop and ask myself if that should say <= but no, we don't want to process #6 in our current list. We should be good now.)
Yes! Here, cleaned up a little bit, is our output:
ready Normals: 6 Offsets 6 0 <5, 0, 0>, <0, 0, 0>, <1, 0, 0> - <0, 5, 0>, <0, 0, 0>, <0, 1, 0> 1 <0, 0, 0>, <1, 0, 0>, <2, 0, 0> - <0, 0, 0>, <0, 1, 0>, <0, 2, 0> 2 <1, 0, 0>, <2, 0, 0>, <3, 0, 0> - <0, 1, 0>, <0, 2, 0>, <0, 3, 0> 3 <2, 0, 0>, <3, 0, 0>, <4, 0, 0> - <0, 2, 0>, <0, 3, 0>, <0, 4, 0> 4 <3, 0, 0>, <4, 0, 0>, <5, 0, 0> - <0, 3, 0>, <0, 4, 0>, <0, 5, 0> 5 <4, 0, 0>, <5, 0, 0>, <0, 0, 0> - <0, 4, 0>, <0, 5, 0>, <0, 0, 0> done
You can see that each line processes the item before the one of interest, the one of interest, and the one after, and that it wraps around before item zero and after its five. We're good.
I'm going to call this a wrap for now, finish the article, and continue later with the processing and web writing. Why? Because I'm tired, I'm at a good commit point, and rather than work tired, I like to work when I have a chance of being smart. But first, let's sum up what has happened.
Summing Up
First of all, as I usually do, I'm telling you my thinking, including any mistakes, and showing you what really happens, including any mistakes. And there are always mistakes. I've been programming for quite a few years now, and I still make little mistakes every day. This could be because I'm not very good at programming, but if you think so, come look at what's happening in Lexicolo and you'll conclude that maybe I'm pretty good. Mistakes are part of programming.
What I do that is different from some programmers, especially less experienced ones, is that I test my code very frequently. Often I'll write actual automated tests: there are a few articles here that do that. But even if I don't automate tests, I run my script frequently to see what it's doing. So I notice really soon that something is wrong. Usually, there's just one thing wrong (so far), so if I fix that, it's back to doing what I want. Then I push forward, maybe making another mistake, but testing frequently so that I find and fix it.
To make that code/test cycle go smoothly, I start from simple examples and build up. Here, I started with an idea for reading three lines from each of two cards, extended it, tweaked how it indexed, adjusted for wrap around, and probably three other things, doing each one one at a time. I try never to add two capabilities at once. I add one, test, then add the other. This means that I see the errors in the first bit sooner (if there are any) and I less often have to debug two things at once.
It may seem to you that things would go slower with all these little steps and so much testing, but the fact is they go faster because all my debugging is like what we say here "oh yeah it should be less than or equal".
So my advice is, try these ideas, if you feel like it, and see how they work for you. And if you enjoy what I'm doing here, please tell your friends to check out the site and subscribe to it.
Thanks, and check back soon!
Leave a Reply