c80059ba53
Due to Nasal/view.nas making a sequential copy of views, inserting new view number 8 for Tower View AGL broke custom views, e.g. for aircraft 777). We no longer require Model View (it's identical to Helicopter view now), so this way we can have Tower View AGL without altering sequential view numbers.
869 lines
27 KiB
Text
869 lines
27 KiB
Text
##
|
|
## view.nas
|
|
##
|
|
## Nasal code for implementing view-specific functionality.
|
|
|
|
# For debugging. Returns string containing description of <x>.
|
|
#
|
|
var str = func(x, prefix='') {
|
|
prefix = '';
|
|
var ret = '';
|
|
ret = ret ~ prefix ~ typeof(x) ~ ':';
|
|
if (typeof(x) == "hash") {
|
|
ret = ret ~ '{';
|
|
#prefix = prefix ~ ' ';
|
|
foreach(var key; keys(x)){
|
|
ret = ret ~ prefix ~ " '" ~ key ~ "'" ~ ':';
|
|
ret = ret ~ str(x[key], prefix ~ ' ');
|
|
}
|
|
ret = ret ~ '}'
|
|
}
|
|
else {
|
|
ret = ret ~ sprintf('%s', x);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
var index = nil; # current view index
|
|
var views = nil; # list of all view branches (/sim/view[n]) as props.Node
|
|
var current = nil; # current view branch (e.g. /sim/view[1]) as props.Node
|
|
var fovProp = nil;
|
|
|
|
var hasmember = func(class, member) {
|
|
if (contains(class, member))
|
|
return 1;
|
|
if (!contains(class, "parents"))
|
|
return 0;
|
|
if (typeof(class.parents) != "vector")
|
|
return 0;
|
|
foreach (var parent; class.parents)
|
|
if (hasmember(parent, member))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
|
|
# Dynamically calculate limits so that it takes STEPS iterations to
|
|
# traverse the whole range, the maximum FOV is fixed at 120 degrees,
|
|
# and the minimum corresponds to normal maximum human visual acuity
|
|
# (~1 arc minute of resolution, although apparently people vary widely
|
|
# in this ability). Quick derivation of the math:
|
|
#
|
|
# mul^steps = max/min
|
|
# steps * ln(mul) = ln(max/min)
|
|
# mul = exp(ln(max/min) / steps)
|
|
var STEPS = 40;
|
|
var ACUITY = 1/60; # Maximum angle subtended by one pixel (== 1 arc minute)
|
|
var max = var min = var mul = 0;
|
|
var calcMul = func {
|
|
max = 120; # Fixed at 120 degrees
|
|
min = getprop("/sim/startup/xsize") * ACUITY;
|
|
mul = math.exp(math.ln(max/min) / STEPS);
|
|
}
|
|
|
|
##
|
|
# Handler. Increase FOV by one step
|
|
#
|
|
var increase = func {
|
|
calcMul();
|
|
var val = fovProp.getValue() * mul;
|
|
if(val == max) { return; }
|
|
if(val > max) { val = max }
|
|
fovProp.setDoubleValue(val);
|
|
var popup=getprop("/sim/view-name-popup");
|
|
if(popup == 1 or popup==nil) gui.popupTip(sprintf("FOV: %.1f", val));
|
|
}
|
|
|
|
##
|
|
# Handler. Decrease FOV by one step
|
|
#
|
|
var decrease = func {
|
|
calcMul();
|
|
var val = fovProp.getValue() / mul;
|
|
fovProp.setDoubleValue(val);
|
|
var popup=getprop("/sim/view-name-popup");
|
|
if(popup == 1 or popup==nil) gui.popupTip(sprintf("FOV: %.1f%s", val, val < min ? " (overzoom)" : ""));
|
|
}
|
|
|
|
##
|
|
# Handler. Reset FOV to default.
|
|
#
|
|
var resetFOV = func {
|
|
setprop("/sim/current-view/field-of-view",
|
|
getprop("/sim/current-view/config/default-field-of-view-deg"));
|
|
}
|
|
|
|
var resetViewPos = func {
|
|
var v = current.getNode("config");
|
|
setprop("/sim/current-view/x-offset-m", 0);
|
|
setprop("/sim/current-view/y-offset-m", 0);
|
|
setprop("/sim/current-view/z-offset-m", 0);
|
|
}
|
|
|
|
var resetViewDir = func {
|
|
var v = current.getNode("config");
|
|
setprop("/sim/current-view/heading-offset-deg", v.getNode("heading-offset-deg", 1).getValue() or 0);
|
|
setprop("/sim/current-view/pitch-offset-deg", v.getNode("pitch-offset-deg", 1).getValue() or 0);
|
|
setprop("/sim/current-view/roll-offset-deg", v.getNode("roll-offset-deg", 1).getValue() or 0);
|
|
}
|
|
|
|
##
|
|
# Handler. Step to the next (force=1) or next enabled view.
|
|
#
|
|
var stepView = func(step, force = 0) {
|
|
step = step > 0 ? 1 : -1;
|
|
var n = index;
|
|
for (var i = 0; i < size(views); i += 1) {
|
|
n += step;
|
|
if (n < 0)
|
|
n = size(views) - 1;
|
|
elsif (n >= size(views))
|
|
n = 0;
|
|
var e = views[n].getNode("enabled");
|
|
if (force or (e == nil or e.getBoolValue()) and
|
|
(views[n].getNode("name")!=nil))
|
|
break;
|
|
}
|
|
setprop("/sim/current-view/view-number", n);
|
|
|
|
# And pop up a nice reminder
|
|
var popup=getprop("/sim/view-name-popup");
|
|
if(popup == 1 or popup==nil) gui.popupTip(views[n].getNode("name").getValue());
|
|
}
|
|
|
|
##
|
|
# Get view index by name.
|
|
#
|
|
var indexof = func(name) {
|
|
forindex (var i; views)
|
|
if (views[i].getNode("name", 1).getValue() == name)
|
|
return i;
|
|
return nil;
|
|
}
|
|
|
|
##
|
|
# Standard view "slew" rate, in degrees/sec.
|
|
#
|
|
var VIEW_PAN_RATE = 60;
|
|
|
|
##
|
|
# Pans the view horizontally. The argument specifies a relative rate
|
|
# (or number of "steps" -- same thing) to the standard rate.
|
|
#
|
|
var panViewDir = func(step) {
|
|
if (getprop("/sim/freeze/master"))
|
|
var prop = "/sim/current-view/heading-offset-deg";
|
|
else
|
|
var prop = "/sim/current-view/goal-heading-offset-deg";
|
|
|
|
controls.slewProp(prop, step * VIEW_PAN_RATE);
|
|
}
|
|
|
|
##
|
|
# Pans the view vertically. The argument specifies a relative rate
|
|
# (or number of "steps" -- same thing) to the standard rate.
|
|
#
|
|
var panViewPitch = func(step) {
|
|
if (getprop("/sim/freeze/master"))
|
|
var prop = "/sim/current-view/pitch-offset-deg";
|
|
else
|
|
var prop = "/sim/current-view/goal-pitch-offset-deg";
|
|
|
|
controls.slewProp(prop, step * VIEW_PAN_RATE);
|
|
}
|
|
|
|
|
|
##
|
|
# Reset view to default using current view manager (see default_handler).
|
|
#
|
|
var resetView = func {
|
|
manager.reset();
|
|
}
|
|
|
|
|
|
##
|
|
# Default view handler used by view.manager.
|
|
#
|
|
var default_handler = {
|
|
reset : func {
|
|
resetViewDir();
|
|
resetFOV();
|
|
},
|
|
};
|
|
|
|
|
|
# If <from>/<path> exists and <to>/<path> doesn't, copy the former to the
|
|
# latter.
|
|
#
|
|
var set_default = func(from, path, to) {
|
|
from_ = from.getNode(path);
|
|
if (from_ != nil) {
|
|
if (to.getNode(path) == nil) {
|
|
to.setValue(path, from_.getValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
##
|
|
# View manager. Administrates optional Nasal view handlers.
|
|
# Usage: view.manager.register(<view-id>, <view-handler>);
|
|
#
|
|
# view-id: the view's name (e.g. "Chase View") or index number
|
|
# view-handler: a hash with any combination of the functions listed in the
|
|
# following example, or none at all. Only define the interface
|
|
# functions that you really need! The hash may contain local
|
|
# variables and other, non-interface functions.
|
|
#
|
|
# Example:
|
|
#
|
|
# var some_view_handler = {
|
|
# init : func {}, # called only once at startup
|
|
# start : func {}, # called when view is switched to our view
|
|
# stop : func {}, # called when view is switched away from our view
|
|
# reset : func {}, # called with view.resetView()
|
|
# update : func { 0 }, # called iteratively if defined. Must return
|
|
# }; # interval in seconds until next invocation
|
|
# # Don't define it if you don't need it!
|
|
#
|
|
# view.manager.register("Some View", some_view_handler);
|
|
#
|
|
#
|
|
var manager = {
|
|
current : { node: nil, handler: default_handler },
|
|
init : func {
|
|
me.current = nil;
|
|
me.views = {};
|
|
me.loopid = 0;
|
|
var viewnodes = props.globals.getNode("sim").getChildren("view");
|
|
var helicopter_view = viewnodes[1];
|
|
forindex (var i; viewnodes) {
|
|
# Install this view, marking as multiplayer if appropriate.
|
|
var multiplayer = 1;
|
|
if (i==4 or i==6) {
|
|
# Fly-by and Tower Look From views do not currently
|
|
# support multiplayer aircraft.
|
|
multiplayer = 0;
|
|
}
|
|
var viewnode = viewnodes[i];
|
|
me.views[i] = {
|
|
node: viewnode,
|
|
handler: default_handler,
|
|
multiplayer: multiplayer
|
|
};
|
|
# If this view is similar to helicopter view, copy across
|
|
# Helicopter View target offsets if not specified. E.g.
|
|
# this allows Tower View AGL to work on aircraft that don't
|
|
# know about it but need non-zero target-*-offset-m values
|
|
# to centre the view on the middle of the aircraft.
|
|
if (i==2 or i==3 or i==5 or i==7) {
|
|
set_default(helicopter_view, "config/target-x-offset-m", viewnode);
|
|
set_default(helicopter_view, "config/target-y-offset-m", viewnode);
|
|
set_default(helicopter_view, "config/target-z-offset-m", viewnode);
|
|
}
|
|
}
|
|
setlistener("/sim/current-view/view-number", func(n) {
|
|
manager.set_view(n.getValue());
|
|
}, 1);
|
|
},
|
|
register : func(which, handler = nil) {
|
|
var n = num(which);
|
|
if (n == nil) n = indexof(which);
|
|
if (n == nil) {
|
|
printf('Unable to register view handler: which=%s', which);
|
|
return;
|
|
}
|
|
if (handler == nil)
|
|
handler = default_handler;
|
|
me.views[n]["handler"] = handler;
|
|
var viewnodes = props.globals.getNode("sim").getChildren("view");
|
|
me.views[n]["node"] = viewnodes[n];
|
|
if (hasmember(handler, "init"))
|
|
handler.init(me.views[n].node);
|
|
me.set_view();
|
|
},
|
|
set_view : func(which = nil) {
|
|
if (which == nil)
|
|
which = index;
|
|
elsif (num(which) == nil)
|
|
which = indexof(which);
|
|
|
|
me.loopid += 1;
|
|
if (me.current != nil) {
|
|
if (me.current.multiplayer) {
|
|
model_view_handler.stop();
|
|
}
|
|
if (hasmember(me.current.handler, "stop"))
|
|
me.current.handler.stop();
|
|
}
|
|
|
|
me.current = me.views[which];
|
|
|
|
if (hasmember(me.current.handler, "start"))
|
|
me.current.handler.start();
|
|
if (hasmember(me.current.handler, "update"))
|
|
me._loop_(me.loopid += 1);
|
|
if (me.current != nil and me.current.multiplayer) {
|
|
model_view_handler.start();
|
|
}
|
|
screenWidthCompens.update();
|
|
},
|
|
reset : func {
|
|
if (hasmember(me.current.handler, "reset"))
|
|
me.current.handler.reset();
|
|
else
|
|
default_handler.reset();
|
|
},
|
|
_loop_ : func(id) {
|
|
id == me.loopid or return;
|
|
settimer(func { me._loop_(id) }, me.current.handler.update() or 0);
|
|
},
|
|
multiplayer_callback: func(data) {
|
|
# We are called by model_view_handler when the user wants to
|
|
# look at a different aircraft.
|
|
#
|
|
# We can be called very early due to model_view_handler_class's use
|
|
# of a listener, so we need to protect against me.current.node not
|
|
# being set up yet.
|
|
#
|
|
if (me.current.node != nil) {
|
|
me.current.node.getNode("config").setValues({
|
|
"root": data.root,});
|
|
me.current.node.getNode("config/root").setValue(data.root)
|
|
}
|
|
},
|
|
};
|
|
|
|
|
|
var fly_by_view_handler = {
|
|
init : func {
|
|
me.latN = props.globals.getNode("/sim/viewer/latitude-deg", 1);
|
|
me.lonN = props.globals.getNode("/sim/viewer/longitude-deg", 1);
|
|
me.altN = props.globals.getNode("/sim/viewer/altitude-ft", 1);
|
|
me.vnN = props.globals.getNode("/velocities/speed-north-fps", 1);
|
|
me.veN = props.globals.getNode("/velocities/speed-east-fps", 1);
|
|
me.hdgN = props.globals.getNode("/orientation/heading-deg", 1);
|
|
|
|
setlistener("/sim/signals/reinit", func(n) { n.getValue() or me.reset() });
|
|
setlistener("/sim/crashed", func(n) { n.getValue() and me.reset() });
|
|
setlistener("/sim/freeze/replay-state", func {
|
|
settimer(func { me.reset() }, 1); # time for replay to catch up
|
|
});
|
|
me.reset();
|
|
},
|
|
start : func {
|
|
me.reset();
|
|
},
|
|
reset: func {
|
|
me.chase = -getprop("/sim/chase-distance-m");
|
|
# me.course = me.hdgN.getValue();
|
|
var vn = me.vnN.getValue();
|
|
var ve = me.veN.getValue();
|
|
me.course = (0.5*math.pi - math.atan2(vn, ve))*R2D;
|
|
|
|
me.last = geo.aircraft_position();
|
|
me.setpos(1);
|
|
# me.dist = 20;
|
|
},
|
|
setpos : func(force = 0) {
|
|
var pos = geo.aircraft_position();
|
|
var vn = me.vnN.getValue();
|
|
var ve = me.veN.getValue();
|
|
|
|
var dist = 0.0;
|
|
if ( force ) {
|
|
# predict distance based on speed
|
|
var mps = math.sqrt( vn*vn + ve*ve ) * FT2M;
|
|
dist = mps * 3.5; # 3.5 seconds worth of travel
|
|
} else {
|
|
# use actual distance
|
|
dist = me.last.distance_to(pos);
|
|
# reset when too far (i.e. position changed due to skipping time in replay mode)
|
|
if (dist>5000) return me.reset();
|
|
}
|
|
|
|
# check if the aircraft has moved enough
|
|
if (dist < 1.7 * me.chase and !force)
|
|
return 1.13;
|
|
|
|
# "predict" and remember next aircraft position
|
|
# var course = me.hdgN.getValue();
|
|
var course = (0.5*math.pi - math.atan2(vn, ve))*R2D;
|
|
var delta_alt = (pos.alt() - me.last.alt()) * 0.5;
|
|
pos.apply_course_distance(course, dist * 0.8);
|
|
pos.set_alt(pos.alt() + delta_alt);
|
|
me.last.set(pos);
|
|
|
|
# apply random deviation
|
|
var radius = me.chase * (0.5 * rand() + 0.7);
|
|
var agl = getprop("/position/altitude-agl-ft") * FT2M;
|
|
if (agl > me.chase)
|
|
var angle = rand() * 2 * math.pi;
|
|
else
|
|
var angle = ((2 * rand() - 1) * 0.15 + 0.5) * (rand() < 0.5 ? -math.pi : math.pi);
|
|
|
|
var dev_alt = math.cos(angle) * radius;
|
|
var dev_side = math.sin(angle) * radius;
|
|
pos.apply_course_distance(course + 90, dev_side);
|
|
|
|
# and make sure it's not under ground
|
|
var lat = pos.lat();
|
|
var lon = pos.lon();
|
|
var alt = pos.alt();
|
|
var elev = geo.elevation(lat, lon);
|
|
if (elev != nil) {
|
|
elev += 2; # min elevation
|
|
if (alt + dev_alt < elev and dev_alt < 0)
|
|
dev_alt = -dev_alt;
|
|
if (alt + dev_alt < elev)
|
|
alt = elev;
|
|
else
|
|
alt += dev_alt;
|
|
}
|
|
|
|
# set new view point
|
|
me.latN.setValue(lat);
|
|
me.lonN.setValue(lon);
|
|
me.altN.setValue(alt * M2FT);
|
|
return 7.3;
|
|
},
|
|
update : func {
|
|
return me.setpos();
|
|
},
|
|
};
|
|
|
|
|
|
# Helper for views that can show multiplayer aircaft as well as the user's
|
|
# aircraft. Used by <manager> above.
|
|
#
|
|
var model_view_handler_class = {
|
|
new: func() {
|
|
var m = { parents:[model_view_handler_class]};
|
|
m.current = nil;
|
|
m.legendN = props.globals.initNode("/sim/current-view/model-view", "");
|
|
m.dialog = props.Node.new({ "dialog-name": "model-view" });
|
|
m.listener = setlistener("/sim/signals/multiplayer-updated", func m._update_(), 1);
|
|
m.reset();
|
|
return m;
|
|
},
|
|
start: func() {
|
|
fgcommand("dialog-show", me.dialog);
|
|
me.next(0);
|
|
},
|
|
stop: func() {
|
|
fgcommand("dialog-close", me.dialog);
|
|
},
|
|
reset: func {
|
|
me.select(0);
|
|
},
|
|
find: func(callsign) {
|
|
forindex (var i; me.list) {
|
|
if (me.list[i].callsign == callsign)
|
|
return i;
|
|
}
|
|
return nil;
|
|
},
|
|
select: func(which, by_callsign=0) {
|
|
if (by_callsign or num(which) == nil) {
|
|
which = me.find(which) or 0; # turn callsign into index
|
|
}
|
|
me.setup(me.list[which]);
|
|
},
|
|
next: func(step) {
|
|
var i = me.find(me.current);
|
|
i = i == nil ? 0 : math.mod(i + step, size(me.list));
|
|
me.setup(me.list[i]);
|
|
},
|
|
_update_: func {
|
|
var self = {
|
|
callsign: getprop("/sim/multiplay/callsign"),
|
|
model:,
|
|
node: props.globals,
|
|
root: '/',
|
|
};
|
|
# It looks like we can get called (from me.new()) before
|
|
# multiplayer.model.list is created, so need to check whether it
|
|
# exists.
|
|
if (hasmember(multiplayer.model, 'list')) {
|
|
me.list = [self] ~ multiplayer.model.list;
|
|
}
|
|
else {
|
|
me.list = [self];
|
|
}
|
|
if (!me.find(me.current))
|
|
me.select(0);
|
|
},
|
|
setup: func(data) {
|
|
if (data.root == '/') {
|
|
var ident = '[' ~ data.callsign ~ ']';
|
|
} else {
|
|
var ident = '"' ~ data.callsign ~ '" (' ~ data.model ~ ')';
|
|
}
|
|
|
|
me.current = data.callsign;
|
|
me.legendN.setValue(data.callsign);
|
|
manager.multiplayer_callback(data);
|
|
},
|
|
};
|
|
|
|
var model_view_handler = model_view_handler_class.new();
|
|
# Note that gui/dialogs/model-view.xml expects to be able to call
|
|
# view.model_view_handler.next() etc.
|
|
|
|
var pilot_view_limiter = {
|
|
new : func {
|
|
return { parents: [pilot_view_limiter] };
|
|
},
|
|
init : func {
|
|
me.hdgN = props.globals.getNode("/sim/current-view/heading-offset-deg");
|
|
me.xoffsetN = props.globals.getNode("/sim/current-view/x-offset-m");
|
|
me.xoffset_lowpass = aircraft.lowpass.new(0.1);
|
|
me.last_offset = 0;
|
|
me.needs_start = 0;
|
|
},
|
|
start : func {
|
|
var limits = current.getNode("config/limits", 1);
|
|
me.left = {
|
|
heading_max : abs(limits.getNode("left/heading-max-deg", 1).getValue() or 1000),
|
|
threshold : abs(limits.getNode("left/x-offset-threshold-deg", 1).getValue() or 0),
|
|
xoffset_max : abs(limits.getNode("left/x-offset-max-m", 1).getValue() or 0),
|
|
};
|
|
me.right = {
|
|
heading_max : -abs(limits.getNode("right/heading-max-deg", 1).getValue() or 1000),
|
|
threshold : -abs(limits.getNode("right/x-offset-threshold-deg", 1).getValue() or 0),
|
|
xoffset_max : -abs(limits.getNode("right/x-offset-max-m", 1).getValue() or 0),
|
|
};
|
|
me.left.scale = me.left.xoffset_max / (me.left.heading_max - me.left.threshold);
|
|
me.right.scale = me.right.xoffset_max / (me.right.heading_max - me.right.threshold);
|
|
me.last_hdg = normdeg(me.hdgN.getValue());
|
|
me.enable_xoffset = me.right.xoffset_max > 0.001 or me.left.xoffset_max > 0.001;
|
|
|
|
me.needs_start = 0;
|
|
},
|
|
update : func {
|
|
if (getprop("/devices/status/keyboard/ctrl"))
|
|
return;
|
|
|
|
if( getprop("/sim/signals/reinit") )
|
|
{
|
|
me.needs_start = 1;
|
|
return;
|
|
}
|
|
else if( me.needs_start )
|
|
me.start();
|
|
|
|
var hdg = normdeg(me.hdgN.getValue());
|
|
if (abs(me.last_hdg - hdg) > 180) # avoid wrap-around skips
|
|
me.hdgN.setDoubleValue(hdg = me.last_hdg);
|
|
elsif (hdg > me.left.heading_max)
|
|
me.hdgN.setDoubleValue(hdg = me.left.heading_max);
|
|
elsif (hdg < me.right.heading_max)
|
|
me.hdgN.setDoubleValue(hdg = me.right.heading_max);
|
|
me.last_hdg = hdg;
|
|
|
|
# translate view on X axis to look far right or far left
|
|
if (me.enable_xoffset) {
|
|
var offset = 0;
|
|
if (hdg > me.left.threshold)
|
|
offset = (me.left.threshold - hdg) * me.left.scale;
|
|
elsif (hdg < me.right.threshold)
|
|
offset = (me.right.threshold - hdg) * me.right.scale;
|
|
|
|
var new_offset = me.xoffset_lowpass.filter(offset);
|
|
me.xoffsetN.setDoubleValue(me.xoffsetN.getValue() - me.last_offset + new_offset);
|
|
me.last_offset = new_offset;
|
|
}
|
|
return 0;
|
|
},
|
|
};
|
|
|
|
|
|
var panViewDir = func(step) { # FIXME overrides panViewDir function from above; needs better integration
|
|
if (getprop("/sim/freeze/master"))
|
|
var prop = "/sim/current-view/heading-offset-deg";
|
|
else
|
|
var prop = "/sim/current-view/goal-heading-offset-deg";
|
|
var viewVal = getprop(prop);
|
|
var delta = step * VIEW_PAN_RATE * getprop("/sim/time/delta-realtime-sec");
|
|
var viewValSlew = normdeg(viewVal + delta);
|
|
var headingMax = abs(current.getNode("config/limits/left/heading-max-deg", 1).getValue() or 1000);
|
|
var headingMin = -abs(current.getNode("config/limits/right/heading-max-deg", 1).getValue() or 1000);
|
|
if (viewValSlew > headingMax)
|
|
viewValSlew = headingMax;
|
|
elsif (viewValSlew < headingMin)
|
|
viewValSlew = headingMin;
|
|
setprop(prop, viewValSlew);
|
|
}
|
|
|
|
|
|
#------------------------------------------------------------------------------
|
|
#
|
|
# Saves/restores/moves the view point (position, orientation, field-of-view).
|
|
# Moves are interpolated with sinusoidal characteristic. There's only one
|
|
# instance of this class, available as "view.point".
|
|
#
|
|
# Usage:
|
|
# view.point.save(); ... save current view and return reference to
|
|
# saved values in the form of a props.Node
|
|
#
|
|
# view.point.restore(); ... restore saved view parameters
|
|
#
|
|
# view.point.move(<prop> [, <time>]);
|
|
# ... set view parameters from a props.Node with
|
|
# optional move time in seconds. <prop> may be
|
|
# nil, in which case nothing happens.
|
|
#
|
|
# A parameter set as expected by set() and returned by save() is a props.Node
|
|
# object containing any (or none) of these children:
|
|
#
|
|
# <heading-offset-deg>
|
|
# <pitch-offset-deg>
|
|
# <roll-offset-deg>
|
|
# <x-offset-m>
|
|
# <y-offset-m>
|
|
# <z-offset-m>
|
|
# <field-of-view>
|
|
# <move-time-sec>
|
|
#
|
|
# The <move-time> isn't really a property of the view, but is available
|
|
# for convenience. The time argument in the move() method overrides it.
|
|
|
|
|
|
##
|
|
# Normalize angle to -180 <= angle < 180
|
|
#
|
|
var normdeg = func(a) {
|
|
while (a >= 180)
|
|
a -= 360;
|
|
while (a < -180)
|
|
a += 360;
|
|
return a;
|
|
}
|
|
|
|
|
|
##
|
|
# Manages one translation/rotation axis. (For simplicity reasons the
|
|
# field-of-view parameter is also managed by this class.)
|
|
#
|
|
var ViewAxis = {
|
|
new : func(prop) {
|
|
var m = { parents : [ViewAxis] };
|
|
m.prop = props.globals.getNode(prop, 1);
|
|
if (m.prop.getType() == "NONE")
|
|
m.prop.setDoubleValue(0);
|
|
|
|
m.from = m.to = m.prop.getValue();
|
|
return m;
|
|
},
|
|
reset : func {
|
|
me.from = me.to = normdeg(me.prop.getValue());
|
|
},
|
|
target : func(v) {
|
|
me.to = normdeg(v);
|
|
},
|
|
move : func(blend) {
|
|
me.prop.setValue(me.from + blend * (me.to - me.from));
|
|
},
|
|
};
|
|
|
|
|
|
|
|
##
|
|
# view.point: handles smooth view movements
|
|
#
|
|
var point = {
|
|
init : func {
|
|
me.axes = {
|
|
"heading-offset-deg" : ViewAxis.new("/sim/current-view/goal-heading-offset-deg"),
|
|
"pitch-offset-deg" : ViewAxis.new("/sim/current-view/goal-pitch-offset-deg"),
|
|
"roll-offset-deg" : ViewAxis.new("/sim/current-view/goal-roll-offset-deg"),
|
|
"x-offset-m" : ViewAxis.new("/sim/current-view/x-offset-m"),
|
|
"y-offset-m" : ViewAxis.new("/sim/current-view/y-offset-m"),
|
|
"z-offset-m" : ViewAxis.new("/sim/current-view/z-offset-m"),
|
|
"field-of-view" : ViewAxis.new("/sim/current-view/field-of-view"),
|
|
};
|
|
me.storeN = props.Node.new();
|
|
me.dtN = props.globals.getNode("/sim/time/delta-realtime-sec", 1);
|
|
me.currviewN = props.globals.getNode("/sim/current-view", 1);
|
|
me.blend = 0;
|
|
me.loop_id = 0;
|
|
props.copy(props.globals.getNode("/sim/view/config"), me.storeN);
|
|
},
|
|
save : func {
|
|
me.storeN = props.Node.new();
|
|
props.copy(me.currviewN, me.storeN);
|
|
return me.storeN;
|
|
},
|
|
restore : func {
|
|
me.move(me.storeN);
|
|
},
|
|
move : func(prop, time = nil) {
|
|
prop != nil or return;
|
|
var n = prop.getNode("view-number");
|
|
if (n != nil)
|
|
setprop("/sim/current-view/view-number",n.getValue());
|
|
foreach (var a; keys(me.axes)) {
|
|
var n = prop.getNode(a);
|
|
me.axes[a].reset();
|
|
if (n != nil)
|
|
me.axes[a].target(n.getValue());
|
|
}
|
|
var m = prop.getNode("move-time-sec");
|
|
if (m != nil)
|
|
time = m.getValue();
|
|
|
|
if (time == nil)
|
|
time = 1;
|
|
|
|
me.blend = -1; # range -1 .. 1
|
|
me._loop_(me.loop_id += 1, time);
|
|
},
|
|
_loop_ : func(id, time) {
|
|
me.loop_id == id or return;
|
|
me.blend += me.dtN.getValue() / time;
|
|
if (me.blend > 1)
|
|
me.blend = 1;
|
|
|
|
var b = (math.sin(me.blend * math.pi / 2) + 1) / 2; # range 0 .. 1
|
|
foreach (var a; keys(me.axes))
|
|
me.axes[a].move(b);
|
|
|
|
if (me.blend < 1)
|
|
settimer(func { me._loop_(id, time) }, 0);
|
|
},
|
|
};
|
|
|
|
|
|
|
|
##
|
|
# view.ScreenWidthCompens: optional FOV compensation for wider screens.
|
|
# It keeps an equivalent of 55° FOV on a 4:3 zone centered on the screen
|
|
# whichever is the screen width/height ratio. Works only if width >= height.
|
|
|
|
var screenWidthCompens = {
|
|
defaultFov: nil,
|
|
oldW: nil, oldH: nil, oldOpt: nil,
|
|
assumedW: 4, assumedH: 3,
|
|
fovStore: [],
|
|
lastViewStatus: {},
|
|
statusNode: nil, # = /sim/current-view/field-of-view-compensation
|
|
getStatus: func me.statusNode.getValue(),
|
|
setStatus: func(state) me.statusNode.setValue(state),
|
|
wNode: nil, # = /sim/startup/xsize
|
|
hNode: nil, # = /sim/startup/ysize
|
|
getDimensions: func [me.wNode.getValue(),me.hNode.getValue()],
|
|
calcNewFov: func(fov=55, oldW=nil, oldH=nil, w=nil, h=nil) {
|
|
var dim = me.getDimensions();
|
|
if (w == nil) w = dim[0];
|
|
if (h == nil) h = dim[1];
|
|
if (oldW == nil) oldW = me.assumedW;
|
|
if (oldH == nil) oldH = me.assumedH;
|
|
if (w/h == oldW/oldH or h > w) return fov;
|
|
else return math.atan2(w/h, oldW/oldH / math.tan(fov * D2R)) * R2D;
|
|
},
|
|
init: func() {
|
|
me.defaultFov = getprop("/sim/current-view/config/default-field-of-view-deg");
|
|
me.statusNode = props.globals.getNode("/sim/current-view/field-of-view-compensation", 1);
|
|
me.wNode = props.globals.getNode("/sim/startup/xsize", 1);
|
|
me.hNode = props.globals.getNode("/sim/startup/ysize", 1);
|
|
(me.oldW, me.oldH) = me.getDimensions();
|
|
|
|
setsize(me.fovStore, size(views));
|
|
forindex (var i; views) {
|
|
me.fovStore[i] = views[i].getNode("config/default-field-of-view-deg", 1).getValue() or 55;
|
|
me.lastViewStatus[i] = { w:me.assumedW, h:me.assumedH };
|
|
}
|
|
me.update(opt:nil, force:1);
|
|
},
|
|
toggle: func() me.update(!me.getStatus(), 1),
|
|
update: func(opt=nil, force=0) {
|
|
if (opt == nil)
|
|
opt = me.getStatus();
|
|
else me.setStatus(opt);
|
|
var (w, h) = me.getDimensions();
|
|
# Update config/default-field-of-view-deg nodes if state changed:
|
|
if (force or me.oldOpt != opt or me.oldW/me.oldH != w/h) {
|
|
me.oldW = w;
|
|
me.oldH = h;
|
|
me.oldOpt = opt;
|
|
if (!opt) {
|
|
setprop("/sim/current-view/config/default-field-of-view-deg", me.defaultFov);
|
|
forindex (var i; views)
|
|
views[i].setValue("config/default-field-of-view-deg", me.fovStore[i]);
|
|
} else {
|
|
setprop("/sim/current-view/config/default-field-of-view-deg",
|
|
me.calcNewFov(fov:me.defaultFov, w:w, h:h));
|
|
forindex (var i; views)
|
|
views[i].setValue("config/default-field-of-view-deg",
|
|
me.calcNewFov(fov:me.fovStore[i], w:w, h:h));
|
|
}
|
|
}
|
|
# Update this view if necessary:
|
|
if (!opt) (w,h) = (me.assumedW,me.assumedH); # back to default FOV
|
|
var thisview = me.lastViewStatus[index];
|
|
if (thisview.w/thisview.h != w/h) {
|
|
fovProp.setValue(me.calcNewFov(fovProp.getValue(), thisview.w, thisview.h, w, h))
|
|
and
|
|
((thisview.opt,thisview.w,thisview.h) = (opt,w,h));
|
|
}
|
|
},
|
|
};
|
|
|
|
|
|
_setlistener("/sim/signals/nasal-dir-initialized", func {
|
|
views = props.globals.getNode("/sim").getChildren("view");
|
|
fovProp = props.globals.getNode("/sim/current-view/field-of-view");
|
|
point.init();
|
|
|
|
setlistener("/sim/current-view/view-number", func(n) {
|
|
current = views[index = n.getValue()];
|
|
}, 1);
|
|
|
|
props.globals.initNode("/position/altitude-agl-ft"); # needed by Fly-By View
|
|
screenWidthCompens.init();
|
|
manager.init();
|
|
manager.register("Fly-By View", fly_by_view_handler);
|
|
});
|
|
_setlistener("/sim/signals/reinit", func {
|
|
screenWidthCompens.update(opt:nil,force:1);
|
|
});
|
|
_setlistener("/sim/startup/xsize", func {
|
|
screenWidthCompens.update();
|
|
});
|
|
_setlistener("/sim/startup/ysize", func {
|
|
screenWidthCompens.update();
|
|
});
|
|
|
|
|
|
var fdm_init_listener = _setlistener("/sim/signals/fdm-initialized", func {
|
|
removelistener(fdm_init_listener); # uninstall, so we're only called once
|
|
var zoffset = nil;
|
|
foreach (var v; views) {
|
|
var index = v.getIndex();
|
|
if (index > 7 and index < 100) {
|
|
globals["view"] = nil;
|
|
die("\n***\n*\n* Illegal use of reserved view index "
|
|
~ index ~ ". Use indices >= 100!\n*\n***");
|
|
} elsif (index >= 100 and index < 200) {
|
|
if (v.getNode("name") == nil)
|
|
continue;
|
|
var e = v.getNode("enabled");
|
|
if (e != nil) {
|
|
aircraft.data.add(e);
|
|
e.setAttribute("userarchive", 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
forindex (var i; views) {
|
|
var limits = views[i].getNode("config/limits/enabled");
|
|
if (limits != nil) {
|
|
func (i) {
|
|
var limiter = pilot_view_limiter.new();
|
|
setlistener(limits, func(n) {
|
|
manager.register(i, n.getBoolValue() ? limiter : nil);
|
|
manager.set_view();
|
|
}, 1);
|
|
}(i);
|
|
}
|
|
}
|
|
});
|