diff --git a/cylinder.js b/cylinder.js index 2f7d1b6..d4378a9 100644 --- a/cylinder.js +++ b/cylinder.js @@ -7,6 +7,7 @@ import { clearRangeImages, } from "./interaction"; import { setSliderValue } from "./panel"; +import { saveCylinder } from "./inspect"; var radius = 0.5; @@ -182,6 +183,10 @@ function paintRange() { paintRangeImages(rangeImages); } +function saveCylinderToInspectMode() { + saveCylinder(centerPoint, radius, height, vector); +} + export { openCylindricalImages, createCylinder, @@ -189,4 +194,5 @@ export { applyCylindricalRadius, setScene, applyCylindricalHeight, + saveCylinderToInspectMode, }; diff --git a/inspect.js b/inspect.js new file mode 100644 index 0000000..7b66327 --- /dev/null +++ b/inspect.js @@ -0,0 +1,319 @@ +import * as THREE from "three"; +import { getAllImages } from "./single-image-loader"; + +import { create, all } from "mathjs"; + +const math = create(all, {}); + +var scene; +var spheres = []; +var cylinders = []; +var planes = []; + +const HOVER_COLOR = 0x66aaaa; +const NORMAL_COLOR = 0xaaaaaa; + +function setScene(s) { + scene = s; +} + +function saveSphere(C, r) { + const geometry = new THREE.SphereGeometry(0.05, 10, 10); + const material = new THREE.MeshBasicMaterial({ + color: NORMAL_COLOR, + }); + const sphereObject = new THREE.Mesh(geometry, material); + + scene.add(sphereObject); + + sphereObject.position.set(C.x, C.y, C.z); + sphereObject.userData.radius = r; + sphereObject.name = "Sphere" + spheres.length; + sphereObject.visible = false; + spheres.push(sphereObject); +} + +function saveCylinder(C, r, h, V) { + const geometry = new THREE.CylinderGeometry(0.05, 0.05, 0.1, 10).rotateX(Math.PI / 2); + const material = new THREE.MeshBasicMaterial({ + color: NORMAL_COLOR, + }); + + const cylinderObject = new THREE.Mesh(geometry, material); + + scene.add(cylinderObject); + + cylinderObject.lookAt(V.normalize()); + cylinderObject.position.set(C.x, C.y, C.z); + + cylinderObject.userData.radius = r; + cylinderObject.userData.height = h; + cylinderObject.userData.vector = V; + + cylinderObject.name = "Cylinder" + cylinders.length; + cylinderObject.visible = false; + cylinders.push(cylinderObject); +} + +function savePlane(abstractPlane, planeHeight, planeWidth, planeDistance, centerPoint, t, b) { + var coplanarPoint = abstractPlane.coplanarPoint(new THREE.Vector3().copy(centerPoint)); + var focalPoint = new THREE.Vector3().addVectors(coplanarPoint, abstractPlane.normal); + + const boxGeometry = new THREE.BoxGeometry(0.1, 0.1, 0.02, 10, 10, 10); + const boxMaterial = new THREE.MeshBasicMaterial({ + color: NORMAL_COLOR, + }); + const box = new THREE.Mesh(boxGeometry, boxMaterial); + + scene.add(box); + box.lookAt(focalPoint); + box.position.set(centerPoint.x, centerPoint.y, centerPoint.z); + + box.userData.abstractPlane = abstractPlane; + box.userData.height = planeHeight; + box.userData.width = planeWidth; + box.userData.distance = planeDistance; + box.userData.t = t; + box.userData.b = b; + + box.name = "Plane" + planes.length; + box.visible = false; + planes.push(box); +} + +function setFiguresVisibility(show) { + if (spheres.length > 0) spheres.forEach((s) => (s.visible = show)); + if (cylinders.length > 0) cylinders.forEach((c) => (c.visible = show)); + if (planes.length > 0) planes.forEach((p) => (p.visible = show)); +} + +function openFigure(objects) { + let object = firstFigure(objects); + if (!object) return; + if (object.name.startsWith("Sphere") && spheres.length > 0) + spheres.forEach((s) => { + if (s.name == object.name) openSphericalImages(s.position, s.userData.radius); + }); + if (object.name.startsWith("Cylinder") && cylinders.length > 0) + cylinders.forEach((c) => { + if (c.name == object.name) + openCylindricalImages( + c.userData.vector, + c.position, + c.userData.radius, + c.userData.height + ); + }); + if (object.name.startsWith("Plane") && planes.length > 0) + planes.forEach((p) => { + if (p.name == object.name) + openPlaneImages( + p.userData.abstractPlane, + p.userData.height, + p.userData.width, + p.userData.distance, + p.position, + p.userData.t, + p.userData.b + ); + }); +} + +function hoveringFigure(objects) { + let object = firstFigure(objects); + if (!object) object = { name: "" }; + if (spheres.length > 0) + spheres.forEach((s) => { + if (s.name == object.name) s.material.color.setHex(HOVER_COLOR); + else s.material.color.setHex(NORMAL_COLOR); + }); + if (cylinders.length > 0) + cylinders.forEach((c) => { + if (c.name == object.name) c.material.color.setHex(HOVER_COLOR); + else c.material.color.setHex(NORMAL_COLOR); + }); + if (planes.length > 0) + planes.forEach((p) => { + if (p.name == object.name) p.material.color.setHex(HOVER_COLOR); + else p.material.color.setHex(NORMAL_COLOR); + }); +} + +function firstFigure(objects) { + for (let i = 0; i < objects.length; i++) { + const o = objects[i]; + if ( + o.object.name.startsWith("Sphere") || + o.object.name.startsWith("Cylinder") || + o.object.name.startsWith("Plane") + ) + return o.object; + } + return null; +} + +function openSphericalImages(C, radius) { + let images = getAllImages(); + let json = []; + + images.forEach((object) => { + let P_inter = object.userData.intersection; + let P_real = object.position; + if (P_inter == null) return; + if (C.distanceTo(P_real) < radius) { + const real_pos = getSphere2DCoords(P_real, C); + const inter_pos = getSphere2DCoords(P_inter, C); + json.push({ + name: object.name, + x_real: real_pos.x, + y_real: real_pos.y, + x_inter: inter_pos.x, + y_inter: inter_pos.y, + isLandscape: object.userData.isLandscape, + heightToWidthRatio: object.userData.heightToWidthRatio, + zoom: object.userData.zoom, + }); + } + }); + + let jsonContent = JSON.stringify(json); + localStorage.setItem("images", jsonContent); + const url = "openseadragon.html?mode=spherical"; + + window.open(url, "_blank"); +} + +function getSphere2DCoords(P, C) { + const V = new THREE.Vector3().subVectors(P, C).normalize(); + const phi = math.acos(V.y); + const theta = math.atan2(V.x, V.z); + return { x: theta, y: phi }; +} + +function openCylindricalImages(vector, centerPoint, radius, height) { + let images = getAllImages(); + let json = []; + + const V = new THREE.Vector3(vector.x, vector.y, vector.z).multiplyScalar(height / 2); + const endPoint1 = new THREE.Vector3(centerPoint.x, centerPoint.y, centerPoint.z).add(V); + const endPoint2 = new THREE.Vector3(centerPoint.x, centerPoint.y, centerPoint.z).sub(V); + const infiniteVector = new THREE.Vector3(V.x, V.y, V.z).multiplyScalar(1000); + const point1 = new THREE.Vector3(centerPoint.x, centerPoint.y, centerPoint.z).add( + infiniteVector + ); + const point2 = new THREE.Vector3(centerPoint.x, centerPoint.y, centerPoint.z).sub( + infiniteVector + ); + + const segment = new THREE.Line3(endPoint1, endPoint2); + const infiniteLine = new THREE.Line3(point1, point2); + + var originProjected = new THREE.Vector3(); + const origin = new THREE.Vector3(0, 0, 0); + infiniteLine.closestPointToPoint(origin, false, originProjected); + const originVector = new THREE.Vector3().subVectors(originProjected, origin).normalize(); + + images.forEach((object) => { + const P_real = new THREE.Vector3().copy(object.position); + const P_inter = new THREE.Vector3().copy(object.userData.intersection); + if (P_inter == null) return; + + var lProjected = new THREE.Vector3(); + var sProjected = new THREE.Vector3(); + infiniteLine.closestPointToPoint(P_real, true, lProjected); + segment.closestPointToPoint(P_real, true, sProjected); + const lineDistance = lProjected.distanceTo(P_real).toFixed(5); + const segmentDistance = sProjected.distanceTo(P_real).toFixed(5); + if (lineDistance < radius && lineDistance == segmentDistance) { + const real_pos = getCylinder2DCoords(P_real, segment, originVector, centerPoint); + const inter_pos = getCylinder2DCoords(P_inter, segment, originVector, centerPoint); + json.push({ + name: object.name, + x_real: real_pos.x, + y_real: real_pos.y, + x_inter: inter_pos.x, + y_inter: inter_pos.y, + isLandscape: object.userData.isLandscape, + heightToWidthRatio: object.userData.heightToWidthRatio, + zoom: object.userData.zoom, + }); + } + }); + + let jsonContent = JSON.stringify(json); + localStorage.setItem("images", jsonContent); + const url = "openseadragon.html?mode=cylindrical"; + + window.open(url, "_blank"); +} + +function getCylinder2DCoords(P, segment, originVector, centerPoint) { + var sProjected = new THREE.Vector3(); + segment.closestPointToPoint(P, true, sProjected); + const pointVector = new THREE.Vector3().subVectors(sProjected, P).normalize(); + const x = originVector.angleTo(pointVector); + const y = centerPoint.distanceTo(sProjected); + return { x: x, y: -y }; +} + +function openPlaneImages(abstractPlane, planeHeight, planeWidth, planeDistance, centerPoint, t, b) { + let images = getAllImages(); + let json = []; + + images.forEach((object) => { + const P_real = object.position; + const P_inter = object.userData.intersection; + if (P_inter == null) return; + var P2 = new THREE.Vector3(); + abstractPlane.projectPoint(P_real, P2); + + const real_pos = getPlane2DCoords(P_real, centerPoint, abstractPlane, t, b); + + if ( + P_real.distanceTo(P2) < planeDistance / 2 && + math.abs(real_pos.x) < planeWidth / 2 && + math.abs(real_pos.y) < planeHeight / 2 + ) { + const inter_pos = getPlane2DCoords(P_inter, centerPoint, abstractPlane, t, b); + json.push({ + name: object.name, + x_real: real_pos.x, + y_real: real_pos.y, + x_inter: inter_pos.x, + y_inter: inter_pos.y, + isLandscape: object.userData.isLandscape, + heightToWidthRatio: object.userData.heightToWidthRatio, + zoom: object.userData.zoom, + }); + } + }); + + let jsonContent = JSON.stringify(json); + localStorage.setItem("images", jsonContent); + const url = "openseadragon.html?mode=plane"; + + window.open(url, "_blank"); +} + +function getPlane2DCoords(P, centerPoint, abstractPlane, t, b) { + var P2 = new THREE.Vector3(); + abstractPlane.projectPoint(P, P2); + return worldCoordsToPlaneCoords(P, centerPoint, t, b); +} + +function worldCoordsToPlaneCoords(P, centerPoint, t, b) { + const V = new THREE.Vector3().subVectors(P, centerPoint); + const tv = new THREE.Vector3().copy(V).dot(t); + const bv = new THREE.Vector3().copy(V).dot(b); + return { x: -tv, y: bv }; +} + +export { + saveSphere, + saveCylinder, + savePlane, + setFiguresVisibility, + setScene, + openFigure, + hoveringFigure, +}; diff --git a/interaction.js b/interaction.js index 8b7620d..92541f4 100644 --- a/interaction.js +++ b/interaction.js @@ -11,6 +11,8 @@ import { setMultiSettings, resetUI, } from "./panel.js"; +import { openFigure, hoveringFigure } from "./inspect.js"; + import { create, all } from "mathjs"; const math = create(all, {}); @@ -24,6 +26,7 @@ var camera; var scene; var controls; var mode = "multi"; +var authoringMode = true; const HOVER_COLOR = 0xccffff; const SELECTION_COLOR = 0xd6b4fc; @@ -81,7 +84,13 @@ function onClick() { var intersects = raycaster.intersectObject(scene, true); if (intersects.length == 0) return; - console.log(intersects[0]); + + if (!authoringMode) { + openFigure(intersects); + render(); + return; + } + var object = firstImage(intersects); if (object == null) return; @@ -149,6 +158,11 @@ function onHover() { raycaster.setFromCamera(mouse, camera); var intersects = raycaster.intersectObject(scene, true); if (intersects.length > 0) { + if (!authoringMode) { + hoveringFigure(intersects); + render(); + return; + } var object = firstImage(intersects); if (object != null) { // New Hover @@ -229,6 +243,10 @@ function setSelectionMode(m) { mode = m; } +function setAuthoringMode(m) { + authoringMode = m; +} + export { addInteraction, openImagesToOpenSeaDragon, @@ -238,4 +256,5 @@ export { clearRangeImages, paintRangeImages, setSelectionMode, + setAuthoringMode, }; diff --git a/main.js b/main.js index cf5a324..e49e335 100644 --- a/main.js +++ b/main.js @@ -6,6 +6,7 @@ import { loadImages } from "./multiple-image-loader.js"; import { addInteraction } from "./interaction.js"; import { createPanel } from "./panel.js"; import { setIntersectionPosition } from "./single-image-loader.js"; +import { setScene } from "./inspect.js"; //INIT THREE.Cache.enabled = true; @@ -105,6 +106,8 @@ await loadImages( addInteraction(camera, scene, controls); +setScene(scene); + window.addEventListener("resize", onWindowResize, false); function onWindowResize() { diff --git a/panel.js b/panel.js index 6142e8e..641af55 100644 --- a/panel.js +++ b/panel.js @@ -1,8 +1,18 @@ import { GUI } from "three/addons/libs/lil-gui.module.min.js"; -import { setSize, setOffset, setWireframe } from "./single-image-loader.js"; -import { openImagesToOpenSeaDragon, clearSelection, setSelectionMode } from "./interaction.js"; +import { setSize, setOffset, setWireframe, setImageVisibility } from "./single-image-loader.js"; +import { + openImagesToOpenSeaDragon, + clearSelection, + setSelectionMode, + setAuthoringMode, +} from "./interaction.js"; -import { applySphericalRadius, openSphericalImages, cancelSphere } from "./sphere.js"; +import { + applySphericalRadius, + openSphericalImages, + cancelSphere, + saveSphereToInspectMode, +} from "./sphere.js"; import { cancelPlane, @@ -10,6 +20,7 @@ import { changePlaneHeight, changePlaneWidth, openPlane, + savePlaneToInspectMode, } from "./plane.js"; import { @@ -17,10 +28,14 @@ import { applyCylindricalRadius, openCylindricalImages, applyCylindricalHeight, + saveCylinderToInspectMode, } from "./cylinder.js"; +import { setFiguresVisibility } from "./inspect.js"; + const panel = new GUI({ width: 290 }); +const mode_folder = panel.addFolder("Mode"); const folder1 = panel.addFolder("Image Settings"); const folder2 = panel.addFolder("Individual Selection"); const folder3 = panel.addFolder("Sphere"); @@ -30,6 +45,44 @@ const folder5 = panel.addFolder("Cylinder"); const infoElement = document.getElementById("info"); function createPanel() { + let mode_settings = { + "Change to Authoring mode": function () { + hideController("Mode", "Change to Authoring mode"); + showController("Mode", "Change to Inspect mode"); + hideFolder("Individual Selection"); + showFolder("Image Settings"); + showFolder("Sphere"); + showFolder("Plane"); + showFolder("Cylinder"); + + resetMessage(); + setSelectionMode("multi"); + + setImageVisibility(true); + setFiguresVisibility(false); + setAuthoringMode(true); + }, + "Change to Inspect mode": function () { + showController("Mode", "Change to Authoring mode"); + hideController("Mode", "Change to Inspect mode"); + hideFolder("Individual Selection"); + hideFolder("Image Settings"); + hideFolder("Sphere"); + hideFolder("Plane"); + hideFolder("Cylinder"); + + setMessage("Open predefined figures"); + setSelectionMode("multi"); + + cancelSphere(); + cancelPlane(); + cancelCylinder(); + clearSelection(); + setImageVisibility(false); + setFiguresVisibility(true); + setAuthoringMode(false); + }, + }; let settings1 = { "Image size": 1.0, "Camera image separation": 0.2, @@ -63,6 +116,7 @@ function createPanel() { hideController("Sphere", "Create Sphere"); hideController("Sphere", "Radius"); hideController("Sphere", "Open in 2D viewer"); + hideController("Sphere", "Save figure to Inspect mode"); hideFolder("Individual Selection"); hideFolder("Plane"); @@ -75,6 +129,9 @@ function createPanel() { openSphericalImages(); }, Radius: 0.5, + "Save figure to Inspect mode": function () { + saveSphereToInspectMode(); + }, Cancel: function () { cancelSphere(); @@ -82,6 +139,7 @@ function createPanel() { showController("Sphere", "Create Sphere"); hideController("Sphere", "Radius"); hideController("Sphere", "Open in 2D viewer"); + hideController("Sphere", "Save figure to Inspect mode"); showFolder("Plane"); showFolder("Cylinder"); @@ -100,6 +158,7 @@ function createPanel() { hideController("Plane", "Open in 2D viewer"); showController("Plane", "Cancel"); hideController("Plane", "Create Plane"); + hideController("Plane", "Save figure to Inspect mode"); hideFolder("Individual Selection"); hideFolder("Sphere"); @@ -108,6 +167,9 @@ function createPanel() { setMessage("Select 3 images"); setSelectionMode("plane"); }, + "Save figure to Inspect mode": function () { + savePlaneToInspectMode(); + }, Cancel: function () { cancelPlane(); hideController("Plane", "Width"); @@ -116,6 +178,7 @@ function createPanel() { hideController("Plane", "Open in 2D viewer"); hideController("Plane", "Cancel"); showController("Plane", "Create Plane"); + hideController("Plane", "Save figure to Inspect mode"); showFolder("Sphere"); showFolder("Cylinder"); @@ -139,6 +202,7 @@ function createPanel() { hideController("Cylinder", "Open in 2D viewer"); showController("Cylinder", "Cancel"); hideController("Cylinder", "Create Cylinder"); + hideController("Cylinder", "Save figure to Inspect mode"); hideFolder("Individual Selection"); hideFolder("Sphere"); @@ -152,6 +216,9 @@ function createPanel() { }, Radius: 0.5, Height: 1.0, + "Save figure to Inspect mode": function () { + saveCylinderToInspectMode(); + }, Cancel: function () { cancelCylinder(); @@ -160,6 +227,7 @@ function createPanel() { hideController("Cylinder", "Open in 2D viewer"); hideController("Cylinder", "Cancel"); showController("Cylinder", "Create Cylinder"); + hideController("Cylinder", "Save figure to Inspect mode"); showFolder("Sphere"); showFolder("Plane"); @@ -170,6 +238,9 @@ function createPanel() { }, }; + mode_folder.add(mode_settings, "Change to Inspect mode"); + mode_folder.add(mode_settings, "Change to Authoring mode"); + folder1.add(settings1, "Image size", 0.0, 5.0, 0.01).onChange(setSize); folder1.add(settings1, "Camera image separation", 0.01, 2.0, 0.01).onChange(setOffset); folder1.add(settings1, "Image wireframe").onChange(setWireframe); @@ -180,6 +251,7 @@ function createPanel() { folder3.add(settings3, "Create Sphere"); folder3.add(settings3, "Radius", 0.0, 5.0, 0.01).onChange(applySphericalRadius); folder3.add(settings3, "Open in 2D viewer"); + folder3.add(settings3, "Save figure to Inspect mode"); folder3.add(settings3, "Cancel"); folder4.add(settings4, "Create Plane"); @@ -187,12 +259,14 @@ function createPanel() { folder4.add(settings4, "Height", 0.0, 10.0, 0.01).onChange(changePlaneHeight); folder4.add(settings4, "Max distance", 0.0, 5.0, 0.01).onChange(changePlaneDistance); folder4.add(settings4, "Open in 2D viewer"); + folder4.add(settings4, "Save figure to Inspect mode"); folder4.add(settings4, "Cancel"); folder5.add(settings5, "Create Cylinder"); folder5.add(settings5, "Radius", 0.0, 5.0, 0.01).onChange(applyCylindricalRadius); folder5.add(settings5, "Height", 0.0, 5.0, 0.01).onChange(applyCylindricalHeight); folder5.add(settings5, "Open in 2D viewer"); + folder5.add(settings5, "Save figure to Inspect mode"); folder5.add(settings5, "Cancel"); // Initialize panel @@ -200,17 +274,21 @@ function createPanel() { //hideFolder("Sphere"); //hideFolder("Plane"); //hideFolder("Cylinder"); + hideController("Mode", "Change to Authoring mode"); hideController("Sphere", "Radius"); hideController("Sphere", "Open in 2D viewer"); + hideController("Sphere", "Save figure to Inspect mode"); hideController("Sphere", "Cancel"); hideController("Plane", "Width"); hideController("Plane", "Height"); hideController("Plane", "Max distance"); hideController("Plane", "Open in 2D viewer"); + hideController("Plane", "Save figure to Inspect mode"); hideController("Plane", "Cancel"); hideController("Cylinder", "Radius"); hideController("Cylinder", "Height"); hideController("Cylinder", "Open in 2D viewer"); + hideController("Cylinder", "Save figure to Inspect mode"); hideController("Cylinder", "Cancel"); } @@ -266,6 +344,7 @@ function setSphereSettings() { hideController("Sphere", "Create Sphere"); showController("Sphere", "Radius"); showController("Sphere", "Open in 2D viewer"); + showController("Sphere", "Save figure to Inspect mode"); setMessage(""); } @@ -276,6 +355,7 @@ function setCylinderSettings() { showController("Cylinder", "Open in 2D viewer"); showController("Cylinder", "Cancel"); hideController("Cylinder", "Create Cylinder"); + showController("Cylinder", "Save figure to Inspect mode"); setMessage(""); } @@ -287,6 +367,7 @@ function setPlaneSettings() { showController("Plane", "Open in 2D viewer"); showController("Plane", "Cancel"); hideController("Plane", "Create Plane"); + showController("Plane", "Save figure to Inspect mode"); setMessage(""); } diff --git a/plane.js b/plane.js index 9995e2f..c862216 100644 --- a/plane.js +++ b/plane.js @@ -6,6 +6,8 @@ import { paintRangeImages, clearRangeImages, } from "./interaction"; +import { savePlane } from "./inspect"; + import { create, all } from "mathjs"; const math = create(all, {}); @@ -139,7 +141,7 @@ function openPlane() { const real_pos = get2DCoords(P_real); if ( - P.distanceTo(P2) < planeDistance / 2 && + P_real.distanceTo(P2) < planeDistance / 2 && math.abs(real_pos.x) < planeWidth / 2 && math.abs(real_pos.y) < planeHeight / 2 ) { @@ -208,6 +210,10 @@ function paintRange() { paintRangeImages(rangeImages); } +function savePlaneToInspectMode() { + savePlane(abstractPlane, planeHeight, planeWidth, planeDistance, centerPoint, t, b); +} + export { setScene, createPlane, @@ -216,4 +222,5 @@ export { changePlaneHeight, changePlaneWidth, openPlane, + savePlaneToInspectMode, }; diff --git a/single-image-loader.js b/single-image-loader.js index 4c555c6..7b64db7 100644 --- a/single-image-loader.js +++ b/single-image-loader.js @@ -152,4 +152,16 @@ function getIntersectionPosition(scene, position, direction) { return position; } -export { loadImage, setSize, setOffset, getAllImages, setWireframe, setIntersectionPosition }; +function setImageVisibility(show) { + images.forEach((i) => (i.visible = show)); +} + +export { + loadImage, + setSize, + setOffset, + getAllImages, + setWireframe, + setIntersectionPosition, + setImageVisibility, +}; diff --git a/sphere.js b/sphere.js index 87a7cf6..64aa7f2 100644 --- a/sphere.js +++ b/sphere.js @@ -7,6 +7,7 @@ import { clearRangeImages, } from "./interaction"; import { create, all } from "mathjs"; +import { saveSphere } from "./inspect"; const math = create(all, {}); @@ -112,4 +113,15 @@ function paintRange() { paintRangeImages(rangeImages); } -export { openSphericalImages, createSphere, cancelSphere, applySphericalRadius, setScene }; +function saveSphereToInspectMode() { + saveSphere(C, radius); +} + +export { + openSphericalImages, + createSphere, + cancelSphere, + applySphericalRadius, + setScene, + saveSphereToInspectMode, +};