src/plugins/FaceAlignedSectionPlanesPlugin/FaceAlignedSectionPlanesPlugin.js
import {math} from "../../viewer/scene/math/math.js";
import {Plugin} from "../../viewer/Plugin.js";
import {SectionPlane} from "../../viewer/scene/sectionPlane/SectionPlane.js";
import {FaceAlignedSectionPlanesControl} from "./FaceAlignedSectionPlanesControl.js";
import {Overview} from "./Overview.js";
const tempAABB = math.AABB3();
const tempVec3 = math.vec3();
/**
* FaceAlignedSectionPlanesPlugin is a {@link Viewer} plugin that creates and edits face-aligned {@link SectionPlane}s.
*
* [<img src="https://xeokit.github.io/xeokit-sdk/assets/images/FaceAlignedSectionPlanesPlugin.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#gizmos_FaceAlignedSectionPlanesPlugin)
*
* [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/slicing/#FaceAlignedSectionPlanesPlugin)]
*
* ## Overview
*
* * Use the FaceAlignedSectionPlanesPlugin to
* create and edit {@link SectionPlane}s to slice portions off your models and reveal internal structures.
*
* * As shown in the screen capture above, FaceAlignedSectionPlanesPlugin shows an overview of all your SectionPlanes (on the right, in
* this example).
* * Click a plane in the overview to activate a 3D control with which you can interactively
* reposition its SectionPlane in the main canvas.
* * 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.
* * Use {@link BCFViewpointsPlugin} to save and load SectionPlanes in BCF viewpoints.
*
* ## Usage
*
* In the example below, we'll use a {@link GLTFLoaderPlugin} to load a model, and a FaceAlignedSectionPlanesPlugin
* to slice it open with two {@link SectionPlane}s. We'll show the overview in the bottom right of the Viewer
* canvas. Finally, we'll programmatically activate the 3D editing control, so that we can use it to interactively
* reposition our second SectionPlane.
*
* ````JavaScript
* import {Viewer, GLTFLoaderPlugin, FaceAlignedSectionPlanesPlugin} from "xeokit-sdk.es.js";
*
* // Create a Viewer and arrange its Camera
*
* const viewer = new Viewer({
* canvasId: "myCanvas"
* });
*
* viewer.camera.eye = [-5.02, 2.22, 15.09];
* viewer.camera.look = [4.97, 2.79, 9.89];
* viewer.camera.up = [-0.05, 0.99, 0.02];
*
* // Add a GLTFLoaderPlugin
*
* const gltfLoader = new GLTFLoaderPlugin(viewer);
*
* // Add a FaceAlignedSectionPlanesPlugin, with overview visible
*
* const faceAlignedSectionPlanes = new FaceAlignedSectionPlanesPlugin(viewer, {
* overviewCanvasID: "myOverviewCanvas",
* overviewVisible: true,
* controlElementId: "myControlElement", // ID of element to capture drag events that move the SectionPlane
* dragSensitivity: 1 // Sensitivity factor that governs the rate at which dragging moves the SectionPlane
* });
*
* // Load a model
*
* const model = gltfLoader.load({
* id: "myModel",
* src: "./models/gltf/schependomlaan/scene.glb"
* });
*
* // Create a couple of section planes
* // These will be shown in the overview
*
* faceAlignedSectionPlanes.createSectionPlane({
* id: "mySectionPlane",
* pos: [1.04, 1.95, 9.74],
* dir: [1.0, 0.0, 0.0]
* });
*
* faceAlignedSectionPlanes.createSectionPlane({
* id: "mySectionPlane2",
* pos: [2.30, 4.46, 14.93],
* dir: [0.0, -0.09, -0.79]
* });
*
* // Show the FaceAlignedSectionPlanesPlugin's 3D editing gizmo,
* // to interactively reposition one of our SectionPlanes
*
* faceAlignedSectionPlanes.showControl("mySectionPlane2");
*
* const mySectionPlane2 = faceAlignedSectionPlanes.sectionPlanes["mySectionPlane2"];
*
* // Programmatically reposition one of our SectionPlanes
* // This also updates its position as shown in the overview gizmo
*
* mySectionPlane2.pos = [11.0, 6.0, -12];
* mySectionPlane2.dir = [0.4, 0.0, 0.5];
* ````
*/
export class FaceAlignedSectionPlanesPlugin extends Plugin {
/**
* @constructor
* @param {Viewer} viewer The Viewer.
* @param {Object} cfg Plugin configuration.
* @param {String} [cfg.id="SectionPlanes"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
* @param {String} [cfg.overviewCanvasId] ID of a canvas element to display the overview.
* @param {String} [cfg.overviewVisible=true] Initial visibility of the overview canvas.
* @param {String} cfg.controlElementId ID of an HTML element that catches drag events to move the active SectionPlane.
* @param {Number} [cfg.dragSensitivity=1] Sensitivity factor that governs the rate at which dragging on the control element moves SectionPlane.
*/
constructor(viewer, cfg = {}) {
super("FaceAlignedSectionPlanesPlugin", viewer);
this._freeControls = [];
this._sectionPlanes = viewer.scene.sectionPlanes;
this._controls = {};
this._shownControlId = null;
this._dragSensitivity = cfg.dragSensitivity || 1;
if (cfg.overviewCanvasId !== null && cfg.overviewCanvasId !== undefined) {
const overviewCanvas = document.getElementById(cfg.overviewCanvasId);
if (!overviewCanvas) {
this.warn("Can't find overview canvas: '" + cfg.overviewCanvasId + "' - will create plugin without overview");
} else {
this._overview = new Overview(this, {
overviewCanvas: overviewCanvas,
visible: cfg.overviewVisible,
onHoverEnterPlane: ((id) => {
this._overview.setPlaneHighlighted(id, true);
}),
onHoverLeavePlane: ((id) => {
this._overview.setPlaneHighlighted(id, false);
}),
onClickedPlane: ((id) => {
if (this.getShownControl() === id) {
this.hideControl();
return;
}
this.showControl(id);
const sectionPlane = this.sectionPlanes[id];
const sectionPlanePos = sectionPlane.pos;
tempAABB.set(this.viewer.scene.aabb);
math.getAABB3Center(tempAABB, tempVec3);
tempAABB[0] += sectionPlanePos[0] - tempVec3[0];
tempAABB[1] += sectionPlanePos[1] - tempVec3[1];
tempAABB[2] += sectionPlanePos[2] - tempVec3[2];
tempAABB[3] += sectionPlanePos[0] - tempVec3[0];
tempAABB[4] += sectionPlanePos[1] - tempVec3[1];
tempAABB[5] += sectionPlanePos[2] - tempVec3[2];
this.viewer.cameraFlight.flyTo({
aabb: tempAABB,
fitFOV: 65
});
}),
onClickedNothing: (() => {
this.hideControl();
})
});
}
}
if (cfg.controlElementId === null || cfg.controlElementId === undefined) {
this.error("Parameter expected: controlElementId");
} else {
this._controlElement = document.getElementById(cfg.controlElementId);
if (!this._controlElement) {
this.warn("Can't find control element: '" + cfg.controlElementId + "' - will create plugin without control element");
}
}
this._onSceneSectionPlaneCreated = viewer.scene.on("sectionPlaneCreated", (sectionPlane) => {
// SectionPlane created, either via FaceAlignedSectionPlanesPlugin#createSectionPlane(), or by directly
// instantiating a SectionPlane independently of FaceAlignedSectionPlanesPlugin, which can be done
// by BCFViewpointsPlugin#loadViewpoint().
this._sectionPlaneCreated(sectionPlane);
});
}
/**
* Sets the factor that governs how fast a SectionPlane moves as we drag on the control element.
*
* @param {Number} dragSensitivity The dragging sensitivity factor.
*/
setDragSensitivity(dragSensitivity) {
this._dragSensitivity = dragSensitivity || 1;
}
/**
* Gets the factor that governs how fast a SectionPlane moves as we drag on the control element.
*
* @return {Number} The dragging sensitivity factor.
*/
getDragSensitivity() {
return this._dragSensitivity;
}
/**
* Sets if the overview canvas is visible.
*
* @param {Boolean} visible Whether or not the overview canvas is visible.
*/
setOverviewVisible(visible) {
if (this._overview) {
this._overview.setVisible(visible);
}
}
/**
* Gets if the overview canvas is visible.
*
* @return {Boolean} True when the overview canvas is visible.
*/
getOverviewVisible() {
if (this._overview) {
return this._overview.getVisible();
}
}
/**
* Returns a map of the {@link SectionPlane}s created by this FaceAlignedSectionPlanesPlugin.
*
* @returns {{String:SectionPlane}} A map containing the {@link SectionPlane}s, each mapped to its {@link SectionPlane#id}.
*/
get sectionPlanes() {
return this._sectionPlanes;
}
/**
* Creates a {@link SectionPlane}.
*
* The {@link SectionPlane} will be registered by {@link SectionPlane#id} in {@link FaceAlignedSectionPlanesPlugin#sectionPlanes}.
*
* @param {Object} params {@link SectionPlane} configuration.
* @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.
* @param {Number[]} [params.pos=[0,0,0]] World-space position of the {@link SectionPlane}.
* @param {Number[]} [params.dir=[0,0,-1]] World-space vector indicating the orientation of the {@link SectionPlane}.
* @param {Boolean} [params.active=true] Whether the {@link SectionPlane} is initially active. Only clips while this is true.
* @returns {SectionPlane} The new {@link SectionPlane}.
*/
createSectionPlane(params = {}) {
if (params.id !== undefined && params.id !== null && this.viewer.scene.components[params.id]) {
this.error("Viewer component with this ID already exists: " + params.id);
delete params.id;
}
// Note that SectionPlane constructor fires "sectionPlaneCreated" on the Scene,
// which FaceAlignedSectionPlanesPlugin handles and calls #_sectionPlaneCreated to create gizmo and add to overview canvas.
const sectionPlane = new SectionPlane(this.viewer.scene, {
id: params.id,
pos: params.pos,
dir: params.dir,
active: true || params.active
});
return sectionPlane;
}
_sectionPlaneCreated(sectionPlane) {
const control = (this._freeControls.length > 0) ? this._freeControls.pop() : new FaceAlignedSectionPlanesControl(this);
control._setSectionPlane(sectionPlane);
control.setVisible(false);
this._controls[sectionPlane.id] = control;
if (this._overview) {
this._overview.addSectionPlane(sectionPlane);
}
sectionPlane.once("destroyed", () => {
this._sectionPlaneDestroyed(sectionPlane);
});
}
/**
* Inverts the direction of {@link SectionPlane#dir} on every existing SectionPlane.
*
* Inverts all SectionPlanes, including those that were not created with FaceAlignedSectionPlanesPlugin.
*/
flipSectionPlanes() {
const sectionPlanes = this.viewer.scene.sectionPlanes;
for (let id in sectionPlanes) {
const sectionPlane = sectionPlanes[id];
sectionPlane.flipDir();
}
}
/**
* Shows the 3D editing gizmo for a {@link SectionPlane}.
*
* @param {String} id ID of the {@link SectionPlane}.
*/
showControl(id) {
const control = this._controls[id];
if (!control) {
this.error("Control not found: " + id);
return;
}
this.hideControl();
control.setVisible(true);
if (this._overview) {
this._overview.setPlaneSelected(id, true);
}
this._shownControlId = id;
}
/**
* Gets the ID of the {@link SectionPlane} that the 3D editing gizmo is shown for.
*
* Returns ````null```` when the editing gizmo is not shown.
*
* @returns {String} ID of the the {@link SectionPlane} that the 3D editing gizmo is shown for, if shown, else ````null````.
*/
getShownControl() {
return this._shownControlId;
}
/**
* Hides the 3D {@link SectionPlane} editing gizmo if shown.
*/
hideControl() {
for (let id in this._controls) {
if (this._controls.hasOwnProperty(id)) {
this._controls[id].setVisible(false);
if (this._overview) {
this._overview.setPlaneSelected(id, false);
}
}
}
this._shownControlId = null;
}
/**
* Destroys a {@link SectionPlane} created by this FaceAlignedSectionPlanesPlugin.
*
* @param {String} id ID of the {@link SectionPlane}.
*/
destroySectionPlane(id) {
let sectionPlane = this.viewer.scene.sectionPlanes[id];
if (!sectionPlane) {
this.error("SectionPlane not found: " + id);
return;
}
this._sectionPlaneDestroyed(sectionPlane);
sectionPlane.destroy();
if (id === this._shownControlId) {
this._shownControlId = null;
}
}
_sectionPlaneDestroyed(sectionPlane) {
if (this._overview) {
this._overview.removeSectionPlane(sectionPlane);
}
const control = this._controls[sectionPlane.id];
if (!control) {
return;
}
control.setVisible(false);
control._setSectionPlane(null);
delete this._controls[sectionPlane.id];
this._freeControls.push(control);
}
/**
* Destroys all {@link SectionPlane}s created by this FaceAlignedSectionPlanesPlugin.
*/
clear() {
const ids = Object.keys(this._sectionPlanes);
for (let i = 0, len = ids.length; i < len; i++) {
this.destroySectionPlane(ids[i]);
}
}
/**
* @private
*/
send(name, value) {
switch (name) {
case "snapshotStarting": // Viewer#getSnapshot() about to take snapshot - hide controls
for (let id in this._controls) {
if (this._controls.hasOwnProperty(id)) {
this._controls[id].setCulled(true);
}
}
break;
case "snapshotFinished": // Viewer#getSnapshot() finished taking snapshot - show controls again
for (let id in this._controls) {
if (this._controls.hasOwnProperty(id)) {
this._controls[id].setCulled(false);
}
}
break;
case "clearSectionPlanes":
this.clear();
break;
}
}
/**
* Destroys this FaceAlignedSectionPlanesPlugin.
*
* Also destroys each {@link SectionPlane} created by this FaceAlignedSectionPlanesPlugin.
*
* Does not destroy the canvas the FaceAlignedSectionPlanesPlugin was configured with.
*/
destroy() {
this.clear();
if (this._overview) {
this._overview.destroy();
}
this._destroyFreeControls();
super.destroy();
}
_destroyFreeControls() {
let control = this._freeControls.pop();
while (control) {
control._destroy();
control = this._freeControls.pop();
}
this.viewer.scene.off(this._onSceneSectionPlaneCreated);
}
}