diff --git a/src/qelmt/arc.rs b/src/qelmt/arc.rs index a533fb5..b030fb9 100644 --- a/src/qelmt/arc.rs +++ b/src/qelmt/arc.rs @@ -226,62 +226,36 @@ impl Arc { let v1 = &lwpolyline.vertices[0]; let v2 = &lwpolyline.vertices[1]; - // 检查第一个顶点是否有bulge值 - if v1.bulge == 0.0 { + // DXF 的 bulge 保存在起点上,表示从 v1 到 v2 这一段是否为圆弧。 + if v1.bulge.abs() <= 1e-10 { return Err("No bulge value found for arc conversion".to_string()); } - // 计算弦长和中点 + // bulge = tan(圆弧夹角 / 4),符号只表示绘制方向: + // 正值为逆时针,负值为顺时针。半径必须始终取正值,不能随 bulge 变成负数。 let chord_length = ((v2.x - v1.x).powi(2) + (v2.y - v1.y).powi(2)).sqrt(); + if chord_length <= 1e-10 { + return Err("Arc endpoints must be different".to_string()); + } + let mid_x = (v1.x + v2.x) / 2.0; let mid_y = (v1.y + v2.y) / 2.0; - - // 根据bulge值计算圆弧参数 - // bulge = tan(angle/4),其中angle是圆弧的包含角 let bulge = v1.bulge; - let included_angle = 4.0 * bulge.atan(); // 圆弧包含角(弧度) + let included_angle = 4.0 * bulge.atan(); + let radius = chord_length * (1.0 + bulge * bulge) / (4.0 * bulge.abs()); - // 计算半径 - let radius = chord_length / (2.0 * (included_angle / 2.0).sin()); - - // 计算圆心位置 - // 弦的方向向量 + // 计算圆心相对弦中点的有符号距离。 + // 保留符号后,同一套公式可以同时处理正负 bulge 和大于 180° 的圆弧。 let chord_dx = v2.x - v1.x; let chord_dy = v2.y - v1.y; + let center_distance = chord_length * (1.0 - bulge * bulge) / (4.0 * bulge); + let center_x = mid_x - chord_dy / chord_length * center_distance; + let center_y = mid_y + chord_dx / chord_length * center_distance; - // 弦的垂直方向(向左旋转90度) - let perp_dx = -chord_dy; - let perp_dy = chord_dx; - let perp_length = (perp_dx * perp_dx + perp_dy * perp_dy).sqrt(); - - // 标准化垂直向量 - let unit_perp_x = perp_dx / perp_length; - let unit_perp_y = perp_dy / perp_length; - - // 计算圆心到弦中点的距离 - let center_distance = radius * (included_angle / 2.0).cos(); - - // 根据bulge的符号确定圆心位置 - let center_x = if bulge > 0.0 { - mid_x + center_distance * unit_perp_x - } else { - mid_x - center_distance * unit_perp_x - }; - let center_y = if bulge > 0.0 { - mid_y + center_distance * unit_perp_y - } else { - mid_y - center_distance * unit_perp_y - }; - - // 计算起始角度和结束角度 let start_angle = (v1.y - center_y).atan2(v1.x - center_x); let end_angle = (v2.y - center_y).atan2(v2.x - center_x); - - // 将弧度转换为度数 let mut start_angle_deg = start_angle.to_degrees(); let mut end_angle_deg = end_angle.to_degrees(); - - // 标准化角度到0-360度范围 if start_angle_deg < 0.0 { start_angle_deg += 360.0; } @@ -289,17 +263,22 @@ impl Arc { end_angle_deg += 360.0; } - // 计算角度跨度 let arc_angle = included_angle.to_degrees().abs(); + // QET 的 angle 使用正角度。DXF 为顺时针圆弧(负 bulge)时, + // 改用 DXF 终点作为 QET 起点,反向表达后图形位置和形状保持不变。 + let qet_start_angle = if included_angle < 0.0 { + end_angle_deg + } else { + start_angle_deg + }; - // 创建Arc对象 Ok(Arc { - x: center_x - radius, // 边界框左上角x坐标 - y: -center_y - radius, // 边界框左上角y坐标(y轴翻转) - width: radius * 2.0, // 边界框宽度 - height: radius * 2.0, // 边界框高度 - start: start_angle_deg, // 起始角度(度) - angle: arc_angle, // 圆弧角度跨度(度) + x: center_x - radius, + y: -center_y - radius, + width: radius * 2.0, + height: radius * 2.0, + start: qet_start_angle, + angle: arc_angle, style: if lwpolyline.thickness > 0.1 { format!( "line-style:normal;line-weight:normal;filling:none;color:{}", @@ -315,3 +294,40 @@ impl Arc { }) } } + +#[cfg(test)] +mod tests { + use super::Arc; + use dxf::entities::LwPolyline; + use dxf::LwPolylineVertex; + + #[test] + fn negative_bulge_creates_positive_upper_semicircle() { + let polyline = LwPolyline { + vertices: vec![ + LwPolylineVertex { + x: 0.0, + y: 0.0, + bulge: -1.0, + ..Default::default() + }, + LwPolylineVertex { + x: 5.0, + y: 0.0, + ..Default::default() + }, + ], + ..Default::default() + }; + + let arc = Arc::try_from(&polyline).expect("negative bulge should form an arc"); + let xml = simple_xml_builder::XMLElement::from(&arc).to_string(); + + assert!(xml.contains("x=\"0\""), "unexpected XML: {xml}"); + assert!(xml.contains("y=\"-2.5\""), "unexpected XML: {xml}"); + assert!(xml.contains("width=\"5\""), "unexpected XML: {xml}"); + assert!(xml.contains("height=\"5\""), "unexpected XML: {xml}"); + assert!(xml.contains("start=\"0\""), "unexpected XML: {xml}"); + assert!(xml.contains("angle=\"180\""), "unexpected XML: {xml}"); + } +} diff --git a/src/qelmt/mod.rs b/src/qelmt/mod.rs index 48e3087..62a8342 100644 --- a/src/qelmt/mod.rs +++ b/src/qelmt/mod.rs @@ -718,6 +718,64 @@ impl Objects { } } +/// 判断 LWPOLYLINE 的有效线段中是否包含 bulge 圆弧。 +/// 开放折线的最后一个顶点没有“下一顶点”,其 bulge 不参与绘制; +/// 闭合折线则还要检查最后一个顶点到第一个顶点的闭合段。 +fn lwpolyline_has_bulged_segment(polyline: &LwPolyline) -> bool { + let segment_count = if polyline.is_closed() { + polyline.vertices.len() + } else { + polyline.vertices.len().saturating_sub(1) + }; + + polyline + .vertices + .iter() + .take(segment_count) + .any(|vertex| vertex.bulge.abs() > 1e-10) +} + +/// 将一条同时包含直线和 bulge 圆弧的 LWPOLYLINE 按段拆开。 +/// 每个顶点的 bulge 描述“当前顶点 -> 下一顶点”:非零生成 Arc,零值生成 Line。 +fn decompose_lwpolyline_segments( + polyline: &LwPolyline, + color: HexColor, +) -> Result, &'static str> { + let vertex_count = polyline.vertices.len(); + if vertex_count < 2 { + return Err("Error empty LwPolyline"); + } + + let segment_count = if polyline.is_closed() { + vertex_count + } else { + vertex_count - 1 + }; + let mut objects = Vec::with_capacity(segment_count); + + for index in 0..segment_count { + // 复用现有的两顶点 Line/Arc 转换器。临时段必须设为开放, + // 防止两顶点闭合折线被转换器再次补上一条回程线段。 + let mut segment = polyline.clone(); + segment.flags = 0; + segment.vertices = vec![ + polyline.vertices[index], + polyline.vertices[(index + 1) % vertex_count], + ]; + + if segment.vertices[0].bulge.abs() > 1e-10 { + let arc = Arc::try_from_lwpolyline_with_color(&segment, color) + .map_err(|_| "Error converting LwPolyline bulge segment")?; + objects.push(Objects::Arc(arc)); + } else { + let line = Line::try_from_lwpolyline_with_color(&segment, color)?; + objects.push(Objects::Line(line)); + } + } + + Ok(objects) +} + fn rotate_objects_around(objects: &mut [Objects], pivot_x: f64, pivot_y: f64, angle_deg: f64) { if angle_deg.abs() <= f64::EPSILON { return; @@ -1304,27 +1362,28 @@ impl<'a> ObjectsBuilder<'a> { ellipse.y -= self.offset.y; Ok(Objects::Ellipse(ellipse)) - } else if polygon::is_rounded_rectangle(lwpolyline) { - // 拆分圆角四边形为圆弧和直线段 + } else if lwpolyline_has_bulged_segment(lwpolyline) { + // 不能把混合折线整体降级为 Polygon,否则 bulge 信息会被丢弃, + // 原图中的圆弧就会变成连接端点的直线。 let mut decomposed_objects = - polygon::decompose_rounded_rectangle_with_color(lwpolyline, color); + decompose_lwpolyline_segments(lwpolyline, color)?; - // 对每个对象应用缩放和偏移 + // 拆出的每一段仍需继承原实体的线型、线宽、缩放和位置偏移。 for obj in &mut decomposed_objects { - obj.scale(self.scale_fact.x, self.scale_fact.y); match obj { - Objects::Arc(ref mut arc) => { - arc.x += self.offset.x; - arc.y -= self.offset.y; + Objects::Arc(arc) => { + arc.update_line_style(&update_line_style); + if let Some(w) = lw { + arc.update_line_weight(w); + } } - Objects::Line(ref mut line) => { - line.x1 += self.offset.x; - line.y1 -= self.offset.y; - line.x2 += self.offset.x; - line.y2 -= self.offset.y; + Objects::Line(line) => { + line.update_line_style(&update_line_style); } _ => {} } + obj.scale(self.scale_fact.x, self.scale_fact.y); + obj.translate(self.offset.x, -self.offset.y); } Ok(Objects::Group(decomposed_objects)) @@ -2593,11 +2652,11 @@ pub(crate) fn strip_mtext_control_sequences(input: &str) -> String { #[cfg(test)] mod tests { use super::{ - BlockInstance, BlockMetadata, Definition, Description, Line, Objects, ObjectsBuilder, - ScaleEntity, + xml_elements_for_object, BlockInstance, BlockMetadata, Definition, Description, Line, + Objects, ObjectsBuilder, ScaleEntity, }; - use dxf::entities::{Attribute, Entity, EntityType, Insert}; - use dxf::{Drawing, Point}; + use dxf::entities::{Attribute, Entity, EntityType, Insert, LwPolyline}; + use dxf::{Drawing, LwPolylineVertex, Point}; fn connector_insert(rotation: f64) -> Insert { let mut insert = Insert { @@ -2651,6 +2710,49 @@ mod tests { assert!(xml.contains(">() + .join(""); + + assert_eq!(1, xml.matches("