///|
/// Parse a Double from chars[start..end], supporting scientific notation (e.g. -3.41e-003).
fn parse_obj_double(chars : Array[Int], start : Int, end : Int) -> Double {
  if start >= end {
    return 0.0
  }
  let mut i = start
  let mut negative = false
  if chars[i] == 45 {
    // '-'
    negative = true
    i = i + 1
  } else if chars[i] == 43 {
    // '+'
    i = i + 1
  }
  let mut result = 0.0
  // Integer part
  while i < end {
    let d = chars[i] - 48
    if d >= 0 && d <= 9 {
      result = result * 10.0 + d.to_double()
      i = i + 1
    } else {
      break
    }
  }
  // Fractional part
  if i < end && chars[i] == 46 {
    // '.'
    i = i + 1
    let mut frac_div = 1.0
    while i < end {
      let d = chars[i] - 48
      if d >= 0 && d <= 9 {
        frac_div = frac_div * 10.0
        result = result + d.to_double() / frac_div
        i = i + 1
      } else {
        break
      }
    }
  }
  // Exponent part (e/E)
  if i < end && (chars[i] == 101 || chars[i] == 69) {
    // 'e' or 'E'
    i = i + 1
    let mut exp_neg = false
    if i < end && chars[i] == 45 {
      exp_neg = true
      i = i + 1
    } else if i < end && chars[i] == 43 {
      i = i + 1
    }
    let mut exp = 0
    while i < end {
      let d = chars[i] - 48
      if d >= 0 && d <= 9 {
        exp = exp * 10 + d
        i = i + 1
      } else {
        break
      }
    }
    let mut multiplier = 1.0
    for _k in 0.. Int {
  if start >= end {
    return 0
  }
  let mut i = start
  let mut negative = false
  if chars[i] == 45 {
    negative = true
    i = i + 1
  }
  let mut result = 0
  while i < end {
    let d = chars[i] - 48
    if d >= 0 && d <= 9 {
      result = result * 10 + d
      i = i + 1
    } else {
      break
    }
  }
  if negative {
    -result
  } else {
    result
  }
}

///|
/// Skip whitespace (space/tab) and return the new position.
fn skip_ws(chars : Array[Int], start : Int, end : Int) -> Int {
  let mut i = start
  while i < end && (chars[i] == 32 || chars[i] == 9) {
    i = i + 1
  }
  i
}

///|
/// Find the next whitespace or end.
fn find_ws(chars : Array[Int], start : Int, end : Int) -> Int {
  let mut i = start
  while i < end && chars[i] != 32 && chars[i] != 9 {
    i = i + 1
  }
  i
}

///|
/// Find the next occurrence of char code or end.
fn find_char(chars : Array[Int], ch : Int, start : Int, end : Int) -> Int {
  let mut i = start
  while i < end && chars[i] != ch {
    i = i + 1
  }
  i
}

///|
/// Parse an OBJ face vertex index component: "v", "v/vt", "v/vt/vn", or "v//vn".
/// Returns (vertex_index, texcoord_index, normal_index) as 1-based or 0 if absent.
fn parse_face_vertex(
  chars : Array[Int],
  start : Int,
  end : Int,
) -> (Int, Int, Int) {
  // Find first '/'
  let slash1 = find_char(chars, 47, start, end) // '/'
  if slash1 >= end {
    // Just vertex index: "v"
    return (parse_obj_int(chars, start, end), 0, 0)
  }
  let vi = parse_obj_int(chars, start, slash1)
  let after_slash1 = slash1 + 1
  // Find second '/'
  let slash2 = find_char(chars, 47, after_slash1, end)
  if slash2 >= end {
    // "v/vt"
    let vti = parse_obj_int(chars, after_slash1, end)
    return (vi, vti, 0)
  }
  if slash2 == after_slash1 {
    // "v//vn"
    let vni = parse_obj_int(chars, slash2 + 1, end)
    return (vi, 0, vni)
  }
  // "v/vt/vn"
  let vti = parse_obj_int(chars, after_slash1, slash2)
  let vni = parse_obj_int(chars, slash2 + 1, end)
  (vi, vti, vni)
}

///|
/// Parse a Wavefront OBJ string into a Mesh3D.
/// Supports v, vn, vt, f directives.
/// Handles scientific notation, N-gon triangulation, negative indices, and flat normal generation.
pub fn Mesh3D::from_obj(source : String) -> Mesh3D {
  let chars : Array[Int] = []
  for ch in source {
    chars.push(ch.to_int())
  }
  let len = chars.length()
  // Collected positions, normals, texcoords
  let positions : Array[Double] = [] // x,y,z triples
  let normals : Array[Double] = [] // nx,ny,nz triples
  let texcoords : Array[Double] = [] // u,v pairs
  // Output mesh data
  let vertex_data : Array[Double] = []
  let indices : Array[Int] = []
  let mut vertex_count = 0
  // Track whether any normals were provided
  let mut has_normals = false
  // For flat-normal post-processing: store face start indices
  let face_starts : Array[Int] = [] // index into indices array where each face's triangles start
  let face_tri_counts : Array[Int] = [] // number of triangles per face
  // Parse line by line
  let mut pos = 0
  while pos < len {
    // Find end of line
    let mut line_end = pos
    while line_end < len && chars[line_end] != 10 && chars[line_end] != 13 {
      line_end = line_end + 1
    }
    // Skip leading whitespace
    let line_start = skip_ws(chars, pos, line_end)
    if line_start < line_end {
      let first = chars[line_start]
      if first == 118 {
        // 'v'
        if line_start + 1 < line_end && chars[line_start + 1] == 110 {
          // "vn" - vertex normal
          let mut p = skip_ws(chars, line_start + 2, line_end)
          let e1 = find_ws(chars, p, line_end)
          let nx = parse_obj_double(chars, p, e1)
          p = skip_ws(chars, e1, line_end)
          let e2 = find_ws(chars, p, line_end)
          let ny = parse_obj_double(chars, p, e2)
          p = skip_ws(chars, e2, line_end)
          let e3 = find_ws(chars, p, line_end)
          let nz = parse_obj_double(chars, p, e3)
          normals.push(nx)
          normals.push(ny)
          normals.push(nz)
        } else if line_start + 1 < line_end && chars[line_start + 1] == 116 {
          // "vt" - texture coordinate
          let mut p = skip_ws(chars, line_start + 2, line_end)
          let e1 = find_ws(chars, p, line_end)
          let u = parse_obj_double(chars, p, e1)
          p = skip_ws(chars, e1, line_end)
          let e2 = find_ws(chars, p, line_end)
          let v = parse_obj_double(chars, p, e2)
          texcoords.push(u)
          texcoords.push(v)
        } else if line_start + 1 < line_end &&
          (chars[line_start + 1] == 32 || chars[line_start + 1] == 9) {
          // "v " - vertex position
          let mut p = skip_ws(chars, line_start + 1, line_end)
          let e1 = find_ws(chars, p, line_end)
          let x = parse_obj_double(chars, p, e1)
          p = skip_ws(chars, e1, line_end)
          let e2 = find_ws(chars, p, line_end)
          let y = parse_obj_double(chars, p, e2)
          p = skip_ws(chars, e2, line_end)
          let e3 = find_ws(chars, p, line_end)
          let z = parse_obj_double(chars, p, e3)
          positions.push(x)
          positions.push(y)
          positions.push(z)
        }
      } else if first == 102 &&
        line_start + 1 < line_end &&
        (chars[line_start + 1] == 32 || chars[line_start + 1] == 9) {
        // "f " - face
        let face_verts : Array[(Int, Int, Int)] = [] // (vi, vti, vni)
        let mut p = skip_ws(chars, line_start + 1, line_end)
        while p < line_end {
          let tok_end = find_ws(chars, p, line_end)
          if tok_end > p {
            face_verts.push(parse_face_vertex(chars, p, tok_end))
          }
          p = skip_ws(chars, tok_end, line_end)
        }
        let num_verts = positions.length() / 3
        let num_norms = normals.length() / 3
        let num_texs = texcoords.length() / 2
        // Check if this face has normals
        let mut face_has_normals = false
        for fv in face_verts {
          let (_, _, vni) = fv
          if vni != 0 {
            face_has_normals = true
            has_normals = true
            break
          }
        }
        // Emit vertices for this face
        let base_idx = vertex_count
        for fv in face_verts {
          let (vi_raw, vti_raw, vni_raw) = fv
          // Resolve vertex index (1-based, negative = relative)
          let vi = if vi_raw < 0 { num_verts + vi_raw + 1 } else { vi_raw }
          let pi = (vi - 1) * 3
          let px = if pi >= 0 && pi + 2 < positions.length() {
            positions[pi]
          } else {
            0.0
          }
          let py = if pi >= 0 && pi + 2 < positions.length() {
            positions[pi + 1]
          } else {
            0.0
          }
          let pz = if pi >= 0 && pi + 2 < positions.length() {
            positions[pi + 2]
          } else {
            0.0
          }
          // Resolve normal
          let mut nx = 0.0
          let mut ny = 0.0
          let mut nz = 0.0
          if face_has_normals && vni_raw != 0 {
            let vni = if vni_raw < 0 {
              num_norms + vni_raw + 1
            } else {
              vni_raw
            }
            let ni = (vni - 1) * 3
            if ni >= 0 && ni + 2 < normals.length() {
              nx = normals[ni]
              ny = normals[ni + 1]
              nz = normals[ni + 2]
            }
          }
          // Resolve texcoord
          let mut u = 0.0
          let mut v = 0.0
          if vti_raw != 0 {
            let vti = if vti_raw < 0 { num_texs + vti_raw + 1 } else { vti_raw }
            let ti = (vti - 1) * 2
            if ti >= 0 && ti + 1 < texcoords.length() {
              u = texcoords[ti]
              v = texcoords[ti + 1]
            }
          }
          push_vertex(vertex_data, px, py, pz, nx, ny, nz, u, v)
          vertex_count = vertex_count + 1
        }
        // Triangulate (fan from first vertex)
        let n_face = face_verts.length()
        let face_idx_start = indices.length()
        let mut tri_count = 0
        for k in 1..<(n_face - 1) {
          indices.push(base_idx)
          indices.push(base_idx + k)
          indices.push(base_idx + k + 1)
          tri_count = tri_count + 1
        }
        if !face_has_normals {
          face_starts.push(face_idx_start)
          face_tri_counts.push(tri_count)
        }
      }
    }
    // Advance past newline
    pos = line_end
    if pos < len && chars[pos] == 13 {
      pos = pos + 1 // CR
    }
    if pos < len && chars[pos] == 10 {
      pos = pos + 1 // LF
    }
  }
  // Post-process: compute flat normals for faces without explicit normals
  if !has_normals || face_starts.length() > 0 {
    for fi in 0.. 1.0e-10 {
        nx = nx / mag
        ny = ny / mag
        nz = nz / mag
      }
      // Write normal to all vertices in this face's triangles
      for ti in 0..