forked from 1b-agent/opencadstudio-upstream
Move the 30 flat modules under src/scene/ into six folders by role: convert (entity→geometry), text (fonts/shaping), model (render models), pick (hit-test/selection/grips/xclip), view (camera/transform/render), cache (block + property caches). pipeline/ is unchanged. Full path migration — all `scene::X` references become `scene::<group>::X`, no compatibility re-exports. render.rs items that were `pub(super)` (parent was scene) become `pub(in crate::scene)` to keep their original scene-wide visibility. No behavior change; build and tests are green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
4.3 KiB
Rust
112 lines
4.3 KiB
Rust
// Wavefront OBJ mesh importer.
|
|
//
|
|
// Parses vertex positions, optional normals, and triangle/quad faces.
|
|
// Quads are split into two triangles.
|
|
// Only the first object/group is imported (no multi-object support needed).
|
|
|
|
use crate::scene::model::mesh_model::MeshModel;
|
|
|
|
/// Parse OBJ text into a MeshModel.
|
|
/// Returns `None` if the file has no usable geometry.
|
|
pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
|
let mut positions: Vec<[f32; 3]> = Vec::new();
|
|
let mut normals_raw: Vec<[f32; 3]> = Vec::new();
|
|
// Each face vertex: (pos_idx, normal_idx_opt)
|
|
let mut face_verts: Vec<(usize, Option<usize>)> = Vec::new();
|
|
|
|
for line in src.lines() {
|
|
let line = line.trim();
|
|
if line.starts_with('#') || line.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let mut parts = line.split_whitespace();
|
|
let keyword = parts.next().unwrap_or("");
|
|
|
|
match keyword {
|
|
"v" => {
|
|
let vals: Vec<f32> = parts.filter_map(|s| s.parse().ok()).collect();
|
|
if vals.len() >= 3 {
|
|
// OBJ is Y-up right-handed; OpenCADStudio's world is Z-up.
|
|
// Rotate +90° about X so OBJ Y→world Z (up): (x, y, z) → (x, -z, y).
|
|
positions.push([vals[0], -vals[2], vals[1]]);
|
|
}
|
|
}
|
|
"vn" => {
|
|
let vals: Vec<f32> = parts.filter_map(|s| s.parse().ok()).collect();
|
|
if vals.len() >= 3 {
|
|
normals_raw.push([vals[0], -vals[2], vals[1]]);
|
|
}
|
|
}
|
|
"f" => {
|
|
// Collect vertex descriptors "v", "v/vt", "v/vt/vn", "v//vn"
|
|
let descs: Vec<(usize, Option<usize>)> = parts
|
|
.filter_map(|token| {
|
|
let mut it = token.split('/');
|
|
let pos_i: usize = it.next()?.parse::<i32>().ok()
|
|
.map(|i| if i < 0 { positions.len() as i32 + i } else { i - 1 })? as usize;
|
|
it.next(); // skip vt
|
|
let norm_i = it.next().and_then(|s| s.parse::<i32>().ok())
|
|
.map(|i| if i < 0 { normals_raw.len() as i32 + i } else { i - 1 } as usize);
|
|
Some((pos_i, norm_i))
|
|
})
|
|
.collect();
|
|
// Fan-triangulate: (0,1,2), (0,2,3), …
|
|
for k in 1..(descs.len() as isize - 1) {
|
|
face_verts.push(descs[0]);
|
|
face_verts.push(descs[k as usize]);
|
|
face_verts.push(descs[k as usize + 1]);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
if positions.is_empty() || face_verts.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Build flat (un-indexed) vertex + normal arrays.
|
|
let mut verts: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
|
let mut norms: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
|
let mut indices: Vec<u32> = Vec::with_capacity(face_verts.len());
|
|
|
|
for (vi, (pos_i, norm_i)) in face_verts.iter().enumerate() {
|
|
let pos = *positions.get(*pos_i).unwrap_or(&[0.0; 3]);
|
|
verts.push(pos);
|
|
let norm = norm_i
|
|
.and_then(|ni| normals_raw.get(ni).copied())
|
|
.unwrap_or([0.0, 0.0, 0.0]);
|
|
norms.push(norm);
|
|
indices.push(vi as u32);
|
|
}
|
|
|
|
// If no normals were provided in the OBJ file, compute face normals.
|
|
if normals_raw.is_empty() {
|
|
for tri in indices.chunks_exact(3) {
|
|
let a = verts[tri[0] as usize];
|
|
let b = verts[tri[1] as usize];
|
|
let c = verts[tri[2] as usize];
|
|
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
let nx = ab[1] * ac[2] - ab[2] * ac[1];
|
|
let ny = ab[2] * ac[0] - ab[0] * ac[2];
|
|
let nz = ab[0] * ac[1] - ab[1] * ac[0];
|
|
let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-12);
|
|
let n = [nx / len, ny / len, nz / len];
|
|
norms[tri[0] as usize] = n;
|
|
norms[tri[1] as usize] = n;
|
|
norms[tri[2] as usize] = n;
|
|
}
|
|
}
|
|
|
|
Some(MeshModel {
|
|
name: String::new(),
|
|
verts,
|
|
normals: norms,
|
|
indices,
|
|
color,
|
|
selected: false,
|
|
})
|
|
}
|