// 3MF 解析。
//
// 3MF(3D Manufacturing Format)是现代切片软件实际使用的格式——PrusaSlicer、
// Bambu Studio、Cura 导出的都是它,定位上就是 STL 的正式替代品。
//
// 它和 STL 有两处根本不同:
//
//   1. **它是个 ZIP 包**(OPC 容器),模型数据装在里面一个 XML 部件里,
//      通常叫 3D/3dmodel.model。
//   2. **顶点是共享的**。三角形存的是顶点表下标,和 OBJ 一样,
//      而不是像 STL 那样每个三角形把自己的三个顶点重写一遍。
//
// 第二点对「能不能打印」这个问题特别重要:**拓扑关系是现成的**。
// STL 里「哪些三角形共用同一个顶点」得靠容差焊接去猜(差 1e-9 的算不算同一个点?),
// 3MF 直接写清楚了。所以同一个模型,3MF 的水密性判断比 STL 更可靠。
//
// 代价是格式复杂得多:要解 ZIP、要解 XML,对象还能互相引用拼成装配体。

///|
/// 一个 3MF 对象。
///
/// 两种形态二选一:要么自带网格(顶点表 + 三角形索引),要么是一组组件
/// (引用别的对象,各带一个变换矩阵)。用两个数组共存来表示,
/// 比套一层枚举少绕几下——解析时本来就是一个一个往里塞。
priv struct MfObject {
  /// 共享顶点表
  vertices : Array[Vec3]
  /// 三角形,存的是顶点表下标
  triangles : Array[(Int, Int, Int)]
  /// 组件:引用的对象 id + 变换矩阵
  components : Array[(Int, FixedArray[Double]?)]
}

///|
/// 3MF 是个 ZIP 包。只看魔数,真正的校验在 `parse_3mf` 里做——
/// 「打开才知道」的检查不该塞进一个只看开头的判断函数。
pub fn is_3mf(data : Bytes) -> Bool {
  data.length() >= 4 &&
  data[0] == b'P' &&
  data[1] == b'K' &&
  // 空 ZIP 的开头是 PK\x05\x06,这里要的是有内容的那种
  data[2] == b'\x03' &&
  data[3] == b'\x04'
}

///|
/// 解析 3MF:解 ZIP → 找模型部件 → 解 XML → 按 build 里列的对象拼出网格。
pub fn parse_3mf(data : Bytes) -> Result[Mesh, String] {
  // 解 ZIP。fzip 自带 zip bomb 防护(解压比超过 1000 倍直接拒绝)
  // 和不安全路径拒绝(绝对路径、`..` 一律不收)——解析用户传上来的文件,
  // 这两道防线是必须的,自己写很容易漏。
  let entries = @fzip.unzip_sync(data.to_fixedarray()) catch {
    @fzip.FzipError(message~, ..) =>
      return Err("这不是一个合法的 3MF:ZIP 解不开——\{message}")
  }
  let xml = match find_model_part(entries) {
    Ok(text) => text
    Err(msg) => return Err(msg)
  }
  parse_model_xml(xml)
}

///|
/// 从压缩包里找出模型部件。
///
/// 正规做法是读 `_rels/.rels`,里面记着哪个部件是模型。绝大多数文件确实就叫
/// `3D/3dmodel.model`。两条路都走:先按规范查关系表,查不到再退化成
/// 「找唯一一个 .model 文件」——有些导出器写的关系表不规范,但文件名是对的。
fn find_model_part(
  entries : Array[(String, FixedArray[Byte])],
) -> Result[String, String] {
  let target = match find_entry(entries, "_rels/.rels") {
    Some(raw) => rels_model_target(@utf8.decode_lossy(Bytes::from_array(raw)))
    None => None
  }
  match target {
    Some(path) =>
      match find_entry(entries, path) {
        Some(raw) => Ok(@utf8.decode_lossy(Bytes::from_array(raw)))
        None =>
          Err(
            "3MF 的关系表指向「\{path}」,但包里没有这个部件",
          )
      }
    None => {
      // 退路:找 .model 文件
      let models : Array[String] = []
      for entry in entries {
        let (name, _) = entry
        if name.to_lower().has_suffix(".model") {
          models.push(name)
        }
      }
      if models.length() == 0 {
        Err(
          "这个 ZIP 里没有 3D 模型部件(找不到 .model 文件),不是 3MF 文件",
        )
      } else if models.length() > 1 {
        Err(
          "这个 3MF 里有 \{models.length()} 个 .model 部件,关系表也没说是哪个,认不出来",
        )
      } else {
        match find_entry(entries, models[0]) {
          Some(raw) => Ok(@utf8.decode_lossy(Bytes::from_array(raw)))
          None => Err("读不到模型部件「\{models[0]}」")
        }
      }
    }
  }
}

///|
/// 从 `_rels/.rels` 里找出模型部件路径。
///
/// 关系表长这样:
///
/// ```xml
/// 
/// ```
///
/// 认的是 Type 后缀而不是 Id——Id 是随便起的。
fn rels_model_target(xml : String) -> String? {
  let reader = @xml.Reader::from_string(xml)
  let mut found : String? = None
  while found is None {
    let event = reader.read_event() catch {
      // 关系表坏了不该让整个解析失败,退回去按文件名找就行
      _ => break
    }
    let element = match event.kind {
      @xml.EventKind::Start(e) | @xml.EventKind::Empty(e) => e
      // **必须在这里停下来。**
      //
      // 读到文件末尾之后,`read_event` 会一直返回 `Eof` 而不会抛错。
      // 少了这个分支,一个「关系表是合法 XML、但里面没有 3dmodel 关系」的包
      // 就会在这里无限空转——CPU 跑满,命令行和网页都卡死,而且永远等不到报错。
      // 现实中把 .docx 改名成 .3mf 就是这个样子。
      @xml.EventKind::Eof => break
      _ => continue
    }
    if local_name(element.name) != "Relationship" {
      continue
    }
    let kind = match element.get("Type") {
      Some(t) => t
      None => continue
    }
    if !kind.has_suffix("/3dmodel") {
      continue
    }
    // 关系对上了但没有 Target——文件不规范,不算数,继续往后找
    match element.get("Target") {
      Some(t) => found = Some(normalize_part_path(t))
      None => ()
    }
  }
  found
}

///|
/// OPC 里的路径可能写成 `/3D/3dmodel.model` 这种带前导斜杠的,
/// 而压缩包里存的是 `3D/3dmodel.model`。统一去掉前导斜杠。
fn normalize_part_path(path : String) -> String {
  if path.has_prefix("/") {
    path[1:].to_owned()
  } else {
    path
  }
}

///|
/// 按名字找一个部件。
///
/// 大小写不敏感——OPC 的部件名在规范里不区分大小写,
/// 实际文件里 `3D/3dmodel.model` 和 `3d/3DModel.model` 都出现过。
fn find_entry(
  entries : Array[(String, FixedArray[Byte])],
  name : String,
) -> FixedArray[Byte]? {
  let want = normalize_entry_name(name)
  for entry in entries {
    let (entry_name, bytes) = entry
    if normalize_entry_name(entry_name) == want {
      return Some(bytes)
    }
  }
  None
}

///|
/// 把压缩包里的条目名规范化,好用来比对。
///
/// 两件事:
///
/// 1. **转小写**——OPC 的部件名不区分大小写。
/// 2. **反斜杠换成正斜杠**——ZIP 规范要求条目名用正斜杠,但 Windows 上一些
///    打包工具(比如 PowerShell 的 `Compress-Archive`)会写成反斜杠。
///    不归一化的话,一个结构完好的包会因为 `3D\3dmodel.model` 匹配不上
///    `3D/3dmodel.model` 而被判成「没有模型部件」——报错完全指错了方向。
fn normalize_entry_name(name : String) -> String {
  let lower = name.to_lower()
  if lower.contains("\\") {
    lower.replace_all(old="\\", new="/")
  } else {
    lower
  }
}

///|
/// 去掉命名空间前缀,只留本地名。
///
/// 3MF 的 XML 用默认命名空间,正常情况下标签就是 ``。
/// 但规范允许写前缀形式 ``,
/// 那就得把 `m:` 切掉才能按名字匹配。
fn local_name(name : String) -> String {
  let mut last : StringView = name[:]
  for part in name.split(":") {
    last = part
  }
  last.to_owned()
}

///|
/// 取命名空间前缀。没有冒号就是空串(默认命名空间)。
fn prefix_of(name : String) -> String {
  match name.find(":") {
    Some(i) => name[:i].to_owned()
    None => ""
  }
}

///|
/// 3MF 核心命名空间的识别特征。
///
/// 不写死完整 URI 是因为后面可能跟版本号(规范的命名空间是
/// `http://schemas.microsoft.com/3dmanufacturing/core/2015/02`)。
const CORE_NS_MARK : String = "3dmanufacturing/core"

///|
/// 从根元素的命名空间声明里找出**核心命名空间用的是哪个前缀**。
///
/// 为什么需要这个:只看本地名会把别的命名空间的同名元素也认成核心元素。
/// 最现实的例子是 3MF 的切片扩展——里面有 `` / ``,
/// 那是**二维**顶点,规范只定义了 x 和 y,**没有 z**。不区分命名空间的话
/// 它会被当成核心的三维顶点,报一句「少了坐标属性 z」,
/// 结果是一份完全合法的文件整个解析不了。
///
/// 返回 `None` 表示根元素上没有声明核心命名空间,那就退回老规矩:
/// 不带前缀的元素都算核心的。
fn detect_core_prefix(element : @xml.XmlElement) -> String? {
  for attr in element.attributes {
    if !attr.value.contains(CORE_NS_MARK) {
      continue
    }
    // 默认命名空间写成 xmlns="...",前缀是空串
    if attr.name == "xmlns" {
      return Some("")
    }
    if attr.name.has_prefix("xmlns:") {
      return Some(attr.name[6:].to_owned())
    }
  }
  None
}

///|
/// 解析 3D/3dmodel.model。
///
/// 关心的标签只有几个:`model`(拿单位)、`object`(攒顶点和三角形)、
/// `component`(装配体引用)、`item`(build 里列出要打印的对象)。
/// 其余(metadata、basematerials、切片信息……)全部跳过——
/// 它们和「能不能打印」没关系。
fn parse_model_xml(xml : String) -> Result[Mesh, String] {
  let reader = @xml.Reader::from_string(xml)
  let objects : Map[Int, MfObject] = Map([])
  // build 里列出的 (对象 id, 变换)
  let build : Array[(Int, FixedArray[Double]?)] = []

  // 单位换算到毫米。STL 没有单位概念,3MF 有,不换算的话
  // 一个以英寸建模的模型体积会差 25.4³ ≈ 16387 倍
  let mut unit_scale = 1.0

  // 核心命名空间用的是哪个前缀。`None` = 还没见到根元素
  let mut core_prefix : String? = None

  // 当前正在攒的对象
  let mut cur_id : Int? = None
  let mut cur_vertices : Array[Vec3] = []
  let mut cur_triangles : Array[(Int, Int, Int)] = []
  let mut cur_components : Array[(Int, FixedArray[Double]?)] = []

  while !reader.is_eof() {
    let event = reader.read_event() catch {
      err =>
        return Err(
          "3MF 的模型 XML 解析失败(第 \{reader.line()} 行):\{err}",
        )
    }
    let element = match event.kind {
      @xml.EventKind::Start(e) | @xml.EventKind::Empty(e) => e
      @xml.EventKind::End(name) => {
        // 对象在这里收尾。但自闭合的 `` 不会走到这——
        // 那种情况由下一次 object 开始时的 flush 兜住。
        // 同样要认命名空间:`` 不是核心的 object
        let end_is_core = match core_prefix {
          Some(p) => prefix_of(name) == p
          None => prefix_of(name) == ""
        }
        if end_is_core && local_name(name) == "object" {
          flush_object(
            objects, cur_id, cur_vertices, cur_triangles, cur_components,
          )
          cur_id = None
          cur_vertices = []
          cur_triangles = []
          cur_components = []
        }
        continue
      }
      _ => continue
    }
    let tag = local_name(element.name)

    if tag == "model" {
      // 根元素上声明了核心命名空间,记下它用的前缀
      core_prefix = detect_core_prefix(element)
    }

    // 只处理核心命名空间的元素。
    //
    // 带别的前缀(比如切片扩展的 `s:`)一律跳过——它们的本地名可能和核心元素
    // 撞车,但语义完全不同。最典型的是 ``:那是**二维**顶点,
    // 只有 x 和 y,当成三维顶点就会报「少了坐标属性 z」,整份合法文件都读不了。
    let prefix = prefix_of(element.name)
    let is_core = match core_prefix {
      Some(p) => prefix == p
      // 根元素上没声明命名空间,退回老规矩:不带前缀的当核心元素
      None => prefix == ""
    }
    if !is_core {
      continue
    }

    if tag == "model" {
      // 单位:micron / millimeter / centimeter / inch / foot / meter
      match element.get("requiredextensions") {
        Some(required) =>
          if obj_words(required).length() > 0 {
            return Err(
              "3MF 声明了本工具未实现的必需扩展 requiredextensions=\"\{required}\";不能仅按核心三角网格解释",
            )
          }
        None => ()
      }
      match element.get("unit") {
        Some(u) =>
          unit_scale = match unit_scale_of(u) {
            Ok(v) => v
            Err(msg) => return Err(msg)
          }
        None => ()
      }
    } else if tag == "object" {
      // 开始一个新对象。先把上一个收尾——这样自闭合的、漏了结束标签的
      // 坏文件也能解析出大部分内容,而不是丢掉整个模型
      flush_object(objects, cur_id, cur_vertices, cur_triangles, cur_components)
      let id = match attribute_int(element, "id", "object") {
        Ok(v) => v
        Err(msg) => return Err(msg)
      }
      cur_id = Some(id)
      cur_vertices = []
      cur_triangles = []
      cur_components = []
    } else if tag == "vertex" {
      let v = match attribute_vec3(element) {
        Ok(v) => v
        Err(msg) => return Err(msg)
      }
      cur_vertices.push(v)
    } else if tag == "triangle" {
      // 三角形存的是顶点表下标,不是坐标
      let a = match attribute_int(element, "v1", "triangle") {
        Ok(v) => v
        Err(msg) => return Err(msg)
      }
      let b = match attribute_int(element, "v2", "triangle") {
        Ok(v) => v
        Err(msg) => return Err(msg)
      }
      let c = match attribute_int(element, "v3", "triangle") {
        Ok(v) => v
        Err(msg) => return Err(msg)
      }
      cur_triangles.push((a, b, c))
    } else if tag == "component" {
      let id = match attribute_int(element, "objectid", "component") {
        Ok(v) => v
        Err(msg) => return Err(msg)
      }
      let tf = match attribute_transform(element) {
        Ok(t) => t
        Err(msg) => return Err(msg)
      }
      cur_components.push((id, tf))
    } else if tag == "item" {
      let id = match attribute_int(element, "objectid", "item") {
        Ok(v) => v
        Err(msg) => return Err(msg)
      }
      let tf = match attribute_transform(element) {
        Ok(t) => t
        Err(msg) => return Err(msg)
      }
      build.push((id, tf))
    }
  }
  // 最后一个对象
  flush_object(objects, cur_id, cur_vertices, cur_triangles, cur_components)

  if build.length() == 0 {
    return Err(
      "这个 3MF 的 build 里没有列任何对象,没有东西可以打印",
    )
  }

  // 按 build 列出的对象拼出最终网格
  let triangles : Array[Triangle] = []
  for (id, tf) in build {
    // item 不带 transform 就是原样放进去
    let transform = match tf {
      Some(t) => t
      None => identity_transform()
    }
    let body = match build_object(objects, id, transform, 0) {
      Ok(v) => v
      Err(msg) => return Err(msg)
    }
    for tri in body {
      triangles.push(tri)
    }
  }

  if triangles.length() == 0 {
    // 措辞要说清楚两种可能,别让人以为文件坏了。
    // 3MF 的 Beam Lattice 扩展明确规定:挂了 beamlattice 的 mesh
    // **可以一个三角形都没有**(规范原话:MAY consist of no triangles),
    // 几何全写在扩展元素里。这种文件是合法的,只是本工具处理不了
    return Err(
      "这个 3MF 的 build 引用的对象里没有三角形——可能是空对象," +
      "也可能是几何写在扩展元素里的对象(比如 beam lattice,本工具只处理三角网格)",
    )
  }
  let scaled = scale_mesh(triangles, unit_scale)
  for tri in scaled {
    if !finite_vertex(tri.a) || !finite_vertex(tri.b) || !finite_vertex(tri.c) {
      return Err("3MF 单位换算后坐标超出有限范围")
    }
  }
  Ok(Mesh::new(scaled))
}

///|
/// 把一个对象的当前状态收进对象表。
///
/// 抽成函数是因为「收尾」要在两个地方发生:遇到结束标签时,
/// 以及开始下一个对象之前。后一处是给自闭合或者结构不完整的文件兜底的。
fn flush_object(
  objects : Map[Int, MfObject],
  id : Int?,
  vertices : Array[Vec3],
  triangles : Array[(Int, Int, Int)],
  components : Array[(Int, FixedArray[Double]?)],
) -> Unit {
  match id {
    Some(i) => objects.set(i, MfObject::{ vertices, triangles, components, })
    None => ()
  }
}

///|
/// 把所有顶点乘一个系数。
///
/// 单位换算单独走一遍,而不是在解析顶点时乘——因为变换矩阵里的平移量
/// 也是同一个单位的长度,两处都得乘。统一到最后做,不会漏。
fn scale_mesh(triangles : Array[Triangle], scale : Double) -> Array[Triangle] {
  if scale == 1.0 {
    return triangles
  }
  let out : Array[Triangle] = []
  for t in triangles {
    out.push({
      normal: t.normal,
      a: t.a.scale(scale),
      b: t.b.scale(scale),
      c: t.c.scale(scale),
    })
  }
  out
}

///|
/// 把一个对象(可能是装配体)展开成三角形,顺便套上变换。
///
/// `depth` 是递归深度。3MF 允许对象互相引用,**引用成环是坏文件,
/// 但真会出现**,不设上限就会栈溢出。
fn build_object(
  objects : Map[Int, MfObject],
  id : Int,
  transform : FixedArray[Double],
  depth : Int,
) -> Result[Array[Triangle], String] {
  if depth > 32 {
    return Err(
      "3MF 的对象引用套得太深(超过 32 层),可能是循环引用",
    )
  }
  let obj = match objects.get(id) {
    Some(o) => o
    None => return Err("3MF 里引用了不存在的对象 id=\{id}")
  }
  let out : Array[Triangle] = []
  if obj.triangles.length() > 0 {
    // 共享顶点表:三角形存的是下标,到这里才真正取出坐标
    let n = obj.vertices.length()
    for (i, j, k) in obj.triangles {
      if i < 0 || j < 0 || k < 0 || i >= n || j >= n || k >= n {
        return Err(
          "3MF 对象 id=\{id} 里有三角形的顶点下标越界(顶点表只有 \{n} 个," +
          "引用到了 \{i}/\{j}/\{k})",
        )
      }
      let a = apply_transform(obj.vertices[i], transform)
      let b = apply_transform(obj.vertices[j], transform)
      let c = apply_transform(obj.vertices[k], transform)
      if !finite_vertex(a) || !finite_vertex(b) || !finite_vertex(c) {
        return Err("3MF 对象 id=\{id} 变换后坐标超出有限范围")
      }
      out.push(Triangle::from_vertices(a, b, c))
    }
  }
  for (child_id, child_tf) in obj.components {
    // 组件自己的变换要先做,再套外层的——顺序反了位置就错了
    let combined = match child_tf {
      Some(t) => compose_transform(t, transform)
      None => transform
    }
    for value in combined {
      if not_finite(value) {
        return Err("3MF 对象 id=\{child_id} 的嵌套变换溢出")
      }
    }
    let sub = match build_object(objects, child_id, combined, depth + 1) {
      Ok(v) => v
      Err(msg) => return Err(msg)
    }
    for tri in sub {
      out.push(tri)
    }
  }
  Ok(out)
}

///|
/// 3MF 里的长度单位换算成毫米。
///
/// STL 没有单位概念,一路按毫米处理;3MF 有,不换算的话
/// 一个以英寸建模的模型体积会差 25.4³ ≈ 16387 倍,报告里的数字全是错的。
fn unit_scale_of(unit : String) -> Result[Double, String] {
  match unit {
    "micron" => Ok(0.001)
    "millimeter" => Ok(1.0)
    "centimeter" => Ok(10.0)
    "inch" => Ok(25.4)
    "foot" => Ok(304.8)
    "meter" => Ok(1000.0)
    _ =>
      Err(
        "3MF 里写了一个不认识的单位「\{unit}」," +
        "规范里只有 micron / millimeter / centimeter / inch / foot / meter",
      )
  }
}

///|
/// 单位矩阵。build 里的 item 和 component 都可以不带 transform,
/// 不带就是原样放。
fn identity_transform() -> FixedArray[Double] {
  [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
}

///|
/// 把一个点按 3MF 的变换矩阵变换。
///
/// 3MF 写的是 12 个数,代表一个 4×3 矩阵,按行排:
///
/// ```text
/// m00 m01 m02
/// m10 m11 m12
/// m20 m21 m22
/// m30 m31 m32      ← 平移
/// ```
///
/// 用的是**行向量**约定:点当作 1×4 的 `[x y z 1]` 去右乘矩阵:
///
/// ```text
/// x' = x·m00 + y·m10 + z·m20 + m30
/// y' = x·m01 + y·m11 + z·m21 + m31
/// z' = x·m02 + y·m12 + z·m22 + m32
/// ```
///
/// 注意平移在 m30/m31/m32,也就是数组的第 9/10/11 个——**不是前三个**。
/// 按列优先、或者把平移放前面,都会得到完全错误的位置。
fn apply_transform(v : Vec3, m : FixedArray[Double]) -> Vec3 {
  Vec3::{
    x: v.x * m[0] + v.y * m[3] + v.z * m[6] + m[9],
    y: v.x * m[1] + v.y * m[4] + v.z * m[7] + m[10],
    z: v.x * m[2] + v.y * m[5] + v.z * m[8] + m[11],
  }
}

///|
/// 先把点按 `first` 变换、再按 `second` 变换,合成一个矩阵。
///
/// 行向量约定下「先 A 后 B」就是 A×B。4×3 的矩阵要隐式补上第 4 列
/// (前三行填 0、最后一行填 1)才能相乘。
fn compose_transform(
  first : FixedArray[Double],
  second : FixedArray[Double],
) -> FixedArray[Double] {
  let out : Array[Double] = []
  for i = 0; i < 4; i = i + 1 {
    for j = 0; j < 3; j = j + 1 {
      let mut sum = 0.0
      for k = 0; k < 3; k = k + 1 {
        sum = sum + first[i * 3 + k] * second[k * 3 + j]
      }
      // 第 4 行(i=3)还要加上 second 自己的平移
      if i == 3 {
        sum = sum + second[9 + j]
      }
      out.push(sum)
    }
  }
  [
    out[0],
    out[1],
    out[2],
    out[3],
    out[4],
    out[5],
    out[6],
    out[7],
    out[8],
    out[9],
    out[10],
    out[11],
  ]
}

///|
/// 读一个整数属性。
///
/// `parse_double` 收的比整数字面量宽得多:`1.5`、`1e3`、`2_0`、`nan`、`inf`
/// 它都认。所以这里必须自己把关——不把关的话 `` 会
/// 静默变成下标 0,用户拿到一个由错顶点拼出来的网格,而且毫无提示。
///
/// 注意 `inf` 要单独判:`inf.round()` 还是 `inf`,光比 `value != value.round()`
/// 拦不住它。
fn attribute_int(
  element : @xml.XmlElement,
  name : String,
  tag : String,
) -> Result[Int, String] {
  match element.get(name) {
    Some(raw) => {
      let value = @string.parse_double(raw) catch {
        _ =>
          return Err("3MF 的 <\{tag}> 里 \{name}=\"\{raw}\" 不是个整数")
      }
      if value.is_nan() || value.is_inf() || value != value.round() {
        return Err("3MF 的 <\{tag}> 里 \{name}=\"\{raw}\" 不是个整数")
      }
      Ok(value.to_int())
    }
    None => Err("3MF 的 <\{tag}> 少了一个必需的属性 \{name}")
  }
}

///|
/// 读 x / y / z 三个坐标属性。
///
/// 坐标也要查有限性:`x="inf"` 或 `x="nan"` 会让包围盒和体积算出一堆垃圾,
/// 而且从报告上看不出是文件坏了——数字就是难看点而已,不如直接报错。
fn attribute_vec3(element : @xml.XmlElement) -> Result[Vec3, String] {
  let out : Array[Double] = []
  for name in ["x", "y", "z"] {
    let raw = match element.get(name) {
      Some(v) => v
      None => return Err("3MF 的  少了坐标属性 \{name}")
    }
    let value = @string.parse_double(raw) catch {
      _ => return Err("3MF 的  里 \{name}=\"\{raw}\" 不是个数字")
    }
    if value.is_nan() || value.is_inf() {
      return Err(
        "3MF 的  里 \{name}=\"\{raw}\" 不是个有限的数字",
      )
    }
    out.push(value)
  }
  Ok(Vec3::new(out[0], out[1], out[2]))
}

///|
/// 读可选的位置变换属性。
///
/// 12 个数,空格分隔。个数不对就报错——少一个数会让整条装配链错位,
/// 而错位后的模型看起来「有东西」但体积完全不对,是最难查的那种错。
fn attribute_transform(
  element : @xml.XmlElement,
) -> Result[FixedArray[Double]?, String] {
  let raw = match element.get("transform") {
    Some(v) => v
    None => return Ok(None)
  }
  let out : Array[Double] = []
  for token in obj_words(raw) {
    if token.length() == 0 {
      continue
    }
    let value = @string.parse_double(token) catch {
      _ => return Err("3MF 的 transform 里『\{token}』不是个数字")
    }
    if not_finite(value) {
      return Err("3MF 的 transform 里『\{token}』不是有限数字")
    }
    out.push(value)
  }
  if out.length() != 12 {
    return Err(
      "3MF 的 transform 应该有 12 个数(4 行 3 列的矩阵),实际有 \{out.length()} 个",
    )
  }
  Ok(
    Some([
      out[0],
      out[1],
      out[2],
      out[3],
      out[4],
      out[5],
      out[6],
      out[7],
      out[8],
      out[9],
      out[10],
      out[11],
    ]),
  )
}