Reference Source

src/plugins/FaceAlignedSectionPlanesPlugin/FaceAlignedSectionPlanesPlugin.js

  1. import {math} from "../../viewer/scene/math/math.js";
  2. import {Plugin} from "../../viewer/Plugin.js";
  3. import {SectionPlane} from "../../viewer/scene/sectionPlane/SectionPlane.js";
  4. import {FaceAlignedSectionPlanesControl} from "./FaceAlignedSectionPlanesControl.js";
  5. import {Overview} from "./Overview.js";
  6.  
  7. const tempAABB = math.AABB3();
  8. const tempVec3 = math.vec3();
  9.  
  10. /**
  11. * FaceAlignedSectionPlanesPlugin is a {@link Viewer} plugin that creates and edits face-aligned {@link SectionPlane}s.
  12. *
  13. * [<img src="https://xeokit.github.io/xeokit-sdk/assets/images/FaceAlignedSectionPlanesPlugin.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#gizmos_FaceAlignedSectionPlanesPlugin)
  14. *
  15. * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/slicing/#FaceAlignedSectionPlanesPlugin)]
  16. *
  17. * ## Overview
  18. *
  19. * * Use the FaceAlignedSectionPlanesPlugin to
  20. * create and edit {@link SectionPlane}s to slice portions off your models and reveal internal structures.
  21. *
  22. * * As shown in the screen capture above, FaceAlignedSectionPlanesPlugin shows an overview of all your SectionPlanes (on the right, in
  23. * this example).
  24. * * Click a plane in the overview to activate a 3D control with which you can interactively
  25. * reposition its SectionPlane in the main canvas.
  26. * * Configure the plugin with an HTML element that the user can click-and-drag on to reposition the SectionPlane for which the control is active.
  27. * * Use {@link BCFViewpointsPlugin} to save and load SectionPlanes in BCF viewpoints.
  28. *
  29. * ## Usage
  30. *
  31. * In the example below, we'll use a {@link GLTFLoaderPlugin} to load a model, and a FaceAlignedSectionPlanesPlugin
  32. * to slice it open with two {@link SectionPlane}s. We'll show the overview in the bottom right of the Viewer
  33. * canvas. Finally, we'll programmatically activate the 3D editing control, so that we can use it to interactively
  34. * reposition our second SectionPlane.
  35. *
  36. * ````JavaScript
  37. * import {Viewer, GLTFLoaderPlugin, FaceAlignedSectionPlanesPlugin} from "xeokit-sdk.es.js";
  38. *
  39. * // Create a Viewer and arrange its Camera
  40. *
  41. * const viewer = new Viewer({
  42. * canvasId: "myCanvas"
  43. * });
  44. *
  45. * viewer.camera.eye = [-5.02, 2.22, 15.09];
  46. * viewer.camera.look = [4.97, 2.79, 9.89];
  47. * viewer.camera.up = [-0.05, 0.99, 0.02];
  48. *
  49. * // Add a GLTFLoaderPlugin
  50. *
  51. * const gltfLoader = new GLTFLoaderPlugin(viewer);
  52. *
  53. * // Add a FaceAlignedSectionPlanesPlugin, with overview visible
  54. *
  55. * const faceAlignedSectionPlanes = new FaceAlignedSectionPlanesPlugin(viewer, {
  56. * overviewCanvasID: "myOverviewCanvas",
  57. * overviewVisible: true,
  58. * controlElementId: "myControlElement", // ID of element to capture drag events that move the SectionPlane
  59. * dragSensitivity: 1 // Sensitivity factor that governs the rate at which dragging moves the SectionPlane
  60. * });
  61. *
  62. * // Load a model
  63. *
  64. * const model = gltfLoader.load({
  65. * id: "myModel",
  66. * src: "./models/gltf/schependomlaan/scene.glb"
  67. * });
  68. *
  69. * // Create a couple of section planes
  70. * // These will be shown in the overview
  71. *
  72. * faceAlignedSectionPlanes.createSectionPlane({
  73. * id: "mySectionPlane",
  74. * pos: [1.04, 1.95, 9.74],
  75. * dir: [1.0, 0.0, 0.0]
  76. * });
  77. *
  78. * faceAlignedSectionPlanes.createSectionPlane({
  79. * id: "mySectionPlane2",
  80. * pos: [2.30, 4.46, 14.93],
  81. * dir: [0.0, -0.09, -0.79]
  82. * });
  83. *
  84. * // Show the FaceAlignedSectionPlanesPlugin's 3D editing gizmo,
  85. * // to interactively reposition one of our SectionPlanes
  86. *
  87. * faceAlignedSectionPlanes.showControl("mySectionPlane2");
  88. *
  89. * const mySectionPlane2 = faceAlignedSectionPlanes.sectionPlanes["mySectionPlane2"];
  90. *
  91. * // Programmatically reposition one of our SectionPlanes
  92. * // This also updates its position as shown in the overview gizmo
  93. *
  94. * mySectionPlane2.pos = [11.0, 6.0, -12];
  95. * mySectionPlane2.dir = [0.4, 0.0, 0.5];
  96. * ````
  97. */
  98. export class FaceAlignedSectionPlanesPlugin extends Plugin {
  99.  
  100. /**
  101. * @constructor
  102. * @param {Viewer} viewer The Viewer.
  103. * @param {Object} cfg Plugin configuration.
  104. * @param {String} [cfg.id="SectionPlanes"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
  105. * @param {String} [cfg.overviewCanvasId] ID of a canvas element to display the overview.
  106. * @param {String} [cfg.overviewVisible=true] Initial visibility of the overview canvas.
  107. * @param {String} cfg.controlElementId ID of an HTML element that catches drag events to move the active SectionPlane.
  108. * @param {Number} [cfg.dragSensitivity=1] Sensitivity factor that governs the rate at which dragging on the control element moves SectionPlane.
  109. */
  110. constructor(viewer, cfg = {}) {
  111.  
  112. super("FaceAlignedSectionPlanesPlugin", viewer);
  113.  
  114. this._freeControls = [];
  115. this._sectionPlanes = viewer.scene.sectionPlanes;
  116. this._controls = {};
  117. this._shownControlId = null;
  118. this._dragSensitivity = cfg.dragSensitivity || 1;
  119.  
  120. if (cfg.overviewCanvasId !== null && cfg.overviewCanvasId !== undefined) {
  121.  
  122. const overviewCanvas = document.getElementById(cfg.overviewCanvasId);
  123.  
  124. if (!overviewCanvas) {
  125. this.warn("Can't find overview canvas: '" + cfg.overviewCanvasId + "' - will create plugin without overview");
  126.  
  127. } else {
  128.  
  129. this._overview = new Overview(this, {
  130. overviewCanvas: overviewCanvas,
  131. visible: cfg.overviewVisible,
  132.  
  133. onHoverEnterPlane: ((id) => {
  134. this._overview.setPlaneHighlighted(id, true);
  135. }),
  136.  
  137. onHoverLeavePlane: ((id) => {
  138. this._overview.setPlaneHighlighted(id, false);
  139. }),
  140.  
  141. onClickedPlane: ((id) => {
  142. if (this.getShownControl() === id) {
  143. this.hideControl();
  144. return;
  145. }
  146. this.showControl(id);
  147. const sectionPlane = this.sectionPlanes[id];
  148. const sectionPlanePos = sectionPlane.pos;
  149. tempAABB.set(this.viewer.scene.aabb);
  150. math.getAABB3Center(tempAABB, tempVec3);
  151. tempAABB[0] += sectionPlanePos[0] - tempVec3[0];
  152. tempAABB[1] += sectionPlanePos[1] - tempVec3[1];
  153. tempAABB[2] += sectionPlanePos[2] - tempVec3[2];
  154. tempAABB[3] += sectionPlanePos[0] - tempVec3[0];
  155. tempAABB[4] += sectionPlanePos[1] - tempVec3[1];
  156. tempAABB[5] += sectionPlanePos[2] - tempVec3[2];
  157. this.viewer.cameraFlight.flyTo({
  158. aabb: tempAABB,
  159. fitFOV: 65
  160. });
  161. }),
  162.  
  163. onClickedNothing: (() => {
  164. this.hideControl();
  165. })
  166. });
  167. }
  168. }
  169.  
  170. if (cfg.controlElementId === null || cfg.controlElementId === undefined) {
  171. this.error("Parameter expected: controlElementId");
  172. } else {
  173. this._controlElement = document.getElementById(cfg.controlElementId);
  174. if (!this._controlElement) {
  175. this.warn("Can't find control element: '" + cfg.controlElementId + "' - will create plugin without control element");
  176.  
  177. }
  178. }
  179.  
  180. this._onSceneSectionPlaneCreated = viewer.scene.on("sectionPlaneCreated", (sectionPlane) => {
  181.  
  182. // SectionPlane created, either via FaceAlignedSectionPlanesPlugin#createSectionPlane(), or by directly
  183. // instantiating a SectionPlane independently of FaceAlignedSectionPlanesPlugin, which can be done
  184. // by BCFViewpointsPlugin#loadViewpoint().
  185.  
  186. this._sectionPlaneCreated(sectionPlane);
  187. });
  188. }
  189.  
  190. /**
  191. * Sets the factor that governs how fast a SectionPlane moves as we drag on the control element.
  192. *
  193. * @param {Number} dragSensitivity The dragging sensitivity factor.
  194. */
  195. setDragSensitivity(dragSensitivity) {
  196. this._dragSensitivity = dragSensitivity || 1;
  197. }
  198.  
  199. /**
  200. * Gets the factor that governs how fast a SectionPlane moves as we drag on the control element.
  201. *
  202. * @return {Number} The dragging sensitivity factor.
  203. */
  204. getDragSensitivity() {
  205. return this._dragSensitivity;
  206. }
  207.  
  208. /**
  209. * Sets if the overview canvas is visible.
  210. *
  211. * @param {Boolean} visible Whether or not the overview canvas is visible.
  212. */
  213. setOverviewVisible(visible) {
  214. if (this._overview) {
  215. this._overview.setVisible(visible);
  216. }
  217. }
  218.  
  219. /**
  220. * Gets if the overview canvas is visible.
  221. *
  222. * @return {Boolean} True when the overview canvas is visible.
  223. */
  224. getOverviewVisible() {
  225. if (this._overview) {
  226. return this._overview.getVisible();
  227. }
  228. }
  229.  
  230. /**
  231. * Returns a map of the {@link SectionPlane}s created by this FaceAlignedSectionPlanesPlugin.
  232. *
  233. * @returns {{String:SectionPlane}} A map containing the {@link SectionPlane}s, each mapped to its {@link SectionPlane#id}.
  234. */
  235. get sectionPlanes() {
  236. return this._sectionPlanes;
  237. }
  238.  
  239. /**
  240. * Creates a {@link SectionPlane}.
  241. *
  242. * The {@link SectionPlane} will be registered by {@link SectionPlane#id} in {@link FaceAlignedSectionPlanesPlugin#sectionPlanes}.
  243. *
  244. * @param {Object} params {@link SectionPlane} configuration.
  245. * @param {String} [params.id] Unique ID to assign to the {@link SectionPlane}. Must be unique among all components in the {@link Viewer}'s {@link Scene}. Auto-generated when omitted.
  246. * @param {Number[]} [params.pos=[0,0,0]] World-space position of the {@link SectionPlane}.
  247. * @param {Number[]} [params.dir=[0,0,-1]] World-space vector indicating the orientation of the {@link SectionPlane}.
  248. * @param {Boolean} [params.active=true] Whether the {@link SectionPlane} is initially active. Only clips while this is true.
  249. * @returns {SectionPlane} The new {@link SectionPlane}.
  250. */
  251. createSectionPlane(params = {}) {
  252.  
  253. if (params.id !== undefined && params.id !== null && this.viewer.scene.components[params.id]) {
  254. this.error("Viewer component with this ID already exists: " + params.id);
  255. delete params.id;
  256. }
  257.  
  258. // Note that SectionPlane constructor fires "sectionPlaneCreated" on the Scene,
  259. // which FaceAlignedSectionPlanesPlugin handles and calls #_sectionPlaneCreated to create gizmo and add to overview canvas.
  260.  
  261. const sectionPlane = new SectionPlane(this.viewer.scene, {
  262. id: params.id,
  263. pos: params.pos,
  264. dir: params.dir,
  265. active: true || params.active
  266. });
  267. return sectionPlane;
  268. }
  269.  
  270. _sectionPlaneCreated(sectionPlane) {
  271. const control = (this._freeControls.length > 0) ? this._freeControls.pop() : new FaceAlignedSectionPlanesControl(this);
  272. control._setSectionPlane(sectionPlane);
  273. control.setVisible(false);
  274. this._controls[sectionPlane.id] = control;
  275. if (this._overview) {
  276. this._overview.addSectionPlane(sectionPlane);
  277. }
  278. sectionPlane.once("destroyed", () => {
  279. this._sectionPlaneDestroyed(sectionPlane);
  280. });
  281. }
  282.  
  283. /**
  284. * Inverts the direction of {@link SectionPlane#dir} on every existing SectionPlane.
  285. *
  286. * Inverts all SectionPlanes, including those that were not created with FaceAlignedSectionPlanesPlugin.
  287. */
  288. flipSectionPlanes() {
  289. const sectionPlanes = this.viewer.scene.sectionPlanes;
  290. for (let id in sectionPlanes) {
  291. const sectionPlane = sectionPlanes[id];
  292. sectionPlane.flipDir();
  293. }
  294. }
  295.  
  296. /**
  297. * Shows the 3D editing gizmo for a {@link SectionPlane}.
  298. *
  299. * @param {String} id ID of the {@link SectionPlane}.
  300. */
  301. showControl(id) {
  302. const control = this._controls[id];
  303. if (!control) {
  304. this.error("Control not found: " + id);
  305. return;
  306. }
  307. this.hideControl();
  308. control.setVisible(true);
  309. if (this._overview) {
  310. this._overview.setPlaneSelected(id, true);
  311. }
  312. this._shownControlId = id;
  313. }
  314.  
  315. /**
  316. * Gets the ID of the {@link SectionPlane} that the 3D editing gizmo is shown for.
  317. *
  318. * Returns ````null```` when the editing gizmo is not shown.
  319. *
  320. * @returns {String} ID of the the {@link SectionPlane} that the 3D editing gizmo is shown for, if shown, else ````null````.
  321. */
  322. getShownControl() {
  323. return this._shownControlId;
  324. }
  325.  
  326. /**
  327. * Hides the 3D {@link SectionPlane} editing gizmo if shown.
  328. */
  329. hideControl() {
  330. for (let id in this._controls) {
  331. if (this._controls.hasOwnProperty(id)) {
  332. this._controls[id].setVisible(false);
  333. if (this._overview) {
  334. this._overview.setPlaneSelected(id, false);
  335. }
  336. }
  337. }
  338. this._shownControlId = null;
  339. }
  340.  
  341. /**
  342. * Destroys a {@link SectionPlane} created by this FaceAlignedSectionPlanesPlugin.
  343. *
  344. * @param {String} id ID of the {@link SectionPlane}.
  345. */
  346. destroySectionPlane(id) {
  347. let sectionPlane = this.viewer.scene.sectionPlanes[id];
  348. if (!sectionPlane) {
  349. this.error("SectionPlane not found: " + id);
  350. return;
  351. }
  352. this._sectionPlaneDestroyed(sectionPlane);
  353. sectionPlane.destroy();
  354.  
  355. if (id === this._shownControlId) {
  356. this._shownControlId = null;
  357. }
  358. }
  359.  
  360. _sectionPlaneDestroyed(sectionPlane) {
  361. if (this._overview) {
  362. this._overview.removeSectionPlane(sectionPlane);
  363. }
  364. const control = this._controls[sectionPlane.id];
  365. if (!control) {
  366. return;
  367. }
  368. control.setVisible(false);
  369. control._setSectionPlane(null);
  370. delete this._controls[sectionPlane.id];
  371. this._freeControls.push(control);
  372. }
  373.  
  374. /**
  375. * Destroys all {@link SectionPlane}s created by this FaceAlignedSectionPlanesPlugin.
  376. */
  377. clear() {
  378. const ids = Object.keys(this._sectionPlanes);
  379. for (let i = 0, len = ids.length; i < len; i++) {
  380. this.destroySectionPlane(ids[i]);
  381. }
  382. }
  383.  
  384. /**
  385. * @private
  386. */
  387. send(name, value) {
  388. switch (name) {
  389.  
  390. case "snapshotStarting": // Viewer#getSnapshot() about to take snapshot - hide controls
  391. for (let id in this._controls) {
  392. if (this._controls.hasOwnProperty(id)) {
  393. this._controls[id].setCulled(true);
  394. }
  395. }
  396. break;
  397.  
  398. case "snapshotFinished": // Viewer#getSnapshot() finished taking snapshot - show controls again
  399. for (let id in this._controls) {
  400. if (this._controls.hasOwnProperty(id)) {
  401. this._controls[id].setCulled(false);
  402. }
  403. }
  404. break;
  405.  
  406. case "clearSectionPlanes":
  407. this.clear();
  408. break;
  409. }
  410. }
  411.  
  412. /**
  413. * Destroys this FaceAlignedSectionPlanesPlugin.
  414. *
  415. * Also destroys each {@link SectionPlane} created by this FaceAlignedSectionPlanesPlugin.
  416. *
  417. * Does not destroy the canvas the FaceAlignedSectionPlanesPlugin was configured with.
  418. */
  419. destroy() {
  420. this.clear();
  421. if (this._overview) {
  422. this._overview.destroy();
  423. }
  424. this._destroyFreeControls();
  425. super.destroy();
  426. }
  427.  
  428. _destroyFreeControls() {
  429. let control = this._freeControls.pop();
  430. while (control) {
  431. control._destroy();
  432. control = this._freeControls.pop();
  433. }
  434. this.viewer.scene.off(this._onSceneSectionPlaneCreated);
  435. }
  436. }