///|
/// 文本格式导出。先做 OBJ 与 ASCII STL:两者都不需要二进制打包,
/// 结果可以直接用 `diff` 比对,也就顺势成了回归测试的一部分。
/// 二进制 STL 见文件末尾:它需要位打包,但也带来一个好处——
/// 可以写回读,用「导出再导入」检验网格是不是真的没丢东西。
pub fn Mesh::to_obj(self : Mesh, name : String) -> String {
let out = StringBuilder()
out.write_string("# mooncsg ")
out.write_string(name)
out.write_string("\n")
out.write_string("# vertices ")
out.write_string(self.vertex_count().to_string())
out.write_string(", triangles ")
out.write_string(self.triangle_count().to_string())
out.write_string("\n")
for p in self.positions {
out.write_string("v ")
out.write_string(p.x.to_string())
out.write_string(" ")
out.write_string(p.y.to_string())
out.write_string(" ")
out.write_string(p.z.to_string())
out.write_string("\n")
}
let mut i = 0
while i < self.triangle_count() {
// OBJ 索引从 1 开始
out.write_string("f ")
out.write_string((self.indices[i * 3] + 1).to_string())
out.write_string(" ")
out.write_string((self.indices[i * 3 + 1] + 1).to_string())
out.write_string(" ")
out.write_string((self.indices[i * 3 + 2] + 1).to_string())
out.write_string("\n")
i = i + 1
}
out.to_string()
}
///|
/// ASCII STL。STL 不带顶点索引,每个三角形重复三个顶点,
/// 所以文件通常比 OBJ 大——换来的是所有切片软件/查看器都认。
pub fn Mesh::to_stl(self : Mesh, name : String) -> String {
let out = StringBuilder()
out.write_string("solid ")
out.write_string(name)
out.write_string("\n")
let mut i = 0
while i < self.triangle_count() {
let (a, b, c) = self.triangle(i)
let n = b.sub(a).cross(c.sub(a)).normalize()
out.write_string(" facet normal ")
out.write_string(n.x.to_string())
out.write_string(" ")
out.write_string(n.y.to_string())
out.write_string(" ")
out.write_string(n.z.to_string())
out.write_string("\n outer loop\n")
out.write_string(" vertex ")
out.write_string(a.x.to_string())
out.write_string(" ")
out.write_string(a.y.to_string())
out.write_string(" ")
out.write_string(a.z.to_string())
out.write_string("\n")
out.write_string(" vertex ")
out.write_string(b.x.to_string())
out.write_string(" ")
out.write_string(b.y.to_string())
out.write_string(" ")
out.write_string(b.z.to_string())
out.write_string("\n")
out.write_string(" vertex ")
out.write_string(c.x.to_string())
out.write_string(" ")
out.write_string(c.y.to_string())
out.write_string(" ")
out.write_string(c.z.to_string())
out.write_string("\n")
out.write_string(" endloop\n endfacet\n")
i = i + 1
}
out.write_string("endsolid ")
out.write_string(name)
out.write_string("\n")
out.to_string()
}
///|
fn write_f32(buf : @buffer.Buffer, v : Double) -> Unit {
buf.write_uint_le(Float::from_double(v).reinterpret_as_uint())
}
///|
/// 小端读 4 字节。不用 `Bytes::unsafe_extract_uint_le`:那是实验 API,
/// 版本升级会被打断;4 个字节自己拼更稳,也不依赖字节序假设。
fn read_u32_le(data : Bytes, offset : Int32) -> UInt32 {
data.at(offset).to_uint() |
(data.at(offset + 1).to_uint() << 8) |
(data.at(offset + 2).to_uint() << 16) |
(data.at(offset + 3).to_uint() << 24)
}
///|
fn read_f32(data : Bytes, offset : Int32) -> Double {
Float::reinterpret_from_uint(read_u32_le(data, offset)).to_double()
}
///|
/// 二进制 STL:80 字节头 + `uint32` 三角形数 + 每三角形 50 字节
/// (法线 12 字节 + 三顶点 36 字节 + 属性 2 字节)。全部小端。
///
/// 头 80 字节留空。很多工具往里塞模型名,但格式本身不规定,
/// 空着头反而省掉一次「名字超过 80 字节怎么办」的讨论。
/// 顶点坐标按 `f32` 存储,这是格式本身的限制。往返测试因此用
/// 「相对误差 < 1e-6」而不是逐位相等:`f32` 只有 24 位有效数字。
pub fn Mesh::to_stl_binary(self : Mesh) -> Bytes {
let buf = @buffer.Buffer()
let mut i = 0
while i < 80 {
buf.write_byte(b'\x00')
i = i + 1
}
buf.write_uint_le(self.triangle_count().reinterpret_as_uint())
let mut t = 0
while t < self.triangle_count() {
let (a, b, c) = self.triangle(t)
let raw = b.sub(a).cross(c.sub(a))
let len = raw.length()
let n = if len > 0.0 { raw.div(len) } else { Vec3::zero() }
write_f32(buf, n.x)
write_f32(buf, n.y)
write_f32(buf, n.z)
write_f32(buf, a.x)
write_f32(buf, a.y)
write_f32(buf, a.z)
write_f32(buf, b.x)
write_f32(buf, b.y)
write_f32(buf, b.z)
write_f32(buf, c.x)
write_f32(buf, c.y)
write_f32(buf, c.z)
buf.write_byte(b'\x00')
buf.write_byte(b'\x00')
t = t + 1
}
buf.to_bytes()
}
///|
/// 二进制 STL 回读。
///
/// STL 没有索引,同一个顶点会在每个相邻三角形里重复出现。这里按
/// `f32` 位模式做精确去重(不是 eps 距离):同一位置写出去时位模式
/// 相同,读回来也相同,所以能还原原始顶点数。这个等价关系只在
/// 「导出后未经过任何编辑」时成立,正好是往返测试要的性质。
pub fn Mesh::from_stl_binary(data : Bytes) -> Mesh? {
if data.length() < 84 {
return None
}
let tri = read_u32_le(data, 80).reinterpret_as_int()
if tri < 0 || tri > 10000000 {
return None
}
if data.length() < 84 + tri * 50 {
return None
}
let positions : Array[Vec3] = []
let indices : Array[Int32] = []
let seen : @hashmap.HashMap[String, Int32] = @hashmap.HashMap(
[],
capacity=1024,
)
let mut t = 0
while t < tri {
let base = 84 + t * 50
let mut k = 0
while k < 3 {
let off = base + 12 + k * 12
let x = read_f32(data, off)
let y = read_f32(data, off + 4)
let z = read_f32(data, off + 8)
let key = "\{Float::from_double(x).reinterpret_as_uint()}," +
"\{Float::from_double(y).reinterpret_as_uint()}," +
"\{Float::from_double(z).reinterpret_as_uint()}"
match seen.get(key) {
Some(idx) => indices.push(idx)
None => {
let idx : Int32 = positions.length()
positions.push(Vec3::new(x, y, z))
seen.set(key, idx)
indices.push(idx)
}
}
k = k + 1
}
t = t + 1
}
Some(Mesh::new(positions, indices))
}