diff --git a/README.md b/README.md index b6a5079..f4fb912 100644 --- a/README.md +++ b/README.md @@ -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 +源项目地址: -## 注意:提前配置好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 ` | 设置 SPLINE 的折线细分数量,默认值为 20;数值越大越平滑 | +| `-i`, `--info` | 输出实体统计和转换耗时 | +| `-d`, `--dtext` | 当前为保留参数;现有实现始终把 TEXT/MTEXT 转为动态文本 | + +查看完整参数: ```bash -./dxf2elmt my_file.dxf -v +dxf2elmt --help +``` + +## 当前实体映射 + +| DXF 实体 | ELMT 图元 | 当前处理方式 | +| --- | --- | --- | +| `LINE` | `` | 保留两个端点、颜色、线型和数值线宽 | +| `CIRCLE` | `` | 以等宽、等高椭圆表示圆 | +| `ARC` | `` | 保留圆心、半径、起始角和圆弧角度 | +| `ELLIPSE` | `` | 转换中心、长轴和短长轴比例 | +| `POLYLINE` | `` / `` / `` | 两个顶点转直线;识别为圆时转椭圆;其余转多边形 | +| `LWPOLYLINE` | `` / `` / `` / `` | 根据顶点数、圆形特征和 bulge 分别处理,详细规则见下文 | +| `SPLINE` | `` | 按 `spline-step` 采样为折线,不保留参数曲线本身 | +| `SOLID` | 填充 `` | 按 DXF 的 `1-2-4-3` 顶点顺序恢复边界并填充 | +| `TRACE` | 填充 `` | 与 SOLID 相同,按四边形边界转换并填充 | +| `TEXT` | `` | 转换内容、位置、旋转、字号、宽度系数和对齐方式 | +| `MTEXT` | `` | 部分支持,转换内容、位置、方向、字号和对齐方式 | +| `ATTDEF`(属性定义) | `` | 转换属性标签及其文本样式和位置 | +| `LEADER` | 一组 `` | 相邻顶点生成线段,首段可带箭头 | +| `INSERT` | 块实例或 `` | 普通 INSERT 展开块;特定连接点块转为端子 | + +未列出的实体当前不会生成 ELMT 图元。 + +### LWPOLYLINE 映射规则 + +- 两个顶点且第一个顶点 `bulge != 0`:转换为圆弧。 +- 两个顶点且没有 bulge:转换为直线。 +- 多个顶点且整体被识别为圆:转换为椭圆。 +- 同时包含直线段和 bulge 圆弧段:按段拆分为多个 `` 和 ``,避免圆弧丢失。 +- 其他情况:转换为 ``。 +- 开放多段线不会把最后一个顶点错误连接回第一个顶点;闭合多段线会保留闭合段。 + +## 样式映射 + +### 线宽 + +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 的 `` 定义。 +- 普通 INSERT 会展开为带 `block_name` 和 `block_instance` 元数据的块实例图元。 +- 嵌套块可以递归转换;序列化时内部图元使用最外层有效实例元数据。 +- 名称为 `ARROWUNKNOWN` 且带有 `connector` 属性的 INSERT 会转换为 + QElectroTech ``,属性值同时用于端子名称和连接点标签。 +- 名称为 `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) diff --git a/src/main.rs b/src/main.rs index da13200..f3bf959 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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}"); diff --git a/src/qelmt/arc.rs b/src/qelmt/arc.rs index b030fb9..f5f9988 100644 --- a/src/qelmt/arc.rs +++ b/src/qelmt/arc.rs @@ -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, }) } diff --git a/src/qelmt/ellipse.rs b/src/qelmt/ellipse.rs index 5035941..1f660b8 100644 --- a/src/qelmt/ellipse.rs +++ b/src/qelmt/ellipse.rs @@ -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; } diff --git a/src/qelmt/line.rs b/src/qelmt/line.rs index 9f02082..473eae1 100644 --- a/src/qelmt/line.rs +++ b/src/qelmt/line.rs @@ -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 { diff --git a/src/qelmt/mod.rs b/src/qelmt/mod.rs index 62a8342..cc745cc 100644 --- a/src/qelmt/mod.rs +++ b/src/qelmt/mod.rs @@ -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 = 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::>(); + + 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 { + (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(">() + .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(), ))], })], })], diff --git a/src/qelmt/polygon.rs b/src/qelmt/polygon.rs index 2ececcc..9c2cebc 100644 --- a/src/qelmt/polygon.rs +++ b/src/qelmt/polygon.rs @@ -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(&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, )) -} \ No newline at end of file +} + +#[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::>(); + + 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")); + } +}