Files
cadcraft/crates/engine/src/cmd/annotate/common.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

171 lines
6.2 KiB
Rust

use super::{Accept, DimAssoc, DimKind, Dimension, EntityKind, Handle, Input, Prompt, Result, Segment, Session, Value, Vec2, Vec3, v3};
pub(super) fn style(s: &Session) -> String {
s.doc().map(|d| d.header.str("DIMSTYLE", "Standard")).unwrap_or_else(|_| "Standard".into())
}
pub(super) fn dim(s: &Session, kind: DimKind, defpt: Vec2, p13: Vec2, p14: Vec2, p15: Vec2, text: &str) -> EntityKind {
EntityKind::Dimension(Dimension {
kind,
defpt: v3(defpt),
text_mid: Vec3::ZERO,
p13: v3(p13),
p14: v3(p14),
p15: v3(p15),
p16: v3(Vec2::ZERO),
text: text.into(),
style: style(s),
measurement: 0.0,
text_rotation: 0.0,
user_text_pos: false,
block: None,
overrides: s.doc().map(|d| d.dim_overrides()).unwrap_or_default(),
assoc: Vec::new(),
marks: Default::default(),
})
}
/// Horizontal (0) or vertical (90°) from where the dimension line is placed (AutoCAD's rule).
fn auto_rotation(p1: Vec2, p2: Vec2, at: Vec2) -> f64 {
let (x0, x1) = (p1.x.min(p2.x), p1.x.max(p2.x));
let (y0, y1) = (p1.y.min(p2.y), p1.y.max(p2.y));
let outside_y = at.y < y0 || at.y > y1;
let outside_x = at.x < x0 || at.x > x1;
if outside_x && !outside_y { std::f64::consts::FRAC_PI_2 } else { 0.0 }
}
pub(super) fn linear_kind(p1: Vec2, p2: Vec2, at: Vec2, rotation: Option<f64>, aligned: bool) -> DimKind {
if aligned { DimKind::Aligned } else { DimKind::Linear { rotation: rotation.unwrap_or_else(|| auto_rotation(p1, p2, at)) } }
}
/// Add a dimension. With DIMASSOC on, definition points that sit on object endpoints,
/// vertices or centres are attached to them (associative dimensions).
pub(super) fn add_dim(s: &mut Session, k: EntityKind) -> Result<Handle> {
let mut k = k;
if let EntityKind::Dimension(dm) = &mut k
&& dm.assoc.is_empty()
&& assoc_enabled(s)
{
dm.assoc = crate::assoc::reassociate(s.doc()?, &s.space(), dm);
}
let h = s.add_entity(k)?;
s.last_dim = Some(h);
Ok(h)
}
pub(super) fn assoc_enabled(s: &Session) -> bool {
s.doc().map(|d| d.header.i64("DIMASSOC", 2) != 0).unwrap_or(false)
}
pub(super) fn with_assoc(k: EntityKind, assoc: Vec<DimAssoc>) -> EntityKind {
match k {
EntityKind::Dimension(mut d) => {
d.assoc = assoc;
EntityKind::Dimension(d)
}
other => other,
}
}
/// Centre and radius of a circle/arc entity.
pub(super) fn circle_of(s: &Session, h: Handle) -> Option<(Vec2, f64)> {
match &s.doc().ok()?.entity(h)?.kind {
EntityKind::Circle(c) => Some((c.center.xy(), c.radius)),
EntityKind::Arc(a) => Some((a.center.xy(), a.radius)),
_ => None,
}
}
/// The dimension line location of a radius/diameter dimension puts the text there, inside or
/// outside the circle, as a user-positioned text (DXF flag 70 bit 128), like AutoCAD.
pub(super) fn text_at(mut k: EntityKind, at: Vec2) -> EntityKind {
if let EntityKind::Dimension(d) = &mut k {
d.text_mid = v3(at);
d.user_text_pos = true;
}
k
}
pub(super) fn hex_param(p: &Value, key: &str) -> Option<Handle> {
p.get(key).and_then(|v| v.as_str().and_then(Handle::parse_hex).or_else(|| v.as_u64().map(Handle)))
}
// ---------------- interactive ----------------
pub(super) fn pick_at(s: &Session, p: Vec2) -> Option<Handle> {
let ap = s.pixel_size() * s.settings.pickbox.max(1.0) * 1.5;
crate::select::pick(s.doc().ok()?, &s.space(), p, ap)
}
/// The straight or curved piece of `h` picked at `p`: a line, an arc, or the polyline segment nearest `p`.
pub(in crate::cmd) fn segment_at(s: &Session, h: Handle, p: Vec2) -> Option<Segment> {
match &s.doc().ok()?.entity(h)?.kind {
EntityKind::Line(l) => Some(Segment::Line(cadcraft_geom::Line::new(l.a.xy(), l.b.xy()))),
EntityKind::Arc(a) => Some(Segment::Arc { arc: cadcraft_geom::Arc::new(a.center.xy(), a.radius, a.start, a.end), ccw: true }),
EntityKind::LwPolyline(pl) => cadcraft_geom::Polyline { vertices: pl.vertices.clone(), closed: pl.closed }
.segments()
.into_iter()
.min_by(|x, y| x.closest(p).dist(p).total_cmp(&y.closest(p).dist(p))),
_ => None,
}
}
/// What a dimension placement prompt asks after Mtext, Text or Angle.
#[derive(Clone, Copy, PartialEq)]
enum DimAsk {
Text,
Angle,
}
/// The Mtext / Text / Angle options of the dimension placement prompts: replace the measured text
/// (`<>` stands for the measurement) and rotate the text.
#[derive(Default)]
pub(super) struct DimText {
text: String,
angle: f64,
asking: Option<DimAsk>,
}
impl DimText {
/// Opens the sub-prompt for Mtext, Text or Angle; `false` for any other keyword.
pub(super) fn keyword(&mut self, k: &str) -> bool {
self.asking = match k {
"Mtext" | "Text" => Some(DimAsk::Text),
"Angle" => Some(DimAsk::Angle),
_ => return false,
};
true
}
pub(super) fn prompt(&self) -> Option<Prompt> {
Some(match self.asking? {
DimAsk::Text => Prompt::new("Enter dimension text (<> = measurement)", Accept::TEXT),
DimAsk::Angle => Prompt::new("Specify angle of dimension text", Accept::NUMBER),
})
}
/// Takes the answer to the open sub-prompt (Enter keeps the current value); `false` when none is open.
pub(super) fn answer(&mut self, s: &mut Session, i: &Input) -> bool {
let Some(a) = self.asking else { return false };
match (a, i) {
(DimAsk::Text, Input::Text(t)) => self.text = t.clone(),
(DimAsk::Angle, Input::Text(t)) => match s.angle_settings().direction(t) {
Some(r) => self.angle = r,
None => {
s.echo("Requires a valid angle.");
return true;
}
},
_ => {}
}
self.asking = None;
true
}
/// `k` with this text and text angle.
pub(super) fn apply(&self, mut k: EntityKind) -> EntityKind {
if let EntityKind::Dimension(d) = &mut k {
d.text = self.text.clone();
d.text_rotation = self.angle;
}
k
}
}