<html>

<head>
  <script src="https://api.catenda.com/js/v1/viewer3d"></script>
</head>

<body>

  <div id="splash">
    <div id="title">
      <div id="title-text">
        Selection
      </div>
      <div id="play">
        <button id="play-button" disabled=true onclick="start()">
          <i id="play-icon" class="fa fa-play"></i>
        </button>
      </div>
    </div>
    <div id="error">
      The example is unavailable<br>
      Please try again later
    </div>
    <div id="logo">
      <image id="logo-image" src="https://archbee-image-uploads.s3.amazonaws.com/v9iuLCcKE2amnKqiFVllF/qCmgrBQdRA0cjxeSZ0Rvn_logo-api-light.png" />
    </div>
  </div>
  <div id="gui"></div>
  <div id="scene">
    <div id="viewer-container">
      <div id="viewer-3d"></div>
    </div>
    <div id="console">
      <div id="console-header">
        <div id="console-header-text">Output</div>
        <button id="console-copy" onclick="copyConsole()">
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M11 12H1C0.447 12 0 11.553 0 11V1C0 0.448 0.447 0 1 0H11C11.553 0 12 0.448 12 1V11C12 11.553 11.553 12 11 12Z" fill="currentColor" />
            <path d="M15 16H4V14H14V4H16V15C16 15.553 15.553 16 15 16Z" fill="currentColor" />
          </svg>
          <span id="tooltip">Copy</span>
        </button>
      </div>
      <div id="console-text"></div>
    </div>
  </div>
  <script type="text/javascript">
    bimsync.setOnLoadCallback(function() {
      document.getElementById("play-button").disabled = false
    });
    bimsync.load();
  </script>
</body>

</html>
html {
  height: 100%;
  overflow: hidden;
  width: 100%;
}

body {
  background-color: #ade0bc;
  background-image: url("https://archbee-image-uploads.s3.amazonaws.com/oGjtxJ5MUZmGondFfGBOZ-0l0obCRKymJG7Q9KTtAhl-20240826-204445.png");
  background-position: left;
  background-repeat: no-repeat;
  background-size: cover;
  color: #004d47;
  font-family: Inter;
  height: 100%;
  margin: 0;
  overflow: hidden;
  padding: 0;
  width: 100%;
}

#console {
  display: flex;
  flex-direction: column;
  height: 100px;
  width: 100%;
  /* Be above measurements */
  z-index: 1;
}

#console-copy {
  align-items: center;
  background-color: #f3f3f5;
  border: none;
  color: #6e7574;
  display: flex;
  justify-content: center;
  margin-right: 16px;
  padding: 5px;
  /* Be above measurements */
  z-index: 1;
}

#console-copy:hover {
  color: #212322;
  cursor: pointer;
}

#console-copy:active {
  color: #c5c7c7;
}

#console-copy #tooltip {
  border-radius: 6px;
  bottom: 102px;
  background-color: black;
  color: #fff;
  font-size: 12px;
  padding: 10px;
  position: absolute;
  right: 2px;
  visibility: hidden;
  width: 40px;
}

#console-copy:hover #tooltip {
  visibility: visible;
}

#console-header {
  align-items: center;
  background-color: #f3f3f5;
  display: flex;
  height: 32px;
  justify-content: space-between;
}

#console-header-text {
  color: #6e7574;
  font-size: 14px;
  margin-left: 16px;
  user-select: none;
}

#console-text {
  background-color: white;
  color: black;
  flex: 1;
  font-family: monospace;
  font-size: 14px;
  overflow: auto;
  padding: 10px 16px 10px 16px;
  word-break: break-all;
}

#error {
  align-self: center;
  bottom: 50px;
  display: none;
  font-size: 20px;
  font-weight: 600;
  position: absolute;
  text-align: center;
}

#gui {
  display: none;
  left: 20px;
  position: absolute;
  top: 20px;
  /* Be above measurements */
  z-index: 1;
}

#logo {
  bottom: 15px;
  position: absolute;
  right: 15px;
}

#logo-image {
  width: 200px;
}

#play {
  align-items: center;
  display: flex;
  justify-content: center;
}

#play-button {
  align-items: center;
  background-color: #004d47;
  border-color: transparent;
  border-radius: 50%;
  color: #ade0bc;
  display: flex;
  font-size: 26px;
  height: 60px;
  justify-content: center;
  margin-top: 24px;
  width: 60px;
}

#play-button:disabled {
  cursor: wait;
}

#play-button:hover:enabled {
  background-color: #001e1c;
  cursor: pointer;
}

#play-icon {
  padding-left: 5px;
}

#scene {
  display: flex;
  flex-direction: column;
  height: 100%;
  justify-content: flex-end;
  width: 100%;
}

#splash {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 100%;
}

#splash #title {
  display: flex;
  flex: 1;
  flex-direction: column;
  font-size: 36px;
  font-weight: 600;
  height: 100%;
  justify-content: center;
  text-align: center;
}

#title-text {
  align-items: center;
  display: flex;
  flex-direction: row;
  justify-content: center;
}

#viewer-container {
  flex: 1;
  overflow: hidden;
}

#viewer-3d {
  background: linear-gradient(rgb(237, 255, 254) 0px, rgb(255, 255, 255));
  height: 100%;
  width: 100%;
}

.lil-gui {
  --background-color: #30343a;
  --focus-color: #00665e;
  --hover-color: #005952;
  --number-color: #ade0bc;
  --string-color: #ade0bc;
  --text-color: #ade0bc;
  --title-background-color: #24272b;
  --title-text-color: #ade0bc;
  --widget-color: #004d47;
  --width: 180px;
}
// Global variables to ease access
let viewer3d = null;
let gui = null;
const modelId = "my-model-id";

// Enum of Ids for HTML elements referenced in this file
const ElementId = {
  CONSOLE_COPY: "console-copy",
  CONSOLE_TEXT: "console-text",
  ERROR: "error",
  GUI: "gui",
  LOADING_ICON: "loading-icon",
  PLAY_BUTTON: "play-button",
  PLAY_ICON: "play-icon",
  SPLASH: "splash",
  TOOLTIP: "tooltip",
  VIEWER_3D: "viewer-3d"
};

// Enum of style values used in this file
const Style = {
  FLEX: "flex",
  NONE: "none"
};

// Reset tooltip text
const consoleCopy = document.getElementById(ElementId.CONSOLE_COPY);
const onCopyLeave = function () {
  document.getElementById(ElementId.TOOLTIP).innerHTML = "Copy";
};
consoleCopy.addEventListener("mouseleave", onCopyLeave);
consoleCopy.addEventListener("touchend", onCopyLeave);

/**
 *  Catenda 3D viewer Selection Example.
 *  The runExample function contains the 3D viewer related calls to fulfill the example shown.
 */

// The code specific to the example
const runExample = function () {
  // gui labels
  const DESELECT = "Deselect";
  const DESELECT_ALL = "Deselect All";
  const DESELECT_DOORS = "Deselect Doors";
  const DESELECT_WALLS = "Deselect Walls";
  const DESELECT_WINDOWS = "Deselect Windows";
  const SELECT = "Select";
  const SELECT_DOORS = "Select Doors";
  const SELECT_WALLS = "Select Walls";
  const SELECT_WINDOWS = "Select Windows";
  const SELECTED = "Selected";

  // Keep track of number of selected objects
  let selectedCount = 0;

  // Hide spaces in the 3D viewer
  hideSpaces();

  // Utility function to deselect products by provided ifc type
  const deselectProducts = function (ifcType) {
    // Filer products by ifc type
    const products = viewer3d
      .getProducts()
      .filter((product) => product.ifcType === ifcType);
    const objectIds = products.map(({ objectId }) => objectId);
    viewer3d.deselect(objectIds);
    const selectedObjectIds = viewer3d.getSelected();
    setOutput(selectedObjectIds);
  };

  // Utility function to select products by provided ifc type
  const selectProducts = function (ifcType) {
    // Filer products by ifc type
    const products = viewer3d
      .getProducts()
      .filter((product) => product.ifcType === ifcType);
    const objectIds = products.map(({ objectId }) => objectId);
    viewer3d.select(objectIds);
    const selectedObjectIds = viewer3d.getSelected();
    setOutput(selectedObjectIds);
  };

  /**
   * Define button callbacks
   */
  const onDeselectAllClick = function () {
    viewer3d.deselectAll();
    const selectedObjectIds = viewer3d.getSelected();
    setOutput(selectedObjectIds);
    disableGuiProperty(DESELECT_ALL);
    deselectFolder.controllers.forEach((controller) => {
      controller.disable();
    });
    selectFolder.controllers.forEach((controller) => {
      controller.enable();
    });
    selectedCount = 0;
  };
  const onDeselectDoorsClick = function (ifcType) {
    deselectProducts("IfcDoor");
    enableGuiProperty(SELECT_DOORS);
    disableGuiProperty(DESELECT_DOORS);
    // Is this the last selection to be deselected
    if (--selectedCount === 0) {
      disableGuiProperty(DESELECT_ALL);
    }
  };
  const onDeselectWallsClick = function (ifcType) {
    deselectProducts("IfcWallStandardCase");
    enableGuiProperty(SELECT_WALLS);
    disableGuiProperty(DESELECT_WALLS);
    // Is this the last selection to be deselected
    if (--selectedCount === 0) {
      disableGuiProperty(DESELECT_ALL);
    }
  };
  const onDeselectWindowsClick = function (ifcType) {
    deselectProducts("IfcWindow");
    enableGuiProperty(SELECT_WINDOWS);
    disableGuiProperty(DESELECT_WINDOWS);
    // Is this the last selection to be deselected
    if (--selectedCount === 0) {
      disableGuiProperty(DESELECT_ALL);
    }
  };
  const onSelectDoorsClick = function (ifcType) {
    selectProducts("IfcDoor");
    disableGuiProperty(SELECT_DOORS);
    enableGuiProperty(DESELECT_ALL);
    enableGuiProperty(DESELECT_DOORS);
    selectedCount++;
  };
  const onSelectWallsClick = function (ifcType) {
    selectProducts("IfcWallStandardCase");
    disableGuiProperty(SELECT_WALLS);
    enableGuiProperty(DESELECT_ALL);
    enableGuiProperty(DESELECT_WALLS);
    selectedCount++;
  };
  const onSelectWindowsClick = function (ifcType) {
    selectProducts("IfcWindow");
    disableGuiProperty(SELECT_WINDOWS);
    enableGuiProperty(DESELECT_ALL);
    enableGuiProperty(DESELECT_WINDOWS);
    selectedCount++;
  };

  // Map of gui items to data and callbacks
  const state = {
    [DESELECT_ALL]: onDeselectAllClick,
    [DESELECT_DOORS]: onDeselectDoorsClick,
    [DESELECT_WALLS]: onDeselectWallsClick,
    [DESELECT_WINDOWS]: onDeselectWindowsClick,
    [SELECT_DOORS]: onSelectDoorsClick,
    [SELECT_WALLS]: onSelectWallsClick,
    [SELECT_WINDOWS]: onSelectWindowsClick
  };

  /**
   * Build gui panel
   */
  const selectFolder = gui.addFolder(SELECT);
  selectFolder.add(state, SELECT_DOORS);
  selectFolder.add(state, SELECT_WALLS);
  selectFolder.add(state, SELECT_WINDOWS);
  const deselectFolder = gui.addFolder(DESELECT);
  deselectFolder.add(state, DESELECT_DOORS).disable();
  deselectFolder.add(state, DESELECT_WALLS).disable();
  deselectFolder.add(state, DESELECT_WINDOWS).disable();
  deselectFolder.add(state, DESELECT_ALL).disable();

  /**
   * Listen to viewer events
   */
  const onClick = function (event) {
    event.preventDefault();
  };
  viewer3d.addEventListener("viewer3d.click", onClick);
};

/**
 *  Code required for bootstrapping a 3D viewer instance with which to execute the example
 */

// Bootstrap a 3D viewer instance and load a model
const initialize = async function (onViewerReady) {
  // Show the loading indicator until the 3D viewer is ready
  showLoadingIndicator();
  // Hide any previous error message
  hideElement(ElementId.ERROR);

  // Create a gui instance for 3D viewer controls
  gui = new lil.GUI({
    container: document.getElementById("gui")
  });
  gui.title("Options");

  // HTML Div element where the 3D viewer is mounted
  const div3d = document.getElementById(ElementId.VIEWER_3D);

  // Options used when creating a 3D viewer instance
  const viewerOptions = {
    enableTouch: true,
    textRenderMode: "dom"
  };

  // Create a 3D viewer instance
  viewer3d = new bimsync.viewer3d.Viewer3D(div3d, viewerOptions);

  // Options used when loading the model
  const loadModelOptions = {
    modelId: modelId
  };

  // Fetch a 3D viewer token for the model to load
  const url3d = await getToken();
  // Load the model
  viewer3d
    .loadModelsFromToken(url3d, loadModelOptions)
    .then(function () {
      // 3D viewer is ready, stop loading
      showPlayButton();
      // Hide the splash screen
      hideElement(ElementId.SPLASH);
      // Show the 3D viewer gui
      showElement(ElementId.GUI);
      // 3D viewer is ready, run the callback
      onViewerReady();
    })
    .catch(function (error) {
      showError();
    });
};

// Entry point called when user clicks the play button
const start = function () {
  // Bootstrap a 3D viewer instance then run the example
  initialize(runExample);
};

/**
 * Utility functions used in this file
 */

// Set console text in clipboard
const copyConsole = async function () {
  await navigator.clipboard.writeText(
    document.getElementById(ElementId.CONSOLE_TEXT).innerHTML
  );
  document.getElementById(ElementId.TOOLTIP).innerHTML = "Copied!";
};

// Disable a property in the gui panel
const disableGuiProperty = function (propertyName) {
  gui
    .controllersRecursive()
    .find(({ property }) => property === propertyName)
    .disable();
};
// Enable a property in the gui panel
const enableGuiProperty = function (propertyName) {
  gui
    .controllersRecursive()
    .find(({ property }) => property === propertyName)
    .enable();
};

// Retrieve a 3D viewer token
const getToken = async function () {
  /**
   * Fetch a 3D viewer token from the Catenda API
   * Here we are using a CodePen specific endpoint for demo purposes
   * Replace this with your own token endpoint in a real application
   * Find out more at https://developers.catenda.com/viewer-3d/bim-models
   * The model this token provides access to is licensed under the Creative Commons Attribution 4.0 International License.
   * (C) original authors
   * https://github.com/buildingSMART/Sample-Test-Files
   * More info and a link to the full license text is available on http://creativecommons.org/licenses/by/4.0/
   */
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const tryCount = 4;
  let response, result;
  // Try to fetch the token a few times before giving up
  for (let i = 1; i <= tryCount; i++) {
    response = await fetch(
      "https://documentation-edge.developers.catenda.com/token3d"
    );
    if (response.ok) {
      break;
    } else {
      // If the response is not ok, we wait a bit before trying again
      if (i < tryCount) {
        await delay(i * 1000);
      }
      continue;
    }
  }
  // If the response is ok, we can proceed
  if (response && response.ok) {
    const data = await response.json();
    result = data.url;
  } else {
    showError();
  }
  return result;
};

// Hide an HTML element passing its id
const hideElement = function (elementId) {
  document.getElementById(elementId).style.display = Style.NONE;
};

// Hide spaces in the 3D viewer
const hideSpaces = function () {
  const spaceObjectIds = viewer3d
    .getProducts()
    .filter((product) => product.ifcType === "IfcSpace")
    .map((space) => space.objectId);
  viewer3d.hide(spaceObjectIds);
};

// Set the text displayed in the output panel
const setOutput = function (value) {
  const text = value instanceof Object ? JSON.stringify(value) : value;
  document.getElementById(ElementId.CONSOLE_TEXT).innerHTML = text;
};

// Show an HTML element passing its id
const showElement = function (elementId) {
  document.getElementById(elementId).style.display = Style.FLEX;
};

// Reset after an error
const showError = function () {
  viewer3d.dispose();
  gui.destroy();
  showPlayButton();
  showElement(ElementId.ERROR);
};

// Display the loading indicator
const showLoadingIndicator = function () {
  const playButton = document.getElementById(ElementId.PLAY_BUTTON);
  playButton.disabled = true;
  const playIconElement = document.getElementById(ElementId.PLAY_ICON);
  let loadingIcon = document.createElement("i");
  loadingIcon.id = ElementId.LOADING_ICON;
  loadingIcon.classList.add("fa", "fa-spinner", "fa-spin");
  playIconElement.replaceWith(loadingIcon);
};

// Display the play button
const showPlayButton = function () {
  const playButton = document.getElementById(ElementId.PLAY_BUTTON);
  playButton.disabled = false;
  const loadingIconElement = document.getElementById(ElementId.LOADING_ICON);
  let playIcon = document.createElement("i");
  playIcon.id = ElementId.PLAY_ICON;
  playIcon.classList.add("fa", "fa-play");
  loadingIconElement.replaceWith(playIcon);
};

// Update a value used in the gui panel to reflect a change
const updateGuiPropertyValue = function (propertyName, value) {
  gui
    .controllersRecursive()
    .find(({ property }) => property === propertyName)
    .setValue(value);
};

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css

External JavaScript

  1. https://cdn.jsdelivr.net/npm/lil-gui@0.19