text: import * as THREE from "https://unpkg.com/three@0.127.0/build/three.module.js";
  import {OrbitControls} from "https://unpkg.com/three@0.127.0/examples/jsm/controls/OrbitControls.js";
  
  let container, camera, renderer, geom_size = 400, d, scene, top_volumes_names;
  
  const geometry_name = "geometry"
  
  function add_geometry(obj) {
  
      let opt = {numfaces: 100000, numnodes: 1000, wireframe: false};
      if (d.has("all")) {
          opt.numfaces *= 100;
          opt.numnodes *= 100;
      }
  
      if (d.has("dflt") || d.has("dflt_colors"))
          opt.dflt_colors = true;
  
      let volumes = obj.fMasterVolume.fNodes.arr
      top_volumes_names = []
      for (var i in volumes) {
          const name = volumes[i].fName
          top_volumes_names.push(name)
      }
  
      let obj3d = JSROOT.GEO.build(obj, opt);
  
      if (!obj3d) return;
  
      obj3d.name = geometry_name
      scene.add(obj3d);
  
      add_interface()
  
      let box3 = new THREE.Box3().setFromObject(obj3d);
  
      geom_size = box3.getSize(new THREE.Vector3()).length();
  
      camera.far = geom_size * 5;
      camera.updateProjectionMatrix();
  }
  
  function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
  }
  
  
  function render() {
      // set rendering order for transparent volumes, create with JSROOT
      JSROOT.GEO.produceRenderOrder(scene, camera.position, 'dflt');
      renderer.render(scene, camera);
  }
  
  container = document.querySelector('#event-visualization')
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, geom_size * 5);
  camera.position.y = geom_size;
  
  scene = new THREE.Scene();
  // access scene from global scope
  window.scene = scene
  scene.background = new THREE.Color(0xffffff);
  
  renderer = new THREE.WebGLRenderer({antialias: true});
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  
  var canvas = renderer.domElement;
  container.appendChild(canvas);
  
  
  const controls = new OrbitControls(camera, canvas);
  // controls.enableDamping = true
  
  
  controls.mouseButtons = {
      LEFT: THREE.MOUSE.ROTATE,
      MIDDLE: THREE.MOUSE.DOLLY,
      RIGHT: THREE.MOUSE.PAN
  }
  
  
  function add_interface() {
  
      const geometry = scene.getObjectByName(geometry_name);
      console.assert(geometry.type === "Object3D", "children is not 'Object3D', probably something wrong")
  
      // these are the top level physical volumes (from world)
      console.assert(geometry.children.length === 1, "geometry has more than one children, probably something wrong");
      console.assert(geometry.name === geometry_name, "geometry name does not match, maybe its not the geometry")
      let world = geometry.children[0];
      // console.log(world.children.length, top_volumes_names.length, top_volumes_names)
      //console.assert(world.children.length === top_volumes_names.length, "world has wrong number of children, probably something wrong"); // number of top volumes
  
      const top_volumes = world.children;
  
      let interface_body = document.querySelector("#visualization-controls")
  
      // global opacity control
      {
          // TODO: implement
      }
      // toggle visibility
      {
          // TODO: implement
      }
  
      const vetoKeyWord = "Veto"
      const vetoGroupKeywords = ["Top", "Bottom", "East", "West", "Front", "Back"] // they should be mutually exclusive
      let vetoGroupIndices = new Map()
  
      for (const index in top_volumes) {
          const name = top_volumes_names[index];
          if (name.toLowerCase().includes(vetoKeyWord.toLowerCase())) {
              // it is a veto
              for (var i in vetoGroupKeywords) {
                  let group = vetoGroupKeywords[i]
                  if (name.toLowerCase().includes(group.toLowerCase())) {
                      if (typeof vetoGroupIndices[group] === "undefined") {
                          vetoGroupIndices[group] = []
                      }
                      vetoGroupIndices[group].push(index)
                  }
              }
              continue;
          }
          const button = document.createElement("button");
          button.classList.add("btn")
          button.classList.add("btn-success")
          button.name = name
          button.innerText = name
          button.isOn = true
          button.addEventListener('click', (event) => {
              let i;
              var thisElement = event.currentTarget
              thisElement.isOn = !thisElement.isOn
              if (!thisElement.isOn) {
                  thisElement.classList.remove("btn-success")
                  thisElement.classList.add("btn-danger")
  
                  for (i in top_volumes_names) {
                      if (top_volumes_names[i] === thisElement.name) {
                          top_volumes[i].traverse((node) => {
                              if (node.isMesh) {
                                  node.visible = false;
                              }
                          });
                      }
                  }
              } else {
                  thisElement.classList.remove("btn-danger")
                  thisElement.classList.add("btn-success")
  
                  for (i in top_volumes_names) {
                      if (top_volumes_names[i] === thisElement.name) {
                          top_volumes[i].traverse((node) => {
                              if (node.isMesh) {
                                  node.visible = true;
                              }
                          });
                      }
                  }
              }
  
          });
  
          var slider = document.createElement("input");
          slider.name = name
          slider.type = "range"
          slider.min = "0"
          slider.max = "1"
          slider.step = "0.01"
          slider.classList.add("volume-slider")
          slider.addEventListener('change', (event) => {
              var thisElement = event.currentTarget
              for (var i in top_volumes_names) {
                  if (top_volumes_names[i] === thisElement.name) {
                      top_volumes[i].traverse((node) => {
                          if (node.isMesh) {
                              node.material.transparent = true
                              node.material.opacity = thisElement.value;
                          }
                      });
                  }
              }
          });
  
          var container = document.createElement("div");
          container.appendChild(button);
          container.appendChild(slider);
          interface_body.appendChild(container);
  
      }
      var title = document.createElement("h2");
      title.innerText = "Vetoes"
      interface_body.appendChild(title);
  
      for (var key in vetoGroupIndices) {
          var value = vetoGroupIndices[key]
          console.log(key, value)
          const names = value.map(x => top_volumes_names[x])
          console.log("names", names)
          const button = document.createElement("button");
          button.classList.add("btn")
          button.classList.add("btn-success")
          button.name = key
          button.innerText = key
          button.isOn = true
          button.addEventListener('click', (event) => {
              let i;
              var thisElement = event.currentTarget
              thisElement.isOn = !thisElement.isOn
              var visible = true
              if (!thisElement.isOn) {
                  thisElement.classList.remove("btn-success")
                  thisElement.classList.add("btn-danger")
                  visible = false
              } else {
                  thisElement.classList.remove("btn-danger")
                  thisElement.classList.add("btn-success")
              }
  
              for (i in top_volumes_names) {
                  console.log(names, top_volumes_names[i], names.includes(top_volumes_names[i]))
                  if (names.includes(top_volumes_names[i])) {
                      top_volumes[i].traverse((node) => {
                          if (node.isMesh) {
                              node.visible = visible;
                          }
                      });
                  }
              }
  
          });
  
          var slider = document.createElement("input");
          slider.name = key
          slider.type = "range"
          slider.min = "0"
          slider.max = "1"
          slider.step = "0.01"
          slider.classList.add("volume-slider")
          slider.addEventListener('change', (event) => {
              var thisElement = event.currentTarget
              for (var i in top_volumes_names) {
                  if (names.includes(top_volumes_names[i])) {
                      top_volumes[i].traverse((node) => {
                          if (node.isMesh) {
                              node.material.transparent = true
                              node.material.opacity = thisElement.value;
                          }
                      });
                  }
              }
          });
  
  
          var container = document.createElement("div");
          container.appendChild(button);
          container.appendChild(slider);
  
          interface_body.appendChild(container);
      }
      // global slider
      slider = document.createElement("input");
      slider.name = name
      slider.type = "range"
      slider.min = "0"
      slider.max = "1"
      slider.step = "0.01"
      slider.classList.add("global-slider")
      slider.addEventListener('change', (event) => {
          var thisElement = event.currentTarget
          var opacity = thisElement.value;
          var event = new Event('change');
          let elements = document.getElementsByClassName("volume-slider");
          for (var i in elements) {
              console.log(i, elements.length);
              elements[i].value = opacity;
              elements[i].dispatchEvent(event);
              if (i >= elements.length - 1) {
                  break;
              }
          }
      });
      container = document.createElement("div");
      container.appendChild(slider);
      var title = document.createElement("h3");
      title.innerText = "Global Opacity"
      interface_body.appendChild(title);
      interface_body.appendChild(container);
  }
  
  function init() {
      d = JSROOT.decodeUrl();
      {
          let filename = "static/geometry/BabyIAXO.gdml.root"
          if (filename.indexOf(".root") > 0) {
              let itemname = "geo";
              JSROOT.openFile(filename)
                  .then(file => file.readObject(itemname))
                  .then(add_geometry);
          }
      }
  
      {
          const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.8);
          scene.add(light);
      }
      {
          scene.add(new THREE.AmbientLight(0x404040));
      }
  
      camera.position.x = 0.5 * geom_size;
      camera.position.z = 0.5 * geom_size;
      camera.position.y = 0.1 * geom_size;
  
      camera.lookAt(scene.position);
  
      controls.update();
  
      window.addEventListener('resize', onWindowResize, false);
  
  
      animate();
  
  }
  
  function animate() {
  
      requestAnimationFrame(animate);
      try {
          controls.update();
      } catch (err) {
          console.log("failed to update controls")
      }
      render();
  
  }
  
  function dispose() {
      const json = scene.toJSON();
      console.log(json);
      console.log(renderer.info)
      scene.traverse((o) => {
          //console.log(o);
          if (o.geometry) {
              o.geometry.dispose()
              //console.log("dispose geometry ", o.geometry)
          }
  
          if (o.material) {
              if (o.material.length) {
                  for (let i = 0; i < o.material.length; ++i) {
                      o.material[i].dispose()
                      //console.log("dispose material ", o.material[i])
                  }
              } else {
                  o.material.dispose()
                  //console.log("dispose material ", o.material)
              }
          }
      });
      renderer.renderLists.dispose()
  
      while (scene.children.length > 0) {
          scene.remove(scene.children[0]);
      }
  
      animate();
  
      console.log("Dispose!")
      console.log(renderer.info)
  }
  
  const button = document.createElement("button");
  button.classList.add("btn-warning")
  button.innerText = "DISPOSE!"
  button.addEventListener('click', () => {
      dispose();
  });
  container.appendChild(button);
  
  
  JSROOT.require('geom').then(init);
  
  