## ## view.nas ## ## Nasal code for implementing view-specific functionality. # 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); gui.popupTip(sprintf("FOV: %.1f", val)); } ## # Handler. Decrease FOV by one step # var decrease = func { calcMul(); var val = fovProp.getValue() / mul; fovProp.setDoubleValue(val); 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 = views[getprop("/sim/current-view/view-number")].getNode("config"); setprop("/sim/current-view/x-offset-m", v.getNode("x-offset-m", 1).getValue() or 0); setprop("/sim/current-view/y-offset-m", v.getNode("y-offset-m", 1).getValue() or 0); setprop("/sim/current-view/z-offset-m", v.getNode("z-offset-m", 1).getValue() or 0); } var resetViewDir = func { setprop("/sim/current-view/goal-heading-offset-deg", getprop("/sim/current-view/config/heading-offset-deg")); setprop("/sim/current-view/goal-pitch-offset-deg", getprop("/sim/current-view/config/pitch-offset-deg")); setprop("/sim/current-view/goal-roll-offset-deg", getprop("/sim/current-view/config/roll-offset-deg")); } ## # Handler. Step to the next (force=1) or next enabled view. # var stepView = func(step, force = 0) { step = step > 0 ? 1 : -1; var n = getprop("/sim/current-view/view-number"); for (var i = 0; i < size(views); i += 1) { n += step; if (n < 0) n = size(views) - 1; elsif (n >= size(views)) n = 0; if (force or (var e = views[n].getNode("enabled")) == nil or e.getBoolValue()) break; } setprop("/sim/current-view/view-number", n); # And pop up a nice reminder 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(); }, }; ## # View manager. Administrates optional Nasal view handlers. # Usage: view.manager.register(, ); # # view-id: the view's name, e.g. "Chase View" # 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.views = {}; me.loopid = 0; var viewnodes = props.globals.getNode("sim").getChildren("view"); forindex (var i; viewnodes) me.views[i] = { node: viewnodes[i], handler: default_handler }; setlistener("/sim/current-view/view-number", func(n) { manager.set_view(n.getValue()); }, 1); }, register : func(which, handler = nil) { if (num(which) == nil) which = view.indexof(which); if (handler == nil) handler = default_handler; me.views[which]["handler"] = handler; if (contains(handler, "init")) handler.init(me.views[which].node); me.set_view(); }, set_view : func(which = nil) { if (which == nil) which = getprop("/sim/current-view/view-number"); elsif (num(which) == nil) which = view.indexof(which); me.loopid += 1; if (contains(me.current.handler, "stop")) me.current.handler.stop(); me.current = me.views[which]; if (contains(me.current.handler, "start")) me.current.handler.start(); if (contains(me.current.handler, "update")) me._loop_(me.loopid += 1); }, reset : func { if (contains(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()); }, }; ## # View handler for fly-by view. # 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.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(); me.last = geo.aircraft_position(); me.setpos(1); me.dist = 20; }, setpos : func(force = 0) { var pos = geo.aircraft_position(); # check if the aircraft has moved enough var dist = me.last.distance_to(pos); if (dist < 1.7 * me.chase and !force) return 1.13; # "predict" and remember next aircraft position var course = me.hdgN.getValue(); 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") * geo.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 * geo.M2FT); return 6.3; }, update : func { return me.setpos(); }, }; #------------------------------------------------------------------------------ # # 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( [,