diff --git a/src/qelmt/mod.rs b/src/qelmt/mod.rs index 37e632a..48e3087 100644 --- a/src/qelmt/mod.rs +++ b/src/qelmt/mod.rs @@ -1,4 +1,4 @@ -use dxf::entities::{AttributeDefinition, Entity, EntityType}; +use dxf::entities::{AttributeDefinition, Entity, EntityType, Insert}; use dxf::entities::{LwPolyline, Polyline}; use dxf::enums::{AttachmentPoint, HorizontalTextJustification, Units, VerticalTextJustification}; use dxf::{Block, Drawing}; @@ -187,6 +187,23 @@ fn qet_insert_rotation(rotation: f64) -> f64 { -rotation } +fn connector_attribute(insert: &Insert) -> Option<&str> { + insert + .attributes() + .find(|attribute| { + attribute + .attribute_tag + .trim() + .eq_ignore_ascii_case("connector") + }) + .map(|attribute| attribute.value.trim()) + .filter(|value| !value.is_empty()) +} + +fn is_arrowunknown_terminal(insert: &Insert) -> bool { + insert.name.trim().eq_ignore_ascii_case("ARROWUNKNOWN") && connector_attribute(insert).is_some() +} + // 用于判断多边形是否接近圆形的特征,有点类似函数声明 trait Circularity { // 判断图形是否为圆形 @@ -584,7 +601,7 @@ pub(crate) enum Objects { DynamicText(DynamicText), Text(Text), Line(Line), - //Terminal(Terminal), + Terminal(Terminal), Group(Vec), BlockInstance(BlockInstance), } @@ -620,6 +637,10 @@ impl Objects { line.x2 += offset_x; line.y2 += offset_y; } + Objects::Terminal(terminal) => { + terminal.x += offset_x; + terminal.y += offset_y; + } Objects::Group(objects) => { for object in objects { object.translate(offset_x, offset_y); @@ -662,6 +683,13 @@ impl Objects { line.x2 = x2; line.y2 = y2; } + Objects::Terminal(terminal) => { + let (x, y) = + rotate_point_around(terminal.x, terminal.y, pivot_x, pivot_y, angle_deg); + terminal.x = x; + terminal.y = y; + terminal.orientation = terminal.orientation.rotated(angle_deg); + } Objects::Group(objects) => { rotate_objects_around(objects, pivot_x, pivot_y, angle_deg); } @@ -763,6 +791,7 @@ impl ScaleEntity for Objects { Objects::DynamicText(dynamic_text) => dynamic_text.scale(fact_x, fact_y), Objects::Text(text) => text.scale(fact_x, fact_y), Objects::Line(line) => line.scale(fact_x, fact_y), + Objects::Terminal(terminal) => terminal.scale(fact_x, fact_y), Objects::Group(vec) => vec.iter_mut().for_each(|ob| ob.scale(fact_x, fact_y)), Objects::BlockInstance(block) => block .objects @@ -780,6 +809,7 @@ impl ScaleEntity for Objects { Objects::DynamicText(dynamic_text) => dynamic_text.left_bound(), Objects::Text(text) => text.left_bound(), Objects::Line(line) => line.left_bound(), + Objects::Terminal(terminal) => terminal.x, Objects::Group(vec) => { let lb = vec.iter().min_by(|ob1, ob2| { ob1.left_bound() @@ -806,6 +836,7 @@ impl ScaleEntity for Objects { Objects::DynamicText(dynamic_text) => dynamic_text.right_bound(), Objects::Text(text) => text.right_bound(), Objects::Line(line) => line.right_bound(), + Objects::Terminal(terminal) => terminal.x, Objects::Group(vec) => { let rb = vec.iter().max_by(|ob1, ob2| { ob1.right_bound() @@ -831,6 +862,7 @@ impl ScaleEntity for Objects { Objects::DynamicText(dynamic_text) => dynamic_text.top_bound(), Objects::Text(text) => text.top_bound(), Objects::Line(line) => line.top_bound(), + Objects::Terminal(terminal) => terminal.y, Objects::Group(vec) => { let tb = vec.iter().min_by(|ob1, ob2| { ob1.top_bound() @@ -856,6 +888,7 @@ impl ScaleEntity for Objects { Objects::DynamicText(dynamic_text) => dynamic_text.bot_bound(), Objects::Text(text) => text.bot_bound(), Objects::Line(line) => line.bot_bound(), + Objects::Terminal(terminal) => terminal.y, Objects::Group(vec) => { let bb = vec.iter().max_by(|ob1, ob2| { ob1.bot_bound() @@ -1327,6 +1360,15 @@ impl<'a> ObjectsBuilder<'a> { Ok(Objects::Polygon(poly)) } EntityType::Insert(ins) => { + if is_arrowunknown_terminal(ins) { + if let Some(mut terminal) = Terminal::from_insert(ins) { + terminal.scale(self.scale_fact.x, self.scale_fact.y); + terminal.x += self.offset.x; + terminal.y -= self.offset.y; + return Ok(Objects::Terminal(terminal)); + } + } + //info!("Found an Insert Block: {ins:?}"); info!("Found an Insert Block: {}", &ins.name); let Some(block) = self.blocks.iter().find(|bl| bl.name == ins.name) else { @@ -1435,6 +1477,7 @@ impl From<&Objects> for Either> { Objects::DynamicText(dtext) => Either::Left(dtext.into()), Objects::Text(txt) => Either::Left(txt.into()), Objects::Line(line) => Either::Left(line.into()), + Objects::Terminal(terminal) => Either::Left(terminal.into()), Objects::Group(block) => Either::Right( // 迭代组中的每个对象,try_from转换,filter_map和ok忽略转换失败对象,collect()收集转换成功元素 block @@ -1466,6 +1509,7 @@ impl TryFrom<&Objects> for XMLElement { Objects::DynamicText(dtext) => Ok(dtext.into()), Objects::Text(txt) => Ok(txt.into()), Objects::Line(line) => Ok(line.into()), + Objects::Terminal(terminal) => Ok(terminal.into()), Objects::Group(_) => Err("Unsupported"), Objects::BlockInstance(_) => Err("Unsupported"), } @@ -1655,6 +1699,10 @@ impl From<(&Drawing, u32)> for Description { .filter_map(|ent| match &ent.specific { EntityType::Insert(ins) => { // 检查Insert类型entity的is_visible属性 + if is_arrowunknown_terminal(ins) { + return ObjectsBuilder::new(ent, spline_step).build().ok(); + } + if !ent.common.is_visible { return None; } @@ -1711,18 +1759,128 @@ impl From<(&Drawing, u32)> for Description { } } -//probably don't need to worry about this as they won't exist in the dxf... -/*pub struct Terminal { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TermOrientation { + North, + East, + South, + West, +} + +impl TermOrientation { + fn vector(self) -> (f64, f64) { + match self { + Self::North => (0.0, -1.0), + Self::East => (1.0, 0.0), + Self::South => (0.0, 1.0), + Self::West => (-1.0, 0.0), + } + } + + fn from_vector(x: f64, y: f64) -> Self { + if x.abs() >= y.abs() { + if x >= 0.0 { + Self::East + } else { + Self::West + } + } else if y >= 0.0 { + Self::South + } else { + Self::North + } + } + + fn rotated(self, angle_deg: f64) -> Self { + let angle = angle_deg.to_radians(); + let (x, y) = self.vector(); + Self::from_vector( + x * angle.cos() - y * angle.sin(), + x * angle.sin() + y * angle.cos(), + ) + } +} + +impl Display for TermOrientation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::North => "n", + Self::East => "e", + Self::South => "s", + Self::West => "w", + }) + } +} + +#[derive(Debug)] +pub struct Terminal { x: f64, y: f64, uuid: Uuid, name: String, - orientation: TermOrient, - //type? - // Generic - // Indoor Terminal Block - // External Terminal Block -}*/ + connector: String, + orientation: TermOrientation, +} + +impl Terminal { + fn from_insert(insert: &Insert) -> Option { + let connector = connector_attribute(insert)?.to_owned(); + Some(Self { + x: insert.location.x, + y: -insert.location.y, + uuid: Uuid::new_v4(), + name: connector.clone(), + connector, + // SWE 的通用连接点块以东向为零角度;DXF 到 QET 时 Y 轴翻转。 + orientation: TermOrientation::East.rotated(qet_insert_rotation(insert.rotation)), + }) + } +} + +impl ScaleEntity for Terminal { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + self.x *= fact_x; + self.y *= fact_y; + + let (dir_x, dir_y) = self.orientation.vector(); + if fact_x.abs() > f64::EPSILON || fact_y.abs() > f64::EPSILON { + self.orientation = TermOrientation::from_vector(dir_x * fact_x, dir_y * fact_y); + } + } + + fn left_bound(&self) -> f64 { + self.x + } + + fn right_bound(&self) -> f64 { + self.x + } + + fn top_bound(&self) -> f64 { + self.y + } + + fn bot_bound(&self) -> f64 { + self.y + } +} + +impl From<&Terminal> for XMLElement { + fn from(terminal: &Terminal) -> Self { + let mut terminal_xml = XMLElement::new("terminal"); + terminal_xml.add_attribute("x", two_dec(terminal.x)); + terminal_xml.add_attribute("y", two_dec(terminal.y)); + terminal_xml.add_attribute("uuid", format!("{{{}}}", terminal.uuid)); + terminal_xml.add_attribute("name", &terminal.name); + terminal_xml.add_attribute("orientation", &terminal.orientation); + terminal_xml.add_attribute("type", "Generic"); + + let mut connection_point = XMLElement::new("connection_point"); + connection_point.add_attribute("tag", &terminal.connector); + terminal_xml.add_child(connection_point); + terminal_xml + } +} #[derive(Debug)] pub struct Names { @@ -2434,7 +2592,64 @@ pub(crate) fn strip_mtext_control_sequences(input: &str) -> String { #[cfg(test)] mod tests { - use super::{BlockInstance, BlockMetadata, Description, Line, Objects, ScaleEntity}; + use super::{ + BlockInstance, BlockMetadata, Definition, Description, Line, Objects, ObjectsBuilder, + ScaleEntity, + }; + use dxf::entities::{Attribute, Entity, EntityType, Insert}; + use dxf::{Drawing, Point}; + + fn connector_insert(rotation: f64) -> Insert { + let mut insert = Insert { + name: "ARROWUNKNOWN".into(), + location: Point::new(12.0, 34.0, 0.0), + rotation, + ..Default::default() + }; + let mut drawing = Drawing::new(); + insert.add_attribute( + &mut drawing, + Attribute { + attribute_tag: "connector".into(), + value: "N:0-C:0".into(), + ..Default::default() + }, + ); + insert + } + + #[test] + fn arrowunknown_connector_becomes_qet_terminal() { + let entity = Entity::new(EntityType::Insert(connector_insert(90.0))); + let object = ObjectsBuilder::new(&entity, 20) + .build() + .expect("ARROWUNKNOWN with connector should be supported"); + let xml = simple_xml_builder::XMLElement::try_from(&object) + .expect("terminal should serialize") + .to_string(); + + assert!(xml.contains("