Reference Source

src/plugins/BCFViewpointsPlugin/BCFViewpointsPlugin.js

  1. import {Plugin} from "../../viewer/Plugin.js";
  2. import {SectionPlane} from "../../viewer/scene/sectionPlane/SectionPlane.js";
  3. import {Bitmap} from "../../viewer/scene/Bitmap/index.js";
  4. import {LineSet} from "../../viewer/scene/LineSet/index.js";
  5.  
  6. import {math} from "../../viewer/scene/math/math.js";
  7.  
  8. const tempVec3 = math.vec3();
  9. const tempVec3a = math.vec3();
  10. const tempVec3b = math.vec3();
  11. const tempVec3c = math.vec3();
  12.  
  13. /**
  14. * {@link Viewer} plugin that saves and loads BCF viewpoints as JSON objects.
  15. *
  16. * [<img src="http://xeokit.github.io/xeokit-sdk/assets/images/BCFViewpointsPlugin.png">](/examples/index.html#BCF_SaveViewpoint)
  17. *
  18. * * [[Example 1: Saving viewer state to a BCF viewpoint](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_SaveViewpoint)]
  19. * * [[Example 2: Loading viewer state from a BCF viewpoint](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_LoadViewpoint)]
  20. *
  21. * ## Overview
  22. *
  23. * BCF is an open standard that enables workflow communications between BIM software tools. An XML schema, called
  24. * Building Collaboration Format (BCF), encodes messages that inform one BIM tool of issues found by another.
  25. *
  26. * A BCF viewpoint captures a viewpoint of a model that highlights an issue. The viewpoint can then be loaded by another
  27. * viewer to examine the issue.
  28. *
  29. * Using this plugin, a xeokit {@link Viewer} can exchange BCF-encoded viewpoints with other BIM software,
  30. * allowing us to use the Viewer to report and view issues in BIM models.
  31. *
  32. * This plugin's viewpoints conform to the <a href="https://github.com/buildingSMART/BCF-API">BCF Version 2.1</a> specification.
  33. *
  34. * ## Supported BCF Elements
  35. *
  36. * BCFViewpointsPlugin saves and loads the following state in BCF viewpoints:
  37. *
  38. * * {@link Camera} position, orientation and projection
  39. * * {@link Entity} visibilities and selection states
  40. * * {@link SectionPlane}s to slice the model
  41. * * {@link LineSet}s to show 3D lines
  42. * * {@link Bitmap}s to show images
  43. *
  44. * ## Saving a BCF Viewpoint
  45. *
  46. * In the example below we'll create a {@link Viewer}, load an ````.XKT```` model into it using an {@link XKTLoaderPlugin},
  47. * slice the model in half using a {@link SectionPlanesPlugin}, create a grid ground plane using a {@link LineSet} and a 2D
  48. * plan view using a {@link Bitmap}, then use a {@link BCFViewpointsPlugin#getViewpoint}
  49. * to save a viewpoint to JSON, which we'll log to the JavaScript developer console.
  50. *
  51. * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_SaveViewpoint)]
  52. *
  53. * ````javascript
  54. * import {Viewer, XKTLoaderPlugin, SectionPlanesPlugin,
  55. * LineSet, Bitmap, buildGridGeometry, BCFViewpointsPlugin} from "xeokit-sdk.es.js";
  56. *
  57. * // Create a Viewer
  58. * const viewer = new Viewer({
  59. * canvasId: "myCanvas",
  60. * transparent: true
  61. * });
  62. *
  63. * // Set camera position and orientation
  64. * viewer.scene.camera.eye = [-48.93, 54.54, 50.41];
  65. * viewer.scene.camera.look = [0.55, -0.61, -0.55];
  66. * viewer.scene.camera.up = [0, -1, 0];
  67. * viewer.scene.camera.perspective.fov = 60;
  68. *
  69. * // Add a XKTLoaderPlugin
  70. * const xktLoader = new XKTLoaderPlugin(viewer);
  71. *
  72. * // Add a SectionPlanesPlugin
  73. * const sectionPlanes = new SectionPlanesPlugin(viewer);
  74. *
  75. * // Add a BCFViewpointsPlugin
  76. * const bcfViewpoints = new BCFViewpointsPlugin(viewer);
  77. *
  78. * // Load an .XKT model
  79. * const modelNode = xktLoader.load({
  80. * id: "myModel",
  81. * src: "./models/xkt/Schependomlaan.xkt",
  82. * edges: true // Emphasise edges
  83. * });
  84. *
  85. * // Slice it in half
  86. * sectionPlanes.createSectionPlane({
  87. * id: "myClip",
  88. * pos: [0, 0, 0],
  89. * dir: [0.5, 0.0, 0.5]
  90. * });
  91. *
  92. * // Create a bitmap
  93. * const bitmap = new Bitmap(viewer.scene, {
  94. * src: "../assets/images/schependomlaanPlanView.png",
  95. * visible: true,
  96. * height: 24.0,
  97. * pos: [-15, 0, -10],
  98. * normal: [0, -1, 0],
  99. * up: [0, 0, 1],
  100. * collidable: false,
  101. * opacity: 1.0,
  102. * clippable: false,
  103. * pickable: true
  104. * });
  105. *
  106. * // Create a grid ground plane
  107. * const geometryArrays = buildGridGeometry({
  108. * size: 60,
  109. * divisions: 10
  110. * });
  111. *
  112. * new LineSet(viewer.scene, {
  113. * positions: geometryArrays.positions,
  114. * indices: geometryArrays.indices,
  115. * position: [10,0,10],
  116. * clippable: false
  117. * });
  118. *
  119. * // When model is loaded, select some objects and capture a BCF viewpoint to the console
  120. * modelNode.on("loaded", () => {
  121. *
  122. * const scene = viewer.scene;
  123. *
  124. * scene.setObjectsSelected([
  125. * "3b2U496P5Ebhz5FROhTwFH",
  126. * "2MGtJUm9nD$Re1_MDIv0g2",
  127. * "3IbuwYOm5EV9Q6cXmwVWqd",
  128. * "3lhisrBxL8xgLCRdxNG$2v",
  129. * "1uDn0xT8LBkP15zQc9MVDW"
  130. * ], true);
  131. *
  132. * const viewpoint = bcfViewpoints.getViewpoint();
  133. * const viewpointStr = JSON.stringify(viewpoint, null, 4);
  134. *
  135. * console.log(viewpointStr);
  136. * });
  137. * ````
  138. *
  139. * The saved BCF viewpoint would look something like below. Note that some elements are truncated for brevity.
  140. *
  141. * ````json
  142. * {
  143. * "perspective_camera": {
  144. * "camera_view_point": { "x": -48.93, "y": 54.54, "z": 50.41 },
  145. * "camera_direction": { "x": 0.55, "y": -0.61, "z": -0.55},
  146. * "camera_up_vector": { "x": 0.37, "y": -0.41, "z": 0.83 },
  147. * "field_of_view": 60.0
  148. * },
  149. * "lines": [{
  150. * "start_point": { "x": 1.0, "y": 1.0, "z": 1.0 },
  151. * "end_point": { "x": 0.0, "y": 0.0, "z": 0.0 },
  152. * //...(truncated)
  153. * }],
  154. * "bitmaps": [{
  155. * "bitmap_type": "png",
  156. * "bitmap_data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB9AAAAdp...", //...(truncated)
  157. * "location": { "x": -15, "y": 10, "z": 0 },
  158. * "normal": { "x": 0, "y": 0, "z": -1 },
  159. * "up": { "x": 0, "y": -1, "z": 0 },
  160. * "height": 24
  161. * }],
  162. * "clipping_planes": [{
  163. * "location": { "x": 0.0, "y": 0.0, "z": 0.0 },
  164. * "direction": { "x": 0.5, "y": 0.0, "z": 0.5 }
  165. * }],
  166. * "snapshot": {
  167. * "snapshot_type": "png",
  168. * "snapshot_data": "data:image/png;base64,......"
  169. * },
  170. * "components": {
  171. * "visibility": {
  172. * "default_visibility": false,
  173. * "exceptions": [{
  174. * "ifc_guid": "4$cshxZO9AJBebsni$z9Yk",
  175. * "originating_system": "xeokit.io",
  176. * "authoring_tool_id": "xeokit/v3.2"
  177. * },
  178. * //...
  179. * ]
  180. * },
  181. * "selection": [{
  182. * "ifc_guid": "4$cshxZO9AJBebsni$z9Yk",
  183. * },
  184. * //...
  185. * ]
  186. * }
  187. * }
  188. * ````
  189. *
  190. * ## Saving View Setup Hints
  191. *
  192. * BCFViewpointsPlugin can optionally save hints in the viewpoint, which indicate how to set up the view when
  193. * loading it again.
  194. *
  195. * Here's the {@link BCFViewpointsPlugin#getViewpoint} call again, this time saving some hints:
  196. *
  197. * ````javascript
  198. * const viewpoint = bcfViewpoints.getViewpoint({ // Options
  199. * spacesVisible: true, // Force IfcSpace types visible in the viewpoint (default is false)
  200. * spaceBoundariesVisible: false, // Show IfcSpace boundaries in the viewpoint (default is false)
  201. * openingsVisible: true // Force IfcOpening types visible in the viewpoint (default is false)
  202. * });
  203. * ````
  204. *
  205. * ## Loading a BCF Viewpoint
  206. *
  207. * Assuming that we have our BCF viewpoint in a JSON object, let's now restore it with {@link BCFViewpointsPlugin#setViewpoint}:
  208. *
  209. * ````javascript
  210. * bcfViewpoints.setViewpoint(viewpoint);
  211. * ````
  212. *
  213. * ## Handling BCF Incompatibility with xeokit's Camera
  214. *
  215. * xeokit's {@link Camera#look} is the current 3D *point-of-interest* (POI).
  216. *
  217. * A BCF viewpoint, however, has a direction vector instead of a POI, and so {@link BCFViewpointsPlugin#getViewpoint} saves
  218. * xeokit's POI as a normalized vector from {@link Camera#eye} to {@link Camera#look}, which unfortunately loses
  219. * that positional information. Loading the viewpoint with {@link BCFViewpointsPlugin#setViewpoint} will restore {@link Camera#look} to
  220. * the viewpoint's camera position, offset by the normalized vector.
  221. *
  222. * As shown below, providing a ````rayCast```` option to ````setViewpoint```` will set {@link Camera#look} to the closest
  223. * surface intersection on the direction vector. Internally, ````setViewpoint```` supports this option by firing a ray
  224. * along the vector, and if that hits an {@link Entity}, sets {@link Camera#look} to ray's intersection point with the
  225. * Entity's surface.
  226. *
  227. * ````javascript
  228. * bcfViewpoints.setViewpoint(viewpoint, {
  229. * rayCast: true // <<--------------- Attempt to set Camera#look to surface intersection point (default)
  230. * });
  231. * ````
  232. *
  233. * ## Dealing With Loaded Models that are not in the Viewpoint
  234. *
  235. * If, for example, we load model "duplex", hide some objects, then save a BCF viewpoint with
  236. * ````BCFViewpointsPlugin#getViewpoint````, then load another model, "schependomlaan", then load the viewpoint again
  237. * with ````BCFViewpointsPlugin#setViewpoint````, then sometimes all of the objects in model "schependomlaan" become
  238. * visible, along with the visible objects in the viewpoint, which belong to model "duplex".
  239. *
  240. * The reason is that, when saving a BCF viewpoint, BCF logic works like the following pseudo code:
  241. *
  242. * ````
  243. * If numVisibleObjects < numInvisibleObjects
  244. * save IDs of visible objects in BCF
  245. * exceptions = "visible objects"
  246. * else
  247. * save IDS of invisible objects in BCF
  248. * exceptions = "invisible objects"
  249. * ````
  250. *
  251. * When loading the viewpoint again:
  252. *
  253. * ````
  254. * If exceptions = "visible objects"
  255. * hide all objects
  256. * show visible objects in BCF
  257. * else
  258. * show all objects
  259. * hide invisible objects in BCF
  260. * ````
  261. *
  262. * When the exception is "visible objects", loading the viewpoint shows all the objects in the first, which includes
  263. * objects in "schependomlaan", which can be confusing, because those were not even loaded when we first
  264. * saved the viewpoint..
  265. *
  266. * To solve this, we can supply a ````defaultInvisible```` option to {@link BCFViewpointsPlugin#getViewpoint}, which
  267. * will force the plugin to save the IDs of all visible objects while making invisible objects the exception.
  268. *
  269. * That way, when we load the viewpoint again, after loading model "schependomlaan", the plugin will hide all objects
  270. * in the scene first (which will include objects belonging to model "schependomlaan"), then make the objects in the
  271. * viewpoint visible (which will only be those of object "duplex").
  272. *
  273. * ````javascript
  274. * const viewpoint = bcfViewpoints.getViewpoint({ // Options
  275. * //..
  276. * defaultInvisible: true
  277. * });
  278. * ````
  279. *
  280. * [[Run an example](/examples/index.html#BCF_LoadViewpoint_defaultInvisible)]
  281. *
  282. * ## Behaviour with XKTLoaderPlugin globalizeObjectIds
  283. *
  284. * Whenever we use {@link XKTLoaderPlugin} to load duplicate copies of the same model, after configuring
  285. * {@link XKTLoaderPlugin#globalizeObjectIds} ````true```` to avoid ````Entity```` ID clashes, this has consequences
  286. * for BCF viewpoints created by {@link BCFViewpointsPlugin#getViewpoint}.
  287. *
  288. * When no duplicate copies of a model are loaded like this, viewpoints created by {@link BCFViewpointsPlugin#getViewpoint} will
  289. * continue to load as usual in other BIM viewers. Conversely, a viewpoint created for a single model in other BIM viewers
  290. * will continue to load as usual with ````BCFViewpointsPlugin````.
  291. *
  292. * When duplicate copies of a model are loaded, however, viewpoints created by {@link BCFViewpointsPlugin#getViewpoint}
  293. * will contain certain changes that will affect the viewpoint's portability, however. Such viewpoints will
  294. * use ````authoring_tool_id```` fields to save the globalized ````Entity#id```` values, which enables the viewpoints to
  295. * capture the states of the individual ````Entitys```` that represent the duplicate IFC elements. Take a look at the
  296. * following two examples to learn more.
  297. *
  298. * * [Example: Saving a BCF viewpoint containing duplicate models](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_SaveViewpoint_MultipleModels)
  299. * * [Example: Loading a BCF viewpoint containing duplicate models](https://xeokit.github.io/xeokit-sdk/examples/index.html#BCF_LoadViewpoint_MultipleModels)
  300. *
  301. * **Caveat:** when loading a BCF viewpoint, we always assume that we have loaded in our target BIM viewer the same models that were
  302. * loaded in the viewpoint's original authoring application when the viewpoint was created. In the case of multi-model
  303. * viewpoints, the target BIM viewer, whether it be xeokit or another BIM viewer, will need to first have those exact
  304. * models loaded, with their objects having globalized IDs, following the same prefixing scheme we're using in
  305. * xeokit. Then, the viewpoint's ````authoring_tool_id```` fields will be able to resolve to their objects within the
  306. * target viewer.
  307. *
  308. * @class BCFViewpointsPlugin
  309. */
  310. class BCFViewpointsPlugin extends Plugin {
  311.  
  312. /**
  313. * @constructor
  314. * @param {Viewer} viewer The Viewer.
  315. * @param {Object} cfg Plugin configuration.
  316. * @param {String} [cfg.id="BCFViewpoints"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
  317. * @param {String} [cfg.originatingSystem] Identifies the originating system for BCF records.
  318. * @param {String} [cfg.authoringTool] Identifies the authoring tool for BCF records.
  319. */
  320. constructor(viewer, cfg = {}) {
  321.  
  322. super("BCFViewpoints", viewer, cfg);
  323.  
  324. /**
  325. * Identifies the originating system to include in BCF viewpoints saved by this plugin.
  326. * @property originatingSystem
  327. * @type {string}
  328. */
  329. this.originatingSystem = cfg.originatingSystem || "xeokit.io";
  330.  
  331. /**
  332. * Identifies the authoring tool to include in BCF viewpoints saved by this plugin.
  333. * @property authoringTool
  334. * @type {string}
  335. */
  336. this.authoringTool = cfg.authoringTool || "xeokit.io";
  337. }
  338.  
  339. /**
  340. * Saves viewer state to a BCF viewpoint.
  341. *
  342. * See ````BCFViewpointsPlugin```` class comments for more info.
  343. *
  344. * @param {*} [options] Options for getting the viewpoint.
  345. * @param {Boolean} [options.spacesVisible=false] Indicates whether ````IfcSpace```` types should be forced visible in the viewpoint.
  346. * @param {Boolean} [options.openingsVisible=false] Indicates whether ````IfcOpening```` types should be forced visible in the viewpoint.
  347. * @param {Boolean} [options.spaceBoundariesVisible=false] Indicates whether the boundaries of ````IfcSpace```` types should be visible in the viewpoint.
  348. * @param {Boolean} [options.spacesTranslucent=false] Indicates whether ````IfcSpace```` types should be forced translucent in the viewpoint.
  349. * @param {Boolean} [options.spaceBoundariesTranslucent=false] Indicates whether the boundaries of ````IfcSpace```` types should be forced translucent in the viewpoint.
  350. * @param {Boolean} [options.openingsTranslucent=true] Indicates whether ````IfcOpening```` types should be forced translucent in the viewpoint.
  351. * @param {Boolean} [options.snapshot=true] Indicates whether the snapshot should be included in the viewpoint.
  352. * @param {Boolean} [options.defaultInvisible=false] When ````true````, will save the default visibility of all objects
  353. * as ````false````. This means that when we load the viewpoint again, and there are additional models loaded that
  354. * were not saved in the viewpoint, those models will be hidden when we load the viewpoint, and that only the
  355. * objects in the viewpoint will be visible.
  356. * @param {Boolean} [options.reverseClippingPlanes=false] When ````true````, clipping planes are reversed (https://github.com/buildingSMART/BCF-XML/issues/193)
  357. * @returns {*} BCF JSON viewpoint object
  358. */
  359. getViewpoint(options = {}) {
  360. const scene = this.viewer.scene;
  361. const camera = scene.camera;
  362. const realWorldOffset = scene.realWorldOffset;
  363. const reverseClippingPlanes = (options.reverseClippingPlanes === true);
  364. let bcfViewpoint = {};
  365.  
  366. // Camera
  367. let lookDirection = math.normalizeVec3(math.subVec3(camera.look, camera.eye, math.vec3()));
  368. let eye = camera.eye;
  369. let up = camera.up;
  370.  
  371. if (camera.yUp) {
  372. // BCF is Z up
  373. lookDirection = YToZ(lookDirection);
  374. eye = YToZ(eye);
  375. up = YToZ(up);
  376. }
  377.  
  378. const camera_view_point = xyzArrayToObject(math.addVec3(eye, realWorldOffset));
  379.  
  380. if (camera.projection === "ortho") {
  381. bcfViewpoint.orthogonal_camera = {
  382. camera_view_point: camera_view_point,
  383. camera_direction: xyzArrayToObject(lookDirection),
  384. camera_up_vector: xyzArrayToObject(up),
  385. view_to_world_scale: camera.ortho.scale,
  386. };
  387. } else {
  388. bcfViewpoint.perspective_camera = {
  389. camera_view_point: camera_view_point,
  390. camera_direction: xyzArrayToObject(lookDirection),
  391. camera_up_vector: xyzArrayToObject(up),
  392. field_of_view: camera.perspective.fov,
  393. };
  394. }
  395.  
  396. // Section planes
  397.  
  398. const sectionPlanes = scene.sectionPlanes;
  399. for (let id in sectionPlanes) {
  400. if (sectionPlanes.hasOwnProperty(id)) {
  401. let sectionPlane = sectionPlanes[id];
  402. if (!sectionPlane.active) {
  403. continue;
  404. }
  405. let location = sectionPlane.pos;
  406.  
  407. let direction;
  408. if (reverseClippingPlanes) {
  409. direction = math.negateVec3(sectionPlane.dir, math.vec3());
  410. } else {
  411. direction = sectionPlane.dir;
  412. }
  413.  
  414. if (camera.yUp) {
  415. // BCF is Z up
  416. location = YToZ(location);
  417. direction = YToZ(direction);
  418. }
  419. math.addVec3(location, realWorldOffset);
  420.  
  421. location = xyzArrayToObject(location);
  422. direction = xyzArrayToObject(direction);
  423. if (!bcfViewpoint.clipping_planes) {
  424. bcfViewpoint.clipping_planes = [];
  425. }
  426. bcfViewpoint.clipping_planes.push({location, direction});
  427. }
  428. }
  429.  
  430. // Lines
  431.  
  432. const lineSets = scene.lineSets;
  433. for (let id in lineSets) {
  434. if (lineSets.hasOwnProperty(id)) {
  435. const lineSet = lineSets[id];
  436. if (!bcfViewpoint.lines) {
  437. bcfViewpoint.lines = [];
  438. }
  439. const positions = lineSet.positions;
  440. const indices = lineSet.indices;
  441. for (let i = 0, len = indices.length / 2; i < len; i++) {
  442. const a = indices[i * 2];
  443. const b = indices[(i * 2) + 1];
  444. bcfViewpoint.lines.push({
  445. start_point: {
  446. x: positions[a * 3 + 0],
  447. y: positions[a * 3 + 1],
  448. z: positions[a * 3 + 2]
  449. },
  450. end_point: {
  451. x: positions[b * 3 + 0],
  452. y: positions[b * 3 + 1],
  453. z: positions[b * 3 + 2]
  454. }
  455. });
  456. }
  457.  
  458. }
  459. }
  460.  
  461. // Bitmaps
  462.  
  463. const bitmaps = scene.bitmaps;
  464. for (let id in bitmaps) {
  465. if (bitmaps.hasOwnProperty(id)) {
  466. let bitmap = bitmaps[id];
  467. let location = bitmap.pos;
  468. let normal = bitmap.normal;
  469. let up = bitmap.up;
  470. if (camera.yUp) {
  471. // BCF is Z up
  472. location = YToZ(location);
  473. normal = YToZ(normal);
  474. up = YToZ(up);
  475. }
  476. math.addVec3(location, realWorldOffset);
  477. if (!bcfViewpoint.bitmaps) {
  478. bcfViewpoint.bitmaps = [];
  479. }
  480. bcfViewpoint.bitmaps.push({
  481. bitmap_type: bitmap.type,
  482. bitmap_data: bitmap.imageData,
  483. location: xyzArrayToObject(location),
  484. normal: xyzArrayToObject(normal),
  485. up: xyzArrayToObject(up),
  486. height: bitmap.height
  487. });
  488. }
  489. }
  490.  
  491. // Entity states
  492.  
  493. bcfViewpoint.components = {
  494. visibility: {
  495. view_setup_hints: {
  496. spaces_visible: !!options.spacesVisible,
  497. space_boundaries_visible: !!options.spaceBoundariesVisible,
  498. openings_visible: !!options.openingsVisible,
  499. spaces_translucent: !!options.spaces_translucent,
  500. space_boundaries_translucent: !!options.space_boundaries_translucent,
  501. openings_translucent: !!options.openings_translucent
  502. }
  503. }
  504. };
  505.  
  506. const opacityObjectIds = new Set(scene.opacityObjectIds);
  507. const xrayedObjectIds = new Set(scene.xrayedObjectIds);
  508. const colorizedObjectIds = new Set(scene.colorizedObjectIds);
  509.  
  510. const coloringMap = Object.values(scene.objects)
  511. .filter(entity => opacityObjectIds.has(entity.id) || colorizedObjectIds.has(entity.id) || xrayedObjectIds.has(entity.id))
  512. .reduce((coloringMap, entity) => {
  513.  
  514. let color = colorizeToRGB(entity.colorize);
  515. let alpha;
  516.  
  517. if (entity.xrayed) {
  518. if (scene.xrayMaterial.fillAlpha === 0.0 && scene.xrayMaterial.edgeAlpha !== 0.0) {
  519. // BCF can't deal with edges. If xRay is implemented only with edges, set an arbitrary opacity
  520. alpha = 0.1;
  521. } else {
  522. alpha = scene.xrayMaterial.fillAlpha;
  523. }
  524. alpha = Math.round(alpha * 255).toString(16).padStart(2, "0");
  525. color = alpha + color;
  526. } else if (opacityObjectIds.has(entity.id)) {
  527. alpha = Math.round(entity.opacity * 255).toString(16).padStart(2, "0");
  528. color = alpha + color;
  529. }
  530.  
  531. if (!coloringMap[color]) {
  532. coloringMap[color] = [];
  533. }
  534.  
  535. const objectId = entity.id;
  536. const originalSystemId = entity.originalSystemId;
  537. const component = {
  538. ifc_guid: originalSystemId,
  539. originating_system: this.originatingSystem
  540. };
  541. if (originalSystemId !== objectId) {
  542. component.authoring_tool_id = objectId;
  543. }
  544.  
  545. coloringMap[color].push(component);
  546.  
  547. return coloringMap;
  548.  
  549. }, {});
  550.  
  551. const coloringArray = Object.entries(coloringMap).map(([color, components]) => {
  552. return {color, components};
  553. });
  554.  
  555. bcfViewpoint.components.coloring = coloringArray;
  556.  
  557. const objectIds = scene.objectIds;
  558. const visibleObjects = scene.visibleObjects;
  559. const visibleObjectIds = scene.visibleObjectIds;
  560. const invisibleObjectIds = objectIds.filter(id => !visibleObjects[id]);
  561. const selectedObjectIds = scene.selectedObjectIds;
  562.  
  563. if (options.defaultInvisible || visibleObjectIds.length < invisibleObjectIds.length) {
  564. bcfViewpoint.components.visibility.exceptions = this._createBCFComponents(visibleObjectIds);
  565. bcfViewpoint.components.visibility.default_visibility = false;
  566. } else {
  567. bcfViewpoint.components.visibility.exceptions = this._createBCFComponents(invisibleObjectIds);
  568. bcfViewpoint.components.visibility.default_visibility = true;
  569. }
  570.  
  571. bcfViewpoint.components.selection = this._createBCFComponents(selectedObjectIds);
  572.  
  573. bcfViewpoint.components.translucency = this._createBCFComponents(scene.xrayedObjectIds);
  574.  
  575. if (options.snapshot !== false) {
  576. bcfViewpoint.snapshot = {
  577. snapshot_type: "png",
  578. snapshot_data: this.viewer.getSnapshot({format: "png"})
  579. };
  580. }
  581.  
  582. return bcfViewpoint;
  583. }
  584.  
  585. _createBCFComponents(objectIds) {
  586. const scene = this.viewer.scene;
  587. const components = [];
  588. for (let i = 0, len = objectIds.length; i < len; i++) {
  589. const objectId = objectIds[i];
  590. const entity = scene.objects[objectId];
  591. if (entity) {
  592. const component = {
  593. ifc_guid: entity.originalSystemId,
  594. originating_system: this.originatingSystem
  595. };
  596. if (entity.originalSystemId !== objectId) {
  597. component.authoring_tool_id = objectId;
  598. }
  599. components.push(component);
  600. }
  601. }
  602. return components;
  603. }
  604.  
  605. /**
  606. * Sets viewer state to the given BCF viewpoint.
  607. *
  608. * Note that xeokit's {@link Camera#look} is the **point-of-interest**, whereas the BCF ````camera_direction```` is a
  609. * direction vector. Therefore, when loading a BCF viewpoint, we set {@link Camera#look} to the absolute position
  610. * obtained by offsetting the BCF ````camera_view_point```` along ````camera_direction````.
  611. *
  612. * When loading a viewpoint, we also have the option to find {@link Camera#look} as the closest point of intersection
  613. * (on the surface of any visible and pickable {@link Entity}) with a 3D ray fired from ````camera_view_point```` in
  614. * the direction of ````camera_direction````.
  615. *
  616. * @param {*} bcfViewpoint BCF JSON viewpoint object,
  617. * shows default visible entities and restores camera to initial default position.
  618. * @param {*} [options] Options for setting the viewpoint.
  619. * @param {Boolean} [options.rayCast=true] When ````true```` (default), will attempt to set {@link Camera#look} to the closest
  620. * point of surface intersection with a ray fired from the BCF ````camera_view_point```` in the direction of ````camera_direction````.
  621. * @param {Boolean} [options.immediate=true] When ````true```` (default), immediately set camera position.
  622. * @param {Boolean} [options.duration] Flight duration in seconds. Overrides {@link CameraFlightAnimation#duration}. Only applies when ````immediate```` is ````false````.
  623. * @param {Boolean} [options.reset=true] When ````true```` (default), set {@link Entity#xrayed} and {@link Entity#highlighted} ````false```` on all scene objects.
  624. * @param {Boolean} [options.reverseClippingPlanes=false] When ````true````, clipping planes are reversed (https://github.com/buildingSMART/BCF-XML/issues/193)
  625. * @param {Boolean} [options.updateCompositeObjects=false] When ````true````, then when visibility and selection updates refer to composite objects (eg. an IfcBuildingStorey),
  626. * then this method will apply the updates to objects within those composites.
  627. */
  628. setViewpoint(bcfViewpoint, options = {}) {
  629. if (!bcfViewpoint) {
  630. return;
  631. }
  632.  
  633. const viewer = this.viewer;
  634. const scene = viewer.scene;
  635. const camera = scene.camera;
  636. const rayCast = (options.rayCast !== false);
  637. const immediate = (options.immediate !== false);
  638. const reset = (options.reset !== false);
  639. const realWorldOffset = scene.realWorldOffset;
  640. const reverseClippingPlanes = (options.reverseClippingPlanes === true);
  641.  
  642. scene.clearSectionPlanes();
  643.  
  644. if (bcfViewpoint.clipping_planes && bcfViewpoint.clipping_planes.length > 0) {
  645. bcfViewpoint.clipping_planes.forEach(function (e) {
  646. let pos = xyzObjectToArray(e.location, tempVec3);
  647. let dir = xyzObjectToArray(e.direction, tempVec3);
  648.  
  649. if (reverseClippingPlanes) {
  650. math.negateVec3(dir);
  651. }
  652. math.subVec3(pos, realWorldOffset);
  653.  
  654. if (camera.yUp) {
  655. pos = ZToY(pos);
  656. dir = ZToY(dir);
  657. }
  658. new SectionPlane(scene, {pos, dir});
  659. });
  660. }
  661.  
  662. scene.clearLines();
  663.  
  664. if (bcfViewpoint.lines && bcfViewpoint.lines.length > 0) {
  665. const positions = [];
  666. const indices = [];
  667. let i = 0;
  668. bcfViewpoint.lines.forEach((e) => {
  669. if (!e.start_point) {
  670. return;
  671. }
  672. if (!e.end_point) {
  673. return;
  674. }
  675. positions.push(e.start_point.x);
  676. positions.push(e.start_point.y);
  677. positions.push(e.start_point.z);
  678. positions.push(e.end_point.x);
  679. positions.push(e.end_point.y);
  680. positions.push(e.end_point.z);
  681. indices.push(i++);
  682. indices.push(i++);
  683. });
  684. new LineSet(scene, {
  685. positions,
  686. indices,
  687. clippable: false,
  688. collidable: true
  689. });
  690. }
  691.  
  692. scene.clearBitmaps();
  693.  
  694. if (bcfViewpoint.bitmaps && bcfViewpoint.bitmaps.length > 0) {
  695. bcfViewpoint.bitmaps.forEach(function (e) {
  696. const bitmap_type = e.bitmap_type || "jpg"; // "jpg" | "png"
  697. const bitmap_data = e.bitmap_data; // base64
  698. let location = xyzObjectToArray(e.location, tempVec3a);
  699. let normal = xyzObjectToArray(e.normal, tempVec3b);
  700. let up = xyzObjectToArray(e.up, tempVec3c);
  701. let height = e.height || 1;
  702. if (!bitmap_type) {
  703. return;
  704. }
  705. if (!bitmap_data) {
  706. return;
  707. }
  708. if (!location) {
  709. return;
  710. }
  711. if (!normal) {
  712. return;
  713. }
  714. if (!up) {
  715. return;
  716. }
  717. if (camera.yUp) {
  718. location = ZToY(location);
  719. normal = ZToY(normal);
  720. up = ZToY(up);
  721. }
  722. new Bitmap(scene, {
  723. src: bitmap_data,
  724. type: bitmap_type,
  725. pos: location,
  726. normal: normal,
  727. up: up,
  728. clippable: false,
  729. collidable: true,
  730. height
  731. });
  732. });
  733. }
  734.  
  735. if (reset) {
  736. scene.setObjectsXRayed(scene.xrayedObjectIds, false);
  737. scene.setObjectsHighlighted(scene.highlightedObjectIds, false);
  738. scene.setObjectsSelected(scene.selectedObjectIds, false);
  739. }
  740.  
  741. if (bcfViewpoint.components) {
  742.  
  743. if (bcfViewpoint.components.visibility) {
  744.  
  745. if (!bcfViewpoint.components.visibility.default_visibility) {
  746. scene.setObjectsVisible(scene.objectIds, false);
  747. if (bcfViewpoint.components.visibility.exceptions) {
  748. bcfViewpoint.components.visibility.exceptions.forEach((component) => this._withBCFComponent(options, component, entity => entity.visible = true));
  749. }
  750. } else {
  751. scene.setObjectsVisible(scene.objectIds, true);
  752. if (bcfViewpoint.components.visibility.exceptions) {
  753. bcfViewpoint.components.visibility.exceptions.forEach((component) => this._withBCFComponent(options, component, entity => entity.visible = false));
  754. }
  755. }
  756.  
  757. const view_setup_hints = bcfViewpoint.components.visibility.view_setup_hints;
  758. if (view_setup_hints) {
  759. if (view_setup_hints.spaces_visible === false) {
  760. scene.setObjectsVisible(viewer.metaScene.getObjectIDsByType("IfcSpace"), false);
  761. }
  762. if (view_setup_hints.spaces_translucent !== undefined) {
  763. scene.setObjectsXRayed(viewer.metaScene.getObjectIDsByType("IfcSpace"), true);
  764. }
  765. if (view_setup_hints.space_boundaries_visible !== undefined) {
  766.  
  767. }
  768. if (view_setup_hints.openings_visible === false) {
  769. scene.setObjectsVisible(viewer.metaScene.getObjectIDsByType("IfcOpening"), true);
  770. }
  771. if (view_setup_hints.space_boundaries_translucent !== undefined) {
  772.  
  773. }
  774. if (view_setup_hints.openings_translucent !== undefined) {
  775. scene.setObjectsXRayed(viewer.metaScene.getObjectIDsByType("IfcOpening"), true);
  776. }
  777. }
  778. }
  779.  
  780. if (bcfViewpoint.components.selection) {
  781. scene.setObjectsSelected(scene.selectedObjectIds, false);
  782. bcfViewpoint.components.selection.forEach(component => this._withBCFComponent(options, component, entity => entity.selected = true));
  783.  
  784. }
  785.  
  786. if (bcfViewpoint.components.translucency) {
  787. scene.setObjectsXRayed(scene.xrayedObjectIds, false);
  788. bcfViewpoint.components.translucency.forEach(component => this._withBCFComponent(options, component, entity => entity.xrayed = true));
  789. }
  790.  
  791. if (bcfViewpoint.components.coloring) {
  792. bcfViewpoint.components.coloring.forEach(coloring => {
  793.  
  794. let color = coloring.color;
  795. let alpha = 0;
  796. let alphaDefined = false;
  797.  
  798. if (color.length === 8) {
  799. alpha = parseInt(color.substring(0, 2), 16) / 256;
  800. if (alpha <= 1.0 && alpha >= 0.95) {
  801. alpha = 1.0;
  802. }
  803. color = color.substring(2);
  804. alphaDefined = true;
  805. }
  806.  
  807. const colorize = [
  808. parseInt(color.substring(0, 2), 16) / 256,
  809. parseInt(color.substring(2, 4), 16) / 256,
  810. parseInt(color.substring(4, 6), 16) / 256
  811. ];
  812.  
  813. coloring.components.map(component =>
  814. this._withBCFComponent(options, component, entity => {
  815. entity.colorize = colorize;
  816. if (alphaDefined) {
  817. entity.opacity = alpha;
  818. }
  819. }));
  820. });
  821. }
  822. }
  823.  
  824. if (bcfViewpoint.perspective_camera || bcfViewpoint.orthogonal_camera) {
  825. let eye;
  826. let look;
  827. let up;
  828. let projection;
  829.  
  830. if (bcfViewpoint.perspective_camera) {
  831. eye = xyzObjectToArray(bcfViewpoint.perspective_camera.camera_view_point, tempVec3);
  832. look = xyzObjectToArray(bcfViewpoint.perspective_camera.camera_direction, tempVec3);
  833. up = xyzObjectToArray(bcfViewpoint.perspective_camera.camera_up_vector, tempVec3);
  834.  
  835. camera.perspective.fov = bcfViewpoint.perspective_camera.field_of_view;
  836.  
  837. projection = "perspective";
  838. } else {
  839. eye = xyzObjectToArray(bcfViewpoint.orthogonal_camera.camera_view_point, tempVec3);
  840. look = xyzObjectToArray(bcfViewpoint.orthogonal_camera.camera_direction, tempVec3);
  841. up = xyzObjectToArray(bcfViewpoint.orthogonal_camera.camera_up_vector, tempVec3);
  842.  
  843. camera.ortho.scale = bcfViewpoint.orthogonal_camera.view_to_world_scale;
  844.  
  845. projection = "ortho";
  846. }
  847.  
  848. math.subVec3(eye, realWorldOffset);
  849.  
  850. if (camera.yUp) {
  851. eye = ZToY(eye);
  852. look = ZToY(look);
  853. up = ZToY(up);
  854. }
  855.  
  856. if (rayCast) {
  857. const hit = scene.pick({
  858. pickSurface: true, // <<------ This causes picking to find the intersection point on the entity
  859. origin: eye,
  860. direction: look
  861. });
  862. look = (hit ? hit.worldPos : math.addVec3(eye, look, tempVec3));
  863. } else {
  864. look = math.addVec3(eye, look, tempVec3);
  865. }
  866.  
  867. if (immediate) {
  868. camera.eye = eye;
  869. camera.look = look;
  870. camera.up = up;
  871. camera.projection = projection;
  872. } else {
  873. viewer.cameraFlight.flyTo({eye, look, up, duration: options.duration, projection});
  874. }
  875. }
  876. }
  877.  
  878. _withBCFComponent(options, component, callback) {
  879.  
  880. const viewer = this.viewer;
  881. const scene = viewer.scene;
  882.  
  883. if (component.authoring_tool_id && component.originating_system === this.originatingSystem) {
  884.  
  885. const id = component.authoring_tool_id;
  886. const entity = scene.objects[id];
  887.  
  888. if (entity) {
  889. callback(entity);
  890. return
  891. }
  892.  
  893. if (options.updateCompositeObjects) {
  894. const metaObject = viewer.metaScene.metaObjects[id];
  895. if (metaObject) {
  896. scene.withObjects(viewer.metaScene.getObjectIDsInSubtree(id), callback);
  897. return;
  898. }
  899. }
  900. }
  901.  
  902. if (component.ifc_guid) {
  903.  
  904. const originalSystemId = component.ifc_guid;
  905. const entity = scene.objects[originalSystemId];
  906.  
  907. if (entity) {
  908. callback(entity);
  909. return;
  910. }
  911.  
  912. if (options.updateCompositeObjects) {
  913. const metaObject = viewer.metaScene.metaObjects[originalSystemId];
  914. if (metaObject) {
  915. scene.withObjects(viewer.metaScene.getObjectIDsInSubtree(originalSystemId), callback);
  916. return;
  917. }
  918. }
  919.  
  920. Object.keys(scene.models).forEach((modelId) => {
  921.  
  922. const id = math.globalizeObjectId(modelId, originalSystemId);
  923. const entity = scene.objects[id];
  924.  
  925. if (entity) {
  926. callback(entity);
  927. return;
  928. }
  929.  
  930. if (options.updateCompositeObjects) {
  931. const metaObject = viewer.metaScene.metaObjects[id];
  932. if (metaObject) {
  933. scene.withObjects(viewer.metaScene.getObjectIDsInSubtree(id), callback);
  934.  
  935. }
  936. }
  937. });
  938. }
  939. }
  940.  
  941. /**
  942. * Destroys this BCFViewpointsPlugin.
  943. */
  944. destroy() {
  945. super.destroy();
  946. }
  947. }
  948.  
  949. function xyzArrayToObject(arr) {
  950. return {"x": arr[0], "y": arr[1], "z": arr[2]};
  951. }
  952.  
  953. function xyzObjectToArray(xyz, arry) {
  954. arry = new Float64Array(3);
  955. arry[0] = xyz.x;
  956. arry[1] = xyz.y;
  957. arry[2] = xyz.z;
  958. return arry;
  959. }
  960.  
  961. function YToZ(vec) {
  962. return new Float64Array([vec[0], -vec[2], vec[1]]);
  963. }
  964.  
  965. function ZToY(vec) {
  966. return new Float64Array([vec[0], vec[2], -vec[1]]);
  967. }
  968.  
  969. function colorizeToRGB(color) {
  970. let rgb = "";
  971. rgb += Math.round(color[0] * 255).toString(16).padStart(2, "0");
  972. rgb += Math.round(color[1] * 255).toString(16).padStart(2, "0");
  973. rgb += Math.round(color[2] * 255).toString(16).padStart(2, "0");
  974. return rgb;
  975. }
  976.  
  977. export {BCFViewpointsPlugin};