<html>

<head>
  <script src="https://api.catenda.com/js/v1/viewer2d"></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-2d"></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.setOnViewer2dLoadCallback(function() {
      document.getElementById("play-button").disabled = false
    });
    bimsync.loadViewer2d();
  </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%;
}
#console-copy {
  align-items: center;
  background-color: #f3f3f5;
  border: none;
  color: #6e7574;
  display: flex;
  justify-content: center;
  margin-right: 16px;
  padding: 5px;
}

#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;
}

#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-2d {
  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 viewer2d = 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_2D: "viewer-2d"
};

// 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 2D viewer Selection Example.
 *  The runExample function contains the 2D viewer related calls to fulfill the example shown.
 */

// The code specific to the example
const runExample = function () {
  // gui labels
  const DESELECT = "Deselect an Object";
  const DESELECT_ALL = "Deselect All Objects";
  const SELECT = "Select an Object";

  const disableGuiProperty = function (propertyName) {
    gui
      .controllersRecursive()
      .find(({ property }) => property === propertyName)
      .disable();
  };

  const enableGuiProperty = function (propertyName) {
    gui
      .controllersRecursive()
      .find(({ property }) => property === propertyName)
      .enable();
  };

  // Prepare a list of known object ids to select
  const objectIds = [
    "533602643180",
    "533602643184",
    "533602643302",
    "533602643461",
    "533602643470",
    "533602644476",
    "533602645188",
    "533602645194",
    "533602647117",
    "533602647138",
    "533602647204",
    "533602648488",
    "533602648494"
  ];
  // Keep track of latest object id from list was selected to ease deselect
  let objectIdsIndex = 0;

  /**
   * Start off by showing a storey
   */
  const storeys = viewer2d.getStoreys();
  if (storeys.length > 0) {
    viewer2d.showStorey(storeys[1].id);
  }
  viewer2d.setViewport("fit", { expandInPercentage: 15 });

  /**
   * Map on screen buttons to viewer behaviour
   */
  const onDeselectClick = function () {
    viewer2d.deselect([objectIds[--objectIdsIndex]]);
    if (objectIdsIndex === 0) {
      disableGuiProperty(DESELECT);
    }
    enableGuiProperty(SELECT);
    setOutput(viewer2d.getSelected());
  };
  const onDeselectAllClick = function () {
    viewer2d.deselectAll();
    objectIdsIndex = 0;
    disableGuiProperty(DESELECT);
    disableGuiProperty(DESELECT_ALL);
    enableGuiProperty(SELECT);
    setOutput(viewer2d.getSelected());
  };
  const onSelectClick = function () {
    viewer2d.select([objectIds[objectIdsIndex++]]);
    if (objectIdsIndex === objectIds.length - 1) {
      disableGuiProperty(SELECT);
    }
    enableGuiProperty(DESELECT);
    enableGuiProperty(DESELECT_ALL);
    setOutput(viewer2d.getSelected());
  };

  /**
   *  Map of gui items to data and callbacks
   */
  const state = {
    [DESELECT]: onDeselectClick,
    [DESELECT_ALL]: onDeselectAllClick,
    [SELECT]: onSelectClick
  };

  /**
   * Build gui panel
   */
  gui.add(state, SELECT);
  gui.add(state, DESELECT).disable();
  gui.add(state, DESELECT_ALL).disable();

  /**
   * Listen to viewer events
   */
  const onClick = function (event) {
    event.preventDefault();
  };

  viewer2d.addEventListener("viewer2d.click", onClick);
};

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

// 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!";
};

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

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

  // HTML Div element where the 2D viewer is mounted
  const div2d = document.getElementById(ElementId.VIEWER_2D);

  // Options used when creating a 2D viewer instance
  const viewerOptions = {
    hoverSpaces: false,
    selectColor: "#ade0bc",
    showViewpoint: false
  };

  // Create a 2D viewer instance
  viewer2d = new bimsync.viewer2d.Viewer2D(div2d, viewerOptions);

  // Options used when loading the model
  const loadModelOptions = {
    modelId: modelId,
    modelName: "My Model Name"
  };

  // Fetch a 2D viewer token for the model to load
  const url2d = await getToken();
  // Load the model
  viewer2d
    .loadUrl(url2d, loadModelOptions)
    .then(function () {
      // 2D viewer is ready, stop loading
      showPlayButton();
      // Hide the splash screen
      hideElement(ElementId.SPLASH);
      // Show the 2D viewer controls
      showElement(ElementId.GUI);
      // 2D 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 2D viewer instance then run the example
  initialize(runExample);
};

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

// 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 2D viewer token
const getToken = async function () {
  /**
   * Fetch a 2D 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-2d/create-2d-viewer-token
   * 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/token2d"
    );
    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;
};

// 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 () {
  viewer2d.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