diff --git a/src/qelmt/dynamictext.rs b/src/qelmt/dynamictext.rs index af0b127..998c01a 100644 --- a/src/qelmt/dynamictext.rs +++ b/src/qelmt/dynamictext.rs @@ -174,24 +174,63 @@ impl ScaleEntity for DynamicText { self.align_y *= fact_y; self.font.scale(fact_y); self.text_height = self.text_height * fact_y; + self.reference_rectangle_width *= fact_x.abs(); } fn left_bound(&self) -> f64 { - self.x + self.horizontal_bounds().0 } fn right_bound(&self) -> f64 { - //todo!() - 1.0 + self.horizontal_bounds().1 } fn top_bound(&self) -> f64 { - self.y + self.vertical_bounds().0 } fn bot_bound(&self) -> f64 { - //todo!() - 1.0 + self.vertical_bounds().1 + } +} + +impl DynamicText { + // 文本的绘制位置和对齐点必须同步移动,否则归一化后文字仍会偏离图形。 + pub(crate) fn translate(&mut self, offset_x: f64, offset_y: f64) { + self.x += offset_x; + self.y += offset_y; + self.align_x += offset_x; + self.align_y += offset_y; + } + + fn horizontal_bounds(&self) -> (f64, f64) { + // 普通 TEXT 没有参考矩形宽度,使用文本内容估算宽度以参与整体包围盒计算。 + let width = if self.reference_rectangle_width > 0.0 { + self.reference_rectangle_width + } else { + estimate_text_width( + &self.text, + self.text_height, + self.relative_x_scale_factor, + ) + } + .abs(); + + match self.h_alignment { + HAlignment::Left => (self.x, self.x + width), + HAlignment::Center => (self.x - width / 2.0, self.x + width / 2.0), + HAlignment::Right => (self.x - width, self.x), + } + } + + fn vertical_bounds(&self) -> (f64, f64) { + let height = self.text_height.abs(); + + match self.v_alignment { + VAlignment::Top => (self.y, self.y + height), + VAlignment::Center => (self.y - height / 2.0, self.y + height / 2.0), + VAlignment::Bottom => (self.y - height, self.y), + } } } @@ -523,3 +562,55 @@ fn estimate_text_width(text: &str, line_height: f64, relative_x_scale_factor: f6 base_width * scale } + +#[cfg(test)] +mod tests { + use super::{DynamicText, HAlignment, ScaleEntity, VAlignment}; + use crate::qelmt::FontInfo; + use hex_color::HexColor; + use uuid::Uuid; + + fn dynamic_text() -> DynamicText { + DynamicText { + text: "AB".into(), + info_name: None, + x: -50.0, + y: -40.0, + z: 0.0, + rotation: 0.0, + uuid: Uuid::new_v4(), + h_alignment: HAlignment::Left, + font: FontInfo::default(), + text_from: "UserText".into(), + v_alignment: VAlignment::Bottom, + frame: false, + text_height: 10.0, + text_width: -1.0, + relative_x_scale_factor: 1.0, + keep_visual_rotation: false, + color: HexColor::BLACK, + reference_rectangle_width: 0.0, + align_x: -50.0, + align_y: -40.0, + } + } + + #[test] + fn estimated_bounds_stay_near_negative_text_coordinates() { + let text = dynamic_text(); + + assert_eq!((-50.0, -35.0), text.horizontal_bounds()); + assert_eq!((-50.0, -40.0), text.vertical_bounds()); + } + + #[test] + fn scaling_updates_reference_rectangle_width() { + let mut text = dynamic_text(); + text.reference_rectangle_width = 20.0; + + text.scale(2.0, 3.0); + + assert_eq!((-100.0, -60.0), text.horizontal_bounds()); + assert_eq!((-150.0, -120.0), text.vertical_bounds()); + } +} diff --git a/src/qelmt/mod.rs b/src/qelmt/mod.rs index eb92216..152e3be 100644 --- a/src/qelmt/mod.rs +++ b/src/qelmt/mod.rs @@ -219,6 +219,8 @@ impl Definition { let description = { let mut description: Description = (drw, spline_step).into(); description.scale(scale_factor, scale_factor); + // DXF 实体可能位于整张 CAD 图纸的任意绝对坐标,导出元件时需要转换为局部坐标。 + description.center_on_origin(); description }; @@ -382,6 +384,42 @@ impl Objects { _ => Children { slice: [].iter() }, } } + + // 对所有图元递归应用同一平移量,保持块内部图形的相对位置不变。 + fn translate(&mut self, offset_x: f64, offset_y: f64) { + match self { + Objects::Arc(arc) => { + arc.x += offset_x; + arc.y += offset_y; + } + Objects::Ellipse(ellipse) => { + ellipse.x += offset_x; + ellipse.y += offset_y; + } + Objects::Polygon(polygon) => { + for coordinate in &mut polygon.coordinates { + coordinate.x += offset_x; + coordinate.y += offset_y; + } + } + Objects::DynamicText(dynamic_text) => dynamic_text.translate(offset_x, offset_y), + Objects::Text(text) => { + text.x += offset_x; + text.y += offset_y; + } + Objects::Line(line) => { + line.x1 += offset_x; + line.y1 += offset_y; + line.x2 += offset_x; + line.y2 += offset_y; + } + Objects::Group(objects) => { + for object in objects { + object.translate(offset_x, offset_y); + } + } + } + } } // 结构体,包含一个 stack 字段,存储 Children 迭代器的栈,使用生命周期参数 'a 确保引用的有效性 @@ -771,8 +809,7 @@ impl<'a> ObjectsBuilder<'a> { dtext.scale(self.scale_fact.x, self.scale_fact.y); - dtext.x += self.offset.x; - dtext.y -= self.offset.y; + dtext.translate(self.offset.x, -self.offset.y); Objects::DynamicText(dtext) }, @@ -813,8 +850,7 @@ impl<'a> ObjectsBuilder<'a> { dtext.scale(self.scale_fact.x, self.scale_fact.y); - dtext.x += self.offset.x; - dtext.y -= self.offset.y; + dtext.translate(self.offset.x, -self.offset.y); Objects::DynamicText(dtext) }, @@ -1057,8 +1093,7 @@ impl<'a> ObjectsBuilder<'a> { dtext.scale(self.scale_fact.x, self.scale_fact.y); - dtext.x += self.offset.x; - dtext.y -= self.offset.y; + dtext.translate(self.offset.x, -self.offset.y); Objects::DynamicText(dtext) }), @@ -1117,6 +1152,18 @@ pub struct Description { objects: Vec, } +impl Description { + // 将包围盒中心移动到局部原点,避免 CAD 中的插入位置泄漏到 QET 元件坐标。 + fn center_on_origin(&mut self) { + let offset_x = -((self.left_bound() + self.right_bound()) / 2.0); + let offset_y = -((self.top_bound() + self.bot_bound()) / 2.0); + + for object in &mut self.objects { + object.translate(offset_x, offset_y); + } + } +} + // 实现Description的ScaleEntity特征 impl ScaleEntity for Description { fn scale(&mut self, fact_x: f64, fact_y: f64) { @@ -1149,7 +1196,7 @@ impl ScaleEntity for Description { }); if let Some(rb) = rb { - rb.left_bound() + rb.right_bound() } else { 0.0 } @@ -1177,7 +1224,7 @@ impl ScaleEntity for Description { }); if let Some(bb) = bb { - bb.top_bound() + bb.bot_bound() } else { 0.0 } @@ -1989,3 +2036,46 @@ pub(crate) fn strip_mtext_control_sequences(input: &str) -> String { result } + +#[cfg(test)] +mod tests { + use super::{Description, Line, Objects, ScaleEntity}; + + #[test] + fn description_bounds_use_outer_edges_for_single_group() { + let description = Description { + objects: vec![Objects::Group(vec![Objects::Line(Line::new( + 12.0, + -8.0, + 42.0, + 16.0, + "line-style:normal;line-weight:thin;filling:none;color:black".into(), + ))])], + }; + + assert_eq!(12.0, description.left_bound()); + assert_eq!(42.0, description.right_bound()); + assert_eq!(-8.0, description.top_bound()); + assert_eq!(16.0, description.bot_bound()); + } + + #[test] + fn centering_description_removes_source_drawing_offset() { + let mut description = Description { + objects: vec![Objects::Group(vec![Objects::Line(Line::new( + 1_290.0, + -2_070.0, + 1_330.0, + -2_050.0, + "line-style:normal;line-weight:thin;filling:none;color:black".into(), + ))])], + }; + + description.center_on_origin(); + + assert_eq!(-20.0, description.left_bound()); + assert_eq!(20.0, description.right_bound()); + assert_eq!(-10.0, description.top_bound()); + assert_eq!(10.0, description.bot_bound()); + } +}