<ul class="flex-container">
  <li class="flex-item">
    <p>1) Polygone initial</p>
    <canvas id="initial2d"></canvas>
  </li>
  <li class="flex-item">
    <p>2) Squelette droit du polygone</p>
    <canvas id="canvas_skeleton"></canvas>
  </li>
  <li class="flex-item">
    <p>3) Squelette droit après post-traitement</p>
    <canvas id="canvas_post_process"></canvas>
  </li>
  <li class="flex-item">
    <p>4) Calcul de Dmax = <b id="hmax">...</b> </p>
    <canvas id="canvas_post_process_vertices"></canvas>
  </li>
  <li class="flex-item">
    <p>5) Triangulation du squelette droit </p>
    <canvas id="canvas_triangulated_skeleton"></canvas>
  </li>
  <li class="flex-item-three-js">
    <p>6) Création toiture </p>
    <canvas id="scene_roof"> </canvas>
  </li>
  <li class="flex-item-three-js">
    <p>7) Création façade </p>
    <canvas id="walls_scene"> </canvas>
  </li>
  <li class="flex-item-building-three-js">
    <p>8) Bâtiment complet </p>
    <canvas id="building_scene"> </canvas>
  </li>
</ul>
.flex-container {
  /* We first create a flex layout context */
  display: flex;

  /* Then we define the flow direction 
     and if we allow the items to wrap 
   * Remember this is the same as:
   * flex-direction: row;
   * flex-wrap: wrap;
   */
  flex-flow: row wrap;

  /* Then we define how is distributed the remaining space */
  justify-content: space-around;

  padding: 0;
  margin: 0;
  list-style: none;
}

.flex-item {
  padding: 5px;
  width: 32%;
  height: 300px;
  margin-top: 10px;
  color: black;
  font-weight: bold;
  text-align: center;
  border: 1px solid black;
  canvas {
    height: 250px;
    width: 100%;
  }
}
.flex-item-three-js {
  padding: 5px;
  width: 32%;
  height: 300px;
  margin-top: 10px;
  color: black;
  font-weight: bold;
  text-align: center;
  border: 1px solid black;
  canvas {
    height: 250px !important;
    width: 100% !important;
  }
}
.flex-item-building-three-js {
  padding: 5px;
  width: 100%;
  height: 350px;
  margin-top: 10px;
  color: black;
  font-weight: bold;
  text-align: center;
  border: 1px solid black;
  canvas {
    height: 300px !important;
    width: 100% !important;
  }
}
import {
  Vector2,
  Vector3,
  Box3,
  Mesh,
  PerspectiveCamera,
  PointsMaterial,
  MeshBasicMaterial,
  Scene,
  Color,
  DoubleSide,
  WebGLRenderer,
  BufferGeometry,
  Points,
  Float32BufferAttribute
} from "https://esm.sh/three";
import {
  OrbitControls,
  TextGeometry,
  FontLoader
} from "https://esm.sh/three/addons";
import * as BufferGeometryUtils from "https://esm.sh/three/addons/utils/BufferGeometryUtils.js";

import straightSkeleton from "https://esm.sh/straight-skeleton";
import earcut from "https://esm.sh/earcut";

const initialPolygon = {
  type: "Polygon",
  coordinates: [
    [
      [259521.14076069795, 6253167.799861707],
      [259606.34863939675, 6253192.28359733],
      [259590.5611086523, 6253248.230426137],
      [259505.31590718575, 6253223.7466905145],
      [259521.14076069795, 6253167.799861707]
    ]
  ]
};

const roofMinHeight = 48;
const roofMaxHeight = 58;
const roofHeight = 10;

const minHeight = 31;
/////////////// Affichage du polygone initial + construction du skeleton droit et son affichage //////////////////:

const drawInitial2d = (skeletonBox) => {
  // 2D canvas
  const initial2d = document.getElementById("initial2d");
  const initialCtx = initial2d.getContext("2d");

  initial2d.width = initial2d.clientWidth * window.devicePixelRatio;
  initial2d.height = initial2d.clientHeight * window.devicePixelRatio;

  initialCtx.fillStyle = "#eee";
  initialCtx.fillRect(0, 0, initial2d.width, initial2d.height);

  const padding = 15 * window.devicePixelRatio;
  const scale = Math.min(
    (initial2d.width - padding * 2) / (skeletonBox.maxX - skeletonBox.minX),
    (initial2d.height - padding * 2) / (skeletonBox.maxY - skeletonBox.minY)
  );
  const offsetX =
    (initial2d.width - (skeletonBox.maxX - skeletonBox.minX) * scale) / 2;
  const offsetY =
    (initial2d.height - (skeletonBox.maxY - skeletonBox.minY) * scale) / 2;

  initialCtx.strokeStyle = "#000";
  initialCtx.lineWidth = window.devicePixelRatio;
  initialCtx.fillStyle = "#ffb6e9";

  for (const polygon of initialPolygon.coordinates) {
    initialCtx.beginPath();

    for (let i = 0; i < polygon.length; i++) {
      const vertex = polygon[i];
      const x = (vertex[0] - skeletonBox.minX) * scale + offsetX;
      const y = (vertex[1] - skeletonBox.minY) * scale + offsetY;

      if (i === 0) {
        initialCtx.moveTo(x, y);
      } else {
        initialCtx.lineTo(x, y);
      }
    }

    initialCtx.closePath();
    initialCtx.stroke();
    initialCtx.fill();
  }
};

const draw2d = (
  skeletonBox,
  skeleton,
  cansvas_id,
  draw_triangle = false,
  burst_geometry = false,
  display_vertices = false
) => {
  // 2D canvas
  const canvas2d = document.getElementById(cansvas_id);
  const ctx = canvas2d.getContext("2d");

  canvas2d.width = canvas2d.clientWidth * window.devicePixelRatio;
  canvas2d.height = canvas2d.clientHeight * window.devicePixelRatio;

  ctx.fillStyle = "#eee";
  ctx.fillRect(0, 0, canvas2d.width, canvas2d.height);

  const padding = 15 * window.devicePixelRatio;
  const scale = Math.min(
    (canvas2d.width - padding * 2) / (skeletonBox.maxX - skeletonBox.minX),
    (canvas2d.height - padding * 2) / (skeletonBox.maxY - skeletonBox.minY)
  );
  const offsetX =
    (canvas2d.width - (skeletonBox.maxX - skeletonBox.minX) * scale) / 2;
  const offsetY =
    (canvas2d.height - (skeletonBox.maxY - skeletonBox.minY) * scale) / 2;

  ctx.strokeStyle = "#000";
  ctx.lineWidth = window.devicePixelRatio;
  ctx.fillStyle = "#ffb6e9";
  for (const polygon of skeleton.polygons) {
    ctx.beginPath();

    if (polygon.length == 4) {
      ctx.fillStyle = "green";
    } else {
      ctx.fillStyle = "#ffb6e9";
    }

    const vertices = [];
    if (Array.isArray(polygon) == false) {
      for (let i = 0; i < polygon.vertices.length; i++) {
        const vertex = polygon.vertices[i];
        const x = (vertex.x - skeletonBox.minX) * scale + offsetX;
        let y = (vertex.y - skeletonBox.minY) * scale + offsetY;
        if (burst_geometry) {
          let isFirstPolygon =
            skeleton.polygons
              .filter((p) => p.vertices.length > 0)
              .indexOf(polygon) == 0;
          if (isFirstPolygon) {
            y -= 20;
          }
        }
        let position = "";
        if (i == 0) {
          position = "start";
        } else if (i == polygon.vertices.length - 1) {
          position = "end";
        }
        vertices.push([position, x, y]);

        if (draw_triangle) {
          if (i % 3 === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
        } else {
          if (i === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
        }
      }
    } else {
      for (let i = 0; i < polygon.length; i++) {
        const vertex = skeleton.vertices[polygon[i]];
        const x = (vertex[0] - skeletonBox.minX) * scale + offsetX;
        const y = (vertex[1] - skeletonBox.minY) * scale + offsetY;

        if (i === 0) {
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      }
    }

    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    if (burst_geometry) {
      for (const vertice of vertices) {
        ctx.beginPath();
        ctx.fillStyle = "black";
        ctx.font = "25px serif";
        ctx.fillText(vertice[0], vertice[1], vertice[2]);
        ctx.fillStyle = "red";
        ctx.arc(vertice[1], vertice[2], 5, 0, Math.PI * 2);
        ctx.closePath();
        ctx.fill();
      }
    }
  }
};

straightSkeleton.SkeletonBuilder.init().then(() => {
  // Construction du skeleton
  const skeleton = straightSkeleton.SkeletonBuilder.buildFromGeoJSONPolygon(
    initialPolygon
  );

  // Construction de l'étendue de la géométrie afin de centrer la géométrie dans la vue
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;

  for (const vertex of skeleton.vertices) {
    minX = Math.min(minX, vertex[0]);
    minY = Math.min(minY, vertex[1]);
    maxX = Math.max(maxX, vertex[0]);
    maxY = Math.max(maxY, vertex[1]);
  }

  const skeletonBox = { minX, minY, maxX, maxY };

  drawInitial2d(skeletonBox);
  draw2d(skeletonBox, skeleton, "canvas_skeleton");

  postProcessSkeleton(skeleton, skeletonBox);
});

/////////////// Post traitement du sekeleton droit + affichage ///////////////

export class StraightSkeletonResult {
  vertices: Vector2[];
  polygons: {
    vertices: Vector2[];
    edgeStart: Vector2;
    edgeEnd: Vector2;
  }[];

  constructor(source) {
    this.vertices = source.vertices.map((v) => new Vector2(v[0], v[1]));
    this.polygons = source.polygons.map((p) => {
      const vertices = p.map((v) => this.vertices[v]);

      return {
        vertices: vertices,
        edgeStart: vertices[vertices.length - 1],
        edgeEnd: vertices[0]
      };
    });
  }
}

const postProcessSkeleton = (skeleton, skeletonBox) => {
  const skeletonResult = new StraightSkeletonResult(skeleton);

  for (let i = 0; i < skeletonResult.polygons.length; i++) {
    const polygon = skeletonResult.polygons[i];

    // Pour les polygone roses (ceux dont on doit conserver uniquement les sommets de fins et de debut)
    // On recherchera les polygones rouges du skeleton adjacents à leurs extrémités
    if (polygon.vertices.length === 3) {
      // Polygone de notre skeleton adjacent au sommet du debut
      const prevPolygon = skeletonResult.polygons.find(
        (p) => p.edgeEnd.equals(polygon.edgeStart) && p.vertices.length > 3
      );
      // Polygone de notre skeleton adjacent au sommet de fin
      const nextPolygon = skeletonResult.polygons.find(
        (p) => p.edgeStart.equals(polygon.edgeEnd) && p.vertices.length > 3
      );

      if (prevPolygon && nextPolygon) {
        // Projection des polygones rouges adjacents à notre polygone rose

        // Le seul sommet de notre polygone rose qui est ni sa fin, ni son debut
        const extrudedPoint = polygon.vertices.find((p) => {
          return !p.equals(polygon.edgeStart) && !p.equals(polygon.edgeEnd);
        });

        // Le sommet de nos polygones rouges qui sont à projeter
        const prevPolygonExtrudedPoint = prevPolygon.vertices.find((v) =>
          v.equals(extrudedPoint)
        );
        const nextPolygonExtrudedPoint = nextPolygon.vertices.find((v) =>
          v.equals(extrudedPoint)
        );

        // Le sommet situé au milieu du segment[sommet debut, sommet fin] de notre polygone rose
        // Qui correspond au sommet souhaité pour notre projection
        const middle = new Vector2()
          .addVectors(polygon.edgeStart, polygon.edgeEnd)
          .multiplyScalar(0.5);
        prevPolygonExtrudedPoint.x = nextPolygonExtrudedPoint.x = middle.x;
        prevPolygonExtrudedPoint.y = nextPolygonExtrudedPoint.y = middle.y;

        // Projection faite, on conserve uniquement les sommets de debut et fin de notre polygone rose
        polygon.vertices = [];
      }
    }
  }

  draw2d(skeletonBox, skeletonResult, "canvas_post_process");

  evaluateHmax(skeletonResult, skeletonBox);
};

/////////////// Evaluation du Dmax et visualisation des sommets ///////////////
const _signedDistanceToLine = function (point, line) {
  const lineVector = new Vector2().subVectors(line[1], line[0]);
  const pointVector = new Vector2().subVectors(point, line[0]);
  const cross = lineVector.x * pointVector.y - lineVector.y * pointVector.x;
  const lineLength = lineVector.length();

  return cross / lineLength;
};

const evaluateHmax = (skeletonPostProcessingResult, skeletonBox) => {
  const SkeletonHeights = (function () {
    let heights = [];

    for (const polygon of skeletonPostProcessingResult.polygons) {
      const edgeLine = [polygon.edgeStart, polygon.edgeEnd];

      for (const vertex of polygon.vertices) {
        const dst = _signedDistanceToLine(vertex, edgeLine);
        heights.push(dst);
      }
    }

    return heights;
  })();
  const hMax = Math.max(...SkeletonHeights);
  document.getElementById("hmax").firstChild.textContent = hMax;

  draw2d(
    skeletonBox,
    skeletonPostProcessingResult,
    "canvas_post_process_vertices",
    false,
    true,
    true
  );

  triangulateSkeleton(skeletonPostProcessingResult, skeletonBox, hMax);
};

/////////////// Triangulation du skeleton droit + affichage ///////////////
const triangulatedPolygon: {
  polygons: {
    vertices: Vector2[];
  }[];
} = {
  polygons: []
};

const triangulateSkeleton = (
  skeletonPostProcessingResult,
  skeletonBox,
  hMax
) => {
  for (const polygon of skeletonPostProcessingResult.polygons) {
    const polygonVertices: number[] = [];
    const polygonTriangleVertices: Vector2[] = [];
    triangulatedPolygon.polygons.push({ vertices: polygonTriangleVertices });

    for (const vertex of polygon.vertices) {
      polygonVertices.push(vertex.x, vertex.y);
    }
    const triangles = earcut(polygonVertices).reverse();

    for (let i = 0; i < triangles.length; i++) {
      const index = triangles[i];

      const x = polygonVertices[index * 2];
      const y = polygonVertices[index * 2 + 1];
      const vertex = new Vector2(x, y);
      polygonTriangleVertices.push(vertex);
    }
  }

  draw2d(
    skeletonBox,
    triangulatedPolygon,
    "canvas_triangulated_skeleton",
    true
  );

  addZtoVerticesToCreateRoof(skeletonPostProcessingResult, skeletonBox, hMax);
};

/////////////// Affectation des altitudes pour création de la toiture et son affichage ///////////////

const addZtoVerticesToCreateRoof = (
  skeletonPostProcessingResult,
  skeletonBox,
  hMax
) => {
  const roofPositions: Vector3[] = [];

  for (const polygon of skeletonPostProcessingResult.polygons) {
    const polygonVertices: number[] = [];

    const edgeLine = [
      new Vector2(polygon.edgeStart.x, polygon.edgeStart.y),
      new Vector2(polygon.edgeEnd.x, polygon.edgeEnd.y)
    ];

    for (const vertex of polygon.vertices) {
      polygonVertices.push(vertex.x, vertex.y);
    }
    const triangles = earcut(polygonVertices).reverse();

    for (let i = 0; i < triangles.length; i++) {
      const index = triangles[i];

      const x = polygonVertices[index * 2];
      const y = polygonVertices[index * 2 + 1];
      const vertex = new Vector2(x, y);

      // distance maximale entre le point "vertex" et le vecteur [sommet debut, sommet fin] "edgeLine"
      const dst = _signedDistanceToLine(vertex, edgeLine);
      // Interpolation de l'altitude du point "vertex"
      const vertexZ = roofMinHeight + (roofHeight * dst) / hMax;

      roofPositions.push(new Vector3(x, y, vertexZ));
    }
  }

  displayRoof(skeletonPostProcessingResult, skeletonBox, hMax, roofPositions);
};

const displayRoof = (
  skeletonPostProcessingResult,
  skeletonBox,
  hMax,
  positions
) => {
  let camera, scene, renderer, controls;

  const loader = new FontLoader();

  ////// Roof polygons
  const material = new MeshBasicMaterial({
    color: "#ffb6e9",
    side: DoubleSide,
    wireframe: false
  });

  const roofGeometry = new BufferGeometry();
  roofGeometry.setFromPoints(positions);

  const roofMesh = new Mesh(roofGeometry, material);
  const scene_canvas = document.getElementById("scene_roof");
  const center = new Vector3(
    skeletonBox.minX + (skeletonBox.maxX - skeletonBox.minX) / 2,
    skeletonBox.minY + (skeletonBox.maxY - skeletonBox.minY) / 2,
    roofMinHeight + roofHeight / 2
  );
  function init() {
    renderer = new WebGLRenderer({ canvas: scene_canvas });
    renderer.setSize(scene_canvas.clientWidth, scene_canvas.clientHeight);
    scene = new Scene();
    scene.background = new Color("white");

    camera = new PerspectiveCamera(
      30,
      scene_canvas.clientWidth / scene_canvas.clientHeight,
      1,
      10000
    );
    // Our up axes here is the Z
    camera.up.set(0, 0, 1);

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

    camera.position.set(skeletonBox.maxX, skeletonBox.minY, minHeight * 4);

    camera.lookAt(center);
    controls.target.copy(center);
    controls.update();

    scene.add(roofMesh);

    window.addEventListener("resize", onWindowResize);
  }
  function onWindowResize() {
    camera.aspect = scene_canvas.clientWidth / scene_canvas.clientHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(scene_canvas.clientWidth, scene_canvas.clientHeight);
  }

  function animate() {
    requestAnimationFrame(animate);

    controls.update();

    renderer.render(scene, camera);
  }

  init();
  animate();

  // display Z labels
  loader.load(
    "https://esm.sh/three@0.172.0/examples/fonts/helvetiker_regular.typeface.json",
    function (font) {
      for (let index = 0; index < positions.length; index++) {
        const roofVertice = positions[index];
        const labelMesh = getPointLabelMesh(
          font,
          Math.ceil(roofVertice.z) + " m"
        );
        labelMesh.position.set(roofVertice.x, roofVertice.y, roofVertice.z);
        scene.add(labelMesh);
        //console.log(labelMesh, roofVertice);
      }
    }
  );

  const getPointLabelMesh = (font, label) => {
    const geometry = new TextGeometry(label, {
      font: font,
      size: 2,
      depth: 0.1
    });
    const labelMesh = new Mesh(
      geometry,
      new MeshBasicMaterial({
        color: "black",
        side: DoubleSide
      })
    );
    labelMesh.rotation.y = Math.PI / 2;
    labelMesh.rotation.z = Math.PI / 2;
    return labelMesh;
  };

  displayWalls(skeletonPostProcessingResult, skeletonBox, hMax, roofGeometry);
};

/////////////// Modélisation des façades et leurs affichages ///////////////

function createWallTriangles(positions: Array<Vector3>) {
  const postionsResult = positions.slice();

  for (let index = 0; index < positions.length; index++) {
    const A = positions[index];
    const B = positions[index + 1] ? positions[index + 1] : positions[0];
    // Triangle ABA'
    postionsResult.push(A); // A
    postionsResult.push(B); // B
    postionsResult.push(new Vector3(A.x, A.y, minHeight)); // A'

    // Triangle A',B,B'
    postionsResult.push(new Vector3(A.x, A.y, minHeight)); // A'
    postionsResult.push(B); // B
    postionsResult.push(new Vector3(B.x, B.y, minHeight)); // B'
  }

  return postionsResult;
}
const displayWalls = (
  skeletonPostProcessingResult,
  skeletonBox,
  Hmax,
  roofGeometry
) => {
  const vertices: Vector3[] = [];
  let camera, scene, renderer, controls;

  for (const polygon of skeletonPostProcessingResult.polygons) {
    const edgeLine = [polygon.edgeStart, polygon.edgeEnd];

    // les polygones avec uniquement 2 sommets
    if (polygon.vertices.length == 0) {
      const middle = new Vector2()
        .addVectors(polygon.edgeStart, polygon.edgeEnd)
        .multiplyScalar(0.5);

      const startDst = _signedDistanceToLine(polygon.edgeStart, edgeLine);
      const endDst = _signedDistanceToLine(polygon.edgeEnd, edgeLine);

      // A
      vertices.push(
        new Vector3(
          polygon.edgeStart.x,
          polygon.edgeStart.y,
          roofMinHeight + (roofHeight * startDst) / Hmax
        )
      );
      // B
      vertices.push(new Vector3(middle.x, middle.y, roofMaxHeight));
      // C
      vertices.push(
        new Vector3(
          polygon.edgeEnd.x,
          polygon.edgeEnd.y,
          roofMinHeight + (roofHeight * endDst) / Hmax
        )
      );
    }
  }
  const wallTrianglesVertices = createWallTriangles(vertices);

  //const material = new PointsMaterial({ color: 0x888888, size: 10 });
  const material = new MeshBasicMaterial({
    color: "#ffb6e9",
    side: DoubleSide
  });

  const wallGeometry = new BufferGeometry();
  wallGeometry.setFromPoints(wallTrianglesVertices);

  const wallMesh = new Mesh(wallGeometry, material);

  const center = new Vector3(
    skeletonBox.minX + (skeletonBox.maxX - skeletonBox.minX) / 2,
    skeletonBox.minY + (skeletonBox.maxY - skeletonBox.minY) / 2,
    minHeight + roofHeight / 2
  );

  const walls_scene = document.getElementById("walls_scene");

  function init() {
    renderer = new WebGLRenderer({ canvas: walls_scene });
    renderer.setSize(walls_scene.clientWidth, walls_scene.clientHeight);
    scene = new Scene();
    scene.background = new Color("white");

    camera = new PerspectiveCamera(
      70,
      walls_scene.clientWidth / walls_scene.clientHeight,
      1,
      10000
    );
    // Our up axes here is the Z
    camera.up.set(0, 0, 1);

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

    camera.position.set(skeletonBox.maxX, skeletonBox.maxY, roofMaxHeight * 2);

    camera.lookAt(center);
    controls.target.copy(center);
    controls.update();

    scene.add(wallMesh);

    window.addEventListener("resize", onWindowResize);
  }
  function onWindowResize() {
    camera.aspect = walls_scene.clientWidth / walls_scene.clientHeight;
    renderer.setSize(walls_scene.clientWidth, walls_scene.clientHeight);
    camera.updateProjectionMatrix();
  }
  function animate() {
    requestAnimationFrame(animate);

    controls.update();

    renderer.render(scene, camera);
  }

  init();
  animate();

  displayBuilding(roofGeometry, wallGeometry, skeletonBox);
};

/////////////// Réprésentation du bâtiment complet ///////////////

const displayBuilding = (roofGeometry, wallGeometry, skeletonBox) => {
  let camera, scene, renderer, controls;

  const buildingGeometry = BufferGeometryUtils.mergeGeometries(
    [roofGeometry, wallGeometry],
    true
  );
  const roofMaterial = new MeshBasicMaterial({
    color: "green",
    side: DoubleSide
  });
  const wallMaterial = new MeshBasicMaterial({
    color: "#ffb6e9",
    side: DoubleSide
  });
  const center = new Vector3(
    skeletonBox.minX + (skeletonBox.maxX - skeletonBox.minX) / 2,
    skeletonBox.minY + (skeletonBox.maxY - skeletonBox.minY) / 2,
    minHeight + roofHeight / 2
  );

  const buildingMesh = new Mesh(buildingGeometry, [roofMaterial, wallMaterial]);

  const building_scene = document.getElementById("building_scene");

  function init() {
    renderer = new WebGLRenderer({ canvas: building_scene });
    renderer.setSize(building_scene.clientWidth, building_scene.clientHeight);
    scene = new Scene();
    scene.background = new Color("white");

    camera = new PerspectiveCamera(
      70,
      building_scene.clientWidth / building_scene.clientHeight,
      1,
      10000
    );
    // Our up axes here is the Z
    camera.up.set(0, 0, 1);

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

    camera.position.set(skeletonBox.maxX, skeletonBox.maxY, roofMaxHeight * 2);

    camera.lookAt(center);
    controls.target.copy(center);
    controls.update();

    scene.add(buildingMesh);

    window.addEventListener("resize", onWindowResize);
  }
  function onWindowResize() {
    camera.aspect = building_scene.clientWidth / building_scene.clientHeight;
    renderer.setSize(building_scene.clientWidth, building_scene.clientHeight);
    camera.updateProjectionMatrix();
  }
  function animate() {
    requestAnimationFrame(animate);

    controls.update();

    renderer.render(scene, camera);
  }

  init();
  animate();
};
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.