cad-editor/src/entities/tolerance.rs
Hakan Seven b9cebab951 feat(properties): conform entity property groups to PROPERTIES.md
Rewrite each entity's property builder to the per-entity group/row spec:
the correct sections (Geometry, Misc, Text, Pattern, Lines & Arrows, Leaders,
Block, …) with the spec's rows and real computed values — Diameter,
Circumference, Area, Delta X/Y/Z, Angle, Total angle, Arc length, Radius ratio,
etc. — plus apply routing for the editable rows.

Covers ~26 entities (Line, Circle, Arc, Point, Ellipse, Spline, polylines,
Ray/XLine, Insert, Text, MText, Hatch, Leader, MultiLeader, Tolerance, Solid,
Solid3D/Region/Body, meshes, MLine, Raster/Wipeout, Underlay, Table, Viewport,
Attribute, OLE). The 3D Visualization/Material group and the full Dimension
spec are deferred (they need document access for material/dimstyle name
resolution). Rows whose data is not in the pinned acadrust revision are shown
as empty placeholders, to be filled when the dependency is bumped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:03:18 +03:00

363 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use acadrust::entities::Tolerance;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::convert::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::scene::model::wire_model::SnapHint;
use crate::scene::text::lff;
use crate::scene::view::transform;
// ── GDT text parser ───────────────────────────────────────────────────────────
/// Parse a DXF tolerance text string into rows of cell strings.
///
/// DXF format: `{\Fgdt;p}%%v0.5%%vA%%vB%%v%%v^J{\Fgdt;j}%%v0.1%%vA%%v%%v%%v`
/// - `^J` → row separator
/// - `%%v` → cell separator within a row
/// - `{\Fgdt;X}` → GDT symbol X (mapped to a text label)
fn parse_gdt_rows(raw: &str) -> Vec<Vec<String>> {
raw.split("^J")
.filter(|row| !row.trim().is_empty())
.map(|row| {
row.split("%%v")
.map(|cell| substitute_gdt_codes(cell.trim()))
.collect()
})
.collect()
}
/// Replace `{\Fgdt;X}` sequences with a short ASCII label and strip other
/// MTEXT-style format codes `{\...}`.
fn substitute_gdt_codes(s: &str) -> String {
let mut out = String::new();
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
// Collect to closing '}'
let mut inner = String::new();
let mut depth = 1usize;
for c in chars.by_ref() {
match c {
'{' => {
depth += 1;
inner.push(c);
}
'}' => {
depth -= 1;
if depth == 0 {
break;
}
inner.push(c);
}
_ => {
inner.push(c);
}
}
}
// Is it a GDT font switch?
if let Some(rest) = inner.strip_prefix("\\Fgdt;") {
// rest is the symbol letter(s)
if let Some(sym_char) = rest.chars().next() {
out.push_str(gdt_char_to_ascii(sym_char));
}
}
// other format codes: skip
} else {
out.push(ch);
}
}
out
}
/// Map a GDT font character to a short ASCII approximation.
fn gdt_char_to_ascii(c: char) -> &'static str {
match c {
'a' => "SRT", // Straightness
'b' => "FLT", // Flatness
'c' => "FLT", // Flatness
'd' => "PSF", // Profile of Surface
'e' => "CYL", // Cylindricity
'f' => "PRL", // Profile of Line
'g' => "CIR", // Circularity
'h' => "PAR", // Parallelism
'i' => "SYM", // Symmetry
'j' => "PRP", // Perpendicularity
'k' => "PLN", // Profile of Line
'l' => "(L)", // LMC
'm' => "(M)", // MMC / Diameter
'n' => "ANG", // Angularity
'o' => "(o)", // at maximum material boundary
'p' => "POS", // Position
'q' => "(q)",
'r' => "RUN", // Circular Runout
's' => "(S)", // RFS / Regardless of Feature Size
't' => "TRN", // Total Runout
'u' => "CON", // Concentricity
'v' => "(v)",
'w' => "(w)",
_ => "?",
}
}
// ── Feature-control frame builder ─────────────────────────────────────────────
/// Tessellate a Tolerance entity into CXF-style polyline output.
///
/// Returns (`box_lines`, `text_strokes`) where:
/// - `box_lines` — 3-D line segments forming the outer border and dividers
/// - `text_strokes` — 2-D polylines from the CXF tessellator
///
/// Since TruckObject has either Lines (3-D) or Text (2-D CXF), we render the
/// box frame via a separate Lines path (added by the caller) and the text cells
/// via the Text path. For simplicity this function packs everything into a
/// single Vec<Vec<[f32;2]>> by projecting the box lines to 2-D and using NaN
/// polylines as separators — exactly what the Text wire-builder does.
fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
if tol.text.is_empty() {
return vec![];
}
let rows = parse_gdt_rows(&tol.text);
if rows.is_empty() {
return vec![];
}
// ── Metrics ──────────────────────────────────────────────────────────
let h = if tol.text_height > 1e-6 {
tol.text_height as f32
} else {
2.5_f32
};
// DIMGAP — stored on the entity (resolved from the dim style at creation).
// Fall back to AutoCAD's 0.35 × height convention only when missing.
let gap = if tol.dimension_gap > 1e-6 {
tol.dimension_gap as f32
} else {
(h * 0.35).max(0.1)
};
let cell_h = h + 2.0 * gap;
let char_w = h * 0.65;
let min_cell_w = h * 1.4;
// Column widths: max across all rows
let max_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
let mut col_widths: Vec<f32> = vec![0.0_f32; max_cols];
for row in &rows {
for (ci, cell) in row.iter().enumerate() {
let w = (cell.len() as f32 * char_w).max(min_cell_w);
if ci < col_widths.len() {
col_widths[ci] = col_widths[ci].max(w);
}
}
}
let total_w: f32 = col_widths.iter().sum();
let total_h = cell_h * rows.len() as f32;
// ── Transform helpers (local space — translation applied in tessellate.rs) ──
let angle = (tol.direction.y as f32).atan2(tol.direction.x as f32);
let (sa, ca) = angle.sin_cos();
// Rotate only; origin is kept as f64 and applied later with full precision.
let rot = |x: f32, y: f32| -> [f32; 2] { [x * ca - y * sa, x * sa + y * ca] };
let mut out: Vec<Vec<[f32; 2]>> = Vec::new();
// ── Outer border ──────────────────────────────────────────────────────
out.push(vec![
rot(0.0, 0.0),
rot(total_w, 0.0),
rot(total_w, total_h),
rot(0.0, total_h),
rot(0.0, 0.0),
]);
// ── Row separators ─────────────────────────────────────────────────────
for ri in 1..rows.len() {
let y = cell_h * ri as f32;
out.push(vec![rot(0.0, y), rot(total_w, y)]);
}
// ── Column dividers ────────────────────────────────────────────────────
let mut x_cursor = 0.0_f32;
for ci in 0..col_widths.len().saturating_sub(1) {
x_cursor += col_widths[ci];
for ri in 0..rows.len() {
if ci + 1 < rows[ri].len() {
let y0 = cell_h * ri as f32;
let y1 = y0 + cell_h;
out.push(vec![rot(x_cursor, y0), rot(x_cursor, y1)]);
}
}
}
// ── Text content per cell ─────────────────────────────────────────────
for (ri, row) in rows.iter().enumerate() {
let row_y = cell_h * ri as f32 + gap;
let mut cell_x = 0.0_f32;
for (ci, cell) in row.iter().enumerate() {
let cw = if ci < col_widths.len() {
col_widths[ci]
} else {
0.0
};
if !cell.is_empty() {
let text_w = cell.len() as f32 * char_w;
let tx = cell_x + (cw - text_w) * 0.5;
// Tessellate text in local frame then transform
let (local_strokes, _) =
lff::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, "txt", cell);
for polyline in local_strokes {
let transformed: Vec<[f32; 2]> = polyline
.into_iter()
.map(|[px, py]| rot(px + tx, py + row_y))
.collect();
if !transformed.is_empty() {
out.push(transformed);
}
}
}
cell_x += cw;
}
}
out
}
// ── TruckConvertible ──────────────────────────────────────────────────────────
impl TruckConvertible for Tolerance {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
if self.text.is_empty() {
return None;
}
let snap_pt = glam::DVec3::new(
self.insertion_point.x,
self.insertion_point.y,
self.insertion_point.z,
);
// Build the feature-control frame in local space; origin stored as f64.
let strokes = tessellate_tolerance(self);
let origin = [self.insertion_point.x, self.insertion_point.y];
Some(TruckEntity {
object: TruckObject::Text(vec![TextStroke {
strokes,
origin,
color: None,
fill_tris: vec![],
}]),
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
tangent_geoms: vec![],
key_vertices: vec![],
fill_tris: vec![],
})
}
}
// ── Grippable ─────────────────────────────────────────────────────────────────
impl Grippable for Tolerance {
fn grips(&self) -> Vec<GripDef> {
vec![square_grip(
0,
glam::DVec3::new(
self.insertion_point.x,
self.insertion_point.y,
self.insertion_point.z,
),
)]
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
if grip_id == 0 {
match apply {
GripApply::Translate(d) => {
self.insertion_point.x += d.x as f64;
self.insertion_point.y += d.y as f64;
self.insertion_point.z += d.z as f64;
}
GripApply::Absolute(p) => {
self.insertion_point.x = p.x as f64;
self.insertion_point.y = p.y as f64;
self.insertion_point.z = p.z as f64;
}
}
}
}
}
// ── PropertyEditable ──────────────────────────────────────────────────────────
impl PropertyEditable for Tolerance {
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
vec![
PropSection {
title: "Text".into(),
props: vec![
ro("Text style", "tol_text_style", String::new()),
edit("Text height", "tol_text_height", self.text_height),
],
},
PropSection {
title: "Geometry".into(),
props: vec![
edit("Position X", "tol_ix", self.insertion_point.x),
edit("Position Y", "tol_iy", self.insertion_point.y),
edit("Position Z", "tol_iz", self.insertion_point.z),
],
},
PropSection {
title: "Misc".into(),
props: vec![
ro(
"Dimension style",
"tol_dim_style",
if self.dimension_style_name.is_empty() {
"(default)".to_string()
} else {
self.dimension_style_name.clone()
},
),
edit("Direction X", "tol_dir_x", self.direction.x),
edit("Direction Y", "tol_dir_y", self.direction.y),
edit("Direction Z", "tol_dir_z", self.direction.z),
],
},
]
}
fn apply_geom_prop(&mut self, field: &str, value: &str) {
let Ok(v) = value.trim().parse::<f64>() else {
return;
};
match field {
"tol_ix" => self.insertion_point.x = v,
"tol_iy" => self.insertion_point.y = v,
"tol_iz" => self.insertion_point.z = v,
"tol_text_height" if v > 0.0 => self.text_height = v,
"tol_dir_x" => self.direction.x = v,
"tol_dir_y" => self.direction.y = v,
"tol_dir_z" => self.direction.z = v,
_ => {}
}
}
}
// ── Transformable ─────────────────────────────────────────────────────────────
impl Transformable for Tolerance {
fn apply_transform(&mut self, t: &EntityTransform) {
transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
transform::reflect_xy_point(
&mut entity.insertion_point.x,
&mut entity.insertion_point.y,
p1,
p2,
);
});
}
}