fix: 修复混合 LWPOLYLINE 的 bulge 圆弧丢失 按段拆分直线和圆弧,并修正负 bulge 的半径及 QET 起始角度计算。

qdj
邱德佳 1 week ago
parent c851c6b5a3
commit 6b0a51ea4a

@ -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}");
}
}

@ -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<Vec<Objects>, &'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("<connection_point tag=\"N:0-C:0\""));
}
#[test]
fn mixed_lwpolyline_keeps_line_and_negative_bulge_arc() {
let polyline = LwPolyline {
constant_width: 0.35,
vertices: vec![
LwPolylineVertex {
x: 0.0,
y: 0.0,
..Default::default()
},
LwPolylineVertex {
x: 0.0,
y: 1.666_666_7,
bulge: -1.0,
..Default::default()
},
LwPolylineVertex {
x: 5.0,
y: 1.666_666_7,
..Default::default()
},
],
..Default::default()
};
let entity = Entity::new(EntityType::LwPolyline(polyline));
let object = ObjectsBuilder::new(&entity, 20)
.build()
.expect("mixed LWPOLYLINE should be supported");
let xml = xml_elements_for_object(&object, None)
.into_iter()
.map(|element| element.to_string())
.collect::<Vec<_>>()
.join("");
assert_eq!(1, xml.matches("<line ").count(), "unexpected XML: {xml}");
assert_eq!(1, xml.matches("<arc ").count(), "unexpected XML: {xml}");
assert!(!xml.contains("<polygon "), "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}");
}
#[test]
fn description_bounds_use_outer_edges_for_single_group() {
let description = Description {

Loading…
Cancel
Save