Annotation.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. import * as THREE from "../libs/three.js/build/three.module.js";
  2. import {Action} from "./Actions.js";
  3. import {Utils} from "./utils.js";
  4. import {EventDispatcher} from "./EventDispatcher.js";
  5. export class Annotation extends EventDispatcher {
  6. constructor (args = {}) {
  7. super();
  8. this.scene = null;
  9. this._title = args.title || 'No Title';
  10. this._description = args.description || '';
  11. this.offset = new THREE.Vector3();
  12. this.uuid = THREE.Math.generateUUID();
  13. if (!args.position) {
  14. this.position = null;
  15. } else if (args.position.x != null) {
  16. this.position = args.position;
  17. } else {
  18. this.position = new THREE.Vector3(...args.position);
  19. }
  20. this.cameraPosition = (args.cameraPosition instanceof Array)
  21. ? new THREE.Vector3().fromArray(args.cameraPosition) : args.cameraPosition;
  22. this.cameraTarget = (args.cameraTarget instanceof Array)
  23. ? new THREE.Vector3().fromArray(args.cameraTarget) : args.cameraTarget;
  24. this.radius = args.radius;
  25. this.view = args.view || null;
  26. this.keepOpen = false;
  27. this.descriptionVisible = false;
  28. this.showDescription = true;
  29. this.actions = args.actions || [];
  30. this.isHighlighted = false;
  31. this._visible = true;
  32. this.__visible = true;
  33. this._display = true;
  34. this._expand = false;
  35. this.collapseThreshold = [args.collapseThreshold, 100].find(e => e !== undefined);
  36. this.children = [];
  37. this.parent = null;
  38. this.boundingBox = new THREE.Box3();
  39. let iconClose = exports.resourcePath + '/icons/close.svg';
  40. this.domElement = $(`
  41. <div class="annotation" oncontextmenu="return false;">
  42. <div class="annotation-titlebar">
  43. <span class="annotation-label"></span>
  44. </div>
  45. <div class="annotation-description">
  46. <span class="annotation-description-close">
  47. <img src="${iconClose}" width="16px">
  48. </span>
  49. <span class="annotation-description-content">${this._description}</span>
  50. </div>
  51. </div>
  52. `);
  53. this.elTitlebar = this.domElement.find('.annotation-titlebar');
  54. this.elTitle = this.elTitlebar.find('.annotation-label');
  55. this.elTitle.append(this._title);
  56. this.elDescription = this.domElement.find('.annotation-description');
  57. this.elDescriptionClose = this.elDescription.find('.annotation-description-close');
  58. // this.elDescriptionContent = this.elDescription.find(".annotation-description-content");
  59. this.clickTitle = () => {
  60. if(this.hasView()){
  61. this.moveHere(this.scene.getActiveCamera());
  62. }
  63. this.dispatchEvent({type: 'click', target: this});
  64. };
  65. this.elTitle.click(this.clickTitle);
  66. this.actions = this.actions.map(a => {
  67. if (a instanceof Action) {
  68. return a;
  69. } else {
  70. return new Action(a);
  71. }
  72. });
  73. for (let action of this.actions) {
  74. action.pairWith(this);
  75. }
  76. let actions = this.actions.filter(
  77. a => a.showIn === undefined || a.showIn.includes('scene'));
  78. for (let action of actions) {
  79. let elButton = $(`<img src="${action.icon}" class="annotation-action-icon">`);
  80. this.elTitlebar.append(elButton);
  81. elButton.click(() => action.onclick({annotation: this}));
  82. }
  83. this.elDescriptionClose.hover(
  84. e => this.elDescriptionClose.css('opacity', '1'),
  85. e => this.elDescriptionClose.css('opacity', '0.5')
  86. );
  87. this.elDescriptionClose.click(e => this.setHighlighted(false));
  88. // this.elDescriptionContent.html(this._description);
  89. this.domElement.mouseenter(e => this.setHighlighted(true));
  90. this.domElement.mouseleave(e => this.setHighlighted(false));
  91. this.domElement.on('touchstart', e => {
  92. this.setHighlighted(!this.isHighlighted);
  93. });
  94. this.display = false;
  95. //this.display = true;
  96. }
  97. installHandles(viewer){
  98. if(this.handles !== undefined){
  99. return;
  100. }
  101. let domElement = $(`
  102. <div style="position: absolute; left: 300; top: 200; pointer-events: none">
  103. <svg width="300" height="600">
  104. <line x1="0" y1="0" x2="1200" y2="200" style="stroke: black; stroke-width:2" />
  105. <circle cx="50" cy="50" r="4" stroke="black" stroke-width="2" fill="gray" />
  106. <circle cx="150" cy="50" r="4" stroke="black" stroke-width="2" fill="gray" />
  107. </svg>
  108. </div>
  109. `);
  110. let svg = domElement.find("svg")[0];
  111. let elLine = domElement.find("line")[0];
  112. let elStart = domElement.find("circle")[0];
  113. let elEnd = domElement.find("circle")[1];
  114. let setCoordinates = (start, end) => {
  115. elStart.setAttribute("cx", `${start.x}`);
  116. elStart.setAttribute("cy", `${start.y}`);
  117. elEnd.setAttribute("cx", `${end.x}`);
  118. elEnd.setAttribute("cy", `${end.y}`);
  119. elLine.setAttribute("x1", start.x);
  120. elLine.setAttribute("y1", start.y);
  121. elLine.setAttribute("x2", end.x);
  122. elLine.setAttribute("y2", end.y);
  123. let box = svg.getBBox();
  124. svg.setAttribute("width", `${box.width}`);
  125. svg.setAttribute("height", `${box.height}`);
  126. svg.setAttribute("viewBox", `${box.x} ${box.y} ${box.width} ${box.height}`);
  127. let ya = start.y - end.y;
  128. let xa = start.x - end.x;
  129. if(ya > 0){
  130. start.y = start.y - ya;
  131. }
  132. if(xa > 0){
  133. start.x = start.x - xa;
  134. }
  135. domElement.css("left", `${start.x}px`);
  136. domElement.css("top", `${start.y}px`);
  137. };
  138. $(viewer.renderArea).append(domElement);
  139. let annotationStartPos = this.position.clone();
  140. let annotationStartOffset = this.offset.clone();
  141. $(this.domElement).draggable({
  142. start: (event, ui) => {
  143. annotationStartPos = this.position.clone();
  144. annotationStartOffset = this.offset.clone();
  145. $(this.domElement).find(".annotation-titlebar").css("pointer-events", "none");
  146. console.log($(this.domElement).find(".annotation-titlebar"));
  147. },
  148. stop: () => {
  149. $(this.domElement).find(".annotation-titlebar").css("pointer-events", "");
  150. },
  151. drag: (event, ui ) => {
  152. let renderAreaWidth = viewer.renderer.getSize(new THREE.Vector2()).width;
  153. //let renderAreaHeight = viewer.renderer.getSize().height;
  154. let diff = {
  155. x: ui.originalPosition.left - ui.position.left,
  156. y: ui.originalPosition.top - ui.position.top
  157. };
  158. let nDiff = {
  159. x: -(diff.x / renderAreaWidth) * 2,
  160. y: (diff.y / renderAreaWidth) * 2
  161. };
  162. let camera = viewer.scene.getActiveCamera();
  163. let oldScreenPos = new THREE.Vector3()
  164. .addVectors(annotationStartPos, annotationStartOffset)
  165. .project(camera);
  166. let newScreenPos = oldScreenPos.clone();
  167. newScreenPos.x += nDiff.x;
  168. newScreenPos.y += nDiff.y;
  169. let newPos = newScreenPos.clone();
  170. newPos.unproject(camera);
  171. let newOffset = new THREE.Vector3().subVectors(newPos, this.position);
  172. this.offset.copy(newOffset);
  173. }
  174. });
  175. let updateCallback = () => {
  176. let position = this.position;
  177. let scene = viewer.scene;
  178. const renderAreaSize = viewer.renderer.getSize(new THREE.Vector2());
  179. let renderAreaWidth = renderAreaSize.width;
  180. let renderAreaHeight = renderAreaSize.height;
  181. let start = this.position.clone();
  182. let end = new THREE.Vector3().addVectors(this.position, this.offset);
  183. let toScreen = (position) => {
  184. let camera = scene.getActiveCamera();
  185. let screenPos = new THREE.Vector3();
  186. let worldView = new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
  187. let ndc = new THREE.Vector4(position.x, position.y, position.z, 1.0).applyMatrix4(worldView);
  188. // limit w to small positive value, in case position is behind the camera
  189. ndc.w = Math.max(ndc.w, 0.1);
  190. ndc.divideScalar(ndc.w);
  191. screenPos.copy(ndc);
  192. screenPos.x = renderAreaWidth * (screenPos.x + 1) / 2;
  193. screenPos.y = renderAreaHeight * (1 - (screenPos.y + 1) / 2);
  194. return screenPos;
  195. };
  196. start = toScreen(start);
  197. end = toScreen(end);
  198. setCoordinates(start, end);
  199. };
  200. viewer.addEventListener("update", updateCallback);
  201. this.handles = {
  202. domElement: domElement,
  203. setCoordinates: setCoordinates,
  204. updateCallback: updateCallback
  205. };
  206. }
  207. removeHandles(viewer){
  208. if(this.handles === undefined){
  209. return;
  210. }
  211. //$(viewer.renderArea).remove(this.handles.domElement);
  212. this.handles.domElement.remove();
  213. viewer.removeEventListener("update", this.handles.updateCallback);
  214. delete this.handles;
  215. }
  216. get visible () {
  217. return this._visible;
  218. }
  219. set visible (value) {
  220. if (this._visible === value) {
  221. return;
  222. }
  223. this._visible = value;
  224. //this.traverse(node => {
  225. // node.display = value;
  226. //});
  227. this.dispatchEvent({
  228. type: 'visibility_changed',
  229. annotation: this
  230. });
  231. }
  232. get display () {
  233. return this._display;
  234. }
  235. set display (display) {
  236. if (this._display === display) {
  237. return;
  238. }
  239. this._display = display;
  240. if (display) {
  241. // this.domElement.fadeIn(200);
  242. this.domElement.show();
  243. } else {
  244. // this.domElement.fadeOut(200);
  245. this.domElement.hide();
  246. }
  247. }
  248. get expand () {
  249. return this._expand;
  250. }
  251. set expand (expand) {
  252. if (this._expand === expand) {
  253. return;
  254. }
  255. if (expand) {
  256. this.display = false;
  257. } else {
  258. this.display = true;
  259. this.traverseDescendants(node => {
  260. node.display = false;
  261. });
  262. }
  263. this._expand = expand;
  264. }
  265. get title () {
  266. return this._title;
  267. }
  268. set title (title) {
  269. if (this._title === title) {
  270. return;
  271. }
  272. this._title = title;
  273. this.elTitle.empty();
  274. this.elTitle.append(this._title);
  275. this.dispatchEvent({
  276. type: "annotation_changed",
  277. annotation: this,
  278. });
  279. }
  280. get description () {
  281. return this._description;
  282. }
  283. set description (description) {
  284. if (this._description === description) {
  285. return;
  286. }
  287. this._description = description;
  288. const elDescriptionContent = this.elDescription.find(".annotation-description-content");
  289. elDescriptionContent.empty();
  290. elDescriptionContent.append(this._description);
  291. this.dispatchEvent({
  292. type: "annotation_changed",
  293. annotation: this,
  294. });
  295. }
  296. add (annotation) {
  297. if (!this.children.includes(annotation)) {
  298. this.children.push(annotation);
  299. annotation.parent = this;
  300. let descendants = [];
  301. annotation.traverse(a => { descendants.push(a); });
  302. for (let descendant of descendants) {
  303. let c = this;
  304. while (c !== null) {
  305. c.dispatchEvent({
  306. 'type': 'annotation_added',
  307. 'annotation': descendant
  308. });
  309. c = c.parent;
  310. }
  311. }
  312. }
  313. }
  314. level () {
  315. if (this.parent === null) {
  316. return 0;
  317. } else {
  318. return this.parent.level() + 1;
  319. }
  320. }
  321. hasChild(annotation) {
  322. return this.children.includes(annotation);
  323. }
  324. remove (annotation) {
  325. if (this.hasChild(annotation)) {
  326. annotation.removeAllChildren();
  327. annotation.dispose();
  328. this.children = this.children.filter(e => e !== annotation);
  329. annotation.parent = null;
  330. }
  331. }
  332. removeAllChildren() {
  333. this.children.forEach((child) => {
  334. if (child.children.length > 0) {
  335. child.removeAllChildren();
  336. }
  337. this.remove(child);
  338. });
  339. }
  340. updateBounds () {
  341. let box = new THREE.Box3();
  342. if (this.position) {
  343. box.expandByPoint(this.position);
  344. }
  345. for (let child of this.children) {
  346. child.updateBounds();
  347. box.union(child.boundingBox);
  348. }
  349. this.boundingBox.copy(box);
  350. }
  351. traverse (handler) {
  352. let expand = handler(this);
  353. if (expand === undefined || expand === true) {
  354. for (let child of this.children) {
  355. child.traverse(handler);
  356. }
  357. }
  358. }
  359. traverseDescendants (handler) {
  360. for (let child of this.children) {
  361. child.traverse(handler);
  362. }
  363. }
  364. flatten () {
  365. let annotations = [];
  366. this.traverse(annotation => {
  367. annotations.push(annotation);
  368. });
  369. return annotations;
  370. }
  371. descendants () {
  372. let annotations = [];
  373. this.traverse(annotation => {
  374. if (annotation !== this) {
  375. annotations.push(annotation);
  376. }
  377. });
  378. return annotations;
  379. }
  380. setHighlighted (highlighted) {
  381. if (highlighted) {
  382. this.domElement.css('opacity', '0.8');
  383. this.elTitlebar.css('box-shadow', '0 0 5px #fff');
  384. this.domElement.css('z-index', '1000');
  385. if (this._description) {
  386. this.descriptionVisible = true;
  387. this.elDescription.fadeIn(200);
  388. this.elDescription.css('position', 'relative');
  389. }
  390. } else {
  391. this.domElement.css('opacity', '0.5');
  392. this.elTitlebar.css('box-shadow', '');
  393. this.domElement.css('z-index', '100');
  394. this.descriptionVisible = false;
  395. this.elDescription.css('display', 'none');
  396. }
  397. this.isHighlighted = highlighted;
  398. }
  399. hasView () {
  400. let hasPosTargetView = this.cameraTarget.x != null;
  401. hasPosTargetView = hasPosTargetView && this.cameraPosition.x != null;
  402. let hasRadiusView = this.radius !== undefined;
  403. let hasView = hasPosTargetView || hasRadiusView;
  404. return hasView;
  405. };
  406. moveHere (camera) {
  407. if (!this.hasView()) {
  408. return;
  409. }
  410. let view = this.scene.view;
  411. let animationDuration = 500;
  412. let easing = TWEEN.Easing.Quartic.Out;
  413. let endTarget;
  414. if (this.cameraTarget) {
  415. endTarget = this.cameraTarget;
  416. } else if (this.position) {
  417. endTarget = this.position;
  418. } else {
  419. endTarget = this.boundingBox.getCenter(new THREE.Vector3());
  420. }
  421. if (this.cameraPosition) {
  422. let endPosition = this.cameraPosition;
  423. Utils.moveTo(this.scene, endPosition, endTarget);
  424. } else if (this.radius) {
  425. let direction = view.direction;
  426. let endPosition = endTarget.clone().add(direction.multiplyScalar(-this.radius));
  427. let startRadius = view.radius;
  428. let endRadius = this.radius;
  429. { // animate camera position
  430. let tween = new TWEEN.Tween(view.position).to(endPosition, animationDuration);
  431. tween.easing(easing);
  432. tween.start();
  433. }
  434. { // animate radius
  435. let t = {x: 0};
  436. let tween = new TWEEN.Tween(t)
  437. .to({x: 1}, animationDuration)
  438. .onUpdate(function () {
  439. view.radius = this.x * endRadius + (1 - this.x) * startRadius;
  440. });
  441. tween.easing(easing);
  442. tween.start();
  443. }
  444. }
  445. };
  446. dispose () {
  447. if (this.domElement.parentElement) {
  448. this.domElement.parentElement.removeChild(this.domElement);
  449. }
  450. };
  451. toString () {
  452. return 'Annotation: ' + this._title;
  453. }
  454. };