diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 5e3bd76..b6339e3 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ -# These are supported funding model platforms +# GitHub 仓库页显示的项目赞助渠道。 ko_fi: vadoola buy_me_a_coffee: vadoola diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 2baac5c..b2260ec 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -1,12 +1,15 @@ +# 手动触发的多架构发布包构建;产物只上传到本次 GitHub Actions 运行。 name: Package on: + # 避免每次提交都执行耗时的交叉编译。 workflow_dispatch: jobs: build: runs-on: ${{ matrix.os }}-latest strategy: + # 覆盖 Linux 四种架构、macOS 两种架构和 Windows 两种架构。 fail-fast: true matrix: include: @@ -45,6 +48,7 @@ jobs: targets: ${{ matrix.target }} - name: Check crate + # 在原生可运行的平台先验证 crates.io 打包内容,不真正发布。 if: matrix.os == 'macos' || matrix.os == 'windows' || matrix.os == 'ubuntu' && matrix.arch == 'amd64' run: cargo publish --dry-run --target ${{ matrix.target }} @@ -52,12 +56,14 @@ jobs: run: cargo clippy --release -- -D warnings - name: Test (release mode) + # 只在可直接运行目标二进制的平台执行测试;测试后清理以免混入打包产物。 if: matrix.os == 'macos' || matrix.os == 'ubuntu' || matrix.os == 'windows' && matrix.arch == 'amd64' run: | cargo test --release --verbose -- --nocapture && cargo clean - name: Install Cross + # Linux 非宿主架构通过 cross 的容器工具链编译。 if: matrix.os == 'ubuntu' run: cargo install cross --git https://github.com/cross-rs/cross @@ -70,6 +76,7 @@ jobs: run: cargo build --release --target ${{ matrix.target }} - name: Upload build artifacts + # 同时兼容 Unix 无扩展名程序和 Windows `.exe`。 uses: actions/upload-artifact@v4 with: name: build-${{ matrix.os }}-${{ matrix.target }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index cbb17a2..3c5c75c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,18 +1,20 @@ +# 发布标签触发的跨平台质量检查:格式、编译、Clippy 和单元测试。 name: Rust on: + # 仅版本号标签触发,例如 0.5.0。 push: tags: ["[0-9]+.[0-9]+.[0-9]+*"] env: CARGO_TERM_COLOR: always -# Linters inspired from here: https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md jobs: rust: name: ${{ matrix.os }}-latest runs-on: ${{ matrix.os }}-latest strategy: + # 任一平台失败后停止其他矩阵任务,尽快暴露发布阻塞。 fail-fast: true matrix: include: @@ -27,15 +29,17 @@ jobs: components: rustfmt, clippy - name: fmt + # 确保仓库源码已经按 rustfmt 规则提交。 run: cargo fmt --all -- --check - name: build + # 当前只由标签触发;条件保留了历史 PR 工作流的 Windows 例外。 if: matrix.os != 'windows' || github.event_name != 'pull_request' run: cargo build --verbose - name: clippy if: matrix.os != 'windows' || github.event_name != 'pull_request' - #run: cargo clippy -- -D warnings + # 暂未把警告提升为错误,便于现有代码继续发布。 run: cargo clippy -- - name: test diff --git a/.gitignore b/.gitignore index 9a446b2..5edc2ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,18 @@ +# Rust 编译产物。 /target + +# 可选的容器/交叉编译本地配置。 Dockerfile -*.dxf -*.elmt cross.toml /.cargo + +# 转换输入、输出及随项目分发的辅助可执行文件。 +*.dxf +*.elmt *.txt *.exe + +# 编辑器、平台缓存和本仓库选择不跟踪的锁文件。 /.vscode Cargo.lock -.DS_Store \ No newline at end of file +.DS_Store diff --git a/Cargo.toml b/Cargo.toml index 5351e42..1f84dc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,22 +14,30 @@ strip = true lto = true [profile.dev.package."*"] +# 开发构建仍优化第三方依赖,减少 DXF 解析和 XML 生成的等待时间。 opt-level = 3 [dependencies] +# DXF 解析、ELMT XML 构造与样条曲线采样。 dxf = "0.6.0" simple-xml-builder = "1.1.0" bspline = "1.1.0" + +# 命令行、错误上下文、临时文件与 Windows 通配符支持。 uuid = { version = "1.16", features = ["serde", "v4"] } tempfile = "3.15" clap = { version = "4.5", features = ["derive"] } anyhow = "1.0.97" wild = "2.2" + +# 图元转换过程中使用的并行、颜色、迭代器和文本处理工具。 rayon = "1.10.0" hex_color = "3.0.0" itertools = "0.14" parley = "0.2.0" unicode-segmentation = "1.12.0" + +# tracing 默认写日志;venator 仅在同名 feature 启用时接收追踪数据。 tracing = "0.1" venator = { version = "1.1", optional = true } tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/src/dxf_loader.rs b/src/dxf_loader.rs index f12e7c2..6c3e0dc 100644 --- a/src/dxf_loader.rs +++ b/src/dxf_loader.rs @@ -1,3 +1,9 @@ +//! DXF 文件加载与兼容性清洗。 +//! +//! 正常情况下直接交给 `dxf` crate 解析。某些第三方 DXF 会在 OBJECTS +//! 区段的 XRECORD 中写入解析器不支持的组码;首次加载失败后,本模块只删除 +//! 这些 XRECORD 组码对并用临时文件重试,不会改写用户的源文件。 + use anyhow::{Context, Result}; use dxf::Drawing; use std::io::Write; @@ -5,11 +11,15 @@ use std::path::Path; use tempfile::NamedTempFile; use tracing::warn; +/// 清洗后的 DXF 字节及删除的“组码行 + 值行”对数量。 struct SanitizedDxf { bytes: Vec, removed_pairs: usize, } +/// 加载 DXF;直接解析失败时,尝试清理不受支持的 XRECORD 组码后再解析一次。 +/// +/// 清洗副本使用临时文件保存,原始 `path` 始终只读。 pub fn load_file(path: &Path) -> Result { match Drawing::load_file(path) { Ok(drawing) => Ok(drawing), @@ -41,12 +51,17 @@ pub fn load_file(path: &Path) -> Result { } } +/// 读取源文件并执行内存清洗;无需清洗时返回 `None`。 fn sanitize_file(path: &Path) -> Result> { let bytes = std::fs::read(path).with_context(|| format!("Could not read {}", path.display()))?; Ok(sanitize_unsupported_xrecord_pairs(&bytes)) } +/// 只移除 `OBJECTS/ XRECORD` 范围内不属于 DXF 标准集合的组码对。 +/// +/// DXF 文本格式以两行为一组:第一行是组码,第二行是对应值。函数保留原始 +/// 换行符和未命中的全部字节,避免因为清洗改变编码或其他区段内容。 fn sanitize_unsupported_xrecord_pairs(bytes: &[u8]) -> Option { let lines = split_lines_preserving_endings(bytes); let mut output = Vec::with_capacity(bytes.len()); @@ -56,6 +71,7 @@ fn sanitize_unsupported_xrecord_pairs(bytes: &[u8]) -> Option { let mut in_xrecord = false; let mut index = 0; + // 逐组码对扫描,同时维护当前所在 SECTION 和对象类型。 while index < lines.len() { let code_line = lines[index]; let Some(value_line) = lines.get(index + 1).copied() else { @@ -93,6 +109,10 @@ fn sanitize_unsupported_xrecord_pairs(bytes: &[u8]) -> Option { }) } +/// 根据当前组码和值推进 SECTION/XRECORD 状态机。 +/// +/// `0 SECTION` 后的 `2 ` 决定区段名称;在 OBJECTS 区段中,每个 +/// `0 ` 都会开始一个新对象,因此可据此进入或离开 XRECORD。 fn update_section_state( code: i32, value: &[u8], @@ -127,6 +147,7 @@ fn update_section_state( } } +/// 判断组码是否属于 `dxf` crate 当前能够处理的标准范围。 fn is_supported_group_code(code: i32) -> bool { matches!( code, @@ -142,6 +163,7 @@ fn is_supported_group_code(code: i32) -> bool { ) } +/// 从保留原始换行的 ASCII 行中解析 DXF 组码。 fn parse_group_code(line: &[u8]) -> Option { std::str::from_utf8(trimmed_ascii(line)) .ok()? @@ -149,6 +171,7 @@ fn parse_group_code(line: &[u8]) -> Option { .ok() } +/// 去掉 ASCII 空白并返回原切片中的有效部分,不分配新字符串。 fn trimmed_ascii(line: &[u8]) -> &[u8] { let mut start = 0; let mut end = line.len(); @@ -164,6 +187,7 @@ fn trimmed_ascii(line: &[u8]) -> &[u8] { &line[start..end] } +/// 按行切分字节,同时把 `\n` 留在各行末尾以便原样重建文件。 fn split_lines_preserving_endings(bytes: &[u8]) -> Vec<&[u8]> { let mut lines = Vec::new(); let mut start = 0; @@ -184,6 +208,8 @@ fn split_lines_preserving_endings(bytes: &[u8]) -> Vec<&[u8]> { #[cfg(test)] mod tests { + //! 清洗器回归测试:限制删除范围,避免误伤 ENTITIES 等其他区段。 + use super::sanitize_unsupported_xrecord_pairs; #[test] diff --git a/src/file_writer.rs b/src/file_writer.rs index 7d38abd..2d57aa7 100644 --- a/src/file_writer.rs +++ b/src/file_writer.rs @@ -1,3 +1,8 @@ +//! ELMT 输出目标选择。 +//! +//! 正常模式在 DXF 旁创建同名 `.elmt` 文件;`--verbose` 模式使用临时文件, +//! 使上层仍可走同一套 XML 写入流程,同时避免在磁盘留下正式输出文件。 + extern crate tempfile; use anyhow::Context; @@ -5,6 +10,10 @@ use std::fs::File; use std::path::{Path, PathBuf}; use tempfile::tempfile; +/// 根据输出模式创建 XML 写入目标。 +/// +/// `verbose_output == false` 时会覆盖同路径下已有的 `.elmt` 文件;为 `true` +/// 时返回匿名临时文件,最终 XML 由调用方另外打印到标准输出。 pub fn create_file( verbose_output: bool, _info: bool, diff --git a/src/main.rs b/src/main.rs index f3bf959..15e37ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,9 @@ +//! 命令行程序入口。 +//! +//! 整体流程为:解析参数 → 加载并容错清洗 DXF → 构造 QElectroTech 元件定义 +//! → 序列化为 `.elmt` XML → 按需输出实体统计。真正的图元映射位于 +//! [`qelmt`] 模块,DXF 加载兼容逻辑位于 [`dxf_loader`] 模块。 + #![warn( clippy::all, clippy::pedantic, @@ -32,23 +38,23 @@ mod qelmt; #[command(name = "dxf2elmt")] #[command(author, version, about = "A CLI program to convert .dxf files into .elmt files", long_about = None)] struct Args { - /// The .dxf file to convert + /// 一个或多个需要转换的 DXF 文件路径。 //#[clap(short, long, value_parser)] file_names: Vec, - /// Activates verbose output, eliminates .elmt file writing + /// 不创建 `.elmt` 文件,改为把生成的 XML 输出到标准输出。 #[clap(short, long, value_parser, default_value_t = false)] verbose: bool, - /// Converts text entities into dynamic text instead of the default text box + /// 文本转换开关;当前转换流程始终生成动态文本,此参数暂作兼容保留。 #[clap(short, long, value_parser, default_value_t = false)] dtext: bool, - /// Determine the number of lines you want each spline to have (more lines = greater resolution) + /// SPLINE 的采样段数;数值越大,折线逼近越平滑。 #[clap(short, long, value_parser, default_value_t = 20)] spline_step: u32, - /// Toggles information output... defaults to off + /// 输出源实体数量和累计转换耗时。 #[clap(short, long, value_parser, default_value_t = false)] info: bool, } @@ -57,6 +63,7 @@ pub mod file_writer; #[allow(clippy::too_many_lines)] fn main() -> Result<()> { + // `venator` 特性用于交互式追踪;默认构建则同时准备文件日志和 stderr 日志。 #[cfg(feature = "venator")] let tr_reg = tracing_subscriber::registry() .with(Venator::default()) @@ -66,7 +73,7 @@ fn main() -> Result<()> { let tr_reg = { let file_layer = if let std::result::Result::Ok(file) = std::fs::File::create("dxf2elmt.log") { - //if we can create a log file use it + // 日志文件不可创建时继续转换,仅关闭文件日志层。 Some(tracing_subscriber::fmt::layer().with_writer(std::sync::Arc::new(file))) } else { None @@ -95,13 +102,13 @@ fn main() -> Result<()> { trace!("Starting dxf2elmt"); - // Start recording time + // 计时覆盖本次命令处理的全部输入文件。 let now: Instant = Instant::now(); - // Collect arguments + // `wild::args` 在 Windows 上补充通配符展开能力。 let args: Args = Args::parse_from(wild::args()); - // Load dxf file + // 为批量文件转换创建统一的 tracing span,便于诊断递归块转换。 let dxf_loop_span = span!(Level::TRACE, "Looping over dxf files"); let dxf_loop_guard = dxf_loop_span.enter(); for file_name in args.file_names { @@ -109,17 +116,17 @@ fn main() -> Result<()> { .file_stem() .unwrap_or_else(|| file_name.as_os_str()) .to_string_lossy(); - // 加载dxf文件,获取dxf解析对象drawing + // 优先直接解析 DXF;遇到特定 XRECORD 兼容问题时加载器会清洗后重试。 let drawing: Drawing = dxf_loader::load_file(&file_name).context(format!( "Failed to load {friendly_file_name}...\n\tMake sure the file is a valid .dxf file.", ))?; - // 传入文件名、步长、dxf解析对象drawing,创建elmt的definition标签 + // Definition 会完成单位缩放、图元映射、块展开和局部坐标居中。 let q_elmt = Definition::new(friendly_file_name.clone(), args.spline_step, &drawing); if !args.verbose && args.info { println!("{friendly_file_name} loaded..."); } - // Initialize counts + // 下列计数只用于 `--info` 诊断,不参与转换结果。 let mut circle_count: u32 = 0; let mut line_count: u32 = 0; let mut arc_count: u32 = 0; @@ -133,7 +140,7 @@ fn main() -> Result<()> { let mut block_count: u32 = 0; let mut other_count: u32 = 0; - // Loop through all entities, counting the element types + // 统计模型空间顶层实体;块内部实体不会计入这里。 //drawing.entities().for_each(|e| match e.specific { drawing.entities().for_each(|e| match e.specific { EntityType::Circle(ref _circle) => { @@ -174,10 +181,10 @@ fn main() -> Result<()> { } }); - // Create output file for .elmt + // 普通模式创建同名 `.elmt`,verbose 模式使用临时文件承接序列化输出。 let out_file = file_writer::create_file(args.verbose, args.info, &file_name)?; - // Write to output file + // simple-xml-builder 负责把内部 Definition 对象树写成 ELMT XML。 let out_xml = XMLElement::from(&q_elmt); out_xml .write(&out_file) @@ -186,7 +193,7 @@ fn main() -> Result<()> { if args.info { println!("Conversion complete!\n"); - // Print stats + // 输出源 DXF 顶层实体统计,便于确认哪些类型仍未支持。 println!("STATS"); println!("~~~~~~~~~~~~~~~"); println!("Circles: {circle_count}"); diff --git a/src/qelmt/aci_colors.rs b/src/qelmt/aci_colors.rs index 64c354b..95db9f5 100644 --- a/src/qelmt/aci_colors.rs +++ b/src/qelmt/aci_colors.rs @@ -1,5 +1,12 @@ +//! AutoCAD Color Index(ACI)到 RGB 的查找表。 +//! +//! 图元公共属性只有 ACI 索引时,通过本表生成 QET 样式中的 `rgb(r,g,b)`; +//! 超出当前表范围的索引回退为白色。 - +/// 当前运行时使用的 RGB 三元组表。 +/// +/// 注意:索引 7 的白色项目前被注释,因此这里是 255 项;维护脚本会生成完整 +/// 256 项表。调整颜色映射前必须先核对这两者,避免误以为当前数组与标准索引完全对齐。 pub const ACI_COLORS: [(u8,u8,u8); 255] = [ (0,0,0), (255,0,0), @@ -259,6 +266,7 @@ pub const ACI_COLORS: [(u8,u8,u8); 255] = [ (0,0,0), ]; +/// 将 ACI 索引转换为 RGB;无可用表项时返回白色。 pub fn aci_to_rgb(aci: u8) -> (u8,u8,u8) { if(aci >= ACI_COLORS.len() as u8) { return (255,255,255); diff --git a/src/qelmt/arc.rs b/src/qelmt/arc.rs index f5f9988..13f774c 100644 --- a/src/qelmt/arc.rs +++ b/src/qelmt/arc.rs @@ -1,3 +1,8 @@ +//! DXF 圆弧到 QElectroTech `` 图元的转换。 +//! +//! 支持原生 ARC,也支持由两个 LWPOLYLINE 顶点及起点 `bulge` 重建圆弧。 +//! DXF 与 QET 的 Y 轴方向相反,因此圆心、包围盒和起始角需要统一换算。 + use super::{two_dec, ScaleEntity}; use dxf::entities; use dxf::entities::LwPolyline; @@ -5,9 +10,11 @@ use hex_color::HexColor; use simple_xml_builder::XMLElement; #[derive(Debug)] +/// QET 圆弧:`x/y` 是外接矩形左上角,`start/angle` 使用角度制。 pub struct Arc { - //need to brush up on my Rust scoping rules, isn't there a way to make this pub to just the module? + /// 外接矩形左上角 X,供组合图元应用偏移。 pub x: f64, + /// 外接矩形左上角 Y,已从 DXF 坐标系翻转。 pub y: f64, width: f64, @@ -25,7 +32,7 @@ impl From<&entities::Arc> for Arc { } impl Arc { - /// 从DXF Arc实体创建Arc,支持自定义颜色 + /// 从 DXF ARC 创建 QET 圆弧,并处理法向量导致的镜像。 pub fn from_arc_with_color(arc: &entities::Arc, color: HexColor) -> Self { let is_mirror = arc.normal.z < 0.0; @@ -85,8 +92,7 @@ impl Arc { angle_span }, - //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? + // 与现有 ELMT 输出保持一致:基础图元默认关闭抗锯齿。 antialias: false, style: format!( "line-style:normal;filling:none;color:{}", @@ -98,6 +104,7 @@ impl Arc { impl From<&Arc> for XMLElement { fn from(arc: &Arc) -> Self { + // ELMT 几何坐标保留两位小数,角度按 QET 格式取整。 let mut arc_xml: XMLElement = XMLElement::new("arc"); arc_xml.add_attribute("x", two_dec(arc.x)); arc_xml.add_attribute("y", two_dec(arc.y)); @@ -112,6 +119,7 @@ impl From<&Arc> for XMLElement { } impl Arc { + /// 直接按 QET 外接矩形参数构造圆弧,主要供多段线拆分逻辑使用。 pub fn new( x: f64, y: f64, @@ -133,6 +141,7 @@ impl Arc { } } + /// 让上层在不暴露内部 `style` 字段的情况下追加线型属性。 pub fn update_line_style(&mut self, update_fn: F) where F: FnOnce(&mut String), @@ -140,10 +149,12 @@ impl Arc { update_fn(&mut self.style); } + /// 把毫米线宽归一化后写入 QET 样式串。 pub fn update_line_width_mm(&mut self, line_width_mm: f64) { super::update_style_line_width_mm(&mut self.style, line_width_mm); } + /// 围绕 QET 坐标系中的支点旋转圆弧中心和起始角。 pub(crate) fn rotate_around(&mut self, pivot_x: f64, pivot_y: f64, angle_deg: f64) { let center_x = self.x + self.width / 2.0; let center_y = self.y + self.height / 2.0; @@ -181,7 +192,6 @@ impl ScaleEntity for Arc { } } -// 在 mod.rs 中添加从 LwPolyline 转换为 Arc 的方法 impl TryFrom<&LwPolyline> for Arc { type Error = String; @@ -191,7 +201,9 @@ impl TryFrom<&LwPolyline> for Arc { } impl Arc { - /// 从DXF LwPolyline实体创建Arc,支持自定义颜色 + /// 从恰好两个顶点的 LWPOLYLINE 重建圆弧。 + /// + /// `bulge = tan(圆心角 / 4)`;零 bulge 或重合端点无法形成圆弧。 pub fn try_from_lwpolyline_with_color( lwpolyline: &LwPolyline, color: HexColor, @@ -271,6 +283,8 @@ impl Arc { #[cfg(test)] mod tests { + //! bulge 方向和 QET 正角度表达的回归测试。 + use super::Arc; use dxf::entities::LwPolyline; use dxf::LwPolylineVertex; diff --git a/src/qelmt/dynamictext.rs b/src/qelmt/dynamictext.rs index b1fdaf9..1f5497b 100644 --- a/src/qelmt/dynamictext.rs +++ b/src/qelmt/dynamictext.rs @@ -1,3 +1,9 @@ +//! TEXT、MTEXT 和属性定义到 QET `` 的统一转换。 +//! +//! 构建器先提取三种 DXF 文本共有的内容、字体、位置、旋转和对齐信息, +//! 再估算缺失的文本宽度。绘制位置与对齐锚点会一起参与缩放、平移和块旋转, +//! 以保证元件归一化到局部原点后文字仍与图形对齐。 + use super::{two_dec, FontInfo, ScaleEntity, TextEntity}; use dxf::entities::{self, AttributeDefinition}; use dxf::enums::AttachmentPoint; @@ -15,6 +21,7 @@ use uuid::Uuid; use super::{HAlignment, VAlignment}; #[derive(Debug)] +/// QET 动态文本对象;同时保存绘制锚点和对齐锚点。 pub struct DynamicText { text: String, info_name: Option, @@ -40,6 +47,7 @@ pub struct DynamicText { impl From<&DynamicText> for XMLElement { fn from(txt: &DynamicText) -> Self { + // QET 动态文本的位置约定来自元素编辑器的实际渲染结果。 let mut dtxt_xml = XMLElement::new("dynamic_text"); // taken from QET_ElementScaler: "ElmtDynText::AsSVGstring" // // Position und Rotationspunkt berechnen: @@ -140,11 +148,7 @@ impl From<&DynamicText> for XMLElement { dtxt_xml.add_attribute("align_x", two_dec(align_x)); dtxt_xml.add_attribute("align_y", two_dec(align_y)); - //If I ever add support for other text_from types, element and composite text - //I'll need to add more smarts here, as there may be some other children components - //for now since it only supports user_text I'm just statically adding the single child - //component needed - //match txt.text_from + // 当前只生成 UserText,因此子节点固定为一个 ``;复合文本需扩展这里。 let mut text_xml = XMLElement::new("text"); text_xml.add_text(&txt.text); dtxt_xml.add_child(text_xml); @@ -195,7 +199,7 @@ impl ScaleEntity for DynamicText { } impl DynamicText { - // 文本的绘制位置和对齐点必须同步移动,否则归一化后文字仍会偏离图形。 + /// 同步移动绘制位置和对齐点,否则归一化后文字会偏离图形。 pub(crate) fn translate(&mut self, offset_x: f64, offset_y: f64) { self.x += offset_x; self.y += offset_y; @@ -203,6 +207,7 @@ impl DynamicText { self.align_y += offset_y; } + /// 围绕块插入点旋转两个锚点,并叠加文本旋转角。 pub(crate) fn rotate_around(&mut self, pivot_x: f64, pivot_y: f64, angle_deg: f64) { let (x, y) = super::rotate_point_around(self.x, self.y, pivot_x, pivot_y, angle_deg); let (align_x, align_y) = @@ -215,6 +220,7 @@ impl DynamicText { self.rotation = super::normalize_angle(self.rotation + angle_deg); } + /// 根据水平对齐方式把锚点和估算宽度换算为左右边界。 fn horizontal_bounds(&self) -> (f64, f64) { // 普通 TEXT 没有参考矩形宽度,使用文本内容估算宽度以参与整体包围盒计算。 let width = if self.reference_rectangle_width > 0.0 { @@ -231,6 +237,7 @@ impl DynamicText { } } + /// 根据垂直对齐方式把锚点和文字高度换算为上下边界。 fn vertical_bounds(&self) -> (f64, f64) { let height = self.text_height.abs(); @@ -242,12 +249,14 @@ impl DynamicText { } } +/// 把 TEXT、MTEXT、ATTDEF 三种来源收敛为同一套动态文本构造流程。 pub struct DTextBuilder<'a> { text: TextEntity<'a>, color: Option, } impl<'a> DTextBuilder<'a> { + /// 从单行 DXF TEXT 开始构建。 pub fn from_text(text: &'a entities::Text) -> Self { Self { text: TextEntity::Text(text), @@ -255,6 +264,7 @@ impl<'a> DTextBuilder<'a> { } } + /// 从多行 DXF MTEXT 开始构建。 pub fn from_mtext(text: &'a entities::MText) -> Self { Self { text: TextEntity::MText(text), @@ -262,6 +272,7 @@ impl<'a> DTextBuilder<'a> { } } + /// 从块属性定义 ATTDEF 开始构建。 pub fn from_attrib(attrib: &'a AttributeDefinition) -> Self { Self { text: TextEntity::Attrib(attrib), @@ -269,6 +280,7 @@ impl<'a> DTextBuilder<'a> { } } + /// 覆盖由 DXF 实体公共属性解析出的文本颜色。 pub fn color(self, color: HexColor) -> Self { Self { color: Some(color), @@ -276,6 +288,7 @@ impl<'a> DTextBuilder<'a> { } } + /// 提取来源字段、清理控制序列并生成最终动态文本。 pub fn build(self) -> DynamicText { let ( x, @@ -384,8 +397,7 @@ impl<'a> DTextBuilder<'a> { } }; - // Create a FontContext (font database) and LayoutContext (scratch space). - // These are both intended to be constructed rarely (perhaps even once per app): + // 以下保留的实验代码尝试用 parley 做真实字形布局;当前未参与转换。 /*let mut font_cx = FontContext::new(); let mut layout_cx = LayoutContext::new(); @@ -415,8 +427,7 @@ impl<'a> DTextBuilder<'a> { dbg!(&y); dbg!(&self.text);*/ let value = super::strip_mtext_control_sequences(&value); - // 将 DXF 中读取到的 text_height 放大 2 倍(用户要求) - // let text_height = text_height * 2.0; + // 当前直接沿用 DXF text_height,不再额外放大。 let text_height = text_height; let text_width = if reference_rectangle_width > 0.0 { reference_rectangle_width @@ -464,14 +475,14 @@ impl<'a> DTextBuilder<'a> { } } -/// 计算MText的实际旋转角度 +/// 计算 MTEXT 的实际旋转角度。 /// /// # Arguments /// * `rotation_angle` - DXF中的rotation_angle字段(编号50) /// * `x_axis_direction` - DXF中的x_axis_direction向量(编号11,21,31) /// /// # Returns -/// 返回实际的旋转角度(弧度) +/// 返回 QET 使用的角度制数值。 fn calculate_mtext_rotation(rotation_angle: f64, x_axis_direction: &dxf::Vector) -> f64 { // 检查x_axis_direction是否为默认值(1.0, 0.0, 0.0) let default_x_axis = (x_axis_direction.x - 1.0).abs() < 1e-10 @@ -552,6 +563,7 @@ pub fn adjust_mtext_coordinates( (adjusted_x, adjusted_y) } +/// 在没有参考矩形时,以最长一行的字素数估算文本宽度。 fn estimate_text_width(text: &str, line_height: f64, relative_x_scale_factor: f64) -> f64 { let effective_height = line_height.abs(); if effective_height == 0.0 { diff --git a/src/qelmt/ellipse.rs b/src/qelmt/ellipse.rs index 1f660b8..32de09a 100644 --- a/src/qelmt/ellipse.rs +++ b/src/qelmt/ellipse.rs @@ -1,17 +1,24 @@ +//! 圆和椭圆图元转换。 +//! +//! DXF CIRCLE、ELLIPSE,以及被圆度检测识别为圆的 POLYLINE/LWPOLYLINE, +//! 都统一输出为 QET ``。位置字段采用外接矩形左上角坐标。 + use super::{two_dec, Circularity, ScaleEntity}; use dxf::entities::{self, Circle, LwPolyline, Polyline}; use hex_color::HexColor; use simple_xml_builder::XMLElement; #[derive(Debug)] +/// QET 椭圆及其外接矩形、颜色和样式。 pub struct Ellipse { height: f64, width: f64, style: String, color: HexColor, - //need to brush up on my Rust scoping rules, isn't there a way to make this pub to just the module? + /// 外接矩形左上角 X,供上层块变换使用。 pub x: f64, + /// 外接矩形左上角 Y,已转换为 QET 坐标系。 pub y: f64, antialias: bool, @@ -26,8 +33,7 @@ impl From<&Circle> for Ellipse { width: circ.radius * 2.0, color: HexColor::BLACK, - //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? + // 保持项目现有 ELMT 输出约定,基础图元默认关闭抗锯齿。 antialias: false, style: "line-style:normal;filling:none;color:black".into(), } @@ -43,8 +49,7 @@ impl From<&entities::Ellipse> for Ellipse { height: ellipse.major_axis.x * 2.0 * ellipse.minor_axis_ratio, color: HexColor::BLACK, - //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? + // 保持项目现有 ELMT 输出约定,基础图元默认关闭抗锯齿。 antialias: false, style: "line-style:normal;filling:none;color:black".into(), } @@ -59,12 +64,7 @@ impl TryFrom<&Polyline> for Ellipse { return Err("Polyline has poor circularity, can't convert"); } - //I did this fold because min requires the vertex to have the Ordering trait - //but I forogot min_by exists taking a lambda, so I could compare them using - //the value I need. However my first quick attemp wasn't working - //Using min_by would probably be more effecietn than the fold - //So this is probably worth coming back to...but it's a low priority - //because the below code works. + // 浮点数没有全序,这里用 fold 分别求顶点极值并构造外接矩形。 let x = poly .vertices() .fold(f64::MAX, |min_x, vtx| min_x.min(vtx.location.x)); @@ -88,8 +88,7 @@ impl TryFrom<&Polyline> for Ellipse { width: max_x - x, color: HexColor::BLACK, - //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? + // 保持项目现有 ELMT 输出约定,基础图元默认关闭抗锯齿。 antialias: false, style: "line-style:normal;filling:none;color:black".into(), }) @@ -105,6 +104,7 @@ impl TryFrom<&LwPolyline> for Ellipse { } impl Ellipse { + /// 将圆形 LWPOLYLINE 转为椭圆;不满足圆度判定时返回错误供上层降级处理。 pub fn try_from_lwpolyline_with_color( poly: &LwPolyline, color: HexColor, @@ -155,8 +155,7 @@ impl Ellipse { width: max_x - x, color, - //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? + // 保持项目现有 ELMT 输出约定,基础图元默认关闭抗锯齿。 antialias: false, style, }) @@ -165,6 +164,7 @@ impl Ellipse { impl From<&Ellipse> for XMLElement { fn from(ell: &Ellipse) -> Self { + // style 保存完整画笔描述,color 属性用于兼容 QET 对椭圆颜色的读取。 let mut ell_xml: XMLElement = XMLElement::new("ellipse"); ell_xml.add_attribute("x", two_dec(ell.x)); ell_xml.add_attribute("y", two_dec(ell.y)); @@ -178,10 +178,12 @@ impl From<&Ellipse> for XMLElement { } impl Ellipse { + /// 整体替换 QET 样式串。 pub fn set_style(&mut self, style: String) { self.style = style; } + /// 由上层闭包追加或替换虚线等样式属性。 pub fn update_line_style(&mut self, update_fn: F) where F: FnOnce(&mut String), @@ -189,6 +191,7 @@ 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); } @@ -201,6 +204,7 @@ impl Ellipse { self.color } + /// 围绕插入点旋转椭圆中心;椭圆自身轴向暂不单独记录旋转角。 pub(crate) fn rotate_around(&mut self, pivot_x: f64, pivot_y: f64, angle_deg: f64) { let center_x = self.x + self.width / 2.0; let center_y = self.y + self.height / 2.0; diff --git a/src/qelmt/line.rs b/src/qelmt/line.rs index 473eae1..6f4a042 100644 --- a/src/qelmt/line.rs +++ b/src/qelmt/line.rs @@ -1,3 +1,8 @@ +//! 直线和引线转换。 +//! +//! DXF LINE、两个顶点且无圆弧 bulge 的多段线会生成单个 QET ``; +//! LEADER 则按相邻顶点拆成多条线,首段可携带简单箭头。 + use super::two_dec; use super::LineEnd; use super::ScaleEntity; @@ -6,12 +11,12 @@ use hex_color::HexColor; use simple_xml_builder::XMLElement; #[derive(Debug)] +/// QET 线段。坐标公开给块展开和统一平移逻辑使用。 pub struct Line { length2: f64, end2: LineEnd, length1: f64, - //need to brush up on my Rust scoping rules, isn't there a way to make this pub to just the module? pub x1: f64, pub y1: f64, pub x2: f64, @@ -22,6 +27,7 @@ pub struct Line { antialias: bool, } +/// DXF LEADER 展开得到的连续 QET 线段集合。 pub struct Leader(pub Vec); impl From<&entities::Line> for Line { @@ -41,15 +47,15 @@ impl TryFrom<&Polyline> for Line { Ok(Line { x1: poly.__vertices_and_handles[0].0.location.x, y1: -poly.__vertices_and_handles[0].0.location.y, - length1: 1.5, //why is this statically set at 1.5? + // QET 箭头尺寸目前沿用历史默认值;普通线端样式为 None,不会显示箭头。 + length1: 1.5, end1: LineEnd::None, x2: poly.__vertices_and_handles[1].0.location.x, y2: -poly.__vertices_and_handles[1].0.location.y, - length2: 1.5, //why is this statically set at 1.5? + length2: 1.5, end2: LineEnd::None, - //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? + // 与现有 ELMT 输出保持一致,基础图元默认关闭抗锯齿。 antialias: false, style: "line-style:normal;filling:none;color:black".into(), }) @@ -65,7 +71,7 @@ impl TryFrom<&LwPolyline> for Line { } impl Line { - /// 从DXF LwPolyline实体创建Line,支持自定义颜色 + /// 将恰好两个顶点的 LWPOLYLINE 转为直线;bulge 圆弧由上层优先处理。 pub fn try_from_lwpolyline_with_color(poly: &LwPolyline, color: HexColor) -> Result { if poly.vertices.len() != 2 { return Err("Error can't convert polyline with more than 2 points into a Line"); @@ -74,15 +80,14 @@ impl Line { Ok(Line { x1: poly.vertices[0].x, y1: -poly.vertices[0].y, - length1: 1.5, //why is this statically set at 1.5? + length1: 1.5, end1: LineEnd::None, x2: poly.vertices[1].x, y2: -poly.vertices[1].y, - length2: 1.5, //why is this statically set at 1.5? + length2: 1.5, end2: LineEnd::None, - //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? + // 与现有 ELMT 输出保持一致,基础图元默认关闭抗锯齿。 antialias: false, style: format!( "line-style:normal;filling:none;color:{}", @@ -96,6 +101,7 @@ impl Line { impl From<&entities::Leader> for Leader { fn from(leader: &entities::Leader) -> Self { + // windows(2) 把顶点链转换为相邻线段,避免重复维护当前/下一顶点索引。 Leader( leader .vertices @@ -111,15 +117,15 @@ impl From<&entities::Leader> for Leader { Line { x1: pt_slice[0].x, y1: -pt_slice[0].y, - length1: 1.5, //In order to get the arrow sizing, I need to read in the dimension styling first + // 尚未解析 DIMSTYLE,因此箭头长度使用固定的历史默认值。 + length1: 1.5, end1, x2: pt_slice[1].x, y2: -pt_slice[1].y, - length2: 1.5, //In order to get the arrow sizing, I need to read in the dimension styling first + length2: 1.5, end2: LineEnd::None, - //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? + // 与现有 ELMT 输出保持一致,基础图元默认关闭抗锯齿。 antialias: false, style: "line-style:normal;filling:none;color:black".into(), } @@ -147,20 +153,19 @@ impl From<&Line> for XMLElement { } impl Line { - /// 从DXF Line实体创建Line,支持自定义颜色 + /// 从原生 DXF LINE 创建 QET 线段,并完成 Y 轴翻转。 pub fn from_line_with_color(line: &entities::Line, color: HexColor) -> Self { Line { x1: line.p1.x, y1: -line.p1.y, - length1: 1.5, //why is this statically set at 1.5? + length1: 1.5, end1: LineEnd::None, x2: line.p2.x, y2: -line.p2.y, - length2: 1.5, //why is this statically set at 1.5? + length2: 1.5, end2: LineEnd::None, - //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? + // 与现有 ELMT 输出保持一致,基础图元默认关闭抗锯齿。 antialias: false, style: format!( "line-style:normal;filling:none;color:{}", @@ -169,6 +174,7 @@ impl Line { } } + /// 直接按 QET 坐标构造无箭头线段,供多段线拆分使用。 pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, style: String) -> Self { Line { length2: 1.5, @@ -184,6 +190,7 @@ impl Line { } } + /// 让上层在不暴露内部样式字段的情况下修改线型。 pub fn update_line_style(&mut self, update_fn: F) where F: FnOnce(&mut String), @@ -205,13 +212,7 @@ impl ScaleEntity for Line { self.y1 *= fact_y; self.y2 *= fact_y; - //while writing this scaling code, I'm looking at - //QET_ElementScaler from plc-user to see if there are - //any easy to overlook mistakes that I might make - //doing the scaling. It seems they limit these lengths - //to 99.0, but I'm not sure why at the moment. I'll go - //ahead and limit them as well, and try to come back to - //figure out what the purpose here is + // 箭头长度按较小缩放因子等比缩放,并遵循 QET_ElementScaler 的 99 上限。 self.length1 *= fact_x.min(fact_y); self.length1 = self.length1.min(99.0); diff --git a/src/qelmt/mod.rs b/src/qelmt/mod.rs index cc745cc..3bad56b 100644 --- a/src/qelmt/mod.rs +++ b/src/qelmt/mod.rs @@ -1,3 +1,13 @@ +//! DXF 到 QElectroTech ELMT 的核心对象模型与转换流程。 +//! +//! 本模块先把 `dxf::Drawing` 转成统一的 [`Objects`] 图元树,再执行单位缩放、 +//! 块插入变换、包围盒计算和局部坐标居中,最后序列化为 `` XML。 +//! 各基础图元的几何细节分布在 `arc`、`line`、`ellipse`、`polygon` 和 +//! `dynamictext` 子模块中。 +//! +//! 坐标约定:DXF 的 Y 轴向上,QET 的 Y 轴向下,因此进入内部 QET 对象时 +//! 通常会对 Y 坐标取反。块旋转也要相应反号,不能直接照搬 DXF 角度。 + use dxf::entities::{AttributeDefinition, Entity, EntityType, Insert}; use dxf::entities::{LwPolyline, Polyline}; use dxf::enums::{AttachmentPoint, HorizontalTextJustification, Units, VerticalTextJustification}; @@ -35,22 +45,19 @@ pub use polygon::Polygon; pub mod ellipse; pub use ellipse::Ellipse; -// 在 DXF 图纸中根据名称查找特定的块(Block) +/// 按名称查找第一个 DXF 块定义。 +/// +/// INSERT 只携带块名;如果源文件存在重复块名,当前行为以迭代顺序中的第一个为准。 fn find_block<'a>(drw: &'a Drawing, name: &str) -> Option<&'a Block> { - //this is ugly there has to be a cleaner way to filter this....but for my first attempt at pulling the - //blocks out of the drawing it works. - //I mean would this ever return more than 1? I would assume block names have to be unique? - //but maybe not, the blocks have a handle, which is a u64. There is a get by handle function - //but not a get by name function....maybe the handle is what is unique and there can be duplicate names? - //a quick glance through the dxf code it looks like the handle might be given to the library user when inserting - //an entity? So I don't think there is any easy way to get the handle drw.blocks().filter(|bl| bl.name == name).take(1).next() } +/// 生成 QET 要求的带花括号 UUID 文本。 fn qet_uuid_string() -> String { format!("{{{}}}", Uuid::new_v4()) } +/// 收集模型空间直接或间接引用的块,供 `` 定义区输出。 fn referenced_blocks<'a>(drw: &'a Drawing) -> Vec<&'a Block> { let blocks: Vec<&Block> = drw.blocks().collect(); let mut names = HashSet::new(); @@ -64,6 +71,7 @@ fn referenced_blocks<'a>(drw: &'a Drawing) -> Vec<&'a Block> { .collect() } +/// 递归跟踪 INSERT 引用;`visiting` 用来阻止循环块引用造成无限递归。 fn collect_insert_names<'a, I>( entities: I, blocks: &[&'a Block], @@ -91,18 +99,20 @@ fn collect_insert_names<'a, I>( } } -// 枚举作用 :表示两种可能的类型之一 -// Definition结构体,表示 ELMT 文件的定义,包含元素的尺寸、热点、版本、UUID 等信息 #[allow(dead_code)] #[derive(Debug)] +/// 图元序列化结果可能是一个 XML 节点,也可能是展开后的多个节点。 enum Either { Left(L), Right(R), } #[derive(Debug)] +/// 一个完整的 QET ``,包含元件元数据、图形描述和被引用块定义。 pub struct Definition { + /// QET 项目类型;本工具只生成普通 element。 r#type: ItemType, + /// 元件画布宽高和热点均由最终图元包围盒计算。 width: i64, height: i64, hotspot_x: i64, @@ -113,12 +123,14 @@ pub struct Definition { names: Names, element_infos: Option, informations: &'static str, + /// 主图形,序列化为 ``。 description: Description, + /// 源图中实际被 INSERT 引用的块定义。 blocks: Vec, - //counts } #[derive(Debug)] +/// 写入 `` 的单个块定义及其源块基点偏移。 struct QetBlockDefinition { name: String, base_dx: f64, @@ -127,24 +139,22 @@ struct QetBlockDefinition { } #[derive(Debug)] +/// 已展开的 INSERT;子图元在序列化时继承同一块实例元数据。 pub(crate) struct BlockInstance { metadata: BlockMetadata, objects: Vec, } #[derive(Debug)] +/// QET 用于重新识别同一块实例各组成图元的名称和实例 UUID。 pub(crate) struct BlockMetadata { name: String, instance_id: String, } -//Since the ScaleEntity trait was added to all the objects/elements -//and I need to add the get bounds to all it probably makes sense to have - -//them all within the same trait instead of multiple traits, as a collective -//set of functions needed by the objects...but I should probably come up with -//a better trait name then. For now I'll leave it and just get the code working -// 特征定义,定义了一组方法,用于缩放实体、获取实体的边界和判断实体是否为圆形 +/// 所有内部图元都必须提供缩放能力和 QET 坐标系下的包围盒。 +/// +/// 包围盒用于确定元件画布、热点和把源 CAD 绝对坐标归一化为局部坐标。 trait ScaleEntity { fn scale(&mut self, fact_x: f64, fact_y: f64); @@ -155,6 +165,7 @@ trait ScaleEntity { fn bot_bound(&self) -> f64; } +/// 把任意角度归一化到 `[0, 360)`。 pub(crate) fn normalize_angle(angle: f64) -> f64 { let normalized = angle % 360.0; if normalized < 0.0 { @@ -164,6 +175,7 @@ pub(crate) fn normalize_angle(angle: f64) -> f64 { } } +/// 在 QET 二维坐标系中绕指定支点旋转一个点。 pub(crate) fn rotate_point_around( x: f64, y: f64, @@ -183,10 +195,12 @@ pub(crate) fn rotate_point_around( ) } +/// DXF 与 QET 的 Y 轴方向相反,因此 INSERT 旋转角需要取反。 fn qet_insert_rotation(rotation: f64) -> f64 { -rotation } +/// 读取 INSERT 中名为 `connector` 的非空属性值,忽略标签大小写和两端空白。 fn connector_attribute(insert: &Insert) -> Option<&str> { insert .attributes() @@ -200,31 +214,29 @@ fn connector_attribute(insert: &Insert) -> Option<&str> { .filter(|value| !value.is_empty()) } +/// 判断 INSERT 是否符合当前唯一受支持的端子表示法。 +/// +/// 只有块名为 `ARROWUNKNOWN` 且携带 `connector` 属性时才会生成 QET +/// ``。其他隐藏几何、XDATA 或文字标签不会自动变成可接线端子。 fn is_arrowunknown_terminal(insert: &Insert) -> bool { insert.name.trim().eq_ignore_ascii_case("ARROWUNKNOWN") && connector_attribute(insert).is_some() } -// 用于判断多边形是否接近圆形的特征,有点类似函数声明 +/// 用周长与面积的圆度指标识别可安全导出为椭圆的多段线。 trait Circularity { - // 判断图形是否为圆形 + /// 判断直线顶点组成的轮廓是否近似圆形。 fn is_circular(&self) -> bool; - // 返回圆形判断的容差范围 (0.98..=1.02),即允许2%的误差 + /// 圆度 `4πA/P²` 的容差范围;理想圆等于 1。 fn match_range() -> std::ops::RangeInclusive { - //this boundary of 2% has been chosen arbitrarily, I might adjust this later - //I know in one of my sample files, I'm getting a value of 0.99.... - //since Associated Constants in a trait can't have a default value - //I'm using this function that defaults to a constant range of 2% - //Then I could also easily overwrite it if I wanted to change the tolerance - //for a specific type + // 2% 是针对现有样图的经验容差,避免轻微离散误差阻止圆形识别。 0.98..=1.02 } - // 考虑bugle圆形判断 + /// 判断是否由 bulge 圆弧段组成近似圆。 fn is_circular_with_bulge(&self) -> bool; } -// 为 Polyline 类型实现圆形检测,有点类似函数定义 impl Circularity for Polyline { fn is_circular(&self) -> bool { let poly_perim: f64 = { @@ -239,11 +251,7 @@ impl Circularity for Polyline { }; let poly_area = { - //because instead of being able to access the Vec like in LwPolyline, vertices() returns - //an iter of dxf Vertex's which don't implement clone so I can't use circular_tuple_windows - //there is probably a cleaner way of iterating over this, but it's late, I'm getting tired - //and just want to see if this basic idea will work on my sample file, or see if I'm chasing - //up the wrong tree. + // Polyline 只暴露顶点迭代器,先复制位置才能首尾相接计算鞋带公式面积。 let tmp_pts: Vec = self.vertices().map(|v| v.clone().location).collect(); let len = tmp_pts.len(); let mut poly_area: f64 = tmp_pts @@ -265,7 +273,6 @@ impl Circularity for Polyline { } } -// 为 LwPolyline 类型实现相同的圆形检测逻辑 impl Circularity for LwPolyline { fn is_circular(&self) -> bool { let poly_perim: f64 = self @@ -297,7 +304,7 @@ impl Circularity for LwPolyline { } fn is_circular_with_bulge(&self) -> bool { - // 检查是否有显著的bulge值 + // 当前仅识别所有有效顶点都近似半圆 bulge(|bulge|≈1)的闭合轮廓。 let has_bulge = self.vertices.iter().any(|v| v.bulge.abs() > 0.5); if !has_bulge { return false; @@ -321,17 +328,17 @@ impl Circularity for LwPolyline { } } -// 实现Definition的方法 -// 最后返回文本的definition标签 impl Definition { - // 创建新的 Definition 实例,对应elmt的标签 传入参数:文件名, 步长, dxf图纸 + /// 从整张 DXF 构造 QET ``。 + /// + /// `spline_step` 控制样条离散精度;`name` 写入元件英文名称。构建过程还会 + /// 输出被引用的块定义,但只把模型空间图元放进主 ``。 pub fn new(name: impl Into, spline_step: u32, drw: &Drawing) -> Self { /*for st in drw.styles() { dbg!(st); }*/ - // 根据制图系统的类型缩放处理 + // 先按 DXF 头部单位统一缩放,再用最终包围盒移动到元件局部原点。 let scale_factor = Self::scale_factor(drw.header.default_drawing_units); - // 创建description,对应elmt的标签,里面就是实际的画图元素标签 let description = { let mut description: Description = (drw, spline_step).into(); description.scale(scale_factor, scale_factor); @@ -340,6 +347,7 @@ impl Definition { description }; + // `` 只保留从模型空间可达的块,避免把未使用的块库全部写入 ELMT。 let available_blocks: Vec<&Block> = drw.blocks().collect(); let block_definitions = referenced_blocks(drw) .into_iter() @@ -365,8 +373,7 @@ impl Definition { }) .collect(); - // 计算图纸的宽度,x热点坐标 - //The below calculation for width and hotspot_x are taken from the qet source code + // 画布宽度和热点算法与 QET 源码一致:向 10 的网格上取整并留出边距。 let (width, hotspot_x) = { let tmp_width = description.right_bound() - description.left_bound(); let int_width = tmp_width.round() as i64; @@ -385,8 +392,7 @@ impl Definition { ) }; - // 计算图纸的高度,y热点坐标 - //The below calculation for height and hotspot_y are taken from the qet source code + // 高度使用同一套网格和边距规则。 let (height, hotspot_y) = { let tmp_height = description.bot_bound() - description.top_bound(); let int_height = tmp_height.round() as i64; @@ -405,7 +411,7 @@ impl Definition { ) }; - // Definition结构体初始化 :设置所有必要字段,包括类型、版本、UUID等 + // ELMT 版本和 link_type 当前固定为普通元件所需值。 Definition { r#type: ItemType::Element, width, @@ -418,7 +424,7 @@ impl Definition { names: Names { names: vec![Name { lang: "en".into(), - value: name.into(), //need to truncate the extension + value: name.into(), }], }, element_infos: None, @@ -428,6 +434,7 @@ impl Definition { } } + /// 把一个 DXF 块包装成可写入 `` 的嵌套 definition。 fn qet_block_definition( name: String, base_dx: f64, @@ -463,6 +470,7 @@ impl Definition { } } + /// 根据图元包围盒计算 QET 画布宽高与热点。 fn geometry_attrs(description: &Description) -> (i64, i64, i64, i64) { let (width, hotspot_x) = { let tmp_width = description.right_bound() - description.left_bound(); @@ -503,14 +511,11 @@ impl Definition { (width, height, hotspot_x, hotspot_y) } - // 根据不同的测量单位返回相应的缩放因子 + /// 把 DXF 头部单位换算为当前 QET 画布使用的坐标比例。 fn scale_factor(unit: Units) -> f64 { - //so per discussion at https://qelectrotech.org/forum/viewtopic.php?pid=20685#p20685 - //we are in agreement to scale things to 1mm = 2px; - //all the below values are the converted equivalent of 2px per 1mm in the designated unit - //unit conversions taken from: https://www.unitconverters.net/length-converter.html + // 历史实现约定 1 mm 对应 QET 中的目标像素比例;无单位图纸暂不缩放。 match unit { - Units::Unitless => 1.0, //for now if the drawing is untiless don't scale it + Units::Unitless => 1.0, Units::Inches => 50.8, Units::Feet => 609.6, Units::Miles | Units::USSurveyMile => 3_218_694.437_4, @@ -534,20 +539,15 @@ impl Definition { Units::USSurveyFeet => 609.601_219_2, Units::USSurveyInch => 50.800_101_6, - //I'm finding very little references to US Survey yard at all. The only real - //link I could find was on the Wikipedia page for the Yard, which stated: - //"The US survey yard is very slightly longer." and linked to the US Survey Foot page - //I'll assume for now that 1 US Survey Yard is equal to 3 US Survey Feet. Which seems - //like a reasonable assumption, and would result in something slightly larger than a yard + // 美制测量码按 3 个美制测量英尺换算。 Units::USSurveyYard => 1_828.803_657_6, } } } -// 将 Definition 结构体转换为 XML 元素 impl From<&Definition> for XMLElement { - // 创建 根元素 fn from(def: &Definition) -> Self { + // 子节点顺序遵循 QET 元件格式:UUID、名称、信息、图形、块定义。 let mut def_xml = XMLElement::new("definition"); def_xml.add_attribute("height", def.height); def_xml.add_attribute("width", def.width); @@ -581,7 +581,6 @@ impl From<&Definition> for XMLElement { } } -// 枚举类型,表示各种图形对象(弧线、椭圆、多边形、文本、线条等) impl From<&QetBlockDefinition> for XMLElement { fn from(block: &QetBlockDefinition) -> Self { let mut block_xml = XMLElement::new("block"); @@ -594,6 +593,7 @@ impl From<&QetBlockDefinition> for XMLElement { } #[derive(Debug)] +/// 所有可输出图元的统一枚举;Group 和 BlockInstance 允许递归组合。 pub(crate) enum Objects { Arc(Arc), Ellipse(Ellipse), @@ -607,9 +607,7 @@ pub(crate) enum Objects { } impl Objects { - // 返回一个 Descendants 迭代器,用于遍历对象的所有后代节 - // 返回一个 Children 迭代器,用于遍历对象的直接子节点 - // 对所有图元递归应用同一平移量,保持块内部图形的相对位置不变。 + /// 对图元树递归应用同一平移量,保持块内部相对位置不变。 fn translate(&mut self, offset_x: f64, offset_y: f64) { match self { Objects::Arc(arc) => { @@ -675,6 +673,7 @@ impl Objects { } } + /// 围绕同一支点递归旋转图元树,并同步更新圆弧角度、文字角度和端子方向。 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), @@ -720,6 +719,7 @@ impl Objects { } } + /// 清除块定义内部的实例元数据,避免 `` 模板被误标为实际实例。 fn clear_block_instance_metadata(&mut self) { match self { Objects::Group(objects) => { @@ -797,6 +797,7 @@ fn decompose_lwpolyline_segments( 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; @@ -807,11 +808,7 @@ fn rotate_objects_around(objects: &mut [Objects], pivot_x: f64, pivot_y: f64, an } } -// 结构体,包含一个 stack 字段,存储 Children 迭代器的栈,使用生命周期参数 'a 确保引用的有效性 -// Descendants迭代器实现,深度优先遍历 :使用栈结构实现树形结构的深度优先搜索 -// Children结构体,提供对直接子节点的简单迭代访 -// Children迭代器实现 -// 实现Objects的ScaleEntity特征 +/// 递归组合图元的最左边界;空组返回 0。 fn group_left_bound(objects: &[Objects]) -> f64 { objects .iter() @@ -824,6 +821,7 @@ fn group_left_bound(objects: &[Objects]) -> f64 { .unwrap_or(0.0) } +/// 递归组合图元的最右边界;空组返回 0。 fn group_right_bound(objects: &[Objects]) -> f64 { objects .iter() @@ -836,6 +834,7 @@ fn group_right_bound(objects: &[Objects]) -> f64 { .unwrap_or(0.0) } +/// 递归组合图元的顶部边界;QET 坐标中较小的 Y 位于上方。 fn group_top_bound(objects: &[Objects]) -> f64 { objects .iter() @@ -848,6 +847,7 @@ fn group_top_bound(objects: &[Objects]) -> f64 { .unwrap_or(0.0) } +/// 递归组合图元的底部边界;QET 坐标中较大的 Y 位于下方。 fn group_bot_bound(objects: &[Objects]) -> f64 { objects .iter() @@ -862,7 +862,7 @@ fn group_bot_bound(objects: &[Objects]) -> f64 { impl ScaleEntity for Objects { fn scale(&mut self, fact_x: f64, fact_y: f64) { - // match 是 Rust 的模式匹配,类似于 switch 语句但更强大 + // Group/BlockInstance 必须递归缩放,不能只修改容器本身。 match self { Objects::Arc(arc) => arc.scale(fact_x, fact_y), Objects::Ellipse(ellipse) => ellipse.scale(fact_x, fact_y), @@ -987,6 +987,7 @@ impl ScaleEntity for Objects { } #[derive(Debug)] +/// 当前实体及其嵌套块共同使用的二维缩放因子。 struct ScaleFactor { x: f64, y: f64, @@ -999,15 +1000,18 @@ impl Default for ScaleFactor { } #[derive(Default, Debug)] +/// 当前实体相对于父块或模型空间的 DXF 坐标偏移。 pub(crate) struct Offset { x: f64, y: f64, } -// 构建器结构体 #[derive(Debug)] +/// 把一个 `dxf::Entity` 转换为内部 [`Objects`] 的上下文构建器。 +/// +/// 构建器携带样条精度、可递归查找的块表、父级偏移和累计缩放。它是所有实体 +/// 类型分派的中心,无法转换或被过滤的实体通过 `Err` 交给上层忽略。 pub struct ObjectsBuilder<'a> { - // &'a Entity 是对 Entity 的引用 ent: &'a Entity, spline_step: u32, blocks: &'a [&'a Block], @@ -1016,7 +1020,7 @@ pub struct ObjectsBuilder<'a> { } impl<'a> ObjectsBuilder<'a> { - // 创建新的实例 + /// 为单个实体创建默认无偏移、单位缩放的构建器。 pub fn new(ent: &'a Entity, spline_step: u32) -> Self { Self { ent, @@ -1027,12 +1031,12 @@ impl<'a> ObjectsBuilder<'a> { } } - // 设置可用的块引用列表 + /// 提供 INSERT 递归展开时可查找的全部块。 pub fn blocks(self, blocks: &'a [&'a Block]) -> Self { Self { blocks, ..self } } - // 设置 X、Y 坐标偏移 + /// 设置来自父块插入位置和块基点的坐标偏移。 pub fn offsets(self, x: f64, y: f64) -> Self { Self { offset: Offset { x, y }, @@ -1040,7 +1044,7 @@ impl<'a> ObjectsBuilder<'a> { } } - // 设置 X、Y 缩放因子 + /// 设置父级 INSERT 累积的 X/Y 缩放。 pub fn scaling(self, fact_x: f64, fact_y: f64) -> Self { Self { scale_fact: ScaleFactor { @@ -1052,6 +1056,7 @@ impl<'a> ObjectsBuilder<'a> { } #[allow(clippy::too_many_lines)] + /// 按实体类型生成内部图元,并统一应用可见性、辅助图层、线型和线宽规则。 pub fn build(self) -> Result { // 检查entity的is_visible属性,如果为false则不进行转换 if !self.ent.common.is_visible { @@ -1070,13 +1075,13 @@ impl<'a> ObjectsBuilder<'a> { return Err("Skip orientation helper layer"); } - // 获取线型名称 + // 线型名和公共 lineweight 对所有可描边实体生效。 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 + // 把已知 DXF 虚线名称映射为 QET `line-style:dashed`。 let update_line_style = |style: &mut String| { println!("line_type_name:{line_type_name}"); // 只有当line_type_name需要虚线样式时才进行修改,提高性能 @@ -1086,7 +1091,7 @@ impl<'a> ObjectsBuilder<'a> { || line_type_name == "INTERROMPUx2" || line_type_name == "TRACECAD_ISO02W100" { - // 使用正则表达式替换line-style部分 + // 样式是分号分隔字符串,直接替换既有 line-style 项。 if style.contains("line-style:") { *style = style.replace( &format!( @@ -1583,9 +1588,7 @@ impl<'a> ObjectsBuilder<'a> { } } -// 将 Objects 枚举转换为 Either> 类型 -// Either::Left(XMLElement) :单个 XML 元素 -// Either::Right(Vec) :XML 元素向量 +/// 把基础图元转换为单个 XML 节点,把组合图元递归展开为节点列表。 impl From<&Objects> for Either> { fn from(obj: &Objects) -> Self { match obj { @@ -1597,7 +1600,7 @@ impl From<&Objects> for Either> { 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()收集转换成功元素 + // Group 本身没有 ELMT 标签,只输出其中可序列化的叶子图元。 block .iter() //.flatten() @@ -1615,7 +1618,7 @@ impl From<&Objects> for Either> { } } -// 实现try_from尝试转换 +/// 只接受能够直接对应单个 XML 标签的叶子图元。 impl TryFrom<&Objects> for XMLElement { type Error = &'static str; // add better error later @@ -1634,7 +1637,9 @@ impl TryFrom<&Objects> for XMLElement { } } -// 包含所有图形对象的描述 +/// 递归展开图元,并把最外层有效块实例元数据附加到每个叶子节点。 +/// +/// 嵌套块沿用外层实例信息,使 QET 能把最终展开的所有组成部分识别为一个块实例。 fn xml_elements_for_object( obj: &Objects, block_metadata: Option<&BlockMetadata>, @@ -1676,11 +1681,13 @@ fn xml_elements_for_object( } #[derive(Debug)] +/// QET `` 中的全部可见图元。 pub struct Description { objects: Vec, } impl Description { + /// 把一个块定义的实体转换为模板描述,并清除其中所有实例标记。 fn from_block(block: &Block, spline_step: u32, blocks: &[&Block]) -> Self { let mut description = Self { objects: block @@ -1702,7 +1709,7 @@ impl Description { description } - // 将包围盒中心移动到局部原点,避免 CAD 中的插入位置泄漏到 QET 元件坐标。 + /// 将包围盒中心移动到局部原点,避免 CAD 绝对位置泄漏到 QET 元件坐标。 fn center_on_origin(&mut self) { let offset_x = -((self.left_bound() + self.right_bound()) / 2.0); let offset_y = -((self.top_bound() + self.bot_bound()) / 2.0); @@ -1713,7 +1720,6 @@ impl Description { } } -// 实现Description的ScaleEntity特征 impl ScaleEntity for Description { fn scale(&mut self, fact_x: f64, fact_y: f64) { self.objects @@ -1722,18 +1728,16 @@ impl ScaleEntity for Description { } fn left_bound(&self) -> f64 { - // 找到所有对象中left_bound值最小的那个对象的 + // 空图纸没有边界,统一回退到 0。 let lb = self.objects.iter().min_by(|ob1, ob2| { ob1.left_bound() .partial_cmp(&ob2.left_bound()) .unwrap_or(std::cmp::Ordering::Greater) }); - // 检查是否找到了对象,如果找到了对象,返回该对象的左边界值 if let Some(lb) = lb { lb.left_bound() } else { - // 如果没有找到任何对象(objects为空),返回默认值0.0 0.0 } } @@ -1781,11 +1785,10 @@ impl ScaleEntity for Description { } } -// 实现description转xml impl From<&Description> for XMLElement { fn from(desc: &Description) -> Self { let mut desc_xml = XMLElement::new("description"); - // 遍历description中的obj,即arc,line,text那些 + // 组合图元在此处扁平化为 QET `` 的直接子节点。 for obj in &desc.objects { for elem in xml_elements_for_object(obj, None) { desc_xml.add_child(elem); @@ -1802,21 +1805,22 @@ impl From<&Description> for XMLElement { drw.entities().filter_map(|ent| Objects::try_from(ent).ok()).collect(); } }*/ -// 将 DXF 绘图对象 ( Drawing ) 和样条步长参数 ( u32 ) 转换为 Description 结构体 -// drw: &Drawing :DXF 绘图对象的引用 -// spline_step: u32 :样条曲线的细分步长参数 +/// 把模型空间实体转换为主 ``。 +/// +/// 受支持的端子 INSERT 先走端子识别分支;其他 INSERT 会递归展开块内容并 +/// 应用插入位置、缩放和旋转。统一构建器仍会过滤不可见实体。 impl From<(&Drawing, u32)> for Description { fn from((drw, spline_step): (&Drawing, u32)) -> Self { - // 创建跟踪范围,用于调试和性能分析 + // tracing span 用于查看整张图纸转换耗时和嵌套日志。 let _from_drw_span = span!(Level::TRACE, "Converting Drawing to Description"); Self { - // 遍历绘图中的所有实体,使用 filter_map 进行转换和过滤 + // 转换失败或不支持的实体当前被 filter_map 静默过滤。 objects: drw .entities() .filter_map(|ent| match &ent.specific { EntityType::Insert(ins) => { - // 检查Insert类型entity的is_visible属性 + // 端子连接语义优先于 INSERT 的辅助图形可见性。 if is_arrowunknown_terminal(ins) { return ObjectsBuilder::new(ent, spline_step).build().ok(); } @@ -1825,9 +1829,7 @@ impl From<(&Drawing, u32)> for Description { return None; } - // 使用 find_block(drw, &ins.name) 查找对应的块定义 let block = find_block(drw, &ins.name)?; - // 收集所有可用的块引用 let blocks: Vec<&Block> = drw.blocks().collect(); trace!( "Creating Group from block {}. Pos(x:{}, y:{}). Scale(x:{}, y:{})", @@ -1837,15 +1839,13 @@ impl From<(&Drawing, u32)> for Description { ins.x_scale_factor, ins.y_scale_factor ); - // 递归遍历block中的entities,使用 ObjectsBuilder 构建 Objects 结构体 + // 子实体继承 INSERT 的位置和缩放,再整体围绕插入点旋转。 let mut objects: Vec = block .entities .iter() .filter_map(|ent| { ObjectsBuilder::new(ent, spline_step) - //very confused here, in one test file if I leave out the ins locations here it puts things in the - //wrong location, and puts them in the correct location when I add the ins location in. - //but in another file it's the opposite, not sure why the difference... + // 顶层块沿用 INSERT 位置;嵌套块路径会再结合自身基点。 .offsets(ins.location.x, ins.location.y) .scaling(ins.x_scale_factor, ins.y_scale_factor) .blocks(&blocks) @@ -1869,7 +1869,7 @@ impl From<(&Drawing, u32)> for Description { objects, })) } - // 处理其他非Block类型的实体 + // 非 INSERT 直接交给统一实体构建器。 _ => ObjectsBuilder::new(ent, spline_step).build().ok(), }) .collect(), @@ -1878,6 +1878,7 @@ impl From<(&Drawing, u32)> for Description { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// QET 端子的四向枚举;Y 向下,因此 North 的向量为 `(0,-1)`。 enum TermOrientation { North, East, @@ -1886,6 +1887,7 @@ enum TermOrientation { } impl TermOrientation { + /// 返回 QET 坐标系中的单位方向向量。 fn vector(self) -> (f64, f64) { match self { Self::North => (0.0, -1.0), @@ -1895,6 +1897,7 @@ impl TermOrientation { } } + /// 把任意向量吸附到绝对分量最大的水平或垂直方向。 fn from_vector(x: f64, y: f64) -> Self { if x.abs() >= y.abs() { if x >= 0.0 { @@ -1909,6 +1912,7 @@ impl TermOrientation { } } + /// 旋转方向向量后重新吸附到 QET 支持的四个方向。 fn rotated(self, angle_deg: f64) -> Self { let angle = angle_deg.to_radians(); let (x, y) = self.vector(); @@ -1931,6 +1935,9 @@ impl Display for TermOrientation { } #[derive(Debug)] +/// 一个可接线的 QET ``,由特定 INSERT 的 connector 属性生成。 +/// +/// `name` 和 `` 当前都使用同一个 connector 值。 pub struct Terminal { x: f64, y: f64, @@ -1941,6 +1948,7 @@ pub struct Terminal { } impl Terminal { + /// 从 `ARROWUNKNOWN` INSERT 提取端子位置、名称、标签和旋转方向。 fn from_insert(insert: &Insert) -> Option { let connector = connector_attribute(insert)?.to_owned(); Some(Self { @@ -1960,6 +1968,7 @@ impl ScaleEntity for Terminal { 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); @@ -1985,6 +1994,7 @@ impl ScaleEntity for Terminal { impl From<&Terminal> for XMLElement { fn from(terminal: &Terminal) -> Self { + // terminal 是可接线节点;普通 dynamic_text 即使显示端子编号也没有此语义。 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)); @@ -2001,6 +2011,7 @@ impl From<&Terminal> for XMLElement { } #[derive(Debug)] +/// ELMT 的多语言名称集合。 pub struct Names { names: Vec, } @@ -2019,12 +2030,14 @@ impl From<&Names> for XMLElement { } #[derive(Debug)] +/// 单个语言代码及其显示名称。 pub struct Name { - lang: String, //should this be an enum of language shorts at some point, maybe not worth it + lang: String, value: String, } #[derive(Debug)] +/// 单独包装 UUID,以匹配 ELMT 的 `` 子节点格式。 pub struct ElmtUuid { uuid: Uuid, } @@ -2043,11 +2056,8 @@ impl From<&ElmtUuid> for XMLElement { } } -//I need to check what these other item types are used for. I think it's unlikely -//I'll ever need for this tool, so it might be worth just hard coding the "element" -//string when writing out the XML, but for now I'll just comment out the other enum -//variants to suppress the clippy warnings. #[derive(Debug)] +/// QET definition 类型;DXF 转换器只会生成普通元件。 enum ItemType { Element = 1, /*ElementsCategory = 2, @@ -2084,6 +2094,7 @@ impl Display for ItemType { } #[derive(Debug, PartialEq)] +/// QET 动态文本水平对齐枚举。 enum HAlignment { Left, Center, @@ -2122,21 +2133,20 @@ impl From for HAlignment { impl From for HAlignment { fn from(value: HorizontalTextJustification) -> Self { - //https://ezdxf.readthedocs.io/en/stable/tutorials/text.html#tut-text match value { HorizontalTextJustification::Left => HAlignment::Left, HorizontalTextJustification::Center => HAlignment::Center, HorizontalTextJustification::Middle => HAlignment::Center, HorizontalTextJustification::Right => HAlignment::Right, - //TODO: Handling the Aligned Middle and Fit alignments are a bit more complicated - //for now I'll just default if it gets one of those we Alighn Left + // Aligned/Fit 需要结合第二对齐点缩放文本,当前降级为左对齐。 _ => HAlignment::Left, } } } #[derive(Debug, PartialEq)] +/// QET 动态文本垂直对齐枚举。 enum VAlignment { Top, Center, @@ -2175,7 +2185,6 @@ impl From for VAlignment { impl From for VAlignment { fn from(value: VerticalTextJustification) -> Self { - //https://ezdxf.readthedocs.io/en/stable/tutorials/text.html#tut-text match value { VerticalTextJustification::Top => VAlignment::Top, VerticalTextJustification::Middle => VAlignment::Center, @@ -2187,9 +2196,9 @@ impl From for VAlignment { } impl VAlignment { - /// 根据文本实体类型和垂直对齐方式创建VAlignment - /// 对于Attrib实体,baseline对齐到底部 - /// 对于MText和Text实体,baseline对齐到中间 + /// 根据文本来源映射 DXF 垂直对齐。 + /// + /// ATTDEF 的 baseline 作为底部;其他文本来源的 baseline 当前映射到中部。 pub fn from_text_entity(value: VerticalTextJustification, is_attrib: bool) -> Self { match value { VerticalTextJustification::Top => VAlignment::Top, @@ -2207,6 +2216,7 @@ impl VAlignment { } #[derive(Debug)] +/// QET 线端样式;当前仅用无端点和简单箭头。 enum LineEnd { None, SimpleArrow, @@ -2232,6 +2242,7 @@ impl Display for LineEnd { } #[derive(Debug)] +/// QET 元件连接类型;当前生成的 DXF 元件固定为 simple。 enum LinkType { Simple, /*Master, @@ -2261,6 +2272,7 @@ impl Display for LinkType { } #[derive(Debug)] +/// ELMT `` 元数据集合。 pub struct ElemInfos { elem_info: Vec, } @@ -2277,24 +2289,12 @@ impl From<&ElemInfos> for XMLElement { } #[derive(Debug)] +/// 一个 QET 元件信息字段。 pub struct ElemInfo { - //there seems to be a list in the editor with the following values (per the XML) - // * supplier - // * description - // * machine_manufacturer_reference - // * manufacturer_reference - // * quantity - // * manufacturer - // * label - // * unity - // * plant - // * comment - // * designation - // But can it only ever be these values? Might need to dig into the code. For now I'll use a string + /// QET 字段键,如 supplier、description、manufacturer 或 label。 name: String, - //I would assume show would be a bool...but instead of a true value I'm getting a "1" in the XML - //generated by the element editor. Maybe this means something else? I'll use an i32 for now + /// QET 编辑器以整数而非布尔文本保存显示标志。 show: i32, value: String, @@ -2312,6 +2312,7 @@ impl From<&ElemInfo> for XMLElement { } #[inline] +/// 把浮点数四舍五入到两位小数,统一控制 ELMT 几何输出精度。 pub fn two_dec(num: f64) -> f64 { (num * 100.0).round() / 100.0 } @@ -2364,11 +2365,9 @@ 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? - -/*https://codebrowser.dev/qt5/qtbase/src/gui/text/qfont.cpp.html +// QET 使用 Qt 的 QFont::toString 格式保存字体。下面保留 Qt 5 字段顺序作为 +// 兼容依据;若未来 QET 切换格式,需要同时更新 FontInfo::fmt。 +/* https://codebrowser.dev/qt5/qtbase/src/gui/text/qfont.cpp.html /*! Returns a description of the font. The description is a comma-separated list of the attributes, perfectly suited for use @@ -2414,6 +2413,7 @@ QString QFont::toString() const */ #[derive(Debug)] +/// Qt 字体风格提示的数值映射,顺序必须与 QFont 保持一致。 pub enum FontStyleHint { Helvetica, Times, @@ -2451,6 +2451,7 @@ impl From<&FontStyleHint> for i32 { } #[derive(Debug)] +/// Qt 字体姿态的数值映射。 pub enum FontStyle { Normal, Italic, @@ -2468,6 +2469,7 @@ impl From<&FontStyle> for i32 { } #[derive(Debug)] +/// 可序列化为 QFont::toString 兼容字符串的字体信息。 struct FontInfo { family: String, point_size: f64, @@ -2483,11 +2485,9 @@ struct FontInfo { impl Default for FontInfo { fn default() -> Self { - //Might want to revisit these defaults - //but I'll put something in for now + // 未提供 DXF 字体信息时使用项目既有的宋体 12 点默认值。 Self { family: "宋体".into(), - // 字号12? point_size: 12.0, pixel_size: 0.0, style_hint: FontStyleHint::Helvetica, @@ -2503,6 +2503,7 @@ impl Default for FontInfo { impl Display for FontInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // 字段顺序对应 Qt 5 QFont::toString;固定的第十项为 0。 write!( f, "{},{},{},{},{},{},{},{},{},0{}", @@ -2525,13 +2526,14 @@ impl Display for FontInfo { } impl FontInfo { + /// 用 DXF 文本高度更新点大小;足够大时也写像素大小。 fn apply_height(&mut self, height: f64) { self.point_size = height; - // Qt ignores point size when pixel size is set; tiny pixel fonts lose glyph details - // so only set pixel size when the height is large enough to render reliably. + // Qt 设置像素大小后会忽略点大小;极小像素字体容易丢失字形,因此小于 3 时不设置。 self.pixel_size = if height >= 3.0 { height } else { 0.0 }; } + /// 按 Y 方向比例缩放字体高度。 fn scale(&mut self, fact: f64) { self.point_size *= fact; if self.pixel_size > 0.0 { @@ -2541,43 +2543,23 @@ impl FontInfo { } #[derive(Debug)] +/// 动态文本构建器可接受的三种 DXF 文本来源。 enum TextEntity<'a> { Text(&'a dxf::entities::Text), MText(&'a dxf::entities::MText), Attrib(&'a AttributeDefinition), } -// AutoCAD Color Index (ACI) 到 RGB 转换函数 -// fn aci_to_rgb(aci: u8) -> (u8, u8, u8) { -// match aci { -// 1 => (255, 0, 0), // 红色 -// 2 => (255, 255, 0), // 黄色 -// 3 => (0, 255, 0), // 绿色 -// 4 => (0, 255, 255), // 青色 -// 5 => (0, 0, 255), // 蓝色 -// 6 => (255, 0, 255), // 洋红色 -// // 7 => (255, 255, 255), // 白色 -// 8 => (128, 128, 128), // 深灰色 -// 9 => (192, 192, 192), // 浅灰色 -// 94 => (0, 129, 0), // 深绿色 -// 15 => (129, 86, 86), // 深棕色 - -// // 更多标准颜色可以根据需要添加 -// _ => (0, 0, 0), // 默认黑色 -// } -// } - -// ACI 转换为 HexColor +/// 经 ACI 查找表生成图元样式使用的 `HexColor`。 fn aci_to_hex_color(aci: u8) -> HexColor { let (r, g, b) = aci_to_rgb(aci); HexColor::rgb(r, g, b) } -/// 从PCL格式字符串中提取文本内容 +/// 从项目样图使用的 PCL 风格包装字符串中提取显示文本。 /// -/// 解析Printer Command Language (PCL)格式的字符串,提取其中的文本内容。 -/// 支持格式如:"\pxt1;{\T1.001;传感器S1}" -> "传感器S1" -/// 以及Unicode转义格式:"{\T1.00100;\U+4F20\U+611F\U+5668S2}" -> "传感器S2" +/// 支持 `\pxt1;{\T1.001;传感器S1}` 和 `\U+4F20` 形式的 Unicode 转义; +/// 不符合已知包装格式时原样返回。 /// /// # 参数 /// * `pcl_string` - PCL格式的字符串 @@ -2585,7 +2567,7 @@ fn aci_to_hex_color(aci: u8) -> HexColor { /// # 返回值 /// 提取出的文本内容,如果解析失败则返回原字符串 pub fn extract_text_from_pcl(pcl_string: &str) -> String { - // 移除开头和结尾的空白字符 + // 只用去空白后的切片判断格式;回退时仍返回原字符串。 let trimmed = pcl_string.trim(); // 处理直接以大括号开始的格式:{\T1.00100;\U+4F20\U+611F\U+5668S2} @@ -2638,13 +2620,7 @@ pub fn extract_text_from_pcl(pcl_string: &str) -> String { pcl_string.to_string() } -/// 解码Unicode转义序列,如\U+4F20转换为对应的Unicode字符 -/// -/// # 参数 -/// * `text` - 包含Unicode转义序列的文本 -/// -/// # 返回值 -/// 解码后的文本 +/// 解码 `\U+XXXX` Unicode 转义;无效码点保留原始转义文本。 fn decode_unicode_escapes(text: &str) -> String { let mut result = String::new(); let mut chars = text.chars().peekable(); @@ -2697,7 +2673,9 @@ fn decode_unicode_escapes(text: &str) -> String { result } -/// 移除常见的 MTEXT 控制序列(如 `\fSimSun|b0|i0;`)并做基础替换 +/// 移除常见 MTEXT 控制序列并转换换行、不换行空格和转义括号。 +/// +/// 未识别的反斜杠命令会原样保留,避免静默丢失可能有意义的文本。 pub(crate) fn strip_mtext_control_sequences(input: &str) -> String { let mut result = String::with_capacity(input.len()); let mut chars = input.chars().peekable(); @@ -2758,6 +2736,8 @@ pub(crate) fn strip_mtext_control_sequences(input: &str) -> String { #[cfg(test)] mod tests { + //! 核心转换回归:端子、块、填充、多段线、线宽、包围盒和嵌套变换。 + use super::{ dxf_line_weight_mm, normalize_lw_line_width_mm, xml_elements_for_object, BlockInstance, BlockMetadata, Definition, Description, Line, Objects, ObjectsBuilder, ScaleEntity, diff --git a/src/qelmt/polygon.rs b/src/qelmt/polygon.rs index 9c2cebc..688959f 100644 --- a/src/qelmt/polygon.rs +++ b/src/qelmt/polygon.rs @@ -1,3 +1,9 @@ +//! 多边形、样条离散和填充四边形转换。 +//! +//! 普通 POLYLINE/LWPOLYLINE 直接保存顶点;SPLINE 按命令行采样步数离散; +//! SOLID/TRACE 恢复 DXF 特有的顶点顺序后输出填充多边形。带 bulge 的混合 +//! 多段线通常由上层 `qelmt::mod` 按线段和圆弧拆分。 + use super::{two_dec, ScaleEntity}; use dxf::entities::{LwPolyline, Polyline, Solid, Spline, Trace}; use dxf::Point as DxfPoint; @@ -5,24 +11,23 @@ use hex_color::HexColor; use simple_xml_builder::XMLElement; use std::ops::{Add, Mul}; -//wait Why do I have a coordinate AND a Point struct, that are -//essentially the same. It's been a couple of months, but I'm not -//seeing why I would have done this....almost makes me wondering -//if I started, then stopped, and then didn't realize where I left off -//and started again but used a different name...? -//Might need to take a closer look and clean this up. #[derive(Debug)] +/// 最终写入 QET `` 的坐标。 pub struct Coordinate { pub x: f64, pub y: f64, } #[derive(Copy, Clone, Debug)] +/// `bspline` crate 计算控制点时使用的轻量二维向量。 +/// +/// 它与 `Coordinate` 外形相似,但实现了样条求值所需的加法和数乘。 pub struct Point { pub x: f64, pub y: f64, } impl Point { + /// 创建样条计算点。 pub fn new(x: f64, y: f64) -> Point { Point { x, y } } @@ -47,6 +52,7 @@ impl Add for Point { } #[derive(Debug)] +/// QET 多边形,包括顶点、闭合状态和绘制样式。 pub struct Polygon { style: String, antialias: bool, @@ -66,8 +72,7 @@ impl From<&Polyline> for Polygon { }) .collect(), closed: poly.is_closed(), - //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? + // 与现有 ELMT 输出保持一致,基础图元默认关闭抗锯齿。 antialias: false, style: "line-style:normal;filling:none;color:black".into(), } @@ -81,7 +86,7 @@ impl From<&LwPolyline> for Polygon { } impl Polygon { - /// 从DXF LwPolyline实体创建Polygon,支持自定义颜色 + /// 从 LWPOLYLINE 顶点创建多边形并保留闭合标志和颜色。 pub fn from_lwpolyline_with_color(poly: &LwPolyline, color: HexColor) -> Self { Polygon { coordinates: poly @@ -93,8 +98,7 @@ impl Polygon { }) .collect(), closed: poly.is_closed(), - //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? + // 与现有 ELMT 输出保持一致,基础图元默认关闭抗锯齿。 antialias: false, style: format!( "line-style:normal;filling:none;color:{}", @@ -106,6 +110,7 @@ impl Polygon { impl From<(&Spline, u32)> for Polygon { fn from((spline, spline_step): (&Spline, u32)) -> Self { + // 将 dxf crate 的控制点和节点向量复制为 bspline crate 所需的类型。 let mut i: usize = 0; let mut points: Vec = Vec::new(); for _a in &spline.control_points { @@ -129,10 +134,7 @@ impl From<(&Spline, u32)> for Polygon { let step: f64 = (curr_spline.knot_domain().1 - curr_spline.knot_domain().0) / (spline_step as f64); - //there is probably a way to clean up some of this logic and use iterators - //although it looks like step_by doesn't work on a f64 range...hmmm - //but I haven't inspected it too closely, and for now am pretty much just duplicating - //it as antonioaja had it + // 浮点参数域不能使用整数 step_by,因此显式递增参数并采样曲线。 let coordinates = { let mut coords = Vec::with_capacity( ((curr_spline.knot_domain().1 - curr_spline.knot_domain().0) / step) as usize + 1, @@ -153,8 +155,7 @@ impl From<(&Spline, u32)> for Polygon { Polygon { coordinates, closed: spline.is_closed(), - //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? + // 与现有 ELMT 输出保持一致,基础图元默认关闭抗锯齿。 antialias: false, style: "line-style:normal;filling:none;color:black".into(), } @@ -185,6 +186,7 @@ impl From<&Trace> for Polygon { impl From<&Polygon> for XMLElement { fn from(poly: &Polygon) -> Self { + // QET 用 x1/y1、x2/y2... 属性序列表示多边形顶点。 let mut poly_xml: XMLElement = XMLElement::new("polygon"); for (count, coord) in poly.coordinates.iter().enumerate() { @@ -192,7 +194,7 @@ impl From<&Polygon> for XMLElement { poly_xml.add_attribute(format!("y{}", (count + 1)), two_dec(coord.y)); } - //closed defaults to true, don't need to write it out unless it's false + // closed=true 是 QET 默认值,仅开放多边形需要显式写 false。 if !poly.closed { poly_xml.add_attribute("closed", poly.closed); } @@ -228,6 +230,7 @@ impl Polygon { } } + /// 让上层统一追加线型或颜色样式。 pub fn update_line_style(&mut self, update_fn: F) where F: FnOnce(&mut String), @@ -235,6 +238,7 @@ impl Polygon { 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); } @@ -250,8 +254,7 @@ impl ScaleEntity for Polygon { fn left_bound(&self) -> f64 { let min_coord = self.coordinates.iter().min_by(|c1, c2| { - //if we get a None for the compare, then just returns Greater which will ignore it - //for finding the minimum + // NaN 无法比较;把它排到后面,使正常坐标仍可确定最小边界。 c1.x.partial_cmp(&c2.x) .unwrap_or(std::cmp::Ordering::Greater) }); @@ -265,8 +268,7 @@ impl ScaleEntity for Polygon { fn right_bound(&self) -> f64 { let max_coord = self.coordinates.iter().max_by(|c1, c2| { - //if we get a None for the compare, then just returns Less which will ignore it - //for finding the maximum + // NaN 无法比较;把它排到前面,使正常坐标仍可确定最大边界。 c1.x.partial_cmp(&c2.x).unwrap_or(std::cmp::Ordering::Less) }); @@ -279,8 +281,7 @@ impl ScaleEntity for Polygon { fn top_bound(&self) -> f64 { let min_coord = self.coordinates.iter().min_by(|c1, c2| { - //if we get a None for the compare, then just returns Greater which will ignore it - //for finding the minimum + // NaN 无法比较;把它排到后面,使正常坐标仍可确定最小边界。 c1.y.partial_cmp(&c2.y) .unwrap_or(std::cmp::Ordering::Greater) }); @@ -294,8 +295,7 @@ impl ScaleEntity for Polygon { fn bot_bound(&self) -> f64 { let max_coord = self.coordinates.iter().max_by(|c1, c2| { - //if we get a None for the compare, then just returns Less which will ignore it - //for finding the maximum + // NaN 无法比较;把它排到前面,使正常坐标仍可确定最大边界。 c1.y.partial_cmp(&c2.y).unwrap_or(std::cmp::Ordering::Less) }); @@ -308,8 +308,7 @@ impl ScaleEntity for Polygon { } -/// 判断LwPolyline是否为带圆角的四边形 -/// 条件:恰好8个顶点,闭合,每个顶点的bulge值要么为0(直线段)要么非0(圆弧段) +/// 判断 LWPOLYLINE 是否符合“四条直边 + 四个圆角”的八顶点特征。 pub fn is_rounded_rectangle(lwpolyline: &LwPolyline) -> bool { // 检查顶点数量 if lwpolyline.vertices.len() != 8 { @@ -331,6 +330,7 @@ pub fn decompose_rounded_rectangle(lwpolyline: &LwPolyline) -> Vec Vec { let mut objects = Vec::new(); let vertices = &lwpolyline.vertices; @@ -364,11 +364,12 @@ pub fn decompose_rounded_rectangle_with_color(lwpolyline: &LwPolyline, color: He objects } -/// 根据bulge值创建圆弧 +/// 使用黑色样式从一段 bulge 几何创建圆弧。 fn create_arc_from_bulge(start_x: f64, start_y: f64, end_x: f64, end_y: f64, bulge: f64) -> Option { create_arc_from_bulge_with_color(start_x, start_y, end_x, end_y, bulge, HexColor::BLACK) } +/// 根据弦端点和 bulge 恢复圆心、半径、起始角和角度跨度。 fn create_arc_from_bulge_with_color(start_x: f64, start_y: f64, end_x: f64, end_y: f64, bulge: f64, color: HexColor) -> Option { if bulge.abs() <= 1e-10 { return None; diff --git a/src/qelmt/text.rs b/src/qelmt/text.rs index c916ae4..7f0c532 100644 --- a/src/qelmt/text.rs +++ b/src/qelmt/text.rs @@ -1,9 +1,15 @@ +//! 旧式 QET `` 图元转换。 +//! +//! 当前主流程主要使用 [`super::DynamicText`];该类型保留给仍需要静态文本 +//! XML 的路径。它只掌握锚点而没有可靠字形测量,因此右/下边界尚未实现。 + use super::{two_dec, FontInfo, ScaleEntity}; use dxf::entities; use hex_color::HexColor; use simple_xml_builder::XMLElement; #[derive(Debug)] +/// QET 静态文本的内容、字体、颜色、位置和旋转。 pub struct Text { rotation: f64, value: String, @@ -15,6 +21,7 @@ pub struct Text { impl From<(&entities::Text, HexColor)> for Text { fn from((txt, color): (&entities::Text, HexColor)) -> Self { + // STANDARD 是 DXF 默认样式,无需在 QET 字体串中重复写出。 let mut font = FontInfo::default(); font.apply_height(txt.text_height); if !txt.text_style_name.is_empty() && txt.text_style_name != "STANDARD" { @@ -38,6 +45,7 @@ impl From<(&entities::Text, HexColor)> for Text { impl From<&Text> for XMLElement { fn from(txt: &Text) -> Self { + // QET 静态文本把内容保存在 `text` 属性,而不是子节点中。 let mut txt_xml: XMLElement = XMLElement::new("text"); txt_xml.add_attribute("x", two_dec(txt.x)); txt_xml.add_attribute("y", two_dec(txt.y)); @@ -50,6 +58,7 @@ impl From<&Text> for XMLElement { } impl Text { + /// 围绕块插入支点旋转文本锚点,并叠加文本自身旋转角。 pub(crate) fn rotate_around(&mut self, pivot_x: f64, pivot_y: f64, angle_deg: f64) { let (x, y) = super::rotate_point_around(self.x, self.y, pivot_x, pivot_y, angle_deg); self.x = x; @@ -74,12 +83,12 @@ impl ScaleEntity for Text { } fn right_bound(&self) -> f64 { - //need to be able to measure text size to get this + // 静态 Text 没有字形布局结果,不能可靠计算右边界。 todo!() } fn bot_bound(&self) -> f64 { - //need to be able to measure text size to get this + // 静态 Text 没有字形布局结果,不能可靠计算下边界。 todo!() } } diff --git a/src/util/generate_aci_colors.py b/src/util/generate_aci_colors.py index f0271dd..9d76984 100644 --- a/src/util/generate_aci_colors.py +++ b/src/util/generate_aci_colors.py @@ -1,9 +1,13 @@ -# generate_aci_colors.py -# 生成 AutoCAD ACI 颜色表 (0-255),输出 Rust 源码文件 aci_colors.rs +"""生成 AutoCAD ACI 0..255 颜色表,并输出 Rust 源码 `aci_colors.rs`。 + +该脚本是维护工具,不参与转换器运行。应在目标输出目录中手动执行,并在提交 +生成结果前核对数组长度和特殊索引的含义。 +""" import math def hsv_to_rgb(h, s, v): + """把 HSV 浮点值换算为 0..255 的 RGB 整数三元组。""" c = v * s x = c * (1 - abs((h / 60) % 2 - 1)) m = v - c @@ -45,6 +49,7 @@ fixed_colors = { 255: (0, 0, 0), } +# 按 ACI 索引顺序构造完整颜色数组。 colors = [] for i in range(256): @@ -58,7 +63,7 @@ for i in range(256): else: colors.append((0, 0, 0)) # 兜底(不会用到) -# 写 Rust 文件 +# 在当前工作目录生成可直接纳入 qelmt 模块的 Rust 源文件。 with open("aci_colors.rs", "w", encoding="utf-8") as f: f.write("// Auto-generated ACI color table (0–255)\n") f.write("pub const ACI_COLORS: [(u8,u8,u8); 256] = [\n")