## ## 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) STEPS = 40; ACUITY = 1/60; # Maximum angle subtended by one pixel (== 1 arc minute) max = min = mul = 0; 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 # increase = func { calcMul(); 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 # decrease = func { calcMul(); val = fovProp.getValue() / mul; fovProp.setDoubleValue(val); gui.popupTip(sprintf("FOV: %.1f%s", val, val < min ? " (overzoom)" : "")); } ## # Handler. Reset FOV to default. # resetFOV = func { setprop("/sim/current-view/field-of-view", getprop("/sim/current-view/config/default-field-of-view-deg")); } ## # Handler. Reset view to default. # resetView = func { if (getprop("/sim/current-view/view-number") == 6) return flyby.setpos(1); 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")); resetFOV(); } ## # Handler. Step to the next view. # var stepView = func(n) { var i = getprop("/sim/current-view/view-number") + n; if (i < 0) i = size(views) - 1; elsif (i >= size(views)) i = 0; setprop("/sim/current-view/view-number", i); # And pop up a nice reminder gui.popupTip(views[i].getNode("name").getValue()); } ## # Standard view "slew" rate, in degrees/sec. # VIEW_PAN_RATE = 60; ## # Pans the view horizontally. The argument specifies a relative rate # (or number of "steps" -- same thing) to the standard rate. # panViewDir = func(step) { if (getprop("/sim/freeze/master")) prop = "/sim/current-view/heading-offset-deg"; else 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. # panViewPitch = func(step) { if (getprop("/sim/freeze/master")) prop = "/sim/current-view/pitch-offset-deg"; else prop = "/sim/current-view/goal-pitch-offset-deg"; controls.slewProp(prop, step * VIEW_PAN_RATE); } ## # Singleton class that manages "Fly-By View". It's started with flyby.init() # and then works autonomously. # var flyby = { 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); me.loopid = 0; me.number = nil; me.currview = nil; forindex (var i; views) if ((var v = views[i].getNode("name")) != nil and v.getValue() == "Fly-By View") me.number = i; if (me.number == nil) die("can't find 'Fly-By View'"); setlistener("/sim/signals/reinit", func { cmdarg().getValue() or me.reset() }); setlistener("/sim/crashed", func { cmdarg().getValue() and me.reset() }); setlistener("/sim/freeze/replay-state", func { settimer(func { me.reset() }, 1); # time for replay to catch up }); setlistener("/sim/current-view/view-number", func { me.currview = cmdarg().getValue(); me.reset(); }, 1); }, reset: func { me.loopid += 1; me.currview == me.number or return; me.chase = -getprop("/sim/chase-distance-m"); me.course = me.hdgN.getValue(); me.coord = geo.aircraft_position(); me.setpos(1); me.dist = 20; me._loop_(me.loopid); }, setpos : func(force = 0) { var pos = geo.aircraft_position(); var dist = me.coord.distance_to(pos); if (dist < 1.7 * me.chase and !force) return 1.13; var side = (rand() - 0.5 < 0) ? -90 : 90; var course = me.hdgN.getValue(); pos.apply_course_distance(course, dist * 0.8); pos.apply_course_distance(course + side, me.chase); var lat = pos.lat(); var lon = pos.lon(); var elev = geo.elevation(lat, lon); var alt = pos.alt(); alt += (alt - me.coord.alt()) * 0.5; if (elev != nil and alt < elev + 5) alt = elev + 5; me.latN.setValue(lat); me.lonN.setValue(lon); me.altN.setValue(alt * geo.M2FT); me.coord.set_latlon(lat, lon, alt); return 6.3; }, _loop_ : func(id) { id == me.loopid or return; settimer(func { me._loop_(id) }, me.setpos()); }, }; #-- view manager -------------------------------------------------------------- # # 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( [,