Reference Source

src/explorer/ModelsExplorer.js

  1. import {math, XKTLoaderPlugin} from "@xeokit/xeokit-sdk/dist/xeokit-sdk.es.js";
  2. import {Controller} from "../Controller.js";
  3. import {ModelsContextMenu} from "../contextMenus/ModelsContextMenu.js";
  4.  
  5. const tempVec3a = math.vec3();
  6.  
  7.  
  8. /**
  9. * Custom data access strategy for {@link XKTLoaderPlugin}.
  10. * @private
  11. */
  12. class BIMViewerDataSource {
  13.  
  14. constructor(server) {
  15. this._server = server;
  16. }
  17.  
  18. setProjectId(projectId) {
  19. this._projectId = projectId;
  20. }
  21.  
  22. setModelId(modelId) {
  23. this._modelId = modelId;
  24. }
  25.  
  26. getManifest(src, ok, error) {
  27. this._server.getSplitModelManifest(this._projectId, this._modelId, src, ok, error);
  28. }
  29.  
  30. getMetaModel(src, ok, error) {
  31. this._server.getSplitModelMetadata(this._projectId, this._modelId, src, ok, error);
  32. }
  33.  
  34. getXKT(src, ok, error) {
  35. this._server.getSplitModelGeometry(this._projectId, this._modelId, src, ok, error)
  36. }
  37. }
  38.  
  39.  
  40. /** @private */
  41. class ModelsExplorer extends Controller {
  42.  
  43. constructor(parent, cfg) {
  44.  
  45. super(parent, cfg);
  46.  
  47. if (!cfg.modelsTabElement) {
  48. throw "Missing config: modelsTabElement";
  49. }
  50.  
  51. if (!cfg.unloadModelsButtonElement) {
  52. throw "Missing config: unloadModelsButtonElement";
  53. }
  54.  
  55. if (!cfg.modelsElement) {
  56. throw "Missing config: modelsElement";
  57. }
  58.  
  59. this._enableAddModels = !!cfg.enableEditModels;
  60. this._modelsTabElement = cfg.modelsTabElement;
  61. this._loadModelsButtonElement = cfg.loadModelsButtonElement;
  62. this._unloadModelsButtonElement = cfg.unloadModelsButtonElement;
  63. this._addModelButtonElement = cfg.addModelButtonElement;
  64. this._modelsElement = cfg.modelsElement;
  65. this._modelsTabButtonElement = this._modelsTabElement.querySelector(".xeokit-tab-btn");
  66.  
  67. if (!this._modelsTabButtonElement) {
  68. throw "Missing DOM element: ,xeokit-tab-btn";
  69. }
  70.  
  71. this._dataSource = new BIMViewerDataSource(this.server);
  72.  
  73. this._xktLoader = new XKTLoaderPlugin(this.viewer, {
  74. dataSource: this._dataSource
  75. });
  76.  
  77. this._modelsContextMenu = new ModelsContextMenu({
  78. enableEditModels: cfg.enableEditModels,
  79. hideOnAction: true
  80. });
  81.  
  82. this._modelsInfo = {};
  83. this._numModels = 0;
  84. this._numModelsLoaded = 0;
  85. this._projectId = null;
  86. }
  87.  
  88. setObjectColors(objectColors) {
  89. this._xktLoader.objectDefaults = objectColors;
  90. }
  91.  
  92. loadProject(projectId, done, error) {
  93. this.server.getProject(projectId, (projectInfo) => {
  94. this.unloadProject();
  95. this._projectId = projectId;
  96. this._modelsInfo = {};
  97. this._numModels = 0;
  98. this._parseProject(projectInfo, done);
  99. if (this._numModelsLoaded < this._numModels) {
  100. this._loadModelsButtonElement.classList.remove("disabled");
  101. }
  102. if (this._numModelsLoaded > 0) {
  103. this._unloadModelsButtonElement.classList.remove("disabled");
  104. }
  105. if (this._enableAddModels) {
  106. this._addModelButtonElement.classList.remove("disabled");
  107. }
  108. }, (errMsg) => {
  109. this.error(errMsg);
  110. if (error) {
  111. error(errMsg);
  112. }
  113. });
  114. }
  115.  
  116. _parseProject(projectInfo, done) {
  117. this._buildModelsMenu(projectInfo);
  118. this._parseViewerConfigs(projectInfo);
  119. this._parseViewerContent(projectInfo, () => {
  120. this._parseViewerState(projectInfo, () => {
  121. done();
  122. });
  123. });
  124. }
  125.  
  126. _buildModelsMenu(projectInfo) {
  127. var html = "";
  128. const modelsInfo = projectInfo.models || [];
  129. this._modelsInfo = {};
  130. this._numModels = modelsInfo.length;
  131. for (let i = 0, len = modelsInfo.length; i < len; i++) {
  132. const modelInfo = modelsInfo[i];
  133. this._modelsInfo[modelInfo.id] = modelInfo;
  134. html += "<div class='xeokit-form-check'>";
  135. html += "<input id='" + modelInfo.id + "' type='checkbox' value=''><span id='span-" + modelInfo.id + "' class='disabled'>" + modelInfo.name + "</span>";
  136. html += "</div>";
  137. }
  138. this._modelsElement.innerHTML = html;
  139. for (let i = 0, len = modelsInfo.length; i < len; i++) {
  140. const modelInfo = modelsInfo[i];
  141. const modelId = modelInfo.id;
  142. const checkBox = document.getElementById("" + modelId);
  143. const span = document.getElementById("span-" + modelId);
  144. checkBox.addEventListener("click", () => {
  145. if (checkBox.checked) {
  146. this.loadModel(modelId);
  147. } else {
  148. this.unloadModel(modelInfo.id);
  149. }
  150. });
  151. span.addEventListener("click", () => {
  152. const model = this.viewer.scene.models[modelId];
  153. const modelLoaded = (!!model);
  154. if (!modelLoaded) {
  155. this.loadModel(modelId);
  156. } else {
  157. this.unloadModel(modelInfo.id);
  158. }
  159. });
  160. span.oncontextmenu = (e) => {
  161. this._modelsContextMenu.context = {
  162. bimViewer: this.bimViewer,
  163. viewer: this.viewer,
  164. modelId: modelId
  165. };
  166. this._modelsContextMenu.show(e.pageX, e.pageY);
  167. e.preventDefault();
  168. };
  169. }
  170. }
  171.  
  172. _parseViewerConfigs(projectInfo) {
  173. const viewerConfigs = projectInfo.viewerConfigs;
  174. if (viewerConfigs) {
  175. this.bimViewer.setConfigs(viewerConfigs);
  176. }
  177. }
  178.  
  179. _parseViewerContent(projectInfo, done) {
  180. const viewerContent = projectInfo.viewerContent;
  181. if (!viewerContent) {
  182. done();
  183. return;
  184. }
  185. this._parseModelsLoaded(viewerContent, () => {
  186. done();
  187. });
  188. }
  189.  
  190. _parseModelsLoaded(viewerContent, done) {
  191. const modelsLoaded = viewerContent.modelsLoaded;
  192. if (!modelsLoaded || (modelsLoaded.length === 0)) {
  193. done();
  194. return;
  195. }
  196. this._loadNextModel(modelsLoaded.slice(0), done);
  197. }
  198.  
  199. _loadNextModel(modelsLoaded, done) {
  200. if (modelsLoaded.length === 0) {
  201. done();
  202. return;
  203. }
  204. const modelId = modelsLoaded.pop();
  205. this.loadModel(modelId,
  206. () => { // Done
  207. this._loadNextModel(modelsLoaded, done);
  208. },
  209. () => { // Error - recover and attempt to load next model
  210. this._loadNextModel(modelsLoaded, done);
  211. });
  212. }
  213.  
  214. _parseViewerState(projectInfo, done) {
  215. const viewerState = projectInfo.viewerState;
  216. if (!viewerState) {
  217. done();
  218. return;
  219. }
  220. this.bimViewer.setViewerState(viewerState, done);
  221. }
  222.  
  223. unloadProject() {
  224. if (!this._projectId) {
  225. return;
  226. }
  227. const models = this.viewer.scene.models;
  228. for (var modelId in models) {
  229. if (models.hasOwnProperty(modelId)) {
  230. const model = models[modelId];
  231. model.destroy();
  232. }
  233. }
  234. this._modelsElement.innerHTML = "";
  235. this._numModelsLoaded = 0;
  236.  
  237. this._loadModelsButtonElement.classList.add("disabled");
  238. this._unloadModelsButtonElement.classList.add("disabled");
  239. if (this._enableAddModels) {
  240. this._addModelButtonElement.classList.add("disabled");
  241. }
  242. const lastProjectId = this._projectId;
  243. this._projectId = null;
  244. this.fire("projectUnloaded", {
  245. projectId: lastProjectId
  246. });
  247. }
  248.  
  249. getLoadedProjectId() {
  250. return this._projectId;
  251. }
  252.  
  253. getModelIds() {
  254. return Object.keys(this._modelsInfo);
  255. }
  256.  
  257. loadModel(modelId, done, error) {
  258. if (!this._projectId) {
  259. const errMsg = "No project currently loaded";
  260. this.error(errMsg);
  261. if (error) {
  262. error(errMsg);
  263. }
  264. return;
  265. }
  266. const modelInfo = this._modelsInfo[modelId];
  267. if (!modelInfo) {
  268. const errMsg = "Model not in currently loaded project";
  269. this.error(errMsg);
  270. if (error) {
  271. error(errMsg);
  272. }
  273. return;
  274. }
  275.  
  276. this.bimViewer._busyModal.show(`${this.viewer.localeService.translate("busyModal.loading") || "Loading"} ${modelInfo.name}`);
  277.  
  278. const externalMetadata = this.bimViewer.getConfig("externalMetadata");
  279.  
  280. if (externalMetadata && !modelInfo.manifest) {
  281. this.server.getMetadata(this._projectId, modelId, (json) => {
  282. this._loadGeometry(modelId, modelInfo, json, done, error);
  283. },
  284. (errMsg) => {
  285. this.bimViewer._busyModal.hide();
  286. this.error(errMsg);
  287. if (error) {
  288. error(errMsg);
  289. }
  290. });
  291. } else {
  292. this._loadGeometry(modelId, modelInfo, null, done, error);
  293. }
  294. }
  295.  
  296. _loadGeometry(modelId, modelInfo, json, done, error) {
  297.  
  298. const modelLoaded = () => {
  299. const checkbox = document.getElementById("" + modelId);
  300. checkbox.checked = true;
  301. this._numModelsLoaded++;
  302. this._unloadModelsButtonElement.classList.remove("disabled");
  303. if (this._numModelsLoaded < this._numModels) {
  304. this._loadModelsButtonElement.classList.remove("disabled");
  305. } else {
  306. this._loadModelsButtonElement.classList.add("disabled");
  307. }
  308. if (this._numModelsLoaded === 1) { // Jump camera to view-fit first model loaded
  309. this._jumpToInitialCamera();
  310. this.fire("modelLoaded", modelId);
  311. this.bimViewer._busyModal.hide();
  312. if (done) {
  313. done();
  314. }
  315. } else {
  316. this.fire("modelLoaded", modelId);
  317. this.bimViewer._busyModal.hide();
  318. if (done) {
  319. done();
  320. }
  321. }
  322. };
  323.  
  324. const loadError = (errMsg) => {
  325. this.bimViewer._busyModal.hide();
  326. this.error(errMsg);
  327. if (error) {
  328. error(errMsg);
  329. }
  330. };
  331.  
  332. if (modelInfo.manifest) {
  333.  
  334. // Load multi-part split model;
  335. // Uses the BIMViewerDataSource, which then uses the BIMViewer's Server strategy
  336.  
  337. this._dataSource.setProjectId(this._projectId);
  338. this._dataSource.setModelId(modelId);
  339.  
  340. const model = this._xktLoader.load({
  341. id: modelId,
  342. manifestSrc: modelInfo.manifest,
  343. excludeUnclassifiedObjects: true,
  344. origin: modelInfo.origin || modelInfo.position,
  345. scale: modelInfo.scale,
  346. rotation: modelInfo.rotation,
  347. matrix: modelInfo.matrix,
  348. edges: (modelInfo.edges !== false),
  349. saoEnabled: modelInfo.saoEnabled,
  350. pbrEnabled: modelInfo.pbrEnabled,
  351. backfaces: modelInfo.backfaces,
  352. globalizeObjectIds: modelInfo.globalizeObjectIds,
  353. reuseGeometries: (modelInfo.reuseGeometries !== false)
  354. });
  355.  
  356. model.on("loaded", modelLoaded);
  357. model.on("error", loadError);
  358.  
  359. } else {
  360.  
  361. // Load single XKT/Metamodel file model;
  362. // Uses the BIMViewer's Server strategy directly
  363.  
  364. this.server.getGeometry(this._projectId, modelId, (arraybuffer) => {
  365. const model = this._xktLoader.load({
  366. id: modelId,
  367. metaModelData: json,
  368. xkt: arraybuffer,
  369. excludeUnclassifiedObjects: true,
  370. origin: modelInfo.origin || modelInfo.position,
  371. scale: modelInfo.scale,
  372. rotation: modelInfo.rotation,
  373. matrix: modelInfo.matrix,
  374. edges: (modelInfo.edges !== false),
  375. saoEnabled: modelInfo.saoEnabled,
  376. pbrEnabled: modelInfo.pbrEnabled,
  377. backfaces: modelInfo.backfaces,
  378. globalizeObjectIds: modelInfo.globalizeObjectIds,
  379. reuseGeometries: (modelInfo.reuseGeometries !== false)
  380. });
  381. model.on("loaded", modelLoaded);
  382. model.on("error", loadError);
  383. }, loadError);
  384. }
  385. }
  386.  
  387. _jumpToInitialCamera() {
  388. const viewer = this.viewer;
  389. const scene = viewer.scene;
  390. const aabb = scene.getAABB(scene.visibleObjectIds);
  391. const diag = math.getAABB3Diag(aabb);
  392. const center = math.getAABB3Center(aabb, tempVec3a);
  393. const camera = scene.camera;
  394. const fitFOV = camera.perspective.fov;
  395. const dist = Math.abs(diag / Math.tan(45 * math.DEGTORAD));
  396. const dir = math.normalizeVec3((camera.yUp) ? [-0.5, -0.7071, -0.5] : [-1, 1, -1]);
  397. const up = math.normalizeVec3((camera.yUp) ? [-0.5, 0.7071, -0.5] : [-1, 1, 1]);
  398. viewer.cameraControl.pivotPos = center;
  399. viewer.cameraControl.planView = false;
  400. viewer.cameraFlight.jumpTo({
  401. look: center,
  402. eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
  403. up: up,
  404. orthoScale: diag * 1.1
  405. });
  406. }
  407.  
  408. unloadModel(modelId) {
  409. const model = this.viewer.scene.models[modelId];
  410. if (!model) {
  411. this.error("Model not loaded: " + modelId);
  412. return;
  413. }
  414. model.destroy();
  415. const checkbox = document.getElementById("" + modelId);
  416. checkbox.checked = false;
  417. const span = document.getElementById("span-" + modelId);
  418. this._numModelsLoaded--;
  419. if (this._numModelsLoaded > 0) {
  420. this._unloadModelsButtonElement.classList.remove("disabled");
  421. } else {
  422. this._unloadModelsButtonElement.classList.add("disabled");
  423. }
  424. if (this._numModelsLoaded < this._numModels) {
  425. this._loadModelsButtonElement.classList.remove("disabled");
  426. } else {
  427. this._loadModelsButtonElement.classList.add("disabled");
  428. }
  429. this.fire("modelUnloaded", modelId);
  430. }
  431.  
  432. unloadAllModels() {
  433. const models = this.viewer.scene.models;
  434. const modelIds = Object.keys(models);
  435. for (var i = 0, len = modelIds.length; i < len; i++) {
  436. const modelId = modelIds[i];
  437. this.unloadModel(modelId);
  438. }
  439. }
  440.  
  441. getNumModelsLoaded() {
  442. return this._numModelsLoaded;
  443. }
  444.  
  445. _getLoadedModelIds() {
  446. return Object.keys(this.viewer.scene.models);
  447. }
  448.  
  449. isModelLoaded(modelId) {
  450. return (!!this.viewer.scene.models[modelId]);
  451. }
  452.  
  453. getModelsInfo() {
  454. return this._modelsInfo;
  455. }
  456.  
  457. getModelInfo(modelId) {
  458. return this._modelsInfo[modelId];
  459. }
  460.  
  461. setEnabled(enabled) {
  462. if (!enabled) {
  463. this._modelsTabButtonElement.classList.add("disabled");
  464. this._unloadModelsButtonElement.classList.add("disabled");
  465. } else {
  466. this._modelsTabButtonElement.classList.remove("disabled");
  467. this._unloadModelsButtonElement.classList.remove("disabled");
  468. }
  469. }
  470.  
  471. /** @private */
  472. destroy() {
  473. super.destroy();
  474. this._xktLoader.destroy();
  475. }
  476. }
  477.  
  478. export {ModelsExplorer};