use crate::entities::{Entity, EntityCommon};
use crate::types::{BoundingBox3D, Color, Handle, LineWeight, Transparency, Vector3};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Tolerance {
pub common: EntityCommon,
pub insertion_point: Vector3,
pub direction: Vector3,
pub normal: Vector3,
pub text: String,
pub dimension_style_name: String,
pub dimension_style_handle: Option<Handle>,
pub text_height: f64,
pub dimension_gap: f64,
}
impl Tolerance {
pub const ENTITY_NAME: &'static str = "TOLERANCE";
pub const SUBCLASS_MARKER: &'static str = "AcDbFcf";
pub const OBJECT_TYPE: u16 = 0x2E;
pub fn new() -> Self {
Tolerance {
common: EntityCommon::default(),
insertion_point: Vector3::ZERO,
direction: Vector3::UNIT_X,
normal: Vector3::UNIT_Z,
text: String::new(),
dimension_style_name: "Standard".to_string(),
dimension_style_handle: None,
text_height: 0.18,
dimension_gap: 0.09,
}
}
pub fn with_text(insertion_point: Vector3, text: impl Into<String>) -> Self {
Tolerance {
insertion_point,
text: text.into(),
..Self::new()
}
}
pub fn new_full(
insertion_point: Vector3,
text: impl Into<String>,
direction: Vector3,
style: impl Into<String>,
) -> Self {
Tolerance {
insertion_point,
text: text.into(),
direction: direction.normalize(),
dimension_style_name: style.into(),
..Self::new()
}
}
pub fn point_toward(&mut self, target: Vector3) {
let dir = target - self.insertion_point;
if dir.length() > 1e-10 {
self.direction = dir.normalize();
}
}
pub fn rotate(&mut self, angle: f64) {
let cos_a = angle.cos();
let sin_a = angle.sin();
let n = self.normal.normalize();
let d = self.direction;
let dot = n.x * d.x + n.y * d.y + n.z * d.z;
let cross = Vector3::new(
n.y * d.z - n.z * d.y,
n.z * d.x - n.x * d.z,
n.x * d.y - n.y * d.x,
);
self.direction = Vector3::new(
d.x * cos_a + cross.x * sin_a + n.x * dot * (1.0 - cos_a),
d.y * cos_a + cross.y * sin_a + n.y * dot * (1.0 - cos_a),
d.z * cos_a + cross.z * sin_a + n.z * dot * (1.0 - cos_a),
);
}
pub fn rotation_angle(&self) -> f64 {
let n = self.normal.normalize();
let d = self.direction.normalize();
let dot = n.x * d.x + n.y * d.y + n.z * d.z;
let projected = Vector3::new(d.x - n.x * dot, d.y - n.y * dot, d.z - n.z * dot);
if projected.length() < 1e-10 {
return 0.0;
}
let x_axis = if (n.z.abs() - 1.0).abs() < 1e-10 {
Vector3::UNIT_X
} else {
Vector3::new(-n.y, n.x, 0.0).normalize()
};
let y_axis = Vector3::new(
n.y * x_axis.z - n.z * x_axis.y,
n.z * x_axis.x - n.x * x_axis.z,
n.x * x_axis.y - n.y * x_axis.x,
);
let px = projected.x * x_axis.x + projected.y * x_axis.y + projected.z * x_axis.z;
let py = projected.x * y_axis.x + projected.y * y_axis.y + projected.z * y_axis.z;
py.atan2(px)
}
pub fn text_lines(&self) -> Vec<&str> {
self.text.split("^J").collect()
}
pub fn line_count(&self) -> usize {
self.text.matches("^J").count() + 1
}
pub fn position_tolerance(
tolerance: f64,
datum_a: Option<&str>,
datum_b: Option<&str>,
datum_c: Option<&str>,
) -> String {
let mut text = format!("{{\\Fgdt;p}}%%v{}%%v", tolerance);
if let Some(a) = datum_a {
text.push_str(a);
}
text.push_str("%%v");
if let Some(b) = datum_b {
text.push_str(b);
}
text.push_str("%%v");
if let Some(c) = datum_c {
text.push_str(c);
}
text.push_str("%%v");
text
}
pub fn flatness_tolerance(tolerance: f64) -> String {
format!("{{\\Fgdt;c}}%%v{}%%v%%v%%v%%v", tolerance)
}
pub fn perpendicularity_tolerance(tolerance: f64, datum: &str) -> String {
format!("{{\\Fgdt;j}}%%v{}%%v{}%%v%%v%%v", tolerance, datum)
}
pub fn parallelism_tolerance(tolerance: f64, datum: &str) -> String {
format!("{{\\Fgdt;h}}%%v{}%%v{}%%v%%v%%v", tolerance, datum)
}
pub fn concentricity_tolerance(tolerance: f64, datum: &str) -> String {
format!("{{\\Fgdt;u}}%%v{}%%v{}%%v%%v%%v", tolerance, datum)
}
pub fn symmetry_tolerance(tolerance: f64, datum: &str) -> String {
format!("{{\\Fgdt;i}}%%v{}%%v{}%%v%%v%%v", tolerance, datum)
}
pub fn runout_tolerance(tolerance: f64, datum: &str, total: bool) -> String {
let symbol = if total { "t" } else { "r" };
format!("{{\\Fgdt;{}}}%%v{}%%v{}%%v%%v%%v", symbol, tolerance, datum)
}
pub fn cylindricity_tolerance(tolerance: f64) -> String {
format!("{{\\Fgdt;e}}%%v{}%%v%%v%%v%%v", tolerance)
}
pub fn straightness_tolerance(tolerance: f64) -> String {
format!("{{\\Fgdt;a}}%%v{}%%v%%v%%v%%v", tolerance)
}
pub fn circularity_tolerance(tolerance: f64) -> String {
format!("{{\\Fgdt;g}}%%v{}%%v%%v%%v%%v", tolerance)
}
pub fn line_profile_tolerance(tolerance: f64, datum: Option<&str>) -> String {
let datum_str = datum.unwrap_or("");
format!("{{\\Fgdt;k}}%%v{}%%v{}%%v%%v%%v", tolerance, datum_str)
}
pub fn surface_profile_tolerance(tolerance: f64, datum: Option<&str>) -> String {
let datum_str = datum.unwrap_or("");
format!("{{\\Fgdt;d}}%%v{}%%v{}%%v%%v%%v", tolerance, datum_str)
}
pub fn multi_line(lines: &[&str]) -> String {
lines.join("^J")
}
}
impl Default for Tolerance {
fn default() -> Self {
Self::new()
}
}
impl Entity for Tolerance {
fn handle(&self) -> Handle {
self.common.handle
}
fn set_handle(&mut self, handle: Handle) {
self.common.handle = handle;
}
fn layer(&self) -> &str {
&self.common.layer
}
fn set_layer(&mut self, layer: String) {
self.common.layer = layer;
}
fn color(&self) -> Color {
self.common.color
}
fn set_color(&mut self, color: Color) {
self.common.color = color;
}
fn line_weight(&self) -> LineWeight {
self.common.line_weight
}
fn set_line_weight(&mut self, weight: LineWeight) {
self.common.line_weight = weight;
}
fn transparency(&self) -> Transparency {
self.common.transparency
}
fn set_transparency(&mut self, transparency: Transparency) {
self.common.transparency = transparency;
}
fn is_invisible(&self) -> bool {
self.common.invisible
}
fn set_invisible(&mut self, invisible: bool) {
self.common.invisible = invisible;
}
fn bounding_box(&self) -> BoundingBox3D {
let half_width = self.text.len() as f64 * self.text_height * 0.6;
let half_height = self.text_height * self.line_count() as f64;
let min = Vector3::new(
self.insertion_point.x - half_width * 0.1,
self.insertion_point.y - half_height * 0.5,
self.insertion_point.z,
);
let max = Vector3::new(
self.insertion_point.x + half_width,
self.insertion_point.y + half_height * 0.5,
self.insertion_point.z,
);
BoundingBox3D::new(min, max)
}
fn translate(&mut self, offset: Vector3) {
super::translate::translate_tolerance(self, offset);
}
fn entity_type(&self) -> &'static str {
Self::ENTITY_NAME
}
fn apply_transform(&mut self, transform: &crate::types::Transform) {
super::transform::transform_tolerance(self, transform);
}
}
pub mod gdt_symbols {
pub const STRAIGHTNESS: char = 'a';
pub const FLATNESS: char = 'c';
pub const CIRCULARITY: char = 'g';
pub const CYLINDRICITY: char = 'e';
pub const LINE_PROFILE: char = 'k';
pub const SURFACE_PROFILE: char = 'd';
pub const PARALLELISM: char = 'h';
pub const PERPENDICULARITY: char = 'j';
pub const ANGULARITY: char = 'n';
pub const POSITION: char = 'p';
pub const CONCENTRICITY: char = 'u';
pub const SYMMETRY: char = 'i';
pub const CIRCULAR_RUNOUT: char = 'r';
pub const TOTAL_RUNOUT: char = 't';
pub const DIAMETER: char = 'n';
pub const MMC: char = 'm';
pub const LMC: char = 'l';
pub const RFS: char = 's';
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tolerance_creation() {
let tol = Tolerance::new();
assert_eq!(tol.insertion_point, Vector3::ZERO);
assert_eq!(tol.direction, Vector3::UNIT_X);
assert_eq!(tol.normal, Vector3::UNIT_Z);
assert!(tol.text.is_empty());
assert_eq!(tol.dimension_style_name, "Standard");
}
#[test]
fn test_tolerance_with_text() {
let tol = Tolerance::with_text(Vector3::new(10.0, 20.0, 0.0), "test");
assert_eq!(tol.insertion_point.x, 10.0);
assert_eq!(tol.insertion_point.y, 20.0);
assert_eq!(tol.text, "test");
}
#[test]
fn test_tolerance_full() {
let tol = Tolerance::new_full(
Vector3::new(5.0, 5.0, 0.0),
"tolerance text",
Vector3::UNIT_Y,
"DIMSTYLE1",
);
assert_eq!(tol.insertion_point.x, 5.0);
assert_eq!(tol.text, "tolerance text");
assert!((tol.direction.y - 1.0).abs() < 1e-10);
assert_eq!(tol.dimension_style_name, "DIMSTYLE1");
}
#[test]
fn test_tolerance_translate() {
let mut tol = Tolerance::with_text(Vector3::new(0.0, 0.0, 0.0), "test");
tol.translate(Vector3::new(5.0, 10.0, 0.0));
assert_eq!(tol.insertion_point.x, 5.0);
assert_eq!(tol.insertion_point.y, 10.0);
}
#[test]
fn test_tolerance_entity_type() {
let tol = Tolerance::new();
assert_eq!(tol.entity_type(), "TOLERANCE");
}
#[test]
fn test_position_tolerance() {
let text = Tolerance::position_tolerance(0.5, Some("A"), Some("B"), None);
assert!(text.contains("p"));
assert!(text.contains("0.5"));
assert!(text.contains("A"));
assert!(text.contains("B"));
}
#[test]
fn test_flatness_tolerance() {
let text = Tolerance::flatness_tolerance(0.1);
assert!(text.contains("c"));
assert!(text.contains("0.1"));
}
#[test]
fn test_perpendicularity_tolerance() {
let text = Tolerance::perpendicularity_tolerance(0.05, "A");
assert!(text.contains("j"));
assert!(text.contains("0.05"));
assert!(text.contains("A"));
}
#[test]
fn test_multi_line() {
let text = Tolerance::multi_line(&["line1", "line2", "line3"]);
assert_eq!(text, "line1^Jline2^Jline3");
}
#[test]
fn test_text_lines() {
let mut tol = Tolerance::new();
tol.text = "line1^Jline2^Jline3".to_string();
let lines = tol.text_lines();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "line1");
assert_eq!(lines[1], "line2");
assert_eq!(lines[2], "line3");
}
#[test]
fn test_line_count() {
let mut tol = Tolerance::new();
tol.text = "line1".to_string();
assert_eq!(tol.line_count(), 1);
tol.text = "line1^Jline2".to_string();
assert_eq!(tol.line_count(), 2);
tol.text = "line1^Jline2^Jline3".to_string();
assert_eq!(tol.line_count(), 3);
}
#[test]
fn test_point_toward() {
let mut tol = Tolerance::new();
tol.insertion_point = Vector3::new(0.0, 0.0, 0.0);
tol.point_toward(Vector3::new(0.0, 10.0, 0.0));
assert!((tol.direction.y - 1.0).abs() < 1e-10);
assert!(tol.direction.x.abs() < 1e-10);
}
#[test]
fn test_bounding_box() {
let tol = Tolerance::with_text(Vector3::new(10.0, 10.0, 0.0), "test text");
let bb = tol.bounding_box();
assert!(bb.min.x <= 10.0);
assert!(bb.max.x >= 10.0);
}
#[test]
fn test_cylindricity_tolerance() {
let text = Tolerance::cylindricity_tolerance(0.02);
assert!(text.contains("e"));
assert!(text.contains("0.02"));
}
#[test]
fn test_runout_tolerance() {
let circular = Tolerance::runout_tolerance(0.1, "A", false);
assert!(circular.contains("r"));
let total = Tolerance::runout_tolerance(0.1, "A", true);
assert!(total.contains("t"));
}
}