<html>

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

<body>

  <div id="splash">
    <div id="title">
      <div id="title-text">
        Object Color
      </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: 220px;
}
// 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 Object Color 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 GET_OBJECT_COLORS = "Get Object Colors";
  const OBJECTS_COLOR = "Selected Objects Color";
  const RESET_OBJECT_COLORS = "Reset Object Colors";
  const SET_OBJECT_COLORS = "Set Selected Objects Color";

  // Track ids of selected objects
  let selectedObjectIds = [];

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

  /**
   * Define button callbacks
   */
  const onGetObjectColorsClick = function () {
    const objectColorMap = viewer3d.getObjectColors(
      viewer3d.getObjects().map(({ id }) => id)
    );
    setOutput(objectColorMap);
  };
  const onObjectsColorChange = function (color) {
    viewer3d.setObjectColors(selectedObjectIds, color);
    viewer3d.deselectAll();
  };
  const onResetObjectColorsClick = function () {
    viewer3d.resetObjectColors();
  };

  // Map of gui items to data and callbacks
  const state = {
    [GET_OBJECT_COLORS]: onGetObjectColorsClick,
    [OBJECTS_COLOR]: "#ade0bc",
    [RESET_OBJECT_COLORS]: onResetObjectColorsClick
  };

  /**
   * Build gui panel
   */
  gui.addColor(state, OBJECTS_COLOR).onChange(onObjectsColorChange).disable();
  gui.add(state, GET_OBJECT_COLORS);
  gui.add(state, RESET_OBJECT_COLORS);

  /**
   * Listen to viewer events
   */
  const onSelect = function (event) {
    const { selected } = event;
    if (selected.length > 0) {
      enableGuiProperty(OBJECTS_COLOR);
    } else {
      disableGuiProperty(OBJECTS_COLOR);
    }
    selectedObjectIds = selected;
  };
  viewer3d.addEventListener("viewer3d.select", onSelect);
};

/**
 *  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