// 把网格画成 SVG 等轴测图。
//
// 做法是「画家算法」:把每个三角形投影到屏幕上,按离眼睛的远近排序,
// 远的先画近的后画,近的自然盖住远的。这是最古老的消隐方法,对凸的、
// 不怎么自遮挡的模型够用——3D 打印模型基本都属于这类。
//
// 真正准确的消隐要 Z-buffer,那得上光栅化,对这个项目是杀鸡用牛刀。
//
// 输出 SVG 而不是位图,有两个好处:矢量在任何尺寸下都清楚,而且是纯
// 文本,可以直接塞进网页、也能在终端里看(SVG 能转成 data URI)。

///|
/// 渲染参数。
pub(all) struct PreviewOptions {
  /// 输出宽度(像素)
  width : Int
  /// 输出高度(像素)
  height : Int
  /// 绕竖直轴旋转的角度(度)。调这个能从不同侧面看模型
  yaw : Double
  /// 俯仰角(度)。0 是正对着看,90 是从正上方往下看
  pitch : Double
  /// 线框颜色
  stroke : String
  /// 线框粗细
  stroke_width : Double
}

///|
/// 默认渲染参数:从一个斜上方 45 度的常见视角看。
pub fn PreviewOptions::default() -> PreviewOptions {
  {
    width: 480,
    height: 480,
    yaw: 35.0,
    pitch: 30.0,
    stroke: "#1f2430",
    stroke_width: 0.6,
  }
}

///|
/// 把网格渲染成一张 SVG 图。
///
/// 空网格返回一张空图而不是报错——调用方(网页)在文件还没上传时
/// 需要有个东西显示。
pub fn render_svg(mesh : Mesh, options : PreviewOptions) -> String {
  if mesh.is_empty() {
    return ""
  }

  let radians = @math.PI / 180.0
  let yaw = options.yaw * radians
  let pitch = options.pitch * radians

  // 先把模型挪到原点。不居中的话模型会跑到画布外面去。
  let center = match mesh.bounding_box() {
    Some(box) => {
      let size = box.size()
      Vec3::new(
        box.min.x + size.x / 2.0,
        box.min.y + size.y / 2.0,
        box.min.z + size.z / 2.0,
      )
    }
    None => Vec3::zero()
  }

  // 旋转,不含缩放和平移。先绕 Y 轴(竖直方向)转 yaw,再绕 X 轴转 pitch。
  // 返回旋转后的坐标,第三个分量就是深度(越大越远)。
  let rotate = fn(v : Vec3) -> (Double, Double, Double) {
    let x = v.x - center.x
    let y = v.y - center.y
    let z = v.z - center.z
    let x1 = x * @math.cos(yaw) + z * @math.sin(yaw)
    let z1 = -x * @math.sin(yaw) + z * @math.cos(yaw)
    let y2 = y * @math.cos(pitch) - z1 * @math.sin(pitch)
    let z2 = y * @math.sin(pitch) + z1 * @math.cos(pitch)
    (x1, y2, z2)
  }

  // 第一遍:先按「1 像素 = 1 单位」投影一遍,量出旋转之后实际占多大。
  //
  // 不能拿模型的最大边长当缩放依据——旋转之后投影范围会变大,
  // 对角线方向最多能到 √3 倍。按边长算会导致模型撑出画布,
  // 这个坑踩过一次(立方体四角都跑到框外去了)。
  let mut min_x = 1.0e18
  let mut max_x = -1.0e18
  let mut min_y = 1.0e18
  let mut max_y = -1.0e18
  for v in mesh.vertices() {
    let (rx, ry, _) = rotate(v)
    if rx < min_x {
      min_x = rx
    }
    if rx > max_x {
      max_x = rx
    }
    if ry < min_y {
      min_y = ry
    }
    if ry > max_y {
      max_y = ry
    }
  }
  let span_x = max_x - min_x
  let span_y = max_y - min_y
  // 退化情况(所有点重合)给个兜底,免得除零
  let span_x = if span_x < 1.0e-9 { 1.0 } else { span_x }
  let span_y = if span_y < 1.0e-9 { 1.0 } else { span_y }

  // 留 12% 的边距,免得模型的角顶在画布边缘上。
  // 取两个方向里更紧的那个约束,保证宽高都不溢出。
  let fit_x = options.width.to_double() * 0.88 / span_x
  let fit_y = options.height.to_double() * 0.88 / span_y
  let scale = if fit_x < fit_y { fit_x } else { fit_y }

  // 第二遍:按算好的缩放和平移投影。
  let offset_x = (min_x + max_x) / 2.0
  let offset_y = (min_y + max_y) / 2.0
  let half_w = options.width.to_double() / 2.0
  let half_h = options.height.to_double() / 2.0

  // 屏幕 Y 轴向下,所以竖直方向要取负
  let project = fn(v : Vec3) -> (Double, Double, Double) {
    let (rx, ry, rz) = rotate(v)
    (half_w + (rx - offset_x) * scale, half_h - (ry - offset_y) * scale, rz)
  }

  // 光源方向(已单位化)。放在左上偏前,画出来有立体感。
  let light = Vec3::new(-0.4, 0.7, 0.6).normalize()

  // 每个三角形算好投影后的三个点、深度、明暗
  let faces : Array[(Double, String)] = []
  for triangle in mesh.triangles {
    let (ax, ay, az) = project(triangle.a)
    let (bx, by, bz) = project(triangle.b)
    let (cx, cy, cz) = project(triangle.c)

    // 背面剔除:投影后三个点的绕向如果是顺时针,说明这个面背对观察者。
    // 叉乘的符号就是判据。
    let cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
    if cross > 0.0 {
      continue
    }

    // 明暗:法线和光源方向夹角的余弦。取绝对值是因为有些面的顶点
    // 顺序是反的,硬按有符号算会有整片全黑。
    let normal = triangle.computed_normal()
    let mut brightness = normal.dot(light).abs()
    // 压一下对比度:全黑的面看不清楚结构,全白的又糊成一片
    brightness = 0.45 + 0.55 * brightness

    let shade = (brightness * 235.0).to_int()
    let fill = "rgb(\{shade},\{shade},\{shade})"

    // 深度用三个顶点旋转后 Z 的平均值。画家算法要「远的先画」,
    // 而这里 Z 越大越远,所以排序时升序。
    let depth = (az + bz + cz) / 3.0

    faces.push(
      (
        depth,
        "",
      ),
    )
  }

  // 按深度排序,远的在前(先画)
  faces.sort_by(fn(a, b) {
    if a.0 < b.0 {
      -1
    } else if a.0 > b.0 {
      1
    } else {
      0
    }
  })

  let body : Array[String] = []
  for face in faces {
    body.push(face.1)
  }

  "" +
  "" +
  body.join("") +
  ""
}