<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
    "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
  }
}
</script>

<style>
  #viewer { width: 100%; height: {{ height }}; position: relative; }
  #controls {
    position: absolute;
    top: 10px;
    right: 10px;
    background: rgba(255,255,255,0.9);
    padding: 10px;
    border-radius: 4px;
    font-family: sans-serif;
    font-size: 12px;
    z-index: 100;
  }
  #controls label { display: block; margin: 4px 0; cursor: pointer; }
  #controls input { margin-right: 6px; }
  #info-panel {
    display: none;
    position: absolute;
    background: rgba(255,255,255,0.95);
    padding: 10px 14px;
    border-radius: 4px;
    font-family: sans-serif;
    font-size: 12px;
    z-index: 100;
    max-width: 300px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    pointer-events: none;
  }
  #info-panel h4 { margin: 0 0 8px 0; font-size: 13px; }
  #info-panel .info-row { margin: 4px 0; }
  #info-panel .info-row.emphasized { background: #1a73e8; color: white; margin: 4px -8px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }
  #info-panel .info-row.emphasized .info-label { color: white; }
  #info-panel .info-label { color: #666; }
  .badge {
    display: inline-block;
    padding: 2px 8px;
    font-size: 11px;
    font-weight: 600;
    border-radius: 10px;
    text-transform: capitalize;
  }
  .badge-success { background-color: #198754; color: white; }
  .badge-danger { background-color: #dc3545; color: white; }
</style>

<div id="viewer">
  <div id="controls">
    <div style="margin-bottom: 8px;">
      <label style="display: inline;"><strong>Render By</strong></label>
      <select id="renderBy" style="margin-left: 8px; font-size: 12px;">
        <option value="surfaceType">Surface Type</option>
        <option value="boundary">Boundary</option>
        <option value="construction">Construction</option>
        <option value="thermalZone">Thermal Zone</option>
        <option value="spaceType">Space Type</option>
        <option value="buildingStory">Building Story</option>
      </select>
    </div>
    <div style="margin: 8px 0;">
      <label style="display: inline;"><strong>Show Story</strong></label>
      <select id="showStory" style="margin-left: 8px; font-size: 12px;">
        <option value="">All Stories</option>
      </select>
    </div>
    <hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
    <strong>Surface Filters</strong>
    <label><input type="checkbox" id="showFloors" checked> Floors</label>
    <label><input type="checkbox" id="showWalls" checked> Walls</label>
    <label><input type="checkbox" id="showRoofs" checked> Roofs/Ceilings</label>
    <label><input type="checkbox" id="showWindows" checked> Windows</label>
    <label><input type="checkbox" id="showDoors" checked> Doors</label>
    <label><input type="checkbox" id="showShading" checked> Shading</label>
    <label><input type="checkbox" id="showPartitions" checked> Partitions</label>
    <hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
    <label><input type="checkbox" id="showEdges" checked> Show Edges</label>
{% if include_geometry_diagnostics %}
    <hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
    <strong>Geometry Diagnostics</strong>
    <label><input type="checkbox" id="showOnlyNonConvexSurfaces"> Non-Convex Surfaces Only</label>
    <label><input type="checkbox" id="showOnlyIncorrectlyOriented"> Incorrectly Oriented Only</label>
    <label><input type="checkbox" id="showOnlyNonConvexSpaces"> Non-Convex Spaces Only</label>
    <label><input type="checkbox" id="showOnlyNonEnclosedSpaces"> Non-Enclosed Spaces Only</label>
{% endif %}
  </div>
  <div id="info-panel">
    <h4 id="info-name"></h4>
    <div class="info-row" id="info-type-row"><span class="info-label">Surface Type:</span> <span id="info-type"></span></div>
    <div class="info-row" id="info-space-row"><span class="info-label">Space:</span> <span id="info-space"></span></div>
    <div class="info-row" id="info-spaceType-row"><span class="info-label">Space Type:</span> <span id="info-spaceType"></span></div>
    <div class="info-row" id="info-thermalZone-row"><span class="info-label">Thermal Zone:</span> <span id="info-thermalZone"></span></div>
    <div class="info-row" id="info-buildingStory-row"><span class="info-label">Building Story:</span> <span id="info-buildingStory"></span></div>
    <div class="info-row" id="info-construction-row"><span class="info-label">Construction:</span> <span id="info-construction"></span></div>
    <div class="info-row" id="info-boundary-row"><span class="info-label">Boundary:</span> <span id="info-boundary"></span></div>
    <div class="info-row" id="info-boundaryObject-row"><span class="info-label">Adjacent To:</span> <span id="info-boundaryObject"></span></div>
    <div class="info-row" id="info-sunExposure-row"><span class="info-label">Sun Exposure:</span> <span id="info-sunExposure"></span></div>
    <div class="info-row" id="info-windExposure-row"><span class="info-label">Wind Exposure:</span> <span id="info-windExposure"></span></div>
{% if include_geometry_diagnostics %}
    <div class="info-row" id="info-convex-row"><span class="info-label">Convex:</span> <span id="info-convex"></span></div>
    <div class="info-row" id="info-correctlyOriented-row"><span class="info-label">Correctly Oriented:</span> <span id="info-correctlyOriented"></span></div>
    <div class="info-row" id="info-spaceConvex-row"><span class="info-label">Space Convex:</span> <span id="info-spaceConvex"></span></div>
    <div class="info-row" id="info-spaceEnclosed-row"><span class="info-label">Space Enclosed:</span> <span id="info-spaceEnclosed"></span></div>
{% endif %}
  </div>
</div>

<script type="module">
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

const container = document.getElementById("viewer");

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf5f5f5);

const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 5000);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

window.addEventListener('resize', () => {
  camera.aspect = container.clientWidth / container.clientHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(container.clientWidth, container.clientHeight);
  requestRenderIfNotRequested();
});

scene.add(new THREE.AmbientLight(0x888888));
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 0.6));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(1, 2, 1);
scene.add(dirLight);

// Collect all meshes for filtering and selection
const sceneObjects = [];
// Map mesh -> edge lines
const objectEdges = new Map();
// Map mesh -> back face object
const backObjects = new Map();
// Map back object -> front object (for selection)
const backToFront = new Map();

// Selection state
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObject = null;
let originalMaterial = null;

const selectedMaterial = new THREE.MeshStandardMaterial({
  color: 0xffff00,
  emissive: 0x444400,
  side: THREE.DoubleSide
});

const infoPanel = document.getElementById('info-panel');

let selectedBackWasVisible = false;

function selectObject(obj, clickX, clickY) {
  // Restore previous selection
  if (selectedObject && originalMaterial) {
    selectedObject.material = originalMaterial;
    // Restore back object visibility
    const prevBackObj = backObjects.get(selectedObject);
    if (prevBackObj) {
      prevBackObj.visible = selectedBackWasVisible;
    }
  }

  if (obj) {
    selectedObject = obj;
    originalMaterial = obj.material;
    obj.material = selectedMaterial;

    // Hide back object so yellow selection shows through
    const backObj = backObjects.get(obj);
    if (backObj) {
      selectedBackWasVisible = backObj.visible;
      backObj.visible = false;
    }

    // Update info panel with all available details
    const data = obj.userData;
    const renderMode = document.getElementById('renderBy').value;
    document.getElementById('info-name').textContent = data.name || 'Unknown';

    // Map renderBy values to info row IDs
    const renderByToRowId = {
      'surfaceType': 'type',
      'boundary': 'boundary',
      'construction': 'construction',
      'thermalZone': 'thermalZone',
      'spaceType': 'spaceType',
      'buildingStory': 'buildingStory',
    };
    const emphasizedRowId = renderByToRowId[renderMode];

    // Helper to show/hide rows with emphasis
    const setRow = (id, value) => {
      const row = document.getElementById(`info-${id}-row`);
      const span = document.getElementById(`info-${id}`);
      if (value) {
        span.textContent = value;
        row.style.display = 'block';
        row.classList.toggle('emphasized', id === emphasizedRowId);
      } else {
        row.style.display = 'none';
        row.classList.remove('emphasized');
      }
    };

    setRow('type', data.surfaceType);
    setRow('space', data.spaceName);
    setRow('spaceType', data.spaceTypeName);
    setRow('thermalZone', data.thermalZoneName);
    setRow('buildingStory', data.buildingStoryName);
    setRow('construction', data.constructionName);
    setRow('boundary', data.outsideBoundaryCondition);
    setRow('boundaryObject', data.outsideBoundaryConditionObjectName);
    setRow('sunExposure', data.sunExposure);
    setRow('windExposure', data.windExposure);

{% if include_geometry_diagnostics %}
    // Diagnostic fields with pill badge styling
    const setDiagRow = (id, value) => {
      const row = document.getElementById(`info-${id}-row`);
      const span = document.getElementById(`info-${id}`);
      if (row && value !== undefined) {
        span.innerHTML = `<span class="badge ${value ? 'badge-success' : 'badge-danger'}">${value}</span>`;
        row.style.display = 'block';
      } else if (row) {
        row.style.display = 'none';
      }
    };

    setDiagRow('convex', data.convex);
    setDiagRow('correctlyOriented', data.correctlyOriented);
    setDiagRow('spaceConvex', data.spaceConvex);
    setDiagRow('spaceEnclosed', data.spaceEnclosed);
{% endif %}

    // Position panel to the right of click, with offset
    const rect = container.getBoundingClientRect();
    let left = clickX - rect.left + 15;
    let top = clickY - rect.top - 10;

    // Keep panel within container bounds
    infoPanel.style.display = 'block';
    const panelRect = infoPanel.getBoundingClientRect();
    if (left + panelRect.width > container.clientWidth) {
      left = clickX - rect.left - panelRect.width - 15;
    }
    if (top + panelRect.height > container.clientHeight) {
      top = container.clientHeight - panelRect.height - 10;
    }
    if (top < 10) top = 10;

    infoPanel.style.left = left + 'px';
    infoPanel.style.top = top + 'px';
  } else {
    selectedObject = null;
    originalMaterial = null;
    infoPanel.style.display = 'none';
  }
}

// Track mouse position for click vs drag detection
let mouseDownPos = { x: 0, y: 0 };

renderer.domElement.addEventListener('mousedown', (event) => {
  mouseDownPos.x = event.clientX;
  mouseDownPos.y = event.clientY;
});

renderer.domElement.addEventListener('click', (event) => {
  // Ignore if this was a drag (camera orbit)
  const dx = event.clientX - mouseDownPos.x;
  const dy = event.clientY - mouseDownPos.y;
  if (Math.abs(dx) > 5 || Math.abs(dy) > 5) return;

  const rect = renderer.domElement.getBoundingClientRect();
  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
  mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

  raycaster.setFromCamera(mouse, camera);

  // Include both front and back objects for picking
  const visibleObjects = sceneObjects.filter(obj => obj.visible);
  const visibleBackObjects = [...backObjects.values()].filter(obj => obj.visible);
  const allPickable = [...visibleObjects, ...visibleBackObjects];

  const intersects = raycaster.intersectObjects(allPickable);

  if (intersects.length > 0) {
    let hitObj = intersects[0].object;
    // If we hit a back object, resolve to its front object
    if (backToFront.has(hitObj)) {
      hitObj = backToFront.get(hitObj);
    }
    selectObject(hitObj, event.clientX, event.clientY);
  } else {
    selectObject(null);
  }

  requestRenderIfNotRequested();
});

function updateVisibility() {
  const showFloors = document.getElementById('showFloors').checked;
  const showWalls = document.getElementById('showWalls').checked;
  const showRoofs = document.getElementById('showRoofs').checked;
  const showWindows = document.getElementById('showWindows').checked;
  const showDoors = document.getElementById('showDoors').checked;
  const showShading = document.getElementById('showShading').checked;
  const showPartitions = document.getElementById('showPartitions').checked;
  const showEdges = document.getElementById('showEdges').checked;
  const showStory = document.getElementById('showStory').value;

  sceneObjects.forEach(obj => {
    const surfaceType = obj.userData?.surfaceType || '';
    const storyName = obj.userData?.buildingStoryName || '';
    let visible = true;

    // Filter by surface type
    if (surfaceType === 'Floor') visible = showFloors;
    else if (surfaceType === 'Wall') visible = showWalls;
    else if (surfaceType === 'RoofCeiling') visible = showRoofs;
    else if (surfaceType.includes('Window') || surfaceType.includes('Skylight') || surfaceType.includes('TubularDaylight') || surfaceType === 'GlassDoor') visible = showWindows;
    else if (surfaceType.includes('Door')) visible = showDoors;
    else if (surfaceType.includes('Shading')) visible = showShading;
    else if (surfaceType === 'InteriorPartitionSurface') visible = showPartitions;

    // Filter by story
    if (visible && showStory && storyName !== showStory) {
      visible = false;
    }

{% if include_geometry_diagnostics %}
    // Geometry diagnostic filters
    const showOnlyNonConvexSurfaces = document.getElementById('showOnlyNonConvexSurfaces').checked;
    const showOnlyIncorrectlyOriented = document.getElementById('showOnlyIncorrectlyOriented').checked;
    const showOnlyNonConvexSpaces = document.getElementById('showOnlyNonConvexSpaces').checked;
    const showOnlyNonEnclosedSpaces = document.getElementById('showOnlyNonEnclosedSpaces').checked;

    if (visible && showOnlyNonConvexSurfaces && obj.userData.convex !== false) {
      visible = false;
    }
    if (visible && showOnlyIncorrectlyOriented && obj.userData.correctlyOriented !== false) {
      visible = false;
    }
    if (visible && showOnlyNonConvexSpaces && obj.userData.spaceConvex !== false) {
      visible = false;
    }
    if (visible && showOnlyNonEnclosedSpaces && obj.userData.spaceEnclosed !== false) {
      visible = false;
    }
{% endif %}

    obj.visible = visible;

    // Sync edge visibility
    const edges = objectEdges.get(obj);
    if (edges) {
      edges.visible = visible && showEdges;
    }

    // Sync back object visibility
    const backObj = backObjects.get(obj);
    if (backObj) {
      backObj.visible = visible;
    }
  });

  requestRenderIfNotRequested();
}

// Add event listeners to checkboxes and dropdowns
['showFloors', 'showWalls', 'showRoofs', 'showWindows', 'showDoors', 'showShading', 'showPartitions', 'showEdges'].forEach(id => {
  document.getElementById(id).addEventListener('change', updateVisibility);
});
document.getElementById('showStory').addEventListener('change', updateVisibility);
{% if include_geometry_diagnostics %}
['showOnlyNonConvexSurfaces', 'showOnlyIncorrectlyOriented', 'showOnlyNonConvexSpaces', 'showOnlyNonEnclosedSpaces'].forEach(id => {
  document.getElementById(id).addEventListener('change', updateVisibility);
});
{% endif %}

// Color definitions for render modes
const surfaceTypeColors = {
  'Floor': 0x808080, 'Wall': 0xccb266, 'RoofCeiling': 0x994c4c,
  'Window': 0x66b2cc, 'GlassDoor': 0x66b2cc, 'Skylight': 0x66b2cc,
  'TubularDaylightDome': 0x66b2cc, 'TubularDaylightDiffuser': 0x66b2cc,
  'Door': 0x99854c, 'OverheadDoor': 0x99854c,
  'SiteShading': 0x4b7c95, 'BuildingShading': 0x714c99, 'SpaceShading': 0x4c6eb2,
  'InteriorPartitionSurface': 0x9ebc8f, 'AirWall': 0x66b2cc,
};
const surfaceTypeColorsInt = {
  'Floor': 0xbfbfbf, 'Wall': 0xebe2c5, 'RoofCeiling': 0xca9595,
  'Window': 0xc0e2eb, 'GlassDoor': 0xc0e2eb, 'Skylight': 0xc0e2eb,
  'TubularDaylightDome': 0xc0e2eb, 'TubularDaylightDiffuser': 0xc0e2eb,
  'Door': 0xcabc95, 'OverheadDoor': 0xcabc95,
  'SiteShading': 0xbbd1dc, 'BuildingShading': 0xd8cbe5, 'SpaceShading': 0xb7c5e0,
  'InteriorPartitionSurface': 0xd5e2cf, 'AirWall': 0xc0e2eb,
};
const boundaryColors = {
  'Surface': 0x009900, 'Adiabatic': 0xff0000, 'Space': 0xff0000,
  'Outdoors': 0xa3cccc, 'Outdoors_Sun': 0x28cccc, 'Outdoors_Wind': 0x099fa2, 'Outdoors_SunWind': 0x4477a1,
  'Ground': 0xccb77a, 'Foundation': 0x751e7a,
  'OtherSideCoefficients': 0x3f3f3f, 'OtherSideConditionsModel': 0x99004c,
};

// Generate consistent color from string (for dynamic values like thermal zones)
function stringToColor(str) {
  if (!str) return 0xcccccc;
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  // Generate HSL with good saturation and lightness
  const h = Math.abs(hash) % 360;
  return new THREE.Color(`hsl(${h}, 65%, 55%)`).getHex();
}

// Cache for dynamic colors
const dynamicColors = {};
function getDynamicColor(category, name) {
  const key = `${category}_${name}`;
  if (!dynamicColors[key]) {
    dynamicColors[key] = stringToColor(name);
  }
  return dynamicColors[key];
}

function getColorsForObject(obj, renderMode) {
  const data = obj.userData;
  let colorExt, colorInt;

  switch (renderMode) {
    case 'surfaceType':
      colorExt = surfaceTypeColors[data.surfaceType] ?? 0xcccccc;
      colorInt = surfaceTypeColorsInt[data.surfaceType] ?? 0xeeeeee;
      break;
    case 'boundary':
      const bc = data.outsideBoundaryCondition || 'Outdoors';
      // Combine sun/wind exposure for outdoor surfaces
      let boundaryKey = bc;
      if (bc === 'Outdoors') {
        const sun = data.sunExposure === 'SunExposed';
        const wind = data.windExposure === 'WindExposed';
        if (sun && wind) boundaryKey = 'Outdoors_SunWind';
        else if (sun) boundaryKey = 'Outdoors_Sun';
        else if (wind) boundaryKey = 'Outdoors_Wind';
      }
      colorExt = boundaryColors[boundaryKey] ?? boundaryColors[bc] ?? 0xcccccc;
      colorInt = colorExt;
      break;
    case 'construction':
      colorExt = getDynamicColor('construction', data.constructionName);
      colorInt = colorExt;
      break;
    case 'thermalZone':
      colorExt = getDynamicColor('thermalZone', data.thermalZoneName);
      colorInt = colorExt;
      break;
    case 'spaceType':
      colorExt = getDynamicColor('spaceType', data.spaceTypeName);
      colorInt = colorExt;
      break;
    case 'buildingStory':
      colorExt = getDynamicColor('buildingStory', data.buildingStoryName);
      colorInt = colorExt;
      break;
    default:
      colorExt = 0xcccccc;
      colorInt = 0xeeeeee;
  }
  return { colorExt, colorInt };
}

function updateRenderMode() {
  const renderMode = document.getElementById('renderBy').value;
  sceneObjects.forEach(obj => {
    const { colorExt, colorInt } = getColorsForObject(obj, renderMode);
    obj.material.color.setHex(colorExt);
    const backObj = backObjects.get(obj);
    if (backObj) {
      backObj.material.color.setHex(colorInt);
    }
  });

  requestRenderIfNotRequested();
}

document.getElementById('renderBy').addEventListener('change', updateRenderMode);

const gltfData = {{ gltf_data | tojson(indent=indent) }};

const loader = new GLTFLoader();
loader.parse(
  JSON.stringify(gltfData),
  "",
  (gltf) => {
    scene.add(gltf.scene);

    // Collect all meshes with userData for filtering
    const renderMode = document.getElementById('renderBy').value;
    const edgeMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });

    gltf.scene.traverse(obj => {
      if (obj.isMesh && obj.userData?.surfaceType) {
        sceneObjects.push(obj);

        const { colorExt, colorInt } = getColorsForObject(obj, renderMode);

        // Front face material
        obj.material = new THREE.MeshPhongMaterial({
          color: colorExt,
          specular: 0x222222,
          shininess: 30,
          side: THREE.FrontSide
        });

        // Create back face object with interior color
        const backObj = obj.clone();
        backObj.material = new THREE.MeshPhongMaterial({
          color: colorInt,
          specular: 0x222222,
          shininess: 30,
          side: THREE.BackSide
        });
        obj.parent.add(backObj);
        backObjects.set(obj, backObj);
        backToFront.set(backObj, obj);

        // Create edge lines for this mesh
        const edgesGeometry = new THREE.EdgesGeometry(obj.geometry);
        const edges = new THREE.LineSegments(edgesGeometry, edgeMaterial);
        edges.position.copy(obj.position);
        edges.rotation.copy(obj.rotation);
        edges.scale.copy(obj.scale);
        obj.parent.add(edges);
        objectEdges.set(obj, edges);
      }
    });

    // Populate story dropdown with unique values
    const storySelect = document.getElementById('showStory');
    const storyNames = [...new Set(sceneObjects.map(o => o.userData?.buildingStoryName).filter(Boolean))].sort();
    storyNames.forEach(name => {
      const option = document.createElement('option');
      option.value = name;
      option.textContent = name;
      storySelect.appendChild(option);
    });

    // Position camera using bounding box from GLTF metadata
    const bbox = gltfData.scenes?.[0]?.extras?.boundingbox;

    // Add axes (X=red, Y=green, Z=blue) - converted from OpenStudio Z-up to Three.js Y-up
    const axisSize = bbox ? bbox.lookAtR * 4 : 10;

    const xAxisGeometry = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(0, 0, 0), new THREE.Vector3(axisSize, 0, 0)
    ]);
    scene.add(new THREE.Line(xAxisGeometry, new THREE.LineBasicMaterial({ color: 0xff0000 })));

    // OpenStudio Y -> Three.js -Z
    const yAxisGeometry = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -axisSize)
    ]);
    scene.add(new THREE.Line(yAxisGeometry, new THREE.LineBasicMaterial({ color: 0x00ff00 })));

    // OpenStudio Z -> Three.js Y
    const zAxisGeometry = new THREE.BufferGeometry().setFromPoints([
      new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, axisSize, 0)
    ]);
    scene.add(new THREE.Line(zAxisGeometry, new THREE.LineBasicMaterial({ color: 0x0000ff })));

    // North axis (orange) if northAxis is set
    const northAxis = gltfData.scenes?.[0]?.extras?.northAxis;
    if (northAxis && northAxis !== 0) {
      const northAxisRad = -northAxis * Math.PI / 180.0;
      const northAxisGeometry = new THREE.BufferGeometry().setFromPoints([
        new THREE.Vector3(0, 0, 0),
        new THREE.Vector3(-Math.sin(northAxisRad) * axisSize, 0, -Math.cos(northAxisRad) * axisSize)
      ]);
      scene.add(new THREE.Line(northAxisGeometry, new THREE.LineBasicMaterial({ color: 0xff9933 })));
    }

    if (bbox) {
      // Convert from OpenStudio coords (Z-up) to Three.js coords (Y-up)
      const lookAt = new THREE.Vector3(bbox.lookAtX, bbox.lookAtZ, -bbox.lookAtY);
      const radius = 2.5 * bbox.lookAtR;

      // Position camera at an angle (similar to -30, 30 degrees)
      const theta = -30 * Math.PI / 180;
      const phi = 30 * Math.PI / 180;
      camera.position.set(
        radius * Math.cos(theta) * Math.cos(phi) + lookAt.x,
        radius * Math.sin(phi) + lookAt.y,
        -radius * Math.sin(theta) * Math.cos(phi) + lookAt.z
      );

      controls.target.copy(lookAt);
      controls.update();
    }

    requestRenderIfNotRequested();
  },
  (e) => console.error(e)
);

// Render on demand (not every frame) to save CPU
let renderRequested = false;

function render() {
  renderRequested = false;
  controls.update();
  renderer.render(scene, camera);
}

function requestRenderIfNotRequested() {
  if (!renderRequested) {
    renderRequested = true;
    requestAnimationFrame(render);
  }
}

// Re-render when controls change (orbit, zoom, pan)
controls.addEventListener('change', requestRenderIfNotRequested);

// Re-render on window resize
window.addEventListener('resize', requestRenderIfNotRequested);
</script>
