Initial import of NODEDC BIM Viewer

This commit is contained in:
CODEX
2026-06-04 13:25:42 +03:00
commit 9aaf82862f
2075 changed files with 3592575 additions and 0 deletions
+179
View File
@@ -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};
+481
View File
@@ -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};
+179
View File
@@ -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};
+237
View File
@@ -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};
+69
View File
@@ -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
};