Merge pull request #15 from ctrlaltdavid/app/radar

Add Radar app
This commit is contained in:
kasenvr 2020-05-14 23:33:43 -04:00 committed by GitHub
commit aa4bcd1097
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 2816 additions and 0 deletions

View file

@ -52,6 +52,15 @@ var metadata = { "applications": [
"jsfile": "vr-grabscale/VRBuildGrabScale.js",
"icon": "vr-grabscale/logo.png",
"caption": "VR SCALE"
},
{
"isActive": true,
"directory": "radar",
"name": "Radar",
"description": "Show where people are and teleport in the domain.",
"jsfile": "radar/radar.js",
"icon": "radar/assets/radar-i.svg",
"caption": "RADAR"
}
]
};

View file

@ -0,0 +1,3 @@
Distributed under the Apache License, Version 2.0.
See: http://www.apache.org/licenses/LICENSE-2.0.html

View file

@ -0,0 +1,10 @@
# radar.js
Show where people are and teleport in the domain.
Information: http://ctrlaltstudio.com/vircadia/radar
The master source for this app is in the following repository:
https://github.com/ctrlaltdavid/cas-vircadia-stuff/tree/master/apps/radar
[LICENSE](LICENSE)

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" width="50" height="50" viewBox="0 0 50.00 50.00" enable-background="new 0 0 50.00 50.00" xml:space="preserve">
<g>
<path fill="none" stroke-width="3.44274" stroke-linejoin="miter" stroke="#000000" stroke-opacity="1" d="M 44.3755,25C 44.3755,35.7286 35.7286,44.3755 25,44.3755C 14.3515,44.3755 5.6245,35.7286 5.6245,25C 5.6245,14.3515 14.3515,5.6245 25,5.6245C 35.7286,5.6245 44.3755,14.3515 44.3755,25 Z "/>
<line fill="none" stroke-width="3.44274" stroke-linecap="round" stroke-linejoin="miter" stroke="#000000" stroke-opacity="1" x1="36.209" y1="14.9119" x2="25.4003" y2="25.3203"/>
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linecap="round" stroke-linejoin="miter" d="M 28.5,14C 28.5,15.371 27.3889,16.5 26.0397,16.5C 24.6111,16.5 23.5,15.371 23.5,14C 23.5,12.629 24.6111,11.5 26.0397,11.5C 27.3889,11.5 28.5,12.629 28.5,14 Z "/>
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linecap="round" stroke-linejoin="miter" d="M 34.5,33C 34.5,34.371 33.3889,35.5 31.9603,35.5C 30.6111,35.5 29.5,34.371 29.5,33C 29.5,31.629 30.6111,30.5 31.9603,30.5C 33.3889,30.5 34.5,31.629 34.5,33 Z "/>
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linecap="round" stroke-linejoin="miter" d="M 21.4952,33C 21.4952,34.371 20.3662,35.5 18.9952,35.5C 17.6242,35.5 16.4952,34.371 16.4952,33C 16.4952,31.629 17.6242,30.5 18.9952,30.5C 20.3662,30.5 21.4952,31.629 21.4952,33 Z "/>
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" width="50" height="50" viewBox="0 0 50.00 50.00" enable-background="new 0 0 50.00 50.00" xml:space="preserve">
<g>
<path fill="none" stroke-width="3.44274" stroke-linejoin="miter" stroke="#FFFFFF" stroke-opacity="1" d="M 44.3755,25C 44.3755,35.7286 35.7286,44.3755 25,44.3755C 14.3515,44.3755 5.6245,35.7286 5.6245,25C 5.6245,14.3515 14.3515,5.6245 25,5.6245C 35.7286,5.6245 44.3755,14.3515 44.3755,25 Z "/>
<line fill="none" stroke-width="3.44274" stroke-linecap="round" stroke-linejoin="miter" stroke="#FFFFFF" stroke-opacity="1" x1="36.209" y1="14.9119" x2="25.4003" y2="25.3203"/>
<path fill="#FFFFFF" fill-opacity="1" stroke-width="0.2" stroke-linecap="round" stroke-linejoin="miter" d="M 28.5,14C 28.5,15.371 27.3889,16.5 26.0397,16.5C 24.6111,16.5 23.5,15.371 23.5,14C 23.5,12.629 24.6111,11.5 26.0397,11.5C 27.3889,11.5 28.5,12.629 28.5,14 Z "/>
<path fill="#FFFFFF" fill-opacity="1" stroke-width="0.2" stroke-linecap="round" stroke-linejoin="miter" d="M 34.5,33C 34.5,34.371 33.3889,35.5 31.9603,35.5C 30.6111,35.5 29.5,34.371 29.5,33C 29.5,31.629 30.6111,30.5 31.9603,30.5C 33.3889,30.5 34.5,31.629 34.5,33 Z "/>
<path fill="#FFFFFF" fill-opacity="1" stroke-width="0.2" stroke-linecap="round" stroke-linejoin="miter" d="M 21.4952,33C 21.4952,34.371 20.3662,35.5 18.9952,35.5C 17.6242,35.5 16.4952,34.371 16.4952,33C 16.4952,31.629 17.6242,30.5 18.9952,30.5C 20.3662,30.5 21.4952,31.629 21.4952,33 Z "/>
</g>
</svg>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,474 @@
/*!
radar.css
Created by David Rowe on 19 Nov 2017.
Copyright 2017-2020 David Rowe.
Information: http://ctrlaltstudio.com/vircadia/radar
Distributed under the Apache License, Version 2.0.
See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
Attributions:
- Raleway font: https://github.com/impallari/Raleway. Copyright (c) 2010, Matt McInerney (matt@pixelspread.com), Copyright (c)
2011, Pablo Impallari (www.impallari.com|impallari@gmail.com), Copyright (c) 2011, Rodrigo Fuenzalida
(www.rfuenzalida.com|hello@rfuenzalida.com), with Reserved Font Name < Raleway >. Licensed under the SIL Open Font License,
Version 1.1, available at http://scripts.sil.org/OFL.
- Fira Sans font: https://github.com/mozilla/Fira. Digitized data copyright (c) 2012-2015, The Mozilla
Foundation and Telefonica S.A. with Reserved Font Name < Fira >. Licensed under the SIL Open Font License, Version 1.1,
available at http://scripts.sil.org/OFL.
*/
@font-face {
font-family: Raleway-Regular;
src: url(./fonts/Raleway-Regular.ttf)
}
@font-face {
font-family: Raleway-SemiBold;
src: url(./fonts/Raleway-SemiBold.ttf)
}
@font-face {
font-family: FiraSans-Regular;
src: url(./fonts/FiraSans-Regular.ttf)
}
@font-face {
font-family: FiraSans-SemiBold;
src: url(./fonts/FiraSans-SemiBold.ttf)
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
user-select: none;
}
html {
width: 100%;
height: 100%;
color: #e3e3e3;
background-color: #404040;
}
body {
width: 100%;
height: 100%;
overflow: hidden;
}
h1 {
position: relative;
top: 9px;
font-family: "Raleway-Regular";
font-weight: normal;
font-size: 18px;
color: #ffffff;
text-shadow: 1px 1px #252525;
}
h1 span {
font-size: 14px;
}
hr {
width: 100%;
height: 2px;
border: none;
border-top: 1px solid #252525;
border-bottom: 1px solid #575757;
}
header {
position: relative;
top: 0;
left: 0;
width: 100%;
height: 40px;
padding: 0 20px 0 20px;
}
header div {
position: absolute;
right: 20px;
top: 8px;
font-size: 17px;
font-weight: bold;
}
header div span {
padding-left: 10px;
}
header div span:hover {
color: #00b4ef;
}
header div #help-button {
font-size: 21px;
position: relative;
top: 1px;
}
header hr {
position: absolute;
left: 0;
bottom: 0;
}
section {
margin: 40px 20px 40px 20px;
}
#radar-circle {
position: relative;
width: 420px;
height: 420px;
margin: 0 auto 0 auto;
border-radius: 210px;
background-color: rgb(48, 48, 48);
}
#radar-circle-display {
width: 100%;
height: 100%;
}
#radar-circle-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 214px;
border: 1px solid #575757;
background: radial-gradient(rgba(48, 48, 48, 0.0) 0%, rgba(48, 48, 48, 0.0) 60%, rgba(87, 87, 87, 1.0) 80%);
pointer-events: none;
clip-path: circle(210px at center);
}
#radar-circle-overlay img {
width: 100%;
height: 100%;
pointer-events: none;
}
.dot {
position: absolute;
width: 8px;
height: 8px;
margin-left: -4px;
margin-top: -4px;
border-radius: 4px;
padding: 0;
z-index: 20;
text-align: center;
}
.dot:hover {
width: 12px;
height: 12px;
margin-top: -6px;
margin-left: -6px;
border-radius: 6px;
padding: 2px;
z-index: 30;
}
.dot.highlighted {
width: 12px;
height: 12px;
margin-top: -6px;
margin-left: -6px;
border-radius: 6px;
border: 2px solid #00ff00;
padding: 2px;
}
.dot.labeled {
z-index: 30 !important;
}
.my-dot {
background-color: #00ff00 !important;
z-index: 10;
}
#avatar-label {
position: absolute;
height: 14px;
padding: 0 3px 0 3px;
border-radius: 3px;
margin-top: -16px;
color: #ffffff;
background-color: rgba(48, 48, 48, 0.35);
font-family: "Raleway-Regular";
font-weight: normal;
font-size: 14px;
line-height: 13px;
text-align: center;
white-space: nowrap;
z-index: 40;
display: none;
}
#teleport-circle {
position: absolute;
width: 82px;
height: 82px;
border-radius: 41px;
background-color: rgba(0, 255, 0, 0.2);
display: none;
}
#avatar-count {
position: absolute;
right: 0;
top: 0;
font-family: "Raleway-Regular";
font-weight: normal;
font-size: 15px;
}
#avatar-count span {
display: inline-block;
min-width: 15px;
text-align: right;
font-family: "FiraSans-Regular";
font-size: 15px;
}
#radar-scale {
width: 420px;
margin: 10px auto 10px auto;
}
#radar-scale td {
width: 33%;
text-align: center;
font-family: "FiraSans-Regular";
font-weight: normal;
font-size: 15px;
}
#radar-scale td:first-child {
text-align: left;
}
#radar-scale td:last-child {
text-align: right;
}
footer {
position: relative;
width: 100%;
padding: 40px 20px 0 20px;
}
footer hr {
position: absolute;
left: 0;
top: 0;
}
#scale {
width: 100%;
}
#scale thead td {
text-align: left;
padding-bottom: 5px;
font-family: "Raleway-SemiBold";
font-weight: normal;
font-size: 14px;
}
#scale tbody td {
display: block;
float: left;
width: 7.6%;
text-align: center;
font-family: "FiraSans-Regular";
font-weight: normal;
font-size: 15px;
}
#scale input[type=radio]:checked + label::before {
line-height: 11px;
}
.units {
font-family: "Raleway-Regular";
font-weight: normal;
font-size: 13px;
}
input[type=radio] {
display: none;
}
input[type=radio] + label::before {
content: "";
width: 12px;
height: 12px;
border-radius: 6px;
padding: 0;
background-image: linear-gradient(#7D7D7D, #6B6A6B);
display: inline-block;
margin: 0 10px 0 10px;
line-height: 11px;
position: relative;
top: 1px;
}
input[type=radio]:checked + label::before {
font-size: 18px;
content: "\25cf";
color: #00b4ef;
line-height: 9px;
position: relative;
top: 0;
}
input[type=radio] + label:hover::before {
background-image: linear-gradient(#ffffff, #afafaf);
}
form.dialog {
position: absolute;
top: 40px;
bottom: 0;
left: 0;
right: 0;
margin: 20px;
padding: 20px;
z-index: 100;
border-radius: 15px;
border: 1px solid #676767;
box-shadow: 1px 1px #252525;
background-color: #505050;
font-family: "Raleway-Regular";
font-weight: normal;
font-size: 14px;
color: #f3f3f3;
visibility: hidden;
}
form.dialog.visible {
visibility: visible;
}
h2 {
font-family: "Raleway-Regular";
font-weight: normal;
font-size: 16px;
color: #ffffff;
text-shadow: 1px 1px #252525;
}
#version {
position: absolute;
top: 20px;
right: 20px;
font-family: "FiraSans-Regular";
font-weight: normal;
font-size: 15px;
font-style: italic;
color: #e3e3e3;
}
p {
margin-top: 15px;
}
ul {
margin-left: 20px;
}
li {
margin-top: 3px;
}
li li {
margin-top: 1px;
}
a {
color: #00c0ff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.radio {
margin-left: 0;
}
.radio li {
list-style-type: none;
margin-top: 5px;
}
.ok {
position: absolute;
bottom: 20px;
right: 20px;
}
.ok input {
width: 100px;
height: 30px;
border: none;
border-radius: 7.5px;
font-size: 13px;
font-weight: bold;
color: #f8f8f8;
}
.ok input {
background: linear-gradient(#00b4ef, #1080b8);
}
.ok input:focus {
outline: none;
}
.ok input:hover {
background: linear-gradient(#00b4ef, #00b4ef);
}
#help-content {
margin-top: 15px;
padding-right: 4px;
height: 502px;
overflow-y: auto;
}
#help-content p:first-child {
margin-top: 0;
}
::-webkit-scrollbar {
width: 18px;
height: 18px;
}
::-webkit-scrollbar-button {
display: none;
}
::-webkit-scrollbar-track {
border-radius: 9px;
background: #484848;
}
::-webkit-scrollbar-thumb {
border-radius: 9px;
background: #707070;
}

View file

@ -0,0 +1,179 @@
<!--
radar.html
Created by David Rowe on 19 Nov 2017.
Copyright 2017-2020 David Rowe.
Information: http://ctrlaltstudio.com/vircadia/radar
Distributed under the Apache License, Version 2.0.
See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Radar</title>
<link rel="stylesheet" type="text/css" href="radar.css" />
</head>
<body>
<header>
<h1>Radar</h1>
<div>
<span id="settings-button">&#x26ed;</span>
<span id="help-button">?</span>
</div>
<hr />
</header>
<section>
<div id="radar-circle">
<div id="radar-circle-overlay">
<img src="../assets/radar-circle-overlay.png" />
<div id="teleport-circle"></div>
</div>
<div id="radar-circle-display"></div>
<div id="avatar-label"></div>
<div id="avatar-count">Avatars: <span>0</span></div>
</div>
<table id="radar-scale">
<tr><td id="radar-scale-left"></td><td>0m</td><td id="radar-scale-right"></td></tr>
</table>
</section>
<footer>
<hr />
<form id="controls">
<table id="scale">
<thead>
<tr><td colspan="13">Scale <span class="units">m</span></td></tr>
</thead>
<tbody>
<tr>
<td><input type="radio" name="scale" value="10" id="scale10" /><label for="scale10">10</label></td>
<td><input type="radio" name="scale" value="20" id="scale20" /><label for="scale20">20</label></td>
<td><input type="radio" name="scale" value="50" id="scale50" /><label for="scale50">50</label></td>
<td><input type="radio" name="scale" value="100" id="scale100" /><label for="scale100">100</label></td>
<td><input type="radio" name="scale" value="200" id="scale200" /><label for="scale200">200</label></td>
<td><input type="radio" name="scale" value="500" id="scale500" /><label for="scale500">500</label></td>
<td><input type="radio" name="scale" value="1000" id="scale1000" /><label for="scale1000">1k</label></td>
<td><input type="radio" name="scale" value="2000" id="scale2000" /><label for="scale2000">2k</label></td>
<td><input type="radio" name="scale" value="5000" id="scale5000" /><label for="scale5000">5k</label></td>
<td><input type="radio" name="scale" value="10000" id="scale10000" /><label for="scale10000">10k</label></td>
<td><input type="radio" name="scale" value="20000" id="scale20000" /><label for="scale20000">20k</label></td>
<td><input type="radio" name="scale" value="50000" id="scale50000" /><label for="scale50000">50k</label></td>
<td><input type="radio" name="scale" value="100000" id="scale100000" /><label for="scale100000">100k</label></td>
</tr>
</tbody>
</table>
</form>
</footer>
<form id="settings" class="dialog">
<h2>Settings</h2>
<p>Show own avatar:</p>
<ul class="radio">
<li><input type="radio" name="show-own" value="2" id="show-own-always" /><label for="show-own-always">Always.</label></li>
<li><input type="radio" name="show-own" value="1" id="show-own-third" /><label for="show-own-third">When not in first person view.</label></li>
<li><input type="radio" name="show-own" value="0" id="show-own-never" /><label for="show-own-never">Never.</label></li>
</ul>
<p>Refresh rate:</p>
<ul class="radio">
<li><input type="radio" name="refresh-rate" value="4" id="refresh-rate-fastest" /><label for="refresh-rate-fastest">Fast.</label></li>
<li><input type="radio" name="refresh-rate" value="3" id="refresh-rate-faster" /><label for="refresh-rate-faster">Medium-fast.</label></li>
<li><input type="radio" name="refresh-rate" value="2" id="refresh-rate-medium" /><label for="refresh-rate-medium">Medium.</label></li>
<li><input type="radio" name="refresh-rate" value="1" id="refresh-rate-slower" /><label for="refresh-rate-slower">Medium-slow.</label></li>
<li><input type="radio" name="refresh-rate" value="0" id="refresh-rate-slowest" /><label for="refresh-rate-slowest">Slow.</label></li>
</ul>
<div class="ok">
<input type="button" value="OK" />
</div>
</form>
<form id="help" class="dialog">
<h2>About</h2>
<div id="version">2.3.0</div>
<div id="help-content">
<p>Shows where people are in the domain.</p>
<ul>
<li>Works in both HMD and desktop.</li>
<li>The center is your camera location.</li>
<li>
The orientation is per:
<ul>
<li>The tablet in HMD mode.</li>
<li>Your camera in desktop mode.</li>
</ul>
</li>
<li>Your avatar is displayed as a green dot per the settings.</li>
<li>
Other peoples&rsquo; avatar dots are colored:
<ul>
<li>Blue if above you: blueness increases with height difference.</li>
<li>White if level with you.</li>
<li>Red if below you: redness increases with height difference.</li>
</ul>
</li>
<li>
To show display names:
<ul>
<li>Hover a dot to show the avatar&rsquo;s display name.</li>
<li>Click the dot to persist showing its display name.</li>
<li>Stop showing a display name by clicking the dot, the display name, or the radar background.</li>
</ul>
</li>
<li>
To teleport:
<ul>
<li>Click and hold on the radar circle. A green search circle is displayed.</li>
<li>The horizontally closest avatar is highlighted as are any of similar elevation.</li>
<li>Release the click to teleport to that position at the elevation of the closest avatar, or your
current elevation if no avatar is in the search circle.</li>
<li>To cancel a teleport, drag off the radar circle.</li>
</ul>
</li>
<li>The scale sets the search radius and distances above and below.</li>
<li>The avatars count is the number of avatars displayed.</li>
</ul>
<p>Settings:</p>
<ul>
<li>Show own avatar: Configure when to show a dot for your own avatar.</li>
<li>Refresh rate: Configure how often the avatar dots and radar orientation are updated.</li>
</ul>
<p>Disclaimers:</p>
<ul>
<li>The user identification provided by this app is not guaranteed: users can set their display name to
whatever they like.</li>
<li>The content of the display names displayed by this app is not moderated by this app.</li>
</ul>
<p>More information: <a href="http://ctrlaltstudio.com/vircadia/radar">http://ctrlaltstudio.com</a><br />
Copyright 2017-2020 David Rowe.<br />
Distributed under the Apache License, 2.0.</a>.<br />
Donations appreciated.
</p>
</div>
<div class="ok">
<input type="button" value="OK" />
</div>
</form>
<script src="radar.js"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

844
applications/radar/radar.js Normal file
View file

@ -0,0 +1,844 @@
/*!
radar.js
Created by David Rowe on 16 Nov 2017.
Copyright 2017-2020 David Rowe.
Information: http://ctrlaltstudio.com/vircadia/radar
Disclaimers:
1. The user identification provided by this app is not guaranteed: users can set their display name to whatever they like.
2. The content of the display names displayed by this app is not moderated by this app.
Distributed under the Apache License, Version 2.0.
See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
*/
/* global AvatarManager, Camera, HMD, MyAvatar, Overlays, Quat, Settings, Vec3, Window, location */
(function () {
"use strict";
var APP_NAME = "Radar",
APP_VERSION = "2.3.0", // Version number also needs to be set in web page HTML and JavaScript files.
SIMULATE = false,
INSTRUMENT = false,
// Controls.
RADAR_RANGE_DEFAULT = 20,
// EventBridge ID.
SCRIPT_ID = "cas.radar",
// EventBridge messages.
READY_MESSAGE = "readyMessage", // Engine <=> Dialog
VERSIONS_MESSAGE = "versionsMessage", // Engine <== Dialog
GET_CONTROLS_MESSAGE = "getControlsMessage", // Engine <== Dialog
SET_CONTROLS_MESSAGE = "setControlsMessage", // Engine <=> Dialog
GET_SETTINGS_MESSAGE = "getSettingsMessage", // Engine <== Dialog
SET_SETTINGS_MESSAGE = "setSettingsMessage", // Engine <=> Dialog
ROTATION_MESSAGE = "rotationMessage", // Engine ==> Dialog
AVATARS_MESSAGE = "avatarsMessage", // Engine <=> Dialog - World coordinates relative to camera.
CLEAR_MESSAGE = "clearMessage", // Engine ==> Dialog
TELEPORT_MESSAGE = "teleportMessage", // Engine <== Dialog - World coordinates relative to camera.
OPEN_URL_MESSAGE = "openURLMessage", // Engine <== Dialog
LOG_MESSAGE = "logMessage", // Engine <== Dialog
// Application objects.
Preferences,
Communications,
Updates,
App,
// Global objects.
tablet = null,
INFO = "INFO:",
//WARN = "WARN:",
ERROR = "ERROR:",
ERROR_MISSING_CASE = "Missing case:";
if (SIMULATE) {
Script.include("simulate.js");
}
function log() {
var i, length, strings = [];
for (i = 0, length = arguments.length; i < length; i++) {
strings.push(arguments[i]);
}
print("[CtrlAltStudio radar.js] " + strings.join(" "));
}
//#region Preferences ======================================================================================================
Preferences = (function () {
// Manages the application preferences - "controls" and "settings" in the dialog.
var PREFERENCES_ROOT = "com.ctrlaltstudio.radar.",
RADAR_RANGE_SETTING = PREFERENCES_ROOT + "range", // Must be different name to variable for obfuscation.
radarRange = RADAR_RANGE_DEFAULT,
SHOW_OWN_SETTING = PREFERENCES_ROOT + "ownAvatar", // Must be different name to variable for obfuscation.
SHOW_OWN_NEVER = 0,
SHOW_OWN_THIRD = 1,
SHOW_OWN_ALWAYS = 2,
SHOW_OWN_DEFAULT = SHOW_OWN_THIRD,
showOwn = SHOW_OWN_DEFAULT,
REFRESH_RATE_SETTING = PREFERENCES_ROOT + "updateRate", // Must be different name to variable for obfuscation.
REFRESH_RATE_FASTEST = 4,
REFRESH_RATE_FASTER = 3,
REFRESH_RATE_MEDIUM = 2,
REFRESH_RATE_SLOWER = 1,
REFRESH_RATE_SLOWEST = 0,
REFRESH_RATE_DEFAULT = REFRESH_RATE_MEDIUM,
refreshRate = REFRESH_RATE_DEFAULT,
preferencesChangedCallback = null;
function getPreferences() {
return {
radarRange: radarRange,
showOwn: showOwn,
refreshRate: refreshRate
};
}
function getRadarRange() {
return radarRange;
}
function setRadarRange(range) {
radarRange = range;
Settings.setValue(RADAR_RANGE_SETTING, radarRange);
preferencesChangedCallback(getPreferences());
}
function getShowOwn() {
return showOwn;
}
function setShowOwn(show) {
showOwn = show;
Settings.setValue(SHOW_OWN_SETTING, showOwn);
preferencesChangedCallback(getPreferences());
}
function getRefreshRate() {
return refreshRate;
}
function setRefreshRate(rate) {
refreshRate = rate;
Settings.setValue(REFRESH_RATE_SETTING, refreshRate);
preferencesChangedCallback(getPreferences());
}
function setPreferencesChangedCallback(callback) {
preferencesChangedCallback = callback;
}
function setUp() {
radarRange = Settings.getValue(RADAR_RANGE_SETTING, RADAR_RANGE_DEFAULT);
showOwn = Settings.getValue(SHOW_OWN_SETTING, SHOW_OWN_DEFAULT);
refreshRate = Settings.getValue(REFRESH_RATE_SETTING, REFRESH_RATE_DEFAULT);
}
function tearDown() {
// Nothing to do.
}
return {
getRadarRange: getRadarRange,
setRadarRange: setRadarRange,
SHOW_OWN_NEVER: SHOW_OWN_NEVER,
SHOW_OWN_THIRD: SHOW_OWN_THIRD,
SHOW_OWN_ALWAYS: SHOW_OWN_ALWAYS,
getShowOwn: getShowOwn,
setShowOwn: setShowOwn,
REFRESH_RATE_SLOWEST: REFRESH_RATE_SLOWEST,
REFRESH_RATE_SLOWER: REFRESH_RATE_SLOWER,
REFRESH_RATE_MEDIUM: REFRESH_RATE_MEDIUM,
REFRESH_RATE_FASTER: REFRESH_RATE_FASTER,
REFRESH_RATE_FASTEST: REFRESH_RATE_FASTEST,
getRefreshRate: getRefreshRate,
setRefreshRate: setRefreshRate,
getPreferences: getPreferences,
preferencesChanged: {
connect: setPreferencesChangedCallback
},
setUp: setUp,
tearDown: tearDown
};
}());
//#endregion
//#region Communications ===================================================================================================
Communications = (function () {
// Manages the communications with the Web page script.
var
isVersionsChecked = false,
readyCallback = null;
function onWebEventReceived(data) {
var message,
avatarPosition,
cameraVector,
ERROR_FILE_VERSIONS_DONT_MATCH = APP_NAME + " script file version numbers don't match",
ERROR_PLEASE_RELOAD_SCRIPT = "Please reload " + APP_NAME + " script.";
try {
message = JSON.parse(data);
} catch (e) {
return;
}
if (message.id !== SCRIPT_ID) {
return;
}
switch (message.type) {
case READY_MESSAGE:
if (readyCallback) {
readyCallback();
}
tablet.emitScriptEvent(JSON.stringify({
id: SCRIPT_ID,
type: READY_MESSAGE,
hmd: HMD.active
}));
break;
case GET_CONTROLS_MESSAGE:
tablet.emitScriptEvent(JSON.stringify({
id: SCRIPT_ID,
type: SET_CONTROLS_MESSAGE,
controls: {
range: Preferences.getRadarRange()
}
}));
break;
case SET_CONTROLS_MESSAGE:
Preferences.setRadarRange(message.controls.range);
break;
case GET_SETTINGS_MESSAGE:
tablet.emitScriptEvent(JSON.stringify({
id: SCRIPT_ID,
type: SET_SETTINGS_MESSAGE,
settings: {
showOwn: Preferences.getShowOwn(),
refreshRate: Preferences.getRefreshRate()
}
}));
break;
case SET_SETTINGS_MESSAGE:
Preferences.setShowOwn(message.settings.showOwn);
Preferences.setRefreshRate(message.settings.refreshRate);
break;
case AVATARS_MESSAGE:
Updates.avatarsDisplayed();
break;
case TELEPORT_MESSAGE:
avatarPosition = MyAvatar.position;
cameraVector = Vec3.subtract(Camera.position, avatarPosition);
if (message.vector.y === null) {
message.vector.y = -cameraVector.y;
}
MyAvatar.goToLocation(Vec3.sum(Vec3.sum(avatarPosition, message.vector), cameraVector),
false, undefined, message.isAvatar, true);
break;
case OPEN_URL_MESSAGE:
Window.openUrl(message.url);
break;
case VERSIONS_MESSAGE:
if (!isVersionsChecked) {
if (message.scriptVersion !== APP_VERSION || message.htmlVersion !== APP_VERSION) {
log(ERROR, ERROR_FILE_VERSIONS_DONT_MATCH + ": " + APP_VERSION + ", " + message.scriptVersion
+ " (script), " + message.htmlVersion + " (HTML)");
Window.alert(ERROR_FILE_VERSIONS_DONT_MATCH + "!\n" + ERROR_PLEASE_RELOAD_SCRIPT);
}
isVersionsChecked = true;
}
break;
case LOG_MESSAGE:
log(message.message);
break;
default:
log(ERROR, ERROR_MISSING_CASE, 0, data);
}
}
function sendAvatarData(radarRange, avatarData) {
tablet.emitScriptEvent(JSON.stringify({
id: SCRIPT_ID,
type: AVATARS_MESSAGE,
range: radarRange,
data: avatarData
}));
}
function clearAvatarData() {
tablet.emitScriptEvent(JSON.stringify({
id: SCRIPT_ID,
type: CLEAR_MESSAGE
}));
}
function sendRotation(rotation) {
tablet.emitScriptEvent(JSON.stringify({
id: SCRIPT_ID,
type: ROTATION_MESSAGE,
rotation: rotation
}));
}
function connectReadyCallback(callback) {
readyCallback = callback;
}
function disconnectReadyCallback(callback) {
if (readyCallback === callback) {
readyCallback = null;
}
}
return {
onWebEventReceived: onWebEventReceived,
ready: {
connect: connectReadyCallback,
disconnect: disconnectReadyCallback
},
sendAvatarData: sendAvatarData,
clearAvatarData: clearAvatarData,
sendRotation: sendRotation
};
}());
//#endregion
//#region Updates ==========================================================================================================
Updates = (function () {
// Main update loop.
var radarRange,
RADAR_SEARCH_MULTIPLIER = Math.sqrt(2), // Encompass search cylinder.
radarSearchRange,
cameraMode,
showOwn,
isShowOwn,
FIRST_PERSON_CAMERA_MODES = ["first person", "first person look at"],
refreshRate,
ROTATION_INTERVALS = [
200, // Slow
150,
100, // Medium
50,
25 // Fast
],
rotationInterval = ROTATION_INTERVALS[Preferences.REFRESH_RATE_MEDIUM],
rotationTimer = null,
AVATAR_INTERVALS = [
2000, // Slow
1000,
750, // Medium
350,
100 // Fast
],
avatarsInterval = AVATAR_INTERVALS[Preferences.REFRESH_RATE_MEDIUM],
avatarsUpdateTimer = null,
timeToPrepareDataStart,
timeToPrepareData = 0,
timeToDisplayDataStart,
timeToDisplayData = 0,
targetSendTime = 0,
avatarsSendTimer = null,
isDisplayingData = false,
MAX_DISPLAY_NAME_LENGTH = 30,
mySessionUUID,
isRunning = false;
function calculateIsShowOwn() {
isShowOwn = showOwn === Preferences.SHOW_OWN_ALWAYS
|| showOwn === Preferences.SHOW_OWN_THIRD && FIRST_PERSON_CAMERA_MODES.indexOf(cameraMode) === -1;
}
function calculateIntervals() {
rotationInterval = ROTATION_INTERVALS[refreshRate];
avatarsInterval = AVATAR_INTERVALS[refreshRate];
}
function setCameraMode(mode) {
cameraMode = mode;
calculateIsShowOwn();
}
function setPreferences(preferences) {
showOwn = preferences.showOwn;
calculateIsShowOwn();
radarRange = preferences.radarRange;
radarSearchRange = RADAR_SEARCH_MULTIPLIER * radarRange;
refreshRate = preferences.refreshRate;
calculateIntervals();
}
function updateRotation() {
var tabletOrientation,
tabletDirection,
tabletHorizontalDirection,
rotation;
if (App.isHMDMode()) {
tabletOrientation = Overlays.getProperty(HMD.tabletID, "orientation");
tabletDirection = Quat.getUp(tabletOrientation); // Out the top of the tablet.
if (Vec3.dot(tabletDirection, Vec3.UNIT_Y) > 0.5) {
tabletDirection = Vec3.multiply(-1, Quat.getForward(tabletOrientation)); // Out the back of the tablet.
}
tabletHorizontalDirection = Vec3.cross(tabletDirection, Vec3.UNIT_Y);
rotation = Vec3.orientedAngle(Vec3.UNIT_X, tabletHorizontalDirection, Vec3.UNIT_Y);
} else {
rotation = Quat.safeEulerAngles(Camera.orientation).y;
}
Communications.sendRotation(rotation);
rotationTimer = Script.setTimeout(updateRotation, rotationInterval);
}
/*
// Code for displaying avatar dots at positions and elevations for screen snap or testing elevation colours.
var sessionIDs = [Uuid.generate(), Uuid.generate(), Uuid.generate(), Uuid.generate(), Uuid.generate(), Uuid.generate(),
Uuid.generate(), Uuid.generate(), Uuid.generate(), Uuid.generate(), Uuid.generate(), Uuid.generate()];
function updateAvatars() {
var avatarPositions,
avatarNames,
avatarDataRange = 10.0,
avatarData = [],
i;
avatarPositions = [
{ x: 0, y: 0, z: 0 },
{ x: -2.5, y: 0, z: -1 },
{ x: -2.8, y: 0, z: -1.3 },
{ x: -3, y: 0, z: -7 },
{ x: -4, y: 0, z: -7.7 },
{ x: -4.3, y: 0, z: -7.3 },
{ x: -4.3, y: 0, z: -7.8 },
{ x: -3.24, y: 0, z: -8.4 },
{ x: 1, y: -7, z: 6 },
{ x: 5, y: 8, z: -3 }
];
avatarNames = ["ctrlaltdavid", "", "", "", "", "", "", "", "", "wade", "", ""];
for (i = 0; i < avatarPositions.length; i += 1) {
avatarData.push({
uuid: sessionIDs[i],
vector: avatarPositions[i],
name: avatarNames[i]
});
}
avatarData[0].isMyAvatar = true;
Communications.sendAvatarData(avatarDataRange, avatarData);
avatarsUpdateTimer = Script.setTimeout(updateAvatars, avatarsInterval);
}
*/
function limitDisplayName(name) {
// Limit display name here so that extraneous data is not sent in message to HTML code.
if (name.length > MAX_DISPLAY_NAME_LENGTH) {
return name.slice(0, MAX_DISPLAY_NAME_LENGTH) + "&hellip;";
}
return name;
}
function updateAvatars() {
// Finds avatars in cylinder, not sphere.
// Avatar data update loop:
// - The desired update loop time is avatarsInterval.
// - The update loop time is extended if the HTML script takes longer to display the data.
// - The update loop comprises:
// - Preparing the avatar data.
// - Sending the avatar data at the target interval or as soon as the previous data has been displayed.
// - Scheduling preparing the next avatar data so that it is ready at the anticipated send time.
// - Displaying the avatar data in the HTML script. (Includes teleport elevation searching.)
var cameraPosition,
avatarIDs,
myAvatarIndex,
palData,
vector,
sessionUUID,
avatarDatum,
avatarData = [],
avatarDataRange, // The radar range that avatarData is for.
i, length,
MINIMUM_UPDATE_DELAY = 2,
sendDelay,
MINIMUM_SEND_DELAY = 2,
SEND_RETRY_INTERVAL = 25;
timeToPrepareDataStart = Date.now();
// Timer has fired.
avatarsUpdateTimer = null;
// Get avatar data.
cameraPosition = Camera.position;
avatarIDs = AvatarManager.getAvatarsInRange(cameraPosition, radarSearchRange);
avatarDataRange = radarRange;
// Remove own avatar if necessary.
myAvatarIndex = avatarIDs.indexOf(mySessionUUID);
if (myAvatarIndex !== -1 && !isShowOwn) {
avatarIDs.splice(myAvatarIndex, 1);
}
// Collect avatar data.
palData = AvatarManager.getPalData(avatarIDs)["data"]; // Property name as string to avoid obfuscation.
for (i = 0, length = palData.length; i < length; i++) {
// If session display name is undefined then the data is messed up (e.g., spheres problem).
// The pal.js script also ignores items with empty name fields.
if (!palData[i].sessionDisplayName) {
continue;
}
sessionUUID = palData[i].sessionUUID;
// FIXME: AvatarManager.getPalData() returns with sessionUUID === "" for own avatar.
// Manuscript case 19693.
if (sessionUUID === "") {
if (isShowOwn) {
sessionUUID = mySessionUUID;
} else {
continue;
}
}
vector = Vec3.subtract(palData[i].position, cameraPosition);
if (Math.abs(vector.y) <= radarRange) {
if (Vec3.length({ x: vector.x, y: 0, z: vector.z }) <= radarRange) {
avatarDatum = {
uuid: sessionUUID,
vector: vector,
name: limitDisplayName(palData[i].sessionDisplayName)
};
if (sessionUUID === mySessionUUID) {
// Don't set value for each avatar so as to reduce EventBridge message size.
avatarDatum.isMyAvatar = true;
}
avatarData.push(avatarDatum);
}
}
}
timeToPrepareData = Date.now() - timeToPrepareDataStart;
if (INSTRUMENT) {
log("Main script : prepare : " + timeToPrepareData);
// 2019 01 07
// Simulation:
// - 100 avatars: 3ms
// - 1000 avatars: 48ms
}
// Send avatar data at target time, if can.
sendDelay = targetSendTime - Date.now(); // Should hover around 0 for a reasonably loaded radar.
if (INSTRUMENT) {
log("Main script : delay : " + sendDelay);
}
// Schedule preparing next data set so that it is ready to send at later of target interval or display update from
// previous data set.
avatarsUpdateTimer = Script.setTimeout(updateAvatars,
Math.max(Math.max(avatarsInterval, timeToDisplayData) - timeToPrepareData + sendDelay, MINIMUM_UPDATE_DELAY));
function sendData() {
var instrumentTimestamp;
if (!isRunning) {
return;
}
// Delay sending data until after previous lot has been processed.
if (isDisplayingData) {
if (INSTRUMENT) {
log("Main script : reschedule send");
}
avatarsSendTimer = Script.setTimeout(sendData, SEND_RETRY_INTERVAL);
return;
}
avatarsSendTimer = null;
if (INSTRUMENT) {
instrumentTimestamp = Date.now();
}
// Set target for sending next lot of data.
targetSendTime = Date.now() + avatarsInterval;
// Send current lost of data.
isDisplayingData = true;
timeToDisplayDataStart = Date.now();
Communications.sendAvatarData(avatarDataRange, avatarData);
if (INSTRUMENT) {
log("Main script : send : " + (Date.now() - instrumentTimestamp));
// 2019 01 07
// Simulation:
// - 100 avatars: 1ms
// - 1000 avatars: 5ms
}
}
avatarsSendTimer = Script.setTimeout(sendData,
Math.max(sendDelay, MINIMUM_SEND_DELAY)); // Caters for negative sendDelay values.
}
function onSessionUUIDChanged() {
mySessionUUID = MyAvatar.sessionUUID;
}
function start() {
isRunning = true;
mySessionUUID = MyAvatar.sessionUUID;
MyAvatar.sessionUUIDChanged.connect(onSessionUUIDChanged);
if (avatarsUpdateTimer === null) {
targetSendTime = Date.now() + avatarsInterval;
avatarsUpdateTimer = Script.setTimeout(updateAvatars, avatarsInterval);
}
if (rotationTimer === null) {
rotationTimer = Script.setTimeout(updateRotation, rotationInterval);
}
}
function avatarsDisplayed() {
isDisplayingData = false;
timeToDisplayData = Date.now() - timeToDisplayDataStart;
if (INSTRUMENT) {
log("Main script : display : " + timeToDisplayData);
// 2019 01 07
// Simulation:
// - 100 avatars: 16ms
// - 1000 avatars: 25ms
}
}
function stop() {
isRunning = false;
isDisplayingData = false;
if (avatarsSendTimer !== null) {
Script.clearTimeout(avatarsSendTimer);
avatarsSendTimer = null;
}
if (avatarsUpdateTimer !== null) {
Script.clearTimeout(avatarsUpdateTimer);
avatarsUpdateTimer = null;
}
if (rotationTimer !== null) {
Script.clearTimeout(rotationTimer);
rotationTimer = null;
}
MyAvatar.sessionUUIDChanged.disconnect(onSessionUUIDChanged);
}
function setUp() {
setPreferences(Preferences.getPreferences());
Preferences.preferencesChanged.connect(setPreferences);
}
function tearDown() {
// Nothing to do.
}
return {
setPreferences: setPreferences,
setCameraMode: setCameraMode,
start: start,
avatarsDisplayed: avatarsDisplayed,
stop: stop,
setUp: setUp,
tearDown: tearDown
};
}());
//#endregion
//#region App ==============================================================================================================
App = (function () {
// Manages the interactions with the Interface app environment.
var APP_ICON_ACTIVE = Script.resolvePath("./assets/radar-a.svg"),
APP_ICON_INACTIVE = Script.resolvePath("./assets/radar-i.svg"),
APP_HTML_PAGE = Script.resolvePath("./html/radar.html"),
APP_BUTTON_TEXT = "RADAR",
button = null,
isAppActive = false,
HMD_TABLET_BECOMES_TOOLBAR_SETTING = "hmdTabletBecomesToolbar",
isHMDTabletBecomesToolbar = false,
isHMDActive = false;
function onDisplayModeChanged(isHMDMode) {
// Close app if have switched between desktop and HMD modes, else tablet can't be used or toolbar button stays on.
if (isAppActive && isHMDMode !== isHMDActive) {
isHMDTabletBecomesToolbar = Settings.getValue(HMD_TABLET_BECOMES_TOOLBAR_SETTING, false);
isHMDActive = isHMDMode;
tablet.gotoHomeScreen();
}
}
function onPossibleDomainChangeRequired() {
// Clear radar display so that out-of-date display doesn't unexpectedly jump before updating to new location.
Communications.clearAvatarData();
}
function startApp() {
isHMDTabletBecomesToolbar = Settings.getValue(HMD_TABLET_BECOMES_TOOLBAR_SETTING, false);
isHMDActive = HMD.active;
HMD.displayModeChanged.connect(onDisplayModeChanged);
location.possibleDomainChangeRequired.connect(onPossibleDomainChangeRequired);
location.possibleDomainChangeRequiredViaICEForID.connect(onPossibleDomainChangeRequired);
Camera.modeUpdated.connect(Updates.setCameraMode);
Updates.setCameraMode(Camera.mode);
Communications.ready.connect(Updates.start);
tablet.webEventReceived.connect(Communications.onWebEventReceived);
}
function stopApp() {
Updates.stop();
tablet.webEventReceived.disconnect(Communications.onWebEventReceived);
Communications.ready.disconnect(Updates.start);
Camera.modeUpdated.disconnect(Updates.setCameraMode);
location.possibleDomainChangeRequiredViaICEForID.disconnect(onPossibleDomainChangeRequired);
location.possibleDomainChangeRequired.disconnect(onPossibleDomainChangeRequired);
HMD.displayModeChanged.disconnect(onDisplayModeChanged);
}
function onButtonClicked() {
if (!isAppActive) {
tablet.gotoWebScreen(APP_HTML_PAGE);
} else {
tablet.gotoHomeScreen();
}
}
function onTabletScreenChanged(type, url) {
var active;
active = url.slice(0, APP_HTML_PAGE.length) === APP_HTML_PAGE;
if (active === isAppActive) {
return;
}
isAppActive = active;
button.editProperties({ isActive: isAppActive });
if (isAppActive) {
startApp();
} else {
stopApp();
}
}
function isHMDMode() {
return isHMDActive && !isHMDTabletBecomesToolbar;
}
function setUp() {
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
if (!tablet) {
log(ERROR, "Tablet not found!");
return;
}
button = tablet.addButton({
icon: APP_ICON_INACTIVE,
activeIcon: APP_ICON_ACTIVE,
text: APP_BUTTON_TEXT,
isActive: false
});
if (!button) {
log(ERROR, "Tablet button not created!");
tablet = null;
return;
}
tablet.screenChanged.connect(onTabletScreenChanged);
button.clicked.connect(onButtonClicked);
}
function tearDown() {
if (isAppActive) {
stopApp();
tablet.gotoHomeScreen(); // Close desktop window.
}
if (button) {
button.clicked.disconnect(onButtonClicked);
tablet.removeButton(button);
button = null;
}
if (tablet) {
tablet.screenChanged.disconnect(onTabletScreenChanged);
tablet = null;
}
}
return {
isHMDMode: isHMDMode,
setUp: setUp,
tearDown: tearDown
};
}());
//#endregion
//#region Set up and tear down =============================================================================================
function setUp() {
log(INFO, APP_NAME, APP_VERSION);
App.setUp();
Preferences.setUp();
Updates.setUp();
}
function tearDown() {
Script.scriptEnding.disconnect(tearDown);
App.tearDown();
Preferences.tearDown();
Updates.tearDown();
}
setUp();
Script.scriptEnding.connect(tearDown);
//#endregion
}());