diff --git a/app.js b/app.js
index c9a42d9..1ab4223 100755
--- a/app.js
+++ b/app.js
@@ -56,34 +56,37 @@ var app = express();
console.log('use helmet');
app.use(helmet());
-var use_content_security_policy = true
-
+var use_content_security_policy = true;
+// NOTE: operator.html has its own CSP rules that override what is set here
if (use_content_security_policy) {
console.log('using a content security policy');
app.use(helmet.contentSecurityPolicy({
- directives:{
- defaultSrc:["'self'"],
- scriptSrc:["'self'", "'unsafe-inline'",
- 'static.robotwebtools.org',
- 'robotwebtools.org',
- 'webrtc.github.io',
- 'www.gstatic.com',
- 'code.jquery.com',
- 'cdnjs.cloudflare.com',
- 'stackpath.bootstrapcdn.com'],
- connectSrc:["'self'", 'ws://localhost:9090'],
- imgSrc: ["'self'", 'data:'],
- styleSrc:["'self'",
- 'stackpath.bootstrapcdn.com'],
- fontSrc:["'self'"]}}));
+ directives: {
+ defaultSrc:["'self'"],
+ scriptSrc:["'self'", "'unsafe-inline'",
+ 'static.robotwebtools.org',
+ 'robotwebtools.org',
+ 'webrtc.github.io',
+ 'www.gstatic.com',
+ 'code.jquery.com',
+ 'cdnjs.cloudflare.com',
+ 'stackpath.bootstrapcdn.com',
+ 'cdn.jsdelivr.net'],
+ connectSrc:["'self'", 'ws://localhost:9090'],
+ imgSrc: ["'self'", 'data:'],
+ styleSrc:["'self'",
+ 'stackpath.bootstrapcdn.com'],
+ fontSrc:["'self'"]}
+ })
+ );
} else {
// Disable the content security policy. This is helpful during
// development, but risky when deployed.
console.log('WARNING: Not using a content security policy. This risky when deployed!');
app.use(
- helmet({
- contentSecurityPolicy: false,
- })
+ helmet({
+ contentSecurityPolicy: false,
+ })
);
}
diff --git a/operator/operator.css b/operator/operator.css
index 4912a66..e52099c 100644
--- a/operator/operator.css
+++ b/operator/operator.css
@@ -63,6 +63,15 @@ video {
.foreground {
z-index:2;
cursor: pointer;
+ position: relative;
+}
+
+/* Three.js canvas positioning */
+.foreground > canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
}
@@ -343,7 +352,7 @@ video {
background: transparent;
border: solid 3px #000000;
border-radius: 3px;
- /*background-image: linear-gradient(to bottom, #4fc9ee, #0000ff); /*#3aa2d0);*/*/
+ /*background-image: linear-gradient(to bottom, #4fc9ee, #0000ff); /*#3aa2d0);*/
/*box-shadow: inset 0 1px rgba(255, 255, 255, 0.5), 0 0 2px rgba(0, 0, 0, 0.2);*/
/*transition: left 0.15s ease-out;*/
transition: left 0.1s ease-out;
diff --git a/operator/operator.html b/operator/operator.html
index c82f010..2aed2c0 100644
--- a/operator/operator.html
+++ b/operator/operator.html
@@ -5,13 +5,17 @@
+
-
+
-
+
@@ -232,7 +236,10 @@
Settings
-
+
+
+
+
diff --git a/operator/operator_ui_regions.js b/operator/operator_ui_regions.js
index a37d5b2..3022df9 100644
--- a/operator/operator_ui_regions.js
+++ b/operator/operator_ui_regions.js
@@ -1,44 +1,37 @@
'use strict';
-// TODO: This might be redundant with 'interfaceMode' remove after checking
-var strokeOpacity = 0.0;
-var w = videoDimensions.h;
-var h = videoDimensions.w;
-
-
-function setCameraViewPreset() {
- if (panTiltCameraVideoControl.currentMode != null)
- setCameraView(panTiltCameraVideoControl.currentMode);
-}
-
-
-var navigationVideoControl = new VideoControl('navigationVideo', 'nav');
-var manipulationVideoControl = new VideoControl('manipulationVideo', 'manip');
-
/*
* Class for a video stream visualization with an overlay
* This is redundant at the moment but will be necessary
* soon, when we want to have more than one video stream
* visualization.
*/
-function VideoControl(videoId, currentMode=null) {
- this.videoId = videoId;
- this.combinedSVG = document.getElementById(videoId + "Overlay");
- this.combinedSVG.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
- this.overlays = {}; // key is mode id, value is the Overlay object
- this.currentMode = currentMode;
- this.videoDiv = document.getElementById(videoId + "Div");
- this.video = document.getElementById(videoId);
- this.video.setAttribute("height", h);
- this.video.setAttribute("width", w);
- this.isActive = false;
-
- this.addOverlay = function(overlay) {
- this.overlays[overlay.modeId] = overlay;
- this.combinedSVG.appendChild(overlay.svg);
- }
-
- this.setMode = function(modeId) {
+class VideoControl {
+ constructor(videoId, currentMode=null) {
+ this.videoId = videoId;
+ this.combinedSVG = document.getElementById(videoId + "Overlay");
+ this.combinedSVG.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
+ this.overlays = {}; // key is mode id, values are the Overlay objects
+ this.currentMode = currentMode;
+ this.videoDiv = document.getElementById(videoId + "Div");
+ this.video = document.getElementById(videoId);
+ this.video.setAttribute("height", h);
+ this.video.setAttribute("width", w);
+ this.isActive = false;
+ }
+
+ addOverlay(overlay, type="svg") {
+ if (this.overlays.hasOwnProperty(overlay.modeId)) {
+ this.overlays[overlay.modeId].push(overlay);
+ } else {
+ this.overlays[overlay.modeId] = [overlay];
+ }
+
+ if (type === "svg")
+ this.combinedSVG.appendChild(overlay.svg);
+ }
+
+ setMode(modeId) {
this.currentMode = modeId;
let buttonName = modeId + '_mode_button';
let button = document.getElementById(buttonName);
@@ -50,57 +43,129 @@ function VideoControl(videoId, currentMode=null) {
const modeNames = this.getModeNames();
for (let i in modeNames) {
if (modeNames[i] !== modeId) {
- this.overlays[modeNames[i]].hide();
+ this.overlays[modeNames[i]].forEach( function(overlay) {
+ overlay.hide();
+ });
}
}
- this.overlays[modeId].show();
+ this.overlays[modeId].forEach( function(overlay) {
+ overlay.show();
+ });
}
- this.getModeNames = function() {
+ getModeNames() {
return Object.keys(this.overlays);
}
- this.setActive = function(isActive) {
+ setActive(isActive) {
this.isActive = isActive;
if (this.isActive){
this.videoDiv.style.backgroundColor = "rgba(0,0,0,0.1)";
- this.overlays[this.currentMode].show();
+ this.overlays[this.currentMode].forEach( function(overlay) {
+ overlay.show();
+ });
}
else{
this.videoDiv.style.backgroundColor = "rgba(0,0,0,0.5)";
- this.overlays[this.currentMode].hide();
+ this.overlays[this.currentMode].forEach( function(overlay) {
+ overlay.hide();
+ });
+ }
+ }
+}
+
+class THREEManager {
+ constructor(camera, width, height) {
+ this.camera = camera;
+ this.width = width;
+ this.height = height;
+
+ this.overlays = {};
+ }
+
+ addOverlay(overlay) {
+ this.overlays[overlay.modeId] = {
+ overlay: overlay,
+ render: true
+ };
+ }
+
+ pauseOverlayRender(modeId) {
+ this.overlays[modeId].render = false;
+ this.overlays[modeId].overlay.render();
+ }
+
+ resumeOverlayRender(modeId) {
+ this.overlays[modeId].render = true;
+ this.overlays[modeId].overlay.render();
+ }
+
+ animate() {
+ // TODO: Figure out how to properly pass self into a callback function
+ requestAnimationFrame(() => {
+ this.animate();
+ });
+
+ for (const overlay in this.overlays) {
+ if (this.overlays[overlay].render) {
+ this.overlays[overlay].overlay.render();
+ }
}
}
}
/*
-* Class for a video overlay
+* Base class for a video overlay
*/
-function Overlay(modeId) {
- this.modeId = modeId;
- this.regions = [];
+class Overlay {
+ constructor(modeId) {
+ this.modeId = modeId;
+ }
- this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- this.svg.setAttribute('preserveAspectRatio', 'none');
- this.svg.setAttribute('id', modeId + '_ui_overlay');
+ addItem() {
+ console.warn("addItem() should be overridden by the child class");
+ }
- let bgRect = makeRectangle(0, 0, w, h);
- this.curtain = new Region(modeId + '_curtain', null , 'curtain',
- rectToPoly(bgRect), 'white', this.svg, true, 0.5);
+ hide() {
+ console.warn("hide() should be overridden by the child class");
+ }
+
+ show() {
+ console.warn("show() should be overridden by the child class");
+ }
+}
- this.addRegion = function(region) {
+/*
+* Class for an SVG video overlay
+*/
+class OverlaySVG extends Overlay {
+ constructor(modeId) {
+ super(modeId);
+ this.regions = [];
+
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.svg.setAttribute('preserveAspectRatio', 'none');
+ this.svg.setAttribute('id', modeId + '_ui_overlay');
+
+ let bgRect = makeRectangle(0, 0, w, h);
+ this.curtain = new Region(modeId + '_curtain', null , 'curtain',
+ rectToPoly(bgRect), 'white', this.svg, true, 0.5);
+
+ this.addRegion(this.curtain);
+ }
+
+ addRegion(region) {
this.regions.push(region);
}
- this.addRegion(this.curtain);
- this.hide = function() {
+ hide() {
for (let i in this.regions) {
this.regions[i].hide();
}
this.curtain.show();
}
- this.show = function() {
+ show() {
for (let i in this.regions) {
this.regions[i].show();
}
@@ -109,28 +174,96 @@ function Overlay(modeId) {
}
/*
-* Class for a video overlay
+* Class for an THREE.js video overlay
+*/
+class OverlayTHREE extends Overlay {
+ constructor(modeId, threeManager) {
+ super(modeId);
+ this.objs = {};
+
+ this.threeManager = threeManager;
+ this.scene = new THREE.Scene();
+ this.renderer = new THREE.WebGLRenderer({ alpha: true });
+ this.renderer.setSize(this.threeManager.width, this.threeManager.height);
+ $(`#${modeId}_ui_overlay`).parent().parent().prepend(this.renderer.domElement);
+
+ this.composer = new POSTPROCESSING.EffectComposer(this.renderer);
+ this.composer.addPass(new POSTPROCESSING.RenderPass(this.scene, this.threeManager.camera));
+
+ this.threeManager.addOverlay(this);
+ }
+
+ addRenderPass(renderPass) {
+ this.composer.addPass(renderPass);
+ }
+
+ addItem(obj) {
+ this.objs[obj.name] = obj;
+ this.scene.add(obj.mesh);
+ }
+
+ hide() {
+ for (const obj in this.objs) {
+ this.objs[obj].hide();
+ }
+ this.threeManager.pauseOverlayRender(this.modeId);
+ }
+
+ show() {
+ for (const obj in this.objs) {
+ this.objs[obj].show();
+ }
+ this.threeManager.resumeOverlayRender(this.modeId);
+ }
+
+ render() {
+ this.composer.render();
+ }
+}
+
+/*
+* Class for an overlay region
*/
-function Region(regionId, fname, label, poly, color, parentSVG, isContinuous=true, fillOpacity=0.0) {
- this.regionId = regionId;
- this.fname = fname;
- this.label = label;
- this.poly = poly;
- this.isContinuous = isContinuous;
- this.parentSVG = parentSVG;
-
- createRegionSVG(this.parentSVG, this.regionId, this.fname, this.label,
- this.poly, color, this.isContinuous, fillOpacity);
-
- this.hide = function() {
+class Region {
+ constructor(regionId, fname, label, poly, color, parentSVG, isContinuous=true, fillOpacity=0.0) {
+ this.regionId = regionId;
+ this.fname = fname;
+ this.label = label;
+ this.poly = poly;
+ this.isContinuous = isContinuous;
+ this.parentSVG = parentSVG;
+
+ createRegionSVG(this.parentSVG, this.regionId, this.fname, this.label,
+ this.poly, color, this.isContinuous, fillOpacity);
+ }
+
+
+ hide() {
document.getElementById(this.regionId).style.display = 'none';
}
- this.show = function() {
+ show() {
document.getElementById(this.regionId).style.display = 'block';
}
}
+class THREEObject {
+ constructor(name, geo, mat) {
+ this.name = name;
+ this.geo = geo;
+ this.mat = mat;
+ this.mesh = new THREE.Mesh(this.geo, this.mat);
+ }
+
+ hide() {
+ this.mesh.visible = false;
+ }
+
+ show() {
+ this.mesh.visible = true;
+ }
+}
+
function setMode(modeId) {
if (modeId == 'nav') {
if (!navigationVideoControl.isActive) {
@@ -140,6 +273,7 @@ function setMode(modeId) {
if (checkbox.checked)
changeGripperFollow(false);
setCameraView('nav');
+ // TODO: Is there some way to set this button list procedurally?
document.getElementById('lookUpNavButton').disabled = false;
document.getElementById('lookLeftNavButton').disabled = false;
document.getElementById('lookRightNavButton').disabled = false;
@@ -189,6 +323,22 @@ function setMode(modeId) {
}
}
+// TODO: This might be redundant with 'interfaceMode' remove after checking
+var strokeOpacity = 0.0;
+var w = videoDimensions.h;
+var h = videoDimensions.w;
+
+
+function setCameraViewPreset() {
+ if (panTiltCameraVideoControl.currentMode != null)
+ setCameraView(panTiltCameraVideoControl.currentMode);
+}
+
+var navigationVideoControl = new VideoControl('navigationVideo', 'nav');
+var manipulationVideoControl = new VideoControl('manipulationVideo', 'manip');
+
+var threeManager = new THREEManager(new THREE.PerspectiveCamera(69, w/h, 0.1, 1000), w, h);
+
function createUiRegions(debug) {
@@ -203,7 +353,7 @@ function createUiRegions(debug) {
// navigation
/////////////////////////
- let navOverlay = new Overlay('nav');
+ let navOverlay = new OverlaySVG('nav');
// Big rectangle at the borders of the video
let bgRect = makeRectangle(0, 0, w, h);
let smRect = makeSquare((w/2.0)-(w/20.0), (h*(3.0/4.0))-(h/20.0), w/10.0, h/10.0);
@@ -225,6 +375,27 @@ function createUiRegions(debug) {
navOverlay.addRegion(new Region('nav_ccw_region', 'turnCCW' , 'turn 90 degrees CCW',
rectToPoly(rightRect), color, navOverlay.svg, false));
navigationVideoControl.addOverlay(navOverlay);
+
+ let navOverlayTHREE = new OverlayTHREE('nav', threeManager);
+
+ navOverlayTHREE.addItem(new THREEObject(
+ 'reach_visualization_circle',
+ new THREE.CircleGeometry(0.52, 32), // The arm has a 52 centimeter reach (source: https://hello-robot.com/product#:~:text=range%20of%20motion%3A%2052cm)
+ new THREE.MeshBasicMaterial({color: 'rgb(246, 179, 107)', transparent: true, opacity: 0.25}),
+ ));
+ var outlineEffect = new POSTPROCESSING.OutlineEffect(
+ navOverlayTHREE.scene,
+ navOverlayTHREE.threeManager.camera,
+ {visibleEdgeColor: 0xff9900});
+ var outlineEffectPass = new POSTPROCESSING.EffectPass(
+ navOverlayTHREE.threeManager.camera,
+ outlineEffect
+ );
+ outlineEffectPass.renderToScreen = true;
+ outlineEffect.selectObject(navOverlayTHREE.objs.reach_visualization_circle.mesh);
+ navOverlayTHREE.addRenderPass(outlineEffectPass);
+ navigationVideoControl.addOverlay(navOverlayTHREE, "threejs");
+
navigationVideoControl.setMode("nav");
navigationVideoControl.setActive(true);
@@ -233,7 +404,7 @@ function createUiRegions(debug) {
// manipulation
/////////////////////////
- let armOverlay = new Overlay('manip');
+ let armOverlay = new OverlaySVG('manip');
bgRect = makeRectangle(0, h/5.0, w, h-h/5.0);
// Small rectangle at the top of the middle of the video
@@ -423,7 +594,7 @@ function createUiV1Regions(debug) {
/////// UTILITY FUNCTIONS //////////
-function createRegionSVG(parentSVG, id, fname, title, poly, color, isContinuous, fillOpacity, stroke_width = 2, stroke_opacity = 0.3){
+function createRegionSVG(parentSVG, id, fname, title, poly, color, isContinuous, fillOpacity, stroke_width = 2, stroke_opacity = 0.3) {
let path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('fill-opacity', '0.0');
path.setAttribute('stroke-opacity', '1.0');
@@ -501,6 +672,6 @@ function drawText(elementID, text, x, y, font_size=100, center=false, color='whi
document.getElementById(elementID).appendChild(txt);
}
-createUiRegions(true); // debug = true or false
-
+createUiRegions(true); // debug = true or false
+threeManager.animate();
diff --git a/robot/robot.html b/robot/robot.html
index 00c480c..97c6e56 100644
--- a/robot/robot.html
+++ b/robot/robot.html
@@ -43,6 +43,8 @@
+
+
@@ -58,6 +60,7 @@
// When the robot and the operator are first connected, switch to navigation mode.
//console.log('starting in navigation mode')
//turnModeOn('nav')
+ sendTfs();
}