Files
cadcraft/crates/engine/src/cmd/annotate/linear.rs
T
Dylan M. Taylor 2721aa8982 Enforce Rust coding standards via Clippy (#2239)
* chore(clippy): enforce workspace Rust quality gates

* refactor(doc): use explicit entity imports

* refactor(constraints): split residual evaluation

* refactor(fonts): split mtext layout

* refactor(render): use explicit entity imports

* fix(fonts): retain height for empty paragraphs

* fix(doc): repair dimension and dynamic block links

* refactor(render): split entity drawing helpers

* docs: document workspace lint inheritance

* refactor(render): isolate dimension geometry

* refactor(engine): use explicit imports

* refactor(engine): split complex edit paths into helpers

* refactor(io): split DXF read and write helpers

* refactor(ui): split canvas interaction and drawing

* refactor(ui): split command and drawing helpers

* test(i18n): split locale preference checks

* fix(ui): use current egui scope id API

* fix(dim): preserve text offset and ellipse grip behavior
2026-10-27 15:55:09 -04:00

364 lines
15 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 super::common::{DimText, add_dim, assoc_enabled, circle_of, dim, linear_kind, pick_at};
use super::{
Accept, DimKind, Dimension, EngineError, EntityKind, Handle, Input, Interactive, Prim, Prompt, Result, Segment, Session, Step, Value, Vec2, bad,
json, point_req, points_param, str_param, v3,
};
pub(super) fn run_linear(s: &mut Session, p: &Value) -> Result<Value> {
let (a, b) = match p.get("object").and_then(|v| v.as_str().and_then(Handle::parse_hex).or_else(|| v.as_u64().map(Handle))) {
Some(h) => object_extent(s, h).ok_or_else(|| bad("dimlinear", "`object` must be a line, arc, circle or polyline segment"))?,
None => (point_req("dimlinear", p, "p1")?, point_req("dimlinear", p, "p2")?),
};
let at = point_req("dimlinear", p, "at")?;
let rot = p.get("rotation").and_then(Value::as_f64).map(f64::to_radians);
let k = dim(s, linear_kind(a, b, at, rot, false), at, a, b, Vec2::ZERO, str_param(p, "text").unwrap_or(""));
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
}
pub(super) fn run_aligned(s: &mut Session, p: &Value) -> Result<Value> {
let a = point_req("dimaligned", p, "p1")?;
let b = point_req("dimaligned", p, "p2")?;
let at = point_req("dimaligned", p, "at")?;
let k = dim(s, DimKind::Aligned, at, a, b, Vec2::ZERO, str_param(p, "text").unwrap_or(""));
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
}
pub(super) fn run_ordinate(s: &mut Session, p: &Value) -> Result<Value> {
let f = point_req("dimordinate", p, "feature")?;
let l = point_req("dimordinate", p, "leader")?;
let xtype = p.get("xtype").and_then(Value::as_bool).unwrap_or((l - f).y.abs() > (l - f).x.abs());
let k = dim(s, DimKind::Ordinate { x_type: xtype }, Vec2::ZERO, f, l, Vec2::ZERO, "");
Ok(json!({ "handle": add_dim(s, k)?.hex() }))
}
/// The last linear/aligned dimension (for CONTINUE/BASELINE).
fn last_linear(s: &Session) -> Option<Dimension> {
let h = s.last_dim?;
match &s.doc().ok()?.entity(h)?.kind {
EntityKind::Dimension(d) if matches!(d.kind, DimKind::Linear { .. } | DimKind::Aligned) => Some(d.clone()),
_ => None,
}
}
/// Next dimension in a chain from `prev` to a new second origin.
fn chained(s: &Session, prev: &Dimension, next: Vec2, baseline: bool) -> EntityKind {
// DIMDLI × the overall scale of the dimension being continued: its overrides (SETVAR DIM*)
// included, and DIMSCALE 0 falling back to the drawing's DIMSCALE as when it is drawn.
let spacing = s
.doc()
.ok()
.map(|d| {
let st = d.dim_style(&prev.style).cloned().unwrap_or_default().with_overrides(&prev.overrides);
st.baseline_spacing * st.effective_scale(d.header.f64("DIMSCALE", 1.0))
})
.filter(|v| v.is_finite())
.unwrap_or(0.38);
let dir = match prev.kind {
DimKind::Linear { rotation } => Vec2::from_angle(rotation),
_ => (prev.p14.xy() - prev.p13.xy()).normalized(),
};
let first = if baseline { prev.p13.xy() } else { prev.p14.xy() };
let mut defpt = prev.defpt.xy();
if baseline {
// Offset the dimension line away from the measured points.
let n = dir.perp();
let side = if (defpt - prev.p13.xy()).dot(n) >= 0.0 { 1.0 } else { -1.0 };
defpt += n * (spacing * side);
}
EntityKind::Dimension(Dimension {
p13: v3(first),
p14: v3(next),
defpt: v3(defpt),
text: String::new(),
block: None,
assoc: Vec::new(),
marks: Default::default(),
user_text_pos: false,
..prev.clone()
})
}
/// Most dimensions one DIMCONTINUE/DIMBASELINE call adds.
pub(super) const MAX_CHAIN: usize = 100_000;
fn chain_run(s: &mut Session, p: &Value, baseline: bool) -> Result<Value> {
let id = if baseline { "dimbaseline" } else { "dimcontinue" };
let pts = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(id, "`points` is required"))?;
if pts.len() > MAX_CHAIN {
return Err(bad(id, format!("at most {MAX_CHAIN} `points` per call")));
}
let pts = points_param(p, "points").ok_or_else(|| bad(id, "`points` must be a list of points"))?;
// One snap index for the whole chain: looking up each new point's object by scanning the
// drawing made long chains quadratic (the added dimensions are no snap targets).
let snaps = if assoc_enabled(s) { Some(crate::assoc::SnapIndex::new(s.doc()?, &s.space())) } else { None };
let mut out = Vec::new();
for q in pts {
let prev = last_linear(s).ok_or_else(|| bad(id, "no linear dimension to continue"))?;
let mut k = chained(s, &prev, q, baseline);
if let (Some(ix), EntityKind::Dimension(dm)) = (&snaps, &mut k) {
dm.assoc = ix.auto_assoc(&[("p13", dm.p13.xy()), ("p14", dm.p14.xy())]);
}
let h = s.add_entity(k)?;
s.last_dim = Some(h);
out.push(h.hex());
}
Ok(json!({ "handles": out }))
}
pub(super) fn run_continue(s: &mut Session, p: &Value) -> Result<Value> {
chain_run(s, p, false)
}
pub(super) fn run_baseline(s: &mut Session, p: &Value) -> Result<Value> {
chain_run(s, p, true)
}
/// The two points a picked object is dimensioned between (DIMLINEAR "select object").
fn object_extent(s: &Session, h: Handle) -> Option<(Vec2, Vec2)> {
let e = s.doc().ok()?.entity(h)?;
if let Some((c, r)) = circle_of(s, h).filter(|_| matches!(e.kind, EntityKind::Circle(_))) {
return Some((c - Vec2::X * r, c + Vec2::X * r));
}
e.kind.prims().into_iter().find_map(|pr| match pr {
Prim::Seg(sg @ (Segment::Line(_) | Segment::Arc { .. })) => Some((sg.start(), sg.end())),
_ => None,
})
}
pub(super) struct LinearM {
aligned: bool,
pts: Vec<Vec2>,
rotation: Option<f64>,
text: DimText,
selecting: bool,
/// Asking for the dimension line angle (Rotated).
rotating: bool,
}
impl LinearM {
pub(super) fn new(aligned: bool) -> Self {
LinearM { aligned, pts: Vec::new(), rotation: None, text: DimText::default(), selecting: false, rotating: false }
}
}
impl Interactive for LinearM {
fn name(&self) -> &'static str {
if self.aligned { "DIMALIGNED" } else { "DIMLINEAR" }
}
fn prompt(&self, _s: &Session) -> Prompt {
if self.rotating {
return Prompt::new("Specify angle of dimension line", Accept::NUMBER);
}
if let Some(p) = self.text.prompt() {
return p;
}
if self.selecting {
return Prompt::new("Select object to dimension", Accept::POINT);
}
match self.pts.len() {
0 => Prompt::new("Specify first extension line origin", Accept::POINT).default("select object"),
1 => Prompt::new("Specify second extension line origin", Accept::POINT).base_opt(self.pts.first().copied()),
// Once the orientation is forced, only the text options remain.
_ if self.aligned || self.rotation.is_some() => {
Prompt::new("Specify dimension line location", Accept::POINT).kw(&["Mtext", "Text", "Angle"])
}
_ => Prompt::new("Specify dimension line location", Accept::POINT).kw(&["Mtext", "Text", "Angle", "Horizontal", "Vertical", "Rotated"]),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if self.rotating {
if let Input::Text(t) = &i {
let Some(r) = s.angle_settings().direction(t) else {
s.echo("Requires a valid angle.");
return Ok(Step::Continue);
};
self.rotation = Some(r);
}
self.rotating = false;
return Ok(Step::Continue);
}
if self.text.answer(s, &i) {
return Ok(Step::Continue);
}
if self.selecting {
if let Input::Point(p) = i {
let Some(h) = pick_at(s, p) else {
s.echo("*Invalid selection*");
return Ok(Step::Continue);
};
let seg =
s.doc()?.entity(h).and_then(|e| e.kind.prims().into_iter().find_map(|pr| if let Prim::Seg(sg) = pr { Some(sg) } else { None }));
match seg {
Some(sg @ Segment::Line(_)) | Some(sg @ Segment::Arc { .. }) => {
self.pts = vec![sg.start(), sg.end()];
self.selecting = false;
}
None => {
if let Some((c, r)) = circle_of(s, h) {
self.pts = vec![c - Vec2::X * r, c + Vec2::X * r];
self.selecting = false;
} else {
s.echo("Object cannot be dimensioned this way.");
}
}
}
}
return Ok(Step::Continue);
}
match (self.pts.len(), i) {
(0, Input::Enter) => {
self.selecting = true;
Ok(Step::Continue)
}
(n, Input::Point(p)) if n < 2 => {
self.pts.push(p);
Ok(Step::Continue)
}
(_, Input::Point(at)) => {
let (Some(&a), Some(&b)) = (self.pts.first(), self.pts.get(1)) else { return Ok(Step::Continue) };
let k = self.text.apply(dim(s, linear_kind(a, b, at, self.rotation, self.aligned), at, a, b, Vec2::ZERO, ""));
add_dim(s, k)?;
if let Some(EntityKind::Dimension(d)) = s.last_dim.and_then(|h| s.doc().ok()?.entity(h).map(|e| e.kind.clone())) {
let st = s.doc()?.dim_style(&d.style).cloned().unwrap_or_default();
let g = cadcraft_render::dimension_geometry(&d, &st, 1.0);
s.echo(format!("Dimension text = {}", g.value));
}
Ok(Step::Done)
}
(_, Input::Keyword(k)) => {
match k.as_str() {
"Horizontal" => self.rotation = Some(0.0),
"Vertical" => self.rotation = Some(std::f64::consts::FRAC_PI_2),
"Rotated" => self.rotating = true,
other => {
self.text.keyword(other);
}
}
Ok(Step::Continue)
}
(_, Input::Enter) => Ok(Step::Cancel),
_ => Ok(Step::Continue),
}
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
match self.pts.as_slice() {
[a] => vec![crate::cmd::helpers::line(*a, c)],
[a, b] => vec![self.text.apply(dim(s, linear_kind(*a, *b, c, self.rotation, self.aligned), c, *a, *b, Vec2::ZERO, ""))],
_ => Vec::new(),
}
}
}
#[derive(Default)]
pub(super) struct OrdinateM {
feature: Option<Vec2>,
/// Xdatum / Ydatum: the forced ordinate type (`true` = X datum).
xtype: Option<bool>,
text: DimText,
}
impl OrdinateM {
fn kind(&self, s: &Session, f: Vec2, l: Vec2) -> EntityKind {
let x = self.xtype.unwrap_or((l - f).y.abs() > (l - f).x.abs());
self.text.apply(dim(s, DimKind::Ordinate { x_type: x }, Vec2::ZERO, f, l, Vec2::ZERO, ""))
}
}
impl Interactive for OrdinateM {
fn name(&self) -> &'static str {
"DIMORDINATE"
}
fn prompt(&self, _s: &Session) -> Prompt {
if let Some(p) = self.text.prompt() {
return p;
}
match (self.feature, self.xtype) {
(None, _) => Prompt::new("Specify feature location", Accept::POINT),
(Some(f), None) => Prompt::new("Specify leader endpoint", Accept::POINT).kw(&["Xdatum", "Ydatum", "Mtext", "Text", "Angle"]).base(f),
(Some(f), Some(_)) => Prompt::new("Specify leader endpoint", Accept::POINT).kw(&["Mtext", "Text", "Angle"]).base(f),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
if self.text.answer(s, &i) {
return Ok(Step::Continue);
}
match (self.feature, i) {
(None, Input::Point(p)) => {
self.feature = Some(p);
Ok(Step::Continue)
}
(Some(_), Input::Keyword(k)) => {
match k.as_str() {
"Xdatum" => self.xtype = Some(true),
"Ydatum" => self.xtype = Some(false),
other => {
self.text.keyword(other);
}
}
Ok(Step::Continue)
}
(Some(f), Input::Point(l)) => {
add_dim(s, self.kind(s, f, l))?;
Ok(Step::Done)
}
(_, Input::Enter) => Ok(Step::Cancel),
_ => Ok(Step::Continue),
}
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
self.feature.map(|f| vec![self.kind(s, f, c)]).unwrap_or_default()
}
}
pub(super) struct ChainM {
baseline: bool,
created: Vec<Handle>,
}
impl ChainM {
pub(super) fn new(_s: &Session, baseline: bool) -> Self {
ChainM { baseline, created: Vec::new() }
}
}
impl Interactive for ChainM {
fn name(&self) -> &'static str {
if self.baseline { "DIMBASELINE" } else { "DIMCONTINUE" }
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
if last_linear(s).is_none() {
s.echo("No linear dimension to continue. Create one with DIMLINEAR first.");
return Ok(Step::Done);
}
Ok(Step::Continue)
}
fn prompt(&self, s: &Session) -> Prompt {
Prompt::new("Specify a second extension line origin", Accept::POINT)
.kw(&["Select", "Undo"])
.default("Select")
.base_opt(last_linear(s).map(|d| if self.baseline { d.p13.xy() } else { d.p14.xy() }))
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match i {
Input::Point(p) => {
let prev = last_linear(s).ok_or_else(|| EngineError::Other("no linear dimension".into()))?;
let k = chained(s, &prev, p, self.baseline);
self.created.push(add_dim(s, k)?);
Ok(Step::Continue)
}
Input::Keyword(k) if k == "Undo" => {
if let Some(h) = self.created.pop() {
s.doc_mut()?.remove_entity(h);
s.last_dim = self.created.last().copied();
}
Ok(Step::Continue)
}
Input::Enter => Ok(Step::Done),
_ => Ok(Step::Continue),
}
}
fn preview(&self, s: &Session, c: Vec2) -> Vec<EntityKind> {
last_linear(s).map(|d| vec![chained(s, &d, c, self.baseline)]).unwrap_or_default()
}
}