mirror of
https://github.com/aphrody-labs/cadcraft.git
synced 2026-10-14 03:29:06 +00:00
Squash of PR #427 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
139d98bf6f
commit
9e3379882f
22 files changed
+1297
-3
No files matched your search
@@ -705,6 +705,7 @@ pub enum EntityKind {
|
||||
Image(Image),
|
||||
Wipeout(Wipeout),
|
||||
Table(Table),
|
||||
Tolerance(crate::Tolerance),
|
||||
Unknown(Unknown),
|
||||
}
|
||||
|
||||
@@ -755,6 +756,7 @@ impl EntityKind {
|
||||
EntityKind::Image(_) => "Raster Image",
|
||||
EntityKind::Wipeout(_) => "Wipeout",
|
||||
EntityKind::Table(_) => "Table",
|
||||
EntityKind::Tolerance(_) => "Tolerance",
|
||||
EntityKind::Unknown(_) => "Proxy",
|
||||
}
|
||||
}
|
||||
@@ -786,6 +788,7 @@ impl EntityKind {
|
||||
EntityKind::Image(_) => "IMAGE",
|
||||
EntityKind::Wipeout(_) => "WIPEOUT",
|
||||
EntityKind::Table(_) => "ACAD_TABLE",
|
||||
EntityKind::Tolerance(_) => "TOLERANCE",
|
||||
EntityKind::Unknown(u) => u.dxf_type.as_str(),
|
||||
}
|
||||
}
|
||||
@@ -859,6 +862,7 @@ impl EntityKind {
|
||||
.collect(),
|
||||
EntityKind::Image(i) => vec![Prim::Point(i.insert.xy())],
|
||||
EntityKind::Table(t) => vec![Prim::Point(t.insert.xy())],
|
||||
EntityKind::Tolerance(t) => vec![Prim::Point(t.insert.xy())],
|
||||
EntityKind::Unknown(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1018,6 +1022,12 @@ impl EntityKind {
|
||||
t.row_heights.iter_mut().for_each(|h| *h *= s);
|
||||
t.text_height *= s;
|
||||
}
|
||||
EntityKind::Tolerance(t) => {
|
||||
t3(&mut t.insert);
|
||||
// Like text with MIRRTEXT = 0: a mirrored frame keeps reading left to right.
|
||||
let a = if mirror { t.rotation() } else { t.rotation() + rot };
|
||||
t.dir = Vec2::from_angle(cadcraft_geom::norm_angle(a)).to3(0.0);
|
||||
}
|
||||
EntityKind::Unknown(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -1082,6 +1092,7 @@ impl EntityKind {
|
||||
EntityKind::Image(i) => vec![i.insert.xy()],
|
||||
EntityKind::Wipeout(w) => w.boundary.clone(),
|
||||
EntityKind::Table(t) => vec![t.insert.xy()],
|
||||
EntityKind::Tolerance(t) => vec![t.insert.xy()],
|
||||
EntityKind::Unknown(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,8 @@ fn bounds_in(d: &Drawing, e: &Entity, depth: usize, blocks: &mut HashMap<String,
|
||||
let h: f64 = t.row_heights.iter().sum();
|
||||
Bounds2::new(t.insert.xy(), t.insert.xy() + Vec2::new(w, -h))
|
||||
}
|
||||
// Text widths estimated like `text_box`; the renderer measures the font.
|
||||
EntityKind::Tolerance(t) => t.layout(d, &|s, h| h * 0.9 * s.chars().count() as f64).bounds,
|
||||
EntityKind::Image(i) => {
|
||||
let o = i.insert.xy();
|
||||
let u = i.u.xy() * i.size.x;
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
//! Geometric tolerancing (GD&T): the TOLERANCE entity (feature control frames) and its layout.
|
||||
//!
|
||||
//! The frame is stored as the DXF Reference's "visual representation string" (TOLERANCE group 1):
|
||||
//! frame lines separated by `^J`, compartments separated by `%%v`, and the geometric symbols as
|
||||
//! characters of the `gdt` font inside `{\Fgdt;…}` runs. CADCraft never uses that font: every
|
||||
//! symbol is drawn by [`symbol_prims`] from our own geometry (standard ISO/ASME symbol shapes).
|
||||
|
||||
use cadcraft_geom::{Bounds2, Vec2, Vec3};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Caps for hostile frame strings (files, command parameters).
|
||||
pub const MAX_FRAME_CHARS: usize = 4096;
|
||||
pub const MAX_FRAME_ROWS: usize = 8;
|
||||
pub const MAX_FRAME_CELLS: usize = 12;
|
||||
|
||||
/// A TOLERANCE entity: a feature control frame.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tolerance {
|
||||
/// Insertion point: the middle of the left edge of the first frame line.
|
||||
pub insert: Vec3,
|
||||
/// The frame's X-axis direction (DXF group 11).
|
||||
#[serde(default = "x_axis")]
|
||||
pub dir: Vec3,
|
||||
/// Dimension style: text height (DIMTXT × DIMSCALE), gap, colours, text style.
|
||||
#[serde(default = "standard")]
|
||||
pub style: String,
|
||||
/// The frame contents (DXF group 1).
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
fn x_axis() -> Vec3 {
|
||||
Vec3::new(1.0, 0.0, 0.0)
|
||||
}
|
||||
fn standard() -> String {
|
||||
"Standard".into()
|
||||
}
|
||||
|
||||
impl Tolerance {
|
||||
/// The frame's rotation (radians from +X); 0 when the direction is unusable.
|
||||
pub fn rotation(&self) -> f64 {
|
||||
let d = self.dir.xy();
|
||||
if d.is_finite() && d.len() > 1e-12 { d.angle() } else { 0.0 }
|
||||
}
|
||||
/// Text height and padding from the dimension style: DIMTXT and DIMGAP times the style's
|
||||
/// DIMSCALE (the drawing's when 0), or the text style's fixed height.
|
||||
pub fn text_size(&self, d: &crate::Drawing) -> (f64, f64) {
|
||||
let st = d.dim_style(&self.style).cloned().unwrap_or_default();
|
||||
let k = st.effective_scale(d.header.f64("DIMSCALE", 1.0));
|
||||
let fixed = d.text_style(&st.text_style).map(|t| t.height).filter(|h| h.is_finite() && *h > 0.0);
|
||||
let h = fixed.unwrap_or(st.text_height * k);
|
||||
(if h.is_finite() && h > 0.0 { h } else { 0.18 }, st.text_gap * k)
|
||||
}
|
||||
/// The frame laid out with `measure(text, height)` as the width of plain text.
|
||||
pub fn layout(&self, d: &crate::Drawing, measure: &dyn Fn(&str, f64) -> f64) -> GdtLayout {
|
||||
let (h, gap) = self.text_size(d);
|
||||
layout_frame(&self.text, self.insert.xy(), self.rotation(), h, gap, measure)
|
||||
}
|
||||
}
|
||||
|
||||
/// A geometric characteristic, material condition or zone symbol.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum GdtSymbol {
|
||||
Position,
|
||||
Concentricity,
|
||||
Symmetry,
|
||||
Parallelism,
|
||||
Perpendicularity,
|
||||
Angularity,
|
||||
Cylindricity,
|
||||
Flatness,
|
||||
Circularity,
|
||||
Straightness,
|
||||
SurfaceProfile,
|
||||
LineProfile,
|
||||
CircularRunout,
|
||||
TotalRunout,
|
||||
Diameter,
|
||||
/// Maximum material condition (circled M).
|
||||
Mmc,
|
||||
/// Least material condition (circled L).
|
||||
Lmc,
|
||||
/// Regardless of feature size (circled S).
|
||||
Rfs,
|
||||
/// Projected tolerance zone (circled P).
|
||||
Projected,
|
||||
}
|
||||
|
||||
/// Every symbol with its `gdt` font character and its name.
|
||||
const SYMBOLS: [(GdtSymbol, char, &str); 19] = [
|
||||
(GdtSymbol::Position, 'j', "position"),
|
||||
(GdtSymbol::Concentricity, 'r', "concentricity"),
|
||||
(GdtSymbol::Symmetry, 'i', "symmetry"),
|
||||
(GdtSymbol::Parallelism, 'f', "parallelism"),
|
||||
(GdtSymbol::Perpendicularity, 'b', "perpendicularity"),
|
||||
(GdtSymbol::Angularity, 'a', "angularity"),
|
||||
(GdtSymbol::Cylindricity, 'g', "cylindricity"),
|
||||
(GdtSymbol::Flatness, 'c', "flatness"),
|
||||
(GdtSymbol::Circularity, 'e', "circularity"),
|
||||
(GdtSymbol::Straightness, 'u', "straightness"),
|
||||
(GdtSymbol::SurfaceProfile, 'd', "surfaceprofile"),
|
||||
(GdtSymbol::LineProfile, 'k', "lineprofile"),
|
||||
(GdtSymbol::CircularRunout, 'h', "circularrunout"),
|
||||
(GdtSymbol::TotalRunout, 't', "totalrunout"),
|
||||
(GdtSymbol::Diameter, 'n', "diameter"),
|
||||
(GdtSymbol::Mmc, 'm', "mmc"),
|
||||
(GdtSymbol::Lmc, 'l', "lmc"),
|
||||
(GdtSymbol::Rfs, 's', "rfs"),
|
||||
(GdtSymbol::Projected, 'p', "projected"),
|
||||
];
|
||||
|
||||
impl GdtSymbol {
|
||||
/// The symbol's character in the frame string's `{\Fgdt;…}` runs.
|
||||
pub fn code(self) -> char {
|
||||
SYMBOLS.iter().find(|(s, _, _)| *s == self).map(|(_, c, _)| *c).unwrap_or('j')
|
||||
}
|
||||
pub fn from_code(c: char) -> Option<GdtSymbol> {
|
||||
SYMBOLS.iter().find(|(_, k, _)| *k == c).map(|(s, _, _)| *s)
|
||||
}
|
||||
/// Lower-case name ("position", "mmc", …).
|
||||
pub fn name(self) -> &'static str {
|
||||
SYMBOLS.iter().find(|(s, _, _)| *s == self).map(|(_, _, n)| *n).unwrap_or("position")
|
||||
}
|
||||
/// A symbol by name (case, spaces, dashes and underscores ignored) or by a few common
|
||||
/// alternatives ("true position", "runout", "profile", "M", "L", "S").
|
||||
pub fn parse(s: &str) -> Option<GdtSymbol> {
|
||||
let k: String = s.chars().filter(|c| !matches!(c, ' ' | '-' | '_')).flat_map(char::to_lowercase).collect();
|
||||
let alias = match k.as_str() {
|
||||
"trueposition" | "pos" => Some(GdtSymbol::Position),
|
||||
"coaxiality" => Some(GdtSymbol::Concentricity),
|
||||
"roundness" => Some(GdtSymbol::Circularity),
|
||||
"profile" | "surface" | "profileofasurface" => Some(GdtSymbol::SurfaceProfile),
|
||||
"line" | "profileofaline" => Some(GdtSymbol::LineProfile),
|
||||
"runout" => Some(GdtSymbol::CircularRunout),
|
||||
"total" => Some(GdtSymbol::TotalRunout),
|
||||
"m" => Some(GdtSymbol::Mmc),
|
||||
"l" => Some(GdtSymbol::Lmc),
|
||||
"s" => Some(GdtSymbol::Rfs),
|
||||
"dia" | "ø" | "⌀" | "%%c" => Some(GdtSymbol::Diameter),
|
||||
_ => None,
|
||||
};
|
||||
alias.or_else(|| SYMBOLS.iter().find(|(_, _, n)| *n == k).map(|(s, _, _)| *s))
|
||||
}
|
||||
/// A geometric characteristic (the first compartment of a frame line).
|
||||
pub fn is_characteristic(self) -> bool {
|
||||
!matches!(self, GdtSymbol::Diameter | GdtSymbol::Mmc | GdtSymbol::Lmc | GdtSymbol::Rfs | GdtSymbol::Projected)
|
||||
}
|
||||
/// A material condition modifier.
|
||||
pub fn is_material(self) -> bool {
|
||||
matches!(self, GdtSymbol::Mmc | GdtSymbol::Lmc | GdtSymbol::Rfs)
|
||||
}
|
||||
}
|
||||
|
||||
/// A piece of a compartment: plain text or a symbol.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum GdtRun {
|
||||
Text(String),
|
||||
Symbol(GdtSymbol),
|
||||
}
|
||||
|
||||
/// One compartment's runs.
|
||||
pub type Compartment = Vec<GdtRun>;
|
||||
|
||||
/// Parse a frame string into lines of compartments. Unknown formatting codes are skipped, an
|
||||
/// unterminated code ends the string, and the sizes are capped ([`MAX_FRAME_CHARS`],
|
||||
/// [`MAX_FRAME_ROWS`], [`MAX_FRAME_CELLS`]).
|
||||
pub fn parse_frame(text: &str) -> Vec<Vec<Compartment>> {
|
||||
let cs: Vec<char> = text.chars().take(MAX_FRAME_CHARS).collect();
|
||||
let at = |i: usize| cs.get(i).copied();
|
||||
let mut rows: Vec<Vec<Compartment>> = vec![vec![Vec::new()]];
|
||||
// Font stack: whether the `gdt` font is current, per brace level.
|
||||
let mut gdt = false;
|
||||
let mut stack: Vec<bool> = Vec::new();
|
||||
let push = |rows: &mut Vec<Vec<Compartment>>, run: GdtRun| {
|
||||
let Some(cell) = rows.last_mut().and_then(|r| r.last_mut()) else { return };
|
||||
match (cell.last_mut(), run) {
|
||||
(Some(GdtRun::Text(t)), GdtRun::Text(s)) => t.push_str(&s),
|
||||
(_, run) => cell.push(run),
|
||||
}
|
||||
};
|
||||
let mut i = 0;
|
||||
while let Some(c) = at(i) {
|
||||
let next = at(i + 1);
|
||||
let lower = |k: usize| at(k).map(|c| c.to_ascii_lowercase());
|
||||
// Line break: `^J`, `\P` or a raw newline.
|
||||
if (c == '^' && matches!(next, Some('J' | 'j'))) || (c == '\\' && next == Some('P')) || c == '\n' {
|
||||
if rows.len() >= MAX_FRAME_ROWS {
|
||||
break;
|
||||
}
|
||||
rows.push(vec![Vec::new()]);
|
||||
i += if c == '\n' { 1 } else { 2 };
|
||||
continue;
|
||||
}
|
||||
if c == '\r' {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if c == '%' && next == Some('%') {
|
||||
match lower(i + 2) {
|
||||
Some('v') => {
|
||||
if let Some(r) = rows.last_mut()
|
||||
&& r.len() < MAX_FRAME_CELLS
|
||||
{
|
||||
r.push(Vec::new());
|
||||
}
|
||||
}
|
||||
Some('c') => push(&mut rows, GdtRun::Symbol(GdtSymbol::Diameter)),
|
||||
Some('d') => push(&mut rows, GdtRun::Text("°".into())),
|
||||
Some('p') => push(&mut rows, GdtRun::Text("±".into())),
|
||||
Some('%') => push(&mut rows, GdtRun::Text("%".into())),
|
||||
Some(o) => push(&mut rows, GdtRun::Text(format!("%%{o}"))),
|
||||
None => break,
|
||||
}
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'{' => {
|
||||
if stack.len() < 64 {
|
||||
stack.push(gdt);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
'}' => {
|
||||
gdt = stack.pop().unwrap_or(false);
|
||||
i += 1;
|
||||
}
|
||||
'\\' => match next {
|
||||
Some('\\' | '{' | '}') => {
|
||||
if let Some(n) = next {
|
||||
push(&mut rows, GdtRun::Text(n.to_string()));
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
// Font switch `\Fname;` / `\fname|b0|i0;`: the gdt font draws symbols.
|
||||
Some('F' | 'f') => {
|
||||
let Some(end) = (i + 2..cs.len()).find(|k| at(*k) == Some(';')) else { break };
|
||||
let name: String = cs.get(i + 2..end).unwrap_or(&[]).iter().take_while(|c| **c != '|').collect();
|
||||
gdt = name.trim().to_ascii_lowercase().starts_with("gdt");
|
||||
i = end + 1;
|
||||
}
|
||||
// Codes with a value up to `;` (height, colour, tracking, width, oblique, alignment…).
|
||||
Some('A' | 'C' | 'c' | 'H' | 'Q' | 'T' | 'W' | 'S') => {
|
||||
let Some(end) = (i + 2..cs.len()).find(|k| at(*k) == Some(';')) else { break };
|
||||
i = end + 1;
|
||||
}
|
||||
// On/off toggles (underline, overline, strike-through) and the non-breaking space.
|
||||
Some('L' | 'l' | 'O' | 'o' | 'K' | 'k') => i += 2,
|
||||
Some('~') => {
|
||||
push(&mut rows, GdtRun::Text(" ".into()));
|
||||
i += 2;
|
||||
}
|
||||
Some(_) => {
|
||||
push(&mut rows, GdtRun::Text("\\".into()));
|
||||
i += 1;
|
||||
}
|
||||
None => break,
|
||||
},
|
||||
c if gdt => {
|
||||
push(&mut rows, GdtSymbol::from_code(c).map(GdtRun::Symbol).unwrap_or_else(|| GdtRun::Text(c.to_string())));
|
||||
i += 1;
|
||||
}
|
||||
c => {
|
||||
push(&mut rows, GdtRun::Text(c.to_string()));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
/// A compartment with nothing to show.
|
||||
pub fn compartment_is_empty(c: &Compartment) -> bool {
|
||||
c.iter().all(|r| matches!(r, GdtRun::Text(t) if t.trim().is_empty()))
|
||||
}
|
||||
|
||||
/// A tolerance value: optional diameter symbol, the value, optional material condition.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GdtTol {
|
||||
#[serde(default)]
|
||||
pub diameter: bool,
|
||||
#[serde(default)]
|
||||
pub value: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub material: Option<String>,
|
||||
}
|
||||
|
||||
/// A datum reference: the letter(s) and an optional material condition.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GdtDatum {
|
||||
#[serde(default)]
|
||||
pub letter: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub material: Option<String>,
|
||||
}
|
||||
|
||||
/// One frame line as the Geometric Tolerance dialog fills it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct GdtLine {
|
||||
pub symbol: Option<GdtSymbol>,
|
||||
pub tol1: Option<GdtTol>,
|
||||
pub tol2: Option<GdtTol>,
|
||||
pub datums: Vec<GdtDatum>,
|
||||
}
|
||||
|
||||
/// A whole feature control frame: up to two tolerance lines, the projected tolerance zone and
|
||||
/// the datum identifier.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct GdtFrame {
|
||||
pub lines: Vec<GdtLine>,
|
||||
/// Projected tolerance zone height ("" = none).
|
||||
pub height: String,
|
||||
pub projected: bool,
|
||||
/// Datum identifier ("" = none).
|
||||
pub datum_id: String,
|
||||
}
|
||||
|
||||
/// Escape free text for a frame string (no line or compartment breaks, no formatting codes).
|
||||
fn escape(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for c in s.chars().take(256) {
|
||||
match c {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'{' => out.push_str("\\{"),
|
||||
'}' => out.push_str("\\}"),
|
||||
// `%%%` is a literal percent sign (a lone `%` could start a `%%` code).
|
||||
'%' => out.push_str("%%%"),
|
||||
'\n' | '\r' | '^' => out.push(' '),
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn sym(s: GdtSymbol) -> String {
|
||||
format!("{{\\Fgdt;{}}}", s.code())
|
||||
}
|
||||
|
||||
/// The material condition symbol named by `m` ("M", "L", "S", "mmc"…).
|
||||
fn material(m: &Option<String>) -> Option<GdtSymbol> {
|
||||
m.as_deref().and_then(GdtSymbol::parse).filter(|s| s.is_material())
|
||||
}
|
||||
|
||||
/// The frame string for `f` (DXF group 1 of a TOLERANCE).
|
||||
pub fn compose_frame(f: &GdtFrame) -> String {
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
let tol = |t: &Option<GdtTol>| -> String {
|
||||
let Some(t) = t else { return String::new() };
|
||||
let mut s = String::new();
|
||||
if t.diameter {
|
||||
s.push_str(&sym(GdtSymbol::Diameter));
|
||||
}
|
||||
s.push_str(&escape(t.value.trim()));
|
||||
if let Some(m) = material(&t.material) {
|
||||
s.push_str(&sym(m));
|
||||
}
|
||||
if t.value.trim().is_empty() && !t.diameter && material(&t.material).is_none() { String::new() } else { s }
|
||||
};
|
||||
for l in f.lines.iter().take(MAX_FRAME_ROWS) {
|
||||
let mut cells = vec![l.symbol.map(sym).unwrap_or_default(), tol(&l.tol1), tol(&l.tol2)];
|
||||
for d in l.datums.iter().take(3) {
|
||||
let mut s = escape(d.letter.trim());
|
||||
if let Some(m) = material(&d.material) {
|
||||
s.push_str(&sym(m));
|
||||
}
|
||||
cells.push(s);
|
||||
}
|
||||
while cells.last().is_some_and(String::is_empty) {
|
||||
cells.pop();
|
||||
}
|
||||
if !cells.is_empty() {
|
||||
lines.push(cells.join("%%v"));
|
||||
}
|
||||
}
|
||||
if !f.height.trim().is_empty() || f.projected {
|
||||
let mut s = escape(f.height.trim());
|
||||
if f.projected {
|
||||
s.push_str(&sym(GdtSymbol::Projected));
|
||||
}
|
||||
lines.push(s);
|
||||
}
|
||||
if !f.datum_id.trim().is_empty() {
|
||||
lines.push(escape(f.datum_id.trim()));
|
||||
}
|
||||
lines.join("^J")
|
||||
}
|
||||
|
||||
/// A primitive of a laid-out frame, in WCS.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum GdtPrim {
|
||||
Line(Vec<Vec2>),
|
||||
Circle {
|
||||
center: Vec2,
|
||||
radius: f64,
|
||||
},
|
||||
/// Counter-clockwise from `start` to `end` (radians).
|
||||
Arc {
|
||||
center: Vec2,
|
||||
radius: f64,
|
||||
start: f64,
|
||||
end: f64,
|
||||
},
|
||||
/// A filled triangle (arrowheads).
|
||||
Fill([Vec2; 3]),
|
||||
}
|
||||
|
||||
/// A text run of a laid-out frame: left baseline point (WCS), height and rotation.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct GdtText {
|
||||
pub pos: Vec2,
|
||||
pub height: f64,
|
||||
pub rotation: f64,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// A laid-out feature control frame.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct GdtLayout {
|
||||
/// The frame's boxes (drawn with the dimension line colour).
|
||||
pub frame: Vec<GdtPrim>,
|
||||
/// The symbols (drawn with the text colour).
|
||||
pub symbols: Vec<GdtPrim>,
|
||||
pub texts: Vec<GdtText>,
|
||||
pub bounds: Bounds2,
|
||||
}
|
||||
|
||||
/// Lay out the frame `text` at `insert`, rotated by `rotation`, with text height `h` and
|
||||
/// horizontal padding `gap`. `measure(s, h)` is the width of plain text `s` at height `h`.
|
||||
pub fn layout_frame(text: &str, insert: Vec2, rotation: f64, h: f64, gap: f64, measure: &dyn Fn(&str, f64) -> f64) -> GdtLayout {
|
||||
let h = if h.is_finite() && h > 0.0 { h.min(1e12) } else { 0.18 };
|
||||
let gap = if gap.is_finite() { gap.abs().clamp(h * 0.25, h * 2.0) } else { h * 0.5 };
|
||||
let rotation = if rotation.is_finite() { rotation } else { 0.0 };
|
||||
let (insert, row_h) = (if insert.is_finite() { insert } else { Vec2::ZERO }, 2.0 * h);
|
||||
let to_w = |p: Vec2| insert + p.rotate(rotation);
|
||||
let mut out = GdtLayout::default();
|
||||
let mut prev_width = 0.0;
|
||||
let rows: Vec<Vec<Compartment>> = parse_frame(text)
|
||||
.into_iter()
|
||||
.map(|r| r.into_iter().filter(|c| !compartment_is_empty(c)).collect::<Vec<_>>())
|
||||
.filter(|r| !r.is_empty())
|
||||
.collect();
|
||||
let run_w = |r: &GdtRun| match r {
|
||||
GdtRun::Symbol(_) => h,
|
||||
GdtRun::Text(t) => {
|
||||
let w = measure(t, h);
|
||||
if w.is_finite() { w.clamp(0.0, h * 4096.0) } else { 0.0 }
|
||||
}
|
||||
};
|
||||
for (k, row) in rows.iter().enumerate() {
|
||||
let top = h - k as f64 * row_h;
|
||||
let (bot, mid) = (top - row_h, top - h);
|
||||
let mut x = 0.0;
|
||||
for cell in row {
|
||||
// Runs a little apart, so a modifier doesn't touch the value before it.
|
||||
let sep = 0.25 * h;
|
||||
let w: f64 = cell.iter().map(run_w).sum::<f64>() + sep * cell.len().saturating_sub(1) as f64;
|
||||
let cw = (w + 2.0 * gap).max(row_h);
|
||||
// The separator on the cell's left (the frame's left edge for the first cell).
|
||||
out.frame.push(GdtPrim::Line(vec![to_w(Vec2::new(x, top)), to_w(Vec2::new(x, bot))]));
|
||||
let mut cx = x + (cw - w) / 2.0;
|
||||
for r in cell {
|
||||
match r {
|
||||
GdtRun::Symbol(s) => {
|
||||
let o = Vec2::new(cx, mid - h / 2.0);
|
||||
out.symbols.extend(symbol_prims(*s).into_iter().map(|p| map_prim(p, |q| to_w(o + q * h), h, rotation)));
|
||||
}
|
||||
GdtRun::Text(t) => {
|
||||
out.texts.push(GdtText { pos: to_w(Vec2::new(cx, mid - h / 2.0)), height: h, rotation, value: t.clone() });
|
||||
}
|
||||
}
|
||||
cx += run_w(r) + sep;
|
||||
}
|
||||
x += cw;
|
||||
}
|
||||
out.frame.push(GdtPrim::Line(vec![to_w(Vec2::new(x, top)), to_w(Vec2::new(x, bot))]));
|
||||
// Top edge: the first line's, or the part sticking out past the line above.
|
||||
if k == 0 {
|
||||
out.frame.push(GdtPrim::Line(vec![to_w(Vec2::new(0.0, top)), to_w(Vec2::new(x, top))]));
|
||||
} else if x > prev_width {
|
||||
out.frame.push(GdtPrim::Line(vec![to_w(Vec2::new(prev_width, top)), to_w(Vec2::new(x, top))]));
|
||||
}
|
||||
out.frame.push(GdtPrim::Line(vec![to_w(Vec2::new(0.0, bot)), to_w(Vec2::new(x, bot))]));
|
||||
out.bounds = out.bounds.union(&Bounds2::from_points([
|
||||
to_w(Vec2::new(0.0, top)),
|
||||
to_w(Vec2::new(x, bot)),
|
||||
to_w(Vec2::new(0.0, bot)),
|
||||
to_w(Vec2::new(x, top)),
|
||||
]));
|
||||
prev_width = x;
|
||||
}
|
||||
if rows.is_empty() {
|
||||
out.bounds = Bounds2::from_points([insert]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Map a unit-box primitive through `f` (scale `s`, rotation `rot`).
|
||||
fn map_prim(p: GdtPrim, f: impl Fn(Vec2) -> Vec2, s: f64, rot: f64) -> GdtPrim {
|
||||
match p {
|
||||
GdtPrim::Line(v) => GdtPrim::Line(v.into_iter().map(f).collect()),
|
||||
GdtPrim::Circle { center, radius } => GdtPrim::Circle { center: f(center), radius: radius * s },
|
||||
GdtPrim::Arc { center, radius, start, end } => GdtPrim::Arc { center: f(center), radius: radius * s, start: start + rot, end: end + rot },
|
||||
GdtPrim::Fill(t) => GdtPrim::Fill(t.map(f)),
|
||||
}
|
||||
}
|
||||
|
||||
/// An arrowhead at `tip` pointing along `d` (unit box units).
|
||||
fn arrow(tip: Vec2, d: Vec2) -> GdtPrim {
|
||||
let d = d.normalized();
|
||||
let back = tip - d * 0.3;
|
||||
GdtPrim::Fill([tip, back + d.perp() * 0.1, back - d.perp() * 0.1])
|
||||
}
|
||||
|
||||
/// A symbol drawn in a unit box (x and y in 0..1, the baseline at y = 0).
|
||||
pub fn symbol_prims(s: GdtSymbol) -> Vec<GdtPrim> {
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
let v = Vec2::new;
|
||||
let line = |pts: &[(f64, f64)]| GdtPrim::Line(pts.iter().map(|(x, y)| v(*x, *y)).collect());
|
||||
let circle = |x: f64, y: f64, r: f64| GdtPrim::Circle { center: v(x, y), radius: r };
|
||||
// Circled letters: the circle plus the letter's strokes.
|
||||
let circled = |mut letter: Vec<GdtPrim>| {
|
||||
letter.insert(0, circle(0.5, 0.5, 0.5));
|
||||
letter
|
||||
};
|
||||
match s {
|
||||
GdtSymbol::Position => vec![circle(0.5, 0.5, 0.3), line(&[(0.0, 0.5), (1.0, 0.5)]), line(&[(0.5, 0.0), (0.5, 1.0)])],
|
||||
GdtSymbol::Concentricity => vec![circle(0.5, 0.5, 0.45), circle(0.5, 0.5, 0.25)],
|
||||
GdtSymbol::Symmetry => vec![line(&[(0.0, 0.5), (1.0, 0.5)]), line(&[(0.2, 0.2), (0.8, 0.2)]), line(&[(0.2, 0.8), (0.8, 0.8)])],
|
||||
GdtSymbol::Parallelism => vec![line(&[(0.1, 0.05), (0.5, 0.95)]), line(&[(0.5, 0.05), (0.9, 0.95)])],
|
||||
GdtSymbol::Perpendicularity => vec![line(&[(0.05, 0.05), (0.95, 0.05)]), line(&[(0.5, 0.05), (0.5, 0.95)])],
|
||||
GdtSymbol::Angularity => vec![line(&[(0.95, 0.05), (0.05, 0.05), (0.85, 0.75)])],
|
||||
GdtSymbol::Cylindricity => {
|
||||
let (c, r) = (v(0.5, 0.5), 0.3);
|
||||
let d = Vec2::from_angle(65f64.to_radians());
|
||||
let n = d.perp() * r;
|
||||
vec![circle(0.5, 0.5, r), GdtPrim::Line(vec![c + n - d * 0.5, c + n + d * 0.5]), GdtPrim::Line(vec![c - n - d * 0.5, c - n + d * 0.5])]
|
||||
}
|
||||
GdtSymbol::Flatness => vec![line(&[(0.0, 0.25), (0.7, 0.25), (1.0, 0.75), (0.3, 0.75), (0.0, 0.25)])],
|
||||
GdtSymbol::Circularity => vec![circle(0.5, 0.5, 0.45)],
|
||||
GdtSymbol::Straightness => vec![line(&[(0.0, 0.5), (1.0, 0.5)])],
|
||||
GdtSymbol::SurfaceProfile => {
|
||||
vec![GdtPrim::Arc { center: v(0.5, 0.2), radius: 0.45, start: 0.0, end: PI }, line(&[(0.05, 0.2), (0.95, 0.2)])]
|
||||
}
|
||||
GdtSymbol::LineProfile => vec![GdtPrim::Arc { center: v(0.5, 0.2), radius: 0.45, start: 0.0, end: PI }],
|
||||
GdtSymbol::CircularRunout => vec![line(&[(0.2, 0.05), (0.7, 0.95)]), arrow(v(0.7, 0.95), v(0.5, 0.9))],
|
||||
GdtSymbol::TotalRunout => vec![
|
||||
line(&[(0.05, 0.05), (0.5, 0.05)]),
|
||||
line(&[(0.05, 0.05), (0.45, 0.95)]),
|
||||
line(&[(0.5, 0.05), (0.9, 0.95)]),
|
||||
arrow(v(0.45, 0.95), v(0.4, 0.9)),
|
||||
arrow(v(0.9, 0.95), v(0.4, 0.9)),
|
||||
],
|
||||
GdtSymbol::Diameter => vec![circle(0.5, 0.5, 0.35), line(&[(0.1, 0.0), (0.9, 1.0)])],
|
||||
GdtSymbol::Mmc => circled(vec![line(&[(0.3, 0.25), (0.3, 0.75), (0.5, 0.45), (0.7, 0.75), (0.7, 0.25)])]),
|
||||
GdtSymbol::Lmc => circled(vec![line(&[(0.38, 0.75), (0.38, 0.25), (0.65, 0.25)])]),
|
||||
GdtSymbol::Rfs => circled(vec![
|
||||
GdtPrim::Arc { center: v(0.5, 0.625), radius: 0.125, start: 20f64.to_radians(), end: 3.0 * FRAC_PI_2 },
|
||||
GdtPrim::Arc { center: v(0.5, 0.375), radius: 0.125, start: 200f64.to_radians(), end: FRAC_PI_2 },
|
||||
]),
|
||||
GdtSymbol::Projected => circled(vec![
|
||||
line(&[(0.5, 0.5), (0.38, 0.5), (0.38, 0.75), (0.5, 0.75)]),
|
||||
GdtPrim::Arc { center: v(0.5, 0.625), radius: 0.125, start: -FRAC_PI_2, end: FRAC_PI_2 },
|
||||
line(&[(0.38, 0.5), (0.38, 0.25)]),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_symbols_compartments_and_lines() {
|
||||
let rows = parse_frame("{\\Fgdt;j}%%v{\\Fgdt;n}0.1{\\Fgdt;m}%%vA%%vB^J2.5{\\Fgdt;p}");
|
||||
assert_eq!(rows.len(), 2);
|
||||
let r0 = &rows[0];
|
||||
assert_eq!(r0.len(), 4);
|
||||
assert_eq!(r0[0], vec![GdtRun::Symbol(GdtSymbol::Position)]);
|
||||
assert_eq!(r0[1], vec![GdtRun::Symbol(GdtSymbol::Diameter), GdtRun::Text("0.1".into()), GdtRun::Symbol(GdtSymbol::Mmc)]);
|
||||
assert_eq!(r0[3], vec![GdtRun::Text("B".into())]);
|
||||
assert_eq!(rows[1][0], vec![GdtRun::Text("2.5".into()), GdtRun::Symbol(GdtSymbol::Projected)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_then_parse() {
|
||||
let f = GdtFrame {
|
||||
lines: vec![GdtLine {
|
||||
symbol: Some(GdtSymbol::Flatness),
|
||||
tol1: Some(GdtTol { diameter: false, value: "0.05".into(), material: None }),
|
||||
tol2: None,
|
||||
datums: vec![GdtDatum { letter: "A".into(), material: Some("L".into()) }],
|
||||
}],
|
||||
height: String::new(),
|
||||
projected: false,
|
||||
datum_id: "B".into(),
|
||||
};
|
||||
let s = compose_frame(&f);
|
||||
assert_eq!(s, "{\\Fgdt;c}%%v0.05%%v%%vA{\\Fgdt;l}^JB");
|
||||
let rows = parse_frame(&s);
|
||||
assert_eq!(rows[0][3], vec![GdtRun::Text("A".into()), GdtRun::Symbol(GdtSymbol::Lmc)]);
|
||||
assert_eq!(rows[1][0], vec![GdtRun::Text("B".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_strings_do_not_panic() {
|
||||
let huge = "%%v{\\Fgdt;j}".repeat(100_000);
|
||||
for s in ["{\\Fgdt;", "{\\Fgdt", "\\", "%%", "%", "{{{{", "}}}}", "^", "\\P\\P\\P", "{\\H", &huge, "\u{0}%%v\u{ffff}"] {
|
||||
let rows = parse_frame(s);
|
||||
assert!(rows.len() <= MAX_FRAME_ROWS && rows.iter().all(|r| r.len() <= MAX_FRAME_CELLS));
|
||||
let l = layout_frame(s, Vec2::new(f64::NAN, 1.0), f64::INFINITY, f64::NAN, -1.0, &|t, h| t.len() as f64 * h);
|
||||
assert!(l.frame.iter().all(|p| matches!(p, GdtPrim::Line(v) if v.iter().all(|q| q.is_finite()))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_boxes_each_compartment() {
|
||||
let l = layout_frame("{\\Fgdt;j}%%v0.1%%vA", Vec2::ZERO, 0.0, 1.0, 0.5, &|t, h| t.chars().count() as f64 * h);
|
||||
// Three cells: four separators, top and bottom.
|
||||
assert_eq!(l.frame.len(), 6);
|
||||
assert_eq!(l.texts.len(), 2);
|
||||
assert!((l.bounds.min.y + 1.0).abs() < 1e-9 && (l.bounds.max.y - 1.0).abs() < 1e-9);
|
||||
assert!(l.bounds.min.x.abs() < 1e-9 && l.bounds.max.x > 6.0);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
mod constraint;
|
||||
mod entity;
|
||||
mod extents;
|
||||
mod gdt;
|
||||
mod header;
|
||||
pub mod library;
|
||||
mod plotstyle;
|
||||
@@ -22,6 +23,7 @@ pub use cadcraft_geom as geom;
|
||||
pub use constraint::*;
|
||||
pub use entity::*;
|
||||
pub use extents::{MAX_BLOCK_DEPTH, entity_bounds};
|
||||
pub use gdt::*;
|
||||
pub use header::{HVal, Header};
|
||||
pub use plotstyle::*;
|
||||
pub use store::EntityStore;
|
||||
|
||||
@@ -177,7 +177,11 @@ fn candidates(d: &Drawing, kind: Kind) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
Kind::Dimstyles => {
|
||||
let used = lower(&mut entities(d).filter_map(|e| if let EntityKind::Dimension(dm) = &e.kind { Some(dm.style.clone()) } else { None }));
|
||||
let used = lower(&mut entities(d).filter_map(|e| match &e.kind {
|
||||
EntityKind::Dimension(dm) => Some(dm.style.clone()),
|
||||
EntityKind::Tolerance(t) => Some(t.style.clone()),
|
||||
_ => None,
|
||||
}));
|
||||
let cur = d.header.str("DIMSTYLE", "Standard");
|
||||
d.dim_styles
|
||||
.iter()
|
||||
|
||||
@@ -211,6 +211,9 @@ impl Used {
|
||||
EntityKind::Leader(l) => {
|
||||
self.dim_styles.insert(up(&l.style));
|
||||
}
|
||||
EntityKind::Tolerance(t) => {
|
||||
self.dim_styles.insert(up(&t.style));
|
||||
}
|
||||
EntityKind::MLeader(m) => {
|
||||
self.mleader_styles.insert(up(&m.style));
|
||||
self.text_styles.extend(m.text.iter().map(|t| up(&t.style)));
|
||||
|
||||
@@ -28,6 +28,7 @@ mod qselect;
|
||||
mod settings;
|
||||
mod spline_opts;
|
||||
mod table;
|
||||
mod tolerance;
|
||||
mod trimextend;
|
||||
mod utility;
|
||||
mod view;
|
||||
@@ -180,6 +181,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(settings::specs());
|
||||
v.extend(utility::specs());
|
||||
v.extend(table::specs());
|
||||
v.extend(tolerance::specs());
|
||||
v.extend(constraints::specs());
|
||||
v
|
||||
})
|
||||
|
||||
@@ -1256,6 +1256,7 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
EntityKind::Tolerance(t) => Some(super::tolerance::explode(d, e, t)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1439,6 +1439,10 @@ pub(crate) fn flatten_kind(k: &mut EntityKind) {
|
||||
i.v.z = 0.0;
|
||||
}
|
||||
EntityKind::Table(t) => z(&mut t.insert),
|
||||
EntityKind::Tolerance(t) => {
|
||||
z(&mut t.insert);
|
||||
t.dir.z = 0.0;
|
||||
}
|
||||
EntityKind::Spline(_) | EntityKind::Viewport(_) | EntityKind::Wipeout(_) | EntityKind::Unknown(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,6 +693,7 @@ fn dims_using(d: &cadcraft_doc::Drawing, name: &str) -> Vec<(cadcraft_doc::Space
|
||||
let uses = match &e.kind {
|
||||
EntityKind::Dimension(dm) => dm.style.eq_ignore_ascii_case(name),
|
||||
EntityKind::Leader(l) => l.style.eq_ignore_ascii_case(name),
|
||||
EntityKind::Tolerance(t) => t.style.eq_ignore_ascii_case(name),
|
||||
_ => false,
|
||||
};
|
||||
if uses {
|
||||
@@ -723,6 +724,7 @@ fn run_dimstyle_rename(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
d.modify_entity(h, |e| match &mut e.kind {
|
||||
EntityKind::Dimension(dm) => dm.style = to.clone(),
|
||||
EntityKind::Leader(l) => l.style = to.clone(),
|
||||
EntityKind::Tolerance(t) => t.style = to.clone(),
|
||||
_ => {}
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
//! TOLERANCE: geometric tolerances (feature control frames) and their explosion.
|
||||
|
||||
use cadcraft_doc::{Drawing, Entity, EntityKind, GdtDatum, GdtFrame, GdtLine, GdtPrim, GdtSymbol, GdtTol, Handle, MAX_FRAME_CHARS, Tolerance};
|
||||
use cadcraft_geom::{PolyVertex, Vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::helpers::{line, lwpoly, v3};
|
||||
use super::*;
|
||||
use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("tolerance", "Tolerance...", run_tolerance)
|
||||
.menu(&["Dimension", "Tolerance..."])
|
||||
.alias(&["tol"])
|
||||
.params(
|
||||
"{at: [x,y], symbol?: position|concentricity|symmetry|parallelism|perpendicularity|angularity|cylindricity|flatness|circularity|straightness|surfaceprofile|lineprofile|circularrunout|totalrunout, tol1?: \"%%c0.1(M)\" | {value, diameter?, material?: M|L|S}, tol2?, datums?: [\"A(M)\", \"B\", {letter, material?}] (up to 3), line2?: {symbol?, tol1?, tol2?, datums?}, height? (projected zone), projected?, datumId?, rotation? (degrees), style? (dimension style; default DIMSTYLE), text? (the frame string as in DXF, instead of the fields)}",
|
||||
)
|
||||
.interactive(|s| Ok(Box::new(ToleranceM::new(s)))),
|
||||
]
|
||||
}
|
||||
|
||||
/// A tolerance typed as text: an optional diameter prefix (`%%c`, `Ø`, `⌀`, `dia`), the value and
|
||||
/// an optional material condition suffix (`(M)`, `(L)`, `(S)`).
|
||||
pub fn parse_tol(s: &str) -> GdtTol {
|
||||
let mut t = s.trim();
|
||||
let mut diameter = false;
|
||||
for p in ["%%c", "%%C", "Ø", "ø", "⌀", "dia ", "DIA "] {
|
||||
if let Some(r) = t.strip_prefix(p) {
|
||||
diameter = true;
|
||||
t = r.trim_start();
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (rest, material) = split_material(t);
|
||||
GdtTol { diameter, value: rest.to_string(), material }
|
||||
}
|
||||
|
||||
/// A datum reference typed as text: the letter(s) and an optional `(M)` / `(L)` / `(S)` suffix.
|
||||
pub fn parse_datum(s: &str) -> GdtDatum {
|
||||
let (letter, material) = split_material(s.trim());
|
||||
GdtDatum { letter: letter.to_string(), material }
|
||||
}
|
||||
|
||||
fn split_material(t: &str) -> (&str, Option<String>) {
|
||||
let up = t.to_ascii_uppercase();
|
||||
for m in ["M", "L", "S"] {
|
||||
if up.ends_with(&format!("({m})")) {
|
||||
return (t.get(..t.len().saturating_sub(3)).unwrap_or("").trim_end(), Some(m.to_string()));
|
||||
}
|
||||
}
|
||||
(t, None)
|
||||
}
|
||||
|
||||
fn tol_value(v: Option<&Value>) -> Option<GdtTol> {
|
||||
match v? {
|
||||
Value::String(s) => Some(parse_tol(s)),
|
||||
Value::Number(n) => Some(GdtTol { value: n.to_string(), ..Default::default() }),
|
||||
o @ Value::Object(_) => Some(GdtTol {
|
||||
diameter: bool_or(o, "diameter", false),
|
||||
value: match o.get("value") {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(Value::Number(n)) => n.to_string(),
|
||||
_ => String::new(),
|
||||
},
|
||||
material: str_param(o, "material").map(str::to_string),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn datums_value(v: Option<&Value>) -> Vec<GdtDatum> {
|
||||
let Some(a) = v.and_then(Value::as_array) else { return Vec::new() };
|
||||
a.iter()
|
||||
.take(3)
|
||||
.filter_map(|d| match d {
|
||||
Value::String(s) => Some(parse_datum(s)),
|
||||
o @ Value::Object(_) => {
|
||||
Some(GdtDatum { letter: str_param(o, "letter").unwrap_or("").to_string(), material: str_param(o, "material").map(str::to_string) })
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn symbol_value(cmd: &str, v: Option<&Value>) -> Result<Option<GdtSymbol>> {
|
||||
let Some(s) = v.and_then(Value::as_str).map(str::trim).filter(|s| !s.is_empty() && !s.eq_ignore_ascii_case("none")) else {
|
||||
return Ok(None);
|
||||
};
|
||||
match GdtSymbol::parse(s).or_else(|| s.chars().next().filter(|_| s.chars().count() == 1).and_then(GdtSymbol::from_code)) {
|
||||
Some(sym) if sym.is_characteristic() => Ok(Some(sym)),
|
||||
_ => Err(bad(cmd, format!("unknown geometric characteristic `{s}`"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn line_value(cmd: &str, p: &Value) -> Result<GdtLine> {
|
||||
Ok(GdtLine {
|
||||
symbol: symbol_value(cmd, p.get("symbol"))?,
|
||||
tol1: tol_value(p.get("tol1")),
|
||||
tol2: tol_value(p.get("tol2")),
|
||||
datums: datums_value(p.get("datums")),
|
||||
})
|
||||
}
|
||||
|
||||
/// The frame string the parameters describe.
|
||||
fn frame_text(p: &Value) -> Result<String> {
|
||||
if let Some(t) = str_param(p, "text") {
|
||||
return Ok(t.chars().take(MAX_FRAME_CHARS).collect());
|
||||
}
|
||||
let mut lines = vec![line_value("tolerance", p)?];
|
||||
if let Some(l2) = p.get("line2").filter(|v| v.is_object()) {
|
||||
lines.push(line_value("tolerance", l2)?);
|
||||
}
|
||||
let height = match p.get("height") {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(Value::Number(n)) => n.to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let f = GdtFrame { lines, height, projected: bool_or(p, "projected", false), datum_id: str_param(p, "datumId").unwrap_or("").to_string() };
|
||||
Ok(cadcraft_doc::compose_frame(&f))
|
||||
}
|
||||
|
||||
fn current_dimstyle(s: &Session) -> String {
|
||||
s.doc().map(|d| d.header.str("DIMSTYLE", "Standard")).unwrap_or_else(|_| "Standard".into())
|
||||
}
|
||||
|
||||
fn make(style: String, at: Vec2, rotation: f64, text: String) -> Tolerance {
|
||||
Tolerance { insert: v3(at), dir: Vec2::from_angle(rotation).to3(0.0), style, text }
|
||||
}
|
||||
|
||||
fn run_tolerance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let at = point_req("tolerance", p, "at")?;
|
||||
let text = frame_text(p)?;
|
||||
if cadcraft_doc::parse_frame(&text).iter().flatten().all(cadcraft_doc::compartment_is_empty) {
|
||||
return Err(bad("tolerance", "the feature control frame is empty"));
|
||||
}
|
||||
let style = str_param(p, "style").map(str::to_string).unwrap_or_else(|| current_dimstyle(s));
|
||||
if s.doc()?.dim_style(&style).is_none() {
|
||||
return Err(bad("tolerance", format!("no dimension style `{style}`")));
|
||||
}
|
||||
let t = make(style, at, f64_or(p, "rotation", 0.0).to_radians(), text.clone());
|
||||
let h = s.add_entity(EntityKind::Tolerance(t))?;
|
||||
Ok(json!({ "handle": h.hex(), "text": text }))
|
||||
}
|
||||
|
||||
/// TOLERANCE at the command line: characteristic, two tolerance values, up to three datums,
|
||||
/// then the location.
|
||||
struct ToleranceM {
|
||||
step: usize,
|
||||
line: GdtLine,
|
||||
style: String,
|
||||
}
|
||||
|
||||
const SYMBOL_KW: &[&str] = &[
|
||||
"POsition",
|
||||
"COncentricity",
|
||||
"SYmmetry",
|
||||
"PArallelism",
|
||||
"PErpendicularity",
|
||||
"ANgularity",
|
||||
"CYlindricity",
|
||||
"Flatness",
|
||||
"CIrcularity",
|
||||
"STraightness",
|
||||
"SUrface",
|
||||
"LIne",
|
||||
"Runout",
|
||||
"TOtal",
|
||||
"None",
|
||||
];
|
||||
|
||||
/// Free input split at spaces, Enter meaningful.
|
||||
const VALUE: Accept = Accept { point: false, number: false, text: false, select: false, enter: true };
|
||||
|
||||
impl ToleranceM {
|
||||
fn new(s: &Session) -> Self {
|
||||
ToleranceM { step: 0, line: GdtLine { symbol: Some(GdtSymbol::Position), ..Default::default() }, style: current_dimstyle(s) }
|
||||
}
|
||||
fn text(&self) -> String {
|
||||
cadcraft_doc::compose_frame(&GdtFrame { lines: vec![self.line.clone()], ..Default::default() })
|
||||
}
|
||||
fn at_location(&mut self, s: &mut Session) -> Step {
|
||||
self.step = 6;
|
||||
if cadcraft_doc::parse_frame(&self.text()).iter().flatten().all(cadcraft_doc::compartment_is_empty) {
|
||||
s.echo("The feature control frame is empty.");
|
||||
return Step::Cancel;
|
||||
}
|
||||
Step::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for ToleranceM {
|
||||
fn name(&self) -> &'static str {
|
||||
"TOLERANCE"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
match self.step {
|
||||
0 => Prompt::new("Enter geometric characteristic", VALUE).kw(SYMBOL_KW).default("Position"),
|
||||
1 => Prompt::new("Enter first tolerance value", VALUE).default("none"),
|
||||
2 => Prompt::new("Enter second tolerance value", VALUE).default("none"),
|
||||
n @ 3..=5 => Prompt::new(format!("Enter datum reference {}", n - 2), VALUE).default("none"),
|
||||
_ => Prompt::new("Enter tolerance location", Accept::POINT),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if i == Input::Cancel {
|
||||
return Ok(Step::Cancel);
|
||||
}
|
||||
let text = match &i {
|
||||
Input::Text(t) | Input::Keyword(t) => Some(t.trim().to_string()),
|
||||
_ => None,
|
||||
};
|
||||
let none = |t: &Option<String>| t.as_deref().is_none_or(|t| t.is_empty() || t.eq_ignore_ascii_case("none"));
|
||||
match self.step {
|
||||
0 => {
|
||||
match (&i, &text) {
|
||||
(Input::Enter, _) => {}
|
||||
(_, Some(t)) if t.eq_ignore_ascii_case("none") => self.line.symbol = None,
|
||||
(_, Some(t)) => match GdtSymbol::parse(t).filter(|g| g.is_characteristic()) {
|
||||
Some(g) => self.line.symbol = Some(g),
|
||||
None => {
|
||||
s.echo("Requires a geometric characteristic or None.");
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
},
|
||||
_ => return Ok(Step::Continue),
|
||||
}
|
||||
self.step = 1;
|
||||
}
|
||||
1 | 2 => {
|
||||
if !matches!(i, Input::Enter | Input::Text(_) | Input::Keyword(_)) {
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
let v = (!none(&text)).then(|| parse_tol(text.as_deref().unwrap_or("")));
|
||||
if self.step == 1 {
|
||||
self.line.tol1 = v;
|
||||
} else {
|
||||
self.line.tol2 = v;
|
||||
}
|
||||
self.step += 1;
|
||||
}
|
||||
3..=5 => {
|
||||
if !matches!(i, Input::Enter | Input::Text(_) | Input::Keyword(_)) {
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
if none(&text) {
|
||||
return Ok(self.at_location(s));
|
||||
}
|
||||
self.line.datums.push(parse_datum(text.as_deref().unwrap_or("")));
|
||||
if self.step == 5 {
|
||||
return Ok(self.at_location(s));
|
||||
}
|
||||
self.step += 1;
|
||||
}
|
||||
_ => match i {
|
||||
Input::Point(p) => {
|
||||
let t = make(self.style.clone(), p, 0.0, self.text());
|
||||
s.add_entity(EntityKind::Tolerance(t))?;
|
||||
return Ok(Step::Done);
|
||||
}
|
||||
Input::Enter => return Ok(Step::Cancel),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
fn preview(&self, _s: &Session, c: Vec2) -> Vec<EntityKind> {
|
||||
if self.step == 6 { vec![EntityKind::Tolerance(make(self.style.clone(), c, 0.0, self.text()))] } else { Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPLODE of a feature control frame: lines, circles, arcs, solids (arrowheads) and texts in the
|
||||
/// dimension style's text style.
|
||||
pub(crate) fn explode(d: &Drawing, e: &Entity, t: &Tolerance) -> Vec<Entity> {
|
||||
let g = cadcraft_render::tolerance_in(d, t);
|
||||
let st = d.dim_style(&t.style).cloned().unwrap_or_default();
|
||||
let wf = d.text_style(&st.text_style).map(|s| s.width_factor).filter(|w| w.is_finite() && *w > 0.0).unwrap_or(1.0);
|
||||
let mk = |kind| Entity { handle: Handle(0), common: e.common.clone(), kind };
|
||||
let mut v = Vec::new();
|
||||
for p in g.frame.iter().chain(&g.symbols) {
|
||||
match p {
|
||||
GdtPrim::Line(pts) => match pts.as_slice() {
|
||||
[a, b] => v.push(mk(line(*a, *b))),
|
||||
pts if pts.len() > 2 => v.push(mk(lwpoly(pts.iter().map(|q| PolyVertex::new(*q)).collect(), false))),
|
||||
_ => {}
|
||||
},
|
||||
GdtPrim::Circle { center, radius } => v.push(mk(super::helpers::circle(*center, *radius))),
|
||||
GdtPrim::Arc { center, radius, start, end } => v.push(mk(EntityKind::Arc(cadcraft_doc::Arc {
|
||||
center: v3(*center),
|
||||
radius: *radius,
|
||||
start: cadcraft_geom::norm_angle(*start),
|
||||
end: cadcraft_geom::norm_angle(*end),
|
||||
}))),
|
||||
GdtPrim::Fill([a, b, c]) => v.push(mk(EntityKind::Solid(cadcraft_doc::Solid { corners: [v3(*a), v3(*b), v3(*c), v3(*c)] }))),
|
||||
}
|
||||
}
|
||||
for tx in &g.texts {
|
||||
v.push(mk(EntityKind::Text(cadcraft_doc::Text {
|
||||
insert: v3(tx.pos),
|
||||
align_pt: None,
|
||||
height: tx.height,
|
||||
value: tx.value.clone(),
|
||||
rotation: tx.rotation,
|
||||
width_factor: wf,
|
||||
oblique: 0.0,
|
||||
style: st.text_style.clone(),
|
||||
halign: Default::default(),
|
||||
valign: Default::default(),
|
||||
})));
|
||||
}
|
||||
v
|
||||
}
|
||||
@@ -93,7 +93,7 @@ pub fn stretch_grip(kind: &EntityKind, index: usize, to: Vec2) -> Option<EntityK
|
||||
EntityKind::Point(_) | EntityKind::Text(_) | EntityKind::MText(_) | EntityKind::AttDef(_) | EntityKind::Image(_) | EntityKind::Table(_) => {
|
||||
return Some(translated(kind, delta));
|
||||
}
|
||||
EntityKind::Hatch(_) => return Some(translated(kind, delta)),
|
||||
EntityKind::Hatch(_) | EntityKind::Tolerance(_) => return Some(translated(kind, delta)),
|
||||
EntityKind::Circle(c) => {
|
||||
if index == 0 {
|
||||
return Some(translated(kind, delta));
|
||||
|
||||
@@ -15,6 +15,7 @@ pub fn hit_polylines(d: &Drawing, e: &Entity, tol: f64) -> Vec<Vec<Vec2>> {
|
||||
| EntityKind::Dimension(_)
|
||||
| EntityKind::AttDef(_)
|
||||
| EntityKind::Table(_)
|
||||
| EntityKind::Tolerance(_)
|
||||
| EntityKind::MLeader(_)
|
||||
| EntityKind::Image(_) => {
|
||||
// Text-like objects are hit inside their box.
|
||||
@@ -75,6 +76,7 @@ pub fn entity_distance(d: &Drawing, e: &Entity, p: Vec2, tol: f64) -> f64 {
|
||||
let polys = hit_polylines(d, e, tol);
|
||||
let mut best = polys.iter().map(|pl| dist_to_polyline(pl, p)).fold(f64::INFINITY, f64::min);
|
||||
let filled = matches!(e.kind, EntityKind::Text(_) | EntityKind::MText(_) | EntityKind::AttDef(_) | EntityKind::Solid(_) | EntityKind::Table(_))
|
||||
|| matches!(e.kind, EntityKind::Tolerance(_))
|
||||
|| matches!(&e.kind, EntityKind::Hatch(h) if h.solid);
|
||||
if filled && entity_bounds(d, e, 0).contains(p) {
|
||||
best = 0.0;
|
||||
|
||||
@@ -219,6 +219,7 @@ pub(crate) fn gather(
|
||||
match &e.kind {
|
||||
EntityKind::Text(t) => cands.push((mode::INS, t.justify_point())),
|
||||
EntityKind::MText(t) => cands.push((mode::INS, t.insert.xy())),
|
||||
EntityKind::Tolerance(t) => cands.push((mode::INS, t.insert.xy())),
|
||||
EntityKind::Insert(i) => {
|
||||
cands.push((mode::INS, i.insert.xy()));
|
||||
cands.extend(i.attribs.iter().filter(|a| !a.invisible).map(|a| (mode::INS, a.text.justify_point())));
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//! TOLERANCE (#420): feature control frames from the JSON form and the command line, edited by
|
||||
//! MOVE / ROTATE / MIRROR, exploded into plain geometry and text, hostile input refused.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{EntityKind, Tolerance};
|
||||
use cadcraft_engine::geom::Vec3;
|
||||
use serde_json::json;
|
||||
|
||||
fn tolerance(s: &Session) -> (String, Tolerance) {
|
||||
let d = s.doc().unwrap();
|
||||
d.model
|
||||
.iter()
|
||||
.find_map(|e| match &e.kind {
|
||||
EntityKind::Tolerance(t) => Some((e.handle.hex(), t.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.expect("a tolerance")
|
||||
}
|
||||
|
||||
fn near(a: Vec3, b: Vec3) -> bool {
|
||||
(a.x - b.x).abs() < 1e-9 && (a.y - b.y).abs() < 1e-9
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_form_composes_the_frame() {
|
||||
let mut s = Session::new();
|
||||
let r = s
|
||||
.execute(
|
||||
"tolerance",
|
||||
&json!({
|
||||
"at": [10, 20],
|
||||
"symbol": "position",
|
||||
"tol1": "%%c0.1(M)",
|
||||
"datums": ["A", "B(L)", { "letter": "C", "material": "S" }],
|
||||
"line2": { "symbol": "flatness", "tol1": { "value": 0.02 } },
|
||||
"height": "2.5",
|
||||
"projected": true,
|
||||
"datumId": "D",
|
||||
"rotation": 90
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let want = "{\\Fgdt;j}%%v{\\Fgdt;n}0.1{\\Fgdt;m}%%v%%vA%%vB{\\Fgdt;l}%%vC{\\Fgdt;s}^J{\\Fgdt;c}%%v0.02^J2.5{\\Fgdt;p}^JD";
|
||||
assert_eq!(r["text"], want);
|
||||
let (h, t) = tolerance(&s);
|
||||
assert_eq!(r["handle"], h);
|
||||
assert_eq!(t.text, want);
|
||||
assert_eq!(t.style, "Standard");
|
||||
assert!(near(t.insert, Vec3::new(10.0, 20.0, 0.0)) && near(t.dir, Vec3::new(0.0, 1.0, 0.0)), "{t:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_line_prompts() {
|
||||
let mut s = Session::new();
|
||||
// Characteristic, two tolerance values (the second skipped), datums until an empty input,
|
||||
// then the location.
|
||||
s.script("TOLERANCE\nposition\n%%c0.1(M)\n\nA\nB(M)\n\n5,5\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let (_, t) = tolerance(&s);
|
||||
assert_eq!(t.text, "{\\Fgdt;j}%%v{\\Fgdt;n}0.1{\\Fgdt;m}%%v%%vA%%vB{\\Fgdt;m}");
|
||||
assert!(near(t.insert, Vec3::new(5.0, 5.0, 0.0)));
|
||||
|
||||
// One line with keyword shortcuts; "none" skips a value, the third datum ends the prompts.
|
||||
let mut s = Session::new();
|
||||
s.cmdline("tol F 0.05 none A B C 1,2").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
let (_, t) = tolerance(&s);
|
||||
assert_eq!(t.text, "{\\Fgdt;c}%%v0.05%%v%%vA%%vB%%vC");
|
||||
assert!(near(t.insert, Vec3::new(1.0, 2.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_line_rejects_bad_symbols_and_empty_frames() {
|
||||
let mut s = Session::new();
|
||||
s.cmdline("tolerance").unwrap();
|
||||
s.cmdline("mmc").unwrap();
|
||||
// A material condition is no characteristic: the prompt repeats.
|
||||
assert!(s.prompt_text().contains("geometric characteristic"), "{}", s.prompt_text());
|
||||
s.script("None\n\n\n\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
assert!(s.doc().unwrap().model.iter().all(|e| !matches!(e.kind, EntityKind::Tolerance(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transforms_and_grips() {
|
||||
let mut s = Session::new();
|
||||
s.execute("tolerance", &json!({ "at": [1, 1], "symbol": "flatness", "tol1": "0.1" })).unwrap();
|
||||
let (h, t0) = tolerance(&s);
|
||||
assert_eq!(EntityKind::Tolerance(t0.clone()).grips().len(), 1);
|
||||
s.execute("move", &json!({ "handles": [h], "delta": [2, 3] })).unwrap();
|
||||
assert!(near(tolerance(&s).1.insert, Vec3::new(3.0, 4.0, 0.0)));
|
||||
s.execute("rotate", &json!({ "handles": [h], "base": [0, 0], "angle": 90 })).unwrap();
|
||||
let t = tolerance(&s).1;
|
||||
assert!(near(t.insert, Vec3::new(-4.0, 3.0, 0.0)) && near(t.dir, Vec3::new(0.0, 1.0, 0.0)), "{t:?}");
|
||||
// Mirrored frames keep reading the same way (like text with MIRRTEXT 0).
|
||||
s.execute("mirror", &json!({ "handles": [h], "p1": [0, 0], "p2": [0, 1], "erase": true })).unwrap();
|
||||
let t = tolerance(&s).1;
|
||||
assert!(near(t.insert, Vec3::new(4.0, 3.0, 0.0)) && near(t.dir, Vec3::new(0.0, 1.0, 0.0)), "{t:?}");
|
||||
assert_eq!(t.text, t0.text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explode_gives_lines_symbols_and_text() {
|
||||
let mut s = Session::new();
|
||||
s.execute("tolerance", &json!({ "at": [0, 0], "symbol": "position", "tol1": "%%c0.1(M)", "datums": ["A"] })).unwrap();
|
||||
let (h, _) = tolerance(&s);
|
||||
let r = s.execute("explode", &json!({ "handles": [h] })).unwrap();
|
||||
assert!(r["handles"].as_array().unwrap().len() > 8, "{r}");
|
||||
let d = s.doc().unwrap();
|
||||
let kinds: Vec<&EntityKind> = d.model.iter().map(|e| &e.kind).collect();
|
||||
assert!(!kinds.iter().any(|k| matches!(k, EntityKind::Tolerance(_))));
|
||||
let texts: Vec<&str> = kinds
|
||||
.iter()
|
||||
.filter_map(|k| match k {
|
||||
EntityKind::Text(t) => Some(t.value.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(texts, ["0.1", "A"]);
|
||||
// Position and diameter symbols and the circled M: circles; the boxes: lines.
|
||||
assert_eq!(kinds.iter().filter(|k| matches!(k, EntityKind::Circle(_))).count(), 3);
|
||||
assert!(kinds.iter().filter(|k| matches!(k, EntityKind::Line(_))).count() >= 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_params_are_refused_without_panicking() {
|
||||
let mut s = Session::new();
|
||||
for p in [
|
||||
json!({ "at": [0, 0] }),
|
||||
json!({ "at": [0, 0], "symbol": "nonsense" }),
|
||||
json!({ "at": [0, 0], "symbol": "mmc" }),
|
||||
json!({ "symbol": "position" }),
|
||||
json!({ "at": [0, 0], "symbol": "position", "style": "NoSuchStyle" }),
|
||||
json!({ "at": [0, 0], "text": "" }),
|
||||
] {
|
||||
assert!(s.execute("tolerance", &p).is_err(), "{p}");
|
||||
}
|
||||
let huge = "{\\Fgdt;j}%%v".repeat(100_000);
|
||||
for p in [
|
||||
json!({ "at": [0, 0], "text": "{\\Fgdt;" }),
|
||||
json!({ "at": [0, 0], "text": "x{\\Fgdt;j" }),
|
||||
json!({ "at": [0, 0], "text": huge }),
|
||||
json!({ "at": [0, 0], "symbol": "position", "tol1": "x".repeat(100_000), "datums": [1, null, {}, "A"], "rotation": 1e308 }),
|
||||
json!({ "at": [1e308, -1e308], "tol1": { "value": "%%v^J{}\\" }, "height": 1e300 }),
|
||||
] {
|
||||
if s.execute("tolerance", &p).is_ok() {
|
||||
let t = tolerance(&s).1;
|
||||
assert!(t.text.chars().count() <= cadcraft_engine::doc::MAX_FRAME_CHARS);
|
||||
let _ = s.execute("explode", &json!({ "handles": [tolerance(&s).0] }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,6 +442,16 @@ fn entity(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {
|
||||
}
|
||||
// The file path comes from the IMAGEDEF object (`dxf_image::resolve`).
|
||||
"IMAGE" => EntityKind::Image(crate::dxf_image::read_entity(tags)),
|
||||
"TOLERANCE" => {
|
||||
let dir = t.p(11);
|
||||
let dir = if dir.xy().is_finite() && dir.xy().len() > 1e-12 { dir } else { Vec3::new(1.0, 0.0, 0.0) };
|
||||
EntityKind::Tolerance(Tolerance {
|
||||
insert: t.p(10),
|
||||
dir,
|
||||
style: t.s(3).unwrap_or_else(|| "Standard".into()),
|
||||
text: t.s(1).unwrap_or_default().chars().take(MAX_FRAME_CHARS).collect(),
|
||||
})
|
||||
}
|
||||
other => EntityKind::Unknown(Unknown {
|
||||
dxf_type: other.to_string(),
|
||||
tags: tags.iter().map(|tg| RawTag { code: tg.code, value: tg.str() }).collect(),
|
||||
|
||||
@@ -924,6 +924,14 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
|
||||
w.p2(14, Vec2::new((q.x - o.x) / sx - 0.5, 0.5 - (q.y - o.y) / sy));
|
||||
}
|
||||
}
|
||||
EntityKind::Tolerance(t) => {
|
||||
w.s(0, "TOLERANCE");
|
||||
common(w, e, owner, paper, "AcDbFcf");
|
||||
w.s(3, &t.style);
|
||||
w.p(10, t.insert);
|
||||
w.s(1, &t.text);
|
||||
w.p(11, t.dir);
|
||||
}
|
||||
// Entity types CADCraft doesn't model aren't written; the save command reports them.
|
||||
EntityKind::Unknown(_) => {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//! TOLERANCE entities (feature control frames, DXF Reference group codes 3, 10, 1, 11) are read,
|
||||
//! drawn, written and read back; hostile frame strings never panic.
|
||||
|
||||
use cadcraft_doc::*;
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{read_dxf, write_dxf};
|
||||
|
||||
const FRAME: &str = "{\\Fgdt;j}%%v{\\Fgdt;n}0.1{\\Fgdt;m}%%vA%%vB{\\Fgdt;l}%%vC^J{\\Fgdt;j}%%v0.05%%vA";
|
||||
|
||||
fn dxf(text: &str, dir: (&str, &str)) -> String {
|
||||
format!(
|
||||
"0\nSECTION\n2\nENTITIES\n0\nTOLERANCE\n5\n2A\n8\nGDT\n100\nAcDbEntity\n100\nAcDbFcf\n3\nStandard\n10\n5\n20\n6\n30\n0\n1\n{text}\n11\n{}\n21\n{}\n31\n0\n0\nENDSEC\n0\nEOF\n",
|
||||
dir.0, dir.1
|
||||
)
|
||||
}
|
||||
|
||||
fn tolerances(d: &Drawing) -> Vec<(Common, Tolerance)> {
|
||||
d.model
|
||||
.iter()
|
||||
.filter_map(|e| match &e.kind {
|
||||
EntityKind::Tolerance(t) => Some((e.common.clone(), t.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tolerance_reads_draws_and_roundtrips() {
|
||||
let d = read_dxf(dxf(FRAME, ("0", "1")).as_bytes()).expect("read");
|
||||
let ts = tolerances(&d);
|
||||
assert_eq!(ts.len(), 1, "read as a TOLERANCE, not kept as unknown data");
|
||||
let (common, t) = &ts[0];
|
||||
assert_eq!(common.layer, "GDT");
|
||||
assert_eq!((t.insert, t.dir, t.style.as_str(), t.text.as_str()), (Vec3::new(5.0, 6.0, 0.0), Vec3::new(0.0, 1.0, 0.0), "Standard", FRAME));
|
||||
assert!((t.rotation() - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
|
||||
|
||||
// Drawn: boxes, symbols and text, turned a quarter turn about the insertion point: the frame
|
||||
// grows along +Y from it, its first line straddles x = 5 and the second lies at larger x.
|
||||
let list = cadcraft_render::build(&d, &Space::Model, &cadcraft_render::Options::default());
|
||||
assert!(list.prims.len() > 20, "{} prims", list.prims.len());
|
||||
let b = d.extents(&Space::Model);
|
||||
let (h, _) = t.text_size(&d);
|
||||
assert!((b.min.y - 6.0).abs() < 1e-9 && b.max.y > 6.0 + 8.0 * h, "{b:?}");
|
||||
assert!((b.min.x - (5.0 - h)).abs() < 1e-9 && (b.max.x - (5.0 + 3.0 * h)).abs() < 1e-9, "{b:?}");
|
||||
assert!((list.bounds.min.y - 6.0).abs() < 1e-6 && list.bounds.max.x <= 5.0 + 3.0 * h + 1e-6, "{:?}", list.bounds);
|
||||
|
||||
let again = read_dxf(write_dxf(&d).as_bytes()).expect("reopen");
|
||||
assert_eq!(tolerances(&again), ts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_direction_defaults_to_x() {
|
||||
let text = dxf("A", ("0", "0"));
|
||||
let d = read_dxf(text.as_bytes()).expect("read");
|
||||
assert_eq!(tolerances(&d)[0].1.dir, Vec3::new(1.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_tolerance_does_not_panic() {
|
||||
let huge = "{\\Fgdt;j}%%v".repeat(200_000);
|
||||
let deep = "{".repeat(100_000);
|
||||
for (text, dir) in [
|
||||
("{\\Fgdt;", ("1", "0")),
|
||||
("{\\Fgdt", ("nan", "inf")),
|
||||
("\\", ("1e308", "1e308")),
|
||||
("%%", ("-0", "0")),
|
||||
("^J^J^J^J^J^J^J^J^J^J^J^J", ("1", "0")),
|
||||
(huge.as_str(), ("1", "0")),
|
||||
(deep.as_str(), ("1", "0")),
|
||||
("{\\H1e308x;\\Fgdt;jjjjjjjjjjjjjjjjjjjjjjjjjj}", ("1", "0")),
|
||||
] {
|
||||
if let Ok(d) = read_dxf(dxf(text, dir).as_bytes()) {
|
||||
let _ = cadcraft_render::build(&d, &Space::Model, &cadcraft_render::Options::default());
|
||||
let _ = d.extents(&Space::Model);
|
||||
let _ = read_dxf(write_dxf(&d).as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ pub mod paper;
|
||||
mod plotstyle;
|
||||
mod point;
|
||||
pub mod raster;
|
||||
mod tolerance;
|
||||
pub mod units;
|
||||
mod wide;
|
||||
|
||||
@@ -27,6 +28,7 @@ use cadcraft_geom::{Bounds2, Mat3, Polyline, Vec2};
|
||||
pub use dim::{ArrowGeom, Arrowhead, DimGeometry, DimText, LineRole, arrowhead, dimension_geometry, dimension_geometry_with, format_linear_value};
|
||||
pub use fill::triangulate_evenodd;
|
||||
pub use paper::{PAPER_SIZES, PaperSize, Sheet, paper_size, sheet};
|
||||
pub use tolerance::{gdt_points, tolerance_in};
|
||||
|
||||
/// Handle carried by model-space geometry drawn inside a paper-space viewport (it belongs to no
|
||||
/// single paper-space entity, so selection highlighting never matches it).
|
||||
@@ -669,6 +671,7 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
||||
b.polyline(ctx, rgb, lw, &[o + u, o + v]);
|
||||
}
|
||||
EntityKind::Table(t) => table(b, ctx, t, rgb, lw),
|
||||
EntityKind::Tolerance(t) => tolerance::draw(b, ctx, e, t, lw, tol),
|
||||
EntityKind::Leader(l) => {
|
||||
let pts: Vec<Vec2> = l.vertices.iter().map(|v| v.xy()).collect();
|
||||
stroke(b, &pts);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//! TOLERANCE: feature control frames. The layout lives in `cadcraft_doc::gdt`; here the text is
|
||||
//! measured with the dimension style's font and everything is drawn: the boxes with DIMCLRD,
|
||||
//! symbols and text with DIMCLRT (ByBlock = the entity's own colour, as for dimensions).
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{Drawing, Entity, GdtLayout, GdtPrim, Tolerance};
|
||||
use cadcraft_geom::{Arc as GArc, Circle as GCircle, Segment, Vec2};
|
||||
|
||||
use crate::{Builder, Ctx, Ink, ink};
|
||||
|
||||
/// The frame laid out with the font of its dimension style's text style.
|
||||
pub fn tolerance_in(d: &Drawing, t: &Tolerance) -> GdtLayout {
|
||||
let st = d.dim_style(&t.style).cloned().unwrap_or_default();
|
||||
let dt = crate::dim_text(d, &st);
|
||||
t.layout(d, &|s, h| cadcraft_fonts::text_width(&dt.font, s, h, dt.width_factor))
|
||||
}
|
||||
|
||||
/// Points of a frame primitive, tessellated at `tol` (a fill's triangle as is).
|
||||
pub fn gdt_points(p: &GdtPrim, tol: f64) -> Vec<Vec2> {
|
||||
let mut pts = Vec::new();
|
||||
match p {
|
||||
GdtPrim::Line(v) => pts.clone_from(v),
|
||||
GdtPrim::Circle { center, radius } => GCircle::new(*center, *radius).tessellate(tol, &mut pts),
|
||||
GdtPrim::Arc { center, radius, start, end } => {
|
||||
Segment::Arc { arc: GArc::new(*center, *radius, *start, *end), ccw: true }.tessellate(tol, &mut pts)
|
||||
}
|
||||
GdtPrim::Fill(t) => pts.extend_from_slice(t),
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
pub(crate) fn draw(b: &mut Builder, ctx: &Ctx, e: &Entity, t: &Tolerance, lw: f32, tol: f64) {
|
||||
let d = ctx.d;
|
||||
let st = d.dim_style(&t.style).cloned().unwrap_or_default();
|
||||
let layer_name = if e.common.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { e.common.layer.as_str() };
|
||||
let layer_color = ctx.layer_color(layer_name).unwrap_or(Color::Index(7));
|
||||
let own = match e.common.color {
|
||||
Color::ByLayer => layer_color,
|
||||
Color::ByBlock => ctx.block_color,
|
||||
c => c,
|
||||
};
|
||||
let frame_rgb: Ink = ink(st.dim_line_color, layer_color, own);
|
||||
let text_rgb: Ink = ink(st.text_color, layer_color, own);
|
||||
let g = tolerance_in(d, t);
|
||||
for p in &g.frame {
|
||||
b.polyline(ctx, frame_rgb, lw, &gdt_points(p, tol));
|
||||
}
|
||||
if !b.opts.text {
|
||||
return;
|
||||
}
|
||||
for p in &g.symbols {
|
||||
let pts = gdt_points(p, tol);
|
||||
if matches!(p, GdtPrim::Fill(_)) {
|
||||
b.tris(ctx, text_rgb, &pts);
|
||||
} else {
|
||||
b.polyline(ctx, text_rgb, lw, &pts);
|
||||
}
|
||||
}
|
||||
let dt = crate::dim_text(d, &st);
|
||||
for tx in &g.texts {
|
||||
let p = cadcraft_fonts::TextParams {
|
||||
rotation: tx.rotation,
|
||||
width_factor: dt.width_factor,
|
||||
oblique: dt.oblique,
|
||||
..cadcraft_fonts::TextParams::new(tx.pos, tx.height)
|
||||
};
|
||||
let (sh, _) = cadcraft_fonts::place(&dt.font, &tx.value, &p);
|
||||
b.shaped(ctx, text_rgb, lw, &sh);
|
||||
}
|
||||
}
|
||||
@@ -999,3 +999,4 @@
|
||||
Export... Експорт...
|
||||
Batch Publish... Пакетна публікація...
|
||||
Drawing Recovery Manager... Диспетчер відновлення креслень...
|
||||
Tolerance... Допуск...
|
||||
|
+1
-1
@@ -35,7 +35,7 @@ do them first.
|
||||
| F5 | Arrays are not associative: no ARRAYEDIT; ARRAY/ARRAYRECT/ARRAYPOLAR edit the array in an option loop, then create plain copies (ASsociative, Levels, polar ROWs and grip editing print "not available yet") | `cmd/array.rs` | Medium-high | 8–12 |
|
||||
| F6 | FILLET/CHAMFER/JOIN/LENGTHEN/EXTEND don't handle ellipses and splines; FILLET Trim/Multiple and polyline segment pairs; CHAMFER Angle/Method | [geometry-parity](geometry-parity.md#trim-extend-fillet-chamfer-break-join-lengthen--45) | Medium | 10–15 |
|
||||
| F7 | Dimensions: no DIMSTYLE manager dialog; 43 of ~80 DIMSTYLE variables (DIMTOFL, DIMATFIT, DIMTMOVE, DIMLTYPE, DIMLWD, DIMFXL, DIMTOLJ, DIMALTU …); no smart DIM; no DIMJOGGED, DIMJOGLINE, DIMBREAK, oblique DIMEDIT, inspection; QDIM continuous only (SETVAR DIM* now become style overrides, #129) | `doc/src/tables.rs:223-266`, `annotate.rs:64,364` | High for detailers | 25–35 |
|
||||
| F8 | No TOLERANCE (GD&T feature control frames) | menu "Dimension > Tolerance..." uncovered | Medium (mechanical) | 4–6 |
|
||||
| F8 | TOLERANCE leftovers (feature control frames landed, #420: model, our own symbol drawing, DXF, command-line prompts, explode, grips): no Geometric Tolerance dialog, no LEADER/QLEADER tolerance annotation, no in-place editing of a frame's contents | `cmd/tolerance.rs` | Low-medium (mechanical) | 2–4 |
|
||||
| F9 | MTEXT editor is a dialog box, not in place; no columns, bullets, numbering, tabs, indents; no fields (FIELD, UPDATEFIELD); no spell check (SPELL) | `ui-egui/dialogs.rs:188-240` | Medium-high | 30–45 |
|
||||
| F10 | MLEADER: no MLEADEREDIT/ALIGN/COLLECT; MLEADERSTYLE has 5 fields (block content and spline/no-leader types are per command, not in the style; block content has no attributes) | `annotate.rs`, `cmd/mleader_content.rs`, `tables.rs:417` | Medium | 8–12 |
|
||||
| F11 | Tables: no cell styles, formulas, data links, table breaking | `cmd/table.rs` | Medium | 10–15 |
|
||||
|
||||
Reference in new issue
Block a user