LWPOLYLINE 修复,补全文档

qdj
邱德佳 1 week ago
parent 6b0a51ea4a
commit 8f13b9a20d

@ -1,74 +1,189 @@
# dxf2elmt
dxf2elmt is CLI program which can convert .dxf files into [QElectroTech](https://qelectrotech.org/) .elmt files. The program supports both ascii and binary .dxf files.
The goal of this program is to create a fast and accurate conversion tool to be used with [QElectroTech](https://qelectrotech.org/).
`dxf2elmt` 是一个把 ASCII 或二进制 DXF 图纸转换为
[QElectroTech](https://qelectrotech.org/) `.elmt` 元件文件的命令行工具。
## 源项目地址
https://github.com/qelectrotech/dxf2elmt_vadoola
源项目地址:<https://github.com/qelectrotech/dxf2elmt_vadoola>
## 注意提前配置好RUST的环境
配置环境参考https://www.cnblogs.com/timefiles/p/17930394.html
## 编译
修改完代码后如何运行:
项目使用 Rust最低支持版本MSRV为 1.79.0。
1.先编译cargo build --release
```bash
cargo build --release
```
2.然后在项目目录下找到target->release->dxf2elmt.exe然后按下面"How to Use"的方法测试
编译后的程序位于:
```text
target/release/dxf2elmt.exe
```
## How to Use
dxf2elmt requires only one input from the user, the input file.
## 使用方法
For example:
转换一个或多个 DXF 文件:
```bash
./dxf2elmt my_file.dxf
dxf2elmt drawing.dxf
dxf2elmt first.dxf second.dxf
```
The .elmt file will be output into the same directory as the executable. It will retain the name of the .dxf file.
输出文件与输入 DXF 位于同一目录,文件名不变,仅将扩展名改为 `.elmt`
If you wish to forgo creating an .elmt file, you can use the "-v" argument for verbose output. This will output the contents of the .elmt file to stdout without actually creating the file. For example:
常用参数:
| 参数 | 作用 |
| --- | --- |
| `-v`, `--verbose` | 不写入 `.elmt` 文件,改为把 XML 输出到标准输出 |
| `-s`, `--spline-step <N>` | 设置 SPLINE 的折线细分数量,默认值为 20数值越大越平滑 |
| `-i`, `--info` | 输出实体统计和转换耗时 |
| `-d`, `--dtext` | 当前为保留参数;现有实现始终把 TEXT/MTEXT 转为动态文本 |
查看完整参数:
```bash
./dxf2elmt my_file.dxf -v
dxf2elmt --help
```
## 当前实体映射
| DXF 实体 | ELMT 图元 | 当前处理方式 |
| --- | --- | --- |
| `LINE` | `<line>` | 保留两个端点、颜色、线型和数值线宽 |
| `CIRCLE` | `<ellipse>` | 以等宽、等高椭圆表示圆 |
| `ARC` | `<arc>` | 保留圆心、半径、起始角和圆弧角度 |
| `ELLIPSE` | `<ellipse>` | 转换中心、长轴和短长轴比例 |
| `POLYLINE` | `<line>` / `<ellipse>` / `<polygon>` | 两个顶点转直线;识别为圆时转椭圆;其余转多边形 |
| `LWPOLYLINE` | `<line>` / `<arc>` / `<ellipse>` / `<polygon>` | 根据顶点数、圆形特征和 bulge 分别处理,详细规则见下文 |
| `SPLINE` | `<polygon>` | 按 `spline-step` 采样为折线,不保留参数曲线本身 |
| `SOLID` | 填充 `<polygon>` | 按 DXF 的 `1-2-4-3` 顶点顺序恢复边界并填充 |
| `TRACE` | 填充 `<polygon>` | 与 SOLID 相同,按四边形边界转换并填充 |
| `TEXT` | `<dynamic_text>` | 转换内容、位置、旋转、字号、宽度系数和对齐方式 |
| `MTEXT` | `<dynamic_text>` | 部分支持,转换内容、位置、方向、字号和对齐方式 |
| `ATTDEF`(属性定义) | `<dynamic_text>` | 转换属性标签及其文本样式和位置 |
| `LEADER` | 一组 `<line>` | 相邻顶点生成线段,首段可带箭头 |
| `INSERT` | 块实例或 `<terminal>` | 普通 INSERT 展开块;特定连接点块转为端子 |
未列出的实体当前不会生成 ELMT 图元。
### LWPOLYLINE 映射规则
- 两个顶点且第一个顶点 `bulge != 0`:转换为圆弧。
- 两个顶点且没有 bulge转换为直线。
- 多个顶点且整体被识别为圆:转换为椭圆。
- 同时包含直线段和 bulge 圆弧段:按段拆分为多个 `<line>``<arc>`,避免圆弧丢失。
- 其他情况:转换为 `<polygon>`
- 开放多段线不会把最后一个顶点错误连接回第一个顶点;闭合多段线会保留闭合段。
## 样式映射
### 线宽
ELMT 只输出数值字段 `line-width-mm`,不再输出旧的
`line-weight:thin/normal/...` 档位。
LW 当前提供以下毫米预设值:
```text
0.00, 0.05, 0.09, 0.13, 0.15, 0.18, 0.20, 0.25,
0.30, 0.35, 0.40, 0.50, 0.53, 0.60, 0.70, 0.80,
0.90, 1.00, 1.06, 1.20, 1.40, 1.58, 2.00, 2.11 mm
```
## 重要工具
dxf2json.exe 必要时使用该工具将dxf文件转换为json文件这样可以清晰地查看dxf文件中的实体、块的架构以及对齐方式、颜色、坐标等信息
使用方法:./dxf2json 文件名
默认值为 `0.25 mm`。转换器得到毫米线宽后,会按 LW 的
`normalizedMillimeters` 规则选择距离最近的预设值,例如
`0.33 → 0.35 mm`、`0.68 → 0.70 mm`。超出范围的值会归一到最接近的端点预设。
线宽来源和优先级如下:
1. `LWPOLYLINE.constant_width` 大于 0 时,作为该图元的宽度来源。
2. 否则读取第一个顶点的 `starting_width``ending_width`
3. 没有多段线宽度时,读取 DXF 实体公共组码 370 `line_weight`,其非负值按 `1/100 mm` 转换,例如 `35` 得到 `0.35 mm`
4. `BYLAYER`、`BYBLOCK`、`DEFAULT` 等负值当前不会被伪造成固定线宽,也不会输出 `line-width-mm`
5. 对得到的毫米值执行 LW 预设归一化,再输出 `line-width-mm`
DXF 组码 39 的 `thickness` 是沿挤出方向的三维厚度,不是二维描边宽度。
转换器不会用 `thickness` 计算 `line-width-mm`
### 线型
以下 DXF 线型名称会转换为 ELMT 虚线:
- 名称中包含 `DASH`
- `BORDURE`
- `INTERROMPU`
- `INTERROMPUx2`
- `TRACECAD_ISO02W100`
## Supported Entities
其他线型目前按普通实线处理,不解析 DXF 线型的具体线段和空隙图案。
* Lines
* Circles
* Arcs
* Texts
* Ellipses
* Polylines
* LwPolylines
* Solids
* Splines
* Blocks (there are still some known issues for deeply nested block)
* MText (partial support)
* Leader
### 颜色
## To Do
- `LINE`、`ARC`、`LWPOLYLINE`、`TEXT` 和属性文本会优先读取 ACI 颜色索引,否则读取 24 位真彩色。
- 部分通过几何识别生成的图元仍使用黑色默认值,例如部分 `CIRCLE`、`ELLIPSE`、`POLYLINE`、`SPLINE`、`SOLID` 和 `TRACE` 路径。
- `BYLAYER``BYBLOCK` 颜色尚未完整解析其最终继承值。
* Support for the following
* Remaining 2d entities
* Styling (such as Dimension Styles)
## 坐标、单位和二维化
- DXF Y 轴会翻转以适配 ELMT 坐标方向。
- 图形会根据 DXF 头部的 `default_drawing_units` 缩放;无单位图纸暂不缩放。
- 当前毫米图纸使用代码中的 `3.78` 缩放因子。
- 转换完成后,元件图形会以自身包围盒为依据移动到局部坐标中心。
- INSERT 会应用块基点、插入位置、X/Y 缩放和旋转。
- ELMT 元件是二维图形,因此图元 Z 坐标、挤出方向和 `thickness` 不会转换成三维模型。
## 块和端子
- 被 INSERT 引用的 DXF 块会写入 ELMT 的 `<blocks>` 定义。
- 普通 INSERT 会展开为带 `block_name``block_instance` 元数据的块实例图元。
- 嵌套块可以递归转换;序列化时内部图元使用最外层有效实例元数据。
- 名称为 `ARROWUNKNOWN` 且带有 `connector` 属性的 INSERT 会转换为
QElectroTech `<terminal>`,属性值同时用于端子名称和连接点标签。
- 名称为 `ORIENT` 的辅助图层会跳过,不导出定位标记。
## 已知限制
- `HATCH`、`DIMENSION`、`3DFACE`、`MESH` 等未实现实体会被跳过。
- 不支持真正的三维符号3D 数据只保留可转换的二维 XY 投影。
- `LWPOLYLINE` 的逐段可变宽度尚未完整表达。目前会取第一个顶点的非零起始或结束宽度,并把它应用到整个转换结果。
- `LWPOLYLINE` 的几何宽度当前直接写入毫米字段,因此非毫米 DXF 的宽度还需要补充单位换算。
- 组码 370 的 `BYLAYER` / `BYBLOCK` 线宽尚未解析图层或父块的继承值。
- MTEXT 的内联格式、复杂字体替换、段落布局和背景框尚未完整支持。
- `--dtext` 当前不会改变行为TEXT 和 MTEXT 都默认输出动态文本。
- 深层嵌套块、非均匀缩放后的圆弧以及复杂镜像仍需要更多样图验证。
- 当前错误信息较少,部分不支持的实体会被静默忽略。
## DXF 诊断工具
必要时可使用 `dxf2json.exe` 把 DXF 转换为 JSON以检查实体、块、属性、
对齐方式、颜色、坐标、bulge 和宽度数据。
```bash
dxf2json drawing.dxf
```
## 测试
```bash
cargo test
```
* Better error messages
* Logging
当前回归测试覆盖 XRECORD 清理、端子语义、块实例、嵌套块、SOLID 填充、
混合 LWPOLYLINE 圆弧拆分、负 bulge 圆弧方向以及数值线宽转换。
## Compiling
## 待办事项
Compiled using Rust (MSRV 1.79.0).
- 支持剩余二维实体。
- 完善图层、块和尺寸样式继承。
- 完善非毫米图纸的多段线宽度换算。
- 改进错误信息和日志输出。
## Credits
* [Antonioaja](https://github.com/antonioaja) for creating the initial versions of [dxf2elmt](https://github.com/antonioaja/dxf2elmt). Thank you for all your work.
* [QElectroTech](https://qelectrotech.org/)
* [dxf-rs](https://github.com/IxMilia/dxf-rs)
* [simple-xml-builder](https://github.com/Accelbread/simple-xml-builder)
* [bspline](https://github.com/Twinklebear/bspline)
* [tempfile](https://github.com/Stebalien/tempfile)
- [Antonioaja](https://github.com/antonioaja) 创建了最初版本的
[dxf2elmt](https://github.com/antonioaja/dxf2elmt)。
- [QElectroTech](https://qelectrotech.org/)
- [dxf-rs](https://github.com/IxMilia/dxf-rs)
- [simple-xml-builder](https://github.com/Accelbread/simple-xml-builder)
- [bspline](https://github.com/Twinklebear/bspline)
- [tempfile](https://github.com/Stebalien/tempfile)

@ -129,6 +129,7 @@ fn main() -> Result<()> {
let mut polyline_count: u32 = 0;
let mut lwpolyline_count: u32 = 0;
let mut solid_count: u32 = 0;
let mut trace_count: u32 = 0;
let mut block_count: u32 = 0;
let mut other_count: u32 = 0;
@ -162,6 +163,9 @@ fn main() -> Result<()> {
EntityType::Solid(ref _solid) => {
solid_count += 1;
}
EntityType::Trace(ref _trace) => {
trace_count += 1;
}
EntityType::Insert(ref _insert) => {
block_count += 1;
}
@ -194,6 +198,7 @@ fn main() -> Result<()> {
println!("Polylines: {polyline_count}");
println!("LwPolylines: {lwpolyline_count}");
println!("Solids: {solid_count}");
println!("Traces: {trace_count}");
println!("Blocks: {block_count}");
println!("Currently Unsupported: {other_count}");

@ -88,17 +88,10 @@ impl Arc {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: if arc.thickness > 0.1 {
format!(
"line-style:normal;line-weight:normal;filling:none;color:{}",
color.display_rgb()
)
} else {
format!(
"line-style:normal;line-weight:thin;filling:none;color:{}",
color.display_rgb()
)
},
style: format!(
"line-style:normal;filling:none;color:{}",
color.display_rgb()
),
}
}
}
@ -147,20 +140,8 @@ impl Arc {
update_fn(&mut self.style);
}
pub fn update_line_weight(&mut self, lw: i16) {
let weight_str = match lw {
0 | -1 => "thin",
13..=25 => "thin",
26..=70 => "normal",
71..=100 => "thick",
_ => "extra-thick",
};
// 直接追加,不做复杂替换(简单有效)
if !self.style.is_empty() {
self.style.push_str("; ");
}
self.style.push_str(&format!("line-weight: {}", weight_str));
pub fn update_line_width_mm(&mut self, line_width_mm: f64) {
super::update_style_line_width_mm(&mut self.style, line_width_mm);
}
pub(crate) fn rotate_around(&mut self, pivot_x: f64, pivot_y: f64, angle_deg: f64) {
@ -279,17 +260,10 @@ impl Arc {
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:{}",
color.display_rgb()
)
} else {
format!(
"line-style:normal;line-weight:thin;filling:none;color:{}",
color.display_rgb()
)
},
style: format!(
"line-style:normal;filling:none;color:{}",
color.display_rgb()
),
antialias: false,
})
}

@ -29,12 +29,7 @@ impl From<&Circle> for Ellipse {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: if circ.thickness > 0.5 {
"line-style:normal;line-weight:normal;filling:none;color:black"
} else {
"line-style:normal;line-weight:thin;filling:none;color:black"
}
.into(),
style: "line-style:normal;filling:none;color:black".into(),
}
}
}
@ -51,7 +46,7 @@ impl From<&entities::Ellipse> for Ellipse {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: "line-style:normal;line-weight:thin;filling:none;color:black".into(),
style: "line-style:normal;filling:none;color:black".into(),
}
}
}
@ -96,7 +91,7 @@ impl TryFrom<&Polyline> for Ellipse {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: "line-style:normal;line-weight:thin;filling:none;color:black".into(),
style: "line-style:normal;filling:none;color:black".into(),
})
}
}
@ -141,7 +136,7 @@ impl Ellipse {
.fold(f64::MIN, |max_y, vtx| max_y.max(vtx.y));
let style = format!(
"line-style:normal;line-weight:thin;filling:none;color:rgb({},{},{})",
"line-style:normal;filling:none;color:rgb({},{},{})",
color.r, color.g, color.b
);
@ -194,6 +189,10 @@ impl Ellipse {
update_fn(&mut self.style);
}
pub fn update_line_width_mm(&mut self, line_width_mm: f64) {
super::update_style_line_width_mm(&mut self.style, line_width_mm);
}
pub fn set_color(&mut self, color: HexColor) {
self.color = color;
}

@ -51,12 +51,7 @@ impl TryFrom<&Polyline> for Line {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: if poly.thickness > 0.5 {
"line-style:normal;line-weight:normal;filling:none;color:black"
} else {
"line-style:normal;line-weight:thin;filling:none;color:black"
}
.into(),
style: "line-style:normal;filling:none;color:black".into(),
})
}
}
@ -89,13 +84,10 @@ impl Line {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: if poly.thickness > 0.1 {
format!("line-style:normal;line-weight:normal;filling:none;color:{}", color.display_rgb())
} else if poly.constant_width > 1.0{
format!("line-style:normal;line-weight:hight;filling:none;color:{}", color.display_rgb())
} else {
format!("line-style:normal;line-weight:thin;filling:none;color:{}", color.display_rgb())
},
style: format!(
"line-style:normal;filling:none;color:{}",
color.display_rgb()
),
})
}
@ -129,16 +121,7 @@ impl From<&entities::Leader> for Leader {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
//looks like line thickenss and color information I *might* need to grab from a dimension style
//entity which I haven't implemented yet
/*style: if line.thickness > 0.5 {
"line-style:normal;line-weight:normal;filling:none;color:black"
} else {
"line-style:normal;line-weight:thin;filling:none;color:black"
}
.into(),*/
style: "line-style:normal;line-weight:normal;filling:none;color:black"
.into(),
style: "line-style:normal;filling:none;color:black".into(),
}
})
.collect(),
@ -179,11 +162,10 @@ impl Line {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: if line.thickness > 0.5 {
format!("line-style:normal;line-weight:normal;filling:none;color:{}", color.display_rgb())
} else {
format!("line-style:normal;line-weight:thin;filling:none;color:{}", color.display_rgb())
},
style: format!(
"line-style:normal;filling:none;color:{}",
color.display_rgb()
),
}
}
@ -208,6 +190,11 @@ impl Line {
{
update_fn(&mut self.style);
}
/// 将 LWPOLYLINE 的 constant_width 或顶点宽度原值写入 LW 毫米线宽字段。
pub fn update_line_width_mm(&mut self, line_width_mm: f64) {
super::update_style_line_width_mm(&mut self.style, line_width_mm);
}
}
impl ScaleEntity for Line {

@ -654,6 +654,27 @@ impl Objects {
}
}
/// 仅给可描边图元写入 DXF 的数值线宽;文本和端子没有描边线宽。
fn apply_line_width_mm(&mut self, line_width_mm: f64) {
match self {
Objects::Arc(arc) => arc.update_line_width_mm(line_width_mm),
Objects::Ellipse(ellipse) => ellipse.update_line_width_mm(line_width_mm),
Objects::Polygon(polygon) => polygon.update_line_width_mm(line_width_mm),
Objects::Line(line) => line.update_line_width_mm(line_width_mm),
Objects::Group(objects) => {
for object in objects {
object.apply_line_width_mm(line_width_mm);
}
}
Objects::BlockInstance(block) => {
for object in &mut block.objects {
object.apply_line_width_mm(line_width_mm);
}
}
Objects::DynamicText(_) | Objects::Text(_) | Objects::Terminal(_) => {}
}
}
fn rotate_around(&mut self, pivot_x: f64, pivot_y: f64, angle_deg: f64) {
match self {
Objects::Arc(arc) => arc.rotate_around(pivot_x, pivot_y, angle_deg),
@ -1051,6 +1072,9 @@ impl<'a> ObjectsBuilder<'a> {
// 获取线型名称
let line_type_name: String = self.ent.common.line_type_name.clone();
// DXF 组码 370 以 1/100 mm 保存数值线宽;负值表示 BYLAYER/BYBLOCK 等继承方式,
// 当前没有解析到明确数值时不生成 line-width-mm。
let entity_line_width_mm = dxf_line_weight_mm(self.ent.common.lineweight_enum_value);
// 通用样式修改函数根据line_type_name修改现有style中的line-style
let update_line_style = |style: &mut String| {
@ -1083,7 +1107,7 @@ impl<'a> ObjectsBuilder<'a> {
};
// 实体类型转换处理
match &self.ent.specific {
let result = match &self.ent.specific {
// Circle → Ellipse :圆转换为椭圆
EntityType::Circle(circle) => {
let mut ellipse: Ellipse = circle.into();
@ -1298,10 +1322,10 @@ impl<'a> ObjectsBuilder<'a> {
} else {
HexColor::from_u32(self.ent.common.color_24_bit as u32)
};
// === 获取weight ===
let mut lw: Option<i16> = None;
// LW 直接读取毫米数值 line-width-mm不能再压缩成 thin/normal 等旧档位。
let mut line_width_mm = entity_line_width_mm;
if lwpolyline.constant_width > 0.0 {
lw = Some((lwpolyline.constant_width * 100.0) as i16);
line_width_mm = Some(lwpolyline.constant_width);
} else if let Some(v) = lwpolyline.vertices.get(0) {
let w = if v.starting_width > 0.0 {
v.starting_width
@ -1309,10 +1333,9 @@ impl<'a> ObjectsBuilder<'a> {
v.ending_width
};
if w > 0.0 {
lw = Some((w * 100.0) as i16);
line_width_mm = Some(w);
}
}
// ===============================
match lwpolyline.vertices.len() {
0 | 1 => Err("Error empty LwPolyline"),
@ -1321,9 +1344,8 @@ impl<'a> ObjectsBuilder<'a> {
match Arc::try_from_lwpolyline_with_color(lwpolyline, color) {
Ok(mut arc) => {
arc.update_line_style(&update_line_style);
// 更新线条粗细
if let Some(w) = lw {
arc.update_line_weight(w);
if let Some(width_mm) = line_width_mm {
arc.update_line_width_mm(width_mm);
}
arc.scale(self.scale_fact.x, self.scale_fact.y);
arc.x += self.offset.x;
@ -1340,6 +1362,9 @@ impl<'a> ObjectsBuilder<'a> {
// 根据line_type_name更新线型样式
line.update_line_style(&update_line_style);
if let Some(width_mm) = line_width_mm {
line.update_line_width_mm(width_mm);
}
line.scale(self.scale_fact.x, self.scale_fact.y);
line.x1 += self.offset.x;
@ -1356,6 +1381,9 @@ impl<'a> ObjectsBuilder<'a> {
{
// 根据line_type_name更新线型样式
ellipse.update_line_style(&update_line_style);
if let Some(width_mm) = line_width_mm {
ellipse.update_line_width_mm(width_mm);
}
ellipse.scale(self.scale_fact.x, self.scale_fact.y);
ellipse.x += self.offset.x;
@ -1373,12 +1401,15 @@ impl<'a> ObjectsBuilder<'a> {
match obj {
Objects::Arc(arc) => {
arc.update_line_style(&update_line_style);
if let Some(w) = lw {
arc.update_line_weight(w);
if let Some(width_mm) = line_width_mm {
arc.update_line_width_mm(width_mm);
}
}
Objects::Line(line) => {
line.update_line_style(&update_line_style);
if let Some(width_mm) = line_width_mm {
line.update_line_width_mm(width_mm);
}
}
_ => {}
}
@ -1393,6 +1424,9 @@ impl<'a> ObjectsBuilder<'a> {
// 根据line_type_name更新线型样式
poly.update_line_style(&update_line_style);
if let Some(width_mm) = line_width_mm {
poly.update_line_width_mm(width_mm);
}
poly.scale(self.scale_fact.x, self.scale_fact.y);
for cord in &mut poly.coordinates {
@ -1418,6 +1452,18 @@ impl<'a> ObjectsBuilder<'a> {
Ok(Objects::Polygon(poly))
}
EntityType::Trace(trace) => {
let mut poly: Polygon = trace.into();
poly.scale(self.scale_fact.x, self.scale_fact.y);
for cord in &mut poly.coordinates {
cord.x += self.offset.x;
cord.y -= self.offset.y;
}
Ok(Objects::Polygon(poly))
}
EntityType::Insert(ins) => {
if is_arrowunknown_terminal(ins) {
if let Some(mut terminal) = Terminal::from_insert(ins) {
@ -1520,7 +1566,20 @@ impl<'a> ObjectsBuilder<'a> {
//dbg!(&self.ent.specific);
Err("Need to implement the rest of the entity types")
}
};
let mut object = result?;
// LWPOLYLINE 已优先使用 constant_width/顶点宽度INSERT 内的子图元各自读取线宽。
if !matches!(
&self.ent.specific,
EntityType::LwPolyline(_) | EntityType::Insert(_)
) {
if let Some(line_width_mm) = entity_line_width_mm {
object.apply_line_width_mm(line_width_mm);
}
}
Ok(object)
}
}
@ -2257,6 +2316,54 @@ pub fn two_dec(num: f64) -> f64 {
(num * 100.0).round() / 100.0
}
// 与 LW LineWidthSettings::options() 保持一致。首项是“默认 0.25 mm”
// 后面仍包含可显式选择的 0.25 mm保留这个顺序才能复现等距时的选择结果。
const LW_LINE_WIDTH_PRESETS_MM: [f64; 25] = [
0.25, 0.00, 0.05, 0.09, 0.13, 0.15, 0.18, 0.20, 0.25, 0.30, 0.35, 0.40, 0.50, 0.53, 0.60, 0.70,
0.80, 0.90, 1.00, 1.06, 1.20, 1.40, 1.58, 2.00, 2.11,
];
/// 按 LW 的规则选择距离输入值最近的预设线宽;距离相同时保留列表中靠前的值。
fn normalize_lw_line_width_mm(line_width_mm: f64) -> f64 {
let mut best = LW_LINE_WIDTH_PRESETS_MM[0];
let mut best_distance = (line_width_mm - best).abs();
for preset in LW_LINE_WIDTH_PRESETS_MM.iter().copied().skip(1) {
let distance = (line_width_mm - preset).abs();
if distance < best_distance {
best = preset;
best_distance = distance;
}
}
best
}
/// LW 使用 line-width-mm 保存毫米线宽,不再输出旧的 line-weight 档位字段。
/// 删除 style 中已有的两个线宽字段后,再写入归一化后的 LW 预设值。
fn update_style_line_width_mm(style: &mut String, line_width_mm: f64) {
let mut properties = style
.split(';')
.map(str::trim)
.filter(|part| {
!part.is_empty()
&& !part.starts_with("line-weight:")
&& !part.starts_with("line-width-mm:")
})
.map(str::to_string)
.collect::<Vec<_>>();
let normalized = normalize_lw_line_width_mm(line_width_mm);
properties.push(format!("line-width-mm:{normalized:.2}"));
*style = properties.join(";");
}
/// DXF 实体公共组码 370 的非负值单位为 1/100 mm。
/// BYLAYER、BYBLOCK、DEFAULT 等负值不是具体线宽,不能伪造成固定值。
fn dxf_line_weight_mm(raw_value: i16) -> Option<f64> {
(raw_value >= 0).then(|| f64::from(raw_value) / 100.0)
}
//Should be the relevant Qt5 Code for the font string in Qt5...
//Might need to look it up for Qt6, since it appears to have changed
//and add in support for either or?
@ -2652,11 +2759,13 @@ pub(crate) fn strip_mtext_control_sequences(input: &str) -> String {
#[cfg(test)]
mod tests {
use super::{
xml_elements_for_object, BlockInstance, BlockMetadata, Definition, Description, Line,
Objects, ObjectsBuilder, ScaleEntity,
dxf_line_weight_mm, normalize_lw_line_width_mm, xml_elements_for_object, BlockInstance,
BlockMetadata, Definition, Description, Line, Objects, ObjectsBuilder, ScaleEntity,
};
use dxf::entities::{
Attribute, Entity, EntityType, Insert, Line as DxfLine, LwPolyline, Solid,
};
use dxf::entities::{Attribute, Entity, EntityType, Insert, LwPolyline};
use dxf::{Drawing, LwPolylineVertex, Point};
use dxf::{Block, Drawing, LwPolylineVertex, Point};
fn connector_insert(rotation: f64) -> Insert {
let mut insert = Insert {
@ -2710,6 +2819,35 @@ mod tests {
assert!(xml.contains("<connection_point tag=\"N:0-C:0\""));
}
#[test]
fn solid_inside_block_is_exported_as_filled_polygon() {
let solid = Solid::new(
Point::new(0.0, 0.0, 0.0),
Point::new(10.0, 0.0, 0.0),
Point::new(0.0, 5.0, 0.0),
Point::new(10.0, 5.0, 0.0),
);
let mut drawing = Drawing::new();
drawing.add_block(Block {
name: "FILLED_BLOCK".into(),
entities: vec![Entity::new(EntityType::Solid(solid))],
..Default::default()
});
drawing.add_entity(Entity::new(EntityType::Insert(Insert {
name: "FILLED_BLOCK".into(),
..Default::default()
})));
let definition = Definition::new("filled-block", 20, &drawing);
let xml = simple_xml_builder::XMLElement::from(&definition).to_string();
assert!(xml.contains("filling:black"), "unexpected XML: {xml}");
assert!(
xml.contains("block_name=\"FILLED_BLOCK\""),
"unexpected XML: {xml}"
);
}
#[test]
fn mixed_lwpolyline_keeps_line_and_negative_bulge_arc() {
let polyline = LwPolyline {
@ -2751,6 +2889,49 @@ mod tests {
assert!(xml.contains("height=\"5\""), "unexpected XML: {xml}");
assert!(xml.contains("start=\"0\""), "unexpected XML: {xml}");
assert!(xml.contains("angle=\"180\""), "unexpected XML: {xml}");
assert_eq!(
2,
xml.matches("line-width-mm:0.35").count(),
"直线和圆弧都应继承 constant_width: {xml}"
);
assert!(
!xml.contains("line-weight:"),
"数值线宽不应再携带旧的 line-weight 档位: {xml}"
);
}
#[test]
fn dxf_numeric_lineweight_is_used_and_thickness_is_ignored() {
let mut entity = Entity::new(EntityType::Line(DxfLine {
thickness: 99.0,
p1: Point::new(0.0, 0.0, 0.0),
p2: Point::new(10.0, 0.0, 0.0),
..Default::default()
}));
entity.common.lineweight_enum_value = 35;
let object = ObjectsBuilder::new(&entity, 20)
.build()
.expect("DXF LINE should be converted");
let xml = xml_elements_for_object(&object, None)
.into_iter()
.map(|element| element.to_string())
.collect::<Vec<_>>()
.join("");
assert!(xml.contains("line-width-mm:0.35"), "unexpected XML: {xml}");
assert!(!xml.contains("line-weight:"), "unexpected XML: {xml}");
assert!(!xml.contains("line-width-mm:0.25"), "unexpected XML: {xml}");
assert_eq!(None, dxf_line_weight_mm(-1));
assert_eq!(None, dxf_line_weight_mm(-2));
}
#[test]
fn line_width_is_normalized_to_the_nearest_lw_preset() {
assert_eq!(0.35, normalize_lw_line_width_mm(0.33));
assert_eq!(0.70, normalize_lw_line_width_mm(0.68));
assert_eq!(0.25, normalize_lw_line_width_mm(0.27));
assert_eq!(2.11, normalize_lw_line_width_mm(9.99));
}
#[test]
@ -2761,7 +2942,7 @@ mod tests {
-8.0,
42.0,
16.0,
"line-style:normal;line-weight:thin;filling:none;color:black".into(),
"line-style:normal;line-width-mm:0.00;filling:none;color:black".into(),
))])],
};
@ -2779,7 +2960,7 @@ mod tests {
-2_070.0,
1_330.0,
-2_050.0,
"line-style:normal;line-weight:thin;filling:none;color:black".into(),
"line-style:normal;line-width-mm:0.00;filling:none;color:black".into(),
))])],
};
@ -2804,7 +2985,7 @@ mod tests {
0.0,
10.0,
0.0,
"line-style:normal;line-weight:thin;filling:none;color:black".into(),
"line-style:normal;line-width-mm:0.00;filling:none;color:black".into(),
))],
})],
};
@ -2822,7 +3003,7 @@ mod tests {
0.0,
20.0,
0.0,
"line-style:normal;line-weight:thin;filling:none;color:black".into(),
"line-style:normal;line-width-mm:0.00;filling:none;color:black".into(),
))];
super::rotate_objects_around(&mut objects, 10.0, 0.0, super::qet_insert_rotation(90.0));
@ -2855,7 +3036,7 @@ mod tests {
0.0,
10.0,
0.0,
"line-style:normal;line-weight:thin;filling:none;color:black".into(),
"line-style:normal;line-width-mm:0.00;filling:none;color:black".into(),
))],
})],
})],

@ -1,5 +1,6 @@
use super::{two_dec, ScaleEntity};
use dxf::entities::{LwPolyline, Polyline, Solid, Spline};
use dxf::entities::{LwPolyline, Polyline, Solid, Spline, Trace};
use dxf::Point as DxfPoint;
use hex_color::HexColor;
use simple_xml_builder::XMLElement;
use std::ops::{Add, Mul};
@ -68,12 +69,7 @@ impl From<&Polyline> for Polygon {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: if poly.thickness > 0.1 {
"line-style:normal;line-weight:normal;filling:none;color:black"
} else {
"line-style:normal;line-weight:thin;filling:none;color:black"
}
.into(),
style: "line-style:normal;filling:none;color:black".into(),
}
}
}
@ -100,11 +96,10 @@ impl Polygon {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: if poly.thickness > 0.1 {
format!("line-style:normal;line-weight:normal;filling:none;color:{}", color.display_rgb())
} else {
format!("line-style:normal;line-weight:thin;filling:none;color:{}", color.display_rgb())
},
style: format!(
"line-style:normal;filling:none;color:{}",
color.display_rgb()
),
}
}
}
@ -161,43 +156,30 @@ impl From<(&Spline, u32)> for Polygon {
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: "line-style:normal;line-weight:thin;filling:none;color:black".into(),
style: "line-style:normal;filling:none;color:black".into(),
}
}
}
impl From<&Solid> for Polygon {
fn from(solid: &Solid) -> Self {
Polygon {
coordinates: vec![
Coordinate {
x: solid.first_corner.x,
y: -solid.first_corner.y,
},
Coordinate {
x: solid.second_corner.x,
y: -solid.second_corner.y,
},
Coordinate {
x: solid.third_corner.x,
y: -solid.third_corner.y,
},
Coordinate {
x: solid.fourth_corner.x,
y: -solid.fourth_corner.y,
},
],
closed: true,
//in the original code antialias is always set to false...I'm guessing for performance
//reasons...I'm trying to think if there is a time we might want to turn it on?
antialias: false,
style: if solid.thickness > 0.5 {
"line-style:normal;line-weight:normal;filling:black;color:black"
} else {
"line-style:normal;line-weight:thin;filling:black;color:black"
}
.into(),
}
Self::from_filled_quadrilateral(
&solid.first_corner,
&solid.second_corner,
&solid.third_corner,
&solid.fourth_corner,
)
}
}
impl From<&Trace> for Polygon {
fn from(trace: &Trace) -> Self {
Self::from_filled_quadrilateral(
&trace.first_corner,
&trace.second_corner,
&trace.third_corner,
&trace.fourth_corner,
)
}
}
@ -222,12 +204,40 @@ impl From<&Polygon> for XMLElement {
}
impl Polygon {
fn from_filled_quadrilateral(
first: &DxfPoint,
second: &DxfPoint,
third: &DxfPoint,
fourth: &DxfPoint,
) -> Self {
// DXF SOLID/TRACE 的最后两个顶点以特殊顺序存储;按 1-2-4-3
// 还原边界,避免常见矩形在 QET 中成为无法填充的自交多边形。
let coordinates = [first, second, fourth, third]
.into_iter()
.map(|corner| Coordinate {
x: corner.x,
y: -corner.y,
})
.collect();
Self {
coordinates,
closed: true,
antialias: false,
style: "line-style:normal;filling:black;color:black".into(),
}
}
pub fn update_line_style<F>(&mut self, update_fn: F)
where
F: FnOnce(&mut String),
{
update_fn(&mut self.style);
}
pub fn update_line_width_mm(&mut self, line_width_mm: f64) {
super::update_style_line_width_mm(&mut self.style, line_width_mm);
}
}
impl ScaleEntity for Polygon {
@ -336,11 +346,10 @@ pub fn decompose_rounded_rectangle_with_color(lwpolyline: &LwPolyline, color: He
}
} else {
// 创建直线段
let style = if lwpolyline.thickness > 0.5 {
format!("line-style:normal;line-weight:normal;filling:none;color:color:{}", color.display_rgb())
} else {
format!("line-style:normal;line-weight:thin;filling:none;color:color:{}", color.display_rgb())
};
let style = format!(
"line-style:normal;filling:none;color:color:{}",
color.display_rgb()
);
let line = super::Line::new(
current.x,
@ -457,7 +466,10 @@ fn create_arc_from_bulge_with_color(start_x: f64, start_y: f64, end_x: f64, end_
// let start_angle = (start_y - center_y).atan2(start_x - center_x).to_degrees();
// let angle_span = angle.to_degrees().abs();
let style = format!("line-style:normal;line-weight:thin;filling:none;color:{}", color.display_rgb());
let style = format!(
"line-style:normal;filling:none;color:{}",
color.display_rgb()
);
Some(super::Arc::new(
center_x-radius,
@ -468,4 +480,35 @@ fn create_arc_from_bulge_with_color(start_x: f64, start_y: f64, end_x: f64, end_
arc_angle,
style,
))
}
}
#[cfg(test)]
mod tests {
use super::Polygon;
use dxf::entities::Solid;
use dxf::Point;
#[test]
fn solid_vertices_follow_the_visible_boundary_order() {
let solid = Solid::new(
Point::new(0.0, 0.0, 0.0),
Point::new(10.0, 0.0, 0.0),
Point::new(0.0, 5.0, 0.0),
Point::new(10.0, 5.0, 0.0),
);
let polygon = Polygon::from(&solid);
let coordinates = polygon
.coordinates
.iter()
.map(|coordinate| (coordinate.x, coordinate.y))
.collect::<Vec<_>>();
assert_eq!(
vec![(0.0, 0.0), (10.0, 0.0), (10.0, -5.0), (0.0, -5.0)],
coordinates
);
assert!(polygon.closed);
assert!(polygon.style.contains("filling:black"));
}
}

Loading…
Cancel
Save