///|
/// STL (Stereolithography) 3D mesh parser for binary and ASCII formats.
///|
pub(all) struct Triangle3 {
normal : Vec3
v1 : Vec3
v2 : Vec3
v3 : Vec3
} derive(Debug, Eq)
///|
pub fn Triangle3::new(
normal : Vec3,
v1 : Vec3,
v2 : Vec3,
v3 : Vec3,
) -> Triangle3 {
{ normal, v1, v2, v3, }
}
///|
pub fn Triangle3::min_z(self : Triangle3) -> Double {
let mut min = self.v1.z
if self.v2.z < min {
min = self.v2.z
}
if self.v3.z < min {
min = self.v3.z
}
min
}
///|
pub fn Triangle3::max_z(self : Triangle3) -> Double {
let mut max = self.v1.z
if self.v2.z > max {
max = self.v2.z
}
if self.v3.z > max {
max = self.v3.z
}
max
}
///|
pub(all) struct Mesh3 {
triangles : Array[Triangle3]
bounds : BBox3
} derive(Debug, Eq)
///|
/// Helper to compute 2^e for float32 decoding.
fn double_pow2(e : Int32) -> Double {
let mut res = 1.0
if e > 0 {
for _i = 0; _i < e; _i = _i + 1 {
res = res * 2.0
}
} else if e < 0 {
let neg = -e
for _i = 0; _i < neg; _i = _i + 1 {
res = res * 0.5
}
}
res
}
///|
/// Decode little-endian IEEE 754 32-bit float from bytes.
pub fn read_f32_le(data : Bytes, offset : Int32) -> Double {
if offset + 4 > data.length() {
return 0.0
}
let b0 = data[offset].to_int()
let b1 = data[offset + 1].to_int()
let b2 = data[offset + 2].to_int()
let b3 = data[offset + 3].to_int()
let u = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
let sign = if (u & (1 << 31)) != 0 { -1.0 } else { 1.0 }
let exp = (u >> 23) & 0xFF
let frac = u & 0x7FFFFF
if exp == 0 {
if frac == 0 {
0.0
} else {
sign * (frac.to_double() / 8388608.0) * double_pow2(-126)
}
} else if exp == 255 {
0.0
} else {
let val = 1.0 + frac.to_double() / 8388608.0
let e = exp - 127
sign * val * double_pow2(e)
}
}
///|
/// Decode little-endian uint32.
pub fn read_u32_le(data : Bytes, offset : Int32) -> Int32 {
if offset + 4 > data.length() {
return 0
}
let b0 = data[offset].to_int()
let b1 = data[offset + 1].to_int()
let b2 = data[offset + 2].to_int()
let b3 = data[offset + 3].to_int()
b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
}
///|
/// Construct a Mesh3 from an array of triangles and automatically compute its BBox3.
pub fn Mesh3::from_triangles(triangles : Array[Triangle3]) -> Mesh3 {
if triangles.is_empty() {
let zero = Vec3::zero()
return { triangles, bounds: BBox3::new(zero, zero), }
}
let first = triangles[0].v1
let mut min_x = first.x
let mut max_x = first.x
let mut min_y = first.y
let mut max_y = first.y
let mut min_z = first.z
let mut max_z = first.z
for tri in triangles {
let vs = [tri.v1, tri.v2, tri.v3]
for v in vs {
if v.x < min_x {
min_x = v.x
}
if v.x > max_x {
max_x = v.x
}
if v.y < min_y {
min_y = v.y
}
if v.y > max_y {
max_y = v.y
}
if v.z < min_z {
min_z = v.z
}
if v.z > max_z {
max_z = v.z
}
}
}
let bounds = BBox3::new(
Vec3::new(min_x, min_y, min_z),
Vec3::new(max_x, max_y, max_z),
)
{ triangles, bounds, }
}
///|
/// Center the mesh on the print bed: X/Y centered around (0,0), Z grounded at 0.0.
pub fn Mesh3::center_to_bed(self : Mesh3) -> Mesh3 {
let center = self.bounds.center()
let min_z = self.bounds.min.z
let offset_x = -center.x
let offset_y = -center.y
let offset_z = -min_z
let offset = Vec3::new(offset_x, offset_y, offset_z)
let new_tris : Array[Triangle3] = []
for tri in self.triangles {
new_tris.push({
normal: tri.normal,
v1: tri.v1.add(offset),
v2: tri.v2.add(offset),
v3: tri.v3.add(offset),
})
}
Mesh3::from_triangles(new_tris)
}
///|
/// Parse an STL file (binary or ASCII).
pub fn parse_stl(data : Bytes) -> Result[Mesh3, SlicerError] {
if data.length() < 84 {
return Err(InvalidStl("STL file is too short to be valid"))
}
// Check if Binary STL: Header (80 bytes) + uint32 triangle count (4 bytes)
let num_triangles = read_u32_le(data, 80)
let expected_binary_size = 84 + num_triangles * 50
if expected_binary_size == data.length() && num_triangles > 0 {
// Definite Binary STL
let triangles : Array[Triangle3] = []
let mut offset = 84
for _i = 0; _i < num_triangles; _i = _i + 1 {
let nx = read_f32_le(data, offset)
let ny = read_f32_le(data, offset + 4)
let nz = read_f32_le(data, offset + 8)
let normal = Vec3::new(nx, ny, nz)
let v1x = read_f32_le(data, offset + 12)
let v1y = read_f32_le(data, offset + 16)
let v1z = read_f32_le(data, offset + 20)
let v1 = Vec3::new(v1x, v1y, v1z)
let v2x = read_f32_le(data, offset + 24)
let v2y = read_f32_le(data, offset + 28)
let v2z = read_f32_le(data, offset + 32)
let v2 = Vec3::new(v2x, v2y, v2z)
let v3x = read_f32_le(data, offset + 36)
let v3y = read_f32_le(data, offset + 40)
let v3z = read_f32_le(data, offset + 44)
let v3 = Vec3::new(v3x, v3y, v3z)
triangles.push({ normal, v1, v2, v3, })
offset += 50
}
Ok(Mesh3::from_triangles(triangles))
} else {
// Attempt ASCII STL parsing
parse_ascii_stl(data)
}
}
///|
/// Parse an ASCII STL file.
fn parse_ascii_stl(data : Bytes) -> Result[Mesh3, SlicerError] {
// Convert bytes to string
let sb = StringBuilder()
for b in data {
sb.write_char(b.to_int().unsafe_to_char())
}
let text = sb.to_string()
let lines = text.split("\n")
let triangles : Array[Triangle3] = []
let mut normal = Vec3::zero()
let vertices : Array[Vec3] = []
for line_view in lines {
let line = line_view.trim().to_owned()
if line.has_prefix("facet normal") {
let parts = line.split(" ")
let nums : Array[Double] = []
for p in parts {
let s = p.trim().to_owned()
if !s.is_empty() && s != "facet" && s != "normal" {
match parse_double(s) {
Some(v) => nums.push(v)
None => ()
}
}
}
if nums.length() >= 3 {
normal = Vec3::new(nums[0], nums[1], nums[2])
}
} else if line.has_prefix("vertex") {
let parts = line.split(" ")
let nums : Array[Double] = []
for p in parts {
let s = p.trim().to_owned()
if !s.is_empty() && s != "vertex" {
match parse_double(s) {
Some(v) => nums.push(v)
None => ()
}
}
}
if nums.length() >= 3 {
vertices.push(Vec3::new(nums[0], nums[1], nums[2]))
}
} else if line.has_prefix("endfacet") {
if vertices.length() >= 3 {
triangles.push({
normal,
v1: vertices[0],
v2: vertices[1],
v3: vertices[2],
})
}
vertices.clear()
}
}
if triangles.is_empty() {
Err(InvalidStl("No valid facets found in ASCII STL"))
} else {
Ok(Mesh3::from_triangles(triangles))
}
}
///|
/// Helper to parse floating point string like "12.34", "-0.5", "1e-3".
fn parse_double(s : String) -> Double? {
if s.is_empty() {
return None
}
let mut sign = 1.0
let mut idx = 0
let chars : Array[Char] = []
for c in s {
chars.push(c)
}
if chars[0] == '-' {
sign = -1.0
idx += 1
} else if chars[0] == '+' {
idx += 1
}
let mut int_part = 0.0
let mut frac_part = 0.0
let mut frac_divisor = 1.0
let mut in_frac = false
let mut in_exp = false
let mut exp_sign = 1.0
let mut exp_val = 0
while idx < chars.length() {
let c = chars[idx]
if c == '.' {
in_frac = true
} else if c == 'e' || c == 'E' {
in_exp = true
idx += 1
if idx < chars.length() {
if chars[idx] == '-' {
exp_sign = -1.0
idx += 1
} else if chars[idx] == '+' {
idx += 1
}
}
continue
} else if in_exp {
let d = c.to_int() - '0'.to_int()
if d >= 0 && d <= 9 {
exp_val = exp_val * 10 + d
}
} else if in_frac {
let d = c.to_int() - '0'.to_int()
if d >= 0 && d <= 9 {
frac_divisor = frac_divisor * 10.0
frac_part = frac_part + d.to_double() / frac_divisor
}
} else {
let d = c.to_int() - '0'.to_int()
if d >= 0 && d <= 9 {
int_part = int_part * 10.0 + d.to_double()
}
}
idx += 1
}
let base = sign * (int_part + frac_part)
if in_exp {
let mut mult = 1.0
for _i = 0; _i < exp_val; _i = _i + 1 {
if exp_sign > 0.0 {
mult = mult * 10.0
} else {
mult = mult / 10.0
}
}
Some(base * mult)
} else {
Some(base)
}
}