添加注释

qdj
邱德佳 5 days ago
parent 8f13b9a20d
commit 811ddae3f1

@ -1,3 +1,3 @@
# These are supported funding model platforms
# GitHub 仓库页显示的项目赞助渠道。
ko_fi: vadoola
buy_me_a_coffee: vadoola

@ -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 }}

@ -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

13
.gitignore vendored

@ -1,11 +1,18 @@
# Rust 编译产物。
/target
# 可选的容器/交叉编译本地配置。
Dockerfile
*.dxf
*.elmt
cross.toml
/.cargo
# 转换输入、输出及随项目分发的辅助可执行文件。
*.dxf
*.elmt
*.txt
*.exe
# 编辑器、平台缓存和本仓库选择不跟踪的锁文件。
/.vscode
Cargo.lock
.DS_Store
.DS_Store

@ -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"] }

@ -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<u8>,
removed_pairs: usize,
}
/// 加载 DXF直接解析失败时尝试清理不受支持的 XRECORD 组码后再解析一次。
///
/// 清洗副本使用临时文件保存,原始 `path` 始终只读。
pub fn load_file(path: &Path) -> Result<Drawing> {
match Drawing::load_file(path) {
Ok(drawing) => Ok(drawing),
@ -41,12 +51,17 @@ pub fn load_file(path: &Path) -> Result<Drawing> {
}
}
/// 读取源文件并执行内存清洗;无需清洗时返回 `None`。
fn sanitize_file(path: &Path) -> Result<Option<SanitizedDxf>> {
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<SanitizedDxf> {
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<SanitizedDxf> {
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<SanitizedDxf> {
})
}
/// 根据当前组码和值推进 SECTION/XRECORD 状态机。
///
/// `0 SECTION` 后的 `2 <name>` 决定区段名称;在 OBJECTS 区段中,每个
/// `0 <entity-type>` 都会开始一个新对象,因此可据此进入或离开 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<i32> {
std::str::from_utf8(trimmed_ascii(line))
.ok()?
@ -149,6 +171,7 @@ fn parse_group_code(line: &[u8]) -> Option<i32> {
.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]

@ -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,

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

@ -1,5 +1,12 @@
//! AutoCAD Color IndexACI到 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);

@ -1,3 +1,8 @@
//! DXF 圆弧到 QElectroTech `<arc>` 图元的转换。
//!
//! 支持原生 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<F>(&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;

@ -1,3 +1,9 @@
//! TEXT、MTEXT 和属性定义到 QET `<dynamic_text>` 的统一转换。
//!
//! 构建器先提取三种 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<String>,
@ -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因此子节点固定为一个 `<text>`;复合文本需扩展这里。
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<HexColor>,
}
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 {

@ -1,17 +1,24 @@
//! 圆和椭圆图元转换。
//!
//! DXF CIRCLE、ELLIPSE以及被圆度检测识别为圆的 POLYLINE/LWPOLYLINE
//! 都统一输出为 QET `<ellipse>`。位置字段采用外接矩形左上角坐标。
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<F>(&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;

@ -1,3 +1,8 @@
//! 直线和引线转换。
//!
//! DXF LINE、两个顶点且无圆弧 bulge 的多段线会生成单个 QET `<line>`
//! 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<Line>);
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<Self, &'static str> {
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<F>(&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);

File diff suppressed because it is too large Load Diff

@ -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 `<polygon>` 的坐标。
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<Point> = 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<F>(&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<super::Object
decompose_rounded_rectangle_with_color(lwpolyline, HexColor::BLACK)
}
/// 按每个起点的 bulge 把圆角矩形拆成带指定颜色的线段或圆弧。
pub fn decompose_rounded_rectangle_with_color(lwpolyline: &LwPolyline, color: HexColor) -> Vec<super::Objects> {
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<super::Arc> {
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<super::Arc> {
if bulge.abs() <= 1e-10 {
return None;

@ -1,9 +1,15 @@
//! 旧式 QET `<text>` 图元转换。
//!
//! 当前主流程主要使用 [`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!()
}
}

@ -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 (0255)\n")
f.write("pub const ACI_COLORS: [(u8,u8,u8); 256] = [\n")

Loading…
Cancel
Save