///|
fn scene_item(index : ModelIndex, product : Product) -> SceneItem {
  let geometries = geometries_for_product(index, product)
  let transform = product_transform(index, product)
  let transformed = geometries.map(geometry => {
    ..geometry,
    points: geometry.points.map(point => transform.apply(point)),
  })
  let bounds = transformed.fold(init=BoundingBox::empty(), (result, geometry) => {
    merge_bounds(result, bounds_of_geometry(geometry))
  })
  { product, geometries: transformed, transform, bounds }
}

///|
/// Build a renderer-neutral scene for WebBIM or other front ends.
pub fn build_scene(model : Model) -> BimScene {
  let index = model.index()
  let items = model
    .products()
    .filter(product => product.representation is Some(_))
    .map(product => scene_item(index, product))
  let bounds = items.fold(init=BoundingBox::empty(), (result, item) => {
    merge_bounds(result, item.bounds)
  })
  { items, roots: spatial_roots(index), bounds }
}

///|
pub fn BimScene::product_ids(self : BimScene) -> Array[Int] {
  self.items.map(item => item.product.id)
}

///|
pub fn BimScene::find(self : BimScene, id : Int) -> SceneItem? {
  for item in self.items {
    if item.product.id == id {
      return Some(item)
    }
  }
  None
}

///|
pub fn BimScene::geometry_count(self : BimScene) -> Int {
  self.items.fold(init=0, (count, item) => count + item.geometries.length())
}

///|
pub fn BimScene::to_json(self : BimScene) -> String {
  let item_json = self.items.map(item => {
    "{\"product\":" +
    product_to_json(item.product) +
    ",\"transform\":[" +
    item.transform.tx.to_string() +
    "," +
    item.transform.ty.to_string() +
    "," +
    item.transform.tz.to_string() +
    "]" +
    ",\"geometry\":[" +
    join_json(item.geometries.map(geometry_to_json)) +
    "]}"
  })
  "{\"items\":[" +
  join_json(item_json) +
  "],\"bounds\":{\"min\":" +
  point_json(self.bounds.min) +
  ",\"max\":" +
  point_json(self.bounds.max) +
  ",\"empty\":" +
  self.bounds.empty.to_string() +
  "}}"
}