use std::collections::HashMap;
use oxideav_mesh3d::{Error, Indices, Mesh, Primitive, Result, Scene3D, Topology};
use crate::mtl::parse_mtl;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
struct FaceVert {
v: u32,
vt: u32,
vn: u32,
}
#[derive(Debug)]
enum Element {
Face(Vec<FaceVert>),
Line(Vec<FaceVert>),
Point(Vec<FaceVert>),
}
#[derive(Debug, Default)]
struct PrimAccum {
elements: Vec<Element>,
material: Option<String>,
smoothing_group: Option<String>,
groups: Vec<String>,
merging_group: Option<String>,
bevel: Option<String>,
c_interp: Option<String>,
d_interp: Option<String>,
lod: Option<String>,
usemap: Option<String>,
}
#[derive(Debug, Default)]
struct MeshAccum {
name: Option<String>,
primitives: Vec<PrimAccum>,
}
impl MeshAccum {
fn current_or_new(&mut self) -> &mut PrimAccum {
if self.primitives.is_empty() {
self.primitives.push(PrimAccum::default());
}
self.primitives.last_mut().unwrap()
}
}
#[derive(Debug, Default)]
struct ObjDoc {
positions: Vec<[f32; 3]>,
position_weights: Vec<Option<f32>>,
position_colors: Vec<Option<[f32; 4]>>,
texcoords: Vec<[f32; 2]>,
normals: Vec<[f32; 3]>,
vp: Vec<[f32; 3]>,
mtllibs: Vec<String>,
maplibs: Vec<String>,
resolved_materials: HashMap<String, oxideav_mesh3d::Material>,
meshes: Vec<MeshAccum>,
freeform_directives: Vec<Vec<String>>,
shadow_obj: Option<String>,
trace_obj: Option<String>,
general_directives: Vec<Vec<String>>,
}
fn preprocess_lines(text: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut acc = String::new();
for raw_line in text.split('\n') {
let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);
let no_comment = match line.find('#') {
Some(idx) => &line[..idx],
None => line,
};
let trimmed = no_comment.trim_end();
if let Some(stripped) = trimmed.strip_suffix('\\') {
acc.push_str(stripped);
acc.push(' ');
} else {
acc.push_str(trimmed);
out.push(std::mem::take(&mut acc));
}
}
if !acc.is_empty() {
out.push(acc);
}
out
}
fn parse_face_vertex(tok: &str, n_pos: i64, n_tex: i64, n_norm: i64) -> Result<FaceVert> {
let mut parts = tok.split('/');
let v = parts
.next()
.ok_or_else(|| Error::invalid(format!("face vertex missing position: {tok:?}")))?;
if v.is_empty() {
return Err(Error::invalid(format!(
"face vertex missing position index: {tok:?}"
)));
}
let vt = parts.next().unwrap_or("");
let vn = parts.next().unwrap_or("");
let resolve = |s: &str, n: i64, kind: &str| -> Result<u32> {
if s.is_empty() {
return Ok(0);
}
let raw: i64 = s.parse().map_err(|_| {
Error::invalid(format!(
"invalid {kind} index in face vertex {tok:?}: {s:?}"
))
})?;
let resolved = if raw < 0 { n + 1 + raw } else { raw };
if resolved <= 0 || resolved > n {
return Err(Error::invalid(format!(
"{kind} index out of range in face vertex {tok:?}: {raw} (have {n})"
)));
}
Ok(resolved as u32)
};
Ok(FaceVert {
v: resolve(v, n_pos, "position")?,
vt: resolve(vt, n_tex, "texcoord")?,
vn: resolve(vn, n_norm, "normal")?,
})
}
fn parse_obj_doc(text: &str) -> Result<ObjDoc> {
let mut doc = ObjDoc::default();
doc.meshes.push(MeshAccum::default());
let lines = preprocess_lines(text);
for line in &lines {
let mut tokens = line.split_whitespace();
let Some(keyword) = tokens.next() else {
continue;
};
match keyword {
"v" => {
let coords: Vec<f32> = tokens
.map(str::parse)
.collect::<std::result::Result<Vec<f32>, _>>()
.map_err(|e| Error::invalid(format!("v: bad float ({e})")))?;
let (w, rgb) = match coords.len() {
3 => (None, None),
4 => (Some(coords[3]), None),
6 => (None, Some([coords[3], coords[4], coords[5], 1.0])),
7 => (
Some(coords[3]),
Some([coords[4], coords[5], coords[6], 1.0]),
),
n => {
return Err(Error::invalid(format!(
"v: expected 3, 4, 6, or 7 floats (xyz, xyzw, xyzrgb, or \
xyzwrgb per spec + MeshLab vertex-colour extension), got {n}"
)));
}
};
doc.positions.push([coords[0], coords[1], coords[2]]);
doc.position_weights.push(w);
doc.position_colors.push(rgb);
}
"vt" => {
let coords: Vec<f32> = tokens
.map(str::parse)
.collect::<std::result::Result<Vec<f32>, _>>()
.map_err(|e| Error::invalid(format!("vt: bad float ({e})")))?;
if coords.is_empty() {
return Err(Error::invalid("vt: expected ≥1 coord"));
}
let u = coords[0];
let v = coords.get(1).copied().unwrap_or(0.0);
doc.texcoords.push([u, v]);
}
"vn" => {
let coords: Vec<f32> = tokens
.map(str::parse)
.collect::<std::result::Result<Vec<f32>, _>>()
.map_err(|e| Error::invalid(format!("vn: bad float ({e})")))?;
if coords.len() != 3 {
return Err(Error::invalid(format!(
"vn: expected 3 coords, got {}",
coords.len()
)));
}
doc.normals.push([coords[0], coords[1], coords[2]]);
}
"vp" => {
let coords: Vec<f32> = tokens
.map(str::parse)
.collect::<std::result::Result<Vec<f32>, _>>()
.map_err(|e| Error::invalid(format!("vp: bad float ({e})")))?;
if coords.is_empty() {
return Err(Error::invalid("vp: expected ≥1 coord"));
}
let u = coords[0];
let v = coords.get(1).copied().unwrap_or(0.0);
let w = coords.get(2).copied().unwrap_or(0.0);
doc.vp.push([u, v, w]);
}
"cstype" | "deg" | "curv" | "curv2" | "surf" | "parm" | "trim" | "hole" | "scrv"
| "sp" | "end" | "bzp" | "bsp" | "cdc" | "cdp" | "res" | "bmat" | "step" | "ctech"
| "stech" | "con" => {
let mut entry: Vec<String> = Vec::new();
entry.push(keyword.to_string());
for tok in tokens {
entry.push(tok.to_string());
}
doc.freeform_directives.push(entry);
}
"shadow_obj" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if !v.is_empty() {
doc.shadow_obj = Some(v);
}
}
"trace_obj" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if !v.is_empty() {
doc.trace_obj = Some(v);
}
}
"call" | "csh" => {
let mut entry: Vec<String> = Vec::new();
entry.push(keyword.to_string());
for tok in tokens {
entry.push(tok.to_string());
}
doc.general_directives.push(entry);
}
"f" => {
let n_pos = doc.positions.len() as i64;
let n_tex = doc.texcoords.len() as i64;
let n_norm = doc.normals.len() as i64;
let verts: Vec<FaceVert> = tokens
.map(|t| parse_face_vertex(t, n_pos, n_tex, n_norm))
.collect::<Result<Vec<_>>>()?;
if verts.len() < 3 {
return Err(Error::invalid(format!(
"f: face needs ≥3 vertices, got {}",
verts.len()
)));
}
let mesh = doc.meshes.last_mut().unwrap();
mesh.current_or_new().elements.push(Element::Face(verts));
}
"l" => {
let n_pos = doc.positions.len() as i64;
let n_tex = doc.texcoords.len() as i64;
let n_norm = doc.normals.len() as i64;
let verts: Vec<FaceVert> = tokens
.map(|t| parse_face_vertex(t, n_pos, n_tex, n_norm))
.collect::<Result<Vec<_>>>()?;
if verts.len() < 2 {
return Err(Error::invalid(format!(
"l: line needs ≥2 vertices, got {}",
verts.len()
)));
}
let mesh = doc.meshes.last_mut().unwrap();
mesh.current_or_new().elements.push(Element::Line(verts));
}
"p" => {
let n_pos = doc.positions.len() as i64;
let n_tex = doc.texcoords.len() as i64;
let n_norm = doc.normals.len() as i64;
let verts: Vec<FaceVert> = tokens
.map(|t| parse_face_vertex(t, n_pos, n_tex, n_norm))
.collect::<Result<Vec<_>>>()?;
if verts.is_empty() {
return Err(Error::invalid("p: needs ≥1 vertex"));
}
let mesh = doc.meshes.last_mut().unwrap();
let prim = mesh.current_or_new();
if prim
.elements
.iter()
.any(|e| !matches!(e, Element::Point(_)))
{
let mat = prim.material.clone();
let groups = prim.groups.clone();
let smoothing = prim.smoothing_group.clone();
let merging = prim.merging_group.clone();
let bevel = prim.bevel.clone();
let c_interp = prim.c_interp.clone();
let d_interp = prim.d_interp.clone();
let lod = prim.lod.clone();
let usemap = prim.usemap.clone();
mesh.primitives.push(PrimAccum {
material: mat,
groups,
smoothing_group: smoothing,
merging_group: merging,
bevel,
c_interp,
d_interp,
lod,
usemap,
elements: vec![Element::Point(verts)],
});
} else {
prim.elements.push(Element::Point(verts));
}
}
"bevel" | "c_interp" | "d_interp" | "lod" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if v.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
let current: Option<&str> = match keyword {
"bevel" => last.bevel.as_deref(),
"c_interp" => last.c_interp.as_deref(),
"d_interp" => last.d_interp.as_deref(),
"lod" => last.lod.as_deref(),
_ => unreachable!(),
};
if last.elements.is_empty() {
match keyword {
"bevel" => last.bevel = Some(v),
"c_interp" => last.c_interp = Some(v),
"d_interp" => last.d_interp = Some(v),
"lod" => last.lod = Some(v),
_ => unreachable!(),
}
} else if current != Some(v.as_str()) {
let mat = last.material.clone();
let groups = last.groups.clone();
let smoothing = last.smoothing_group.clone();
let merging = last.merging_group.clone();
let mut bevel = last.bevel.clone();
let mut c_interp = last.c_interp.clone();
let mut d_interp = last.d_interp.clone();
let mut lod = last.lod.clone();
let usemap = last.usemap.clone();
match keyword {
"bevel" => bevel = Some(v),
"c_interp" => c_interp = Some(v),
"d_interp" => d_interp = Some(v),
"lod" => lod = Some(v),
_ => unreachable!(),
}
mesh.primitives.push(PrimAccum {
material: mat,
smoothing_group: smoothing,
merging_group: merging,
groups,
bevel,
c_interp,
d_interp,
lod,
usemap,
elements: Vec::new(),
});
}
}
"mg" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if v.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
if last.elements.is_empty() {
last.merging_group = Some(v);
} else if last.merging_group.as_deref() != Some(v.as_str()) {
let mat = last.material.clone();
let groups = last.groups.clone();
let smoothing = last.smoothing_group.clone();
let bevel = last.bevel.clone();
let c_interp = last.c_interp.clone();
let d_interp = last.d_interp.clone();
let lod = last.lod.clone();
let usemap = last.usemap.clone();
mesh.primitives.push(PrimAccum {
material: mat,
smoothing_group: smoothing,
groups,
merging_group: Some(v),
bevel,
c_interp,
d_interp,
lod,
usemap,
elements: Vec::new(),
});
}
}
"o" => {
let name: String = tokens.collect::<Vec<_>>().join(" ");
let last = doc.meshes.last_mut().unwrap();
if last.name.is_none() && last.primitives.is_empty() {
last.name = if name.is_empty() { None } else { Some(name) };
} else {
doc.meshes.push(MeshAccum {
name: if name.is_empty() { None } else { Some(name) },
primitives: Vec::new(),
});
}
}
"g" => {
let names: Vec<String> = tokens.map(|t| t.to_string()).collect();
if names.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let prim = mesh.current_or_new();
for name in names {
if !prim.groups.iter().any(|g| g == &name) {
prim.groups.push(name);
}
}
}
"s" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if v.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
if last.elements.is_empty() {
last.smoothing_group = Some(v);
} else if last.smoothing_group.as_deref() != Some(v.as_str()) {
let mat = last.material.clone();
let groups = last.groups.clone();
let merging = last.merging_group.clone();
let bevel = last.bevel.clone();
let c_interp = last.c_interp.clone();
let d_interp = last.d_interp.clone();
let lod = last.lod.clone();
let usemap = last.usemap.clone();
mesh.primitives.push(PrimAccum {
material: mat,
smoothing_group: Some(v),
groups,
merging_group: merging,
bevel,
c_interp,
d_interp,
lod,
usemap,
elements: Vec::new(),
});
}
}
"usemtl" => {
let name: String = tokens.collect::<Vec<_>>().join(" ");
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
if last.elements.is_empty() && last.material.is_none() {
last.material = if name.is_empty() { None } else { Some(name) };
} else {
let groups = last.groups.clone();
let smoothing = last.smoothing_group.clone();
let merging = last.merging_group.clone();
let bevel = last.bevel.clone();
let c_interp = last.c_interp.clone();
let d_interp = last.d_interp.clone();
let lod = last.lod.clone();
let usemap = last.usemap.clone();
mesh.primitives.push(PrimAccum {
material: if name.is_empty() { None } else { Some(name) },
groups,
smoothing_group: smoothing,
merging_group: merging,
bevel,
c_interp,
d_interp,
lod,
usemap,
elements: Vec::new(),
});
}
}
"usemap" => {
let v: String = tokens.collect::<Vec<_>>().join(" ");
if v.is_empty() {
continue;
}
let mesh = doc.meshes.last_mut().unwrap();
let last = mesh.current_or_new();
if last.elements.is_empty() {
last.usemap = Some(v);
} else if last.usemap.as_deref() != Some(v.as_str()) {
let mat = last.material.clone();
let groups = last.groups.clone();
let smoothing = last.smoothing_group.clone();
let merging = last.merging_group.clone();
let bevel = last.bevel.clone();
let c_interp = last.c_interp.clone();
let d_interp = last.d_interp.clone();
let lod = last.lod.clone();
mesh.primitives.push(PrimAccum {
material: mat,
smoothing_group: smoothing,
merging_group: merging,
groups,
bevel,
c_interp,
d_interp,
lod,
usemap: Some(v),
elements: Vec::new(),
});
}
}
"mtllib" => {
for tok in tokens {
if !doc.mtllibs.iter().any(|m| m == tok) {
doc.mtllibs.push(tok.to_string());
}
}
}
"maplib" => {
for tok in tokens {
if !doc.maplibs.iter().any(|m| m == tok) {
doc.maplibs.push(tok.to_string());
}
}
}
_ => {}
}
}
Ok(doc)
}
fn build_scene(doc: ObjDoc) -> Result<Scene3D> {
use oxideav_mesh3d::{Axis, Material, Unit};
let mut scene = Scene3D::new();
scene.up_axis = Axis::PosY;
scene.unit = Unit::Metres;
let sp_typed = if !doc.freeform_directives.is_empty() {
let (typed, _) = collect_special_points(&doc);
typed
} else {
Vec::new()
};
let con_typed = if !doc.freeform_directives.is_empty() {
collect_connectivity(&doc)
} else {
Vec::new()
};
let parms_typed = if !doc.freeform_directives.is_empty() {
collect_parms(&doc)
} else {
Vec::new()
};
let approximations_typed = if !doc.freeform_directives.is_empty() {
collect_approximation_techniques(&doc)
} else {
Vec::new()
};
let trim_loops_typed = if !doc.freeform_directives.is_empty() {
collect_trim_loops(&doc)
} else {
Vec::new()
};
let mut material_ids: HashMap<String, oxideav_mesh3d::MaterialId> = HashMap::new();
let mut material_names: Vec<String> = doc.resolved_materials.keys().cloned().collect();
material_names.sort();
for name in &material_names {
let mut mat = doc
.resolved_materials
.get(name)
.cloned()
.unwrap_or_else(Material::new);
if mat.name.is_none() {
mat.name = Some(name.clone());
}
let id = scene.add_material(mat);
material_ids.insert(name.clone(), id);
}
for mesh_acc in doc.meshes {
let has_anything = mesh_acc.primitives.iter().any(|p| !p.elements.is_empty());
if !has_anything {
continue;
}
let mut mesh = Mesh::new(mesh_acc.name.clone());
for prim_acc in mesh_acc.primitives {
let (mut primitive, arities) = build_primitive(
&prim_acc,
&doc.positions,
&doc.position_weights,
&doc.position_colors,
&doc.texcoords,
&doc.normals,
&material_ids,
)?;
if primitive.positions.is_empty() {
continue;
}
if arities.iter().any(|&a| a != 3) {
primitive.extras.insert(
"obj:original_face_arities".to_string(),
serde_json::to_value(&arities).unwrap(),
);
}
mesh.primitives.push(primitive);
}
scene.add_mesh(mesh);
}
if !doc.mtllibs.is_empty() {
scene.extras.insert(
"obj:mtllibs".to_string(),
serde_json::to_value(&doc.mtllibs).unwrap(),
);
}
if !doc.maplibs.is_empty() {
scene.extras.insert(
"obj:maplibs".to_string(),
serde_json::to_value(&doc.maplibs).unwrap(),
);
}
if !doc.positions.is_empty()
&& (doc.freeform_directives.iter().any(|d| {
matches!(
d.first().map(String::as_str),
Some("curv" | "curv2" | "surf" | "bzp" | "bsp" | "cdc" | "cdp")
)
}))
{
scene.extras.insert(
"obj:positions".to_string(),
serde_json::to_value(&doc.positions).unwrap(),
);
if doc.position_weights.iter().any(Option::is_some) {
scene.extras.insert(
"obj:position_weights".to_string(),
serde_json::to_value(&doc.position_weights).unwrap(),
);
}
if doc.position_colors.iter().any(Option::is_some) {
scene.extras.insert(
"obj:position_colors".to_string(),
serde_json::to_value(&doc.position_colors).unwrap(),
);
}
}
if !doc.vp.is_empty() {
scene
.extras
.insert("obj:vp".to_string(), serde_json::to_value(&doc.vp).unwrap());
}
if !doc.freeform_directives.is_empty() {
scene.extras.insert(
"obj:freeform_directives".to_string(),
serde_json::to_value(&doc.freeform_directives).unwrap(),
);
}
if !sp_typed.is_empty() {
scene.extras.insert(
"obj:special_points".to_string(),
serde_json::Value::Array(sp_typed),
);
}
if !con_typed.is_empty() {
scene.extras.insert(
"obj:connectivity".to_string(),
serde_json::Value::Array(con_typed),
);
}
if !parms_typed.is_empty() {
scene.extras.insert(
"obj:parms".to_string(),
serde_json::Value::Array(parms_typed),
);
}
if !approximations_typed.is_empty() {
scene.extras.insert(
"obj:approximations".to_string(),
serde_json::Value::Array(approximations_typed),
);
}
if !trim_loops_typed.is_empty() {
scene.extras.insert(
"obj:trim_loops".to_string(),
serde_json::Value::Array(trim_loops_typed),
);
}
if let Some(name) = &doc.shadow_obj {
scene.extras.insert(
"obj:shadow_obj".to_string(),
serde_json::Value::String(name.clone()),
);
}
if let Some(name) = &doc.trace_obj {
scene.extras.insert(
"obj:trace_obj".to_string(),
serde_json::Value::String(name.clone()),
);
}
if !doc.general_directives.is_empty() {
scene.extras.insert(
"obj:general_directives".to_string(),
serde_json::to_value(&doc.general_directives).unwrap(),
);
}
Ok(scene)
}
fn tessellate_curves(doc: &ObjDoc, samples: u32) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
let mut active_kind: Option<&'static str> = None;
let mut active_degree: Option<u32> = None;
let mut parm_u: Vec<f32> = Vec::new();
let mut bmat_u: Vec<f32> = Vec::new();
let mut step_u: Option<u32> = None;
let mut pending_curves: Vec<&Vec<String>> = Vec::new();
for entry in &doc.freeform_directives {
if entry.is_empty() {
continue;
}
match entry[0].as_str() {
"cstype" => {
flush_block(
&mut out,
doc,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending_curves,
samples,
);
pending_curves.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_degree = None;
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_kind = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
}
"deg" => {
if let Some(d) = entry.get(1).and_then(|t| t.parse::<u32>().ok()) {
active_degree = Some(d);
}
}
"parm" if entry.get(1).map(String::as_str) == Some("u") => {
parm_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("u") => {
bmat_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"step" => {
step_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
}
"curv" => {
pending_curves.push(entry);
}
"end" => {
flush_block(
&mut out,
doc,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending_curves,
samples,
);
pending_curves.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_kind = None;
active_degree = None;
}
_ => {}
}
}
flush_block(
&mut out,
doc,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending_curves,
samples,
);
out
}
#[allow(clippy::too_many_arguments)]
fn flush_block(
out: &mut Vec<Primitive>,
doc: &ObjDoc,
active_kind: Option<&'static str>,
active_degree: Option<u32>,
parm_u: &[f32],
bmat_u: &[f32],
step_u: Option<u32>,
pending_curves: &[&Vec<String>],
samples: u32,
) {
let Some(kind) = active_kind else {
return;
};
for entry in pending_curves {
if entry.len() < 5 {
continue;
}
let Ok(u_min) = entry[1].parse::<f32>() else {
continue;
};
let Ok(u_max) = entry[2].parse::<f32>() else {
continue;
};
let n_pos = doc.positions.len() as i64;
let mut control_points: Vec<[f32; 3]> = Vec::new();
let mut control_weights: Vec<f32> = Vec::new();
let mut bad = false;
for tok in &entry[3..] {
let Ok(raw) = tok.parse::<i64>() else {
bad = true;
break;
};
let resolved = if raw < 0 { n_pos + 1 + raw } else { raw };
if resolved <= 0 || resolved > n_pos {
bad = true;
break;
}
let pos = doc.positions[(resolved as usize) - 1];
control_points.push(pos);
let w = doc.position_weights[(resolved as usize) - 1].unwrap_or(1.0);
control_weights.push(w);
}
if bad || control_points.len() < 2 {
continue;
}
let curve_points = match kind {
"bezier" | "rat_bezier" => sample_bezier(
&control_points,
&control_weights,
kind,
u_min,
u_max,
samples,
),
"bspline" | "rat_bspline" => {
let Some(degree) = active_degree else {
continue;
};
if parm_u.len() != control_points.len() + degree as usize + 1 {
continue;
}
sample_bspline(
&control_points,
&control_weights,
kind,
degree,
parm_u,
u_min,
u_max,
samples,
)
}
"cardinal" => {
if active_degree.is_some_and(|d| d != 3) {
continue;
}
if control_points.len() < 4 {
continue;
}
sample_cardinal(&control_points, samples)
}
"taylor" => {
let degree = match active_degree {
Some(d) => d as usize,
None => control_points.len().saturating_sub(1),
};
if control_points.len() != degree + 1 {
continue;
}
sample_taylor(&control_points, u_min, u_max, samples)
}
"bmatrix" => {
let Some(degree) = active_degree else {
continue;
};
let Some(step) = step_u else {
continue;
};
let Some(n_plus_1) = (degree as usize).checked_add(1) else {
continue;
};
let Some(expected_bmat) = n_plus_1.checked_mul(n_plus_1) else {
continue;
};
if bmat_u.len() != expected_bmat {
continue;
}
if step == 0 {
continue;
}
if control_points.len() < n_plus_1 {
continue;
}
sample_bmatrix(&control_points, bmat_u, degree, step, samples)
}
_ => continue,
};
if curve_points.len() < 2 {
continue;
}
let mut prim = Primitive::new(Topology::LineStrip);
let n = curve_points.len() as u32;
prim.positions = curve_points;
if n > u16::MAX as u32 {
prim.indices = Some(Indices::U32((0..n).collect()));
} else {
prim.indices = Some(Indices::U16((0..n).map(|i| i as u16).collect()));
}
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:curve_kind".to_string(),
serde_json::Value::String(kind.to_string()),
);
let reported_degree = match kind {
"bezier" | "rat_bezier" => (control_points.len() - 1) as u64,
"bspline" | "rat_bspline" => active_degree.unwrap_or(0) as u64,
"cardinal" => 3,
"taylor" => active_degree
.map(u64::from)
.unwrap_or_else(|| (control_points.len() - 1) as u64),
"bmatrix" => active_degree.map(u64::from).unwrap_or(0),
_ => 0,
};
prim.extras.insert(
"obj:curve_degree".to_string(),
serde_json::Value::Number(serde_json::Number::from(reported_degree)),
);
let range_arr = serde_json::Value::Array(vec![
serde_json::Value::from(u_min as f64),
serde_json::Value::from(u_max as f64),
]);
prim.extras
.insert("obj:curve_u_range".to_string(), range_arr);
prim.extras.insert(
"obj:curve_samples".to_string(),
serde_json::Value::Number(serde_json::Number::from(samples as u64)),
);
out.push(prim);
}
}
fn tessellate_curve2(doc: &ObjDoc, samples: u32) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
let mut active_kind: Option<&'static str> = None;
let mut active_degree: Option<u32> = None;
let mut parm_u: Vec<f32> = Vec::new();
let mut bmat_u: Vec<f32> = Vec::new();
let mut step_u: Option<u32> = None;
let mut pending: Vec<&Vec<String>> = Vec::new();
let flush = |out: &mut Vec<Primitive>,
active_kind: Option<&'static str>,
active_degree: Option<u32>,
parm_u: &[f32],
bmat_u: &[f32],
step_u: Option<u32>,
pending: &[&Vec<String>]| {
flush_curve2_block(
out,
doc,
active_kind,
active_degree,
parm_u,
bmat_u,
step_u,
pending,
samples,
);
};
for entry in &doc.freeform_directives {
if entry.is_empty() {
continue;
}
match entry[0].as_str() {
"cstype" => {
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
pending.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_degree = None;
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_kind = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
}
"deg" => {
if let Some(d) = entry.get(1).and_then(|t| t.parse::<u32>().ok()) {
active_degree = Some(d);
}
}
"parm" if entry.get(1).map(String::as_str) == Some("u") => {
parm_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("u") => {
bmat_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"step" => {
step_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
}
"curv2" => {
pending.push(entry);
}
"end" => {
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
pending.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_kind = None;
active_degree = None;
}
_ => {}
}
}
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
out
}
#[allow(clippy::too_many_arguments)]
fn evaluate_curv2_entry(
kind: &'static str,
active_degree: Option<u32>,
parm_u: &[f32],
bmat_u: &[f32],
step_u: Option<u32>,
control_points: &[[f32; 3]],
control_weights: &[f32],
samples: u32,
) -> Option<(f32, f32, Vec<[f32; 3]>)> {
let (u_min, u_max) = match (parm_u.first(), parm_u.last()) {
(Some(&a), Some(&b)) if parm_u.len() >= 2 => (a, b),
_ => (0.0, 1.0),
};
let curve_points = match kind {
"bezier" | "rat_bezier" => {
sample_bezier(control_points, control_weights, kind, u_min, u_max, samples)
}
"bspline" | "rat_bspline" => {
let degree = active_degree?;
if parm_u.len() != control_points.len() + degree as usize + 1 {
return None;
}
sample_bspline(
control_points,
control_weights,
kind,
degree,
parm_u,
u_min,
u_max,
samples,
)
}
"cardinal" => {
if active_degree.is_some_and(|d| d != 3) {
return None;
}
if control_points.len() < 4 {
return None;
}
sample_cardinal(control_points, samples)
}
"taylor" => {
let degree = match active_degree {
Some(d) => d as usize,
None => control_points.len().saturating_sub(1),
};
if control_points.len() != degree + 1 {
return None;
}
sample_taylor(control_points, u_min, u_max, samples)
}
"bmatrix" => {
let degree = active_degree?;
let step = step_u?;
let n_plus_1 = (degree as usize).checked_add(1)?;
let expected_bmat = n_plus_1.checked_mul(n_plus_1)?;
if bmat_u.len() != expected_bmat {
return None;
}
if step == 0 {
return None;
}
if control_points.len() < n_plus_1 {
return None;
}
sample_bmatrix(control_points, bmat_u, degree, step, samples)
}
_ => return None,
};
Some((u_min, u_max, curve_points))
}
type Curv2Polyline = (f32, f32, Vec<[f32; 2]>);
type Curv2PolylineTable = Vec<Option<Curv2Polyline>>;
fn collect_all_curv2_polylines(doc: &ObjDoc, samples: u32) -> Curv2PolylineTable {
let mut out: Curv2PolylineTable = Vec::new();
if samples == 0 {
return out;
}
let n_vp = doc.vp.len() as i64;
let mut active_kind: Option<&'static str> = None;
let mut active_degree: Option<u32> = None;
let mut parm_u: Vec<f32> = Vec::new();
let mut bmat_u: Vec<f32> = Vec::new();
let mut step_u: Option<u32> = None;
let mut pending: Vec<(usize, &Vec<String>)> = Vec::new();
let flush = |out: &mut Curv2PolylineTable,
active_kind: Option<&'static str>,
active_degree: Option<u32>,
parm_u: &[f32],
bmat_u: &[f32],
step_u: Option<u32>,
pending: &[(usize, &Vec<String>)]| {
for (idx, entry) in pending {
while out.len() <= *idx {
out.push(None);
}
let Some(kind) = active_kind else {
continue;
};
if entry.len() < 3 {
continue;
}
let mut control_points: Vec<[f32; 3]> = Vec::new();
let mut control_weights: Vec<f32> = Vec::new();
let mut bad = false;
for tok in &entry[1..] {
let Ok(raw) = tok.parse::<i64>() else {
bad = true;
break;
};
let resolved = if raw < 0 { n_vp + 1 + raw } else { raw };
if resolved <= 0 || resolved > n_vp {
bad = true;
break;
}
let vp = doc.vp[(resolved as usize) - 1];
control_points.push([vp[0], vp[1], 0.0]);
let w = if vp[2] == 0.0 { 1.0 } else { vp[2] };
control_weights.push(w);
}
if bad || control_points.len() < 2 {
continue;
}
let Some((u_min, u_max, pts)) = evaluate_curv2_entry(
kind,
active_degree,
parm_u,
bmat_u,
step_u,
&control_points,
&control_weights,
samples,
) else {
continue;
};
let polyline: Vec<[f32; 2]> = pts.iter().map(|p| [p[0], p[1]]).collect();
out[*idx] = Some((u_min, u_max, polyline));
}
};
let mut curv2_counter: usize = 0;
for entry in &doc.freeform_directives {
if entry.is_empty() {
continue;
}
match entry[0].as_str() {
"cstype" => {
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
pending.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_degree = None;
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_kind = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
}
"deg" => {
if let Some(d) = entry.get(1).and_then(|t| t.parse::<u32>().ok()) {
active_degree = Some(d);
}
}
"parm" if entry.get(1).map(String::as_str) == Some("u") => {
parm_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("u") => {
bmat_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"step" => {
step_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
}
"curv2" => {
pending.push((curv2_counter, entry));
curv2_counter += 1;
}
"end" => {
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
pending.clear();
parm_u.clear();
bmat_u.clear();
step_u = None;
active_kind = None;
active_degree = None;
}
_ => {}
}
}
flush(
&mut out,
active_kind,
active_degree,
&parm_u,
&bmat_u,
step_u,
&pending,
);
while out.len() < curv2_counter {
out.push(None);
}
out
}
fn append_curv2_segment(
loop_out: &mut Vec<[f32; 2]>,
polyline: &[[f32; 2]],
curve_u_min: f32,
curve_u_max: f32,
u0: f32,
u1: f32,
) {
if polyline.len() < 2 {
return;
}
let n = polyline.len();
let span = curve_u_max - curve_u_min;
let to_idx = |u: f32| -> usize {
if span.abs() < f32::EPSILON {
0
} else {
let t = ((u - curve_u_min) / span).clamp(0.0, 1.0);
(t * (n - 1) as f32).round() as usize
}
};
let i0 = to_idx(u0);
let i1 = to_idx(u1);
let forward = i0 <= i1;
let (lo, hi) = if forward { (i0, i1) } else { (i1, i0) };
if hi <= lo {
return;
}
let segment: Vec<[f32; 2]> = if forward {
polyline[lo..=hi].to_vec()
} else {
polyline[lo..=hi].iter().rev().copied().collect()
};
let start = if loop_out.is_empty() { 0 } else { 1 };
for p in &segment[start..] {
loop_out.push(*p);
}
}
fn point_in_polygon(point: [f32; 2], polygon: &[[f32; 2]]) -> bool {
let n = polygon.len();
if n < 3 {
return false;
}
let [px, py] = point;
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let [xi, yi] = polygon[i];
let [xj, yj] = polygon[j];
let intersect = (yi > py) != (yj > py) && {
let denom = yj - yi;
if denom.abs() < f32::EPSILON {
false
} else {
px < (xj - xi) * (py - yi) / denom + xi
}
};
if intersect {
inside = !inside;
}
j = i;
}
inside
}
#[allow(clippy::too_many_arguments)]
fn flush_curve2_block(
out: &mut Vec<Primitive>,
doc: &ObjDoc,
active_kind: Option<&'static str>,
active_degree: Option<u32>,
parm_u: &[f32],
bmat_u: &[f32],
step_u: Option<u32>,
pending: &[&Vec<String>],
samples: u32,
) {
let Some(kind) = active_kind else {
return;
};
let n_vp = doc.vp.len() as i64;
for entry in pending {
if entry.len() < 3 {
continue;
}
let mut control_points: Vec<[f32; 3]> = Vec::new();
let mut control_weights: Vec<f32> = Vec::new();
let mut bad = false;
for tok in &entry[1..] {
let Ok(raw) = tok.parse::<i64>() else {
bad = true;
break;
};
let resolved = if raw < 0 { n_vp + 1 + raw } else { raw };
if resolved <= 0 || resolved > n_vp {
bad = true;
break;
}
let vp = doc.vp[(resolved as usize) - 1];
control_points.push([vp[0], vp[1], 0.0]);
let w = if vp[2] == 0.0 { 1.0 } else { vp[2] };
control_weights.push(w);
}
if bad || control_points.len() < 2 {
continue;
}
let Some((u_min, u_max, curve_points)) = evaluate_curv2_entry(
kind,
active_degree,
parm_u,
bmat_u,
step_u,
&control_points,
&control_weights,
samples,
) else {
continue;
};
if curve_points.len() < 2 {
continue;
}
let mut prim = Primitive::new(Topology::LineStrip);
let n = curve_points.len() as u32;
prim.positions = curve_points;
if n > u16::MAX as u32 {
prim.indices = Some(Indices::U32((0..n).collect()));
} else {
prim.indices = Some(Indices::U16((0..n).map(|i| i as u16).collect()));
}
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras
.insert("obj:curve2".to_string(), serde_json::Value::Bool(true));
prim.extras.insert(
"obj:curve_kind".to_string(),
serde_json::Value::String(kind.to_string()),
);
let reported_degree = match kind {
"bezier" | "rat_bezier" => (control_points.len() - 1) as u64,
"bspline" | "rat_bspline" => active_degree.unwrap_or(0) as u64,
"cardinal" => 3,
"taylor" => active_degree
.map(u64::from)
.unwrap_or_else(|| (control_points.len() - 1) as u64),
"bmatrix" => active_degree.map(u64::from).unwrap_or(0),
_ => 0,
};
prim.extras.insert(
"obj:curve_degree".to_string(),
serde_json::Value::Number(serde_json::Number::from(reported_degree)),
);
prim.extras.insert(
"obj:curve_u_range".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(u_min as f64),
serde_json::Value::from(u_max as f64),
]),
);
prim.extras.insert(
"obj:curve_samples".to_string(),
serde_json::Value::Number(serde_json::Number::from(samples as u64)),
);
out.push(prim);
}
}
fn tessellate_scrv(doc: &ObjDoc, samples: u32) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
if samples == 0 {
return out;
}
let curv2_polylines = collect_all_curv2_polylines(doc, samples);
for entry in &doc.freeform_directives {
if entry.first().map(String::as_str) != Some("scrv") {
continue;
}
let toks = &entry[1..];
if toks.len() < 3 || toks.len() % 3 != 0 {
continue;
}
let mut polyline: Vec<[f32; 2]> = Vec::new();
let mut refs: Vec<serde_json::Value> = Vec::new();
let mut segments: u32 = 0;
let mut bad = false;
for chunk in toks.chunks(3) {
let Ok(u0) = chunk[0].parse::<f32>() else {
bad = true;
break;
};
let Ok(u1) = chunk[1].parse::<f32>() else {
bad = true;
break;
};
let Ok(idx) = chunk[2].parse::<i64>() else {
bad = true;
break;
};
if idx <= 0 {
continue;
}
let slot = idx as usize - 1;
let Some(entry) = curv2_polylines.get(slot).and_then(|e| e.as_ref()) else {
continue;
};
let (curve_u_min, curve_u_max, segment_polyline) = entry;
append_curv2_segment(
&mut polyline,
segment_polyline,
*curve_u_min,
*curve_u_max,
u0,
u1,
);
refs.push(serde_json::Value::Array(vec![
serde_json::Value::from(idx),
serde_json::Value::from(u0 as f64),
serde_json::Value::from(u1 as f64),
]));
segments += 1;
}
if bad || polyline.len() < 2 {
continue;
}
let positions: Vec<[f32; 3]> = polyline.iter().map(|p| [p[0], p[1], 0.0]).collect();
let n = positions.len() as u32;
let mut prim = Primitive::new(Topology::LineStrip);
prim.positions = positions;
prim.indices = if n > u16::MAX as u32 {
Some(Indices::U32((0..n).collect()))
} else {
Some(Indices::U16((0..n).map(|i| i as u16).collect()))
};
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras
.insert("obj:scrv".to_string(), serde_json::Value::Bool(true));
prim.extras.insert(
"obj:scrv_segments".to_string(),
serde_json::Value::Number(serde_json::Number::from(segments as u64)),
);
prim.extras.insert(
"obj:scrv_curv2_refs".to_string(),
serde_json::Value::Array(refs),
);
out.push(prim);
}
out
}
fn tessellate_connectivity(doc: &ObjDoc, samples: u32) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
if samples == 0 {
return out;
}
let curv2_polylines = collect_all_curv2_polylines(doc, samples);
let resolve_side = |q0: f32, q1: f32, idx: i64| -> Option<Vec<[f32; 2]>> {
if idx <= 0 {
return None;
}
let slot = idx as usize - 1;
let entry = curv2_polylines.get(slot).and_then(|e| e.as_ref())?;
let (curve_u_min, curve_u_max, polyline) = entry;
let mut seam: Vec<[f32; 2]> = Vec::new();
append_curv2_segment(&mut seam, polyline, *curve_u_min, *curve_u_max, q0, q1);
if seam.len() < 2 {
return None;
}
Some(seam)
};
let make_prim = |seam: Vec<[f32; 2]>,
side: u8,
surf: i64,
peer_surf: i64,
curv2d: i64,
q0: f32,
q1: f32| {
let positions: Vec<[f32; 3]> = seam.iter().map(|p| [p[0], p[1], 0.0]).collect();
let n = positions.len() as u32;
let mut prim = Primitive::new(Topology::LineStrip);
prim.positions = positions;
prim.indices = if n > u16::MAX as u32 {
Some(Indices::U32((0..n).collect()))
} else {
Some(Indices::U16((0..n).map(|i| i as u16).collect()))
};
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras
.insert("obj:con".to_string(), serde_json::Value::Bool(true));
prim.extras.insert(
"obj:con_side".to_string(),
serde_json::Value::Number(serde_json::Number::from(side)),
);
prim.extras.insert(
"obj:con_surf".to_string(),
serde_json::Value::Number(serde_json::Number::from(surf)),
);
prim.extras.insert(
"obj:con_peer_surf".to_string(),
serde_json::Value::Number(serde_json::Number::from(peer_surf)),
);
prim.extras.insert(
"obj:con_curv2d".to_string(),
serde_json::Value::Number(serde_json::Number::from(curv2d)),
);
prim.extras
.insert("obj:con_q0".to_string(), serde_json::Value::from(q0 as f64));
prim.extras
.insert("obj:con_q1".to_string(), serde_json::Value::from(q1 as f64));
prim
};
for entry in &doc.freeform_directives {
if entry.first().map(String::as_str) != Some("con") {
continue;
}
if entry.len() != 9 {
continue;
}
let (
Ok(surf_1),
Ok(q0_1),
Ok(q1_1),
Ok(curv2d_1),
Ok(surf_2),
Ok(q0_2),
Ok(q1_2),
Ok(curv2d_2),
) = (
entry[1].parse::<i64>(),
entry[2].parse::<f32>(),
entry[3].parse::<f32>(),
entry[4].parse::<i64>(),
entry[5].parse::<i64>(),
entry[6].parse::<f32>(),
entry[7].parse::<f32>(),
entry[8].parse::<i64>(),
)
else {
continue;
};
if let Some(seam) = resolve_side(q0_1, q1_1, curv2d_1) {
out.push(make_prim(seam, 1, surf_1, surf_2, curv2d_1, q0_1, q1_1));
}
if let Some(seam) = resolve_side(q0_2, q1_2, curv2d_2) {
out.push(make_prim(seam, 2, surf_2, surf_1, curv2d_2, q0_2, q1_2));
}
}
out
}
type SpecialPointPrimData = Vec<(SpecialPointKind, Vec<SpecialPointRef>)>;
type SpecialPointRef = (i64, f32, Option<f32>);
fn collect_special_points(doc: &ObjDoc) -> (Vec<serde_json::Value>, SpecialPointPrimData) {
let mut typed: Vec<serde_json::Value> = Vec::new();
let mut prim_data: SpecialPointPrimData = Vec::new();
let n_vp = doc.vp.len() as i64;
if n_vp == 0 {
return (typed, prim_data);
}
let mut active_kind: Option<SpecialPointKind> = None;
for entry in &doc.freeform_directives {
let Some(keyword) = entry.first().map(String::as_str) else {
continue;
};
match keyword {
"cstype" => {
active_kind = None;
}
"end" => {
active_kind = None;
}
"curv" => {
active_kind = Some(SpecialPointKind::Curv);
}
"curv2" => {
active_kind = Some(SpecialPointKind::Curv2);
}
"surf" => {
active_kind = Some(SpecialPointKind::Surf);
}
"sp" => {
let Some(kind) = active_kind else {
continue;
};
let mut resolved: Vec<SpecialPointRef> = Vec::new();
for tok in &entry[1..] {
let Ok(raw) = tok.parse::<i64>() else {
continue;
};
if raw == 0 {
continue;
}
let normalised = if raw > 0 { raw } else { n_vp + raw + 1 };
if normalised <= 0 || normalised > n_vp {
continue;
}
let slot = (normalised - 1) as usize;
let vp = doc.vp[slot];
let u = vp[0];
let v = match kind {
SpecialPointKind::Curv => None,
SpecialPointKind::Curv2 | SpecialPointKind::Surf => Some(vp[1]),
};
let kind_str = kind.as_str();
let mut obj = serde_json::Map::new();
obj.insert(
"element_kind".to_string(),
serde_json::Value::String(kind_str.to_string()),
);
obj.insert(
"vp_index_1based".to_string(),
serde_json::Value::Number(serde_json::Number::from(normalised)),
);
obj.insert("u".to_string(), serde_json::Value::from(u as f64));
obj.insert(
"v".to_string(),
match v {
Some(value) => serde_json::Value::from(value as f64),
None => serde_json::Value::Null,
},
);
typed.push(serde_json::Value::Object(obj));
resolved.push((normalised, u, v));
}
if !resolved.is_empty() {
prim_data.push((kind, resolved));
}
}
_ => {}
}
}
(typed, prim_data)
}
fn tessellate_special_points(doc: &ObjDoc) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
let (_, prim_data) = collect_special_points(doc);
for (kind, points) in prim_data {
let positions: Vec<[f32; 3]> = points
.iter()
.map(|(_, u, v)| [*u, v.unwrap_or(0.0), 0.0])
.collect();
let n = positions.len() as u32;
let mut prim = Primitive::new(Topology::Points);
prim.positions = positions;
prim.indices = if n > u16::MAX as u32 {
Some(Indices::U32((0..n).collect()))
} else {
Some(Indices::U16((0..n).map(|i| i as u16).collect()))
};
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:special_point".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:special_point_element_kind".to_string(),
serde_json::Value::String(kind.as_str().to_string()),
);
let refs: Vec<serde_json::Value> = points
.iter()
.map(|(idx, _, _)| serde_json::Value::Number(serde_json::Number::from(*idx)))
.collect();
prim.extras.insert(
"obj:special_point_vp_refs".to_string(),
serde_json::Value::Array(refs),
);
out.push(prim);
}
out
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SpecialPointKind {
Curv,
Curv2,
Surf,
}
impl SpecialPointKind {
fn as_str(self) -> &'static str {
match self {
SpecialPointKind::Curv => "curv",
SpecialPointKind::Curv2 => "curv2",
SpecialPointKind::Surf => "surf",
}
}
}
fn collect_connectivity(doc: &ObjDoc) -> Vec<serde_json::Value> {
let mut typed: Vec<serde_json::Value> = Vec::new();
for entry in &doc.freeform_directives {
let Some(keyword) = entry.first().map(String::as_str) else {
continue;
};
if keyword != "con" {
continue;
}
if entry.len() != 9 {
continue;
}
let Ok(surf_1) = entry[1].parse::<i64>() else {
continue;
};
let Ok(q0_1) = entry[2].parse::<f64>() else {
continue;
};
let Ok(q1_1) = entry[3].parse::<f64>() else {
continue;
};
let Ok(curv2d_1) = entry[4].parse::<i64>() else {
continue;
};
let Ok(surf_2) = entry[5].parse::<i64>() else {
continue;
};
let Ok(q0_2) = entry[6].parse::<f64>() else {
continue;
};
let Ok(q1_2) = entry[7].parse::<f64>() else {
continue;
};
let Ok(curv2d_2) = entry[8].parse::<i64>() else {
continue;
};
let mut obj = serde_json::Map::new();
obj.insert(
"surf_1".to_string(),
serde_json::Value::Number(serde_json::Number::from(surf_1)),
);
obj.insert("q0_1".to_string(), serde_json::Value::from(q0_1));
obj.insert("q1_1".to_string(), serde_json::Value::from(q1_1));
obj.insert(
"curv2d_1".to_string(),
serde_json::Value::Number(serde_json::Number::from(curv2d_1)),
);
obj.insert(
"surf_2".to_string(),
serde_json::Value::Number(serde_json::Number::from(surf_2)),
);
obj.insert("q0_2".to_string(), serde_json::Value::from(q0_2));
obj.insert("q1_2".to_string(), serde_json::Value::from(q1_2));
obj.insert(
"curv2d_2".to_string(),
serde_json::Value::Number(serde_json::Number::from(curv2d_2)),
);
typed.push(serde_json::Value::Object(obj));
}
typed
}
fn collect_trim_loops(doc: &ObjDoc) -> Vec<serde_json::Value> {
let mut typed: Vec<serde_json::Value> = Vec::new();
let mut active_cstype: Option<&'static str> = None;
let mut active_kind: Option<&'static str> = None;
for entry in &doc.freeform_directives {
let Some(keyword) = entry.first().map(String::as_str) else {
continue;
};
match keyword {
"cstype" => {
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_cstype = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
active_kind = None;
}
"end" => {
active_cstype = None;
active_kind = None;
}
"curv" => active_kind = Some("curv"),
"curv2" => active_kind = Some("curv2"),
"surf" => active_kind = Some("surf"),
"trim" | "hole" | "scrv" => {
let toks = &entry[1..];
if toks.is_empty() || toks.len() % 3 != 0 {
continue;
}
let mut segments: Vec<serde_json::Value> = Vec::new();
let mut ok = true;
for chunk in toks.chunks(3) {
let (Ok(u0), Ok(u1), Ok(curv2d)) = (
chunk[0].parse::<f64>(),
chunk[1].parse::<f64>(),
chunk[2].parse::<i64>(),
) else {
ok = false;
break;
};
let mut seg = serde_json::Map::new();
seg.insert("u0".to_string(), serde_json::Value::from(u0));
seg.insert("u1".to_string(), serde_json::Value::from(u1));
seg.insert(
"curv2d".to_string(),
serde_json::Value::Number(serde_json::Number::from(curv2d)),
);
segments.push(serde_json::Value::Object(seg));
}
if !ok {
continue;
}
let mut obj = serde_json::Map::new();
obj.insert(
"loop_kind".to_string(),
serde_json::Value::String(keyword.to_string()),
);
obj.insert(
"element_kind".to_string(),
serde_json::Value::String(active_kind.unwrap_or("unknown").to_string()),
);
obj.insert(
"cstype".to_string(),
serde_json::Value::String(active_cstype.unwrap_or("unknown").to_string()),
);
obj.insert("segments".to_string(), serde_json::Value::Array(segments));
typed.push(serde_json::Value::Object(obj));
}
_ => {}
}
}
typed
}
fn collect_parms(doc: &ObjDoc) -> Vec<serde_json::Value> {
let mut typed: Vec<serde_json::Value> = Vec::new();
let mut active_cstype: Option<&'static str> = None;
let mut active_kind: Option<&'static str> = None;
for entry in &doc.freeform_directives {
let Some(keyword) = entry.first().map(String::as_str) else {
continue;
};
match keyword {
"cstype" => {
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_cstype = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
active_kind = None;
}
"end" => {
active_cstype = None;
active_kind = None;
}
"curv" => active_kind = Some("curv"),
"curv2" => active_kind = Some("curv2"),
"surf" => active_kind = Some("surf"),
"parm" => {
let Some(direction) = entry.get(1).map(String::as_str) else {
continue;
};
if direction != "u" && direction != "v" {
continue;
}
let Some(kind) = active_kind else {
continue;
};
let values: Vec<f64> = entry[2..]
.iter()
.filter_map(|t| t.parse::<f64>().ok())
.collect();
let mut obj = serde_json::Map::new();
obj.insert(
"direction".to_string(),
serde_json::Value::String(direction.to_string()),
);
obj.insert(
"element_kind".to_string(),
serde_json::Value::String(kind.to_string()),
);
obj.insert(
"cstype".to_string(),
serde_json::Value::String(active_cstype.unwrap_or("unknown").to_string()),
);
obj.insert(
"values".to_string(),
serde_json::Value::Array(
values.into_iter().map(serde_json::Value::from).collect(),
),
);
typed.push(serde_json::Value::Object(obj));
}
_ => {}
}
}
typed
}
fn collect_approximation_techniques(doc: &ObjDoc) -> Vec<serde_json::Value> {
let mut typed: Vec<serde_json::Value> = Vec::new();
let mut active_cstype: Option<&'static str> = None;
for entry in &doc.freeform_directives {
let Some(keyword) = entry.first().map(String::as_str) else {
continue;
};
match keyword {
"cstype" => {
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_cstype = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
}
"end" => {
active_cstype = None;
}
"ctech" | "stech" => {
let Some(technique) = entry.get(1).map(String::as_str) else {
continue;
};
let element_kind = if keyword == "ctech" {
"curve"
} else {
"surface"
};
let expected_args: usize = match (keyword, technique) {
("ctech", "cparm") => 1,
("ctech", "cspace") => 1,
("ctech", "curv") => 2,
("stech", "cparma") => 2,
("stech", "cparmb") => 1,
("stech", "cspace") => 1,
("stech", "curv") => 2,
_ => continue,
};
if entry.len() != 2 + expected_args {
continue;
}
let mut params: Vec<f64> = Vec::with_capacity(expected_args);
let mut ok = true;
for raw in &entry[2..] {
match raw.parse::<f64>() {
Ok(v) => params.push(v),
Err(_) => {
ok = false;
break;
}
}
}
if !ok {
continue;
}
let mut obj = serde_json::Map::new();
obj.insert(
"element_kind".to_string(),
serde_json::Value::String(element_kind.to_string()),
);
obj.insert(
"technique".to_string(),
serde_json::Value::String(technique.to_string()),
);
obj.insert(
"parameters".to_string(),
serde_json::Value::Array(
params.into_iter().map(serde_json::Value::from).collect(),
),
);
obj.insert(
"cstype".to_string(),
serde_json::Value::String(active_cstype.unwrap_or("unknown").to_string()),
);
typed.push(serde_json::Value::Object(obj));
}
_ => {}
}
}
typed
}
fn tessellate_surfaces(doc: &ObjDoc, samples: u32) -> Vec<Primitive> {
let mut out: Vec<Primitive> = Vec::new();
if samples == 0 {
return out;
}
let curv2_polylines = collect_all_curv2_polylines(doc, samples);
let mut active_kind: Option<&'static str> = None;
let mut deg_u: Option<u32> = None;
let mut deg_v: Option<u32> = None;
let mut parm_u: Vec<f32> = Vec::new();
let mut parm_v: Vec<f32> = Vec::new();
let mut bmat_u: Vec<f32> = Vec::new();
let mut bmat_v: Vec<f32> = Vec::new();
let mut step_u: Option<u32> = None;
let mut step_v: Option<u32> = None;
let mut pending_surfs: Vec<&Vec<String>> = Vec::new();
let mut pending_trims: Vec<Vec<[f32; 2]>> = Vec::new();
let mut pending_holes: Vec<Vec<[f32; 2]>> = Vec::new();
let mut pending_scrvs: Vec<Vec<[f32; 2]>> = Vec::new();
let resolve_loop = |entry: &Vec<String>| -> Option<Vec<[f32; 2]>> {
let toks = &entry[1..];
if toks.len() < 3 || toks.len() % 3 != 0 {
return None;
}
let mut polygon: Vec<[f32; 2]> = Vec::new();
for chunk in toks.chunks(3) {
let u0 = chunk[0].parse::<f32>().ok()?;
let u1 = chunk[1].parse::<f32>().ok()?;
let idx = chunk[2].parse::<i64>().ok()?;
if idx <= 0 {
return None;
}
let slot = idx as usize - 1;
let entry = curv2_polylines.get(slot).and_then(|e| e.as_ref())?;
let (curve_u_min, curve_u_max, polyline) = entry;
append_curv2_segment(&mut polygon, polyline, *curve_u_min, *curve_u_max, u0, u1);
}
if polygon.len() < 3 {
return None;
}
Some(polygon)
};
#[allow(clippy::too_many_arguments)]
let flush = |out: &mut Vec<Primitive>,
kind: Option<&'static str>,
deg_u: Option<u32>,
deg_v: Option<u32>,
parm_u: &[f32],
parm_v: &[f32],
bmat_u: &[f32],
bmat_v: &[f32],
step_u: Option<u32>,
step_v: Option<u32>,
surfs: &[&Vec<String>],
trims: &[Vec<[f32; 2]>],
holes: &[Vec<[f32; 2]>],
scrvs: &[Vec<[f32; 2]>]| {
let Some(kind) = kind else {
return;
};
for entry in surfs {
if let Some(prim) = flush_surface(
doc, kind, deg_u, deg_v, parm_u, parm_v, bmat_u, bmat_v, step_u, step_v, entry,
samples, trims, holes, scrvs,
) {
out.push(prim);
}
}
};
for entry in &doc.freeform_directives {
if entry.is_empty() {
continue;
}
match entry[0].as_str() {
"cstype" => {
flush(
&mut out,
active_kind,
deg_u,
deg_v,
&parm_u,
&parm_v,
&bmat_u,
&bmat_v,
step_u,
step_v,
&pending_surfs,
&pending_trims,
&pending_holes,
&pending_scrvs,
);
pending_surfs.clear();
pending_trims.clear();
pending_holes.clear();
pending_scrvs.clear();
deg_u = None;
deg_v = None;
parm_u.clear();
parm_v.clear();
bmat_u.clear();
bmat_v.clear();
step_u = None;
step_v = None;
let mut iter = entry.iter().skip(1);
let first = iter.next().map(String::as_str);
let second = iter.next().map(String::as_str);
active_kind = match (first, second) {
(Some("bezier"), _) => Some("bezier"),
(Some("rat"), Some("bezier")) => Some("rat_bezier"),
(Some("bspline"), _) => Some("bspline"),
(Some("rat"), Some("bspline")) => Some("rat_bspline"),
(Some("cardinal"), _) => Some("cardinal"),
(Some("rat"), Some("cardinal")) => Some("cardinal"),
(Some("taylor"), _) => Some("taylor"),
(Some("rat"), Some("taylor")) => Some("taylor"),
(Some("bmatrix"), _) => Some("bmatrix"),
(Some("rat"), Some("bmatrix")) => Some("bmatrix"),
_ => None,
};
}
"deg" => {
deg_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
deg_v = entry.get(2).and_then(|t| t.parse::<u32>().ok()).or(deg_u);
}
"parm" if entry.get(1).map(String::as_str) == Some("u") => {
parm_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"parm" if entry.get(1).map(String::as_str) == Some("v") => {
parm_v = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("u") => {
bmat_u = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"bmat" if entry.get(1).map(String::as_str) == Some("v") => {
bmat_v = entry[2..]
.iter()
.filter_map(|t| t.parse::<f32>().ok())
.collect();
}
"step" => {
step_u = entry.get(1).and_then(|t| t.parse::<u32>().ok());
step_v = entry.get(2).and_then(|t| t.parse::<u32>().ok());
}
"surf" => pending_surfs.push(entry),
"trim" => {
if let Some(loop_uv) = resolve_loop(entry) {
pending_trims.push(loop_uv);
}
}
"hole" => {
if let Some(loop_uv) = resolve_loop(entry) {
pending_holes.push(loop_uv);
}
}
"scrv" => {
if let Some(loop_uv) = resolve_loop(entry) {
pending_scrvs.push(loop_uv);
}
}
"end" => {
flush(
&mut out,
active_kind,
deg_u,
deg_v,
&parm_u,
&parm_v,
&bmat_u,
&bmat_v,
step_u,
step_v,
&pending_surfs,
&pending_trims,
&pending_holes,
&pending_scrvs,
);
pending_surfs.clear();
pending_trims.clear();
pending_holes.clear();
pending_scrvs.clear();
active_kind = None;
deg_u = None;
deg_v = None;
parm_u.clear();
parm_v.clear();
bmat_u.clear();
bmat_v.clear();
step_u = None;
step_v = None;
}
_ => {}
}
}
flush(
&mut out,
active_kind,
deg_u,
deg_v,
&parm_u,
&parm_v,
&bmat_u,
&bmat_v,
step_u,
step_v,
&pending_surfs,
&pending_trims,
&pending_holes,
&pending_scrvs,
);
out
}
struct TrimRemesh<'a> {
stride: usize,
samples: u32,
s0: f32,
span_s: f32,
t0: f32,
span_t: f32,
trims: &'a [Vec<[f32; 2]>],
holes: &'a [Vec<[f32; 2]>],
lattice: &'a [[f32; 3]],
kept: &'a [bool],
area_eps: f32,
boundary_positions: Vec<[f32; 3]>,
boundary_uvs: Vec<[f32; 2]>,
edge_cache: HashMap<(u32, u32), u32>,
}
impl<'a> TrimRemesh<'a> {
fn uv_of(&self, i: u32) -> [f32; 2] {
let lattice_count = self.lattice.len() as u32;
if i < lattice_count {
let su = (i as usize) % self.stride;
let sv = (i as usize) / self.stride;
[
self.s0 + (su as f32 / self.samples as f32) * self.span_s,
self.t0 + (sv as f32 / self.samples as f32) * self.span_t,
]
} else {
self.boundary_uvs[(i - lattice_count) as usize]
}
}
fn inside(&self, uv: [f32; 2]) -> bool {
let in_trim = self.trims.is_empty()
|| self
.trims
.iter()
.any(|loop_uv| point_in_polygon(uv, loop_uv));
let in_hole = self
.holes
.iter()
.any(|loop_uv| point_in_polygon(uv, loop_uv));
in_trim && !in_hole
}
fn crossing(&mut self, inside_idx: u32, outside_idx: u32) -> u32 {
let key = if inside_idx < outside_idx {
(inside_idx, outside_idx)
} else {
(outside_idx, inside_idx)
};
if let Some(&idx) = self.edge_cache.get(&key) {
return idx;
}
let uv_in = self.uv_of(inside_idx);
let uv_out = self.uv_of(outside_idx);
let mut lo = 0.0f32;
let mut hi = 1.0f32;
for _ in 0..24 {
let mid = 0.5 * (lo + hi);
let uv = [
uv_in[0] + (uv_out[0] - uv_in[0]) * mid,
uv_in[1] + (uv_out[1] - uv_in[1]) * mid,
];
if self.inside(uv) {
lo = mid;
} else {
hi = mid;
}
}
let t = lo;
let pa = self.lattice[inside_idx as usize];
let pb = self.lattice[outside_idx as usize];
let idx = self.lattice.len() as u32 + self.boundary_positions.len() as u32;
self.boundary_positions.push([
pa[0] + (pb[0] - pa[0]) * t,
pa[1] + (pb[1] - pa[1]) * t,
pa[2] + (pb[2] - pa[2]) * t,
]);
self.boundary_uvs.push([
uv_in[0] + (uv_out[0] - uv_in[0]) * t,
uv_in[1] + (uv_out[1] - uv_in[1]) * t,
]);
self.edge_cache.insert(key, idx);
idx
}
fn push_triangle(&self, indices: &mut Vec<u32>, a: u32, b: u32, c: u32) {
let p = self.uv_of(a);
let q = self.uv_of(b);
let r = self.uv_of(c);
let area2 = ((q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])).abs();
if area2 > self.area_eps {
indices.push(a);
indices.push(b);
indices.push(c);
}
}
fn clip_triangle(&mut self, indices: &mut Vec<u32>, a: u32, b: u32, c: u32) {
let ka = self.kept[a as usize];
let kb = self.kept[b as usize];
let kc = self.kept[c as usize];
match (ka, kb, kc) {
(true, true, true) => {
indices.push(a);
indices.push(b);
indices.push(c);
}
(false, false, false) => {}
(true, false, false) => self.clip_one_kept(indices, a, b, c),
(false, true, false) => self.clip_one_kept(indices, b, c, a),
(false, false, true) => self.clip_one_kept(indices, c, a, b),
(true, true, false) => self.clip_two_kept(indices, a, b, c),
(false, true, true) => self.clip_two_kept(indices, b, c, a),
(true, false, true) => self.clip_two_kept(indices, c, a, b),
}
}
fn clip_one_kept(&mut self, indices: &mut Vec<u32>, i: u32, o1: u32, o2: u32) {
let p1 = self.crossing(i, o1);
let p2 = self.crossing(i, o2);
self.push_triangle(indices, i, p1, p2);
}
fn clip_two_kept(&mut self, indices: &mut Vec<u32>, i1: u32, i2: u32, o: u32) {
let p1 = self.crossing(i2, o);
let p2 = self.crossing(i1, o);
self.push_triangle(indices, i1, i2, p1);
self.push_triangle(indices, i1, p1, p2);
}
fn finish(self, indices: &mut [u32]) -> (Vec<[f32; 3]>, Vec<[f32; 2]>) {
let lattice_count = self.lattice.len() as u32;
let mut remap: HashMap<u32, u32> = HashMap::new();
let mut compacted: Vec<[f32; 3]> = Vec::new();
let mut compacted_uvs: Vec<[f32; 2]> = Vec::new();
for idx in indices.iter_mut() {
let old = *idx;
if old < lattice_count {
continue;
}
let next = lattice_count + remap.len() as u32;
let slot = *remap.entry(old).or_insert_with(|| {
compacted.push(self.boundary_positions[(old - lattice_count) as usize]);
compacted_uvs.push(self.boundary_uvs[(old - lattice_count) as usize]);
next
});
*idx = slot;
}
(compacted, compacted_uvs)
}
}
struct ScrvConstraint<'a> {
positions: &'a mut Vec<[f32; 3]>,
uvs: &'a mut Vec<[f32; 2]>,
s0: f32,
span_s: f32,
t0: f32,
span_t: f32,
vertex_cache: HashMap<(i64, i64), u32>,
}
impl ScrvConstraint<'_> {
fn key(&self, uv: [f32; 2]) -> (i64, i64) {
let qs = if self.span_s.abs() > f32::EPSILON {
((uv[0] - self.s0) / self.span_s * (1 << 20) as f32).round() as i64
} else {
0
};
let qt = if self.span_t.abs() > f32::EPSILON {
((uv[1] - self.t0) / self.span_t * (1 << 20) as f32).round() as i64
} else {
0
};
(qs, qt)
}
fn vertex_for(&mut self, uv: [f32; 2], tri: [u32; 3]) -> u32 {
let k = self.key(uv);
if let Some(&idx) = self.vertex_cache.get(&k) {
return idx;
}
let pa = self.uvs[tri[0] as usize];
let pb = self.uvs[tri[1] as usize];
let pc = self.uvs[tri[2] as usize];
let det = (pb[1] - pc[1]) * (pa[0] - pc[0]) + (pc[0] - pb[0]) * (pa[1] - pc[1]);
let pos = if det.abs() < 1e-20 {
let za = self.positions[tri[0] as usize];
let zb = self.positions[tri[1] as usize];
let zc = self.positions[tri[2] as usize];
[
(za[0] + zb[0] + zc[0]) / 3.0,
(za[1] + zb[1] + zc[1]) / 3.0,
(za[2] + zb[2] + zc[2]) / 3.0,
]
} else {
let l0 = ((pb[1] - pc[1]) * (uv[0] - pc[0]) + (pc[0] - pb[0]) * (uv[1] - pc[1])) / det;
let l1 = ((pc[1] - pa[1]) * (uv[0] - pc[0]) + (pa[0] - pc[0]) * (uv[1] - pc[1])) / det;
let l2 = 1.0 - l0 - l1;
let za = self.positions[tri[0] as usize];
let zb = self.positions[tri[1] as usize];
let zc = self.positions[tri[2] as usize];
[
l0 * za[0] + l1 * zb[0] + l2 * zc[0],
l0 * za[1] + l1 * zb[1] + l2 * zc[1],
l0 * za[2] + l1 * zb[2] + l2 * zc[2],
]
};
let idx = self.positions.len() as u32;
self.positions.push(pos);
self.uvs.push(uv);
self.vertex_cache.insert(k, idx);
idx
}
fn apply(&mut self, indices: &mut Vec<u32>, polyline: &[[f32; 2]]) -> bool {
let mut any = false;
for seg in polyline.windows(2) {
if self.insert_segment(indices, seg[0], seg[1]) {
any = true;
}
}
any
}
fn overlaps_surface(&self, indices: &[u32], a: [f32; 2], b: [f32; 2]) -> bool {
let mut tri = 0usize;
while tri * 3 < indices.len() {
let t = [indices[tri * 3], indices[tri * 3 + 1], indices[tri * 3 + 2]];
if self.clip_raw_span(t, a, b).is_some() {
return true;
}
tri += 1;
}
false
}
fn insert_segment(&mut self, indices: &mut Vec<u32>, a: [f32; 2], b: [f32; 2]) -> bool {
let overlapped = self.overlaps_surface(indices, a, b);
let max_iters = 64 * (indices.len() / 3).max(1) + 4096;
let mut iters = 0;
loop {
let mut split_at: Option<usize> = None;
let mut tri = 0usize;
while tri * 3 < indices.len() {
let t = [indices[tri * 3], indices[tri * 3 + 1], indices[tri * 3 + 2]];
if self.crosses_interior(t, a, b) {
split_at = Some(tri);
break;
}
tri += 1;
}
let Some(tri) = split_at else {
break;
};
let t = [indices[tri * 3], indices[tri * 3 + 1], indices[tri * 3 + 2]];
if !self.split_triangle(indices, tri, t, a, b) {
break;
}
iters += 1;
if iters > max_iters {
break;
}
}
overlapped
}
fn crosses_interior(&self, t: [u32; 3], a: [f32; 2], b: [f32; 2]) -> bool {
self.clip_span(t, a, b).is_some()
}
fn clip_raw_span(&self, t: [u32; 3], a: [f32; 2], b: [f32; 2]) -> Option<([f32; 2], [f32; 2])> {
let va = self.uvs[t[0] as usize];
let vb = self.uvs[t[1] as usize];
let vc = self.uvs[t[2] as usize];
let area2 = ((vb[0] - va[0]) * (vc[1] - va[1]) - (vb[1] - va[1]) * (vc[0] - va[0])).abs();
if area2 < 1e-18 {
return None;
}
let eps = (area2.sqrt()) * 1e-4;
let mut t_lo = 0.0f32;
let mut t_hi = 1.0f32;
let edges = [(va, vb), (vb, vc), (vc, va)];
let orient = (vb[0] - va[0]) * (vc[1] - va[1]) - (vb[1] - va[1]) * (vc[0] - va[0]);
let sgn = if orient >= 0.0 { 1.0 } else { -1.0 };
let dir = [b[0] - a[0], b[1] - a[1]];
for (e0, e1) in edges {
let ex = e1[0] - e0[0];
let ey = e1[1] - e0[1];
let nx = -ey * sgn;
let ny = ex * sgn;
let da = nx * (a[0] - e0[0]) + ny * (a[1] - e0[1]);
let db = nx * (b[0] - e0[0]) + ny * (b[1] - e0[1]);
let denom = db - da;
if denom.abs() < 1e-20 {
if da < -eps * (nx * nx + ny * ny).sqrt() {
return None;
}
continue;
}
let tc = -da / denom;
if denom > 0.0 {
if tc > t_lo {
t_lo = tc;
}
} else if tc < t_hi {
t_hi = tc;
}
if t_lo > t_hi {
return None;
}
}
if t_hi - t_lo <= 1e-6 {
return None;
}
let p = [a[0] + dir[0] * t_lo, a[1] + dir[1] * t_lo];
let q = [a[0] + dir[0] * t_hi, a[1] + dir[1] * t_hi];
Some((p, q))
}
fn clip_span(&self, t: [u32; 3], a: [f32; 2], b: [f32; 2]) -> Option<([f32; 2], [f32; 2])> {
let (p, q) = self.clip_raw_span(t, a, b)?;
let va = self.uvs[t[0] as usize];
let vb = self.uvs[t[1] as usize];
let vc = self.uvs[t[2] as usize];
let area2 = ((vb[0] - va[0]) * (vc[1] - va[1]) - (vb[1] - va[1]) * (vc[0] - va[0])).abs();
let eps = area2.sqrt() * 1e-4;
if self.span_on_single_edge(va, vb, vc, p, q, eps) {
return None;
}
Some((p, q))
}
fn span_on_single_edge(
&self,
va: [f32; 2],
vb: [f32; 2],
vc: [f32; 2],
p: [f32; 2],
q: [f32; 2],
eps: f32,
) -> bool {
let on = |e0: [f32; 2], e1: [f32; 2], pt: [f32; 2]| -> bool {
let ex = e1[0] - e0[0];
let ey = e1[1] - e0[1];
let len = (ex * ex + ey * ey).sqrt();
if len < 1e-20 {
return false;
}
let dist = ((pt[0] - e0[0]) * ey - (pt[1] - e0[1]) * ex).abs() / len;
dist <= eps
};
(on(va, vb, p) && on(va, vb, q))
|| (on(vb, vc, p) && on(vb, vc, q))
|| (on(vc, va, p) && on(vc, va, q))
}
fn split_triangle(
&mut self,
indices: &mut Vec<u32>,
tri: usize,
t: [u32; 3],
a: [f32; 2],
b: [f32; 2],
) -> bool {
let Some((p, q)) = self.clip_span(t, a, b) else {
return false;
};
let ip = self.vertex_for(p, t);
let iq = self.vertex_for(q, t);
if ip == iq {
return false;
}
let new_tris = self.constrained_split(t, ip, iq);
if new_tris.is_empty() {
return false;
}
let first = new_tris[0];
indices[tri * 3] = first[0];
indices[tri * 3 + 1] = first[1];
indices[tri * 3 + 2] = first[2];
for nt in &new_tris[1..] {
indices.push(nt[0]);
indices.push(nt[1]);
indices.push(nt[2]);
}
true
}
fn constrained_split(&self, t: [u32; 3], ip: u32, iq: u32) -> Vec<[u32; 3]> {
let p_edge = self.locate_on_edge(t, ip);
let q_edge = self.locate_on_edge(t, iq);
let mut out: Vec<[u32; 3]> = Vec::new();
match (p_edge, q_edge) {
(Some((pa, pb)), Some((qa, qb))) => {
let loop_verts = self.boundary_loop(t, ip, (pa, pb), iq, (qa, qb));
self.fan_with_chord(&loop_verts, ip, iq, &mut out);
}
(Some((pa, pb)), None) => {
self.split_one_interior(t, iq, ip, (pa, pb), &mut out);
}
(None, Some((qa, qb))) => {
self.split_one_interior(t, ip, iq, (qa, qb), &mut out);
}
(None, None) => {
out.push([t[0], t[1], ip]);
out.push([t[1], t[2], ip]);
out.push([t[2], t[0], iq]);
out.push([t[0], ip, iq]);
out.push([t[2], iq, ip]);
out.push([t[1], ip, iq]);
}
}
out
}
fn locate_on_edge(&self, t: [u32; 3], v: u32) -> Option<(u32, u32)> {
let pt = self.uvs[v as usize];
let corners = [t[0], t[1], t[2]];
let uv = [
self.uvs[t[0] as usize],
self.uvs[t[1] as usize],
self.uvs[t[2] as usize],
];
let area2 = ((uv[1][0] - uv[0][0]) * (uv[2][1] - uv[0][1])
- (uv[1][1] - uv[0][1]) * (uv[2][0] - uv[0][0]))
.abs();
let eps = area2.sqrt() * 1e-3;
for e in 0..3 {
let e0 = uv[e];
let e1 = uv[(e + 1) % 3];
let ex = e1[0] - e0[0];
let ey = e1[1] - e0[1];
let len = (ex * ex + ey * ey).sqrt();
if len < 1e-20 {
continue;
}
let dist = ((pt[0] - e0[0]) * ey - (pt[1] - e0[1]) * ex).abs() / len;
let proj = ((pt[0] - e0[0]) * ex + (pt[1] - e0[1]) * ey) / (len * len);
if dist <= eps && (-1e-3..=1.0 + 1e-3).contains(&proj) {
return Some((corners[e], corners[(e + 1) % 3]));
}
}
None
}
fn boundary_loop(
&self,
t: [u32; 3],
ip: u32,
p_edge: (u32, u32),
iq: u32,
q_edge: (u32, u32),
) -> Vec<u32> {
let mut loop_verts: Vec<u32> = Vec::with_capacity(5);
for e in 0..3 {
let from = t[e];
let to = t[(e + 1) % 3];
loop_verts.push(from);
let mut on_edge: Vec<(f32, u32)> = Vec::new();
for (cv, ce) in [(ip, p_edge), (iq, q_edge)] {
if (ce.0 == from && ce.1 == to) || (ce.0 == to && ce.1 == from) {
let pf = self.uvs[from as usize];
let pt = self.uvs[cv as usize];
let d = (pt[0] - pf[0]).hypot(pt[1] - pf[1]);
on_edge.push((d, cv));
}
}
on_edge.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
for (_, cv) in on_edge {
loop_verts.push(cv);
}
}
loop_verts
}
fn fan_with_chord(&self, loop_verts: &[u32], ip: u32, iq: u32, out: &mut Vec<[u32; 3]>) {
let n = loop_verts.len();
if n < 3 {
return;
}
let pos_ip = loop_verts.iter().position(|&v| v == ip);
let pos_iq = loop_verts.iter().position(|&v| v == iq);
let (Some(i), Some(j)) = (pos_ip, pos_iq) else {
for k in 1..n - 1 {
out.push([loop_verts[0], loop_verts[k], loop_verts[k + 1]]);
}
return;
};
let chain = |start: usize, end: usize| -> Vec<u32> {
let mut c = Vec::new();
let mut k = start;
loop {
c.push(loop_verts[k]);
if k == end {
break;
}
k = (k + 1) % n;
}
c
};
for sub in [chain(i, j), chain(j, i)] {
for k in 1..sub.len().saturating_sub(1) {
out.push([sub[0], sub[k], sub[k + 1]]);
}
}
}
fn split_one_interior(
&self,
t: [u32; 3],
inner: u32,
bound: u32,
edge: (u32, u32),
out: &mut Vec<[u32; 3]>,
) {
for e in 0..3 {
let from = t[e];
let to = t[(e + 1) % 3];
if (from == edge.0 && to == edge.1) || (from == edge.1 && to == edge.0) {
out.push([from, bound, inner]);
out.push([bound, to, inner]);
} else {
out.push([from, to, inner]);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn flush_surface(
doc: &ObjDoc,
kind: &'static str,
deg_u: Option<u32>,
deg_v: Option<u32>,
parm_u: &[f32],
parm_v: &[f32],
bmat_u: &[f32],
bmat_v: &[f32],
step_u: Option<u32>,
step_v: Option<u32>,
entry: &[String],
samples: u32,
trims: &[Vec<[f32; 2]>],
holes: &[Vec<[f32; 2]>],
scrvs: &[Vec<[f32; 2]>],
) -> Option<Primitive> {
if entry.len() < 6 {
return None;
}
let s0 = entry[1].parse::<f32>().ok()?;
let s1 = entry[2].parse::<f32>().ok()?;
let t0 = entry[3].parse::<f32>().ok()?;
let t1 = entry[4].parse::<f32>().ok()?;
let du = deg_u? as usize;
let dv = deg_v? as usize;
let bspline = matches!(kind, "bspline" | "rat_bspline");
let cardinal = kind == "cardinal";
let taylor = kind == "taylor";
let bmatrix = kind == "bmatrix";
let (cols, rows) = if bspline {
let need_u = du.checked_add(2)?;
let need_v = dv.checked_add(2)?;
if parm_u.len() < need_u || parm_v.len() < need_v {
return None;
}
(parm_u.len() - du - 1, parm_v.len() - dv - 1) } else if bmatrix {
let su = step_u? as usize;
let sv = step_v? as usize;
if su == 0 || sv == 0 || parm_u.len() < 2 || parm_v.len() < 2 {
return None;
}
let cols = (parm_u.len() - 2)
.checked_mul(su)?
.checked_add(du)?
.checked_add(1)?;
let rows = (parm_v.len() - 2)
.checked_mul(sv)?
.checked_add(dv)?
.checked_add(1)?;
(cols, rows)
} else if cardinal {
if du != 3 || dv != 3 {
return None;
}
let total = entry.len() - 5; let cols = if parm_u.len() > 2 {
parm_u.len() + 1
} else {
isqrt_exact(total)?
};
let rows = if parm_v.len() > 2 {
parm_v.len() + 1
} else if cols != 0 && total % cols == 0 {
total / cols
} else {
return None;
};
(cols, rows)
} else if taylor {
(du.checked_add(1)?, dv.checked_add(1)?)
} else {
let patches_u = if parm_u.len() >= 2 {
parm_u.len() - 1
} else {
1
};
let patches_v = if parm_v.len() >= 2 {
parm_v.len() - 1
} else {
1
};
let cols = du.checked_mul(patches_u)?.checked_add(1)?;
let rows = dv.checked_mul(patches_v)?.checked_add(1)?;
(cols, rows)
};
let expected = cols.checked_mul(rows)?;
if expected != entry.len().saturating_sub(5) {
return None;
}
let n_pos = doc.positions.len() as i64;
let mut grid: Vec<[f32; 3]> = Vec::with_capacity(expected);
let mut weights: Vec<f32> = Vec::with_capacity(expected);
for tok in &entry[5..] {
let first_field = tok.split('/').next().unwrap_or(tok);
let raw = first_field.parse::<i64>().ok()?;
let resolved = if raw < 0 { n_pos + 1 + raw } else { raw };
if resolved <= 0 || resolved > n_pos {
return None;
}
grid.push(doc.positions[(resolved as usize) - 1]);
let w = doc.position_weights[(resolved as usize) - 1].unwrap_or(1.0);
weights.push(w);
}
if grid.len() != expected {
return None;
}
let mut positions = if bspline {
sample_bspline_surface(
&grid, &weights, kind, du as u32, dv as u32, parm_u, parm_v, s0, s1, t0, t1, cols,
rows, samples,
)
} else if cardinal {
sample_cardinal_surface(&grid, cols, rows, samples)
} else if taylor {
sample_taylor_surface(&grid, cols, rows, s0, s1, t0, t1, samples)
} else if bmatrix {
let need_u = du.checked_add(1)?.checked_mul(du.checked_add(1)?)?;
let need_v = dv.checked_add(1)?.checked_mul(dv.checked_add(1)?)?;
if bmat_u.len() != need_u || bmat_v.len() != need_v {
return None;
}
let su = step_u?;
let sv = step_v?;
sample_bmatrix_surface(
&grid, bmat_u, bmat_v, du as u32, dv as u32, su, sv, cols, rows, samples,
)
} else {
let patches_u = cols.saturating_sub(1).checked_div(du).unwrap_or(1).max(1);
let patches_v = rows.saturating_sub(1).checked_div(dv).unwrap_or(1).max(1);
sample_bezier_surface_multipatch(
&grid, &weights, kind, cols, rows, du, dv, patches_u, patches_v, samples,
)
};
if positions.is_empty() {
return None;
}
let stride = samples as usize + 1;
let trimming = !trims.is_empty() || !holes.is_empty();
let kept: Vec<bool> = if trimming {
let mut k = Vec::with_capacity(stride * stride);
let span_s = s1 - s0;
let span_t = t1 - t0;
for sv in 0..stride {
for su in 0..stride {
let u_frac = su as f32 / samples as f32;
let v_frac = sv as f32 / samples as f32;
let u = s0 + u_frac * span_s;
let v = t0 + v_frac * span_t;
let in_trim = trims.is_empty()
|| trims
.iter()
.any(|loop_uv| point_in_polygon([u, v], loop_uv));
let in_hole = holes
.iter()
.any(|loop_uv| point_in_polygon([u, v], loop_uv));
k.push(in_trim && !in_hole);
}
}
k
} else {
Vec::new()
};
let mut indices: Vec<u32> = Vec::with_capacity((samples as usize) * (samples as usize) * 6);
let mut boundary_vertex_count = 0usize;
let span_s = s1 - s0;
let span_t = t1 - t0;
let want_scrv = !scrvs.is_empty();
let mut uvs: Vec<[f32; 2]> = if want_scrv {
let mut v = Vec::with_capacity(positions.len());
for sv in 0..stride {
for su in 0..stride {
v.push([
s0 + (su as f32 / samples as f32) * span_s,
t0 + (sv as f32 / samples as f32) * span_t,
]);
}
}
v
} else {
Vec::new()
};
if trimming {
let cell_area2 = (span_s / samples as f32).abs() * (span_t / samples as f32).abs() * 2.0;
let mut remesh = TrimRemesh {
stride,
samples,
s0,
span_s,
t0,
span_t,
trims,
holes,
lattice: &positions,
kept: &kept,
area_eps: cell_area2 * 1e-6,
boundary_positions: Vec::new(),
boundary_uvs: Vec::new(),
edge_cache: HashMap::new(),
};
for sv in 0..samples as usize {
for su in 0..samples as usize {
let i00 = (sv * stride + su) as u32;
let i10 = (sv * stride + su + 1) as u32;
let i01 = ((sv + 1) * stride + su) as u32;
let i11 = ((sv + 1) * stride + su + 1) as u32;
remesh.clip_triangle(&mut indices, i00, i10, i11);
remesh.clip_triangle(&mut indices, i00, i11, i01);
}
}
let (boundary, boundary_uvs) = remesh.finish(&mut indices);
boundary_vertex_count = boundary.len();
positions.extend(boundary);
if want_scrv {
uvs.extend(boundary_uvs);
}
} else {
for sv in 0..samples as usize {
for su in 0..samples as usize {
let i00 = (sv * stride + su) as u32;
let i10 = (sv * stride + su + 1) as u32;
let i01 = ((sv + 1) * stride + su) as u32;
let i11 = ((sv + 1) * stride + su + 1) as u32;
indices.push(i00);
indices.push(i10);
indices.push(i11);
indices.push(i00);
indices.push(i11);
indices.push(i01);
}
}
}
let mut scrv_constraint_vertices = 0usize;
let mut scrv_constraint_curves = 0usize;
if want_scrv {
let lattice_count = positions.len();
let mut con = ScrvConstraint {
positions: &mut positions,
uvs: &mut uvs,
s0,
span_s,
t0,
span_t,
vertex_cache: HashMap::new(),
};
for scrv in scrvs {
if con.apply(&mut indices, scrv) {
scrv_constraint_curves += 1;
}
}
scrv_constraint_vertices = positions.len() - lattice_count;
}
let mut prim = Primitive::new(Topology::Triangles);
let n_verts = positions.len() as u32;
prim.positions = positions;
prim.indices = if n_verts > u16::MAX as u32 {
Some(Indices::U32(indices))
} else {
Some(Indices::U16(indices.iter().map(|&i| i as u16).collect()))
};
prim.extras.insert(
"obj:tessellated_curve".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:tessellated_surface".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:surface_kind".to_string(),
serde_json::Value::String(kind.to_string()),
);
prim.extras.insert(
"obj:surface_degree".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(du as u64),
serde_json::Value::from(dv as u64),
]),
);
prim.extras.insert(
"obj:surface_u_range".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(s0 as f64),
serde_json::Value::from(s1 as f64),
]),
);
prim.extras.insert(
"obj:surface_v_range".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(t0 as f64),
serde_json::Value::from(t1 as f64),
]),
);
prim.extras.insert(
"obj:surface_samples".to_string(),
serde_json::Value::Number(serde_json::Number::from(samples as u64)),
);
if matches!(kind, "bezier" | "rat_bezier") && du > 0 && dv > 0 {
let patches_u = (cols.saturating_sub(1)) / du;
let patches_v = (rows.saturating_sub(1)) / dv;
if patches_u > 1 || patches_v > 1 {
prim.extras.insert(
"obj:surface_patches".to_string(),
serde_json::Value::Array(vec![
serde_json::Value::from(patches_u as u64),
serde_json::Value::from(patches_v as u64),
]),
);
}
}
if trimming {
prim.extras.insert(
"obj:surface_trimmed".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:surface_trim_loops".to_string(),
serde_json::Value::Number(serde_json::Number::from(trims.len() as u64)),
);
prim.extras.insert(
"obj:surface_hole_loops".to_string(),
serde_json::Value::Number(serde_json::Number::from(holes.len() as u64)),
);
prim.extras.insert(
"obj:surface_trim_boundary_vertices".to_string(),
serde_json::Value::Number(serde_json::Number::from(boundary_vertex_count as u64)),
);
}
if want_scrv {
prim.extras.insert(
"obj:surface_scrv".to_string(),
serde_json::Value::Bool(true),
);
prim.extras.insert(
"obj:surface_scrv_curves".to_string(),
serde_json::Value::Number(serde_json::Number::from(scrv_constraint_curves as u64)),
);
prim.extras.insert(
"obj:surface_scrv_vertices".to_string(),
serde_json::Value::Number(serde_json::Number::from(scrv_constraint_vertices as u64)),
);
}
Some(prim)
}
fn sample_bezier_surface(
grid: &[[f32; 3]],
weights: &[f32],
kind: &str,
cols: usize,
rows: usize,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0 || cols == 0 || rows == 0 || grid.len() != cols * rows {
return Vec::new();
}
let rational = kind == "rat_bezier";
let homo: Vec<[f32; 4]> = grid
.iter()
.zip(weights.iter())
.map(|(p, w)| {
let weight = if rational { *w } else { 1.0 };
[p[0] * weight, p[1] * weight, p[2] * weight, weight]
})
.collect();
let n = samples as usize + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
for sv in 0..n {
let v = if n == 1 {
0.0
} else {
sv as f32 / (n - 1) as f32
};
for su in 0..n {
let u = if n == 1 {
0.0
} else {
su as f32 / (n - 1) as f32
};
let mut col_pts: Vec<[f32; 4]> = Vec::with_capacity(rows);
for r in 0..rows {
let row = &homo[r * cols..r * cols + cols];
col_pts.push(de_casteljau_4d(row, u));
}
let pt = de_casteljau_4d(&col_pts, v);
let [x, y, z, w] = pt;
if rational && w.abs() > f32::EPSILON {
out.push([x / w, y / w, z / w]);
} else {
out.push([x, y, z]);
}
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn sample_bezier_surface_multipatch(
grid: &[[f32; 3]],
weights: &[f32],
kind: &str,
cols: usize,
rows: usize,
deg_u: usize,
deg_v: usize,
patches_u: usize,
patches_v: usize,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0
|| cols == 0
|| rows == 0
|| deg_u == 0
|| deg_v == 0
|| patches_u == 0
|| patches_v == 0
|| grid.len() != cols * rows
|| weights.len() != grid.len()
{
return Vec::new();
}
if patches_u == 1 && patches_v == 1 {
return sample_bezier_surface(grid, weights, kind, cols, rows, samples);
}
let rational = kind == "rat_bezier";
let homo: Vec<[f32; 4]> = grid
.iter()
.zip(weights.iter())
.map(|(p, w)| {
let weight = if rational { *w } else { 1.0 };
[p[0] * weight, p[1] * weight, p[2] * weight, weight]
})
.collect();
let n = samples as usize + 1;
let patch_cols = deg_u + 1;
let patch_rows = deg_v + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
let mut sub_window: Vec<[f32; 4]> = Vec::with_capacity(patch_cols * patch_rows);
let mut col_pts: Vec<[f32; 4]> = Vec::with_capacity(patch_rows);
for sv in 0..n {
let v_global = if n == 1 {
0.0
} else {
sv as f32 * (patches_v as f32) / (n - 1) as f32
};
let mut pv = v_global.floor() as isize;
if pv < 0 {
pv = 0;
}
if pv as usize >= patches_v {
pv = patches_v as isize - 1;
}
let pv = pv as usize;
let t_v = (v_global - pv as f32).clamp(0.0, 1.0);
for su in 0..n {
let u_global = if n == 1 {
0.0
} else {
su as f32 * (patches_u as f32) / (n - 1) as f32
};
let mut pu = u_global.floor() as isize;
if pu < 0 {
pu = 0;
}
if pu as usize >= patches_u {
pu = patches_u as isize - 1;
}
let pu = pu as usize;
let t_u = (u_global - pu as f32).clamp(0.0, 1.0);
sub_window.clear();
let base_u = pu * deg_u;
let base_v = pv * deg_v;
for j in 0..patch_rows {
let row_start = (base_v + j) * cols + base_u;
sub_window.extend_from_slice(&homo[row_start..row_start + patch_cols]);
}
col_pts.clear();
for r in 0..patch_rows {
let row = &sub_window[r * patch_cols..(r + 1) * patch_cols];
col_pts.push(de_casteljau_4d(row, t_u));
}
let pt = de_casteljau_4d(&col_pts, t_v);
let [x, y, z, w] = pt;
if rational && w.abs() > f32::EPSILON {
out.push([x / w, y / w, z / w]);
} else {
out.push([x, y, z]);
}
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn sample_bmatrix_surface(
grid: &[[f32; 3]],
bmat_u: &[f32],
bmat_v: &[f32],
deg_u: u32,
deg_v: u32,
step_u: u32,
step_v: u32,
cols: usize,
rows: usize,
samples: u32,
) -> Vec<[f32; 3]> {
let n_plus_1 = match (deg_u as usize).checked_add(1) {
Some(v) => v,
None => return Vec::new(),
};
let m_plus_1 = match (deg_v as usize).checked_add(1) {
Some(v) => v,
None => return Vec::new(),
};
let need_bmat_u = match n_plus_1.checked_mul(n_plus_1) {
Some(v) => v,
None => return Vec::new(),
};
let need_bmat_v = match m_plus_1.checked_mul(m_plus_1) {
Some(v) => v,
None => return Vec::new(),
};
if samples == 0
|| cols == 0
|| rows == 0
|| step_u == 0
|| step_v == 0
|| grid.len() != cols * rows
|| bmat_u.len() != need_bmat_u
|| bmat_v.len() != need_bmat_v
|| cols < n_plus_1
|| rows < m_plus_1
{
return Vec::new();
}
let su_stride = step_u as usize;
let sv_stride = step_v as usize;
let n_seg_u = (cols - n_plus_1) / su_stride + 1;
let n_seg_v = (rows - m_plus_1) / sv_stride + 1;
let n = samples as usize + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
for sv_i in 0..n {
let gv = if sv_i == n - 1 {
n_seg_v as f32
} else {
sv_i as f32 * n_seg_v as f32 / (n - 1) as f32
};
let mut seg_v = gv.floor() as usize;
let mut tv = gv - seg_v as f32;
if seg_v >= n_seg_v {
seg_v = n_seg_v - 1;
tv = 1.0;
}
let base_v = seg_v * sv_stride;
let mut tv_pow: Vec<f32> = Vec::with_capacity(m_plus_1);
let mut pv = 1.0_f32;
for _ in 0..m_plus_1 {
tv_pow.push(pv);
pv *= tv;
}
let mut v_coef: Vec<f32> = Vec::with_capacity(m_plus_1);
for b in 0..m_plus_1 {
let mut c = 0.0_f32;
for q in 0..m_plus_1 {
c += bmat_v[b * m_plus_1 + q] * tv_pow[q];
}
v_coef.push(c);
}
for su_i in 0..n {
let gu = if su_i == n - 1 {
n_seg_u as f32
} else {
su_i as f32 * n_seg_u as f32 / (n - 1) as f32
};
let mut seg_u = gu.floor() as usize;
let mut tu = gu - seg_u as f32;
if seg_u >= n_seg_u {
seg_u = n_seg_u - 1;
tu = 1.0;
}
let base_u = seg_u * su_stride;
let mut tu_pow: Vec<f32> = Vec::with_capacity(n_plus_1);
let mut pu = 1.0_f32;
for _ in 0..n_plus_1 {
tu_pow.push(pu);
pu *= tu;
}
let mut u_coef: Vec<f32> = Vec::with_capacity(n_plus_1);
for a in 0..n_plus_1 {
let mut c = 0.0_f32;
for p in 0..n_plus_1 {
c += bmat_u[a * n_plus_1 + p] * tu_pow[p];
}
u_coef.push(c);
}
let mut accum = [0.0_f32; 3];
for (b, vc) in v_coef.iter().enumerate() {
let row = (base_v + b) * cols;
for (a, uc) in u_coef.iter().enumerate() {
let cp = grid[row + base_u + a];
let w = uc * vc;
accum[0] += w * cp[0];
accum[1] += w * cp[1];
accum[2] += w * cp[2];
}
}
out.push(accum);
}
}
out
}
fn de_casteljau_4d(points: &[[f32; 4]], t: f32) -> [f32; 4] {
if points.is_empty() {
return [0.0, 0.0, 0.0, 1.0];
}
let mut buf: Vec<[f32; 4]> = points.to_vec();
let n = buf.len();
for level in 1..n {
for j in 0..(n - level) {
buf[j] = [
(1.0 - t) * buf[j][0] + t * buf[j + 1][0],
(1.0 - t) * buf[j][1] + t * buf[j + 1][1],
(1.0 - t) * buf[j][2] + t * buf[j + 1][2],
(1.0 - t) * buf[j][3] + t * buf[j + 1][3],
];
}
}
buf[0]
}
#[allow(clippy::too_many_arguments)]
fn sample_bspline_surface(
grid: &[[f32; 3]],
weights: &[f32],
kind: &str,
deg_u: u32,
deg_v: u32,
knots_u: &[f32],
knots_v: &[f32],
s0: f32,
s1: f32,
t0: f32,
t1: f32,
cols: usize,
rows: usize,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0 || cols == 0 || rows == 0 || grid.len() != cols * rows {
return Vec::new();
}
let nu = deg_u as usize;
let nv = deg_v as usize;
if knots_u.len() != cols + nu + 1 || knots_v.len() != rows + nv + 1 {
return Vec::new();
}
let u_lo_bound = knots_u[nu];
let u_hi_bound = knots_u[cols]; let v_lo_bound = knots_v[nv];
let v_hi_bound = knots_v[rows]; let u_min = s0.max(u_lo_bound);
let u_max = s1.min(u_hi_bound);
let v_min = t0.max(v_lo_bound);
let v_max = t1.min(v_hi_bound);
if u_min > u_max || v_min > v_max {
return Vec::new();
}
let rational = kind == "rat_bspline";
let n = samples as usize + 1;
let nudge = |t: f32, lo: f32, hi: f32| -> f32 {
if t >= hi {
let biased = hi - (hi - lo).abs() * 1e-7 - f32::EPSILON;
if biased < lo { lo } else { biased }
} else {
t
}
};
let u_basis_rows: Vec<Vec<f32>> = (0..n)
.map(|i| {
let t01 = if n == 1 {
0.0
} else {
i as f32 / (n - 1) as f32
};
let u = nudge(u_min + t01 * (u_max - u_min), u_lo_bound, u_hi_bound);
bspline_basis(u, knots_u, nu)
})
.collect();
let v_basis_rows: Vec<Vec<f32>> = (0..n)
.map(|j| {
let t01 = if n == 1 {
0.0
} else {
j as f32 / (n - 1) as f32
};
let v = nudge(v_min + t01 * (v_max - v_min), v_lo_bound, v_hi_bound);
bspline_basis(v, knots_v, nv)
})
.collect();
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
for vb in v_basis_rows.iter() {
for ub in u_basis_rows.iter() {
let mut acc = [0.0f32; 3];
let mut wsum = 0.0f32;
for (j, &bv) in vb.iter().enumerate().take(rows) {
if bv == 0.0 {
continue;
}
for (i, &bu) in ub.iter().enumerate().take(cols) {
if bu == 0.0 {
continue;
}
let idx = j * cols + i;
let w = if rational { weights[idx] } else { 1.0 };
let coeff = bu * bv * w;
if coeff == 0.0 {
continue;
}
wsum += coeff;
acc[0] += coeff * grid[idx][0];
acc[1] += coeff * grid[idx][1];
acc[2] += coeff * grid[idx][2];
}
}
if wsum.abs() > f32::EPSILON {
out.push([acc[0] / wsum, acc[1] / wsum, acc[2] / wsum]);
} else {
out.push(acc);
}
}
}
out
}
fn sample_bezier(
control_points: &[[f32; 3]],
control_weights: &[f32],
kind: &str,
_u_min: f32,
_u_max: f32,
samples: u32,
) -> Vec<[f32; 3]> {
if control_points.is_empty() || samples == 0 {
return Vec::new();
}
let rational = kind == "rat_bezier";
let homogeneous: Vec<[f32; 4]> = control_points
.iter()
.zip(control_weights.iter())
.map(|(p, w)| {
let weight = if rational { *w } else { 1.0 };
[p[0] * weight, p[1] * weight, p[2] * weight, weight]
})
.collect();
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
for i in 0..n_samples {
let t01 = if n_samples == 1 {
0.0
} else {
i as f32 / (n_samples - 1) as f32
};
let t = t01;
let mut buf: Vec<[f32; 4]> = homogeneous.clone();
let n = buf.len();
for level in 1..n {
for j in 0..(n - level) {
buf[j] = [
(1.0 - t) * buf[j][0] + t * buf[j + 1][0],
(1.0 - t) * buf[j][1] + t * buf[j + 1][1],
(1.0 - t) * buf[j][2] + t * buf[j + 1][2],
(1.0 - t) * buf[j][3] + t * buf[j + 1][3],
];
}
}
let [x, y, z, w] = buf[0];
if rational && w.abs() > f32::EPSILON {
out.push([x / w, y / w, z / w]);
} else {
out.push([x, y, z]);
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn sample_bspline(
control_points: &[[f32; 3]],
control_weights: &[f32],
kind: &str,
degree: u32,
knots: &[f32],
u_min: f32,
u_max: f32,
samples: u32,
) -> Vec<[f32; 3]> {
if control_points.is_empty() || samples == 0 {
return Vec::new();
}
let n = degree as usize;
let k_plus_1 = control_points.len(); if knots.len() != k_plus_1 + n + 1 {
return Vec::new();
}
let t_lo_bound = knots[n];
let t_hi_bound = knots[k_plus_1]; let t_min = u_min.max(t_lo_bound);
let t_max = u_max.min(t_hi_bound);
if t_min > t_max {
return Vec::new();
}
let rational = kind == "rat_bspline";
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
for i in 0..n_samples {
let t01 = if n_samples == 1 {
0.0
} else {
i as f32 / (n_samples - 1) as f32
};
let mut t = t_min + t01 * (t_max - t_min);
if t >= t_hi_bound {
t = t_hi_bound - (t_hi_bound - t_lo_bound).abs() * 1e-7 - f32::EPSILON;
if t < t_lo_bound {
t = t_lo_bound;
}
}
let basis = bspline_basis(t, knots, n);
let mut acc = [0.0f32; 3];
let mut wsum = 0.0f32;
for j in 0..k_plus_1 {
let bj = basis[j];
if bj == 0.0 {
continue;
}
let w = if rational { control_weights[j] } else { 1.0 };
let bw = bj * w;
wsum += bw;
acc[0] += bw * control_points[j][0];
acc[1] += bw * control_points[j][1];
acc[2] += bw * control_points[j][2];
}
if rational && wsum.abs() > f32::EPSILON {
out.push([acc[0] / wsum, acc[1] / wsum, acc[2] / wsum]);
} else if !rational && wsum.abs() > f32::EPSILON {
out.push(acc);
} else {
out.push(acc);
}
}
out
}
fn bspline_basis(t: f32, knots: &[f32], degree: usize) -> Vec<f32> {
let m = knots.len();
if m <= degree + 1 {
return Vec::new();
}
let k_plus_1 = m - degree - 1;
let mut basis: Vec<f32> = Vec::with_capacity(m - 1);
for i in 0..(m - 1) {
let inside = if i + 1 == m - 1 {
knots[i] <= t && t <= knots[i + 1]
} else {
knots[i] <= t && t < knots[i + 1]
};
basis.push(if inside { 1.0 } else { 0.0 });
}
for k in 1..=degree {
let new_len = m - 1 - k;
for j in 0..new_len {
let denom_left = knots[j + k] - knots[j];
let denom_right = knots[j + k + 1] - knots[j + 1];
let left = if denom_left.abs() < f32::EPSILON {
0.0
} else {
(t - knots[j]) / denom_left * basis[j]
};
let right = if denom_right.abs() < f32::EPSILON {
0.0
} else {
(knots[j + k + 1] - t) / denom_right * basis[j + 1]
};
basis[j] = left + right;
}
basis.truncate(new_len);
}
debug_assert_eq!(basis.len(), k_plus_1);
basis
}
fn sample_cardinal(control_points: &[[f32; 3]], samples: u32) -> Vec<[f32; 3]> {
if control_points.len() < 4 || samples == 0 {
return Vec::new();
}
let n_segments = control_points.len() - 3;
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
for i in 0..n_samples {
let s = if i == n_samples - 1 {
n_segments as f32
} else {
i as f32 * n_segments as f32 / (n_samples - 1) as f32
};
let mut seg = s.floor() as usize;
let mut t = s - seg as f32;
if seg >= n_segments {
seg = n_segments - 1;
t = 1.0;
}
let c0 = control_points[seg];
let c1 = control_points[seg + 1];
let c2 = control_points[seg + 2];
let c3 = control_points[seg + 3];
let mut b: [[f32; 3]; 4] = [[0.0; 3]; 4];
for a in 0..3 {
b[0][a] = c1[a];
b[1][a] = c1[a] + (c2[a] - c0[a]) / 6.0;
b[2][a] = c2[a] - (c3[a] - c1[a]) / 6.0;
b[3][a] = c2[a];
}
let u = 1.0 - t;
let w0 = u * u * u;
let w1 = 3.0 * u * u * t;
let w2 = 3.0 * u * t * t;
let w3 = t * t * t;
let p = [
w0 * b[0][0] + w1 * b[1][0] + w2 * b[2][0] + w3 * b[3][0],
w0 * b[0][1] + w1 * b[1][1] + w2 * b[2][1] + w3 * b[3][1],
w0 * b[0][2] + w1 * b[1][2] + w2 * b[2][2] + w3 * b[3][2],
];
out.push(p);
}
out
}
fn cardinal_eval_1d(points: &[[f32; 3]], s: f32) -> [f32; 3] {
let n_segments = points.len() - 3;
let mut seg = s.floor() as isize;
let mut t = s - seg as f32;
if seg < 0 {
seg = 0;
t = 0.0;
} else if seg as usize >= n_segments {
seg = n_segments as isize - 1;
t = 1.0;
}
let seg = seg as usize;
let c0 = points[seg];
let c1 = points[seg + 1];
let c2 = points[seg + 2];
let c3 = points[seg + 3];
let mut b: [[f32; 3]; 4] = [[0.0; 3]; 4];
for a in 0..3 {
b[0][a] = c1[a];
b[1][a] = c1[a] + (c2[a] - c0[a]) / 6.0;
b[2][a] = c2[a] - (c3[a] - c1[a]) / 6.0;
b[3][a] = c2[a];
}
let u = 1.0 - t;
let w0 = u * u * u;
let w1 = 3.0 * u * u * t;
let w2 = 3.0 * u * t * t;
let w3 = t * t * t;
[
w0 * b[0][0] + w1 * b[1][0] + w2 * b[2][0] + w3 * b[3][0],
w0 * b[0][1] + w1 * b[1][1] + w2 * b[2][1] + w3 * b[3][1],
w0 * b[0][2] + w1 * b[1][2] + w2 * b[2][2] + w3 * b[3][2],
]
}
fn sample_cardinal_surface(
grid: &[[f32; 3]],
cols: usize,
rows: usize,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0 || cols < 4 || rows < 4 || grid.len() != cols * rows {
return Vec::new();
}
let n = samples as usize + 1;
let u_span = (cols - 3) as f32; let v_span = (rows - 3) as f32;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
for sv in 0..n {
let v = if n == 1 {
0.0
} else {
sv as f32 / (n - 1) as f32 * v_span
};
for su in 0..n {
let u = if n == 1 {
0.0
} else {
su as f32 / (n - 1) as f32 * u_span
};
let mut col_pts: Vec<[f32; 3]> = Vec::with_capacity(rows);
for r in 0..rows {
let row = &grid[r * cols..r * cols + cols];
col_pts.push(cardinal_eval_1d(row, u));
}
out.push(cardinal_eval_1d(&col_pts, v));
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn sample_taylor_surface(
grid: &[[f32; 3]],
cols: usize,
rows: usize,
s0: f32,
s1: f32,
t0: f32,
t1: f32,
samples: u32,
) -> Vec<[f32; 3]> {
if samples == 0 || cols == 0 || rows == 0 || grid.len() != cols * rows {
return Vec::new();
}
let n = samples as usize + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n * n);
let mut col_pts: Vec<[f32; 3]> = Vec::with_capacity(rows);
for sv in 0..n {
let v = if n == 1 {
0.0
} else {
t0 + (sv as f32 / (n - 1) as f32) * (t1 - t0)
};
for su in 0..n {
let u = if n == 1 {
0.0
} else {
s0 + (su as f32 / (n - 1) as f32) * (s1 - s0)
};
col_pts.clear();
for r in 0..rows {
let row_start = r * cols;
let mut acc = grid[row_start + cols - 1];
for i in (0..cols - 1).rev() {
let cij = grid[row_start + i];
acc[0] = acc[0] * u + cij[0];
acc[1] = acc[1] * u + cij[1];
acc[2] = acc[2] * u + cij[2];
}
col_pts.push(acc);
}
let mut acc = col_pts[rows - 1];
for j in (0..rows - 1).rev() {
let cj = col_pts[j];
acc[0] = acc[0] * v + cj[0];
acc[1] = acc[1] * v + cj[1];
acc[2] = acc[2] * v + cj[2];
}
out.push(acc);
}
}
out
}
fn isqrt_exact(n: usize) -> Option<usize> {
if n == 0 {
return None;
}
let mut r = (n as f64).sqrt() as usize;
while r * r > n {
r -= 1;
}
while (r + 1) * (r + 1) <= n {
r += 1;
}
if r * r == n { Some(r) } else { None }
}
fn sample_taylor(
control_points: &[[f32; 3]],
u_min: f32,
u_max: f32,
samples: u32,
) -> Vec<[f32; 3]> {
if control_points.is_empty() || samples == 0 {
return Vec::new();
}
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
let k = control_points.len();
for i in 0..n_samples {
let frac = if n_samples == 1 {
0.0
} else {
i as f32 / (n_samples - 1) as f32
};
let t = u_min + frac * (u_max - u_min);
let mut acc = control_points[k - 1];
for j in (0..(k - 1)).rev() {
acc[0] = acc[0] * t + control_points[j][0];
acc[1] = acc[1] * t + control_points[j][1];
acc[2] = acc[2] * t + control_points[j][2];
}
out.push(acc);
}
out
}
fn sample_bmatrix(
control_points: &[[f32; 3]],
bmat_u: &[f32],
degree: u32,
step: u32,
samples: u32,
) -> Vec<[f32; 3]> {
let Some(n_plus_1) = (degree as usize).checked_add(1) else {
return Vec::new();
};
let Some(expected_bmat) = n_plus_1.checked_mul(n_plus_1) else {
return Vec::new();
};
if control_points.len() < n_plus_1 || bmat_u.len() != expected_bmat || step == 0 || samples == 0
{
return Vec::new();
}
let s = step as usize;
let n_segments = (control_points.len() - n_plus_1) / s + 1;
let n_samples = samples + 1;
let mut out: Vec<[f32; 3]> = Vec::with_capacity(n_samples as usize);
for i in 0..n_samples {
let g = if i == n_samples - 1 {
n_segments as f32
} else {
i as f32 * n_segments as f32 / (n_samples - 1) as f32
};
let mut seg = g.floor() as usize;
let mut t = g - seg as f32;
if seg >= n_segments {
seg = n_segments - 1;
t = 1.0;
}
let base = seg * s;
let mut t_pow: Vec<f32> = Vec::with_capacity(n_plus_1);
let mut p = 1.0_f32;
for _ in 0..n_plus_1 {
t_pow.push(p);
p *= t;
}
let mut accum = [0.0_f32; 3];
for ii in 0..n_plus_1 {
let mut coef = 0.0_f32;
for jj in 0..n_plus_1 {
coef += bmat_u[ii * n_plus_1 + jj] * t_pow[jj];
}
let cp = control_points[base + ii];
accum[0] += coef * cp[0];
accum[1] += coef * cp[1];
accum[2] += coef * cp[2];
}
out.push(accum);
}
out
}
fn is_tessellated_curve(prim: &Primitive) -> bool {
prim.extras
.get("obj:tessellated_curve")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
fn single_line_topology(elements: &[Element]) -> Topology {
if elements.len() != 1 {
return Topology::Lines;
}
let Element::Line(verts) = &elements[0] else {
return Topology::Lines;
};
if verts.len() < 2 {
return Topology::Lines;
}
if verts.len() == 2 {
return Topology::Lines;
}
let same_start_end = verts.first().map(|fv| fv.v) == verts.last().map(|fv| fv.v);
if same_start_end {
Topology::LineLoop
} else {
Topology::LineStrip
}
}
fn build_primitive(
prim_acc: &PrimAccum,
positions: &[[f32; 3]],
position_weights: &[Option<f32>],
position_colors: &[Option<[f32; 4]>],
texcoords: &[[f32; 2]],
normals: &[[f32; 3]],
material_ids: &HashMap<String, oxideav_mesh3d::MaterialId>,
) -> Result<(Primitive, Vec<u32>)> {
let first = prim_acc.elements.first();
let topology = match first {
Some(Element::Face(_)) => Topology::Triangles,
Some(Element::Line(_)) => single_line_topology(&prim_acc.elements),
Some(Element::Point(_)) => Topology::Points,
None => Topology::Triangles,
};
for elt in &prim_acc.elements {
let ok = matches!(
(&topology, elt),
(Topology::Triangles, Element::Face(_))
| (Topology::Lines, Element::Line(_))
| (Topology::LineStrip, Element::Line(_))
| (Topology::LineLoop, Element::Line(_))
| (Topology::Points, Element::Point(_))
);
if !ok {
return Err(Error::unsupported(
"OBJ primitive mixes face / line / point elements under one usemtl",
));
}
}
let has_uv = prim_acc.elements.iter().any(|elt| match elt {
Element::Face(verts) | Element::Line(verts) | Element::Point(verts) => {
verts.iter().any(|fv| fv.vt != 0)
}
});
let has_normal = prim_acc.elements.iter().any(|elt| match elt {
Element::Face(verts) | Element::Line(verts) | Element::Point(verts) => {
verts.iter().any(|fv| fv.vn != 0)
}
});
let has_color = prim_acc.elements.iter().any(|elt| match elt {
Element::Face(verts) | Element::Line(verts) | Element::Point(verts) => {
verts.iter().any(|fv| {
position_colors
.get((fv.v - 1) as usize)
.is_some_and(Option::is_some)
})
}
});
let mut prim = Primitive::new(topology);
if has_uv {
prim.uvs.push(Vec::new());
}
if has_normal {
prim.normals = Some(Vec::new());
}
if has_color {
prim.colors.push(Vec::new());
}
let mut color_present: Vec<bool> = Vec::new();
let mut weights_seen: Vec<Option<f32>> = Vec::new();
let mut indexer: HashMap<FaceVert, u32> = HashMap::new();
let mut arities: Vec<u32> = Vec::new();
let mut local_indices: Vec<u32> = Vec::new();
let intern = |fv: FaceVert,
prim: &mut Primitive,
indexer: &mut HashMap<FaceVert, u32>,
color_present: &mut Vec<bool>,
weights_seen: &mut Vec<Option<f32>>|
-> Result<u32> {
if let Some(&idx) = indexer.get(&fv) {
return Ok(idx);
}
let pos = positions
.get((fv.v - 1) as usize)
.ok_or_else(|| Error::invalid(format!("face references missing position {}", fv.v)))?;
prim.positions.push(*pos);
if has_uv {
let uv = if fv.vt == 0 {
[0.0, 0.0]
} else {
*texcoords.get((fv.vt - 1) as usize).ok_or_else(|| {
Error::invalid(format!("face references missing texcoord {}", fv.vt))
})?
};
prim.uvs[0].push(uv);
}
if has_normal {
let n = if fv.vn == 0 {
[0.0, 0.0, 0.0]
} else {
*normals.get((fv.vn - 1) as usize).ok_or_else(|| {
Error::invalid(format!("face references missing normal {}", fv.vn))
})?
};
prim.normals.as_mut().unwrap().push(n);
}
if has_color {
let rgba = position_colors
.get((fv.v - 1) as usize)
.copied()
.flatten()
.unwrap_or([1.0, 1.0, 1.0, 1.0]);
prim.colors[0].push(rgba);
color_present.push(
position_colors
.get((fv.v - 1) as usize)
.is_some_and(Option::is_some),
);
}
weights_seen.push(position_weights.get((fv.v - 1) as usize).copied().flatten());
let new_idx = (prim.positions.len() - 1) as u32;
indexer.insert(fv, new_idx);
Ok(new_idx)
};
for elt in &prim_acc.elements {
match elt {
Element::Face(verts) => {
let arity = verts.len() as u32;
arities.push(arity);
let resolved: Vec<u32> = verts
.iter()
.map(|&fv| {
intern(
fv,
&mut prim,
&mut indexer,
&mut color_present,
&mut weights_seen,
)
})
.collect::<Result<Vec<_>>>()?;
for i in 1..(resolved.len() - 1) {
local_indices.push(resolved[0]);
local_indices.push(resolved[i]);
local_indices.push(resolved[i + 1]);
}
}
Element::Line(verts) => {
let resolved: Vec<u32> = verts
.iter()
.map(|&fv| {
intern(
fv,
&mut prim,
&mut indexer,
&mut color_present,
&mut weights_seen,
)
})
.collect::<Result<Vec<_>>>()?;
match topology {
Topology::LineStrip => {
local_indices.extend_from_slice(&resolved);
}
Topology::LineLoop => {
let n = resolved.len().saturating_sub(1);
local_indices.extend_from_slice(&resolved[..n]);
}
_ => {
for w in resolved.windows(2) {
local_indices.push(w[0]);
local_indices.push(w[1]);
}
}
}
}
Element::Point(verts) => {
for &fv in verts {
let idx = intern(
fv,
&mut prim,
&mut indexer,
&mut color_present,
&mut weights_seen,
)?;
local_indices.push(idx);
}
}
}
}
if local_indices.iter().any(|&i| i >= u16::MAX as u32) {
prim.indices = Some(Indices::U32(local_indices));
} else {
prim.indices = Some(Indices::U16(
local_indices.into_iter().map(|i| i as u16).collect(),
));
}
if has_color && color_present.iter().any(|&b| b) {
prim.extras.insert(
"obj:vertex_color_present".to_string(),
serde_json::to_value(&color_present).unwrap(),
);
}
if weights_seen.iter().any(Option::is_some) {
prim.extras.insert(
"obj:vertex_weight".to_string(),
serde_json::to_value(&weights_seen).unwrap(),
);
}
if let Some(name) = &prim_acc.material {
if let Some(id) = material_ids.get(name) {
prim.material = Some(*id);
}
prim.extras.insert(
"obj:usemtl".to_string(),
serde_json::Value::String(name.clone()),
);
}
if let Some(s) = &prim_acc.smoothing_group {
prim.extras.insert(
"obj:smoothing_group".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.merging_group {
prim.extras.insert(
"obj:merging_group".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.bevel {
prim.extras.insert(
"obj:bevel".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.c_interp {
prim.extras.insert(
"obj:c_interp".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.d_interp {
prim.extras.insert(
"obj:d_interp".to_string(),
serde_json::Value::String(s.clone()),
);
}
if let Some(s) = &prim_acc.lod {
prim.extras
.insert("obj:lod".to_string(), serde_json::Value::String(s.clone()));
}
if let Some(s) = &prim_acc.usemap {
prim.extras.insert(
"obj:usemap".to_string(),
serde_json::Value::String(s.clone()),
);
}
if !prim_acc.groups.is_empty() {
prim.extras.insert(
"obj:groups".to_string(),
serde_json::to_value(&prim_acc.groups).unwrap(),
);
}
Ok((prim, arities))
}
#[derive(Clone, Debug, Default)]
pub struct ParseOptions {
pub curve_tessellation_samples: u32,
}
pub fn parse_obj(text: &str) -> Result<Scene3D> {
parse_obj_with_resolver(text, |_path| Ok(Vec::new()))
}
pub fn parse_obj_from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Scene3D> {
let path = path.as_ref();
let bytes =
std::fs::read(path).map_err(|e| Error::invalid(format!("OBJ read {path:?}: {e}")))?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| Error::invalid(format!("OBJ {path:?} contained non-UTF-8 bytes")))?;
let parent = path.parent().map(std::path::Path::to_path_buf);
parse_obj_with_resolver(text, |libname| {
let lib_path = match &parent {
Some(dir) => dir.join(libname),
None => std::path::PathBuf::from(libname),
};
std::fs::read(&lib_path)
.map_err(|e| Error::invalid(format!("mtllib read {lib_path:?}: {e}")))
})
}
pub fn parse_obj_with_resolver<R>(text: &str, resolve: R) -> Result<Scene3D>
where
R: FnMut(&str) -> Result<Vec<u8>>,
{
parse_obj_with_options(text, &ParseOptions::default(), resolve)
}
pub fn parse_obj_with_options<R>(
text: &str,
options: &ParseOptions,
mut resolve: R,
) -> Result<Scene3D>
where
R: FnMut(&str) -> Result<Vec<u8>>,
{
let mut doc = parse_obj_doc(text)?;
for lib in doc.mtllibs.clone() {
let bytes = resolve(&lib)?;
if bytes.is_empty() {
continue;
}
let lib_text = std::str::from_utf8(&bytes)
.map_err(|_| Error::invalid(format!("mtllib {lib:?} contained non-UTF-8 bytes")))?;
let materials = parse_mtl(lib_text)?;
for mat in materials {
if let Some(name) = mat.name.clone() {
doc.resolved_materials.insert(name, mat);
}
}
}
let tessellated = if options.curve_tessellation_samples > 0 {
tessellate_curves(&doc, options.curve_tessellation_samples)
} else {
Vec::new()
};
let tessellated_curve2 = if options.curve_tessellation_samples > 0 {
tessellate_curve2(&doc, options.curve_tessellation_samples)
} else {
Vec::new()
};
let tessellated_surfaces = if options.curve_tessellation_samples > 0 {
tessellate_surfaces(&doc, options.curve_tessellation_samples)
} else {
Vec::new()
};
let tessellated_scrv = if options.curve_tessellation_samples > 0 {
tessellate_scrv(&doc, options.curve_tessellation_samples)
} else {
Vec::new()
};
let tessellated_sp = if options.curve_tessellation_samples > 0 {
tessellate_special_points(&doc)
} else {
Vec::new()
};
let tessellated_con = if options.curve_tessellation_samples > 0 {
tessellate_connectivity(&doc, options.curve_tessellation_samples)
} else {
Vec::new()
};
let mut scene = build_scene(doc)?;
if !tessellated.is_empty() {
let mut mesh = Mesh::new(Some("obj:curves".to_string()));
for prim in tessellated {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
if !tessellated_curve2.is_empty() {
let mut mesh = Mesh::new(Some("obj:curves2".to_string()));
for prim in tessellated_curve2 {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
if !tessellated_surfaces.is_empty() {
let mut mesh = Mesh::new(Some("obj:surfaces".to_string()));
for prim in tessellated_surfaces {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
if !tessellated_scrv.is_empty() {
let mut mesh = Mesh::new(Some("obj:scrvs".to_string()));
for prim in tessellated_scrv {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
if !tessellated_sp.is_empty() {
let mut mesh = Mesh::new(Some("obj:sps".to_string()));
for prim in tessellated_sp {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
if !tessellated_con.is_empty() {
let mut mesh = Mesh::new(Some("obj:cons".to_string()));
for prim in tessellated_con {
mesh.primitives.push(prim);
}
scene.add_mesh(mesh);
}
Ok(scene)
}
#[derive(Clone, Debug, Default)]
pub struct SerializeOptions<'a> {
pub mtl_basename: Option<&'a str>,
pub negative_indices: bool,
}
pub fn serialize_obj(scene: &Scene3D, mtl_basename: Option<&str>) -> Result<Vec<u8>> {
serialize_obj_with_options(
scene,
&SerializeOptions {
mtl_basename,
..SerializeOptions::default()
},
)
}
pub fn serialize_obj_with_options(
scene: &Scene3D,
options: &SerializeOptions<'_>,
) -> Result<Vec<u8>> {
let mtl_basename = options.mtl_basename;
let negative = options.negative_indices;
use std::fmt::Write;
let mut out = String::new();
writeln!(out, "# OBJ generated by oxideav-obj").unwrap();
if let Some(base) = mtl_basename {
writeln!(out, "mtllib {base}.mtl").unwrap();
}
if mtl_basename.is_none() {
if let Some(serde_json::Value::Array(list)) = scene.extras.get("obj:mtllibs") {
for entry in list {
if let Some(s) = entry.as_str() {
writeln!(out, "mtllib {s}").unwrap();
}
}
}
}
if let Some(serde_json::Value::Array(list)) = scene.extras.get("obj:maplibs") {
for entry in list {
if let Some(s) = entry.as_str() {
writeln!(out, "maplib {s}").unwrap();
}
}
}
if let Some(serde_json::Value::String(name)) = scene.extras.get("obj:shadow_obj") {
if !name.is_empty() {
writeln!(out, "shadow_obj {name}").unwrap();
}
}
if let Some(serde_json::Value::String(name)) = scene.extras.get("obj:trace_obj") {
if !name.is_empty() {
writeln!(out, "trace_obj {name}").unwrap();
}
}
if let Some(serde_json::Value::Array(generals)) = scene.extras.get("obj:general_directives") {
for entry in generals {
if let serde_json::Value::Array(toks) = entry {
let parts: Vec<&str> = toks.iter().filter_map(|v| v.as_str()).collect();
if parts.is_empty() {
continue;
}
writeln!(out, "{}", parts.join(" ")).unwrap();
}
}
}
let mut positions: Vec<[f32; 3]> = Vec::new();
let mut position_colors: Vec<Option<[f32; 4]>> = Vec::new();
let mut position_weights: Vec<Option<f32>> = Vec::new();
let mut texcoords: Vec<[f32; 2]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut pos_map: HashMap<KeyVec3, u32> = HashMap::new();
let mut tex_map: HashMap<KeyVec2, u32> = HashMap::new();
let mut nor_map: HashMap<KeyVec3, u32> = HashMap::new();
let intern_pos = |p: [f32; 3],
colour: Option<[f32; 4]>,
weight: Option<f32>,
positions: &mut Vec<[f32; 3]>,
colours: &mut Vec<Option<[f32; 4]>>,
weights: &mut Vec<Option<f32>>,
map: &mut HashMap<KeyVec3, u32>|
-> u32 {
let key = KeyVec3::from(p);
if let Some(&i) = map.get(&key) {
let slot = (i - 1) as usize;
if colours[slot].is_none() {
colours[slot] = colour;
}
if weights[slot].is_none() {
weights[slot] = weight;
}
return i;
}
positions.push(p);
colours.push(colour);
weights.push(weight);
let idx = positions.len() as u32;
map.insert(key, idx);
idx
};
let intern_tex =
|p: [f32; 2], texcoords: &mut Vec<[f32; 2]>, map: &mut HashMap<KeyVec2, u32>| -> u32 {
let key = KeyVec2::from(p);
if let Some(&i) = map.get(&key) {
return i;
}
texcoords.push(p);
let idx = texcoords.len() as u32;
map.insert(key, idx);
idx
};
let intern_nor =
|p: [f32; 3], normals: &mut Vec<[f32; 3]>, map: &mut HashMap<KeyVec3, u32>| -> u32 {
let key = KeyVec3::from(p);
if let Some(&i) = map.get(&key) {
return i;
}
normals.push(p);
let idx = normals.len() as u32;
map.insert(key, idx);
idx
};
if let Some(serde_json::Value::Array(src_positions)) = scene.extras.get("obj:positions") {
let src_weights: Vec<Option<f32>> = scene
.extras
.get("obj:position_weights")
.and_then(serde_json::Value::as_array)
.map(|arr| arr.iter().map(|v| v.as_f64().map(|f| f as f32)).collect())
.unwrap_or_default();
let src_colors: Vec<Option<[f32; 4]>> = scene
.extras
.get("obj:position_colors")
.and_then(serde_json::Value::as_array)
.map(|arr| {
arr.iter()
.map(|v| {
v.as_array().map(|c| {
let mut rgba = [1.0; 4];
for (i, x) in c.iter().enumerate().take(4) {
rgba[i] = x.as_f64().map(|f| f as f32).unwrap_or(0.0);
}
rgba
})
})
.collect()
})
.unwrap_or_default();
for (i, pv) in src_positions.iter().enumerate() {
let serde_json::Value::Array(coords) = pv else {
continue;
};
let mut p = [0.0_f32; 3];
for (j, c) in coords.iter().enumerate().take(3) {
p[j] = c.as_f64().map(|f| f as f32).unwrap_or(0.0);
}
let weight = src_weights.get(i).copied().flatten();
let colour = src_colors.get(i).copied().flatten();
intern_pos(
p,
colour,
weight,
&mut positions,
&mut position_colors,
&mut position_weights,
&mut pos_map,
);
}
}
type GlobalTriple = (u32, u32, u32); let mut global_indices: Vec<Vec<Vec<GlobalTriple>>> = Vec::new();
for mesh in &scene.meshes {
let mut mesh_globals: Vec<Vec<GlobalTriple>> = Vec::new();
for prim in &mesh.primitives {
if is_tessellated_curve(prim) {
mesh_globals.push(Vec::new());
continue;
}
let has_uv = !prim.uvs.is_empty();
let has_normal = prim.normals.is_some();
let has_color = !prim.colors.is_empty();
let color_present: Vec<bool> = prim
.extras
.get("obj:vertex_color_present")
.and_then(serde_json::Value::as_array)
.map(|arr| arr.iter().map(|v| v.as_bool().unwrap_or(false)).collect())
.unwrap_or_else(|| vec![has_color; prim.positions.len()]);
let weight_overrides: Vec<Option<f32>> = prim
.extras
.get("obj:vertex_weight")
.and_then(serde_json::Value::as_array)
.map(|arr| arr.iter().map(|v| v.as_f64().map(|f| f as f32)).collect())
.unwrap_or_default();
let mut prim_globals: Vec<GlobalTriple> = Vec::with_capacity(prim.positions.len());
for vi in 0..prim.positions.len() {
let colour = if has_color && color_present.get(vi).copied().unwrap_or(false) {
Some(prim.colors[0][vi])
} else {
None
};
let weight = weight_overrides.get(vi).copied().flatten();
let v_idx = intern_pos(
prim.positions[vi],
colour,
weight,
&mut positions,
&mut position_colors,
&mut position_weights,
&mut pos_map,
);
let vt_idx = if has_uv {
intern_tex(prim.uvs[0][vi], &mut texcoords, &mut tex_map)
} else {
0
};
let vn_idx = if has_normal {
intern_nor(
prim.normals.as_ref().unwrap()[vi],
&mut normals,
&mut nor_map,
)
} else {
0
};
prim_globals.push((v_idx, vt_idx, vn_idx));
}
mesh_globals.push(prim_globals);
}
global_indices.push(mesh_globals);
}
for (i, p) in positions.iter().enumerate() {
let weight = position_weights[i];
let colour = position_colors[i];
let mut s = String::with_capacity(40);
s.push_str("v ");
s.push_str(&fmt_float(p[0]));
s.push(' ');
s.push_str(&fmt_float(p[1]));
s.push(' ');
s.push_str(&fmt_float(p[2]));
if let Some(w) = weight {
s.push(' ');
s.push_str(&fmt_float(w));
}
if let Some(rgb) = colour {
s.push(' ');
s.push_str(&fmt_float(rgb[0]));
s.push(' ');
s.push_str(&fmt_float(rgb[1]));
s.push(' ');
s.push_str(&fmt_float(rgb[2]));
}
writeln!(out, "{s}").unwrap();
}
if let Some(serde_json::Value::Array(vps)) = scene.extras.get("obj:vp") {
for entry in vps {
if let serde_json::Value::Array(coords) = entry {
let parts: Vec<f32> = coords
.iter()
.filter_map(|v| v.as_f64().map(|f| f as f32))
.collect();
if parts.is_empty() {
continue;
}
let trim = if parts.len() >= 3 && parts[2] != 0.0 {
3
} else if parts.len() >= 2 && parts[1] != 0.0 {
2
} else {
1
};
let mut s = String::from("vp");
for coord in parts.iter().take(trim) {
s.push(' ');
s.push_str(&fmt_float(*coord));
}
writeln!(out, "{s}").unwrap();
}
}
}
for t in &texcoords {
writeln!(out, "vt {} {}", fmt_float(t[0]), fmt_float(t[1])).unwrap();
}
for n in &normals {
writeln!(
out,
"vn {} {} {}",
fmt_float(n[0]),
fmt_float(n[1]),
fmt_float(n[2])
)
.unwrap();
}
for (mi, mesh) in scene.meshes.iter().enumerate() {
if mesh.primitives.iter().all(is_tessellated_curve) && !mesh.primitives.is_empty() {
continue;
}
if let Some(name) = &mesh.name {
writeln!(out, "o {name}").unwrap();
}
for (pi, prim) in mesh.primitives.iter().enumerate() {
if is_tessellated_curve(prim) {
continue;
}
let arities: Option<Vec<u32>> = prim
.extras
.get("obj:original_face_arities")
.and_then(|v| serde_json::from_value(v.clone()).ok());
if let Some(serde_json::Value::Array(gs)) = prim.extras.get("obj:groups") {
let names: Vec<&str> = gs.iter().filter_map(|v| v.as_str()).collect();
if !names.is_empty() {
writeln!(out, "g {}", names.join(" ")).unwrap();
}
}
if let Some(s) = prim
.extras
.get("obj:smoothing_group")
.and_then(|v| v.as_str())
{
writeln!(out, "s {s}").unwrap();
}
if let Some(s) = prim
.extras
.get("obj:merging_group")
.and_then(|v| v.as_str())
{
writeln!(out, "mg {s}").unwrap();
}
for keyword in ["bevel", "c_interp", "d_interp", "lod"] {
let key = format!("obj:{keyword}");
if let Some(s) = prim.extras.get(&key).and_then(|v| v.as_str()) {
writeln!(out, "{keyword} {s}").unwrap();
}
}
let mtl_name: Option<String> = prim
.extras
.get("obj:usemtl")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
prim.material.and_then(|id| {
scene
.materials
.get(id.0 as usize)
.and_then(|m| m.name.clone())
})
});
if let Some(name) = &mtl_name {
writeln!(out, "usemtl {name}").unwrap();
}
if let Some(name) = prim.extras.get("obj:usemap").and_then(|v| v.as_str()) {
writeln!(out, "usemap {name}").unwrap();
}
let prim_globals = &global_indices[mi][pi];
let has_uv = !prim.uvs.is_empty();
let has_normal = prim.normals.is_some();
match prim.topology {
Topology::Triangles => {
let face_indices: Vec<u32> = match &prim.indices {
Some(Indices::U16(v)) => v.iter().map(|&x| x as u32).collect(),
Some(Indices::U32(v)) => v.clone(),
None => {
(0..prim.positions.len() as u32).collect()
}
};
if let Some(per_prim_arities) = arities.as_ref() {
let mut tri_pos: usize = 0;
for &arity in per_prim_arities {
let mut verts: Vec<u32> = Vec::with_capacity(arity as usize);
let n_tris = (arity as usize).saturating_sub(2);
verts.push(face_indices[tri_pos * 3]);
verts.push(face_indices[tri_pos * 3 + 1]);
verts.push(face_indices[tri_pos * 3 + 2]);
for k in 1..n_tris {
verts.push(face_indices[(tri_pos + k) * 3 + 2]);
}
tri_pos += n_tris;
write_face(
&mut out,
&verts,
prim_globals,
has_uv,
has_normal,
negative,
positions.len() as u32,
texcoords.len() as u32,
normals.len() as u32,
);
}
let consumed = per_prim_arities
.iter()
.map(|&a| (a as usize).saturating_sub(2))
.sum::<usize>();
for tri in consumed..(face_indices.len() / 3) {
let verts = [
face_indices[tri * 3],
face_indices[tri * 3 + 1],
face_indices[tri * 3 + 2],
];
write_face(
&mut out,
&verts,
prim_globals,
has_uv,
has_normal,
negative,
positions.len() as u32,
texcoords.len() as u32,
normals.len() as u32,
);
}
} else {
for tri in 0..(face_indices.len() / 3) {
let verts = [
face_indices[tri * 3],
face_indices[tri * 3 + 1],
face_indices[tri * 3 + 2],
];
write_face(
&mut out,
&verts,
prim_globals,
has_uv,
has_normal,
negative,
positions.len() as u32,
texcoords.len() as u32,
normals.len() as u32,
);
}
}
}
Topology::Lines => {
let line_indices: Vec<u32> = match &prim.indices {
Some(Indices::U16(v)) => v.iter().map(|&x| x as u32).collect(),
Some(Indices::U32(v)) => v.clone(),
None => (0..prim.positions.len() as u32).collect(),
};
let total_v = positions.len() as u32;
let mut chain: Vec<u32> = Vec::new();
let flush = |chain: &mut Vec<u32>, out: &mut String| {
if chain.len() < 2 {
chain.clear();
return;
}
let parts: Vec<String> = chain
.iter()
.map(|&local| {
fmt_index(prim_globals[local as usize].0, total_v, negative)
})
.collect();
writeln!(out, "l {}", parts.join(" ")).unwrap();
chain.clear();
};
for w in line_indices.chunks_exact(2) {
let (a, b) = (w[0], w[1]);
if chain.is_empty() {
chain.push(a);
chain.push(b);
} else if *chain.last().unwrap() == a {
chain.push(b);
} else {
flush(&mut chain, &mut out);
chain.push(a);
chain.push(b);
}
}
flush(&mut chain, &mut out);
}
Topology::LineStrip | Topology::LineLoop => {
let mut strip_indices: Vec<u32> = match &prim.indices {
Some(Indices::U16(v)) => v.iter().map(|&x| x as u32).collect(),
Some(Indices::U32(v)) => v.clone(),
None => (0..prim.positions.len() as u32).collect(),
};
if matches!(prim.topology, Topology::LineLoop)
&& let Some(&first) = strip_indices.first()
{
strip_indices.push(first);
}
if strip_indices.len() >= 2 {
let total_v = positions.len() as u32;
let parts: Vec<String> = strip_indices
.iter()
.map(|&local| {
fmt_index(prim_globals[local as usize].0, total_v, negative)
})
.collect();
writeln!(out, "l {}", parts.join(" ")).unwrap();
}
}
Topology::Points => {
let pt_indices: Vec<u32> = match &prim.indices {
Some(Indices::U16(v)) => v.iter().map(|&x| x as u32).collect(),
Some(Indices::U32(v)) => v.clone(),
None => (0..prim.positions.len() as u32).collect(),
};
let total_v = positions.len() as u32;
if !pt_indices.is_empty() {
let parts: Vec<String> = pt_indices
.iter()
.map(|&local| {
fmt_index(prim_globals[local as usize].0, total_v, negative)
})
.collect();
writeln!(out, "p {}", parts.join(" ")).unwrap();
}
}
other => {
return Err(Error::unsupported(format!(
"OBJ encoder: topology {other:?} not representable"
)));
}
}
}
}
if let Some(serde_json::Value::Array(directives)) = scene.extras.get("obj:freeform_directives")
{
for entry in directives {
if let serde_json::Value::Array(toks) = entry {
let parts: Vec<&str> = toks.iter().filter_map(|v| v.as_str()).collect();
if parts.is_empty() {
continue;
}
writeln!(out, "{}", parts.join(" ")).unwrap();
}
}
}
Ok(out.into_bytes())
}
#[allow(clippy::too_many_arguments)]
fn write_face(
out: &mut String,
verts: &[u32],
prim_globals: &[(u32, u32, u32)],
has_uv: bool,
has_normal: bool,
negative: bool,
total_v: u32,
total_vt: u32,
total_vn: u32,
) {
use std::fmt::Write;
out.push('f');
for &local in verts {
let (v, vt, vn) = prim_globals[local as usize];
let v_s = fmt_index(v, total_v, negative);
let vt_s = fmt_index(vt, total_vt, negative);
let vn_s = fmt_index(vn, total_vn, negative);
match (has_uv, has_normal) {
(true, true) => write!(out, " {v_s}/{vt_s}/{vn_s}").unwrap(),
(true, false) => write!(out, " {v_s}/{vt_s}").unwrap(),
(false, true) => write!(out, " {v_s}//{vn_s}").unwrap(),
(false, false) => write!(out, " {v_s}").unwrap(),
}
}
out.push('\n');
}
fn fmt_index(idx: u32, total: u32, negative: bool) -> String {
if idx == 0 || !negative {
idx.to_string()
} else {
let raw = (idx as i64) - (total as i64) - 1;
raw.to_string()
}
}
fn fmt_float(x: f32) -> String {
if x == 0.0 {
return "0".to_string();
}
let s = format!("{x:.6}");
let trimmed = s.trim_end_matches('0').trim_end_matches('.').to_string();
if trimmed.is_empty() || trimmed == "-" {
"0".to_string()
} else {
trimmed
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct KeyVec2 {
a: u32,
b: u32,
}
impl From<[f32; 2]> for KeyVec2 {
fn from(v: [f32; 2]) -> Self {
Self {
a: v[0].to_bits(),
b: v[1].to_bits(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct KeyVec3 {
a: u32,
b: u32,
c: u32,
}
impl From<[f32; 3]> for KeyVec3 {
fn from(v: [f32; 3]) -> Self {
Self {
a: v[0].to_bits(),
b: v[1].to_bits(),
c: v[2].to_bits(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preprocess_strips_comments_and_glues_continuations() {
let lines =
preprocess_lines("v 1.0 2.0 \\\n3.0 # comment\nv 4 5 6\n# pure comment\nf 1 2 3");
assert_eq!(lines[0].trim(), "v 1.0 2.0 3.0");
assert_eq!(lines[1].trim(), "v 4 5 6");
assert_eq!(lines[2].trim(), "");
assert_eq!(lines[3].trim(), "f 1 2 3");
}
#[test]
fn fmt_float_is_diff_friendly() {
assert_eq!(fmt_float(1.0), "1");
assert_eq!(fmt_float(0.0), "0");
assert_eq!(fmt_float(-0.5), "-0.5");
assert_eq!(fmt_float(1.0 / 3.0), "0.333333");
}
}