//! Native port of `PrimMesher.cs` profile and extrusion geometry. #![allow(clippy::cast_possible_truncation)] // Reference index and step calculations narrow explicitly. #![allow(clippy::cast_precision_loss)] // C# geometry converts integer sides and steps to Single. #![allow(clippy::cast_sign_loss)] // Bounds checks precede C# float-to-index conversions. #![allow(clippy::approx_constant)] // Decimal compatibility constants are intentional. #![allow(clippy::float_cmp)] // Exact parameter branches are part of the golden algorithm. #![allow(clippy::inconsistent_struct_constructor)] // Field order is fixed by the public API map. #![allow(clippy::inherent_to_string)] // The mapped C# methods are named ToString. #![allow(clippy::inherent_to_string_shadow_display)] // Coord also needs Display for raw output. #![allow(clippy::many_single_char_names)] // Vector and UV formulas follow the reference notation. #![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility mapping. #![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped C# surface. #![allow(clippy::needless_pass_by_value)] // Owned value parameters preserve mapped signatures. #![allow(clippy::should_implement_trait)] // Operator entry points have fixed generated names. #![allow(clippy::similar_names)] // Profile start/stop and step names mirror the reference. #![allow(clippy::struct_excessive_bools)] // PrimMesh exposes the reference mode flags. #![allow(clippy::struct_field_names)] // Angle.angle is the corresponding C# field. #![allow(clippy::too_many_arguments)] // Constructor and extrusion helpers mirror fixed APIs. #![allow(clippy::too_many_lines)] // The extrusion sequence remains reviewable against C#. use crate::{Error, PathType, VertexIndexer}; use std::f32::consts::{PI, TAU}; use std::fmt; use std::fs::File; use std::io::{BufWriter, Write}; use std::path::PathBuf; const MAG_THRESHOLD: f32 = 0.000_000_1; const MAX_PROFILE_VERTICES: usize = 65_536; const MAX_PATH_NODES: usize = 65_536; const MAX_MESH_VERTICES: usize = 16_777_216; #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Coord { pub x: f32, pub y: f32, pub z: f32, } impl Coord { pub fn new(x: f32, y: f32, z: f32) -> Result { Ok(Self { x, y, z }) } pub fn length(&self) -> Result { Ok((self.x * self.x + self.y * self.y + self.z * self.z).sqrt()) } pub fn invert(&mut self) -> Result { self.x = -self.x; self.y = -self.y; self.z = -self.z; Ok(*self) } pub fn normalize(&mut self) -> Result { let mag = self.length()?; if mag > MAG_THRESHOLD { let inverse = 1.0 / mag; self.x *= inverse; self.y *= inverse; self.z *= inverse; } else { self.x = 0.0; self.y = 0.0; self.z = 0.0; } Ok(*self) } pub fn cross(c1: Self, c2: Self) -> Result { Ok(Self::cross_raw(c1, c2)) } fn cross_raw(c1: Self, c2: Self) -> Self { Self { x: c1.y * c2.z - c2.y * c1.z, y: c1.z * c2.x - c2.z * c1.x, z: c1.x * c2.y - c2.x * c1.y, } } pub fn add(v: Self, a: Self) -> Self { Self { x: v.x + a.x, y: v.y + a.y, z: v.z + a.z, } } pub fn mul_with_coord_coord(v: Self, m: Self) -> Self { Self { x: v.x * m.x, y: v.y * m.y, z: v.z * m.z, } } pub fn mul_with_coord_quat(v: Self, q: Quat) -> Self { Self { x: q.w * q.w * v.x + 2.0 * q.y * q.w * v.z - 2.0 * q.z * q.w * v.y + q.x * q.x * v.x + 2.0 * q.y * q.x * v.y + 2.0 * q.z * q.x * v.z - q.z * q.z * v.x - q.y * q.y * v.x, y: 2.0 * q.x * q.y * v.x + q.y * q.y * v.y + 2.0 * q.z * q.y * v.z + 2.0 * q.w * q.z * v.x - q.z * q.z * v.y + q.w * q.w * v.y - 2.0 * q.x * q.w * v.z - q.x * q.x * v.y, z: 2.0 * q.x * q.z * v.x + 2.0 * q.y * q.z * v.y + q.z * q.z * v.z - 2.0 * q.w * q.y * v.x - q.y * q.y * v.z + 2.0 * q.w * q.x * v.y - q.x * q.x * v.z + q.w * q.w * v.z, } } } impl fmt::Display for Coord { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} {} {}", self.x, self.y, self.z) } } impl Coord { pub fn to_string(&self) -> String { format!("{self}") } } #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Quat { pub w: f32, pub x: f32, pub y: f32, pub z: f32, } impl Quat { pub fn new_with_single_single_single_single( x: f32, y: f32, z: f32, w: f32, ) -> Result { Ok(Self { x, y, z, w }) } pub fn new_with_coord_single(mut axis: Coord, mut angle: f32) -> Result { axis.normalize()?; angle *= 0.5; let mut value = Self { x: axis.x * angle.sin(), y: axis.y * angle.sin(), z: axis.z * angle.sin(), w: angle.cos(), }; value.normalize()?; Ok(value) } pub fn length(&self) -> Result { Ok((self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt()) } pub fn normalize(&mut self) -> Result { let mag = self.length()?; if mag > MAG_THRESHOLD { let i = 1.0 / mag; self.x *= i; self.y *= i; self.z *= i; self.w *= i; } else { self.x = 0.0; self.y = 0.0; self.z = 0.0; self.w = 1.0; } Ok(*self) } pub fn mul(q1: Self, q2: Self) -> Self { Self { x: q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, y: q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, z: q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, w: q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z, } } pub fn to_string(&self) -> String { format!( "< X: {}, Y: {}, Z: {}, W: {}>", self.x, self.y, self.z, self.w ) } } #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct UVCoord { pub u: f32, pub v: f32, } impl UVCoord { pub fn new(u: f32, v: f32) -> Result { Ok(Self { u, v }) } pub fn flip(&mut self) -> Result { self.u = 1.0 - self.u; self.v = 1.0 - self.v; Ok(*self) } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Face { pub n1: i32, pub n2: i32, pub n3: i32, pub prim_face: i32, pub uv1: i32, pub uv2: i32, pub uv3: i32, pub v1: i32, pub v2: i32, pub v3: i32, } impl Face { fn vertices(v1: i32, v2: i32, v3: i32) -> Self { Self { v1, v2, v3, ..Self::default() } } pub fn new_with_int32_int32_int32(v1: i32, v2: i32, v3: i32) -> Result { Ok(Self::vertices(v1, v2, v3)) } pub fn new_with_int32_int32_int32_int32_int32_int32( v1: i32, v2: i32, v3: i32, n1: i32, n2: i32, n3: i32, ) -> Result { Ok(Self { v1, v2, v3, n1, n2, n3, ..Self::default() }) } pub fn surface_normal(&mut self, coords: Vec) -> Result { surface_normal_for(&coords, *self) } } #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct ViewerFace { pub coord_index1: i32, pub coord_index2: i32, pub coord_index3: i32, pub n1: Coord, pub n2: Coord, pub n3: Coord, pub prim_face_number: i32, pub uv1: UVCoord, pub uv2: UVCoord, pub uv3: UVCoord, pub v1: Coord, pub v2: Coord, pub v3: Coord, } impl ViewerFace { pub fn new(prim_face_number: i32) -> Result { Ok(Self { prim_face_number, coord_index1: -1, coord_index2: -1, coord_index3: -1, ..Self::default() }) } pub fn scale(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> { let m = Coord { x, y, z }; self.v1 = Coord::mul_with_coord_coord(self.v1, m); self.v2 = Coord::mul_with_coord_coord(self.v2, m); self.v3 = Coord::mul_with_coord_coord(self.v3, m); Ok(()) } pub fn add_pos(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> { let p = Coord { x, y, z }; self.v1 = Coord::add(self.v1, p); self.v2 = Coord::add(self.v2, p); self.v3 = Coord::add(self.v3, p); Ok(()) } pub fn add_rot(&mut self, q: Quat) -> Result<(), Error> { self.v1 = Coord::mul_with_coord_quat(self.v1, q); self.v2 = Coord::mul_with_coord_quat(self.v2, q); self.v3 = Coord::mul_with_coord_quat(self.v3, q); self.n1 = Coord::mul_with_coord_quat(self.n1, q); self.n2 = Coord::mul_with_coord_quat(self.n2, q); self.n3 = Coord::mul_with_coord_quat(self.n3, q); Ok(()) } pub fn calc_surface_normal(&mut self) -> Result<(), Error> { let e1 = Coord { x: self.v2.x - self.v1.x, y: self.v2.y - self.v1.y, z: self.v2.z - self.v1.z, }; let e2 = Coord { x: self.v3.x - self.v1.x, y: self.v3.y - self.v1.y, z: self.v3.z - self.v1.z, }; let mut n = Coord::cross_raw(e1, e2); n.normalize()?; self.n1 = n; self.n2 = n; self.n3 = n; Ok(()) } } #[derive(Clone, Copy, Debug, Default)] struct Angle { angle: f32, x: f32, y: f32, } fn make_angles(sides: i32, start: f32, stop: f32) -> Result<(Vec, Vec), Error> { if sides < 1 || stop <= start || !start.is_finite() || !stop.is_finite() { return Err(Error::Argument); } let sides_usize = usize::try_from(sides).map_err(|_| Error::Argument)?; if sides_usize > MAX_PROFILE_VERTICES { return Err(Error::Argument); } if matches!(sides, 3 | 4 | 24) { return make_special_angles(sides, start, stop); } let step = TAU / (sides as f32); let first = (start / step).floor() as i32; let last = ((stop / step).ceil() as i32).min(sides); let mut angles = Vec::with_capacity(usize::try_from(last - first + 1).map_err(|_| Error::Argument)?); for index in first..=last { let a = (index as f32) * step; angles.push(Angle { angle: a, x: a.cos(), y: a.sin(), }); } if start > angles.first().map_or(start, |a| a.angle) { let first = angles.first().copied().ok_or(Error::Argument)?; let second = angles.get(1).copied().ok_or(Error::Argument)?; angles[0] = intersect_angle(first, second, start); } if angles.len() > 1 { let end = angles.len() - 1; if stop < angles[end].angle { angles[end] = intersect_angle(angles[end - 1], angles[end], stop); } } let normals = if sides < 5 { angles .iter() .map(|a| { let previous = a.angle - step * 0.5; let mut n = Coord { x: previous.cos(), y: previous.sin(), z: 0.0, }; let _ = n.normalize(); n }) .collect() } else { Vec::new() }; Ok((angles, normals)) } fn make_special_angles( sides: i32, start: f32, stop: f32, ) -> Result<(Vec, Vec), Error> { let start = start / TAU; let stop = stop / TAU; let source: Vec = (0..=sides) .map(|index| { let angle = (index as f32) / (sides as f32); Angle { angle, x: (angle * TAU).cos(), y: (angle * TAU).sin(), } }) .collect(); let start_index = (start * sides as f32) as usize; let mut end_index = source.len() - 1; if stop < 1.0 { end_index = (stop * sides as f32) as usize + 1; } if end_index == start_index { end_index += 1; } let mut angles = source .get(start_index..=end_index) .ok_or(Error::Argument)? .to_vec(); let normal_source: Vec = if sides < 5 { (0..=sides) .map(|index| { let angle = ((index as f32) + 0.5) * TAU / sides as f32; Coord { x: angle.cos(), y: angle.sin(), z: 0.0, } }) .collect() } else { Vec::new() }; let normals = if sides < 5 { normal_source[start_index..=end_index].to_vec() } else { Vec::new() }; if start > 0.0 { let first = angles[0]; let second = angles[1]; angles[0] = interpolate_angle(start, first, second); } if stop < 1.0 { let last = angles.len() - 1; angles[last] = interpolate_angle(stop, angles[last - 1], angles[last]); } Ok((angles, normals)) } fn interpolate_angle(point: f32, first: Angle, second: Angle) -> Angle { let ratio = (point - first.angle) / (second.angle - first.angle); Angle { angle: point, x: first.x + ratio * (second.x - first.x), y: first.y + ratio * (second.y - first.y), } } fn intersect_angle(p1: Angle, p2: Angle, angle: f32) -> Angle { let x3 = 0.0_f64; let y3 = 0.0_f64; let x4 = f64::from(angle.cos()); let y4 = f64::from(angle.sin()); let x1 = f64::from(p1.x); let y1 = f64::from(p1.y); let x2 = f64::from(p2.x); let y2 = f64::from(p2.y); let denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); if denominator == 0.0 { return Angle { angle, x: p1.x, y: p1.y, }; } let numerator = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3); let u = numerator / denominator; Angle { angle, x: (x1 + u * (x2 - x1)) as f32, y: (y1 + u * (y2 - y1)) as f32, } } fn idx(value: i32, len: usize) -> Result { let value = usize::try_from(value).map_err(|_| Error::IndexOutOfRange)?; if value >= len { Err(Error::IndexOutOfRange) } else { Ok(value) } } fn surface_normal_for(coords: &[Coord], face: Face) -> Result { let c1 = coords[idx(face.v1, coords.len())?]; let c2 = coords[idx(face.v2, coords.len())?]; let c3 = coords[idx(face.v3, coords.len())?]; let e1 = Coord { x: c2.x - c1.x, y: c2.y - c1.y, z: c2.z - c1.z, }; let e2 = Coord { x: c3.x - c1.x, y: c3.y - c1.y, z: c3.z - c1.z, }; let mut n = Coord::cross_raw(e1, e2); n.normalize()?; Ok(n) } #[derive(Clone, Debug, Default)] pub struct Profile { pub bottom_face_number: i32, pub calc_vertex_normals: bool, pub coords: Vec, pub cut1_coord_indices: Vec, pub cut2_coord_indices: Vec, pub cut_normal1: Coord, pub cut_normal2: Coord, pub error_message: String, pub face_normal: Coord, pub face_numbers: Vec, pub face_u_vs: Vec, pub faces: Vec, pub hollow_coord_indices: Vec, pub hollow_face_number: i32, pub num_hollow_verts: i32, pub num_outer_verts: i32, pub num_prim_faces: i32, pub outer_coord_indices: Vec, pub outer_face_number: i32, pub us: Vec, pub vertex_normals: Vec, } impl Profile { pub fn new_with_constructor() -> Result { Ok(Self { face_normal: Coord { x: 0.0, y: 0.0, z: 1.0, }, hollow_face_number: -1, outer_face_number: -1, ..Self::default() }) } #[allow(clippy::too_many_arguments)] pub fn new_with_int32_single_single_single_int32_boolean_boolean( sides: i32, profile_start: f32, profile_end: f32, hollow: f32, hollow_sides: i32, create_faces: bool, calc_vertex_normals: bool, ) -> Result { let mut value = Self::new_with_constructor()?; value.calc_vertex_normals = calc_vertex_normals; let (angles, outer_normals) = make_angles(sides, profile_start * TAU, profile_end * TAU)?; let has_hollow = hollow > 0.0; let has_cut = profile_start > 0.0 || profile_end < 1.0; let simple = sides < 5 && !has_hollow && !has_cut; let (x_scale, y_scale) = if sides == 4 { (0.707_107, 0.707_107) } else { (0.5, 0.5) }; value.num_outer_verts = i32::try_from(angles.len()).map_err(|_| Error::Argument)?; let hollow_angles = if has_hollow { if hollow_sides == sides { angles.clone() } else { make_angles(hollow_sides, profile_start * TAU, profile_end * TAU)?.0 } } else { Vec::new() }; value.num_hollow_verts = i32::try_from(hollow_angles.len()).map_err(|_| Error::Argument)?; let total = angles .len() .checked_add(hollow_angles.len()) .and_then(|n| n.checked_add(1)) .ok_or(Error::Argument)?; if total > MAX_PROFILE_VERTICES { return Err(Error::Argument); } if !has_hollow && !simple { value.coords.push(Coord::default()); if calc_vertex_normals { value.vertex_normals.push(value.face_normal); } value.us.push(0.0); } for (i, angle) in angles.iter().enumerate() { value.coords.push(Coord { x: angle.x * x_scale, y: angle.y * y_scale, z: 0.0, }); if calc_vertex_normals { value .outer_coord_indices .push(i32::try_from(value.coords.len() - 1).map_err(|_| Error::Argument)?); value.vertex_normals.push(if sides < 5 { outer_normals.get(i).copied().unwrap_or(Coord { x: angle.x, y: angle.y, z: 0.0, }) } else { Coord { x: angle.x, y: angle.y, z: 0.0, } }); value.us.push(angle.angle); } if !has_hollow && !simple && create_faces && angle.angle > 0.0001 { let index = i32::try_from(i).map_err(|_| Error::Argument)?; value.faces.push(Face::vertices(0, index, index + 1)); } } if has_hollow { let mut inner = Vec::with_capacity(hollow_angles.len()); let mut inner_normals = Vec::with_capacity(hollow_angles.len()); let mut inner_us = Vec::with_capacity(hollow_angles.len()); for (i, angle) in hollow_angles.iter().enumerate() { inner.push(Coord { x: hollow * x_scale * angle.x, y: hollow * y_scale * angle.y, z: 0.0, }); if calc_vertex_normals { let mut normal = if hollow_sides < 5 { make_angles(hollow_sides, profile_start * TAU, profile_end * TAU)? .1 .get(i) .copied() .unwrap_or(Coord { x: angle.x, y: angle.y, z: 0.0, }) } else { Coord { x: angle.x, y: angle.y, z: 0.0, } }; normal.x = -normal.x; normal.y = -normal.y; normal.z = -normal.z; inner_normals.push(normal); inner_us.push(if hollow_sides == 4 { angle.angle * hollow * 0.707_107 } else { angle.angle * hollow }); } } inner.reverse(); inner_normals.reverse(); inner_us.reverse(); if create_faces { triangulate_ring(&angles, &hollow_angles, &mut value.faces)?; } for coord in inner { value.coords.push(coord); if calc_vertex_normals { value .hollow_coord_indices .push(i32::try_from(value.coords.len() - 1).map_err(|_| Error::Argument)?); } } value.vertex_normals.extend(inner_normals); value.us.extend(inner_us); } if simple && create_faces { if sides == 3 { value.faces.push(Face::vertices(0, 1, 2)); } else if sides == 4 { value.faces.push(Face::vertices(0, 1, 2)); value.faces.push(Face::vertices(0, 2, 3)); } } if calc_vertex_normals && has_cut { value.initialize_cut_normals(has_hollow)?; } value.make_face_u_vs()?; if calc_vertex_normals { value.assign_face_numbers(sides, has_hollow, has_cut); } Ok(value) } fn initialize_cut_normals(&mut self, has_hollow: bool) -> Result<(), Error> { let last_outer = usize::try_from(self.num_outer_verts - 1).map_err(|_| Error::Argument)?; if has_hollow { self.cut1_coord_indices.extend([ 0, i32::try_from(self.coords.len() - 1).map_err(|_| Error::Argument)?, ]); self.cut2_coord_indices.extend([ i32::try_from(last_outer + 1).map_err(|_| Error::Argument)?, i32::try_from(last_outer).map_err(|_| Error::Argument)?, ]); self.cut_normal1 = Coord { x: self.coords[0].y - self.coords[self.coords.len() - 1].y, y: -(self.coords[0].x - self.coords[self.coords.len() - 1].x), z: 0.0, }; self.cut_normal2 = Coord { x: self.coords[last_outer + 1].y - self.coords[last_outer].y, y: -(self.coords[last_outer + 1].x - self.coords[last_outer].x), z: 0.0, }; } else { self.cut1_coord_indices.extend([0, 1]); self.cut2_coord_indices .extend([i32::try_from(last_outer).map_err(|_| Error::Argument)?, 0]); let first = *self.vertex_normals.get(1).ok_or(Error::IndexOutOfRange)?; let last = *self .vertex_normals .get(self.vertex_normals.len().saturating_sub(2)) .ok_or(Error::IndexOutOfRange)?; self.cut_normal1 = Coord { x: first.y, y: -first.x, z: 0.0, }; self.cut_normal2 = Coord { x: -last.y, y: last.x, z: 0.0, }; } self.cut_normal1.normalize()?; self.cut_normal2.normalize()?; Ok(()) } fn assign_face_numbers(&mut self, sides: i32, has_hollow: bool, has_cut: bool) { let mut face = 1; self.outer_face_number = face; if has_cut && !has_hollow { self.face_numbers.push(-1); } for i in 0..self.num_outer_verts - 1 { if sides < 5 && i <= sides { self.face_numbers.push(face); face += 1; } else { self.face_numbers.push(face); } } if has_cut { self.face_numbers.push(-1); } else { self.face_numbers.push(face); face += 1; } if sides > 4 && (has_hollow || has_cut) { face += 1; } if sides < 5 && (has_hollow || has_cut) && self.num_outer_verts < sides { face += 1; } if has_hollow { for _ in 0..self.num_hollow_verts { self.face_numbers.push(face); } self.hollow_face_number = face; face += 1; } self.bottom_face_number = face; face += 1; if has_hollow && has_cut { self.face_numbers.push(face); face += 1; } for number in &mut self.face_numbers { if *number == -1 { *number = face; face += 1; } } self.num_prim_faces = face; } pub fn make_face_u_vs(&mut self) -> Result<(), Error> { self.face_u_vs = self .coords .iter() .map(|c| UVCoord { u: 1.0 - (0.5 + c.x), v: 1.0 - (0.5 - c.y), }) .collect(); Ok(()) } pub fn copy_with_method(&self) -> Result { self.copy_with_boolean(true) } pub fn copy_with_boolean(&self, need_faces: bool) -> Result { let mut copy = self.clone(); if !need_faces { copy.faces.clear(); } Ok(copy) } pub fn add_pos_with_coord(&mut self, v: Coord) -> Result<(), Error> { self.add_pos_with_single_single_single(v.x, v.y, v.z) } pub fn add_pos_with_single_single_single( &mut self, x: f32, y: f32, z: f32, ) -> Result<(), Error> { for c in &mut self.coords { c.x += x; c.y += y; c.z += z; } Ok(()) } pub fn add_rot(&mut self, q: Quat) -> Result<(), Error> { for c in &mut self.coords { *c = Coord::mul_with_coord_quat(*c, q); } if self.calc_vertex_normals { for n in &mut self.vertex_normals { *n = Coord::mul_with_coord_quat(*n, q); } self.face_normal = Coord::mul_with_coord_quat(self.face_normal, q); self.cut_normal1 = Coord::mul_with_coord_quat(self.cut_normal1, q); self.cut_normal2 = Coord::mul_with_coord_quat(self.cut_normal2, q); } Ok(()) } pub fn scale(&mut self, x: f32, y: f32) -> Result<(), Error> { for c in &mut self.coords { c.x *= x; c.y *= y; } Ok(()) } pub fn flip_normals(&mut self) -> Result<(), Error> { for f in &mut self.faces { std::mem::swap(&mut f.v1, &mut f.v3); } if self.calc_vertex_normals && let Some(n) = self.vertex_normals.last_mut() { n.z = -n.z; } self.face_normal.x = -self.face_normal.x; self.face_normal.y = -self.face_normal.y; self.face_normal.z = -self.face_normal.z; for uv in &mut self.face_u_vs { uv.v = 1.0 - uv.v; } Ok(()) } pub fn add_value2_face_vertex_indices(&mut self, num: i32) -> Result<(), Error> { for f in &mut self.faces { f.v1 = f.v1.checked_add(num).ok_or(Error::Argument)?; f.v2 = f.v2.checked_add(num).ok_or(Error::Argument)?; f.v3 = f.v3.checked_add(num).ok_or(Error::Argument)?; } Ok(()) } pub fn add_value2_face_normal_indices(&mut self, num: i32) -> Result<(), Error> { if self.calc_vertex_normals { for f in &mut self.faces { f.n1 = f.n1.checked_add(num).ok_or(Error::Argument)?; f.n2 = f.n2.checked_add(num).ok_or(Error::Argument)?; f.n3 = f.n3.checked_add(num).ok_or(Error::Argument)?; } } Ok(()) } pub fn dump_raw(&self, path: String, name: String, title: String) -> Result<(), Error> { dump_raw_geometry(&self.coords, &self.faces, path, name, title) } } fn triangulate_ring(outer: &[Angle], inner: &[Angle], faces: &mut Vec) -> Result<(), Error> { let outer_count = outer.len(); let inner_count = inner.len(); let total = outer_count .checked_add(inner_count) .ok_or(Error::Argument)?; if outer_count == inner_count { for i in 0..outer_count.saturating_sub(1) { faces.push(Face::vertices( i32::try_from(i).map_err(|_| Error::Argument)?, i32::try_from(i + 1).map_err(|_| Error::Argument)?, i32::try_from(total - i - 1).map_err(|_| Error::Argument)?, )); faces.push(Face::vertices( i32::try_from(i + 1).map_err(|_| Error::Argument)?, i32::try_from(total - i - 2).map_err(|_| Error::Argument)?, i32::try_from(total - i - 1).map_err(|_| Error::Argument)?, )); } return Ok(()); } let mut oi = 0usize; let mut hi = 0usize; while oi + 1 < outer_count || hi + 1 < inner_count { let outer_next = outer.get(oi + 1).map_or(f32::INFINITY, |a| a.angle); let inner_next = inner.get(hi + 1).map_or(f32::INFINITY, |a| a.angle); let inner_current = total - hi - 1; if outer_next <= inner_next { faces.push(Face::vertices( i32::try_from(oi).map_err(|_| Error::Argument)?, i32::try_from(oi + 1).map_err(|_| Error::Argument)?, i32::try_from(inner_current).map_err(|_| Error::Argument)?, )); oi += 1; } else { faces.push(Face::vertices( i32::try_from(oi).map_err(|_| Error::Argument)?, i32::try_from(inner_current - 1).map_err(|_| Error::Argument)?, i32::try_from(inner_current).map_err(|_| Error::Argument)?, )); hi += 1; } } Ok(()) } #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct PathNode { pub percent_of_path: f32, pub position: Coord, pub rotation: Quat, pub x_scale: f32, pub y_scale: f32, } #[derive(Clone, Debug)] pub struct Path { pub dimple_begin: f32, pub dimple_end: f32, pub hole_size_x: f32, pub hole_size_y: f32, pub path_cut_begin: f32, pub path_cut_end: f32, pub path_nodes: Vec, pub radius: f32, pub revolutions: f32, pub skew: f32, pub steps_per_revolution: i32, pub taper_x: f32, pub taper_y: f32, pub top_shear_x: f32, pub top_shear_y: f32, pub twist_begin: f32, pub twist_end: f32, } impl Default for Path { fn default() -> Self { Self { dimple_begin: 0.0, dimple_end: 1.0, hole_size_x: 1.0, hole_size_y: 0.25, path_cut_begin: 0.0, path_cut_end: 1.0, path_nodes: Vec::new(), radius: 0.0, revolutions: 1.0, skew: 0.0, steps_per_revolution: 24, taper_x: 0.0, taper_y: 0.0, top_shear_x: 0.0, top_shear_y: 0.0, twist_begin: 0.0, twist_end: 0.0, } } } impl Path { pub fn new() -> Result { Ok(Self::default()) } pub fn create(&mut self, path_type: PathType, mut steps: i32) -> Result<(), Error> { if steps < 1 || self.path_cut_end <= self.path_cut_begin || !self.path_cut_begin.is_finite() || !self.path_cut_end.is_finite() { return Err(Error::Argument); } self.taper_x = self.taper_x.clamp(-0.999, 0.999); self.taper_y = self.taper_y.clamp(-0.999, 0.999); self.path_nodes.clear(); if matches!(path_type, PathType::Linear | PathType::Flexible) { let length = self.path_cut_end - self.path_cut_begin; let twist_total = self.twist_end - self.twist_begin; if twist_total.abs() > 0.01 { steps = steps .checked_add((twist_total.abs() * 3.66) as i32) .ok_or(Error::Argument)?; } let count = usize::try_from(steps) .map_err(|_| Error::Argument)? .checked_add(1) .ok_or(Error::Argument)?; if count > MAX_PATH_NODES { return Err(Error::Argument); } self.path_nodes.reserve(count); let step_size = length / (steps as f32); let percent_step = step_size * 0.999_999; let mut percent = self.path_cut_begin; let mut x = self.top_shear_x * self.path_cut_begin; let mut y = self.top_shear_y * self.path_cut_begin; let mut z = -0.5 + percent; let dx = self.top_shear_x * length / (steps as f32); let dy = self.top_shear_y * length / (steps as f32); for step in 0..=steps { let x_scale = if self.taper_x == 0.0 { 1.0 } else if self.taper_x > 0.0 { 1.0 - percent * self.taper_x } else { 1.0 + (1.0 - percent) * self.taper_x }; let y_scale = if self.taper_y == 0.0 { 1.0 } else if self.taper_y > 0.0 { 1.0 - percent * self.taper_y } else { 1.0 + (1.0 - percent) * self.taper_y }; let twist = self.twist_begin + twist_total * percent; self.path_nodes.push(PathNode { percent_of_path: percent, position: Coord { x, y, z }, rotation: Quat::new_with_coord_single( Coord { x: 0.0, y: 0.0, z: 1.0, }, twist, )?, x_scale, y_scale, }); if step < steps { percent += percent_step; x += dx; y += dy; z += step_size; if percent > self.path_cut_end { break; } } } } else { if self.steps_per_revolution < 1 || self.revolutions <= 0.0 { return Err(Error::Argument); } let twist_total = self.twist_end - self.twist_begin; if twist_total.abs() > 0.01 { if twist_total.abs() > PI * 1.5 { steps.checked_mul(2).ok_or(Error::Argument)?; } if twist_total.abs() > PI * 3.0 { steps.checked_mul(4).ok_or(Error::Argument)?; } } let y_path_scale = self.hole_size_y * 0.5; let path_length = self.path_cut_end - self.path_cut_begin; let total_skew = self.skew * 2.0 * path_length; let skew_start = self.path_cut_begin * 2.0 * self.skew - self.skew; let x_shear = self.top_shear_x * (0.25 + 0.5 * (0.5 - self.hole_size_y)); let y_comp = 1.0 + self.top_shear_y.abs() * 0.25; let start = TAU * self.path_cut_begin * self.revolutions - self.top_shear_y * 0.9; let end = TAU * self.path_cut_end * self.revolutions - self.top_shear_y * 0.9; let step_size = TAU / (self.steps_per_revolution as f32); let estimated = ((end - start) / step_size).ceil() as usize + 2; if estimated > MAX_PATH_NODES { return Err(Error::Argument); } self.path_nodes.reserve(estimated); let mut step = (start / step_size) as i32; let mut angle = start; loop { let percent = angle / (TAU * self.revolutions); let angle_percent = (angle - start) / (end - start); let mut xs = (1.0 - self.skew.abs()) * self.hole_size_x; let mut ys = self.hole_size_y; if self.taper_x > 0.01 { xs *= 1.0 - percent * self.taper_x; } else if self.taper_x < -0.01 { xs *= 1.0 + (1.0 - percent) * self.taper_x; } if self.taper_y > 0.01 { ys *= 1.0 - percent * self.taper_y; } else if self.taper_y < -0.01 { ys *= 1.0 + (1.0 - percent) * self.taper_y; } let radius_scale = if self.radius > 0.001 { 1.0 - self.radius * percent } else if self.radius < 0.001 { 1.0 + self.radius * (1.0 - percent) } else { 1.0 }; let twist = self.twist_begin + twist_total * percent; let x = 0.5 * (skew_start + total_skew * angle_percent) + angle.sin() * x_shear; let y = y_comp * angle.cos() * (0.5 - y_path_scale) * radius_scale; let z = (angle + self.top_shear_y).sin() * (0.5 - y_path_scale) * radius_scale; let mut rotation = Quat::new_with_coord_single( Coord { x: 1.0, y: 0.0, z: 0.0, }, angle + self.top_shear_y, )?; if twist_total != 0.0 || self.twist_begin != 0.0 { rotation = Quat::mul( rotation, Quat::new_with_coord_single( Coord { x: 0.0, y: 0.0, z: 1.0, }, twist, )?, ); } self.path_nodes.push(PathNode { percent_of_path: percent, position: Coord { x, y, z }, rotation, x_scale: xs, y_scale: ys, }); if self.path_nodes.len() > MAX_PATH_NODES { return Err(Error::Argument); } if angle >= end - 0.01 { break; } step = step.checked_add(1).ok_or(Error::Argument)?; angle = step_size * (step as f32); if angle > end { angle = end; } } } Ok(()) } } #[derive(Clone, Debug)] pub struct PrimMesh { pub calc_vertex_normals: bool, pub coords: Vec, pub dimple_begin: f32, pub dimple_end: f32, pub error_message: String, pub faces: Vec, pub hole_size_x: f32, pub hole_size_y: f32, pub normals: Vec, pub num_prim_faces: i32, pub path_cut_begin: f32, pub path_cut_end: f32, pub radius: f32, pub revolutions: f32, pub skew: f32, pub sphere_mode: bool, pub steps_per_revolution: i32, pub taper_x: f32, pub taper_y: f32, pub top_shear_x: f32, pub top_shear_y: f32, pub twist_begin: i32, pub twist_end: i32, pub viewer_faces: Vec, pub viewer_mode: bool, sides: i32, profile_start: f32, profile_end: f32, hollow: f32, hollow_sides: i32, normals_processed: bool, profile_outer_face_number: i32, profile_hollow_face_number: i32, has_profile_cut: bool, has_hollow: bool, } impl PrimMesh { pub fn new( mut sides: i32, mut profile_start: f32, mut profile_end: f32, mut hollow: f32, mut hollow_sides: i32, ) -> Result { if !profile_start.is_finite() || !profile_end.is_finite() || !hollow.is_finite() { return Err(Error::Argument); } sides = sides.max(3); hollow_sides = hollow_sides.max(3); profile_start = profile_start.max(0.0); profile_end = profile_end.clamp(0.02, 1.0); if profile_start >= profile_end { profile_start = profile_end - 0.02; } hollow = hollow.clamp(0.0, 0.99); Ok(Self { calc_vertex_normals: false, coords: Vec::new(), dimple_begin: 0.0, dimple_end: 1.0, error_message: String::new(), faces: Vec::new(), hole_size_x: 1.0, hole_size_y: 0.25, normals: Vec::new(), num_prim_faces: 0, path_cut_begin: 0.0, path_cut_end: 1.0, radius: 0.0, revolutions: 1.0, skew: 0.0, sphere_mode: false, steps_per_revolution: 24, taper_x: 0.0, taper_y: 0.0, top_shear_x: 0.0, top_shear_y: 0.0, twist_begin: 0, twist_end: 0, viewer_faces: Vec::new(), viewer_mode: false, sides, profile_start, profile_end, hollow, hollow_sides, normals_processed: false, profile_outer_face_number: -1, profile_hollow_face_number: -1, has_profile_cut: profile_start > 0.0 || profile_end < 1.0, has_hollow: hollow > 0.0, }) } pub fn extrude(&mut self, path_type: PathType) -> Result<(), Error> { self.validate_parameters()?; self.coords.clear(); self.faces.clear(); self.normals.clear(); self.viewer_faces.clear(); self.normals_processed = false; if self.viewer_mode { self.calc_vertex_normals = true; } let twist_begin = self.twist_begin as f32; let twist_end = self.twist_end as f32; let twist_total = twist_end - twist_begin; let mut steps: i32 = 1; if twist_total.abs() > 0.01 { steps = steps .checked_add((twist_total.abs() * 3.66) as i32) .ok_or(Error::Argument)?; } let need_end_faces = if path_type == PathType::Circular { self.path_cut_begin != 0.0 || self.path_cut_end != 1.0 || self.taper_x != 0.0 || self.taper_y != 0.0 || self.skew != 0.0 || twist_total != 0.0 || self.radius != 0.0 } else { true }; let mut adjusted_hollow = self.hollow; let mut initial_rotation = 0.0; if path_type == PathType::Circular { match self.sides { 3 => { initial_rotation = PI; if self.hollow_sides == 4 { adjusted_hollow = adjusted_hollow.min(0.7) * 0.707; } else { adjusted_hollow *= 0.5; } } 4 => { initial_rotation = 0.25 * PI; if self.hollow_sides != 4 { adjusted_hollow *= 0.707; } } _ if self.sides > 4 => { initial_rotation = PI; if self.hollow_sides == 4 { adjusted_hollow = adjusted_hollow.min(0.7) / 0.7; } } _ => {} } } else { match self.sides { 3 => { if self.hollow_sides == 4 { adjusted_hollow = adjusted_hollow.min(0.7) * 0.707; } else { adjusted_hollow *= 0.5; } } 4 => { initial_rotation = 1.25 * PI; if self.hollow_sides != 4 { adjusted_hollow *= 0.707; } } 24 if self.hollow_sides == 4 => adjusted_hollow *= 1.414, _ => {} } } let mut profile = Profile::new_with_int32_single_single_single_int32_boolean_boolean( self.sides, self.profile_start, self.profile_end, adjusted_hollow, self.hollow_sides, true, self.calc_vertex_normals, )?; self.error_message.clone_from(&profile.error_message); let mut cut1_face = profile.bottom_face_number + 1; let mut cut2_face = cut1_face + 1; if !need_end_faces { cut1_face -= 2; cut2_face -= 2; } self.profile_outer_face_number = profile.outer_face_number - i32::from(!need_end_faces); self.profile_hollow_face_number = if self.has_hollow { profile.hollow_face_number - i32::from(!need_end_faces) } else { -1 }; let cut1_vert = if self.has_profile_cut { if self.has_hollow { i32::try_from(profile.coords.len() - 1).map_err(|_| Error::Argument)? } else { 0 } } else { -1 }; let cut2_vert = if self.has_profile_cut { if self.has_hollow { profile.num_outer_verts - 1 } else { profile.num_outer_verts } } else { -1 }; if initial_rotation != 0.0 { profile.add_rot(Quat::new_with_coord_single( Coord { x: 0.0, y: 0.0, z: 1.0, }, initial_rotation, )?)?; if self.viewer_mode { profile.make_face_u_vs()?; } } let mut path = Path { twist_begin, twist_end, top_shear_x: self.top_shear_x, top_shear_y: self.top_shear_y, path_cut_begin: self.path_cut_begin, path_cut_end: self.path_cut_end, dimple_begin: self.dimple_begin, dimple_end: self.dimple_end, skew: self.skew, hole_size_x: self.hole_size_x, hole_size_y: self.hole_size_y, taper_x: self.taper_x, taper_y: self.taper_y, radius: self.radius, revolutions: self.revolutions, steps_per_revolution: self.steps_per_revolution, ..Path::default() }; path.create(path_type, steps)?; let total_vertices = path .path_nodes .len() .checked_mul(profile.coords.len()) .ok_or(Error::Argument)?; if total_vertices > MAX_MESH_VERTICES { return Err(Error::Argument); } self.coords.reserve(total_vertices); let mut last_cut1 = Coord::default(); let mut last_cut2 = Coord::default(); let mut last_v = 0.0; for (node_index, node) in path.path_nodes.iter().copied().enumerate() { let coords_offset = self.coords.len(); let normals_offset = self.normals.len(); let transformed: Vec = profile .coords .iter() .map(|v| { let scaled = Coord { x: v.x * node.x_scale, y: v.y * node.y_scale, z: v.z, }; Coord::add( Coord::mul_with_coord_quat(scaled, node.rotation), node.position, ) }) .collect(); self.coords.extend_from_slice(&transformed); if self.calc_vertex_normals { self.normals.extend( profile .vertex_normals .iter() .map(|n| Coord::mul_with_coord_quat(*n, node.rotation)), ); } if node.percent_of_path < self.path_cut_begin + 0.01 || node.percent_of_path > self.path_cut_end - 0.01 { for source in &profile.faces { let mut face = *source; face.v1 = add_index(face.v1, coords_offset)?; face.v2 = add_index(face.v2, coords_offset)?; face.v3 = add_index(face.v3, coords_offset)?; if self.calc_vertex_normals { face.n1 = add_index(face.n1, normals_offset)?; face.n2 = add_index(face.n2, normals_offset)?; face.n3 = add_index(face.n3, normals_offset)?; } self.faces.push(face); } } let this_v = 1.0 - node.percent_of_path; if node_index > 0 { self.add_side_faces( &profile, coords_offset, this_v, last_v, cut1_vert, cut2_vert, cut1_face, cut2_face, need_end_faces, last_cut1, last_cut2, node.rotation, )?; } last_cut1 = profile.cut_normal1; last_cut2 = profile.cut_normal2; last_v = this_v; if need_end_faces && node_index + 1 == path.path_nodes.len() && self.viewer_mode { for face in &profile.faces { let v1 = idx(face.v1, transformed.len())?; let v2 = idx(face.v2, transformed.len())?; let v3 = idx(face.v3, transformed.len())?; let mut viewer = ViewerFace::new(0)?; viewer.v1 = transformed[v1]; viewer.v2 = transformed[v2]; viewer.v3 = transformed[v3]; viewer.coord_index1 = add_index(face.v1, coords_offset)?; viewer.coord_index2 = add_index(face.v2, coords_offset)?; viewer.coord_index3 = add_index(face.v3, coords_offset)?; let normal = profile.face_normal; viewer.n1 = normal; viewer.n2 = normal; viewer.n3 = normal; viewer.uv1 = profile.face_u_vs[v1]; viewer.uv2 = profile.face_u_vs[v2]; viewer.uv3 = profile.face_u_vs[v3]; if path_type == PathType::Linear { viewer.uv1.flip()?; viewer.uv2.flip()?; viewer.uv3.flip()?; } self.viewer_faces.push(viewer); } } } if self.viewer_mode { self.num_prim_faces = self .viewer_faces .iter() .map(|face| face.prim_face_number) .max() .unwrap_or(-1) + 1; } else { self.num_prim_faces = profile.num_prim_faces; } self.validate_geometry()?; Ok(()) } #[allow(clippy::too_many_arguments)] fn add_side_faces( &mut self, profile: &Profile, offset: usize, this_v: f32, last_v: f32, cut1: i32, cut2: i32, cut1_face: i32, cut2_face: i32, need_end_faces: bool, last_cut1: Coord, last_cut2: Coord, rotation: Quat, ) -> Result<(), Error> { let count = profile.coords.len(); let mut start = offset.checked_add(1).ok_or(Error::Argument)?; if self.sides < 5 || self.has_profile_cut || self.has_hollow { start -= 1; } let end = offset.checked_add(count).ok_or(Error::Argument)?; for current in start..end { let next = if current + 1 == end { start } else { current + 1 }; let previous = current.checked_sub(count).ok_or(Error::IndexOutOfRange)?; let previous_next = next.checked_sub(count).ok_or(Error::IndexOutOfRange)?; let f1 = Face { v1: to_i32(current)?, v2: to_i32(previous)?, v3: to_i32(next)?, n1: to_i32(current)?, n2: to_i32(previous)?, n3: to_i32(next)?, ..Face::default() }; let f2 = Face { v1: to_i32(next)?, v2: to_i32(previous)?, v3: to_i32(previous_next)?, n1: to_i32(next)?, n2: to_i32(previous)?, n3: to_i32(previous_next)?, ..Face::default() }; self.faces.push(f1); self.faces.push(f2); if !self.viewer_mode { continue; } let which = i32::try_from(current - start).map_err(|_| Error::Argument)?; let mut prim = profile .face_numbers .get(usize::try_from(which).map_err(|_| Error::Argument)?) .copied() .or_else(|| profile.face_numbers.first().copied()) .unwrap_or(0); if !need_end_faces { prim = (prim - 1).max(0); } let mut vf1 = ViewerFace::new(prim)?; let mut vf2 = ViewerFace::new(prim)?; let mut u_index = usize::try_from(which).map_err(|_| Error::Argument)?; if !self.has_hollow && self.sides > 4 && u_index < profile.us.len().saturating_sub(1) { u_index += 1; } let (mut u1, mut u2) = if profile.us.is_empty() { (0.0, 0.0) } else { u_index = u_index.min(profile.us.len() - 1); ( profile.us[u_index], profile.us.get(u_index + 1).copied().unwrap_or(1.0), ) }; if which == cut1 || which == cut2 { u1 = 0.0; u2 = 1.0; } else if self.sides < 5 && which < profile.num_outer_verts { u1 *= self.sides as f32; u2 *= self.sides as f32; u2 -= u1 as i32 as f32; u1 -= u1 as i32 as f32; if u2 < 0.1 { u2 = 1.0; } } if self.sphere_mode && which != cut1 && which != cut2 { u1 = u1 * 2.0 - 1.0; u2 = u2 * 2.0 - 1.0; if which >= profile.num_outer_verts { u1 -= self.hollow; u2 -= self.hollow; } } vf1.uv1 = UVCoord { u: u1, v: this_v }; vf1.uv2 = UVCoord { u: u1, v: last_v }; vf1.uv3 = UVCoord { u: u2, v: this_v }; vf2.uv1 = UVCoord { u: u2, v: this_v }; vf2.uv2 = UVCoord { u: u1, v: last_v }; vf2.uv3 = UVCoord { u: u2, v: last_v }; assign_viewer_coords(&mut vf1, &self.coords, f1)?; assign_viewer_coords(&mut vf2, &self.coords, f2)?; if which == cut1 { let current_normal = Coord::mul_with_coord_quat(profile.cut_normal1, rotation); vf1.prim_face_number = cut1_face; vf2.prim_face_number = cut1_face; vf1.n1 = current_normal; vf1.n2 = last_cut1; vf1.n3 = last_cut1; vf2.n1 = current_normal; vf2.n2 = last_cut1; vf2.n3 = current_normal; } else if which == cut2 { let current_normal = Coord::mul_with_coord_quat(profile.cut_normal2, rotation); vf1.prim_face_number = cut2_face; vf2.prim_face_number = cut2_face; vf1.n1 = current_normal; vf1.n2 = last_cut2; vf1.n3 = last_cut2; vf2.n1 = current_normal; vf2.n2 = last_cut2; vf2.n3 = current_normal; } else if (self.sides < 5 && which < profile.num_outer_verts) || (self.hollow_sides < 5 && which >= profile.num_outer_verts) { vf1.calc_surface_normal()?; vf2.calc_surface_normal()?; } else { assign_viewer_normals(&mut vf1, &self.normals, f1)?; assign_viewer_normals(&mut vf2, &self.normals, f2)?; } self.viewer_faces.push(vf1); self.viewer_faces.push(vf2); } Ok(()) } fn validate_parameters(&self) -> Result<(), Error> { let values = [ self.dimple_begin, self.dimple_end, self.hole_size_x, self.hole_size_y, self.path_cut_begin, self.path_cut_end, self.radius, self.revolutions, self.skew, self.taper_x, self.taper_y, self.top_shear_x, self.top_shear_y, ]; if values.iter().any(|v| !v.is_finite()) || self.path_cut_end <= self.path_cut_begin { return Err(Error::Argument); } Ok(()) } fn validate_geometry(&self) -> Result<(), Error> { if self .coords .iter() .any(|c| !c.x.is_finite() || !c.y.is_finite() || !c.z.is_finite()) || self .normals .iter() .any(|c| !c.x.is_finite() || !c.y.is_finite() || !c.z.is_finite()) { return Err(Error::InvalidOperation); } for face in &self.faces { idx(face.v1, self.coords.len())?; idx(face.v2, self.coords.len())?; idx(face.v3, self.coords.len())?; } Ok(()) } pub fn surface_normal(&self, face_index: i32) -> Result { let face = *self .faces .get(idx(face_index, self.faces.len())?) .ok_or(Error::IndexOutOfRange)?; surface_normal_for(&self.coords, face) } pub fn calc_normals(&mut self) -> Result<(), Error> { if self.normals_processed { return Ok(()); } self.normals_processed = true; if !self.calc_vertex_normals { self.normals = Vec::with_capacity(self.faces.len()); } for index in 0..self.faces.len() { let normal = self.surface_normal(i32::try_from(index).map_err(|_| Error::Argument)?)?; self.normals.push(normal); let normal_index = i32::try_from(self.normals.len() - 1).map_err(|_| Error::Argument)?; self.faces[index].n1 = normal_index; self.faces[index].n2 = normal_index; self.faces[index].n3 = normal_index; } Ok(()) } pub fn copy(&self) -> Result { Ok(self.clone()) } pub fn add_pos(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> { for c in &mut self.coords { c.x += x; c.y += y; c.z += z; } for face in &mut self.viewer_faces { face.add_pos(x, y, z)?; } Ok(()) } pub fn add_rot(&mut self, q: Quat) -> Result<(), Error> { for c in &mut self.coords { *c = Coord::mul_with_coord_quat(*c, q); } for n in &mut self.normals { *n = Coord::mul_with_coord_quat(*n, q); } for face in &mut self.viewer_faces { face.add_rot(q)?; } Ok(()) } pub fn scale(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> { let m = Coord { x, y, z }; for c in &mut self.coords { *c = Coord::mul_with_coord_coord(*c, m); } for face in &mut self.viewer_faces { face.scale(x, y, z)?; } Ok(()) } pub fn get_vertex_indexer(&self) -> Result, Error> { if self.viewer_mode && !self.viewer_faces.is_empty() { Ok(Some(VertexIndexer::from_viewer_faces(&self.viewer_faces)?)) } else { Ok(None) } } pub fn dump_raw(&self, path: String, name: String, title: String) -> Result<(), Error> { dump_raw_geometry(&self.coords, &self.faces, path, name, title) } pub fn profile_outer_face_number(&self) -> i32 { self.profile_outer_face_number } pub fn set_profile_outer_face_number(&mut self, value: i32) { self.profile_outer_face_number = value; } pub fn profile_hollow_face_number(&self) -> i32 { self.profile_hollow_face_number } pub fn set_profile_hollow_face_number(&mut self, value: i32) { self.profile_hollow_face_number = value; } pub fn has_profile_cut(&self) -> bool { self.has_profile_cut } pub fn set_has_profile_cut(&mut self, value: bool) { self.has_profile_cut = value; } pub fn has_hollow(&self) -> bool { self.has_hollow } pub fn set_has_hollow(&mut self, value: bool) { self.has_hollow = value; } pub fn params_to_display_string(&self) -> Result { Ok(format!( "sides..................: {}\nhollowSides..........: {}\nprofileStart.........: {}\nprofileEnd...........: {}\nhollow...............: {}\ntwistBegin...........: {}\ntwistEnd.............: {}\ntopShearX............: {}\ntopShearY............: {}\npathCutBegin.........: {}\npathCutEnd...........: {}\ndimpleBegin..........: {}\ndimpleEnd............: {}\nskew.................: {}\nholeSizeX............: {}\nholeSizeY............: {}\ntaperX...............: {}\ntaperY...............: {}\nradius...............: {}\nrevolutions..........: {}\nstepsPerRevolution...: {}\nsphereMode...........: {}\nhasProfileCut........: {}\nhasHollow............: {}\nviewerMode...........: {}", self.sides, self.hollow_sides, self.profile_start, self.profile_end, self.hollow, self.twist_begin, self.twist_end, self.top_shear_x, self.top_shear_y, self.path_cut_begin, self.path_cut_end, self.dimple_begin, self.dimple_end, self.skew, self.hole_size_x, self.hole_size_y, self.taper_x, self.taper_y, self.radius, self.revolutions, self.steps_per_revolution, self.sphere_mode, self.has_profile_cut, self.has_hollow, self.viewer_mode )) } } fn to_i32(value: usize) -> Result { i32::try_from(value).map_err(|_| Error::Argument) } fn add_index(value: i32, offset: usize) -> Result { value.checked_add(to_i32(offset)?).ok_or(Error::Argument) } fn assign_viewer_coords( viewer: &mut ViewerFace, coords: &[Coord], face: Face, ) -> Result<(), Error> { viewer.v1 = coords[idx(face.v1, coords.len())?]; viewer.v2 = coords[idx(face.v2, coords.len())?]; viewer.v3 = coords[idx(face.v3, coords.len())?]; viewer.coord_index1 = face.v1; viewer.coord_index2 = face.v2; viewer.coord_index3 = face.v3; Ok(()) } fn assign_viewer_normals( viewer: &mut ViewerFace, normals: &[Coord], face: Face, ) -> Result<(), Error> { if normals.is_empty() { return viewer.calc_surface_normal(); } viewer.n1 = normals[idx(face.n1, normals.len())?]; viewer.n2 = normals[idx(face.n2, normals.len())?]; viewer.n3 = normals[idx(face.n3, normals.len())?]; Ok(()) } pub(crate) fn dump_raw_geometry( coords: &[Coord], faces: &[Face], path: String, name: String, title: String, ) -> Result<(), Error> { let mut target = PathBuf::from(path); target.push(format!("{name}_{title}.raw")); let file = File::create(target).map_err(|_| Error::InvalidOperation)?; let mut writer = BufWriter::new(file); for face in faces { let a = coords[idx(face.v1, coords.len())?]; let b = coords[idx(face.v2, coords.len())?]; let c = coords[idx(face.v3, coords.len())?]; writeln!(writer, "{a} {b} {c}").map_err(|_| Error::InvalidOperation)?; } writer.flush().map_err(|_| Error::InvalidOperation) } #[cfg(test)] mod tests { use super::*; fn mesh(sides: i32, hollow: f32, viewer: bool) -> PrimMesh { let mut mesh = PrimMesh::new(sides, 0.0, 1.0, hollow, 4).expect("valid mesh"); mesh.viewer_mode = viewer; mesh.calc_vertex_normals = viewer; mesh } #[test] fn golden_box_and_torus_summaries_match_reference() { let mut box_mesh = mesh(4, 0.0, true); box_mesh.extrude(PathType::Linear).expect("box extrusion"); assert_eq!( ( box_mesh.coords.len(), box_mesh.faces.len(), box_mesh.viewer_faces.len(), box_mesh.num_prim_faces, ), (10, 14, 12, 6) ); let mut torus = mesh(24, 0.0, true); torus.hole_size_x = 1.0; torus.hole_size_y = 0.25; torus.extrude(PathType::Circular).expect("torus extrusion"); assert_eq!( ( torus.coords.len(), torus.faces.len(), torus.viewer_faces.len(), torus.num_prim_faces, ), (650, 1248, 1200, 1) ); } #[test] fn special_profile_tables_keep_normalized_u_coordinates() { let profile = Profile::new_with_int32_single_single_single_int32_boolean_boolean( 4, 0.0, 1.0, 0.0, 4, true, true, ) .expect("square profile"); assert_eq!(profile.us, vec![0.0, 0.25, 0.5, 0.75, 1.0]); assert!((profile.vertex_normals[0].x - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-6); assert!((profile.vertex_normals[0].y - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-6); } #[test] fn complex_circular_extrusion_is_deterministic_finite_and_bounded() { fn build() -> PrimMesh { let mut mesh = PrimMesh::new(8, 0.1, 0.85, 0.35, 4).expect("valid mesh"); mesh.viewer_mode = true; mesh.calc_vertex_normals = true; mesh.twist_begin = -1; mesh.twist_end = 2; mesh.top_shear_x = 0.2; mesh.top_shear_y = -0.15; mesh.path_cut_begin = 0.05; mesh.path_cut_end = 0.9; mesh.skew = 0.15; mesh.taper_x = 0.25; mesh.taper_y = -0.2; mesh.radius = 0.2; mesh.revolutions = 1.5; mesh.steps_per_revolution = 32; mesh.extrude(PathType::Circular).expect("complex extrusion"); mesh } let first = build(); let second = build(); assert_eq!(first.coords, second.coords); assert_eq!(first.faces, second.faces); assert_eq!(first.viewer_faces, second.viewer_faces); for face in &first.faces { for index in [face.v1, face.v2, face.v3] { assert!(usize::try_from(index).is_ok_and(|index| index < first.coords.len())); } } for coord in &first.coords { assert!(coord.x.is_finite() && coord.y.is_finite() && coord.z.is_finite()); } for face in &first.viewer_faces { for value in [ face.uv1.u, face.uv1.v, face.uv2.u, face.uv2.v, face.uv3.u, face.uv3.v, ] { assert!(value.is_finite()); } } } #[test] fn excessive_profile_and_path_sizes_are_rejected() { assert!(matches!( Profile::new_with_int32_single_single_single_int32_boolean_boolean( i32::try_from(MAX_PROFILE_VERTICES + 1).expect("test limit"), 0.0, 1.0, 0.0, 4, true, false, ), Err(Error::Argument) )); let mut path = Path::new().expect("path"); assert!(matches!( path.create( PathType::Linear, i32::try_from(MAX_PATH_NODES).expect("test limit") ), Err(Error::Argument) )); } }