From 5ce0de699a6a4b5e8537e45ec6835c2b650a29c7 Mon Sep 17 00:00:00 2001 From: liaoxianglian Date: Tue, 9 Sep 2025 17:12:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86is=5Finvisible=E5=92=8C?= =?UTF-8?q?=E6=A0=87=E6=B3=A8=E6=96=87=E6=9C=AC=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/FUNDING.yml | 3 + .github/workflows/package.yml | 79 ++ .github/workflows/rust.yml | 43 + .gitignore | 11 + Cargo.toml | 50 ++ LICENSE | 21 + README.md | 58 ++ src/file_writer.rs | 26 + src/main.rs | 207 +++++ src/qelmt/arc.rs | 94 ++ src/qelmt/dynamictext.rs | 353 ++++++++ src/qelmt/ellipse.rs | 177 ++++ src/qelmt/line.rs | 211 +++++ src/qelmt/mod.rs | 1541 +++++++++++++++++++++++++++++++++ src/qelmt/polygon.rs | 282 ++++++ src/qelmt/text.rs | 76 ++ 16 files changed, 3232 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/package.yml create mode 100644 .github/workflows/rust.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/file_writer.rs create mode 100644 src/main.rs create mode 100644 src/qelmt/arc.rs create mode 100644 src/qelmt/dynamictext.rs create mode 100644 src/qelmt/ellipse.rs create mode 100644 src/qelmt/line.rs create mode 100644 src/qelmt/mod.rs create mode 100644 src/qelmt/polygon.rs create mode 100644 src/qelmt/text.rs diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5e3bd76 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms +ko_fi: vadoola +buy_me_a_coffee: vadoola diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..2baac5c --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,79 @@ +name: Package + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: true + matrix: + include: + - os: ubuntu + arch: i386 + target: i686-unknown-linux-gnu + - os: ubuntu + arch: armhf + target: armv7-unknown-linux-gnueabihf + - os: ubuntu + arch: amd64 + target: x86_64-unknown-linux-gnu + - os: ubuntu + arch: arm64 + target: aarch64-unknown-linux-gnu + - os: macos + arch: amd64 + target: x86_64-apple-darwin + - os: macos + arch: arm64 + target: aarch64-apple-darwin + - os: windows + arch: i386 + target: i686-pc-windows-msvc + - os: windows + arch: amd64 + target: x86_64-pc-windows-msvc + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Check crate + if: matrix.os == 'macos' || matrix.os == 'windows' || matrix.os == 'ubuntu' && matrix.arch == 'amd64' + run: cargo publish --dry-run --target ${{ matrix.target }} + + - name: Clippy (release mode) + 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 + if: matrix.os == 'ubuntu' + run: cargo install cross --git https://github.com/cross-rs/cross + + - name: Build binary (Linux) + if: matrix.os == 'ubuntu' + run: cross build --release --target ${{ matrix.target }} + + - name: Build binary (macOS/Windows) + if: matrix.os == 'macos' || matrix.os == 'windows' + run: cargo build --release --target ${{ matrix.target }} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-${{ matrix.os }}-${{ matrix.target }} + path: | + target/*/release/dxf2elmt + target/*/release/dxf2elmt.exe + if-no-files-found: error diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..cbb17a2 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,43 @@ +name: Rust + +on: + 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: + - os: ubuntu + - os: macos + - os: windows + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: fmt + run: cargo fmt --all -- --check + + - name: build + 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 + if: matrix.os != 'windows' || github.event_name != 'pull_request' + run: cargo test --verbose -- --nocapture diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a446b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/target +Dockerfile +*.dxf +*.elmt +cross.toml +/.cargo +*.txt +*.exe +/.vscode +Cargo.lock +.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5351e42 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "dxf2elmt" +version = "0.5.0" +edition = "2021" +description = "A CLI program to convert .dxf files into .elmt files" +authors = ["Vadoola ", "Antonio Aguilar "] +readme = "README.md" +repository = "https://github.com/Vadoola/dxf2elmt" +license = "MIT" +rust-version = "1.79.0" + +[profile.release] +strip = true +lto = true + +[profile.dev.package."*"] +opt-level = 3 + +[dependencies] +dxf = "0.6.0" +simple-xml-builder = "1.1.0" +bspline = "1.1.0" +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 = "0.1" +venator = { version = "1.1", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +#[lints.clippy] +#unwrap_used = "deny" + + +#undecided on how I want to handle logging in this crate right now. +#I do want to do a bit of tracing debugging on some of these recursive blocks though +#for now I'll add tracing and venator under a trace feature that is disabled by default +#maybe a log feature which uses trace to log to a text file or syslog or something +#and a seperate feature that logs to venator? Also figure out how best to isolate +#it to a module, so I don' thave #[cfg(feature = ...)] all over the place +#https://www.shuttle.dev/blog/2024/01/09/getting-started-tracing-rust#instrumentation-in-tracing +[features] +default = [] +venator = ["dep:venator"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..72ce6f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Antonio Aguilar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9345c66 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# dxf2elmt +dxf2elmt is CLI program which can convert .dxf files into [QElectroTech](https://qelectrotech.org/) .elmt files. The program supports both ascii and binary .dxf files. + +The goal of this program is to create a fast and accurate conversion tool to be used with [QElectroTech](https://qelectrotech.org/). + +## How to Use +dxf2elmt requires only one input from the user, the input file. + +For example: + +```bash +./dxf2elmt my_file.dxf +``` + +The .elmt file will be output into the same directory as the executable. It will retain the name of the .dxf file. + +If you wish to forgo creating an .elmt file, you can use the "-v" argument for verbose output. This will output the contents of the .elmt file to stdout without actually creating the file. For example: + +```bash +./dxf2elmt my_file.dxf -v +``` + +## Supported Entities + +* Lines +* Circles +* Arcs +* Texts +* Ellipses +* Polylines +* LwPolylines +* Solids +* Splines +* Blocks (there are still some known issues for deeply nested block) +* MText (partial support) +* Leader + +## To Do + +* Support for the following + * Remaining 2d entities + * Styling (such as Dimension Styles) + +* Better error messages +* Logging + +## Compiling + +Compiled using Rust (MSRV 1.79.0). + +## Credits + +* [Antonioaja](https://github.com/antonioaja) for creating the initial versions of [dxf2elmt](https://github.com/antonioaja/dxf2elmt). Thank you for all your work. +* [QElectroTech](https://qelectrotech.org/) +* [dxf-rs](https://github.com/IxMilia/dxf-rs) +* [simple-xml-builder](https://github.com/Accelbread/simple-xml-builder) +* [bspline](https://github.com/Twinklebear/bspline) +* [tempfile](https://github.com/Stebalien/tempfile) diff --git a/src/file_writer.rs b/src/file_writer.rs new file mode 100644 index 0000000..7d38abd --- /dev/null +++ b/src/file_writer.rs @@ -0,0 +1,26 @@ +extern crate tempfile; + +use anyhow::Context; +use std::fs::File; +use std::path::{Path, PathBuf}; +use tempfile::tempfile; + +pub fn create_file( + verbose_output: bool, + _info: bool, + file_name: &Path, +) -> Result { + let old_file_name = file_name.to_string_lossy(); + + let mut file_name = PathBuf::from(file_name); + file_name.set_extension("elmt"); + + let friendly_file_name = file_name.to_string_lossy(); + let mut out_file = tempfile().context("Could not create temporary file"); + if !verbose_output { + out_file = File::create(&file_name).context("Could not create output file"); + println!("{friendly_file_name} was created... \nNow converting {old_file_name}...",); + } + + out_file.context("Could not return output file") +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7a38eca --- /dev/null +++ b/src/main.rs @@ -0,0 +1,207 @@ +#![warn( + clippy::all, + clippy::pedantic, + //clippy::cargo, + //rust_2024_compatibility, +)] +//#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +extern crate dxf; +extern crate simple_xml_builder; +extern crate unicode_segmentation; + +use anyhow::{Context, Ok, Result}; +use clap::Parser; +use dxf::entities::EntityType; +use dxf::Drawing; +use qelmt::Definition; +//use rayon::prelude::*; +use simple_xml_builder::XMLElement; +use std::time::Instant; +use std::{io, path::PathBuf}; +use tracing::{span, trace, warn, Level}; +use tracing_subscriber::prelude::*; + +#[cfg(feature = "venator")] +use venator::Venator; + +mod qelmt; + +#[derive(Parser, Debug)] +#[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 + //#[clap(short, long, value_parser)] + file_names: Vec, + + /// Activates verbose output, eliminates .elmt file writing + #[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) + #[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, +} + +pub mod file_writer; + +#[allow(clippy::too_many_lines)] +fn main() -> Result<()> { + #[cfg(feature = "venator")] + let tr_reg = tracing_subscriber::registry() + .with(Venator::default()) + .with(tracing_subscriber::fmt::Layer::default()); + + #[cfg(not(feature = "venator"))] + 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 + }; + + let stde_layer = tracing_subscriber::fmt::layer() + .pretty() + .with_writer(io::stderr); + tracing_subscriber::registry() + .with(file_layer.map(|fl| { + fl.with_file(true) + .with_line_number(true) + .with_thread_ids(true) + .with_ansi(false) + .with_filter(tracing_subscriber::EnvFilter::from_env("DXF2E_LOG")) + })) + .with( + stde_layer + .with_file(true) + .with_line_number(true) + .with_thread_ids(true) + .with_filter(tracing_subscriber::EnvFilter::from_env("DXF2E_LOG")), + ) + }; + tr_reg.init(); + + trace!("Starting dxf2elmt"); + + // Start recording time + let now: Instant = Instant::now(); + + // Collect arguments + let args: Args = Args::parse_from(wild::args()); + + // Load dxf file + 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 { + let friendly_file_name = file_name + .file_stem() + .unwrap_or_else(|| file_name.as_os_str()) + .to_string_lossy(); + let drawing: Drawing = Drawing::load_file(&file_name).context(format!( + "Failed to load {friendly_file_name}...\n\tMake sure the file is a valid .dxf file.", + ))?; + 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 + let mut circle_count: u32 = 0; + let mut line_count: u32 = 0; + let mut arc_count: u32 = 0; + let mut spline_count: u32 = 0; + let mut text_count: u32 = 0; + let mut ellipse_count: u32 = 0; + let mut polyline_count: u32 = 0; + let mut lwpolyline_count: u32 = 0; + let mut solid_count: u32 = 0; + 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) => { + circle_count += 1; + } + EntityType::Line(ref _line) => { + line_count += 1; + } + EntityType::Arc(ref _arc) => { + arc_count += 1; + } + EntityType::Spline(ref _spline) => { + spline_count += 1; + } + EntityType::Text(ref _text) => { + text_count += 1; + } + EntityType::Ellipse(ref _ellipse) => { + ellipse_count += 1; + } + EntityType::Polyline(ref _polyline) => { + polyline_count += 1; + } + EntityType::LwPolyline(ref _lwpolyline) => { + lwpolyline_count += 1; + } + EntityType::Solid(ref _solid) => { + solid_count += 1; + } + EntityType::Insert(ref _insert) => { + block_count += 1; + } + _ => { + other_count += 1; + } + }); + + // Create output file for .elmt + let out_file = file_writer::create_file(args.verbose, args.info, &file_name)?; + + // Write to output file + let out_xml = XMLElement::from(&q_elmt); + out_xml + .write(&out_file) + .context("Failed to write output file.")?; + + if args.info { + println!("Conversion complete!\n"); + + // Print stats + println!("STATS"); + println!("~~~~~~~~~~~~~~~"); + println!("Circles: {circle_count}"); + println!("Lines: {line_count}"); + println!("Arcs: {arc_count}"); + println!("Splines: {spline_count}"); + println!("Texts: {text_count}"); + println!("Ellipses: {ellipse_count}"); + println!("Polylines: {polyline_count}"); + println!("LwPolylines: {lwpolyline_count}"); + println!("Solids: {solid_count}"); + println!("Blocks: {block_count}"); + println!("Currently Unsupported: {other_count}"); + + println!("\nTime Elapsed: {} ms", now.elapsed().as_millis()); + } + + if args.verbose { + print!("{out_xml}"); + } + } + drop(dxf_loop_guard); + + Ok(()) +} diff --git a/src/qelmt/arc.rs b/src/qelmt/arc.rs new file mode 100644 index 0000000..1c7c945 --- /dev/null +++ b/src/qelmt/arc.rs @@ -0,0 +1,94 @@ +use super::{two_dec, ScaleEntity}; +use dxf::entities; +use simple_xml_builder::XMLElement; + +#[derive(Debug)] +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? + pub x: f64, + pub y: f64, + + width: f64, + height: f64, + start: f64, + angle: f64, + style: String, + antialias: bool, +} + +impl From<&entities::Arc> for Arc { + fn from(arc: &entities::Arc) -> Self { + let temp_angle = if arc.start_angle > arc.end_angle { + (360.0 - arc.start_angle) + arc.end_angle + } else { + arc.end_angle - arc.start_angle + }; + + Arc { + x: arc.center.x - arc.radius, + y: -arc.center.y - arc.radius, + height: arc.radius * 2.0, + width: arc.radius * 2.0, + start: if arc.start_angle < 0.0 { + -arc.start_angle + } else { + arc.start_angle + }, + angle: if temp_angle < 0.0 { + -temp_angle + } else { + temp_angle + }, + + //in the original code antialias is always set to false...I'm guessing for performance + //reasons...I'm trying to think if there is a time we might want to turn it on? + antialias: false, + style: if arc.thickness > 0.1 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(), + } + } +} + +impl From<&Arc> for XMLElement { + fn from(arc: &Arc) -> Self { + 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)); + arc_xml.add_attribute("width", two_dec(arc.width)); + arc_xml.add_attribute("height", two_dec(arc.height)); + arc_xml.add_attribute("start", arc.start.round()); + arc_xml.add_attribute("angle", arc.angle.round()); + arc_xml.add_attribute("antialias", arc.antialias); + arc_xml.add_attribute("style", &arc.style); + arc_xml + } +} + +impl ScaleEntity for Arc { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + self.x *= fact_x; + self.y *= fact_y; + self.width *= fact_x; + self.height *= fact_y; + } + + fn left_bound(&self) -> f64 { + self.x + } + + fn right_bound(&self) -> f64 { + self.x + self.width + } + + fn top_bound(&self) -> f64 { + self.y + } + + fn bot_bound(&self) -> f64 { + self.y + self.height + } +} diff --git a/src/qelmt/dynamictext.rs b/src/qelmt/dynamictext.rs new file mode 100644 index 0000000..1499410 --- /dev/null +++ b/src/qelmt/dynamictext.rs @@ -0,0 +1,353 @@ +use super::{two_dec, FontInfo, ScaleEntity, TextEntity}; +use dxf::entities::{self, AttributeDefinition}; +use hex_color::HexColor; +use simple_xml_builder::XMLElement; +use unicode_segmentation::UnicodeSegmentation; +use uuid::Uuid; + +/*use parley::{ + Alignment, FontContext, FontWeight, InlineBox, Layout, LayoutContext, PositionedLayoutItem, + StyleProperty, +};*/ + +use super::{HAlignment, VAlignment}; + +#[derive(Debug)] +pub struct DynamicText { + text: String, + info_name: Option, + pub x: f64, + pub y: f64, + z: f64, + rotation: f64, + uuid: Uuid, + h_alignment: HAlignment, + font: FontInfo, + text_from: String, + v_alignment: VAlignment, + frame: bool, + text_width: i32, + keep_visual_rotation: bool, + color: HexColor, + reference_rectangle_width: f64, +} + +impl From<&DynamicText> for XMLElement { + fn from(txt: &DynamicText) -> Self { + let mut dtxt_xml = XMLElement::new("dynamic_text"); + // taken from QET_ElementScaler: "ElmtDynText::AsSVGstring" + // // Position und Rotationspunkt berechnen: + // posx = x + (size/8.0)+4.05 - 0.5; + // posy = y + (7.0/5.0*size + 26.0/5.0) - 0.5; + // rotx = (-1) * (((size/8.0)+4.05) - 0.5); + // roty = (-1) * ((7.0/5.0*size + 26.0/5.0) - 0.5); + // + // reversed and slightly modified after looking at the result in element-editor: + // + let pt_size: f64 = txt.font.point_size; + // + // we need the horizontal alignment and the text-width to move to right x-position: + // txt.reference_rectangle_width, // should be text-width (Group code 41) + // txt.attachment_point, // Group code 71 + // // 1 = Top left; 2 = Top center; 3 = Top right + // // 4 = Middle left; 5 = Middle center; 6 = Middle right + // // 7 = Bottom left; 8 = Bottom center; 9 = Bottom right + // + // + // it's just annoying if the value for "reference_rectangle_width" in the dxf is “0.0”... + // + // o.k. ... as long as we do not know the real width: + // "guess" the width by number of characters and font-size: + // + let graphene_count = txt.text.graphemes(true).count(); + let txt_width = if txt.reference_rectangle_width > 2.0 { + txt.reference_rectangle_width + } else { + (graphene_count as f64) * pt_size * 0.75 + }; + + // let x_pos = { + // let x_pos = txt.x + 0.5 - (pt_size / 8.0) - 4.05; + // // match txt.h_alignment { + // // HAlignment::Left => x_pos, + // // HAlignment::Center => x_pos - txt_width / 2.0, + // // HAlignment::Right => x_pos - txt_width, + // // } + // x_pos // 直接返回基础x_pos,不进行水平对齐调整 + // }; + // // let y_pos = txt.y + 0.5 - (7.0 / 5.0 * pt_size + 26.0 / 5.0) + pt_size; + // let y_pos = { + // let base_y_pos = txt.y + 0.5 - (7.0 / 5.0 * pt_size + 26.0 / 5.0) + pt_size; + // match txt.v_alignment { + // VAlignment::Top => base_y_pos, + // VAlignment::Center => base_y_pos - pt_size / 2.0, + // VAlignment::Bottom => base_y_pos - pt_size, + // } + // }; + + // 计算基础位置(不考虑旋转) + let base_x = txt.x + 0.5 - (pt_size / 8.0) - 4.05; + let base_y = { + let base_y_pos = txt.y + 0.5 - (7.0 / 5.0 * pt_size + 26.0 / 5.0) + pt_size; + match txt.v_alignment { + VAlignment::Top => base_y_pos, + VAlignment::Center => base_y_pos - pt_size / 2.0, + VAlignment::Bottom => base_y_pos - pt_size, + } + }; + + // 如果有旋转角度,应用旋转变换 + let (x_pos, y_pos) = if txt.rotation != 0.0 { + let rotation_rad = txt.rotation.to_radians(); + let cos_r = rotation_rad.cos(); + let sin_r = rotation_rad.sin(); + + // 以文本原点为中心进行旋转变换 + let dx = base_x - txt.x; + let dy = base_y - txt.y; + + let rotated_dx = dx * cos_r - dy * sin_r; + let rotated_dy = dx * sin_r + dy * cos_r; + + (txt.x + rotated_dx, txt.y + rotated_dy) + } else { + (base_x, base_y) + }; + + dtxt_xml.add_attribute("x", two_dec(x_pos)); + dtxt_xml.add_attribute("y", two_dec(y_pos)); + dtxt_xml.add_attribute("z", two_dec(txt.z)); + dtxt_xml.add_attribute("rotation", two_dec(txt.rotation)); + dtxt_xml.add_attribute("uuid", format!("{{{}}}", txt.uuid)); + dtxt_xml.add_attribute("font", &txt.font); + dtxt_xml.add_attribute("Halignment", &txt.h_alignment); + dtxt_xml.add_attribute("Valignment", &txt.v_alignment); + dtxt_xml.add_attribute("text_from", &txt.text_from); + dtxt_xml.add_attribute("frame", txt.frame); + dtxt_xml.add_attribute("text_width", txt.text_width); + dtxt_xml.add_attribute("color", txt.color.display_rgb()); + + //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 + let mut text_xml = XMLElement::new("text"); + text_xml.add_text(&txt.text); + dtxt_xml.add_child(text_xml); + + if let Some(i_name) = &txt.info_name { + dtxt_xml.add_attribute("info_name", i_name); + } + + if txt.keep_visual_rotation { + dtxt_xml.add_attribute("keep_visual_rotation", txt.keep_visual_rotation); + } + + dtxt_xml + } +} + +impl ScaleEntity for DynamicText { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + self.x *= fact_x; + self.y *= fact_y; + //self.font.pixel_size *= fact; + self.font.point_size *= fact_x; + } + + fn left_bound(&self) -> f64 { + self.x + } + + fn right_bound(&self) -> f64 { + //todo!() + 1.0 + } + + fn top_bound(&self) -> f64 { + self.y + } + + fn bot_bound(&self) -> f64 { + //todo!() + 1.0 + } +} + +pub struct DTextBuilder<'a> { + text: TextEntity<'a>, + color: Option, +} + +impl<'a> DTextBuilder<'a> { + pub fn from_text(text: &'a entities::Text) -> Self { + Self { + text: TextEntity::Text(text), + color: None, + } + } + + pub fn from_mtext(text: &'a entities::MText) -> Self { + Self { + text: TextEntity::MText(text), + color: None, + } + } + + pub fn from_attrib(attrib: &'a AttributeDefinition) -> Self { + Self { + text: TextEntity::Attrib(attrib), + color: None, + } + } + + pub fn color(self, color: HexColor) -> Self { + Self { + color: Some(color), + ..self + } + } + + pub fn build(self) -> DynamicText { + let ( + x, + y, + z, + rotation, + style_name, + text_height, + value, + h_alignment, + v_alignment, + reference_rectangle_width, + ) = match self.text { + TextEntity::Text(txt) => ( + txt.location.x, + -txt.location.y, + txt.location.z, + txt.rotation, + &txt.text_style_name, + txt.text_height, + txt.value.clone(), + HAlignment::from(txt.horizontal_text_justification), + VAlignment::from(txt.vertical_text_justification), + 0.0, // as Placeholder: no "reference_rectangle_width" with Text!!! + ), + TextEntity::MText(mtxt) => ( + mtxt.insertion_point.x, + -mtxt.insertion_point.y, + mtxt.insertion_point.z, + mtxt.rotation_angle, + &mtxt.text_style_name, + //I'm not sure what the proper value is here for Mtext + //becuase I haven't actually finished supporting it. + //I'll put initial text height for now. But i'm not certain + //exactly what this correlates to. There is also vertical_height, + //which I would guess is the total vertical height for all the lines + //it's possible I would need to take the vertical height and divide + //by the number of lines to get the value I need....I'm not sure yet + mtxt.initial_text_height, + //There are 2 text fields on MTEXT, .text a String and .extended_text a Vec + //Most of the example files I have at the moment are single line MTEXT. + //I edited one of them in QCad, and added a few lines. The value came through in the text field + //with extended_text being empty, and the newlines were deliniated by '\\P'...I might need to look + //the spec a bit to determine what it says for MTEXT, but for now, I'll just assume this is correct + //So looking at the spec, yes '\P' is the MTEXT newline essentially. There is a bunch of MTEXT + //inline codes that can be found at https://ezdxf.readthedocs.io/en/stable/dxfentities/mtext.html + //The extended text is code point 3 in the dxf spec which just says: "Additional text (always in 250-character chunks) (optional)" + //and Code point 1 the normal text value says: "Text string. If the text string is less than 250 characters, all characters appear + //in group 1. If the text string is greater than 250 characters, the string is divided into 250-character chunks, which appear in + //one or more group 3 codes. If group 3 codes are used, the last group is a group 1 and has fewer than 250 characters" + { + let mut val = mtxt.extended_text.join(""); + val.push_str(&mtxt.text); + val.replace("\\P", "\n") + }, + HAlignment::from(mtxt.attachment_point), + VAlignment::from(mtxt.attachment_point), + mtxt.reference_rectangle_width, + ), + TextEntity::Attrib(attrib) => ( + attrib.location.x, + -attrib.location.y, + attrib.location.z, + attrib.rotation, + &attrib.text_style_name, + attrib.text_height, + attrib.text_tag.clone(), + HAlignment::from(attrib.horizontal_text_justification), + VAlignment::from(attrib.vertical_text_justification), + 0.0, // as Placeholder: not need to check if Attrib has something similar + ), + }; + + // Create a FontContext (font database) and LayoutContext (scratch space). + // These are both intended to be constructed rarely (perhaps even once per app): + /*let mut font_cx = FontContext::new(); + let mut layout_cx = LayoutContext::new(); + + // Create a `RangedBuilder` or a `TreeBuilder`, which are used to construct a `Layout`. + const DISPLAY_SCALE : f32 = 1.0; + let mut builder = layout_cx.ranged_builder(&mut font_cx, &value, DISPLAY_SCALE); + + // Set default styles that apply to the entire layout + builder.push_default(StyleProperty::LineHeight(1.3)); + builder.push_default(StyleProperty::FontSize((text_height * self.txt_sc_factor.unwrap()).round() as f32)); + + // Build the builder into a Layout + let mut layout: Layout<()> = builder.build(&value); + + // Run line-breaking and alignment on the Layout + const MAX_WIDTH : Option = Some(1000.0); + layout.break_all_lines(MAX_WIDTH); + layout.align(MAX_WIDTH, Alignment::Start); + + let calc_width = layout.width(); + let calc_height = layout.height(); + dbg!(&value); + dbg!(calc_width); + dbg!(calc_height);*/ + + /*dbg!(&value); + dbg!(&y); + dbg!(&self.text);*/ + DynamicText { + //x: x - (calc_width as f64/2.0), + x, + y, + z, + rotation: if rotation.abs().round() as i64 % 360 != 0 { + rotation - 180.0 + } else { + 0.0 + }, + uuid: Uuid::new_v4(), + font: if style_name == "STANDARD" { + FontInfo { + point_size: text_height, + ..Default::default() + } + } else { + //clearly right now this is exactly the same as the main body of the if block + //I'm jus putting this in for now, to compile while I get the font handling + //working correctly + FontInfo { + point_size: text_height, + ..Default::default() + } + }, + reference_rectangle_width, //liest aus der dxf-Datei!!! + h_alignment, + v_alignment, + text_from: "UserText".into(), + frame: false, + text_width: -1, + color: self.color.unwrap_or(HexColor::BLACK), + + text: value, + keep_visual_rotation: false, + info_name: None, + } + } +} diff --git a/src/qelmt/ellipse.rs b/src/qelmt/ellipse.rs new file mode 100644 index 0000000..7dc4b11 --- /dev/null +++ b/src/qelmt/ellipse.rs @@ -0,0 +1,177 @@ +use super::{two_dec, Circularity, ScaleEntity}; +use dxf::entities::{self, Circle, LwPolyline, Polyline}; +use simple_xml_builder::XMLElement; + +#[derive(Debug)] +pub struct Ellipse { + height: f64, + width: f64, + style: String, + + //need to brush up on my Rust scoping rules, isn't there a way to make this pub to just the module? + pub x: f64, + pub y: f64, + + antialias: bool, +} + +impl From<&Circle> for Ellipse { + fn from(circ: &Circle) -> Self { + Ellipse { + x: circ.center.x - circ.radius, + y: -circ.center.y - circ.radius, + height: circ.radius * 2.0, + width: circ.radius * 2.0, + + //in the original code antialias is always set to false...I'm guessing for performance + //reasons...I'm trying to think if there is a time we might want to turn it on? + antialias: false, + style: if circ.thickness > 0.5 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(), + } + } +} + +impl From<&entities::Ellipse> for Ellipse { + fn from(ellipse: &entities::Ellipse) -> Self { + Ellipse { + x: ellipse.center.x - ellipse.major_axis.x, + y: -ellipse.center.y - ellipse.major_axis.x * ellipse.minor_axis_ratio, + height: ellipse.major_axis.x * 2.0, + width: ellipse.major_axis.x * 2.0 * ellipse.minor_axis_ratio, + + //in the original code antialias is always set to false...I'm guessing for performance + //reasons...I'm trying to think if there is a time we might want to turn it on? + antialias: false, + style: "line-style:normal;line-weight:thin;filling:none;color:black".into(), + } + } +} + +impl TryFrom<&Polyline> for Ellipse { + type Error = &'static str; //add better error later + + fn try_from(poly: &Polyline) -> Result { + if !poly.is_circular() { + 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. + let x = poly + .vertices() + .fold(f64::MAX, |min_x, vtx| min_x.min(vtx.location.x)); + + let max_x = poly + .vertices() + .fold(f64::MIN, |max_x, vtx| max_x.max(vtx.location.x)); + + let y = poly + .vertices() + .fold(f64::MAX, |min_y, vtx| min_y.min(vtx.location.y)); + + let max_y = poly + .vertices() + .fold(f64::MIN, |max_y, vtx| max_y.max(vtx.location.y)); + + Ok(Ellipse { + x, + y: -max_y, + height: max_y - y, + width: max_x - x, + + //in the original code antialias is always set to false...I'm guessing for performance + //reasons...I'm trying to think if there is a time we might want to turn it on? + antialias: false, + style: "line-style:normal;line-weight:thin;filling:none;color:black".into(), + }) + } +} + +impl TryFrom<&LwPolyline> for Ellipse { + type Error = &'static str; //add better error later + + fn try_from(poly: &LwPolyline) -> Result { + if !poly.is_circular() { + return Err("Polyline has poor circularity, can't convert"); + } + + let x = poly + .vertices + .iter() + .fold(f64::MAX, |min_x, vtx| min_x.min(vtx.x)); + + let max_x = poly + .vertices + .iter() + .fold(f64::MIN, |max_x, vtx| max_x.max(vtx.x)); + + let y = poly + .vertices + .iter() + .fold(f64::MAX, |min_y, vtx| min_y.min(vtx.y)); + + let max_y = poly + .vertices + .iter() + .fold(f64::MIN, |max_y, vtx| max_y.max(vtx.y)); + + Ok(Ellipse { + x, + y: -max_y, + height: max_y - y, + width: max_x - x, + + //in the original code antialias is always set to false...I'm guessing for performance + //reasons...I'm trying to think if there is a time we might want to turn it on? + antialias: false, + style: "line-style:normal;line-weight:thin;filling:none;color:black".into(), + }) + } +} + +impl From<&Ellipse> for XMLElement { + fn from(ell: &Ellipse) -> Self { + 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)); + ell_xml.add_attribute("width", two_dec(ell.width)); + ell_xml.add_attribute("height", two_dec(ell.height)); + ell_xml.add_attribute("antialias", ell.antialias); + ell_xml.add_attribute("style", &ell.style); + ell_xml + } +} + +impl ScaleEntity for Ellipse { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + self.x *= fact_x; + self.y *= fact_y; + self.width *= fact_x; + self.height *= fact_y; + } + + fn left_bound(&self) -> f64 { + self.x + } + + fn right_bound(&self) -> f64 { + self.x + self.width + } + + fn top_bound(&self) -> f64 { + self.y + } + + fn bot_bound(&self) -> f64 { + self.y + self.height + } +} diff --git a/src/qelmt/line.rs b/src/qelmt/line.rs new file mode 100644 index 0000000..87c6f04 --- /dev/null +++ b/src/qelmt/line.rs @@ -0,0 +1,211 @@ +use super::two_dec; +use super::LineEnd; +use super::ScaleEntity; +use dxf::entities::{self, LwPolyline, Polyline}; +use simple_xml_builder::XMLElement; + +#[derive(Debug)] +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, + pub y2: f64, + + style: String, + end1: LineEnd, + antialias: bool, +} + +pub struct Leader(pub Vec); + +impl From<&entities::Line> for Line { + fn from(line: &entities::Line) -> Self { + Line { + x1: line.p1.x, + y1: -line.p1.y, + length1: 1.5, //why is this statically set at 1.5? + end1: LineEnd::None, + x2: line.p2.x, + y2: -line.p2.y, + length2: 1.5, //why is this statically set at 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? + antialias: false, + style: if line.thickness > 0.5 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(), + } + } +} + +impl TryFrom<&Polyline> for Line { + type Error = &'static str; //add better error later + + fn try_from(poly: &Polyline) -> Result { + if poly.__vertices_and_handles.len() != 2 { + return Err("Error can't convert polyline with more than 2 points into a 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? + 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? + 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? + antialias: false, + style: if poly.thickness > 0.5 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(), + }) + } +} + +impl TryFrom<&LwPolyline> for Line { + type Error = &'static str; //add better error later + + fn try_from(poly: &LwPolyline) -> Result { + if poly.vertices.len() != 2 { + return Err("Error can't convert polyline with more than 2 points into a Line"); + } + + Ok(Line { + x1: poly.vertices[0].x, + y1: -poly.vertices[0].y, + length1: 1.5, //why is this statically set at 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? + 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? + antialias: false, + style: if poly.thickness > 0.1 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(), + }) + } +} + +impl From<&entities::Leader> for Leader { + fn from(leader: &entities::Leader) -> Self { + Leader( + leader + .vertices + .windows(2) + .enumerate() + .map(|(cnt, pt_slice)| { + let end1 = if leader.use_arrowheads && cnt == 0 { + LineEnd::SimpleArrow + } else { + LineEnd::None + }; + + 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 + 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 + 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? + antialias: false, + //looks like line thickenss and color information I *might* need to grab from a dimension style + //entity which I haven't implemented yet + /*style: if line.thickness > 0.5 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(),*/ + style: "line-style:normal;line-weight:normal;filling:none;color:black" + .into(), + } + }) + .collect(), + ) + } +} + +impl From<&Line> for XMLElement { + fn from(line: &Line) -> Self { + let mut line_xml: XMLElement = XMLElement::new("line"); + line_xml.add_attribute("x1", two_dec(line.x1)); + line_xml.add_attribute("y1", two_dec(line.y1)); + line_xml.add_attribute("length1", two_dec(line.length1)); + line_xml.add_attribute("end1", &line.end1); + line_xml.add_attribute("x2", two_dec(line.x2)); + line_xml.add_attribute("y2", two_dec(line.y2)); + line_xml.add_attribute("length2", two_dec(line.length2)); + line_xml.add_attribute("end2", &line.end2); + line_xml.add_attribute("antialias", line.antialias); + line_xml.add_attribute("style", &line.style); + line_xml + } +} + +impl ScaleEntity for Line { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + self.x1 *= fact_x; + self.x2 *= fact_x; + + 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 + self.length1 *= fact_x.min(fact_y); + self.length1 = self.length1.min(99.0); + + self.length2 *= fact_x.min(fact_y); + self.length2 = self.length2.min(99.0); + } + + fn left_bound(&self) -> f64 { + self.x1.min(self.x2) + } + + fn right_bound(&self) -> f64 { + self.x1.max(self.x2) + } + + fn top_bound(&self) -> f64 { + self.y1.min(self.y2) + } + + fn bot_bound(&self) -> f64 { + self.y1.max(self.y2) + } +} diff --git a/src/qelmt/mod.rs b/src/qelmt/mod.rs new file mode 100644 index 0000000..874bd39 --- /dev/null +++ b/src/qelmt/mod.rs @@ -0,0 +1,1541 @@ +use dxf::entities::{AttributeDefinition, Entity, EntityType}; +use dxf::entities::{LwPolyline, Polyline}; +use dxf::enums::{AttachmentPoint, HorizontalTextJustification, Units, VerticalTextJustification}; +use dxf::{Block, Drawing}; +use dynamictext::DTextBuilder; +use hex_color::HexColor; +use itertools::Itertools; +use simple_xml_builder::XMLElement; +use std::convert::TryFrom; +use std::f64::consts::PI; +use std::fmt::Display; +use uuid::Uuid; + +use tracing::{error, info, span, trace, Level}; + +pub mod arc; +pub use arc::Arc; + +pub mod line; +pub use line::{Leader, Line}; + +pub mod text; +pub use text::Text; + +pub mod dynamictext; +pub use dynamictext::DynamicText; + +pub mod polygon; +pub use polygon::Polygon; + +pub mod ellipse; +pub use ellipse::Ellipse; + +// 在 DXF 图纸中根据名称查找特定的块(Block) +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() +} + +// 枚举作用 :表示两种可能的类型之一 +#[derive(Debug)] +enum Either { + Left(L), + Right(R), +} + +// Definition结构体,表示 ELMT 文件的定义,包含元素的尺寸、热点、版本、UUID 等信息 +#[derive(Debug)] +pub struct Definition { + r#type: ItemType, + width: i64, + height: i64, + hotspot_x: i64, + hotspot_y: i64, + version: String, + link_type: LinkType, + uuid: ElmtUuid, + names: Names, + element_infos: Option, + informations: &'static str, + description: Description, + //counts +} + +//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 +// 特征定义,定义了一组方法,用于缩放实体、获取实体的边界和判断实体是否为圆形 +trait ScaleEntity { + fn scale(&mut self, fact_x: f64, fact_y: f64); + + fn left_bound(&self) -> f64; + fn right_bound(&self) -> f64; + + fn top_bound(&self) -> f64; + fn bot_bound(&self) -> f64; +} + +// 用于判断多边形是否接近圆形的特征,有点类似函数声明 +trait Circularity { + // 判断图形是否为圆形 + fn is_circular(&self) -> bool; + + // 返回圆形判断的容差范围 (0.98..=1.02),即允许2%的误差 + 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 + 0.98..=1.02 + } +} + +// 为 Polyline 类型实现圆形检测,有点类似函数定义 +impl Circularity for Polyline { + fn is_circular(&self) -> bool { + let poly_perim: f64 = { + let tmp_pts: Vec = self.vertices().map(|v| v.clone().location).collect(); + let len = tmp_pts.len(); + tmp_pts + .into_iter() + .circular_tuple_windows() + .map(|(fst, sec)| ((fst.x - sec.x).powf(2.0) + (fst.y - sec.y).powf(2.0)).sqrt()) + .take(len) + .sum() + }; + + 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. + let tmp_pts: Vec = self.vertices().map(|v| v.clone().location).collect(); + let len = tmp_pts.len(); + let mut poly_area: f64 = tmp_pts + .into_iter() + .circular_tuple_windows() + .map(|(fst, sec)| (fst.x * sec.y) - (fst.y * sec.x)) + .take(len) + .sum(); + poly_area /= 2.0; + poly_area.abs() + }; + let t_ratio = 4.0 * PI * poly_area / poly_perim.powf(2.0); + + Self::match_range().contains(&t_ratio) + } +} + +// 为 LwPolyline 类型实现相同的圆形检测逻辑 +impl Circularity for LwPolyline { + fn is_circular(&self) -> bool { + let poly_perim: f64 = self + .vertices + .iter() + .circular_tuple_windows() + .map(|(fst, sec)| { + ((fst.x - sec.x).powf(2.0) + (fst.y - sec.y).powf(2.0)) + .abs() + .sqrt() + }) + .take(self.vertices.len()) + .sum(); + + let poly_area = { + let mut poly_area: f64 = self + .vertices + .iter() + .circular_tuple_windows() + .map(|(fst, sec)| (fst.x * sec.y) - (fst.y * sec.x)) + .take(self.vertices.len()) + .sum(); + poly_area /= 2.0; + poly_area.abs() + }; + let t_ratio = 4.0 * PI * poly_area / poly_perim.powf(2.0); + + Self::match_range().contains(&t_ratio) + } +} + +// 实现Definition的方法 +impl Definition { + // 创建新的 Definition 实例 + pub fn new(name: impl Into, spline_step: u32, drw: &Drawing) -> Self { + /*for st in drw.styles() { + dbg!(st); + }*/ + // 缩放处理 + let scale_factor = Self::scale_factor(drw.header.default_drawing_units); + // 创建description + let description = { + let mut description: Description = (drw, spline_step).into(); + description.scale(scale_factor, scale_factor); + description + }; + + // 计算宽度,x热点坐标 + //The below calculation for width and hotspot_x are taken from the qet source code + let (width, hotspot_x) = { + let tmp_width = description.right_bound() - description.left_bound(); + let int_width = tmp_width.round() as i64; + let upwidth = ((int_width / 10) * 10) + 10; + let xmargin = (upwidth as f64 - tmp_width).round(); + + let width = if int_width % 10 > 6 { + upwidth + 10 + } else { + upwidth + }; + + ( + width, + -((description.left_bound() - (xmargin / 2.0)).round() as i64), + ) + }; + + // 计算高度,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; + let upheight = ((int_height / 10) * 10) + 10; + let ymargin = (upheight as f64 - tmp_height).round(); + + let height = if int_height % 10 > 6 { + upheight + 10 + } else { + upheight + }; + + ( + height, + -((description.top_bound() - (ymargin / 2.0)).round() as i64), + ) + }; + + // 结构体初始化 :设置所有必要字段,包括类型、版本、UUID等 + Definition { + r#type: ItemType::Element, + width, + height, + hotspot_x, + hotspot_y, + version: "0.8.0".into(), + link_type: LinkType::Simple, + uuid: Uuid::new_v4().into(), + names: Names { + names: vec![Name { + lang: "en".into(), + value: name.into(), //need to truncate the extension + }], + }, + element_infos: None, + informations: "Created using dxf2elmt!", + description, + } + } + + // 根据不同的测量单位返回相应的缩放因子 + 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 + match unit { + Units::Unitless => 1.0, //for now if the drawing is untiless don't scale it + Units::Inches => 50.8, + Units::Feet => 609.6, + Units::Miles | Units::USSurveyMile => 3_218_694.437_4, + Units::Millimeters => 2.0, + Units::Centimeters => 20.0, + Units::Meters => 2_000.0, + Units::Kilometers => 2_000_000.0, + Units::Microinches => 50.8E-6, + Units::Mils => 0.0508, + Units::Yards => 1_828.8, + Units::Angstroms => 2.0E-7, + Units::Nanometers => 2.0e-6, + Units::Microns => 0.002, + Units::Decimeters => 200.0, + Units::Decameters => 20_000.0, + Units::Hectometers => 200_000.0, + Units::Gigameters => 2.0e12, + Units::AstronomicalUnits => 299_195_741_382_000.0, + Units::LightYears => 18_921_460_945_160_086_000.0, + Units::Parsecs => 61_713_551_625_599_170_000.0, + 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 + Units::USSurveyYard => 1_828.803_657_6, + } + } +} + +// 将 Definition 结构体转换为 XML 元素 +impl From<&Definition> for XMLElement { + // 创建 根元素 + fn from(def: &Definition) -> Self { + let mut def_xml = XMLElement::new("definition"); + def_xml.add_attribute("height", def.height); + def_xml.add_attribute("width", def.width); + def_xml.add_attribute("hotspot_x", def.hotspot_x); + def_xml.add_attribute("hotspot_y", def.hotspot_y); + def_xml.add_attribute("version", &def.version); + def_xml.add_attribute("link_type", &def.link_type); + def_xml.add_attribute("type", &def.r#type); + + def_xml.add_child((&def.uuid).into()); + def_xml.add_child((&def.names).into()); + if let Some(einfos) = &def.element_infos { + def_xml.add_child(einfos.into()); + } + + let mut info_elmt = XMLElement::new("informations"); + info_elmt.add_text(def.informations); + def_xml.add_child(info_elmt); + + def_xml.add_child((&def.description).into()); + + def_xml + } +} + +// 枚举类型,表示各种图形对象(弧线、椭圆、多边形、文本、线条等) +#[derive(Debug)] +pub(crate) enum Objects { + Arc(Arc), + Ellipse(Ellipse), + Polygon(Polygon), + DynamicText(DynamicText), + Text(Text), + Line(Line), + //Terminal(Terminal), + Group(Vec), +} + +impl Objects { + // 返回一个 Descendants 迭代器,用于遍历对象的所有后代节 + pub fn descendants(&self) -> Descendants<'_> { + Descendants { + stack: vec![self.children()], + } + } + + // 返回一个 Children 迭代器,用于遍历对象的直接子节点 + pub fn children(&self) -> Children<'_> { + match self { + Objects::Group(l) => Children { slice: l.iter() }, + _ => Children { slice: [].iter() }, + } + } +} + +// 结构体,包含一个 stack 字段,存储 Children 迭代器的栈,使用生命周期参数 'a 确保引用的有效性 +pub(crate) struct Descendants<'a> { + stack: Vec>, +} + +// Descendants迭代器实现,深度优先遍历 :使用栈结构实现树形结构的深度优先搜索 +impl<'a> Iterator for Descendants<'a> { + type Item = &'a Objects; + fn next(&mut self) -> Option { + //let iter_span = span!(Level::TRACE, "Iterating Object Descendants"); + //let _span_guard = iter_span.enter(); + while let Some(last) = self.stack.last_mut() { + if let Some(obj) = last.next() { + //trace!("Found more children"); + self.stack.push(obj.children()); + return Some(obj); + } + + self.stack.pop(); + } + None + } +} + +// Children结构体,提供对直接子节点的简单迭代访 +pub(crate) struct Children<'a> { + slice: std::slice::Iter<'a, Objects>, +} + +// Children迭代器实现 +impl<'a> Iterator for Children<'a> { + type Item = &'a Objects; + fn next(&mut self) -> Option { + self.slice.next() + } + + fn size_hint(&self) -> (usize, Option) { + (self.slice.len(), None) + } +} + +// 实现Objects的ScaleEntity特征 +impl ScaleEntity for Objects { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + // match 是 Rust 的模式匹配,类似于 switch 语句但更强大 + match self { + Objects::Arc(arc) => arc.scale(fact_x, fact_y), + Objects::Ellipse(ellipse) => ellipse.scale(fact_x, fact_y), + Objects::Polygon(polygon) => polygon.scale(fact_x, fact_y), + Objects::DynamicText(dynamic_text) => dynamic_text.scale(fact_x, fact_y), + Objects::Text(text) => text.scale(fact_x, fact_y), + Objects::Line(line) => line.scale(fact_x, fact_y), + Objects::Group(vec) => vec.iter_mut().for_each(|ob| ob.scale(fact_x, fact_y)), + } + } + + fn left_bound(&self) -> f64 { + match self { + // left_bound就是x值 + Objects::Arc(arc) => arc.left_bound(), + Objects::Ellipse(ellipse) => ellipse.left_bound(), + Objects::Polygon(polygon) => polygon.left_bound(), + Objects::DynamicText(dynamic_text) => dynamic_text.left_bound(), + Objects::Text(text) => text.left_bound(), + Objects::Line(line) => line.left_bound(), + Objects::Group(vec) => { + let lb = vec.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 { + 0.0 + } + } + } + } + + fn right_bound(&self) -> f64 { + match self { + // right_bound就是x + width值 + Objects::Arc(arc) => arc.right_bound(), + Objects::Ellipse(ellipse) => ellipse.right_bound(), + Objects::Polygon(polygon) => polygon.right_bound(), + Objects::DynamicText(dynamic_text) => dynamic_text.right_bound(), + Objects::Text(text) => text.right_bound(), + Objects::Line(line) => line.right_bound(), + Objects::Group(vec) => { + let rb = vec.iter().max_by(|ob1, ob2| { + ob1.right_bound() + .partial_cmp(&ob2.right_bound()) + .unwrap_or(std::cmp::Ordering::Less) + }); + + if let Some(rb) = rb { + rb.right_bound() + } else { + 0.0 + } + } + } + } + + fn top_bound(&self) -> f64 { + match self { + Objects::Arc(arc) => arc.top_bound(), + Objects::Ellipse(ellipse) => ellipse.top_bound(), + Objects::Polygon(polygon) => polygon.top_bound(), + Objects::DynamicText(dynamic_text) => dynamic_text.top_bound(), + Objects::Text(text) => text.top_bound(), + Objects::Line(line) => line.top_bound(), + Objects::Group(vec) => { + let tb = vec.iter().min_by(|ob1, ob2| { + ob1.top_bound() + .partial_cmp(&ob2.top_bound()) + .unwrap_or(std::cmp::Ordering::Greater) + }); + + if let Some(tb) = tb { + tb.top_bound() + } else { + 0.0 + } + } + } + } + + fn bot_bound(&self) -> f64 { + match self { + Objects::Arc(arc) => arc.bot_bound(), + Objects::Ellipse(ellipse) => ellipse.bot_bound(), + Objects::Polygon(polygon) => polygon.bot_bound(), + Objects::DynamicText(dynamic_text) => dynamic_text.bot_bound(), + Objects::Text(text) => text.bot_bound(), + Objects::Line(line) => line.bot_bound(), + Objects::Group(vec) => { + let bb = vec.iter().max_by(|ob1, ob2| { + ob1.bot_bound() + .partial_cmp(&ob2.bot_bound()) + .unwrap_or(std::cmp::Ordering::Less) + }); + + if let Some(bb) = bb { + bb.bot_bound() + } else { + 0.0 + } + } + } + } +} + +#[derive(Debug)] +struct ScaleFactor { + x: f64, + y: f64, +} + +impl Default for ScaleFactor { + fn default() -> Self { + ScaleFactor { x: 1.0, y: 1.0 } + } +} + +#[derive(Default, Debug)] +pub(crate) struct Offset { + x: f64, + y: f64, +} + +// 构建器结构体 +#[derive(Debug)] +pub struct ObjectsBuilder<'a> { + // &'a Entity 是对 Entity 的引用 + ent: &'a Entity, + spline_step: u32, + blocks: &'a [&'a Block], + offset: Offset, + scale_fact: ScaleFactor, +} + +impl<'a> ObjectsBuilder<'a> { + // 创建新的实例 + pub fn new(ent: &'a Entity, spline_step: u32) -> Self { + Self { + ent, + spline_step, + blocks: &[], + offset: Offset::default(), + scale_fact: ScaleFactor::default(), + } + } + + // 设置可用的块引用列表 + 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 }, + ..self + } + } + + // 设置 X、Y 缩放因子 + pub fn scaling(self, fact_x: f64, fact_y: f64) -> Self { + Self { + scale_fact: ScaleFactor { + x: fact_x, + y: fact_y, + }, + ..self + } + } + + #[allow(clippy::too_many_lines)] + pub fn build(self) -> Result { + // 检查entity的is_visible属性,如果为false则不进行转换 + if !self.ent.common.is_visible { + return Err("Entity is not visible"); + } + + // 实体类型转换处理 + match &self.ent.specific { + // Circle → Ellipse :圆转换为椭圆 + EntityType::Circle(circle) => { + let mut ellipse: Ellipse = circle.into(); + + ellipse.scale(self.scale_fact.x, self.scale_fact.y); + ellipse.x += self.offset.x; + ellipse.y -= self.offset.y; + Ok(Objects::Ellipse(ellipse)) + } + EntityType::Line(line) => { + let mut line: Line = line.into(); + + line.scale(self.scale_fact.x, self.scale_fact.y); + + line.x1 += self.offset.x; + line.y1 -= self.offset.y; + + line.x2 += self.offset.x; + line.y2 -= self.offset.y; + + Ok(Objects::Line(line)) + } + EntityType::Arc(arc) => { + let mut arc: Arc = arc.into(); + + arc.scale(self.scale_fact.x, self.scale_fact.y); + + arc.x += self.offset.x; + arc.y -= self.offset.y; + + Ok(Objects::Arc(arc)) + } + EntityType::Spline(spline) => { + let mut poly: Polygon = (spline, self.spline_step).into(); + + match poly.coordinates.len() { + 0 | 1 => Err("Error removing empty Spline"), + //I'll need to improve my understanding of splines and the math here + //to make sure I do this correctly. + //2 => //convert to line + _ => { + poly.scale(self.scale_fact.x, self.scale_fact.y); + for cord in &mut poly.coordinates { + cord.x += self.offset.x; + cord.y -= self.offset.y; + } + + Ok(Objects::Polygon(poly)) + } + } + } + EntityType::Text(text) => { + Ok( + //right now the dxf2elmt defaults to making all text Static Text... + //it was requested by the QET devs to add in support for Dynamic text + //which was added, but it defaults to OFF, and QET doesn't pass the parameter + //to enable it...I'm wondering if it makes more sense to default to use dynamic text + //for now I'll set it to use dynamic text, and once I get the CLI flag passing through + //I might change the default parameter to use Dynamic Text + if false { + //how best to pass in the flag for dynamic text or not....should the flag also default to true? + let mut text: Text = ( + text, + HexColor::from_u32(self.ent.common.color_24_bit as u32), + ) + .into(); + + text.scale(self.scale_fact.x, self.scale_fact.y); + + text.x += self.offset.x; + text.y -= self.offset.y; + + Objects::Text(text) + } else { + let mut dtext = DTextBuilder::from_text(text) + .color(HexColor::from_u32(self.ent.common.color_24_bit as u32)) + .build(); + + dtext.scale(self.scale_fact.x, self.scale_fact.y); + + dtext.x += self.offset.x; + dtext.y -= self.offset.y; + + Objects::DynamicText(dtext) + }, + ) + } + EntityType::Ellipse(ellipse) => { + let mut ellipse: Ellipse = ellipse.into(); + + ellipse.scale(self.scale_fact.x, self.scale_fact.y); + ellipse.x += self.offset.x; + ellipse.y -= self.offset.y; + + Ok(Objects::Ellipse(ellipse)) + } + EntityType::MText(mtext) => { + Ok( + //right now the dxf2elmt defaults to making all text Static Text... + //it was requested by the QET devs to add in support for Dynamic text + //which was added, but it defaults to OFF, and QET doesn't pass the parameter + //to enable it...I'm wondering if it makes more sense to default to use dynamic text + //for now I'll set it to use dynamic text, and once I get the CLI flag passing through + //I might change the default parameter to use Dynamic Text + if false { + //how best to pass in the flag for dynamic text or not....should the flag also default to true? + /*let mut text: Text = + (mtext, HexColor::from_u32(ent.common.color_24_bit as u32)).into(); + text.x += offset_x; + text.y -= offset_y; + Objects::Text(text)*/ + todo!(); + } else { + let mut dtext = DTextBuilder::from_mtext(mtext) + .color(HexColor::from_u32(self.ent.common.color_24_bit as u32)) + .build(); + + dtext.scale(self.scale_fact.x, self.scale_fact.y); + + dtext.x += self.offset.x; + dtext.y -= self.offset.y; + + Objects::DynamicText(dtext) + }, + ) + } + EntityType::Polyline(polyline) => match polyline.__vertices_and_handles.len() { + 0 | 1 => Err("Error empty Polyline"), + 2 => { + let mut line = Line::try_from(polyline)?; + + line.scale(self.scale_fact.x, self.scale_fact.x); + + line.x1 += self.offset.x; + line.y1 -= self.offset.y; + + line.x2 += self.offset.x; + line.y2 -= self.offset.y; + + Ok(Objects::Line(line)) + } + _ => { + if let Ok(mut ellipse) = Ellipse::try_from(polyline) { + ellipse.scale(self.scale_fact.x, self.scale_fact.y); + + ellipse.x += self.offset.x; + ellipse.y -= self.offset.y; + + Ok(Objects::Ellipse(ellipse)) + } else { + let mut poly: Polygon = polyline.into(); + + poly.scale(self.scale_fact.x, self.scale_fact.y); + + for cord in &mut poly.coordinates { + cord.x += self.offset.x; + cord.y -= self.offset.y; + } + + Ok(Objects::Polygon(poly)) + } + } + }, + EntityType::LwPolyline(lwpolyline) => match lwpolyline.vertices.len() { + 0 | 1 => Err("Error empty LwPolyline"), + 2 => { + let mut line = Line::try_from(lwpolyline)?; + + line.scale(self.scale_fact.x, self.scale_fact.y); + + line.x1 += self.offset.x; + line.y1 -= self.offset.y; + + line.x2 += self.offset.x; + line.y2 -= self.offset.y; + + Ok(Objects::Line(line)) + } + _ => { + if let Ok(mut ellipse) = Ellipse::try_from(lwpolyline) { + ellipse.scale(self.scale_fact.x, self.scale_fact.y); + + ellipse.x += self.offset.x; + ellipse.y -= self.offset.y; + + Ok(Objects::Ellipse(ellipse)) + } else { + let mut poly: Polygon = lwpolyline.into(); + + poly.scale(self.scale_fact.x, self.scale_fact.y); + + for cord in &mut poly.coordinates { + cord.x += self.offset.x; + cord.y -= self.offset.y; + } + + Ok(Objects::Polygon(poly)) + } + } + }, + EntityType::Solid(solid) => { + let mut poly: Polygon = solid.into(); + + poly.scale(self.scale_fact.x, self.scale_fact.y); + + for cord in &mut poly.coordinates { + cord.x += self.offset.x; + cord.y -= self.offset.y; + } + + Ok(Objects::Polygon(poly)) + } + EntityType::Insert(ins) => { + //info!("Found an Insert Block: {ins:?}"); + info!("Found an Insert Block: {}", &ins.name); + let Some(block) = self.blocks.iter().find(|bl| bl.name == ins.name) else { + error!("Block {} not found", ins.name); + return Err("Block Not Found"); + }; + trace!( + "Base Point: x: {} / y: {}", + block.base_point.x, + block.base_point.y + ); + + trace!("Creating Group from block {}. Pos(x:{}, y:{}). Offset(x:{}, y:{}). Scale(x:{}, y:{})", + ins.name, ins.location.x, ins.location.y, self.offset.x, self.offset.y, self.scale_fact.x * ins.x_scale_factor, + self.scale_fact.y * ins.y_scale_factor); + Ok(Objects::Group( + block + .entities + .iter() + .filter_map(|ent| { + ObjectsBuilder::new(ent, self.spline_step) + .offsets( + ins.location.x - block.base_point.x, + ins.location.y - block.base_point.y, + ) + .scaling( + self.scale_fact.x * ins.x_scale_factor, + self.scale_fact.y * ins.y_scale_factor, + ) + .blocks(self.blocks) + .build() + .ok() + }) + .collect(), + )) + } + EntityType::Leader(leader) => { + let ld: Leader = leader.into(); + + Ok(Objects::Group( + ld.0.into_iter() + .map(|mut ln| { + ln.scale(self.scale_fact.x, self.scale_fact.y); + + ln.x1 += self.offset.x; + ln.y1 -= self.offset.y; + + ln.x2 += self.offset.x; + ln.y2 -= self.offset.y; + + Objects::Line(ln) + }) + .collect(), + )) + } + EntityType::AttributeDefinition(attrib) => Ok({ + //need to look up the proper way to get the color for the Attrib + let mut dtext = DTextBuilder::from_attrib(attrib) + .color(HexColor::from_u32(self.ent.common.color_24_bit as u32)) + .build(); + + dtext.scale(self.scale_fact.x, self.scale_fact.y); + + dtext.x += self.offset.x; + dtext.y -= self.offset.y; + + Objects::DynamicText(dtext) + }), + _ => { + //dbg!(&self.ent.specific); + Err("Need to implement the rest of the entity types") + } + } + } +} + +// 将 Objects 枚举转换为 Either> 类型 +// Either::Left(XMLElement) :单个 XML 元素 +// Either::Right(Vec) :XML 元素向量 +impl From<&Objects> for Either> { + fn from(obj: &Objects) -> Self { + match obj { + Objects::Arc(arc) => Either::Left(arc.into()), + Objects::Ellipse(ell) => Either::Left(ell.into()), + Objects::Polygon(poly) => Either::Left(poly.into()), + Objects::DynamicText(dtext) => Either::Left(dtext.into()), + Objects::Text(txt) => Either::Left(txt.into()), + Objects::Line(line) => Either::Left(line.into()), + Objects::Group(block) => Either::Right( + // 迭代组中的每个对象,try_from转换,filter_map和ok忽略转换失败对象,collect()收集转换成功元素 + block + .iter() + //.flatten() + .filter_map(|obj| XMLElement::try_from(obj).ok()) + .collect(), + ), + } + } +} + +// 实现try_from尝试转换 +impl TryFrom<&Objects> for XMLElement { + type Error = &'static str; // add better error later + + fn try_from(obj: &Objects) -> Result { + match obj { + Objects::Arc(arc) => Ok(arc.into()), + Objects::Ellipse(ell) => Ok(ell.into()), + Objects::Polygon(poly) => Ok(poly.into()), + Objects::DynamicText(dtext) => Ok(dtext.into()), + Objects::Text(txt) => Ok(txt.into()), + Objects::Line(line) => Ok(line.into()), + Objects::Group(_) => Err("Unsupported"), + } + } +} + +// 包含所有图形对象的描述 +#[derive(Debug)] +pub struct Description { + objects: Vec, +} + +// 实现Description的ScaleEntity特征 +impl ScaleEntity for Description { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + self.objects + .iter_mut() + .for_each(|ob| ob.scale(fact_x, fact_y)); + } + + fn left_bound(&self) -> f64 { + // 找到所有对象中left_bound值最小的那个对象的 + 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 + } + } + + fn right_bound(&self) -> f64 { + let rb = self.objects.iter().max_by(|ob1, ob2| { + ob1.right_bound() + .partial_cmp(&ob2.right_bound()) + .unwrap_or(std::cmp::Ordering::Less) + }); + + if let Some(rb) = rb { + rb.left_bound() + } else { + 0.0 + } + } + + fn top_bound(&self) -> f64 { + let tb = self.objects.iter().min_by(|ob1, ob2| { + ob1.top_bound() + .partial_cmp(&ob2.top_bound()) + .unwrap_or(std::cmp::Ordering::Greater) + }); + + if let Some(tb) = tb { + tb.top_bound() + } else { + 0.0 + } + } + + fn bot_bound(&self) -> f64 { + let bb = self.objects.iter().max_by(|ob1, ob2| { + ob1.bot_bound() + .partial_cmp(&ob2.bot_bound()) + .unwrap_or(std::cmp::Ordering::Less) + }); + + if let Some(bb) = bb { + bb.top_bound() + } else { + 0.0 + } + } +} + +// 实现description转xml +impl From<&Description> for XMLElement { + fn from(desc: &Description) -> Self { + let mut desc_xml = XMLElement::new("description"); + // 遍历description中的obj,即arc,line,text那些 + for obj in &desc.objects { + if let Ok(elem) = XMLElement::try_from(obj) { + desc_xml.add_child(elem); + } + for obj in obj.descendants() { + if let Ok(elem) = XMLElement::try_from(obj) { + desc_xml.add_child(elem); + } + } + } + desc_xml + } +} + +/*impl TryFrom for Description { + type Error = &'static str; //add better error later + + fn try_from(drw: Drawing) -> Result { + drw.entities().filter_map(|ent| Objects::try_from(ent).ok()).collect(); + } +}*/ +// 将 DXF 绘图对象 ( Drawing ) 和样条步长参数 ( u32 ) 转换为 Description 结构体 +// drw: &Drawing :DXF 绘图对象的引用 +// spline_step: u32 :样条曲线的细分步长参数 +impl From<(&Drawing, u32)> for Description { + fn from((drw, spline_step): (&Drawing, u32)) -> Self { + // 创建跟踪范围,用于调试和性能分析 + let _from_drw_span = span!(Level::TRACE, "Converting Drawing to Description"); + + + Self { + // 遍历绘图中的所有实体,使用 filter_map 进行转换和过滤 + objects: drw + .entities() + .filter_map(|ent| match &ent.specific { + EntityType::Insert(ins) => { + // 检查Insert类型entity的is_visible属性 + if !ent.common.is_visible { + 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:{})", + ins.name, + ins.location.x, + ins.location.y, + ins.x_scale_factor, + ins.y_scale_factor + ); + // 递归遍历block中的entities,使用 ObjectsBuilder 构建 Objects 结构体 + Some(Objects::Group( + 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... + .offsets(ins.location.x, ins.location.y) + .scaling(ins.x_scale_factor, ins.y_scale_factor) + .blocks(&blocks) + .build() + .ok() + }) + .collect(), + )) + } + // 处理其他非Block类型的实体 + _ => ObjectsBuilder::new(ent, spline_step).build().ok(), + }) + .collect(), + } + } +} + +//probably don't need to worry about this as they won't exist in the dxf... +/*pub struct Terminal { + x: f64, + y: f64, + uuid: Uuid, + name: String, + orientation: TermOrient, + //type? + // Generic + // Indoor Terminal Block + // External Terminal Block +}*/ + +#[derive(Debug)] +pub struct Names { + names: Vec, +} + +impl From<&Names> for XMLElement { + fn from(nme: &Names) -> Self { + let mut names_elmt = XMLElement::new("names"); + for name in &nme.names { + let mut nm_elmt = XMLElement::new("name"); + nm_elmt.add_attribute("lang", &name.lang); + nm_elmt.add_text(&name.value); + names_elmt.add_child(nm_elmt); + } + names_elmt + } +} + +#[derive(Debug)] +pub struct Name { + lang: String, //should this be an enum of language shorts at some point, maybe not worth it + value: String, +} + +#[derive(Debug)] +pub struct ElmtUuid { + uuid: Uuid, +} + +impl From for ElmtUuid { + fn from(uuid: Uuid) -> Self { + ElmtUuid { uuid } + } +} + +impl From<&ElmtUuid> for XMLElement { + fn from(uuid: &ElmtUuid) -> Self { + let mut uuid_xml = XMLElement::new("uuid"); + uuid_xml.add_attribute("uuid", format!("{{{}}}", uuid.uuid)); + uuid_xml + } +} + +//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)] +enum ItemType { + Element = 1, + /*ElementsCategory = 2, + ElementsCollection = 4, + ElementsContainer = 6, + ElementsCollectionItem = 7, + TitleBlockTemplate = 8, + TitleBlockTemplatesCollection = 16, + TitleBlockTemplatesCollectionItem = 24, + Diagram = 32, + Project = 64, + All = 127,*/ +} + +impl Display for ItemType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Element => "element", + /*Self::ElementsCategory | Self::ElementsContainer | Self::ElementsCollectionItem => + "elements category", + Self::ElementsCollection => "element", + Self::TitleBlockTemplate | Self::TitleBlockTemplatesCollectionItem => + "title block template", + Self::TitleBlockTemplatesCollection => "title block templates collection", + Self::Diagram => "diagram", + Self::Project => "project", + Self::All => "All",*/ + } + ) + } +} + +#[derive(Debug)] +enum HAlignment { + Left, + Center, + Right, +} + +impl Display for HAlignment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Left => "AlignLeft", + Self::Center => "AlignHCenter", + Self::Right => "AlignRight", + } + ) + } +} + +impl From for HAlignment { + fn from(value: AttachmentPoint) -> Self { + match value { + AttachmentPoint::TopLeft + | AttachmentPoint::MiddleLeft + | AttachmentPoint::BottomLeft => HAlignment::Left, + AttachmentPoint::TopCenter + | AttachmentPoint::MiddleCenter + | AttachmentPoint::BottomCenter => HAlignment::Center, + AttachmentPoint::TopRight + | AttachmentPoint::MiddleRight + | AttachmentPoint::BottomRight => HAlignment::Right, + } + } +} + +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::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 + _ => HAlignment::Left, + } + } +} + +#[derive(Debug)] +enum VAlignment { + Top, + Center, + Bottom, +} + +impl Display for VAlignment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Top => "AlignTop", + Self::Center => "AlignVCenter", + Self::Bottom => "AlignBottom", + } + ) + } +} + +impl From for VAlignment { + fn from(value: AttachmentPoint) -> Self { + match value { + AttachmentPoint::TopLeft | AttachmentPoint::TopCenter | AttachmentPoint::TopRight => { + VAlignment::Top + } + AttachmentPoint::MiddleLeft + | AttachmentPoint::MiddleCenter + | AttachmentPoint::MiddleRight => VAlignment::Center, + AttachmentPoint::BottomLeft + | AttachmentPoint::BottomCenter + | AttachmentPoint::BottomRight => VAlignment::Bottom, + } + } +} + +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, + VerticalTextJustification::Baseline | VerticalTextJustification::Bottom => { + VAlignment::Bottom + } + } + } +} + +#[derive(Debug)] +enum LineEnd { + None, + SimpleArrow, + /*TriangleArrow, + Circle, + Diamond,*/ +} + +impl Display for LineEnd { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::None => "none", + Self::SimpleArrow => "simple", + /*Self::TriangleArrow => "triangle", + Self::Circle => "circle", + Self::Diamond => "diamond",*/ + } + ) + } +} + +#[derive(Debug)] +enum LinkType { + Simple, + /*Master, + Slave, + NextReport, + PrevReport, + TermBlock, + Thumbnail,*/ +} + +impl Display for LinkType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Simple => "simple", + /*Self::Master => "master", + Self::Slave => "slave", + Self::NextReport => "next_report", + Self::PrevReport => "previous_report", + Self::TermBlock => "terminal", + Self::Thumbnail => "thumbnail",*/ + } + ) + } +} + +#[derive(Debug)] +pub struct ElemInfos { + elem_info: Vec, +} + +impl From<&ElemInfos> for XMLElement { + fn from(elems: &ElemInfos) -> Self { + let mut elems_xml = XMLElement::new("elementInformations"); + for elem in &elems.elem_info { + elems_xml.add_child(elem.into()); + } + + elems_xml + } +} + +#[derive(Debug)] +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 + 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 + show: i32, + + value: String, +} + +impl From<&ElemInfo> for XMLElement { + fn from(elem: &ElemInfo) -> Self { + let mut elem_xml = XMLElement::new("elementInformation"); + elem_xml.add_attribute("show", elem.show); + elem_xml.add_attribute("name", &elem.name); + elem_xml.add_text(&elem.value); + + elem_xml + } +} + +#[inline] +pub fn two_dec(num: f64) -> f64 { + (num * 100.0).round() / 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 +/*! + Returns a description of the font. The description is a + comma-separated list of the attributes, perfectly suited for use + in QSettings, and consists of the following: + \list + \li Font family + \li Point size + \li Pixel size + \li Style hint + \li Font weight + \li Font style + \li Underline + \li Strike out + \li Fixed pitch + \li Always \e{0} + \li Capitalization + \li Letter spacing + \li Word spacing + \li Stretch + \li Style strategy + \li Font style (omitted when unavailable) + \endlist + \sa fromString() + */ +QString QFont::toString() const +{ + const QChar comma(QLatin1Char(',')); + QString fontDescription = family() + comma + + QString::number( pointSizeF()) + comma + + QString::number( pixelSize()) + comma + + QString::number((int) styleHint()) + comma + + QString::number( weight()) + comma + + QString::number((int) style()) + comma + + QString::number((int) underline()) + comma + + QString::number((int) strikeOut()) + comma + + QString::number((int)fixedPitch()) + comma + + QString::number((int) false); + QString fontStyle = styleName(); + if (!fontStyle.isEmpty()) + fontDescription += comma + fontStyle; + return fontDescription; +} + */ + +#[derive(Debug)] +pub enum FontStyleHint { + Helvetica, + Times, + Courier, + OldEnglish, + System, + AnyStyle, + Cursive, + Monospace, + Fantasy, +} + +/*impl FontStyleHint { + pub const SansSerif: FontStyleHint = FontStyleHint::Helvetica; + pub const Serif: FontStyleHint = FontStyleHint::Times; + pub const TypeWriter: FontStyleHint = FontStyleHint::Courier; + pub const Decorative: FontStyleHint = FontStyleHint::OldEnglish; +} +*/ + +impl From<&FontStyleHint> for i32 { + fn from(value: &FontStyleHint) -> Self { + match value { + FontStyleHint::Helvetica => 0, + FontStyleHint::Times => 1, + FontStyleHint::Courier => 2, + FontStyleHint::OldEnglish => 3, + FontStyleHint::System => 4, + FontStyleHint::AnyStyle => 5, + FontStyleHint::Cursive => 6, + FontStyleHint::Monospace => 7, + FontStyleHint::Fantasy => 8, + } + } +} + +#[derive(Debug)] +pub enum FontStyle { + Normal, + Italic, + Oblique, +} + +impl From<&FontStyle> for i32 { + fn from(value: &FontStyle) -> Self { + match value { + FontStyle::Normal => 0, + FontStyle::Italic => 1, + FontStyle::Oblique => 2, + } + } +} + +#[derive(Debug)] +struct FontInfo { + family: String, + point_size: f64, + pixel_size: i32, + style_hint: FontStyleHint, + weight: i32, + style: FontStyle, + underline: bool, + strike_out: bool, + fixed_pitch: bool, + style_name: Option, +} + +impl Default for FontInfo { + fn default() -> Self { + //Might want to revisit these defaults + //but I'll put something in for now + Self { + family: "宋体".into(), + point_size: 12.0, + pixel_size: i32::default(), + style_hint: FontStyleHint::Helvetica, + weight: i32::default(), + style: FontStyle::Normal, + underline: false, + strike_out: false, + fixed_pitch: false, + style_name: None, + } + } +} + +impl Display for FontInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{},{},{},{},{},{},{},{},{},0{}", + self.family, + self.point_size.round(), + self.pixel_size, + Into::::into(&self.style_hint), + self.weight, + Into::::into(&self.style), + i32::from(self.underline), + i32::from(self.strike_out), + i32::from(self.fixed_pitch), + if let Some(sn) = &self.style_name { + format!(",{sn}") + } else { + String::new() + }, + ) + } +} + +#[derive(Debug)] +enum TextEntity<'a> { + Text(&'a dxf::entities::Text), + MText(&'a dxf::entities::MText), + Attrib(&'a AttributeDefinition), +} diff --git a/src/qelmt/polygon.rs b/src/qelmt/polygon.rs new file mode 100644 index 0000000..7c2662f --- /dev/null +++ b/src/qelmt/polygon.rs @@ -0,0 +1,282 @@ +use super::{two_dec, ScaleEntity}; +use dxf::entities::{LwPolyline, Polyline, Solid, Spline}; +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)] +pub struct Coordinate { + pub x: f64, + pub y: f64, +} + +#[derive(Copy, Clone, Debug)] +pub struct Point { + pub x: f64, + pub y: f64, +} +impl Point { + pub fn new(x: f64, y: f64) -> Point { + Point { x, y } + } +} +impl Mul for Point { + type Output = Point; + fn mul(self, rhs: f64) -> Point { + Point { + x: self.x * rhs, + y: self.y * rhs, + } + } +} +impl Add for Point { + type Output = Point; + fn add(self, rhs: Point) -> Point { + Point { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +#[derive(Debug)] +pub struct Polygon { + style: String, + antialias: bool, + pub coordinates: Vec, + closed: bool, +} + +impl From<&Polyline> for Polygon { + fn from(poly: &Polyline) -> Self { + Polygon { + coordinates: poly + .__vertices_and_handles + .iter() + .map(|(vertex, _handle)| Coordinate { + x: vertex.location.x, + y: -vertex.location.y, + }) + .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? + antialias: false, + style: if poly.thickness > 0.1 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(), + } + } +} + +impl From<&LwPolyline> for Polygon { + fn from(poly: &LwPolyline) -> Self { + Polygon { + coordinates: poly + .vertices + .iter() + .map(|vertex| Coordinate { + x: vertex.x, + y: -vertex.y, + }) + .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? + antialias: false, + style: if poly.thickness > 0.1 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(), + } + } +} + +impl From<(&Spline, u32)> for Polygon { + fn from((spline, spline_step): (&Spline, u32)) -> Self { + let mut i: usize = 0; + let mut points: Vec = Vec::new(); + for _a in &spline.control_points { + points.push(Point::new( + spline.control_points[i].x, + spline.control_points[i].y, + )); + i += 1; + } + i = 0; + let mut knots: Vec = Vec::new(); + for _a in &spline.knot_values { + knots.push(spline.knot_values[i]); + i += 1; + } + let curr_spline = bspline::BSpline::new( + spline.degree_of_curve.unsigned_abs() as usize, + points, + knots, + ); + 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 + let coordinates = { + let mut coords = Vec::with_capacity( + ((curr_spline.knot_domain().1 - curr_spline.knot_domain().0) / step) as usize + 1, + ); + let mut j: f64 = curr_spline.knot_domain().0; + i = 0; + while j < curr_spline.knot_domain().1 { + coords.push(Coordinate { + x: curr_spline.point(j).x, + y: -curr_spline.point(j).y, + }); + j += step; + i += 1; + } + coords + }; + + 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? + antialias: false, + style: "line-style:normal;line-weight:thin;filling:none;color:black".into(), + } + } +} + +impl From<&Solid> for Polygon { + fn from(solid: &Solid) -> Self { + Polygon { + coordinates: vec![ + Coordinate { + x: solid.first_corner.x, + y: -solid.first_corner.y, + }, + Coordinate { + x: solid.second_corner.x, + y: -solid.second_corner.y, + }, + Coordinate { + x: solid.third_corner.x, + y: -solid.third_corner.y, + }, + Coordinate { + x: solid.fourth_corner.x, + y: -solid.fourth_corner.y, + }, + ], + closed: true, + //in the original code antialias is always set to false...I'm guessing for performance + //reasons...I'm trying to think if there is a time we might want to turn it on? + antialias: false, + style: if solid.thickness > 0.5 { + "line-style:normal;line-weight:normal;filling:none;color:black" + } else { + "line-style:normal;line-weight:thin;filling:none;color:black" + } + .into(), + } + } +} + +impl From<&Polygon> for XMLElement { + fn from(poly: &Polygon) -> Self { + let mut poly_xml: XMLElement = XMLElement::new("polygon"); + + for (count, coord) in poly.coordinates.iter().enumerate() { + poly_xml.add_attribute(format!("x{}", (count + 1)), two_dec(coord.x)); + 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 + if !poly.closed { + poly_xml.add_attribute("closed", poly.closed); + } + + poly_xml.add_attribute("antialias", poly.antialias); + poly_xml.add_attribute("style", &poly.style); + poly_xml + } +} + +impl ScaleEntity for Polygon { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + self.coordinates.iter_mut().for_each(|coord| { + coord.x *= fact_x; + coord.y *= fact_y; + }); + } + + 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 + c1.x.partial_cmp(&c2.x) + .unwrap_or(std::cmp::Ordering::Greater) + }); + + if let Some(min_coord) = min_coord { + min_coord.x + } else { + 0.0 + } + } + + 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 + c1.x.partial_cmp(&c2.x).unwrap_or(std::cmp::Ordering::Less) + }); + + if let Some(max_coord) = max_coord { + max_coord.x + } else { + 0.0 + } + } + + 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 + c1.y.partial_cmp(&c2.y) + .unwrap_or(std::cmp::Ordering::Greater) + }); + + if let Some(min_coord) = min_coord { + min_coord.y + } else { + 0.0 + } + } + + 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 + c1.y.partial_cmp(&c2.y).unwrap_or(std::cmp::Ordering::Less) + }); + + if let Some(max_coord) = max_coord { + max_coord.y + } else { + 0.0 + } + } +} diff --git a/src/qelmt/text.rs b/src/qelmt/text.rs new file mode 100644 index 0000000..4e15f03 --- /dev/null +++ b/src/qelmt/text.rs @@ -0,0 +1,76 @@ +use super::{two_dec, FontInfo, ScaleEntity}; +use dxf::entities; +use hex_color::HexColor; +use simple_xml_builder::XMLElement; + +#[derive(Debug)] +pub struct Text { + rotation: f64, + value: String, + pub x: f64, + pub y: f64, + font: FontInfo, + color: HexColor, +} + +impl From<(&entities::Text, HexColor)> for Text { + fn from((txt, color): (&entities::Text, HexColor)) -> Self { + Text { + x: txt.location.x, + y: -txt.location.y, + rotation: if txt.rotation.abs().round() as i64 % 360 != 0 { + txt.rotation - 180.0 + } else { + 0.0 + }, + color, + font: if &txt.text_style_name == "STANDARD" { + FontInfo::default() + } else { + //txt.text_style_name.clone() + FontInfo::default() + }, + value: txt.value.clone(), + } + } +} + +impl From<&Text> for XMLElement { + fn from(txt: &Text) -> Self { + 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)); + txt_xml.add_attribute("rotation", two_dec(txt.rotation)); + txt_xml.add_attribute("color", txt.color.display_rgb()); + txt_xml.add_attribute("font", &txt.font); + txt_xml.add_attribute("text", &txt.value); + txt_xml + } +} + +impl ScaleEntity for Text { + fn scale(&mut self, fact_x: f64, fact_y: f64) { + self.x *= fact_x; + self.y *= fact_y; + //self.font.pixel_size *= fact; + self.font.point_size *= fact_x; + } + + fn left_bound(&self) -> f64 { + self.x + } + + fn top_bound(&self) -> f64 { + self.y + } + + fn right_bound(&self) -> f64 { + //need to be able to measure text size to get this + todo!() + } + + fn bot_bound(&self) -> f64 { + //need to be able to measure text size to get this + todo!() + } +}