2013 lines
68 KiB
Rust
2013 lines
68 KiB
Rust
//! 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<Self, Error> {
|
|
Ok(Self { x, y, z })
|
|
}
|
|
pub fn length(&self) -> Result<f32, Error> {
|
|
Ok((self.x * self.x + self.y * self.y + self.z * self.z).sqrt())
|
|
}
|
|
pub fn invert(&mut self) -> Result<Self, Error> {
|
|
self.x = -self.x;
|
|
self.y = -self.y;
|
|
self.z = -self.z;
|
|
Ok(*self)
|
|
}
|
|
pub fn normalize(&mut self) -> Result<Self, Error> {
|
|
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<Self, Error> {
|
|
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<Self, Error> {
|
|
Ok(Self { x, y, z, w })
|
|
}
|
|
pub fn new_with_coord_single(mut axis: Coord, mut angle: f32) -> Result<Self, Error> {
|
|
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<f32, Error> {
|
|
Ok((self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt())
|
|
}
|
|
pub fn normalize(&mut self) -> Result<Self, Error> {
|
|
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<Self, Error> {
|
|
Ok(Self { u, v })
|
|
}
|
|
pub fn flip(&mut self) -> Result<Self, Error> {
|
|
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<Self, Error> {
|
|
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<Self, Error> {
|
|
Ok(Self {
|
|
v1,
|
|
v2,
|
|
v3,
|
|
n1,
|
|
n2,
|
|
n3,
|
|
..Self::default()
|
|
})
|
|
}
|
|
pub fn surface_normal(&mut self, coords: Vec<Coord>) -> Result<Coord, Error> {
|
|
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<Self, Error> {
|
|
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<Angle>, Vec<Coord>), 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<Angle>, Vec<Coord>), Error> {
|
|
let start = start / TAU;
|
|
let stop = stop / TAU;
|
|
let source: Vec<Angle> = (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<Coord> = 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<usize, Error> {
|
|
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<Coord, Error> {
|
|
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<Coord>,
|
|
pub cut1_coord_indices: Vec<i32>,
|
|
pub cut2_coord_indices: Vec<i32>,
|
|
pub cut_normal1: Coord,
|
|
pub cut_normal2: Coord,
|
|
pub error_message: String,
|
|
pub face_normal: Coord,
|
|
pub face_numbers: Vec<i32>,
|
|
pub face_u_vs: Vec<UVCoord>,
|
|
pub faces: Vec<Face>,
|
|
pub hollow_coord_indices: Vec<i32>,
|
|
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<i32>,
|
|
pub outer_face_number: i32,
|
|
pub us: Vec<f32>,
|
|
pub vertex_normals: Vec<Coord>,
|
|
}
|
|
|
|
impl Profile {
|
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
|
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<Self, Error> {
|
|
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, Error> {
|
|
self.copy_with_boolean(true)
|
|
}
|
|
pub fn copy_with_boolean(&self, need_faces: bool) -> Result<Self, Error> {
|
|
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<Face>) -> 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<PathNode>,
|
|
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<Self, Error> {
|
|
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<Coord>,
|
|
pub dimple_begin: f32,
|
|
pub dimple_end: f32,
|
|
pub error_message: String,
|
|
pub faces: Vec<Face>,
|
|
pub hole_size_x: f32,
|
|
pub hole_size_y: f32,
|
|
pub normals: Vec<Coord>,
|
|
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<ViewerFace>,
|
|
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<Self, Error> {
|
|
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<Coord> = 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<Coord, Error> {
|
|
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<Self, Error> {
|
|
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<Option<VertexIndexer>, 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<String, Error> {
|
|
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, Error> {
|
|
i32::try_from(value).map_err(|_| Error::Argument)
|
|
}
|
|
fn add_index(value: i32, offset: usize) -> Result<i32, Error> {
|
|
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)
|
|
));
|
|
}
|
|
}
|