annotation_gui_gcp/js/CADView.js (336 lines of code) (raw):
import * as THREE from 'https://unpkg.com/three@0.127.0/build/three.module.js';
import { FBXLoader } from 'https://unpkg.com/three@0.127.0/examples/jsm/loaders/FBXLoader.js';
import { OrbitControls } from 'https://unpkg.com/three@0.127.0/examples/jsm/controls/OrbitControls.js';
//----Variables----//
//DOM element to attach the renderer to
let viewport;
//built-in three.js _cameraControls will be attached to this
let _cameraControls;
//camera attributes
const view_angle = 45;
//----Constructors----//
const renderer = new THREE.WebGLRenderer({ antialias: true });
const _scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(view_angle, 1);
//constructs an instance of a white light
renderer.setClearColor(0x05CB63); // Background color
const pointLight = new THREE.PointLight(0xFFFFFF);
const ambientLight = new THREE.AmbientLight(0x404040); // soft white light
let _raycaster;
// List of three.js 3D objects depicting annotated GCPs
let _gcps = {};
// cad model
let _cad_model = null;
let _cad_model_bbox = null;
let _cad_model_filename = null;
// Marker that points at the tracked camera
let _trackingMarker = null;
window.addEventListener('load', initialize);
function getCompoundBoundingBox(object3D) {
let box = null;
object3D.traverse(function (obj3D) {
let geometry = obj3D.geometry;
if (geometry === undefined) return;
geometry.computeBoundingBox();
if (box === null) {
box = geometry.boundingBox;
} else {
box.union(geometry.boundingBox);
}
});
return box;
}
function onWindowResize() {
resizeCanvas()
}
function resizeCanvas() {
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
function fitCameraToSelection(camera, controls, selection, fitOffset = 1.2) {
const box = new THREE.Box3();
for (const object of selection) box.expandByObject(object);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
const fitHeightDistance = maxSize / (2 * Math.atan(Math.PI * camera.fov / 360));
const fitWidthDistance = fitHeightDistance / camera.aspect;
const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance);
const direction = controls.target.clone()
.sub(camera.position)
.normalize()
.multiplyScalar(distance);
controls.maxDistance = distance * 10;
controls.target.copy(center);
camera.near = distance / 100;
camera.far = distance * 100;
camera.updateProjectionMatrix();
camera.position.copy(controls.target).sub(direction);
controls.update();
}
function load_cad_model(path_model) {
console.log("Loading CAD model " + path_model)
const loader = new FBXLoader();
loader.load(path_model, function (object) {
console.log("Loaded CAD model " + path_model)
_cad_model = object;
_scene.add(object);
// Set the camera position to center of bbox
_cad_model_bbox = getCompoundBoundingBox(object)
let cameraTarget = new THREE.Vector3();
_cad_model_bbox.getCenter(cameraTarget);
object.localToWorld(cameraTarget);
_cameraControls.target = cameraTarget;
fitCameraToSelection(camera, _cameraControls, object.children)
});
}
function setup_scene() {
//Sets up the renderer to the same size as a DOM element
//and attaches it to that element
viewport.appendChild(renderer.domElement);
_cameraControls = new OrbitControls(camera, renderer.domElement);
_cameraControls.movementSpeed = 1;
_cameraControls.domElement = viewport;
_raycaster = new THREE.Raycaster();
_raycaster.params.Points.threshold = 0.1;
camera.position.set(10, 10, 10);
camera.lookAt(new THREE.Vector3(0, 0, 0));
pointLight.position.set(10, 50, 150);
_scene.add(camera);
_scene.add(pointLight);
_scene.add(ambientLight);
initializeTrackingMarker();
resizeCanvas()
}
function makeTextSprite(message, parameters) {
if (parameters === undefined) parameters = {};
let fontface = parameters.hasOwnProperty("fontface") ?
parameters["fontface"] : "Arial";
let fontsize = parameters.hasOwnProperty("fontsize") ?
parameters["fontsize"] : 18;
let borderThickness = parameters.hasOwnProperty("borderThickness") ?
parameters["borderThickness"] : 4;
let borderColor = parameters.hasOwnProperty("borderColor") ?
parameters["borderColor"] : { r: 0, g: 0, b: 0, a: 1.0 };
let backgroundColor = parameters.hasOwnProperty("backgroundColor") ?
parameters["backgroundColor"] : { r: 255, g: 255, b: 255, a: 1.0 };
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
context.font = "Bold " + fontsize + "px " + fontface;
// get size data (height depends only on font size)
let metrics = context.measureText(message);
let textWidth = metrics.width;
// background color
context.fillStyle = "rgba(" + backgroundColor.r + "," + backgroundColor.g + ","
+ backgroundColor.b + "," + backgroundColor.a + ")";
// border color
context.strokeStyle = "rgba(" + borderColor.r + "," + borderColor.g + ","
+ borderColor.b + "," + borderColor.a + ")";
context.lineWidth = borderThickness;
roundRect(context, borderThickness / 2, borderThickness / 2, textWidth + borderThickness, fontsize * 1.4 + borderThickness, 6);
// 1.4 is extra height factor for text below baseline: g,j,p,q.
// text color
context.fillStyle = "rgba(0, 0, 0, 1.0)";
context.fillText(message, borderThickness, fontsize + borderThickness);
// canvas contents will be used for a texture
let texture = new THREE.Texture(canvas)
texture.needsUpdate = true;
let spriteMaterial = new THREE.SpriteMaterial({ map: texture });
let sprite = new THREE.Sprite(spriteMaterial);
sprite.scale.set(500, 250, 1.0);
return sprite;
}
// function for drawing rounded rectangles
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
function initializeTrackingMarker() {
const sphereGeometry = new THREE.SphereGeometry(150);
_trackingMarker = new THREE.Mesh(sphereGeometry);
_trackingMarker.material.color = { 'r': 1.0, 'g': 0, 'b': 0 };
_scene.add(_trackingMarker);
}
function updateTrackingMarker(new_position) {
_trackingMarker.position.copy(new_position);
}
function updateGCPLabels() {
for (var gcp_id in _gcps) {
const sphere = _gcps[gcp_id]["marker"];
const sprite = _gcps[gcp_id]["label"]
const director_vector = new THREE.Vector3();
director_vector.subVectors(camera.position, sphere.position);
const cam_to_gcp_distance = director_vector.length();
sprite.scale.set(cam_to_gcp_distance / 5, cam_to_gcp_distance / 10, 1.0);
sprite.position.addVectors(sphere.position, director_vector.setLength(300.0));
sprite.center.set(0, 1)
}
}
function create_or_update_gcp(gcp_id, xyz, reprojection_xyz, color) {
const gcp_position = new THREE.Vector3(xyz[0], xyz[1], xyz[2]);
const gcp_reprojection = reprojection_xyz ? new THREE.Vector3(reprojection_xyz[0], reprojection_xyz[1], reprojection_xyz[2]) : gcp_position;
let sphere;
let sprite;
let line;
// check if the property/key is defined in the object itself, not in parent
if (!_gcps.hasOwnProperty(gcp_id)) {
// A sphere marks the location of the annotation
const sphereGeometry = new THREE.SphereGeometry(50);
sphere = new THREE.Mesh(sphereGeometry);
_scene.add(sphere);
// A label
sprite = makeTextSprite(gcp_id, { fontsize: 32, fontface: "Georgia" });
_scene.add(sprite);
// A line from the annotation position to its reprojection
const line_material = new THREE.LineBasicMaterial();
const geometry = new THREE.BufferGeometry();
line = new THREE.Line(geometry, line_material);
_scene.add(line);
_gcps[gcp_id] = { "marker": sphere, "label": sprite, "reprojectionLine": line };
}
else {
sphere = _gcps[gcp_id]["marker"];
sprite = _gcps[gcp_id]["label"]
line = _gcps[gcp_id]["reprojectionLine"]
}
sphere.position.copy(gcp_position);
sphere.material.color = { 'r': color[0] / 255.0, 'g': color[1] / 255.0, 'b': color[2] / 255.0 };
sphere.material.needsupdate = true;
line.geometry.setFromPoints([gcp_position, gcp_reprojection]);
line.material.color = { 'r': color[0] / 255.0, 'g': color[1] / 255.0, 'b': color[2] / 255.0 };
}
function update_gcps(annotations) {
for (var gcp_id in _gcps) {
if (!annotations.hasOwnProperty(gcp_id)) {
_scene.remove(_gcps[gcp_id]["marker"])
_scene.remove(_gcps[gcp_id]["label"])
_scene.remove(_gcps[gcp_id]["reprojectionLine"])
delete _gcps[gcp_id];
}
}
for (var gcp_id in annotations) {
create_or_update_gcp(
gcp_id,
annotations[gcp_id]["coordinates"],
annotations[gcp_id].hasOwnProperty("reprojection") ? annotations[gcp_id]["reprojection"] : null,
annotations[gcp_id]["color"],
);
}
updateGCPLabels();
}
function point_camera_at_xy(point) {
// Replace Z with the maximum Z on the whole model
point.y = _cad_model_bbox.max.y
point_camera_at_xyz(point);
}
function point_camera_at_xyz(point) {
console.log(point);
_cameraControls.target.copy(point);
updateTrackingMarker(point);
_cameraControls.update();
}
function onSyncHandler(data) {
update_gcps(data.annotations);
}
function initialize() {
viewport = document.getElementById('viewport');
setup_scene();
load_cad_model(window.location.href + '/model')
viewport.addEventListener('pointerdown', onViewportMouseClick, false);
window.addEventListener("resize", onWindowResize);
const sse = initialize_event_source([
{ event: "sync", handler: onSyncHandler },
{ event: "move_camera", handler: point_camera_at_xy },
]);
window.onunload = () => {
console.log("Unloaded CAD window? closing sse")
sse.close();
}
// call update
update();
post_json({ event: "init" });
}
function remove_point_observation() {
post_json({ event: "remove_point_observation" });
}
function add_or_update_point_observation(xyz) {
const data = {
xyz: xyz,
event: "add_or_update_point_observation",
};
post_json(data);
}
function onViewportMouseClick(event) {
if (!event.ctrlKey && !event.altKey) {
return
}
event.preventDefault();
if (event.ctrlKey) {
switch (event.button) {
case 0: // left
const pickposition = setPickPosition(event)
_raycaster.setFromCamera(pickposition, camera);
const intersections = _raycaster.intersectObject(_cad_model, true);
const intersection = (intersections.length) > 0 ? intersections[0] : null;
if (intersection !== null) {
const xyz = [intersection.point['x'], intersection.point['y'], intersection.point['z']];
add_or_update_point_observation(xyz);
}
break;
case 1: // middle
break;
case 2: // right
remove_point_observation();
break;
}
}
else { // Alt is pressed
switch (event.button) {
case 0: // left
const pickposition = setPickPosition(event)
_raycaster.setFromCamera(pickposition, camera);
const intersections = _raycaster.intersectObject(_cad_model, true);
const intersection = (intersections.length) > 0 ? intersections[0] : null;
if (intersection !== null) {
point_camera_at_xyz(intersection.point);
}
break;
case 1: // middle
break;
case 2: // right
break;
}
}
}
function getCanvasRelativePosition(event) {
const rect = viewport.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * window.innerWidth / rect.width,
y: (event.clientY - rect.top) * window.innerHeight / rect.height,
};
}
function setPickPosition(event) {
const pos = getCanvasRelativePosition(event);
return {
x: (pos.x / window.innerWidth) * 2 - 1,
y: (pos.y / window.innerHeight) * -2 + 1, // note we flip Y
};
}
// function onWindowResize() {
// camera.aspect = window.innerWidth / window.innerHeight;
// camera.updateProjectionMatrix();
// renderer.setSize(window.innerWidth, window.innerHeight);
// }
//a cross-browser method for efficient animation, more info at:
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimFrame = (function () {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
//----Update----//
function update() {
// Update GCP labels so that they track the camera
updateGCPLabels();
_cameraControls.update(1);
pointLight.position.copy(camera.position);
draw();
// requests the browser to call update again at it's own pace
requestAnimFrame(update);
}
function draw() {
renderer.render(_scene, camera);
}