1167 lines
32 KiB
Plaintext
1167 lines
32 KiB
Plaintext
/*
|
||
HS_DollyCam - TourEngine (offloaded)
|
||
- Receives CE_CMD_TOUR from Controller
|
||
- Requests current camera state from Core via CE_CMD_GET_STATE
|
||
- Plays tour via timer and sends CE_INT_SET_CAM to Core
|
||
*/
|
||
|
||
integer CE_CMD_INIT = 1000;
|
||
integer CE_CMD_TOUR = 1011;
|
||
integer CE_CMD_STOP = 1012;
|
||
integer CE_CMD_MOVE = 1010;
|
||
integer CE_CMD_RELEASE = 1001;
|
||
integer CE_CMD_GET_STATE = 1060;
|
||
integer CE_CMD_CFG_DUMP = 1051;
|
||
integer CE_CMD_FOV = 1040; // NEW: TourEngine -> Controller (payload: rad|quiet)
|
||
|
||
integer CE_EVT_READY = 2000;
|
||
integer CE_EVT_DENIED = 2001;
|
||
integer CE_EVT_MOVE_DONE = 2010;
|
||
integer CE_EVT_CFG_DUMP = 2051;
|
||
integer CE_EVT_STATE = 2060;
|
||
|
||
// Internal to Core
|
||
integer CE_INT_SET_CAM = 3000; // <pos>|<focus>
|
||
integer CE_INT_TOUR_BEGIN = 3001; // moveId
|
||
integer CE_INT_TOUR_END = 3002; // moveId
|
||
integer CE_INT_TOUR_STOP = 3003;
|
||
|
||
integer DEBUG_FOV = FALSE;
|
||
float gFovDbgNext = 0.0;
|
||
|
||
float deg2rad(float d) { return d * PI / 180.0; }
|
||
float rad2deg(float r) { return r * 180.0 / PI; }
|
||
|
||
float gMOVE_STEP = 0.025;
|
||
float gDEFAULT_FOCUS_DIST = 10.0;
|
||
float gTourCamMinInterval = 0.033; // 30 Hz link-message cap for tour camera frames
|
||
float gTourPosEps = 0.005;
|
||
float gTourFocusEps = 0.005;
|
||
|
||
key gOwner;
|
||
integer gCamReady = FALSE;
|
||
|
||
// ---- pending tour (waiting for state) ----
|
||
integer gPend = FALSE;
|
||
string gPendReqId = "";
|
||
integer gPendMoveId = 0;
|
||
integer gPendMoveMs = 0;
|
||
string gPendMode = "linear";
|
||
integer gPendCount = 0;
|
||
integer gPendStartFirst = FALSE;
|
||
|
||
list gPendPos;
|
||
list gPendFoc;
|
||
list gPendHoldMs;
|
||
list gPendWeight;
|
||
|
||
integer gPendFovOn = FALSE;
|
||
float gPendFovA = 0.0; // radians
|
||
float gPendFovB = 0.0; // radians
|
||
|
||
string gPipeParse = "";
|
||
integer gPipeAt = 0;
|
||
integer gPipeLen = 0;
|
||
|
||
// ---- TOUR runtime ----
|
||
integer gTourActive = FALSE;
|
||
string gTourMode = "linear";
|
||
|
||
list gTourPos; // vectors (includes captured start)
|
||
list gTourFoc; // vectors (includes captured start)
|
||
list gTourHoldSec; // floats, hold AFTER each point (same length)
|
||
|
||
list gTourSegLen; // floats, per segment (weighted)
|
||
list gTourCumLen; // floats, per point cumulative length
|
||
list gTourPointMoveT; // floats, per point time on "move clock" (excludes holds)
|
||
|
||
integer gTourCount = 0;
|
||
integer gTourLastSeg = 0;
|
||
|
||
float gTourStart = 0.0;
|
||
float gTourMoveDur = 0.0;
|
||
float gTourHoldSum = 0.0;
|
||
float gTourTotalDur = 0.0;
|
||
float gTourPathLen = 0.0;
|
||
|
||
// trapezoid motion params
|
||
float gTourTe = 0.0; // accel duration
|
||
float gTourTc = 0.0; // cruise duration
|
||
float gTourVmax = 0.0;
|
||
float gTourS1 = 0.0;
|
||
float gTourS2 = 0.0;
|
||
|
||
float gTourTeOut = 0.0; // decel duration (asymmetric)
|
||
float TOUR_EASE_FRAC = 0.20;
|
||
float gTourEaseInFrac = TOUR_EASE_FRAC;
|
||
float gTourEaseOutFrac = TOUR_EASE_FRAC;
|
||
|
||
integer gTourSpline = FALSE;
|
||
|
||
integer gTourFovOn = FALSE;
|
||
float gTourFovA = 0.0; // radians
|
||
float gTourFovB = 0.0; // radians
|
||
|
||
float gTourFovLastSend = 0.0;
|
||
float gTourFovLastRad = -1.0;
|
||
float gTourFovMinInterval = 0.10; // 10 Hz
|
||
integer DEBUG_FOV_SEND = FALSE; // zum Verifizieren, danach auf FALSE
|
||
|
||
string LSKEY_LOCK = "HS_LOCK";
|
||
vector KEEP_LOCK_OFFSET = <0,0,1.0>;
|
||
|
||
integer gTourKeepOn = FALSE;
|
||
|
||
vector gKeepFixedFocus = ZERO_VECTOR; // focA (oder first waypoint foc)
|
||
vector gKeepMotifFocus = ZERO_VECTOR; // fixed oder lock-derived
|
||
|
||
float gKeepLockNextPoll = 0.0;
|
||
float gKeepLockPollInterval = 0.05; // 20 Hz lock tracking
|
||
|
||
float gTourKeepK = 0.0; // K = d0 * tan(fov0/2)
|
||
|
||
integer gHoldCamValid = FALSE;
|
||
vector gHoldCamPos = ZERO_VECTOR;
|
||
vector gHoldCamFoc = ZERO_VECTOR;
|
||
float HOLD_CAM_EPS = 0.001;
|
||
|
||
integer gCamSendValid = FALSE;
|
||
float gCamSendLast = -999.0;
|
||
float gCamSendNext = 0.0;
|
||
vector gCamSendPos = ZERO_VECTOR;
|
||
vector gCamSendFoc = ZERO_VECTOR;
|
||
|
||
integer gSegCacheIdx = -1;
|
||
float gSegCacheStart = 0.0;
|
||
float gSegCacheEnd = 0.0;
|
||
float gSegCacheLen = 0.0;
|
||
vector gSegPos0 = ZERO_VECTOR;
|
||
vector gSegPos1 = ZERO_VECTOR;
|
||
vector gSegPos2 = ZERO_VECTOR;
|
||
vector gSegPos3 = ZERO_VECTOR;
|
||
vector gSegFoc0 = ZERO_VECTOR;
|
||
vector gSegFoc1 = ZERO_VECTOR;
|
||
vector gSegFoc2 = ZERO_VECTOR;
|
||
vector gSegFoc3 = ZERO_VECTOR;
|
||
|
||
// Debug (default OFF)
|
||
integer DEBUG_KEEP = FALSE;
|
||
dbgKeep(string s){ if (DEBUG_KEEP) llOwnerSay("[KEEP] " + s); }
|
||
|
||
float clampFovRad(float rad)
|
||
{
|
||
// clamp via degrees 10..179
|
||
float deg = rad2deg(rad);
|
||
deg = clampf(deg, 10.0, 179.0);
|
||
return deg2rad(deg);
|
||
}
|
||
|
||
float keepFovRadFrom(vector pos, vector foc, float baseRad)
|
||
{
|
||
float d = llVecDist(pos, foc);
|
||
if (d < 0.01) d = 0.01;
|
||
|
||
if (gTourKeepK <= 0.000001) {
|
||
return clampFovRad(baseRad);
|
||
}
|
||
|
||
// FOV = 2 * atan(K/d) -> use atan2(K, d)
|
||
float rad = 2.0 * llAtan2(gTourKeepK, d);
|
||
return clampFovRad(rad);
|
||
}
|
||
|
||
keepFovMaybeSend(vector pos, vector foc, float baseRad)
|
||
{
|
||
float rad = keepFovRadFrom(pos, foc, baseRad);
|
||
|
||
float now = llGetTime();
|
||
if ((now - gTourFovLastSend) < gTourFovMinInterval) return;
|
||
if (gTourFovLastRad >= 0.0 && llFabs(rad - gTourFovLastRad) < 0.002) return;
|
||
|
||
gTourFovLastSend = now;
|
||
gTourFovLastRad = rad;
|
||
|
||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|1", gOwner);
|
||
}
|
||
|
||
keepFovSendNow(vector pos, vector foc, float baseRad)
|
||
{
|
||
float rad = keepFovRadFrom(pos, foc, baseRad);
|
||
|
||
gTourFovLastSend = llGetTime();
|
||
gTourFovLastRad = rad;
|
||
|
||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|1", gOwner);
|
||
}
|
||
|
||
// ---------- helpers ----------
|
||
float clampf(float v, float lo, float hi)
|
||
{
|
||
if (v < lo) return lo;
|
||
if (v > hi) return hi;
|
||
return v;
|
||
}
|
||
|
||
float clamp01(float x)
|
||
{
|
||
if (x < 0.0) return 0.0;
|
||
if (x > 1.0) return 1.0;
|
||
return x;
|
||
}
|
||
|
||
integer findCharFrom(string s, string ch, integer start)
|
||
{
|
||
integer L = llStringLength(s);
|
||
integer i;
|
||
for (i = start; i < L; ++i) {
|
||
if (llGetSubString(s, i, i) == ch) return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
integer pipeFieldCount(string s)
|
||
{
|
||
integer L = llStringLength(s);
|
||
if (L < 1) return 0;
|
||
|
||
integer count = 1;
|
||
integer i;
|
||
for (i = 0; i < L; ++i) {
|
||
if (llGetSubString(s, i, i) == "|") ++count;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
string pipeField(string s, integer want)
|
||
{
|
||
integer L = llStringLength(s);
|
||
integer start = 0;
|
||
integer idx = 0;
|
||
|
||
while (start <= L) {
|
||
integer end = findCharFrom(s, "|", start);
|
||
if (end < 0) end = L;
|
||
|
||
if (idx == want) return llGetSubString(s, start, end - 1);
|
||
|
||
++idx;
|
||
start = end + 1;
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
pipeParseBegin(string s)
|
||
{
|
||
gPipeParse = s;
|
||
gPipeAt = 0;
|
||
gPipeLen = llStringLength(s);
|
||
}
|
||
|
||
string pipeNext()
|
||
{
|
||
if (gPipeAt > gPipeLen) return "";
|
||
|
||
integer end = findCharFrom(gPipeParse, "|", gPipeAt);
|
||
if (end < 0) end = gPipeLen;
|
||
|
||
string v = llGetSubString(gPipeParse, gPipeAt, end - 1);
|
||
gPipeAt = end + 1;
|
||
return v;
|
||
}
|
||
|
||
pipeParseClear()
|
||
{
|
||
gPipeParse = "";
|
||
gPipeAt = 0;
|
||
gPipeLen = 0;
|
||
}
|
||
|
||
tourFovMaybeSend(float frac)
|
||
{
|
||
if (!gTourFovOn) return;
|
||
|
||
frac = clamp01(frac);
|
||
float rad = gTourFovA + (gTourFovB - gTourFovA) * frac;
|
||
|
||
float now = llGetTime();
|
||
if ((now - gTourFovLastSend) < gTourFovMinInterval) return;
|
||
|
||
if (gTourFovLastRad >= 0.0 && llFabs(rad - gTourFovLastRad) < 0.002) return;
|
||
|
||
gTourFovLastSend = now;
|
||
gTourFovLastRad = rad;
|
||
|
||
if (DEBUG_FOV_SEND)
|
||
llOwnerSay("[FOV-DBG TOUR] payload-send frac=" + (string)frac + " rad=" + (string)rad);
|
||
|
||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|1", gOwner);
|
||
}
|
||
|
||
tourFovForceFinal()
|
||
{
|
||
if (!gTourFovOn) return;
|
||
|
||
float rad = gTourFovB;
|
||
|
||
if (DEBUG_FOV_SEND)
|
||
llOwnerSay("[FOV-DBG TOUR] payload-final rad=" + (string)rad);
|
||
|
||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|1", gOwner);
|
||
}
|
||
|
||
integer tourSendCam(vector pos, vector foc, integer force)
|
||
{
|
||
float now = llGetTime();
|
||
|
||
if (!force && gCamSendValid) {
|
||
if (gTourCamMinInterval > 0.0 && now < gCamSendNext) return FALSE;
|
||
if (llVecDist(pos, gCamSendPos) <= gTourPosEps && llVecDist(foc, gCamSendFoc) <= gTourFocusEps) return FALSE;
|
||
}
|
||
|
||
gCamSendValid = TRUE;
|
||
gCamSendLast = now;
|
||
if (gTourCamMinInterval > 0.0) {
|
||
if (force || gCamSendNext <= 0.0) gCamSendNext = now + gTourCamMinInterval;
|
||
else {
|
||
gCamSendNext += gTourCamMinInterval;
|
||
if (gCamSendNext < now) gCamSendNext = now + gTourCamMinInterval;
|
||
}
|
||
}
|
||
gCamSendPos = pos;
|
||
gCamSendFoc = foc;
|
||
|
||
llMessageLinked(LINK_SET, CE_INT_SET_CAM, (string)pos + "|" + (string)foc, gOwner);
|
||
return TRUE;
|
||
}
|
||
|
||
tourSendHoldCam(vector pos, vector foc)
|
||
{
|
||
integer first = !gHoldCamValid;
|
||
|
||
if (gHoldCamValid) {
|
||
if (llVecDist(pos, gHoldCamPos) < HOLD_CAM_EPS && llVecDist(foc, gHoldCamFoc) < HOLD_CAM_EPS) return;
|
||
}
|
||
|
||
gHoldCamValid = TRUE;
|
||
gHoldCamPos = pos;
|
||
gHoldCamFoc = foc;
|
||
|
||
tourSendCam(pos, foc, first);
|
||
}
|
||
|
||
tourCacheSegment(integer seg)
|
||
{
|
||
if (seg == gSegCacheIdx) return;
|
||
if (gTourCount < 2) return;
|
||
|
||
integer last = gTourCount - 1;
|
||
integer i1 = seg;
|
||
integer i2 = seg + 1;
|
||
|
||
if (i1 < 0) i1 = 0;
|
||
if (i2 > last) i2 = last;
|
||
|
||
integer i0 = i1 - 1; if (i0 < 0) i0 = 0;
|
||
integer i3 = i2 + 1; if (i3 > last) i3 = last;
|
||
|
||
gSegCacheIdx = seg;
|
||
gSegCacheStart = llList2Float(gTourCumLen, seg);
|
||
gSegCacheEnd = llList2Float(gTourCumLen, seg + 1);
|
||
gSegCacheLen = gSegCacheEnd - gSegCacheStart;
|
||
|
||
gSegPos0 = llList2Vector(gTourPos, i0);
|
||
gSegPos1 = llList2Vector(gTourPos, i1);
|
||
gSegPos2 = llList2Vector(gTourPos, i2);
|
||
gSegPos3 = llList2Vector(gTourPos, i3);
|
||
|
||
gSegFoc0 = llList2Vector(gTourFoc, i0);
|
||
gSegFoc1 = llList2Vector(gTourFoc, i1);
|
||
gSegFoc2 = llList2Vector(gTourFoc, i2);
|
||
gSegFoc3 = llList2Vector(gTourFoc, i3);
|
||
}
|
||
|
||
keepUpdateMotifFocus()
|
||
{
|
||
// default: fixed focus
|
||
gKeepMotifFocus = gKeepFixedFocus;
|
||
|
||
string s = llLinksetDataRead(LSKEY_LOCK);
|
||
if (s == "") return;
|
||
|
||
list p = llParseString2List(s, ["|"], []);
|
||
integer L = llGetListLength(p);
|
||
if (L < 1) return;
|
||
|
||
integer on = (integer)llList2String(p, 0);
|
||
if (!on) return;
|
||
if (L < 2) return;
|
||
|
||
string arg = llList2String(p, 1);
|
||
|
||
// vector lock
|
||
if (llGetSubString(arg, 0, 0) == "<") {
|
||
gKeepMotifFocus = (vector)arg;
|
||
return;
|
||
}
|
||
|
||
// key lock
|
||
key k = (key)arg;
|
||
if (k != NULL_KEY) {
|
||
list d = llGetObjectDetails(k, [OBJECT_POS]);
|
||
if (llGetListLength(d) >= 1) {
|
||
vector tpos = llList2Vector(d, 0);
|
||
gKeepMotifFocus = tpos + KEEP_LOCK_OFFSET;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// fallback: treat as vector string
|
||
gKeepMotifFocus = (vector)arg;
|
||
}
|
||
|
||
vector catmullRomVec(vector p0, vector p1, vector p2, vector p3, float t)
|
||
{
|
||
float t2 = t * t;
|
||
float t3 = t2 * t;
|
||
|
||
return 0.5 * (
|
||
(2.0 * p1) +
|
||
(-p0 + p2) * t +
|
||
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
|
||
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
|
||
);
|
||
}
|
||
|
||
vector tourSplineAt(list pts, integer seg, float u)
|
||
{
|
||
integer last = llGetListLength(pts) - 1;
|
||
|
||
integer i1 = seg;
|
||
integer i2 = seg + 1;
|
||
|
||
integer i0 = i1 - 1; if (i0 < 0) i0 = 0;
|
||
integer i3 = i2 + 1; if (i3 > last) i3 = last;
|
||
|
||
vector p0 = llList2Vector(pts, i0);
|
||
vector p1 = llList2Vector(pts, i1);
|
||
vector p2 = llList2Vector(pts, i2);
|
||
vector p3 = llList2Vector(pts, i3);
|
||
|
||
return catmullRomVec(p0, p1, p2, p3, u);
|
||
}
|
||
|
||
float tourDistAt(float t)
|
||
{
|
||
if (gTourMoveDur <= 0.0001 || gTourPathLen <= 0.0001 || gTourVmax <= 0.0001) return 0.0;
|
||
t = clampf(t, 0.0, gTourMoveDur);
|
||
|
||
if (gTourTe <= 0.0001 && gTourTeOut <= 0.0001) {
|
||
return gTourPathLen * (t / gTourMoveDur);
|
||
}
|
||
|
||
float teIn = gTourTe;
|
||
float teOut = gTourTeOut;
|
||
|
||
if (t <= teIn) {
|
||
return 0.5 * gTourVmax * (t * t / teIn);
|
||
}
|
||
|
||
if (t <= (teIn + gTourTc)) {
|
||
return gTourS1 + gTourVmax * (t - teIn);
|
||
}
|
||
|
||
float t2 = t - (teIn + gTourTc);
|
||
return gTourS2 + gTourVmax * t2 - 0.5 * gTourVmax * (t2 * t2 / teOut);
|
||
}
|
||
|
||
float tourTimeAtDist(float s)
|
||
{
|
||
if (gTourMoveDur <= 0.0001 || gTourPathLen <= 0.0001 || gTourVmax <= 0.0001) return 0.0;
|
||
s = clampf(s, 0.0, gTourPathLen);
|
||
|
||
if (gTourTe <= 0.0001 && gTourTeOut <= 0.0001) {
|
||
return gTourMoveDur * (s / gTourPathLen);
|
||
}
|
||
|
||
float teIn = gTourTe;
|
||
float teOut = gTourTeOut;
|
||
|
||
if (s <= gTourS1) {
|
||
return llSqrt((2.0 * s * teIn) / gTourVmax);
|
||
}
|
||
|
||
if (s <= gTourS2) {
|
||
return teIn + ((s - gTourS1) / gTourVmax);
|
||
}
|
||
|
||
float s3 = s - gTourS2;
|
||
float disc = (teOut * teOut) - ((2.0 * teOut * s3) / gTourVmax);
|
||
if (disc < 0.0) disc = 0.0;
|
||
|
||
float t2 = teOut - llSqrt(disc);
|
||
return (teIn + gTourTc) + t2;
|
||
}
|
||
|
||
tourStopInternal()
|
||
{
|
||
gTourActive = FALSE;
|
||
gPend = FALSE;
|
||
gPendReqId = "";
|
||
gPendStartFirst = FALSE;
|
||
|
||
gTourMode = "linear";
|
||
gTourPos = [];
|
||
gTourFoc = [];
|
||
gTourHoldSec = [];
|
||
gTourSegLen = [];
|
||
gTourCumLen = [];
|
||
gTourPointMoveT = [];
|
||
gTourCount = 0;
|
||
gTourLastSeg = 0;
|
||
|
||
gTourStart = 0.0;
|
||
gTourMoveDur = 0.0;
|
||
gTourHoldSum = 0.0;
|
||
gTourTotalDur = 0.0;
|
||
gTourPathLen = 0.0;
|
||
|
||
gTourTe = gTourTc = gTourVmax = gTourS1 = gTourS2 = 0.0;
|
||
gTourTeOut = 0.0;
|
||
|
||
gTourSpline = FALSE;
|
||
gTourEaseInFrac = TOUR_EASE_FRAC;
|
||
gTourEaseOutFrac = TOUR_EASE_FRAC;
|
||
|
||
gPendFovOn = FALSE;
|
||
gPendFovA = 0.0;
|
||
gPendFovB = 0.0;
|
||
|
||
gTourFovOn = FALSE;
|
||
gTourFovA = 0.0;
|
||
gTourFovB = 0.0;
|
||
gTourFovLastSend = 0.0;
|
||
gTourFovLastRad = -1.0;
|
||
|
||
gTourKeepOn = FALSE;
|
||
gTourKeepK = 0.0;
|
||
gKeepFixedFocus = ZERO_VECTOR;
|
||
gKeepMotifFocus = ZERO_VECTOR;
|
||
gKeepLockNextPoll = 0.0;
|
||
|
||
gHoldCamValid = FALSE;
|
||
gHoldCamPos = ZERO_VECTOR;
|
||
gHoldCamFoc = ZERO_VECTOR;
|
||
|
||
gCamSendValid = FALSE;
|
||
gCamSendLast = -999.0;
|
||
gCamSendNext = 0.0;
|
||
gCamSendPos = ZERO_VECTOR;
|
||
gCamSendFoc = ZERO_VECTOR;
|
||
|
||
gSegCacheIdx = -1;
|
||
gSegCacheStart = 0.0;
|
||
gSegCacheEnd = 0.0;
|
||
gSegCacheLen = 0.0;
|
||
gSegPos0 = ZERO_VECTOR;
|
||
gSegPos1 = ZERO_VECTOR;
|
||
gSegPos2 = ZERO_VECTOR;
|
||
gSegPos3 = ZERO_VECTOR;
|
||
gSegFoc0 = ZERO_VECTOR;
|
||
gSegFoc1 = ZERO_VECTOR;
|
||
gSegFoc2 = ZERO_VECTOR;
|
||
gSegFoc3 = ZERO_VECTOR;
|
||
|
||
llSetTimerEvent(0.0);
|
||
}
|
||
|
||
applyCfgDump(string dump)
|
||
{
|
||
// dump format: key=val|key=val|...
|
||
list parts = llParseString2List(dump, ["|"], []);
|
||
integer i;
|
||
for (i = 0; i < llGetListLength(parts); ++i) {
|
||
string kv = llList2String(parts, i);
|
||
integer eq = llSubStringIndex(kv, "=");
|
||
if (eq < 1) jump next;
|
||
string k = llToLower(llStringTrim(llGetSubString(kv, 0, eq - 1), STRING_TRIM));
|
||
string v = llStringTrim(llGetSubString(kv, eq + 1, -1), STRING_TRIM);
|
||
|
||
if (k == "move_step") gMOVE_STEP = (float)v;
|
||
else if (k == "default_focus_dist") gDEFAULT_FOCUS_DIST = (float)v;
|
||
else if (k == "tour_cam_min_interval") gTourCamMinInterval = clampf((float)v, 0.0, 0.25);
|
||
else if (k == "tour_pos_epsilon") gTourPosEps = clampf((float)v, 0.0, 1.0);
|
||
else if (k == "tour_focus_epsilon") gTourFocusEps = clampf((float)v, 0.0, 1.0);
|
||
@next;
|
||
}
|
||
|
||
if (gTourActive) llSetTimerEvent(gMOVE_STEP);
|
||
}
|
||
|
||
startTourFromState(vector startPos, vector startFoc)
|
||
{
|
||
// ---- FIX: preserve pending FOV before runtime reset ----
|
||
integer keepFovOn = gPendFovOn;
|
||
float keepFovA = gPendFovA;
|
||
float keepFovB = gPendFovB;
|
||
integer keepStartFirst = gPendStartFirst;
|
||
|
||
// Cancel any previous tour runtime
|
||
tourStopInternal();
|
||
|
||
// restore into runtime (tourStopInternal may clear pend vars)
|
||
gTourFovOn = keepFovOn;
|
||
gTourFovA = keepFovA;
|
||
gTourFovB = keepFovB;
|
||
gPendStartFirst = keepStartFirst;
|
||
|
||
// Mode parsing (supports "spline+ease_in+keep")
|
||
string m = llToLower(gPendMode);
|
||
integer useSpline = FALSE;
|
||
string prof = "linear";
|
||
|
||
gTourEaseInFrac = TOUR_EASE_FRAC;
|
||
gTourEaseOutFrac = TOUR_EASE_FRAC;
|
||
|
||
integer wantKeep = FALSE;
|
||
|
||
list partsM = llParseString2List(m, ["+"], []);
|
||
integer mi;
|
||
for (mi = 0; mi < llGetListLength(partsM); ++mi) {
|
||
string tok = llStringTrim(llList2String(partsM, mi), STRING_TRIM);
|
||
if (tok == "") jump mnext;
|
||
|
||
if (tok == "keep" || tok == "keepframe" || tok == "kf") { wantKeep = TRUE; jump mnext; }
|
||
|
||
if (tok == "spline") useSpline = TRUE;
|
||
else if (tok == "linear" || tok == "line") prof = "linear";
|
||
else if (tok == "constant" || tok == "noease" || tok == "no_ease") { prof = "constant"; gTourEaseInFrac = 0.0; gTourEaseOutFrac = 0.0; }
|
||
else if (tok == "ease_in" || tok == "easein") { prof = "ease_in"; gTourEaseInFrac = 0.35; gTourEaseOutFrac = 0.15; }
|
||
else if (tok == "ease_out" || tok == "easeout") { prof = "ease_out"; gTourEaseInFrac = 0.15; gTourEaseOutFrac = 0.35; }
|
||
else if (tok == "ease_in_out" || tok == "easeinout" || tok == "ease-in-out") { prof = "ease_in_out"; gTourEaseInFrac = 0.30; gTourEaseOutFrac = 0.30; }
|
||
else if (tok == "smoothstep") { prof = "smoothstep"; gTourEaseInFrac = 0.45; gTourEaseOutFrac = 0.45; }
|
||
else if (tok == "cubic") { prof = "cubic"; gTourEaseInFrac = 0.25; gTourEaseOutFrac = 0.25; }
|
||
else if (tok == "quint" || tok == "quintic") { prof = "quint"; gTourEaseInFrac = 0.40; gTourEaseOutFrac = 0.40; }
|
||
@mnext;
|
||
}
|
||
|
||
gTourKeepOn = wantKeep;
|
||
gTourSpline = useSpline;
|
||
|
||
if (gTourSpline) {
|
||
if (prof == "linear") gTourMode = "spline";
|
||
else gTourMode = "spline+" + prof;
|
||
} else {
|
||
if (prof == "linear") gTourMode = "linear";
|
||
else gTourMode = prof;
|
||
}
|
||
|
||
if (gPendStartFirst) {
|
||
gTourPos = gPendPos;
|
||
gTourFoc = gPendFoc;
|
||
} else {
|
||
gTourPos = [startPos] + gPendPos;
|
||
gTourFoc = [startFoc] + gPendFoc;
|
||
}
|
||
|
||
// ===== KEEP: choose fixed focus (prefer focA = first waypoint focus) and pick motif focus (lock if on) =====
|
||
if (gTourKeepOn) {
|
||
gKeepFixedFocus = startFoc;
|
||
if (llGetListLength(gPendFoc) > 0) {
|
||
gKeepFixedFocus = llList2Vector(gPendFoc, 0); // focA
|
||
}
|
||
|
||
keepUpdateMotifFocus();
|
||
gKeepLockNextPoll = llGetTime() + gKeepLockPollInterval;
|
||
|
||
// compute K from startPos and CURRENT motif focus
|
||
float baseRad0 = gTourFovA;
|
||
if (!gTourFovOn || baseRad0 <= 0.0001) baseRad0 = deg2rad(60.0);
|
||
|
||
float d0 = llVecDist(startPos, gKeepMotifFocus);
|
||
if (d0 < 0.01) d0 = 0.01;
|
||
|
||
gTourKeepK = d0 * llTan(baseRad0 * 0.5);
|
||
|
||
dbgKeep("keep ON baseDeg=" + (string)rad2deg(baseRad0) + " d0=" + (string)d0 + " K=" + (string)gTourKeepK);
|
||
}
|
||
|
||
if (gPendStartFirst) gTourHoldSec = [];
|
||
else gTourHoldSec = [0.0];
|
||
integer i;
|
||
for (i = 0; i < llGetListLength(gPendHoldMs); ++i) {
|
||
float hs = (float)llList2Integer(gPendHoldMs, i) / 1000.0;
|
||
if (hs < 0.0) hs = 0.0;
|
||
gTourHoldSec += [hs];
|
||
}
|
||
|
||
gTourCount = llGetListLength(gTourPos);
|
||
|
||
gTourMoveDur = (float)gPendMoveMs / 1000.0;
|
||
if (gTourMoveDur < 0.0) gTourMoveDur = 0.0;
|
||
|
||
// Sum holds
|
||
gTourHoldSum = 0.0;
|
||
for (i = 0; i < gTourCount; ++i) gTourHoldSum += llList2Float(gTourHoldSec, i);
|
||
|
||
gTourTotalDur = gTourMoveDur + gTourHoldSum;
|
||
if (gTourTotalDur < 0.01) gTourTotalDur = 0.01;
|
||
|
||
// Segment lengths (weighted)
|
||
gTourSegLen = [];
|
||
gTourCumLen = [0.0];
|
||
gTourPathLen = 0.0;
|
||
|
||
for (i = 0; i < gTourCount - 1; ++i) {
|
||
float L = llVecDist(llList2Vector(gTourPos, i), llList2Vector(gTourPos, i + 1));
|
||
if (L < 0.000001) L = 0.0;
|
||
|
||
float w = 1.0;
|
||
if (i < llGetListLength(gPendWeight)) w = llList2Float(gPendWeight, i);
|
||
w = clampf(w, 0.10, 10.0);
|
||
|
||
float WL = L * w;
|
||
|
||
gTourSegLen += [WL];
|
||
gTourPathLen += WL;
|
||
|
||
gTourCumLen += [gTourPathLen];
|
||
}
|
||
|
||
// Trapezoid profile params (movement only), asymmetric ease
|
||
if (gTourMoveDur > 0.0001 && gTourPathLen > 0.0001) {
|
||
float teIn = gTourMoveDur * gTourEaseInFrac;
|
||
float teOut = gTourMoveDur * gTourEaseOutFrac;
|
||
|
||
if (teIn > 0.0 && teIn < 0.10) teIn = 0.10;
|
||
if (teOut > 0.0 && teOut < 0.10) teOut = 0.10;
|
||
|
||
float maxTe = gTourMoveDur * 0.45;
|
||
if (teIn > maxTe) teIn = maxTe;
|
||
if (teOut > maxTe) teOut = maxTe;
|
||
|
||
if (teIn < 0.01) teIn = 0.0;
|
||
if (teOut < 0.01) teOut = 0.0;
|
||
|
||
if ((teIn + teOut) > gTourMoveDur && (teIn + teOut) > 0.0001) {
|
||
float sc = gTourMoveDur / (teIn + teOut);
|
||
teIn *= sc;
|
||
teOut *= sc;
|
||
gTourTc = 0.0;
|
||
} else {
|
||
gTourTc = gTourMoveDur - teIn - teOut;
|
||
if (gTourTc < 0.0) gTourTc = 0.0;
|
||
}
|
||
|
||
gTourTe = teIn;
|
||
gTourTeOut = teOut;
|
||
|
||
float denom = gTourTc + 0.5 * (gTourTe + gTourTeOut);
|
||
if (denom < 0.01) denom = 0.01;
|
||
|
||
gTourVmax = gTourPathLen / denom;
|
||
|
||
gTourS1 = 0.5 * gTourVmax * gTourTe;
|
||
gTourS2 = gTourS1 + (gTourVmax * gTourTc);
|
||
} else {
|
||
gTourTe = gTourTc = gTourVmax = gTourS1 = gTourS2 = 0.0;
|
||
gTourTeOut = 0.0;
|
||
}
|
||
|
||
// Arrival times on move clock
|
||
gTourPointMoveT = [];
|
||
if (gTourMoveDur <= 0.0001 || gTourPathLen <= 0.0001) {
|
||
for (i = 0; i < gTourCount; ++i) gTourPointMoveT += [0.0];
|
||
} else {
|
||
for (i = 0; i < gTourCount; ++i) {
|
||
float dist = llList2Float(gTourCumLen, i);
|
||
gTourPointMoveT += [tourTimeAtDist(dist)];
|
||
}
|
||
gTourPointMoveT = llListReplaceList(gTourPointMoveT, [gTourMoveDur], gTourCount - 1, gTourCount - 1);
|
||
}
|
||
|
||
// Start runtime
|
||
gTourStart = llGetTime();
|
||
gTourLastSeg = 0;
|
||
gTourActive = TRUE;
|
||
|
||
// Tell Core: begin external drive + set first frame
|
||
llMessageLinked(LINK_SET, CE_INT_TOUR_BEGIN, (string)gPendMoveId, gOwner);
|
||
|
||
vector startFocSend = startFoc;
|
||
if (gTourKeepOn) startFocSend = gKeepMotifFocus;
|
||
|
||
tourSendCam(startPos, startFocSend, TRUE);
|
||
|
||
// send initial FOV (quiet)
|
||
if (gTourKeepOn) {
|
||
float baseRad1 = gTourFovA;
|
||
if (!gTourFovOn || baseRad1 <= 0.0001) baseRad1 = deg2rad(60.0);
|
||
keepFovSendNow(startPos, startFocSend, baseRad1);
|
||
}
|
||
else if (gTourFovOn) {
|
||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTourFovA + "|1", gOwner);
|
||
gTourFovLastSend = llGetTime();
|
||
gTourFovLastRad = gTourFovA;
|
||
}
|
||
|
||
llSetTimerEvent(gMOVE_STEP);
|
||
|
||
gPend = FALSE;
|
||
gPendReqId = "";
|
||
gPendPos = [];
|
||
gPendFoc = [];
|
||
gPendHoldMs = [];
|
||
gPendWeight = [];
|
||
gPendStartFirst = FALSE;
|
||
}
|
||
|
||
requestStateForPending()
|
||
{
|
||
gPendReqId = "TOUR:" + (string)gPendMoveId + ":" + (string)llGetUnixTime();
|
||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, gPendReqId, gOwner);
|
||
}
|
||
|
||
startPendingAtFirst()
|
||
{
|
||
if (!gPend || llGetListLength(gPendPos) < 1 || llGetListLength(gPendFoc) < 1) return;
|
||
startTourFromState(llList2Vector(gPendPos, 0), llList2Vector(gPendFoc, 0));
|
||
}
|
||
|
||
default
|
||
{
|
||
state_entry()
|
||
{
|
||
gOwner = llGetOwner();
|
||
|
||
// cfg sync (move_step etc.)
|
||
llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner);
|
||
}
|
||
|
||
on_rez(integer sp)
|
||
{
|
||
gOwner = llGetOwner();
|
||
tourStopInternal();
|
||
}
|
||
|
||
attach(key id)
|
||
{
|
||
gOwner = llGetOwner();
|
||
if (id == NULL_KEY) {
|
||
if (gTourActive || gPend) tourStopInternal();
|
||
}
|
||
}
|
||
|
||
timer()
|
||
{
|
||
if (!gTourActive) { llSetTimerEvent(0.0); return; }
|
||
|
||
// ---- DEBUG: periodic FOV ticks during ANY active tour (ignore payload for now) ----
|
||
if (DEBUG_FOV) {
|
||
float nowD = llGetTime();
|
||
if (nowD >= gFovDbgNext) {
|
||
gFovDbgNext = nowD + 0.25; // 4 Hz
|
||
|
||
float fracD = 0.0;
|
||
if (gTourTotalDur > 0.001) fracD = (nowD - gTourStart) / gTourTotalDur;
|
||
if (fracD < 0.0) fracD = 0.0;
|
||
if (fracD > 1.0) fracD = 1.0;
|
||
|
||
float radD = deg2rad(30.0 + (90.0 - 30.0) * fracD);
|
||
|
||
llOwnerSay("[FOV-DBG TOUR] send tick frac=" + (string)fracD
|
||
+ " rad=" + (string)radD + " deg=" + (string)rad2deg(radD));
|
||
|
||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)radD + "|1", gOwner);
|
||
}
|
||
}
|
||
|
||
float now = llGetTime();
|
||
float elapsed = now - gTourStart;
|
||
|
||
// baseRad used for keepframe (start FOV)
|
||
float baseRad = gTourFovA;
|
||
if (!gTourFovOn || baseRad <= 0.0001) baseRad = deg2rad(60.0);
|
||
|
||
if (elapsed >= gTourTotalDur) {
|
||
vector endPos = llList2Vector(gTourPos, gTourCount - 1);
|
||
vector endFoc = llList2Vector(gTourFoc, gTourCount - 1);
|
||
|
||
if (gTourKeepOn) {
|
||
keepUpdateMotifFocus();
|
||
endFoc = gKeepMotifFocus;
|
||
}
|
||
|
||
tourSendCam(endPos, endFoc, TRUE);
|
||
llMessageLinked(LINK_SET, CE_INT_TOUR_END, (string)gPendMoveId, gOwner);
|
||
llMessageLinked(LINK_SET, CE_EVT_MOVE_DONE, (string)gPendMoveId, gOwner);
|
||
|
||
if (gTourKeepOn) {
|
||
keepFovSendNow(endPos, endFoc, baseRad);
|
||
} else {
|
||
tourFovForceFinal();
|
||
}
|
||
|
||
tourStopInternal();
|
||
return;
|
||
}
|
||
|
||
// Determine holds (elapsed is overall timeline)
|
||
float holdPassed = 0.0;
|
||
integer holdIdx = -1;
|
||
|
||
integer i;
|
||
for (i = 0; i < gTourCount; ++i) {
|
||
float tArr = llList2Float(gTourPointMoveT, i) + holdPassed;
|
||
float h = llList2Float(gTourHoldSec, i);
|
||
|
||
if (h > 0.0) {
|
||
if (elapsed >= tArr && elapsed < (tArr + h)) {
|
||
holdIdx = i;
|
||
jump foundHold;
|
||
}
|
||
if (elapsed >= (tArr + h)) holdPassed += h;
|
||
}
|
||
}
|
||
@foundHold;
|
||
|
||
if (holdIdx != -1) {
|
||
vector hp = llList2Vector(gTourPos, holdIdx);
|
||
vector hf = llList2Vector(gTourFoc, holdIdx);
|
||
|
||
if (gTourKeepOn) {
|
||
if (now >= gKeepLockNextPoll) {
|
||
keepUpdateMotifFocus();
|
||
gKeepLockNextPoll = now + gKeepLockPollInterval;
|
||
}
|
||
hf = gKeepMotifFocus;
|
||
}
|
||
|
||
tourSendHoldCam(hp, hf);
|
||
|
||
if (gTourKeepOn) {
|
||
keepFovMaybeSend(hp, hf, baseRad);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
float moveT = elapsed - holdPassed;
|
||
gHoldCamValid = FALSE;
|
||
if (moveT < 0.0) moveT = 0.0;
|
||
if (moveT > gTourMoveDur) moveT = gTourMoveDur;
|
||
|
||
float dist;
|
||
if (gTourMoveDur <= 0.0001) dist = gTourPathLen;
|
||
else dist = tourDistAt(moveT);
|
||
|
||
integer segCount = gTourCount - 1;
|
||
integer seg = gTourLastSeg;
|
||
if (seg < 0) seg = 0;
|
||
if (seg >= segCount) seg = segCount - 1;
|
||
|
||
while (seg < segCount - 1 && dist > llList2Float(gTourCumLen, seg + 1) + 0.0001) ++seg;
|
||
while (seg > 0 && dist < llList2Float(gTourCumLen, seg) - 0.0001) --seg;
|
||
|
||
gTourLastSeg = seg;
|
||
tourCacheSegment(seg);
|
||
|
||
float u = 0.0;
|
||
if (gSegCacheLen > 0.000001) u = (dist - gSegCacheStart) / gSegCacheLen;
|
||
u = clampf(u, 0.0, 1.0);
|
||
|
||
vector pos;
|
||
vector foc;
|
||
|
||
if (gTourSpline) {
|
||
pos = catmullRomVec(gSegPos0, gSegPos1, gSegPos2, gSegPos3, u);
|
||
foc = catmullRomVec(gSegFoc0, gSegFoc1, gSegFoc2, gSegFoc3, u);
|
||
} else {
|
||
pos = gSegPos1 + (gSegPos2 - gSegPos1) * u;
|
||
foc = gSegFoc1 + (gSegFoc2 - gSegFoc1) * u;
|
||
}
|
||
|
||
if (gTourKeepOn) {
|
||
if (now >= gKeepLockNextPoll) {
|
||
keepUpdateMotifFocus();
|
||
gKeepLockNextPoll = now + gKeepLockPollInterval;
|
||
}
|
||
foc = gKeepMotifFocus;
|
||
}
|
||
|
||
tourSendCam(pos, foc, FALSE);
|
||
|
||
// FOV: keepframe or ramp
|
||
float frac = 1.0;
|
||
if (gTourPathLen > 0.0001) frac = dist / gTourPathLen;
|
||
else if (gTourMoveDur > 0.0001) frac = moveT / gTourMoveDur;
|
||
|
||
if (gTourKeepOn) {
|
||
keepFovMaybeSend(pos, foc, baseRad);
|
||
} else {
|
||
tourFovMaybeSend(frac);
|
||
}
|
||
}
|
||
|
||
link_message(integer sender, integer num, string str, key id)
|
||
{
|
||
if (num == CE_INT_TOUR_STOP) {
|
||
tourStopInternal();
|
||
return;
|
||
}
|
||
|
||
if (num == CE_EVT_READY) {
|
||
gCamReady = TRUE;
|
||
if (gPend) {
|
||
if (gPendStartFirst) startPendingAtFirst();
|
||
else requestStateForPending();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (num == CE_EVT_DENIED) { gCamReady = FALSE; return; }
|
||
|
||
if (num == CE_EVT_CFG_DUMP) {
|
||
applyCfgDump(str);
|
||
return;
|
||
}
|
||
|
||
if (num == CE_CMD_RELEASE) {
|
||
// Core sendet CE_INT_TOUR_STOP ohnehin (und/oder wir stoppen uns selbst)
|
||
tourStopInternal();
|
||
return;
|
||
}
|
||
|
||
if (num == CE_CMD_STOP) {
|
||
tourStopInternal();
|
||
return;
|
||
}
|
||
|
||
// Move überschreibt Tour: nur uns selbst stoppen – NICHT den Core stoppen
|
||
if (num == CE_CMD_MOVE) {
|
||
if (gTourActive || gPend) tourStopInternal();
|
||
return;
|
||
}
|
||
|
||
if (num == CE_EVT_STATE) {
|
||
// payload: reqId|<pos>|<focus>|<rot>
|
||
if (pipeFieldCount(str) < 4) return;
|
||
|
||
string reqId = pipeField(str, 0);
|
||
if (!gPend || reqId != gPendReqId) return;
|
||
|
||
vector startPos = (vector)pipeField(str, 1);
|
||
vector startFoc = (vector)pipeField(str, 2);
|
||
|
||
// If Core returned zeros (very early), fall back to first waypoint
|
||
if (startPos == ZERO_VECTOR && llGetListLength(gPendPos) > 0) {
|
||
startPos = llList2Vector(gPendPos, 0);
|
||
startFoc = llList2Vector(gPendFoc, 0);
|
||
}
|
||
|
||
startTourFromState(startPos, startFoc);
|
||
return;
|
||
}
|
||
|
||
if (num == CE_CMD_TOUR) {
|
||
integer len = pipeFieldCount(str);
|
||
if (len < 4) return;
|
||
|
||
pipeParseBegin(str);
|
||
integer mid = (integer)pipeNext();
|
||
integer dms = (integer)pipeNext();
|
||
string mode = pipeNext();
|
||
integer count = (integer)pipeNext();
|
||
|
||
if (count < 2) {
|
||
pipeParseClear();
|
||
llMessageLinked(LINK_SET, CE_EVT_MOVE_DONE, (string)mid, gOwner);
|
||
return;
|
||
}
|
||
|
||
// Optional header extension: |FOV|<radA>|<radB>|
|
||
integer base = 4;
|
||
integer fovOn = FALSE;
|
||
float fovA = 0.0;
|
||
float fovB = 0.0;
|
||
|
||
string next = pipeNext();
|
||
if (len >= 7 && llToUpper(next) == "FOV") {
|
||
fovOn = TRUE;
|
||
fovA = (float)pipeNext();
|
||
fovB = (float)pipeNext();
|
||
base = 7;
|
||
}
|
||
else {
|
||
gPipeAt = 0;
|
||
pipeNext(); pipeNext(); pipeNext(); pipeNext();
|
||
}
|
||
|
||
integer per = 3;
|
||
integer needed3 = base + (count * 3);
|
||
integer needed4 = base + (count * 4);
|
||
|
||
if (len >= needed4) per = 4;
|
||
else if (len < needed3) {
|
||
pipeParseClear();
|
||
return;
|
||
}
|
||
|
||
list posIn = [];
|
||
list focIn = [];
|
||
list holdIn = [];
|
||
list wIn = [];
|
||
|
||
integer i;
|
||
for (i = 0; i < count; ++i) {
|
||
vector pos = (vector)pipeNext();
|
||
vector foc = (vector)pipeNext();
|
||
integer hms = (integer)pipeNext();
|
||
if (hms < 0) hms = 0;
|
||
|
||
posIn += [pos];
|
||
focIn += [foc];
|
||
holdIn += [hms];
|
||
|
||
if (per == 4) {
|
||
float w = (float)pipeNext();
|
||
wIn += [w];
|
||
}
|
||
}
|
||
pipeParseClear();
|
||
|
||
// store pending + stop our current playback
|
||
tourStopInternal();
|
||
|
||
gPend = TRUE;
|
||
gPendMoveId = mid;
|
||
gPendMoveMs = dms;
|
||
gPendMode = mode;
|
||
gPendCount = count;
|
||
gPendStartFirst = (llSubStringIndex(llToLower(mode), "startfirst") >= 0);
|
||
gPendPos = posIn;
|
||
gPendFoc = focIn;
|
||
gPendHoldMs = holdIn;
|
||
gPendWeight = wIn;
|
||
|
||
// NEW: pending FOV
|
||
gPendFovOn = fovOn;
|
||
gPendFovA = fovA;
|
||
gPendFovB = fovB;
|
||
|
||
if (!gCamReady) {
|
||
llMessageLinked(LINK_SET, CE_CMD_INIT, "src=TOUR", gOwner);
|
||
return;
|
||
}
|
||
|
||
if (gPendStartFirst) {
|
||
startPendingAtFirst();
|
||
return;
|
||
}
|
||
|
||
requestStateForPending();
|
||
return;
|
||
}
|
||
}
|
||
}
|