Initial import of NODEDC BIM Viewer
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import {TreeViewPlugin, math} from "@xeokit/xeokit-sdk/dist/xeokit-sdk.es.js";
|
||||
import {Controller} from "../Controller.js";
|
||||
import {TreeViewContextMenu} from "../contextMenus/TreeViewContextMenu.js";
|
||||
import {
|
||||
DEFAULT_DOUBLE_CLICK_THRESHOLD,
|
||||
focusTreeNodeInViewer
|
||||
} from "./treeViewFocusHelper.js";
|
||||
|
||||
/** @private */
|
||||
class ClassesExplorer extends Controller {
|
||||
|
||||
constructor(parent, cfg = {}) {
|
||||
|
||||
super(parent);
|
||||
|
||||
if (!cfg.classesTabElement) {
|
||||
throw "Missing config: classesTabElement";
|
||||
}
|
||||
|
||||
if (!cfg.showAllClassesButtonElement) {
|
||||
throw "Missing config: showAllClassesButtonElement";
|
||||
}
|
||||
|
||||
if (!cfg.hideAllClassesButtonElement) {
|
||||
throw "Missing config: hideAllClassesButtonElement";
|
||||
}
|
||||
|
||||
if (!cfg.classesElement) {
|
||||
throw "Missing config: classesElement";
|
||||
}
|
||||
|
||||
this._containerElement = cfg.containerElement;
|
||||
this._classesTabElement = cfg.classesTabElement;
|
||||
this._showAllClassesButtonElement = cfg.showAllClassesButtonElement;
|
||||
this._hideAllClassesButtonElement = cfg.hideAllClassesButtonElement;
|
||||
this._classesTabButtonElement = this._classesTabElement.querySelector(".xeokit-tab-btn");
|
||||
|
||||
if (!this._classesTabButtonElement) {
|
||||
throw "Missing DOM element: xeokit-tab-btn";
|
||||
}
|
||||
|
||||
const classesElement = cfg.classesElement;
|
||||
|
||||
this._treeView = new TreeViewPlugin(this.viewer, {
|
||||
containerElement: classesElement,
|
||||
hierarchy: "types",
|
||||
autoAddModels: false,
|
||||
pruneEmptyNodes: true
|
||||
});
|
||||
|
||||
this._treeViewContextMenu = new TreeViewContextMenu(this.bimViewer, {
|
||||
hideOnAction: true,
|
||||
enableMeasurements: cfg.enableMeasurements,
|
||||
parentNode: cfg.containerElement
|
||||
});
|
||||
|
||||
this._lastNodeClickTime = 0;
|
||||
this._lastNodeClickId = null;
|
||||
this._doubleClickThreshold = this.viewer.doubleClickTimeFrame || DEFAULT_DOUBLE_CLICK_THRESHOLD;
|
||||
|
||||
this._treeView.on("contextmenu", (e) => {
|
||||
this._treeViewContextMenu.context = {
|
||||
bimViewer: this.bimViewer,
|
||||
viewer: e.viewer,
|
||||
treeViewPlugin: e.treeViewPlugin,
|
||||
treeViewNode: e.treeViewNode
|
||||
};
|
||||
this._treeViewContextMenu.show(e.event.pageX, e.event.pageY);
|
||||
});
|
||||
|
||||
this._treeView.on("nodeTitleClicked", (e) => {
|
||||
const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
||||
const nodeId = e.treeViewNode.nodeId || e.treeViewNode.id;
|
||||
const clickCount = e.event && e.event.detail ? e.event.detail : 1;
|
||||
const isSameNode = (this._lastNodeClickId === nodeId);
|
||||
const withinThreshold = isSameNode && ((now - this._lastNodeClickTime) <= this._doubleClickThreshold);
|
||||
const isDoubleClick = (clickCount >= 2) || withinThreshold;
|
||||
|
||||
this._lastNodeClickTime = now;
|
||||
this._lastNodeClickId = nodeId;
|
||||
|
||||
if (isDoubleClick) {
|
||||
console.log("[ClassesExplorer] double click", {
|
||||
nodeId: nodeId,
|
||||
title: e.treeViewNode.title || e.treeViewNode.name
|
||||
});
|
||||
this._onTreeNodeTitleDoubleClicked(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this._onTreeNodeTitleSingleClicked(e);
|
||||
});
|
||||
|
||||
this._onModelLoaded = this.viewer.scene.on("modelLoaded", (modelId) => {
|
||||
if (this.viewer.metaScene.metaModels[modelId]) {
|
||||
const modelInfo = this.bimViewer._modelsExplorer.getModelInfo(modelId);
|
||||
if (!modelInfo) {
|
||||
return;
|
||||
}
|
||||
this._treeView.addModel(modelId, {
|
||||
rootName: modelInfo.name
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => {
|
||||
if (this.viewer.metaScene.metaModels[modelId]) {
|
||||
this._treeView.removeModel(modelId);
|
||||
}
|
||||
});
|
||||
|
||||
this.bimViewer.on("reset", () => {
|
||||
this._treeView.collapse();
|
||||
});
|
||||
}
|
||||
|
||||
setEnabled(enabled) {
|
||||
if (!enabled) {
|
||||
this._classesTabButtonElement.classList.add("disabled");
|
||||
this._showAllClassesButtonElement.classList.add("disabled");
|
||||
this._hideAllClassesButtonElement.classList.add("disabled");
|
||||
} else {
|
||||
this._classesTabButtonElement.classList.remove("disabled");
|
||||
this._showAllClassesButtonElement.classList.remove("disabled");
|
||||
this._hideAllClassesButtonElement.classList.remove("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
expandTreeViewToDepth(depth) {
|
||||
this._treeView.expandToDepth(depth);
|
||||
}
|
||||
|
||||
showNodeInTreeView(objectId) {
|
||||
this._treeView.collapse();
|
||||
this._treeView.showNode(objectId);
|
||||
}
|
||||
|
||||
unShowNodeInTreeView() {
|
||||
this._treeView.unShowNode();
|
||||
}
|
||||
|
||||
_onTreeNodeTitleSingleClicked(e) {
|
||||
const scene = this.viewer.scene;
|
||||
const objectIds = [];
|
||||
e.treeViewPlugin.withNodeTree(e.treeViewNode, (treeViewNode) => {
|
||||
if (treeViewNode.objectId) {
|
||||
objectIds.push(treeViewNode.objectId);
|
||||
}
|
||||
});
|
||||
const checked = e.treeViewNode.checked;
|
||||
if (checked) {
|
||||
scene.setObjectsXRayed(objectIds, false);
|
||||
scene.setObjectsVisible(objectIds, false);
|
||||
scene.setObjectsPickable(objectIds, true);
|
||||
} else {
|
||||
scene.setObjectsXRayed(objectIds, false);
|
||||
scene.setObjectsVisible(objectIds, true);
|
||||
scene.setObjectsPickable(objectIds, true);
|
||||
}
|
||||
}
|
||||
|
||||
_onTreeNodeTitleDoubleClicked(e) {
|
||||
this._focusTreeNodeInViewer(e);
|
||||
}
|
||||
|
||||
_focusTreeNodeInViewer(e) {
|
||||
focusTreeNodeInViewer(this.bimViewer, e.treeViewPlugin, e.treeViewNode);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this._treeView.destroy();
|
||||
this._treeViewContextMenu.destroy();
|
||||
this.viewer.scene.off(this._onModelLoaded);
|
||||
this.viewer.scene.off(this._onModelUnloaded);
|
||||
}
|
||||
}
|
||||
|
||||
export {ClassesExplorer};
|
||||
@@ -0,0 +1,481 @@
|
||||
import {math, XKTLoaderPlugin} from "@xeokit/xeokit-sdk/dist/xeokit-sdk.es.js";
|
||||
import {Controller} from "../Controller.js";
|
||||
import {ModelsContextMenu} from "../contextMenus/ModelsContextMenu.js";
|
||||
|
||||
const tempVec3a = math.vec3();
|
||||
|
||||
|
||||
/**
|
||||
* Custom data access strategy for {@link XKTLoaderPlugin}.
|
||||
* @private
|
||||
*/
|
||||
class BIMViewerDataSource {
|
||||
|
||||
constructor(server) {
|
||||
this._server = server;
|
||||
}
|
||||
|
||||
setProjectId(projectId) {
|
||||
this._projectId = projectId;
|
||||
}
|
||||
|
||||
setModelId(modelId) {
|
||||
this._modelId = modelId;
|
||||
}
|
||||
|
||||
getManifest(src, ok, error) {
|
||||
this._server.getSplitModelManifest(this._projectId, this._modelId, src, ok, error);
|
||||
}
|
||||
|
||||
getMetaModel(src, ok, error) {
|
||||
this._server.getSplitModelMetadata(this._projectId, this._modelId, src, ok, error);
|
||||
}
|
||||
|
||||
getXKT(src, ok, error) {
|
||||
this._server.getSplitModelGeometry(this._projectId, this._modelId, src, ok, error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @private */
|
||||
class ModelsExplorer extends Controller {
|
||||
|
||||
constructor(parent, cfg) {
|
||||
|
||||
super(parent, cfg);
|
||||
|
||||
if (!cfg.modelsTabElement) {
|
||||
throw "Missing config: modelsTabElement";
|
||||
}
|
||||
|
||||
if (!cfg.unloadModelsButtonElement) {
|
||||
throw "Missing config: unloadModelsButtonElement";
|
||||
}
|
||||
|
||||
if (!cfg.modelsElement) {
|
||||
throw "Missing config: modelsElement";
|
||||
}
|
||||
|
||||
this._containerElement = cfg.containerElement;
|
||||
this._enableAddModels = !!cfg.enableEditModels;
|
||||
this._modelsTabElement = cfg.modelsTabElement;
|
||||
this._loadModelsButtonElement = cfg.loadModelsButtonElement;
|
||||
this._unloadModelsButtonElement = cfg.unloadModelsButtonElement;
|
||||
this._addModelButtonElement = cfg.addModelButtonElement;
|
||||
this._modelsElement = cfg.modelsElement;
|
||||
this._modelsTabButtonElement = this._modelsTabElement.querySelector(".xeokit-tab-btn");
|
||||
|
||||
if (!this._modelsTabButtonElement) {
|
||||
throw "Missing DOM element: ,xeokit-tab-btn";
|
||||
}
|
||||
|
||||
this._dataSource = new BIMViewerDataSource(this.server);
|
||||
|
||||
this._xktLoader = new XKTLoaderPlugin(this.viewer, {
|
||||
dataSource: this._dataSource
|
||||
});
|
||||
|
||||
this._modelsContextMenu = new ModelsContextMenu({
|
||||
enableEditModels: cfg.enableEditModels,
|
||||
enableMeasurements: cfg.enableMeasurements,
|
||||
hideOnAction: true,
|
||||
parentNode: this._containerElement
|
||||
});
|
||||
|
||||
this._modelsInfo = {};
|
||||
this._numModels = 0;
|
||||
this._numModelsLoaded = 0;
|
||||
this._projectId = null;
|
||||
}
|
||||
|
||||
setObjectColors(objectColors) {
|
||||
this._xktLoader.objectDefaults = objectColors;
|
||||
}
|
||||
|
||||
loadProject(projectId, done, error) {
|
||||
this.server.getProject(projectId, (projectInfo) => {
|
||||
this.unloadProject();
|
||||
this._projectId = projectId;
|
||||
this._modelsInfo = {};
|
||||
this._numModels = 0;
|
||||
this._parseProject(projectInfo, done);
|
||||
if (this._numModelsLoaded < this._numModels) {
|
||||
this._loadModelsButtonElement.classList.remove("disabled");
|
||||
}
|
||||
if (this._numModelsLoaded > 0) {
|
||||
this._unloadModelsButtonElement.classList.remove("disabled");
|
||||
}
|
||||
if (this._enableAddModels) {
|
||||
this._addModelButtonElement.classList.remove("disabled");
|
||||
}
|
||||
}, (errMsg) => {
|
||||
this.error(errMsg);
|
||||
if (error) {
|
||||
error(errMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_parseProject(projectInfo, done) {
|
||||
this._buildModelsMenu(projectInfo);
|
||||
this._parseViewerConfigs(projectInfo);
|
||||
this._parseViewerContent(projectInfo, () => {
|
||||
this._parseViewerState(projectInfo, () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_buildModelsMenu(projectInfo) {
|
||||
var html = "";
|
||||
const modelsInfo = projectInfo.models || [];
|
||||
this._modelsInfo = {};
|
||||
this._numModels = modelsInfo.length;
|
||||
for (let i = 0, len = modelsInfo.length; i < len; i++) {
|
||||
const modelInfo = modelsInfo[i];
|
||||
this._modelsInfo[modelInfo.id] = modelInfo;
|
||||
html += "<div class='xeokit-form-check'>";
|
||||
html += "<input id='input-" + modelInfo.id + "' type='checkbox' value=''><span id='span-" + modelInfo.id + "' class='disabled'>" + modelInfo.name + "</span>";
|
||||
html += "</div>";
|
||||
}
|
||||
this._modelsElement.innerHTML = html;
|
||||
for (let i = 0, len = modelsInfo.length; i < len; i++) {
|
||||
const modelInfo = modelsInfo[i];
|
||||
const modelId = modelInfo.id;
|
||||
const checkBox = this._containerElement.querySelector("#input-" + modelId);
|
||||
const span = this._containerElement.querySelector("#span-" + modelId);
|
||||
checkBox.addEventListener("click", () => {
|
||||
if (checkBox.checked) {
|
||||
this.loadModel(modelId);
|
||||
} else {
|
||||
this.unloadModel(modelInfo.id);
|
||||
}
|
||||
});
|
||||
span.addEventListener("click", () => {
|
||||
const model = this.viewer.scene.models[modelId];
|
||||
const modelLoaded = (!!model);
|
||||
if (!modelLoaded) {
|
||||
this.loadModel(modelId);
|
||||
} else {
|
||||
this.unloadModel(modelInfo.id);
|
||||
}
|
||||
});
|
||||
span.oncontextmenu = (e) => {
|
||||
this._modelsContextMenu.context = {
|
||||
bimViewer: this.bimViewer,
|
||||
viewer: this.viewer,
|
||||
modelId: modelId
|
||||
};
|
||||
this._modelsContextMenu.show(e.pageX, e.pageY);
|
||||
e.preventDefault();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_parseViewerConfigs(projectInfo) {
|
||||
const viewerConfigs = projectInfo.viewerConfigs;
|
||||
if (viewerConfigs) {
|
||||
this.bimViewer.setConfigs(viewerConfigs);
|
||||
}
|
||||
}
|
||||
|
||||
_parseViewerContent(projectInfo, done) {
|
||||
const viewerContent = projectInfo.viewerContent;
|
||||
if (!viewerContent) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
this._parseModelsLoaded(viewerContent, () => {
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
_parseModelsLoaded(viewerContent, done) {
|
||||
const modelsLoaded = viewerContent.modelsLoaded;
|
||||
if (!modelsLoaded || (modelsLoaded.length === 0)) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
this._loadNextModel(modelsLoaded.slice(0), done);
|
||||
}
|
||||
|
||||
_loadNextModel(modelsLoaded, done) {
|
||||
if (modelsLoaded.length === 0) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const modelId = modelsLoaded.pop();
|
||||
this.loadModel(modelId,
|
||||
() => { // Done
|
||||
this._loadNextModel(modelsLoaded, done);
|
||||
},
|
||||
() => { // Error - recover and attempt to load next model
|
||||
this._loadNextModel(modelsLoaded, done);
|
||||
});
|
||||
}
|
||||
|
||||
_parseViewerState(projectInfo, done) {
|
||||
const viewerState = projectInfo.viewerState;
|
||||
if (!viewerState) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
this.bimViewer.setViewerState(viewerState, done);
|
||||
}
|
||||
|
||||
unloadProject() {
|
||||
if (!this._projectId) {
|
||||
return;
|
||||
}
|
||||
const models = this.viewer.scene.models;
|
||||
for (var modelId in models) {
|
||||
if (models.hasOwnProperty(modelId)) {
|
||||
const model = models[modelId];
|
||||
model.destroy();
|
||||
}
|
||||
}
|
||||
this._modelsElement.innerHTML = "";
|
||||
this._numModelsLoaded = 0;
|
||||
|
||||
this._loadModelsButtonElement.classList.add("disabled");
|
||||
this._unloadModelsButtonElement.classList.add("disabled");
|
||||
if (this._enableAddModels) {
|
||||
this._addModelButtonElement.classList.add("disabled");
|
||||
}
|
||||
const lastProjectId = this._projectId;
|
||||
this._projectId = null;
|
||||
this.fire("projectUnloaded", {
|
||||
projectId: lastProjectId
|
||||
});
|
||||
}
|
||||
|
||||
getLoadedProjectId() {
|
||||
return this._projectId;
|
||||
}
|
||||
|
||||
getModelIds() {
|
||||
return Object.keys(this._modelsInfo);
|
||||
}
|
||||
|
||||
loadModel(modelId, done, error) {
|
||||
if (!this._projectId) {
|
||||
const errMsg = "No project currently loaded";
|
||||
this.error(errMsg);
|
||||
if (error) {
|
||||
error(errMsg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const modelInfo = this._modelsInfo[modelId];
|
||||
if (!modelInfo) {
|
||||
const errMsg = "Model not in currently loaded project";
|
||||
this.error(errMsg);
|
||||
if (error) {
|
||||
error(errMsg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.bimViewer._busyModal.show(`${this.viewer.localeService.translate("busyModal.loading") || "Loading"} ${modelInfo.name}`);
|
||||
|
||||
const externalMetadata = this.bimViewer.getConfig("externalMetadata");
|
||||
|
||||
if (externalMetadata && !modelInfo.manifest) {
|
||||
this.server.getMetadata(this._projectId, modelId, (json) => {
|
||||
this._loadGeometry(modelId, modelInfo, json, done, error);
|
||||
},
|
||||
(errMsg) => {
|
||||
this.bimViewer._busyModal.hide();
|
||||
this.error(errMsg);
|
||||
if (error) {
|
||||
error(errMsg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._loadGeometry(modelId, modelInfo, null, done, error);
|
||||
}
|
||||
}
|
||||
|
||||
_loadGeometry(modelId, modelInfo, json, done, error) {
|
||||
|
||||
const modelLoaded = () => {
|
||||
const checkbox = this._containerElement.querySelector("#input-" + modelId);
|
||||
checkbox.checked = true;
|
||||
this._numModelsLoaded++;
|
||||
this._unloadModelsButtonElement.classList.remove("disabled");
|
||||
if (this._numModelsLoaded < this._numModels) {
|
||||
this._loadModelsButtonElement.classList.remove("disabled");
|
||||
} else {
|
||||
this._loadModelsButtonElement.classList.add("disabled");
|
||||
}
|
||||
if (this._numModelsLoaded === 1) { // Jump camera to view-fit first model loaded
|
||||
this._jumpToInitialCamera();
|
||||
this.fire("modelLoaded", modelId);
|
||||
this.bimViewer._busyModal.hide();
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
} else {
|
||||
this.fire("modelLoaded", modelId);
|
||||
this.bimViewer._busyModal.hide();
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadError = (errMsg) => {
|
||||
this.bimViewer._busyModal.hide();
|
||||
this.error(errMsg);
|
||||
if (error) {
|
||||
error(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
if (modelInfo.manifest) {
|
||||
|
||||
// Load multi-part split model;
|
||||
// Uses the BIMViewerDataSource, which then uses the BIMViewer's Server strategy
|
||||
|
||||
this._dataSource.setProjectId(this._projectId);
|
||||
this._dataSource.setModelId(modelId);
|
||||
|
||||
const model = this._xktLoader.load({
|
||||
id: modelId,
|
||||
manifestSrc: modelInfo.manifest,
|
||||
excludeUnclassifiedObjects: true,
|
||||
origin: modelInfo.origin || modelInfo.position,
|
||||
scale: modelInfo.scale,
|
||||
rotation: modelInfo.rotation,
|
||||
matrix: modelInfo.matrix,
|
||||
edges: (modelInfo.edges !== false),
|
||||
saoEnabled: modelInfo.saoEnabled,
|
||||
pbrEnabled: modelInfo.pbrEnabled,
|
||||
backfaces: modelInfo.backfaces,
|
||||
globalizeObjectIds: modelInfo.globalizeObjectIds,
|
||||
reuseGeometries: (modelInfo.reuseGeometries !== false)
|
||||
});
|
||||
|
||||
model.on("loaded", modelLoaded);
|
||||
model.on("error", loadError);
|
||||
|
||||
} else {
|
||||
|
||||
// Load single XKT/Metamodel file model;
|
||||
// Uses the BIMViewer's Server strategy directly
|
||||
|
||||
this.server.getGeometry(this._projectId, modelId, (arraybuffer) => {
|
||||
const model = this._xktLoader.load({
|
||||
id: modelId,
|
||||
metaModelData: json,
|
||||
xkt: arraybuffer,
|
||||
excludeUnclassifiedObjects: true,
|
||||
origin: modelInfo.origin || modelInfo.position,
|
||||
scale: modelInfo.scale,
|
||||
rotation: modelInfo.rotation,
|
||||
matrix: modelInfo.matrix,
|
||||
edges: (modelInfo.edges !== false),
|
||||
saoEnabled: modelInfo.saoEnabled,
|
||||
pbrEnabled: modelInfo.pbrEnabled,
|
||||
backfaces: modelInfo.backfaces,
|
||||
globalizeObjectIds: modelInfo.globalizeObjectIds,
|
||||
reuseGeometries: (modelInfo.reuseGeometries !== false)
|
||||
});
|
||||
model.on("loaded", modelLoaded);
|
||||
model.on("error", loadError);
|
||||
}, loadError);
|
||||
}
|
||||
}
|
||||
|
||||
_jumpToInitialCamera() {
|
||||
const viewer = this.viewer;
|
||||
const scene = viewer.scene;
|
||||
const aabb = scene.getAABB(scene.visibleObjectIds);
|
||||
const diag = math.getAABB3Diag(aabb);
|
||||
const center = math.getAABB3Center(aabb, tempVec3a);
|
||||
const camera = scene.camera;
|
||||
const fitFOV = camera.perspective.fov;
|
||||
const dist = Math.abs(diag / Math.tan(45 * math.DEGTORAD));
|
||||
const dir = math.normalizeVec3((camera.yUp) ? [-0.5, -0.7071, -0.5] : [-1, 1, -1]);
|
||||
const up = math.normalizeVec3((camera.yUp) ? [-0.5, 0.7071, -0.5] : [-1, 1, 1]);
|
||||
viewer.cameraControl.pivotPos = center;
|
||||
viewer.cameraControl.planView = false;
|
||||
viewer.cameraFlight.jumpTo({
|
||||
look: center,
|
||||
eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
|
||||
up: up,
|
||||
orthoScale: diag * 1.1
|
||||
});
|
||||
}
|
||||
|
||||
unloadModel(modelId) {
|
||||
const model = this.viewer.scene.models[modelId];
|
||||
if (!model) {
|
||||
this.error("Model not loaded: " + modelId);
|
||||
return;
|
||||
}
|
||||
model.destroy();
|
||||
const checkbox = this._containerElement.querySelector("#input-" + modelId);
|
||||
checkbox.checked = false;
|
||||
const span = this._containerElement.querySelector("#span-" + modelId);
|
||||
this._numModelsLoaded--;
|
||||
if (this._numModelsLoaded > 0) {
|
||||
this._unloadModelsButtonElement.classList.remove("disabled");
|
||||
} else {
|
||||
this._unloadModelsButtonElement.classList.add("disabled");
|
||||
}
|
||||
if (this._numModelsLoaded < this._numModels) {
|
||||
this._loadModelsButtonElement.classList.remove("disabled");
|
||||
} else {
|
||||
this._loadModelsButtonElement.classList.add("disabled");
|
||||
}
|
||||
this.fire("modelUnloaded", modelId);
|
||||
}
|
||||
|
||||
unloadAllModels() {
|
||||
const models = this.viewer.scene.models;
|
||||
const modelIds = Object.keys(models);
|
||||
for (var i = 0, len = modelIds.length; i < len; i++) {
|
||||
const modelId = modelIds[i];
|
||||
this.unloadModel(modelId);
|
||||
}
|
||||
}
|
||||
|
||||
getNumModelsLoaded() {
|
||||
return this._numModelsLoaded;
|
||||
}
|
||||
|
||||
_getLoadedModelIds() {
|
||||
return Object.keys(this.viewer.scene.models);
|
||||
}
|
||||
|
||||
isModelLoaded(modelId) {
|
||||
return (!!this.viewer.scene.models[modelId]);
|
||||
}
|
||||
|
||||
getModelsInfo() {
|
||||
return this._modelsInfo;
|
||||
}
|
||||
|
||||
getModelInfo(modelId) {
|
||||
return this._modelsInfo[modelId];
|
||||
}
|
||||
|
||||
setEnabled(enabled) {
|
||||
if (!enabled) {
|
||||
this._modelsTabButtonElement.classList.add("disabled");
|
||||
this._unloadModelsButtonElement.classList.add("disabled");
|
||||
} else {
|
||||
this._modelsTabButtonElement.classList.remove("disabled");
|
||||
this._unloadModelsButtonElement.classList.remove("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this._xktLoader.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export {ModelsExplorer};
|
||||
@@ -0,0 +1,179 @@
|
||||
import {TreeViewPlugin} from "@xeokit/xeokit-sdk/dist/xeokit-sdk.es.js";
|
||||
import {TreeViewContextMenu} from "../contextMenus/TreeViewContextMenu.js";
|
||||
import {Controller} from "../Controller.js";
|
||||
import {
|
||||
DEFAULT_DOUBLE_CLICK_THRESHOLD,
|
||||
focusTreeNodeInViewer
|
||||
} from "./treeViewFocusHelper.js";
|
||||
|
||||
/** @private */
|
||||
class ObjectsExplorer extends Controller {
|
||||
|
||||
constructor(parent, cfg = {}) {
|
||||
|
||||
super(parent);
|
||||
|
||||
if (!cfg.objectsTabElement) {
|
||||
throw "Missing config: objectsTabElement";
|
||||
}
|
||||
|
||||
if (!cfg.showAllObjectsButtonElement) {
|
||||
throw "Missing config: showAllObjectsButtonElement";
|
||||
}
|
||||
|
||||
if (!cfg.hideAllObjectsButtonElement) {
|
||||
throw "Missing config: hideAllObjectsButtonElement";
|
||||
}
|
||||
|
||||
if (!cfg.objectsElement) {
|
||||
throw "Missing config: objectsElement";
|
||||
}
|
||||
|
||||
this._containerElement = cfg.containerElement;
|
||||
this._objectsTabElement = cfg.objectsTabElement;
|
||||
this._showAllObjectsButtonElement = cfg.showAllObjectsButtonElement;
|
||||
this._hideAllObjectsButtonElement = cfg.hideAllObjectsButtonElement;
|
||||
this._objectsTabButtonElement = this._objectsTabElement.querySelector(".xeokit-tab-btn");
|
||||
|
||||
if (!this._objectsTabButtonElement) {
|
||||
throw "Missing DOM element: ,xeokit-tab-btn";
|
||||
}
|
||||
|
||||
const objectsElement = cfg.objectsElement;
|
||||
|
||||
this._treeView = new TreeViewPlugin(this.viewer, {
|
||||
containerElement: objectsElement,
|
||||
hierarchy: "containment",
|
||||
autoAddModels: false,
|
||||
pruneEmptyNodes: true
|
||||
});
|
||||
|
||||
this._treeViewContextMenu = new TreeViewContextMenu(this.bimViewer, {
|
||||
hideOnAction: true,
|
||||
enableMeasurements: cfg.enableMeasurements,
|
||||
parentNode: cfg.containerElement
|
||||
});
|
||||
|
||||
this._lastNodeClickTime = 0;
|
||||
this._lastNodeClickId = null;
|
||||
this._doubleClickThreshold = this.viewer.doubleClickTimeFrame || DEFAULT_DOUBLE_CLICK_THRESHOLD;
|
||||
|
||||
this._treeView.on("contextmenu", (e) => {
|
||||
this._treeViewContextMenu.context = {
|
||||
bimViewer: this.bimViewer,
|
||||
viewer: e.viewer,
|
||||
treeViewPlugin: e.treeViewPlugin,
|
||||
treeViewNode: e.treeViewNode
|
||||
};
|
||||
this._treeViewContextMenu.show(e.event.pageX, e.event.pageY);
|
||||
});
|
||||
|
||||
this._treeView.on("nodeTitleClicked", (e) => {
|
||||
const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
||||
const nodeId = e.treeViewNode.nodeId || e.treeViewNode.id;
|
||||
const clickCount = e.event && e.event.detail ? e.event.detail : 1;
|
||||
const isSameNode = (this._lastNodeClickId === nodeId);
|
||||
const withinThreshold = isSameNode && ((now - this._lastNodeClickTime) <= this._doubleClickThreshold);
|
||||
const isDoubleClick = (clickCount >= 2) || withinThreshold;
|
||||
|
||||
this._lastNodeClickTime = now;
|
||||
this._lastNodeClickId = nodeId;
|
||||
|
||||
if (isDoubleClick) {
|
||||
console.log("[ObjectsExplorer] double click", {
|
||||
nodeId: nodeId,
|
||||
title: e.treeViewNode.title || e.treeViewNode.name
|
||||
});
|
||||
this._onTreeNodeTitleDoubleClicked(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this._onTreeNodeTitleSingleClicked(e);
|
||||
});
|
||||
|
||||
this._onModelLoaded = this.viewer.scene.on("modelLoaded", (modelId) => {
|
||||
if (this.viewer.metaScene.metaModels[modelId]) {
|
||||
const modelInfo = this.bimViewer._modelsExplorer.getModelInfo(modelId);
|
||||
if (!modelInfo) {
|
||||
return;
|
||||
}
|
||||
this._treeView.addModel(modelId, {
|
||||
rootName: modelInfo.name
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => {
|
||||
if (this.viewer.metaScene.metaModels[modelId]) {
|
||||
this._treeView.removeModel(modelId);
|
||||
}
|
||||
});
|
||||
|
||||
this.bimViewer.on("reset", () => {
|
||||
this._treeView.collapse();
|
||||
});
|
||||
}
|
||||
|
||||
setEnabled(enabled) {
|
||||
if (!enabled) {
|
||||
this._objectsTabButtonElement.classList.add("disabled");
|
||||
this._showAllObjectsButtonElement.classList.add("disabled");
|
||||
this._hideAllObjectsButtonElement.classList.add("disabled");
|
||||
} else {
|
||||
this._objectsTabButtonElement.classList.remove("disabled");
|
||||
this._showAllObjectsButtonElement.classList.remove("disabled");
|
||||
this._hideAllObjectsButtonElement.classList.remove("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
expandTreeViewToDepth(depth) {
|
||||
this._treeView.expandToDepth(depth);
|
||||
}
|
||||
|
||||
showNodeInTreeView(objectId) {
|
||||
this._treeView.collapse();
|
||||
this._treeView.showNode(objectId);
|
||||
}
|
||||
|
||||
unShowNodeInTreeView() {
|
||||
this._treeView.unShowNode();
|
||||
}
|
||||
|
||||
_onTreeNodeTitleSingleClicked(e) {
|
||||
const scene = this.viewer.scene;
|
||||
const objectIds = [];
|
||||
e.treeViewPlugin.withNodeTree(e.treeViewNode, (treeViewNode) => {
|
||||
if (treeViewNode.objectId) {
|
||||
objectIds.push(treeViewNode.objectId);
|
||||
}
|
||||
});
|
||||
const checked = e.treeViewNode.checked;
|
||||
if (checked) {
|
||||
scene.setObjectsXRayed(objectIds, false);
|
||||
scene.setObjectsVisible(objectIds, false);
|
||||
scene.setObjectsPickable(objectIds, true);
|
||||
} else {
|
||||
scene.setObjectsXRayed(objectIds, false);
|
||||
scene.setObjectsVisible(objectIds, true);
|
||||
scene.setObjectsPickable(objectIds, true);
|
||||
}
|
||||
}
|
||||
|
||||
_onTreeNodeTitleDoubleClicked(e) {
|
||||
this._focusTreeNodeInViewer(e);
|
||||
}
|
||||
|
||||
_focusTreeNodeInViewer(e) {
|
||||
focusTreeNodeInViewer(this.bimViewer, e.treeViewPlugin, e.treeViewNode);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this._treeView.destroy();
|
||||
this._treeViewContextMenu.destroy();
|
||||
this.viewer.scene.off(this._onModelLoaded);
|
||||
this.viewer.scene.off(this._onModelUnloaded);
|
||||
}
|
||||
}
|
||||
|
||||
export {ObjectsExplorer};
|
||||
@@ -0,0 +1,237 @@
|
||||
import {TreeViewPlugin, math} from "@xeokit/xeokit-sdk/dist/xeokit-sdk.es.js";
|
||||
import {Controller} from "../Controller.js";
|
||||
import {TreeViewContextMenu} from "../contextMenus/TreeViewContextMenu.js";
|
||||
import {
|
||||
DEFAULT_DOUBLE_CLICK_THRESHOLD,
|
||||
focusTreeNodeInViewer
|
||||
} from "./treeViewFocusHelper.js";
|
||||
|
||||
const tempVec3 = math.vec3();
|
||||
|
||||
/** @private */
|
||||
class StoreysExplorer extends Controller {
|
||||
|
||||
constructor(parent, cfg = {}) {
|
||||
|
||||
super(parent);
|
||||
|
||||
if (!cfg.storeysTabElement) {
|
||||
throw "Missing config: storeysTabElement";
|
||||
}
|
||||
|
||||
if (!cfg.showAllStoreysButtonElement) {
|
||||
throw "Missing config: showAllStoreysButtonElement";
|
||||
}
|
||||
|
||||
if (!cfg.hideAllStoreysButtonElement) {
|
||||
throw "Missing config: hideAllStoreysButtonElement";
|
||||
}
|
||||
|
||||
if (!cfg.storeysElement) {
|
||||
throw "Missing config: storeysElement";
|
||||
}
|
||||
|
||||
this._containerElement = cfg.containerElement;
|
||||
this._storeysTabElement = cfg.storeysTabElement;
|
||||
this._showAllStoreysButtonElement = cfg.showAllStoreysButtonElement;
|
||||
this._hideAllStoreysButtonElement = cfg.hideAllStoreysButtonElement;
|
||||
this._storeysTabButtonElement = this._storeysTabElement.querySelector(".xeokit-tab-btn");
|
||||
|
||||
if (!this._storeysTabButtonElement) {
|
||||
throw "Missing DOM element: .xeokit-tab-btn";
|
||||
}
|
||||
|
||||
const storeysElement = cfg.storeysElement;
|
||||
|
||||
this._treeView = new TreeViewPlugin(this.viewer, {
|
||||
containerElement: storeysElement,
|
||||
autoAddModels: false,
|
||||
hierarchy: "storeys",
|
||||
autoExpandDepth: 1
|
||||
});
|
||||
|
||||
this._treeViewContextMenu = new TreeViewContextMenu(this.bimViewer, {
|
||||
hideOnAction: true,
|
||||
enableMeasurements: cfg.enableMeasurements,
|
||||
parentNode: cfg.containerElement
|
||||
});
|
||||
|
||||
this._lastNodeClickTime = 0;
|
||||
this._lastNodeClickId = null;
|
||||
this._doubleClickThreshold = this.viewer.doubleClickTimeFrame || DEFAULT_DOUBLE_CLICK_THRESHOLD;
|
||||
|
||||
this._treeView.on("contextmenu", (e) => {
|
||||
this._treeViewContextMenu.context = {
|
||||
bimViewer: this.bimViewer,
|
||||
viewer: e.viewer,
|
||||
treeViewPlugin: e.treeViewPlugin,
|
||||
treeViewNode: e.treeViewNode,
|
||||
pruneEmptyNodes: true
|
||||
};
|
||||
this._treeViewContextMenu.show(e.event.pageX, e.event.pageY);
|
||||
});
|
||||
|
||||
this._treeView.on("nodeTitleClicked", (e) => {
|
||||
const now = (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
||||
const nodeId = e.treeViewNode.nodeId || e.treeViewNode.id;
|
||||
const clickCount = e.event && e.event.detail ? e.event.detail : 1;
|
||||
const isSameNode = (this._lastNodeClickId === nodeId);
|
||||
const withinThreshold = isSameNode && ((now - this._lastNodeClickTime) <= this._doubleClickThreshold);
|
||||
const isDoubleClick = (clickCount >= 2) || withinThreshold;
|
||||
|
||||
this._lastNodeClickTime = now;
|
||||
this._lastNodeClickId = nodeId;
|
||||
|
||||
if (isDoubleClick) {
|
||||
console.log("[StoreysExplorer] double click", {
|
||||
nodeId: nodeId,
|
||||
title: e.treeViewNode.title || e.treeViewNode.name
|
||||
});
|
||||
this._onTreeNodeTitleDoubleClicked(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this._onTreeNodeTitleSingleClicked(e);
|
||||
});
|
||||
|
||||
this._onModelLoaded = this.viewer.scene.on("modelLoaded", (modelId) =>{
|
||||
const modelInfo = this.bimViewer._modelsExplorer.getModelInfo(modelId);
|
||||
if (!modelInfo) {
|
||||
return;
|
||||
}
|
||||
this._treeView.addModel(modelId, {
|
||||
rootName: modelInfo.name
|
||||
});
|
||||
});
|
||||
|
||||
this._onModelUnloaded = this.viewer.scene.on("modelUnloaded", (modelId) => {
|
||||
if (this.viewer.metaScene.metaModels[modelId]) {
|
||||
this._treeView.removeModel(modelId);
|
||||
}
|
||||
});
|
||||
|
||||
this.bimViewer.on("reset", () => {
|
||||
this._treeView.collapse();
|
||||
this._treeView.expandToDepth(1);
|
||||
});
|
||||
}
|
||||
|
||||
setEnabled(enabled) {
|
||||
if (!enabled) {
|
||||
this._storeysTabButtonElement.classList.add("disabled");
|
||||
this._showAllStoreysButtonElement.classList.add("disabled");
|
||||
this._hideAllStoreysButtonElement.classList.add("disabled");
|
||||
} else {
|
||||
this._storeysTabButtonElement.classList.remove("disabled");
|
||||
this._showAllStoreysButtonElement.classList.remove("disabled");
|
||||
this._hideAllStoreysButtonElement.classList.remove("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
expandTreeViewToDepth(depth) {
|
||||
this._treeView.expandToDepth(depth);
|
||||
}
|
||||
|
||||
showNodeInTreeView(objectId) {
|
||||
this._treeView.collapse();
|
||||
this._treeView.showNode(objectId);
|
||||
}
|
||||
|
||||
unShowNodeInTreeView() {
|
||||
this._treeView.unShowNode();
|
||||
}
|
||||
|
||||
_onTreeNodeTitleSingleClicked(e) {
|
||||
const scene = this.viewer.scene;
|
||||
const objectIds = [];
|
||||
e.treeViewPlugin.withNodeTree(e.treeViewNode, (treeViewNode) => {
|
||||
if (treeViewNode.objectId) {
|
||||
objectIds.push(treeViewNode.objectId);
|
||||
}
|
||||
});
|
||||
const checked = e.treeViewNode.checked;
|
||||
if (checked) {
|
||||
scene.setObjectsXRayed(objectIds, false);
|
||||
scene.setObjectsVisible(objectIds, false);
|
||||
scene.setObjectsPickable(objectIds, true);
|
||||
} else {
|
||||
scene.setObjectsXRayed(objectIds, false);
|
||||
scene.setObjectsVisible(objectIds, true);
|
||||
scene.setObjectsPickable(objectIds, true);
|
||||
}
|
||||
}
|
||||
|
||||
_onTreeNodeTitleDoubleClicked(e) {
|
||||
this._focusTreeNodeInViewer(e);
|
||||
}
|
||||
|
||||
_focusTreeNodeInViewer(e) {
|
||||
focusTreeNodeInViewer(this.bimViewer, e.treeViewPlugin, e.treeViewNode);
|
||||
}
|
||||
|
||||
selectStorey(storeyObjectId, done) {
|
||||
const metaScene = this.viewer.metaScene;
|
||||
const storeyMetaObject = metaScene.metaObjects[storeyObjectId];
|
||||
if (!storeyMetaObject) {
|
||||
this.error("selectStorey() - object is not found: '" + storeyObjectId + "'");
|
||||
return;
|
||||
}
|
||||
if (storeyMetaObject.type !== "IfcBuildingStorey") {
|
||||
this.error("selectStorey() - object is not found: '" + storeyObjectId + "'");
|
||||
return;
|
||||
}
|
||||
const objectIds = storeyMetaObject.getObjectIDsInSubtree();
|
||||
this._selectObjects(objectIds, done);
|
||||
}
|
||||
|
||||
_selectObjects(objectIds, done) {
|
||||
const scene = this.viewer.scene;
|
||||
const aabb = scene.getAABB(objectIds);
|
||||
|
||||
this.viewer.cameraControl.pivotPos = math.getAABB3Center(aabb, tempVec3);
|
||||
|
||||
if (done) {
|
||||
|
||||
scene.setObjectsXRayed(scene.objectIds, true);
|
||||
scene.setObjectsVisible(scene.objectIds, true);
|
||||
scene.setObjectsPickable(scene.objectIds, false);
|
||||
scene.setObjectsSelected(scene.selectedObjectIds, false);
|
||||
|
||||
scene.setObjectsXRayed(objectIds, false);
|
||||
scene.setObjectsVisible(objectIds, true);
|
||||
scene.setObjectsPickable(objectIds, true);
|
||||
|
||||
this.viewer.cameraFlight.flyTo({
|
||||
aabb: aabb
|
||||
}, () => {
|
||||
setTimeout(function () {
|
||||
scene.setObjectsVisible(scene.xrayedObjectIds, false);
|
||||
scene.setObjectsXRayed(scene.xrayedObjectIds, false);
|
||||
}, 500);
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
|
||||
scene.setObjectsVisible(scene.objectIds, false);
|
||||
scene.setObjectsPickable(scene.xrayedObjectIds, true);
|
||||
scene.setObjectsXRayed(scene.xrayedObjectIds, false);
|
||||
scene.setObjectsSelected(scene.selectedObjectIds, false);
|
||||
|
||||
scene.setObjectsVisible(objectIds, true);
|
||||
|
||||
this.viewer.cameraFlight.jumpTo({
|
||||
aabb: aabb
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this._treeView.destroy();
|
||||
this._treeViewContextMenu.destroy();
|
||||
this.viewer.scene.off(this._onModelLoaded);
|
||||
this.viewer.scene.off(this._onModelUnloaded);
|
||||
}
|
||||
}
|
||||
|
||||
export {StoreysExplorer};
|
||||
@@ -0,0 +1,69 @@
|
||||
import {math} from "@xeokit/xeokit-sdk/dist/xeokit-sdk.es.js";
|
||||
|
||||
const DEFAULT_DOUBLE_CLICK_THRESHOLD = 300;
|
||||
const tempVec3 = math.vec3();
|
||||
|
||||
function collectObjectIdsInSubtree(treeViewPlugin, treeViewNode) {
|
||||
const objectIds = [];
|
||||
if (!treeViewPlugin || !treeViewNode) {
|
||||
return objectIds;
|
||||
}
|
||||
treeViewPlugin.withNodeTree(treeViewNode, (node) => {
|
||||
if (node.objectId) {
|
||||
objectIds.push(node.objectId);
|
||||
}
|
||||
});
|
||||
return objectIds;
|
||||
}
|
||||
|
||||
function focusTreeNodeInViewer(bimViewer, treeViewPlugin, treeViewNode) {
|
||||
if (!bimViewer || !treeViewPlugin || !treeViewNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewer = bimViewer.viewer;
|
||||
const scene = viewer.scene;
|
||||
const objectIds = collectObjectIdsInSubtree(treeViewPlugin, treeViewNode);
|
||||
|
||||
if (objectIds.length === 0) {
|
||||
console.log("[treeViewFocusHelper] empty objectIds for node", treeViewNode);
|
||||
return;
|
||||
}
|
||||
|
||||
const aabb = scene.getAABB(objectIds);
|
||||
if (!aabb || !Array.isArray(aabb) || aabb.length !== 6) {
|
||||
console.log("[treeViewFocusHelper] no AABB for objectIds", objectIds);
|
||||
return;
|
||||
}
|
||||
console.log("[treeViewFocusHelper] flyTo", {
|
||||
nodeId: treeViewNode.nodeId || treeViewNode.id,
|
||||
objectIds: objectIds.slice(0, 10),
|
||||
count: objectIds.length,
|
||||
aabb: aabb
|
||||
});
|
||||
|
||||
// ensure visible/pickable before flying
|
||||
scene.setObjectsVisible(objectIds, true);
|
||||
scene.setObjectsPickable(objectIds, true);
|
||||
|
||||
const center = math.getAABB3Center(aabb, tempVec3);
|
||||
viewer.cameraControl.pivotPos = center;
|
||||
|
||||
viewer.cameraFlight.flyTo({
|
||||
aabb,
|
||||
duration: bimViewer.getConfig("viewFitDuration"),
|
||||
fitFOV: bimViewer.getConfig("viewFitFOV")
|
||||
}, () => {
|
||||
const primaryObjectId = treeViewNode.objectId || objectIds[0];
|
||||
if (primaryObjectId) {
|
||||
bimViewer.showObjectInExplorers(primaryObjectId);
|
||||
bimViewer.showObjectProperties(primaryObjectId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
DEFAULT_DOUBLE_CLICK_THRESHOLD,
|
||||
collectObjectIdsInSubtree,
|
||||
focusTreeNodeInViewer
|
||||
};
|
||||
Reference in New Issue
Block a user