diff --git a/Cargo.lock b/Cargo.lock
index dc08d4542e..00fe4ca454 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -978,11 +978,33 @@ dependencies = [
"ruzstd",
]
+[[package]]
+name = "craft-codepage"
+version = "0.1.0"
+source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2"
+
[[package]]
name = "craft-docpath"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb#9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb"
+[[package]]
+name = "craft-dxf"
+version = "0.1.0"
+source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2"
+dependencies = [
+ "craft-codepage",
+ "encoding_rs",
+ "kurbo 0.13.1",
+ "log",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "craft-eps"
+version = "0.1.0"
+source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2"
+
[[package]]
name = "craft-fonts-build"
version = "0.1.0"
@@ -1057,6 +1079,21 @@ dependencies = [
"unicode-normalization",
]
+[[package]]
+name = "craft-svg"
+version = "0.1.0"
+source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2"
+dependencies = [
+ "craft-xml",
+ "flate2",
+ "kurbo 0.13.1",
+ "roxmltree 0.21.2",
+ "thiserror 2.0.21",
+ "usvg 0.48.1",
+ "vectorcraft-geom 0.18.0",
+ "vectorcraft-pathops 0.18.0",
+]
+
[[package]]
name = "craft-ui"
version = "0.1.0"
@@ -1146,6 +1183,15 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "craft-xml"
+version = "0.1.0"
+source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2"
+dependencies = [
+ "quick-xml 0.41.0",
+ "thiserror 2.0.21",
+]
+
[[package]]
name = "crc32fast"
version = "1.5.3"
@@ -6386,13 +6432,14 @@ dependencies = [
"serde_json",
"vectorcraft-color",
"vectorcraft-doc",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
]
[[package]]
name = "vectorcraft-cad"
version = "0.24.1"
dependencies = [
+ "craft-dxf",
"image",
"kurbo 0.13.1",
"serde_json",
@@ -6400,8 +6447,8 @@ dependencies = [
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-effects",
- "vectorcraft-geom",
- "vectorcraft-pathops",
+ "vectorcraft-geom 0.24.1",
+ "vectorcraft-pathops 0.24.1",
"vectorcraft-text",
]
@@ -6418,7 +6465,7 @@ dependencies = [
"vectorcraft-doc",
"vectorcraft-engine",
"vectorcraft-format",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-mcp",
"vectorcraft-render",
"vectorcraft-svg",
@@ -6455,7 +6502,7 @@ dependencies = [
"thiserror 2.0.21",
"tiff",
"vectorcraft-color",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"zune-jpeg 0.5.15",
]
@@ -6470,8 +6517,8 @@ dependencies = [
"svgtypes 0.16.1",
"vectorcraft-color",
"vectorcraft-doc",
- "vectorcraft-geom",
- "vectorcraft-pathops",
+ "vectorcraft-geom 0.24.1",
+ "vectorcraft-pathops 0.24.1",
"vectorcraft-plugins",
"vectorcraft-text",
"vectorcraft-three-d",
@@ -6515,10 +6562,10 @@ dependencies = [
"vectorcraft-doc",
"vectorcraft-eps",
"vectorcraft-format",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-js",
"vectorcraft-metafile",
- "vectorcraft-pathops",
+ "vectorcraft-pathops 0.24.1",
"vectorcraft-pdf",
"vectorcraft-plugins",
"vectorcraft-render",
@@ -6537,6 +6584,7 @@ dependencies = [
name = "vectorcraft-eps"
version = "0.24.1"
dependencies = [
+ "craft-eps",
"craft-pdf",
"encoding_rs",
"fax",
@@ -6552,9 +6600,9 @@ dependencies = [
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-effects",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-metafile",
- "vectorcraft-pathops",
+ "vectorcraft-pathops 0.24.1",
"vectorcraft-pdf",
"vectorcraft-render",
"vectorcraft-testkit",
@@ -6573,10 +6621,20 @@ dependencies = [
"serde_json",
"thiserror 2.0.21",
"vectorcraft-doc",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-testkit",
]
+[[package]]
+name = "vectorcraft-geom"
+version = "0.18.0"
+source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
+dependencies = [
+ "kurbo 0.13.1",
+ "serde",
+ "thiserror 2.0.21",
+]
+
[[package]]
name = "vectorcraft-geom"
version = "0.24.1"
@@ -6611,7 +6669,7 @@ dependencies = [
"vectorcraft-doc",
"vectorcraft-engine",
"vectorcraft-format",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-render",
"vectorcraft-svg",
"vectorcraft-testkit",
@@ -6628,10 +6686,21 @@ dependencies = [
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-effects",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-text",
]
+[[package]]
+name = "vectorcraft-pathops"
+version = "0.18.0"
+source = "git+https://github.com/storytold/vectorcraft.git?rev=6b3a6b8788ebcb420d69449199cbdf323888d27e#6b3a6b8788ebcb420d69449199cbdf323888d27e"
+dependencies = [
+ "kurbo 0.13.1",
+ "linesweeper",
+ "thiserror 2.0.21",
+ "vectorcraft-geom 0.18.0",
+]
+
[[package]]
name = "vectorcraft-pathops"
version = "0.24.1"
@@ -6640,7 +6709,7 @@ dependencies = [
"linesweeper",
"proptest",
"thiserror 2.0.21",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-testkit",
]
@@ -6673,7 +6742,7 @@ dependencies = [
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-effects",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-testkit",
"vectorcraft-text",
"weezl 0.2.1",
@@ -6692,7 +6761,7 @@ dependencies = [
"thiserror 2.0.21",
"vectorcraft-color",
"vectorcraft-doc",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"wasmi",
"wat",
"web-time",
@@ -6717,7 +6786,7 @@ dependencies = [
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-effects",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-plugins",
"vectorcraft-testkit",
"vectorcraft-text",
@@ -6741,6 +6810,7 @@ name = "vectorcraft-svg"
version = "0.24.1"
dependencies = [
"craft-docpath",
+ "craft-svg",
"flate2",
"image",
"kurbo 0.13.1",
@@ -6757,8 +6827,8 @@ dependencies = [
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-effects",
- "vectorcraft-geom",
- "vectorcraft-pathops",
+ "vectorcraft-geom 0.24.1",
+ "vectorcraft-pathops 0.24.1",
"vectorcraft-testkit",
"vectorcraft-text",
]
@@ -6776,7 +6846,7 @@ dependencies = [
"vectorcraft-doc",
"vectorcraft-engine",
"vectorcraft-format",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-render",
"vectorcraft-svg",
]
@@ -6796,7 +6866,7 @@ dependencies = [
"unicode-script",
"unicode-segmentation",
"vectorcraft-doc",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vello_cpu",
"windows-registry",
"write-fonts",
@@ -6806,8 +6876,8 @@ dependencies = [
name = "vectorcraft-three-d"
version = "0.24.1"
dependencies = [
- "vectorcraft-geom",
- "vectorcraft-pathops",
+ "vectorcraft-geom 0.24.1",
+ "vectorcraft-pathops 0.24.1",
]
[[package]]
@@ -6822,8 +6892,8 @@ dependencies = [
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-effects",
- "vectorcraft-geom",
- "vectorcraft-pathops",
+ "vectorcraft-geom 0.24.1",
+ "vectorcraft-pathops 0.24.1",
"vectorcraft-text",
]
@@ -6836,7 +6906,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.21",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
]
[[package]]
@@ -6865,7 +6935,7 @@ dependencies = [
"vectorcraft-engine",
"vectorcraft-eps",
"vectorcraft-format",
- "vectorcraft-geom",
+ "vectorcraft-geom 0.24.1",
"vectorcraft-pdf",
"vectorcraft-plugins",
"vectorcraft-render",
diff --git a/Cargo.toml b/Cargo.toml
index 5bed166e17..c7856d589e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,12 @@ craft-vectorcraft-format = { git = "https://github.com/storytold/craft-libs", re
# craft-update: app-specific pretend versions apply to startup and manual checks.
# Native Affinity documents (craft-libs#914).
craft-affinity = { git = "https://github.com/storytold/craft-libs", rev = "228f6d0c50f1fdee4c5124603602993fc79d4c9d" }
+# SVG parsing, limits, SVGZ and writer (craft-libs#778).
+craft-svg = { git = "https://github.com/storytold/craft-libs", rev = "6d620e971e820b517b11a99cad040ebac670e1e2", features = ["tree"] }
+# EPS container, previews, ASCII85 and DSC comments (craft-libs#778).
+craft-eps = { git = "https://github.com/storytold/craft-libs", rev = "6d620e971e820b517b11a99cad040ebac670e1e2" }
+# DXF tags, reader and writer (craft-libs#778, #926, #927).
+craft-dxf = { git = "https://github.com/storytold/craft-libs", rev = "6d620e971e820b517b11a99cad040ebac670e1e2" }
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "e98299546f5c5c3c22388d16507f8b5e61a5f280", features = ["egui"] }
# internal
vectorcraft-geom = { path = "crates/geom" }
diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs
index fdbec59bfc..3397ada7a5 100644
--- a/apps/vectorcraft/src/main.rs
+++ b/apps/vectorcraft/src/main.rs
@@ -373,11 +373,14 @@ fn main() -> std::process::ExitCode {
// Display Server › X11 opens the window through XWayland instead, where file drops work (#1151),
// and the other choices do so when a pen is attached, where the pen draws (#2028, #4967).
#[cfg(target_os = "linux")]
+ let mut forced_to_x11 = false;
+ #[cfg(target_os = "linux")]
let options = {
let mut options = options;
let pref = saved.as_ref().and_then(|ui| ui.engine_prefs.get("linuxDisplayServer")).and_then(serde_json::Value::as_str);
if xwayland::wanted(pref, |name| std::env::var_os(name), xwayland::pen_attached) {
if xwayland::x11_libraries_found() {
+ forced_to_x11 = true;
if pref != Some("x11") {
log::info!("pen found: opening the window through XWayland, where the pen works (VECTORCRAFT_NATIVE_WAYLAND=1 keeps Wayland)");
} else {
@@ -393,6 +396,11 @@ fn main() -> std::process::ExitCode {
}
options
};
+ #[cfg(not(target_os = "linux"))]
+ let forced_to_x11 = false;
+ // Preferences › User Interface notes under System Title Bar when the desktop draws the frame
+ // with it on or off.
+ let desktop_keeps_frame = titlebar::desktop_keeps_frame(forced_to_x11);
// With Windows Ink, winit reports a pen as a touch and drops its barrel button and which end
// is down; the message hook notes each pointer contact and whether it starts with the barrel
// button held, and writes the pen's end into the pen feed (`windows_stylus`). It also notes the
@@ -595,6 +603,7 @@ fn main() -> std::process::ExitCode {
app.custom_titlebar = custom_titlebar;
app.caption_layout = caption_layout;
app.desktop_frame = !custom_titlebar;
+ app.desktop_keeps_frame = desktop_keeps_frame;
if let Some(port) = control_port {
let rx = control_server::start(port, cc.egui_ctx.clone());
app = app.with_control(rx).with_automation_roots(roots.clone());
diff --git a/apps/vectorcraft/src/tests.rs b/apps/vectorcraft/src/tests.rs
index 99ca880bc9..1994569c0c 100644
--- a/apps/vectorcraft/src/tests.rs
+++ b/apps/vectorcraft/src/tests.rs
@@ -346,6 +346,34 @@ fn the_titlebar_variable_decides_the_frame_before_the_window_opens() {
}
}
+/// Preferences says under System Title Bar that the desktop draws the frame with it on or off
+/// exactly where the start frame without VectorCraft Title Bar on Every Desktop is the desktop's
+/// with System Title Bar off as well as on and `VECTORCRAFT_TITLEBAR` is unset: Linux and the BSDs
+/// only.
+#[test]
+fn the_desktop_keeps_the_frame_where_system_title_bar_does_not_change_it() {
+ use crate::titlebar::desktop_keeps_frame_from;
+ use craft_window::{Frame, Layout};
+ let linux = cfg!(not(any(target_os = "macos", target_os = "windows")));
+ for probed in [Frame::Desktop, Frame::App(Layout::default())] {
+ for forced_to_x11 in [false, true] {
+ let frame = super::start_frame(probed.clone(), None, forced_to_x11, false);
+ let either_way = !super::custom_titlebar_from(&frame, false) && !super::custom_titlebar_from(&frame, true);
+ assert_eq!(desktop_keeps_frame_from(&probed, None, forced_to_x11), linux && either_way, "{probed:?} {forced_to_x11}");
+ }
+ }
+ // KDE, Sway and X11, and GNOME through XWayland, keep the desktop's frame; GNOME on Wayland does not.
+ assert_eq!(desktop_keeps_frame_from(&Frame::Desktop, None, false), linux);
+ assert_eq!(desktop_keeps_frame_from(&Frame::App(Layout::default()), None, true), linux);
+ assert!(!desktop_keeps_frame_from(&Frame::App(Layout::default()), None, false));
+ // While `VECTORCRAFT_TITLEBAR` chooses the frame, System Title Bar does not apply and there is no note.
+ for (frame, overridden) in [(Frame::App(Layout::default()), Some(true)), (Frame::Desktop, Some(false))] {
+ for forced_to_x11 in [false, true] {
+ assert!(!desktop_keeps_frame_from(&frame, overridden, forced_to_x11), "{overridden:?} {forced_to_x11}");
+ }
+ }
+}
+
/// The file extensions the macOS bundle declares: its document types and its own exported type.
fn plist_extensions(plist: &str) -> Vec<&str> {
["CFBundleTypeExtensions", "public.filename-extension"]
diff --git a/apps/vectorcraft/src/titlebar.rs b/apps/vectorcraft/src/titlebar.rs
index 88dce26897..cb98fa2648 100644
--- a/apps/vectorcraft/src/titlebar.rs
+++ b/apps/vectorcraft/src/titlebar.rs
@@ -191,6 +191,25 @@ pub(crate) fn custom_titlebar(saved: Option<&vectorcraft_ui_egui::UiState>) -> (
(custom_titlebar_from(&frame, opted_into_system), caption_layout_from(&frame))
}
+/// Whether the desktop would draw this window's frame with System Title Bar on or off while
+/// VectorCraft Title Bar on Every Desktop is off: `VECTORCRAFT_TITLEBAR` is not set to `app`,
+/// `system` or `desktop` (`overridden` is `None`), and the probe chose
+/// [`craft_window::Frame::Desktop`] or the window opens through XWayland (`forced_to_x11`)
+/// ([`app_draws_frame`]). The saved VectorCraft Title Bar on Every Desktop is left out, so the value
+/// stays `true` on such a desktop while that preference gives the app the frame. Always `false` on
+/// macOS and Windows, where System Title Bar decides the frame by itself.
+pub(crate) fn desktop_keeps_frame_from(probed: &craft_window::Frame, overridden: Option, forced_to_x11: bool) -> bool {
+ let probed_app = matches!(probed, craft_window::Frame::App(_));
+ cfg!(not(any(target_os = "macos", target_os = "windows"))) && overridden.is_none() && !app_draws_frame(probed_app, None, forced_to_x11, false)
+}
+
+/// [`desktop_keeps_frame_from`] for this start's probe, given whether the launch path opens the
+/// window through XWayland.
+pub(crate) fn desktop_keeps_frame(forced_to_x11: bool) -> bool {
+ let choice = probed_choice();
+ desktop_keeps_frame_from(choice.frame(), choice.overridden(), forced_to_x11)
+}
+
pub(crate) fn native_options(custom_titlebar: bool) -> eframe::NativeOptions {
eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
diff --git a/crates/brush/src/colorize.rs b/crates/brush/src/colorize.rs
index 2cf9f43c97..00edeb3113 100644
--- a/crates/brush/src/colorize.rs
+++ b/crates/brush/src/colorize.rs
@@ -114,51 +114,9 @@ fn strip_brushes(n: &mut Node) {
}
}
-/// Mix every solid paint under `n` towards `color` by `amount` (0..1) — the Symbol Stainer.
-pub fn tint_node(n: &mut Node, color: &Color, amount: f32) {
- let amount = amount.clamp(0.0, 1.0);
- for it in &mut n.appearance.items {
- let p = match it {
- AppearanceItem::Fill(f) => &mut f.paint,
- AppearanceItem::Stroke(s) => &mut s.paint,
- };
- if let Paint::Solid { color: c, .. } = p {
- *p = Paint::solid(c.lerp(color, amount));
- }
- }
- if let vectorcraft_doc::NodeKind::Text(t) = &mut n.kind {
- for r in &mut t.runs {
- for p in [&mut r.style.fill, &mut r.style.stroke] {
- if let Paint::Solid { color: c, .. } = p {
- *p = Paint::solid(c.lerp(color, amount));
- }
- }
- }
- }
- if let Some(ch) = n.children_mut() {
- for c in ch.iter_mut() {
- tint_node(Arc::make_mut(c), color, amount);
- }
- }
-}
-
-/// The stain instance `inst` puts on its symbol's art: its visible fill colour and that fill's
-/// opacity (how far the art mixes towards it). `None` for an unstained instance, including one
-/// whose fill has an opacity of 0, so its art keeps its paints and their swatch links.
-pub fn stain(inst: &Node) -> Option<(Color, f32)> {
- let f = inst.appearance.fill().filter(|f| f.visible && f.opacity > 0.0)?;
- Some((f.paint.color()?, f.opacity))
-}
-
-/// A symbol's art for instance `inst`: with the instance's own appearance changes to it (a dynamic
-/// symbol's, [`vectorcraft_doc::symbol_overrides`]), stained by the instance's fill ([`stain`]).
-pub fn instance_art(art: &Node, inst: &Node) -> Node {
- let mut a = vectorcraft_doc::symbol_overrides::dynamic_art(art, inst);
- if let Some((c, amount)) = stain(inst) {
- tint_node(&mut a, &c, amount);
- }
- a
-}
+/// The Symbol Stainer's mix, an instance's stain and its stained art, from
+/// [`vectorcraft_doc::symbol_overrides`], which the effects that reshape an instance also read.
+pub use vectorcraft_doc::symbol_overrides::{instance_art, stain, tint_node};
/// The solid colours of the fills and strokes under `nodes`, most used first (then in the order
/// they first appear): the colours Brush Options offers as Hue Shift's key colour, the first being
diff --git a/crates/cad/Cargo.toml b/crates/cad/Cargo.toml
index 34f77ec929..e529e1d4e2 100644
--- a/crates/cad/Cargo.toml
+++ b/crates/cad/Cargo.toml
@@ -9,6 +9,7 @@ description = "VectorCraft CAD interchange: a hand-written ASCII DXF writer (R12
workspace = true
[dependencies]
+craft-dxf = { workspace = true }
vectorcraft-geom = { workspace = true }
vectorcraft-color = { workspace = true }
vectorcraft-doc = { workspace = true }
diff --git a/crates/cad/src/aci.rs b/crates/cad/src/aci.rs
index 63fbb180f9..39818a78ef 100644
--- a/crates/cad/src/aci.rs
+++ b/crates/cad/src/aci.rs
@@ -1,50 +1,11 @@
-//! The 256-colour index palette CAD apps share, generated rather than tabulated: indices 1–9 are
-//! the standard colours, 10–249 run through 24 hues at five brightnesses each in a full and a
-//! half-saturated tone, and 250–255 are greys.
+//! The colour index palette is `craft_dxf::model::aci`; here are the indices each colour depth
+//! of the writer may use.
use crate::ColorDepth;
-/// The nine standard colours (index 7 shows white on a dark background and black on paper).
-const STANDARD: [[u8; 3]; 9] =
- [[255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], [255, 255, 255], [128, 128, 128], [192, 192, 192]];
-
-/// The brightness of the five shades of each hue.
-const SHADES: [f64; 5] = [1.0, 0.65, 0.5, 0.3, 0.15];
-
/// The RGB of colour index `i` (1–255; anything else is index 7's white).
pub fn aci_rgb(i: u8) -> [u8; 3] {
- match i {
- 1..=9 => STANDARD[usize::from(i - 1)],
- 10..=249 => {
- let hue = f64::from(i / 10 - 1) * 15.0;
- let k = i % 10;
- let v = SHADES[usize::from(k / 2)];
- let pastel = k % 2 == 1;
- hue_rgb(hue).map(|f| {
- let f = if pastel { f + (1.0 - f) / 2.0 } else { f };
- (255.0 * v * f).floor() as u8
- })
- }
- 250..=255 => {
- let g = (51.0 + 204.0 * f64::from(i - 250) / 5.0).floor() as u8;
- [g; 3]
- }
- _ => STANDARD[6],
- }
-}
-
-/// The fully saturated, full-brightness colour of `hue` degrees as 0–1 channels.
-fn hue_rgb(hue: f64) -> [f64; 3] {
- let h = hue.rem_euclid(360.0) / 60.0;
- let x = 1.0 - (h % 2.0 - 1.0).abs();
- match h as u8 {
- 0 => [1.0, x, 0.0],
- 1 => [x, 1.0, 0.0],
- 2 => [0.0, 1.0, x],
- 3 => [0.0, x, 1.0],
- 4 => [x, 0.0, 1.0],
- _ => [1.0, 0.0, x],
- }
+ craft_dxf::model::aci::rgb(i)
}
/// The indices a colour depth may use (true colour: all of them, for the fallback index).
@@ -59,37 +20,13 @@ fn candidates(depth: ColorDepth) -> Box> {
/// The index of `depth` nearest to `rgb` (squared RGB distance; ties go to the lower index).
/// Index 7 stands for both white and black, as CAD apps draw it in the background's contrast.
pub fn nearest_aci(rgb: [u8; 3], depth: ColorDepth) -> u8 {
- let dist = |c: [u8; 3]| rgb.iter().zip(c).map(|(a, b)| (i32::from(*a) - i32::from(b)).pow(2)).sum::();
- candidates(depth).min_by_key(|i| if *i == 7 { dist([255; 3]).min(dist([0; 3])) } else { dist(aci_rgb(*i)) }).unwrap_or(7)
+ craft_dxf::model::aci::nearest(rgb, candidates(depth))
}
#[cfg(test)]
mod tests {
use super::*;
- #[test]
- fn the_palette_matches_its_known_entries() {
- let known: [(u8, [u8; 3]); 14] = [
- (1, [255, 0, 0]),
- (10, [255, 0, 0]),
- (11, [255, 127, 127]),
- (12, [165, 0, 0]),
- (13, [165, 82, 82]),
- (17, [76, 38, 38]),
- (19, [38, 19, 19]),
- (20, [255, 63, 0]),
- (21, [255, 159, 127]),
- (30, [255, 127, 0]),
- (140, [0, 191, 255]),
- (250, [51, 51, 51]),
- (252, [132, 132, 132]),
- (255, [255, 255, 255]),
- ];
- for (i, rgb) in known {
- assert_eq!(aci_rgb(i), rgb, "index {i}");
- }
- }
-
#[test]
fn nearest_index_by_depth() {
assert_eq!(nearest_aci([255, 0, 0], ColorDepth::Aci8), 1);
diff --git a/crates/cad/src/import.rs b/crates/cad/src/import.rs
index f1f4104863..9216c57c29 100644
--- a/crates/cad/src/import.rs
+++ b/crates/cad/src/import.rs
@@ -1,4 +1,4 @@
-//! DXF import: an ASCII DXF drawing of any version as a document.
+//! DXF import: a DXF drawing (ASCII or binary) of any version as a document.
//!
//! The drawing's model space (or a paper layout) comes in at a ratio (1 unit of the art = N
//! drawing units; by default the drawing's own unit at 1:1) or fitted to an artboard, centred
@@ -10,7 +10,8 @@
//! lineweights, linetypes and transparency come through layers and blocks as CAD apps resolve
//! them. What can't come in is listed in the warnings.
//!
-//! - `reader`: the file's text, group codes, sections, tables, blocks and layouts.
+//! - `reader`: what the import uses of the drawing `craft_dxf::model` reads: header, tables,
+//! blocks and layouts.
//! - `entity`: entities to items (geometry and properties).
//! - `curve`: bulges, arcs, ellipses and NURBS as Bézier paths.
//! - `text`: text values to plain text.
@@ -31,9 +32,6 @@ use crate::{DxfVersion, INSUNITS, SCALE_RANGE};
/// Letter, the box Fit to Artboard fills unless told otherwise (points).
pub const FIT_TO: (f64, f64) = (612.0, 792.0);
-/// Why a binary DXF can't be opened.
-pub const BINARY: &str = "this is a binary DXF file, which can't be read: save it from the CAD app as an ASCII DXF and open that";
-
/// How a drawing comes in.
#[derive(Clone, Debug, PartialEq)]
pub struct ImportOptions {
@@ -104,15 +102,20 @@ pub fn is_dxf(bytes: &[u8]) -> bool {
false
}
-/// The pairs of a DXF file and where it is damaged (binary and DWG files are refused).
-fn text(bytes: &[u8]) -> Result, String> {
- if bytes.starts_with(reader::BINARY_SENTINEL) {
- return Err(BINARY.into());
- }
+/// The tags of a DXF file and where it is damaged (DWG files are refused).
+fn tags(bytes: &[u8]) -> Result<(Vec, Option), String> {
if bytes.starts_with(b"AC10") || bytes.starts_with(b"AC1.") {
- return Err("this is a DWG drawing, which can't be read: save it from the CAD app as an ASCII DXF and open that".into());
+ return Err("this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that".into());
}
- Ok(reader::decode(bytes))
+ let (tags, damaged) = craft_dxf::parse_recovering(bytes).map_err(|e| match e {
+ craft_dxf::DxfError::TooLarge { .. } => "the drawing is too large to open".to_string(),
+ _ if bytes.starts_with(reader::BINARY_SENTINEL) => "this binary DXF file is damaged or cut short".to_string(),
+ _ => "not a DXF drawing".to_string(),
+ })?;
+ if tags.is_empty() {
+ return Err("not a DXF drawing".into());
+ }
+ Ok((tags, damaged.map(|line| format!("the file is damaged at line {line}: what follows is left out"))))
}
/// The unit of `$INSUNITS` code `code` and the scale that shows the drawing 1:1 in it.
@@ -161,9 +164,8 @@ fn units_name(code: i64) -> &'static str {
/// Read what `bytes` holds: its version, units, layouts and layers.
pub fn info(bytes: &[u8]) -> Result {
- let text = text(bytes)?;
- let (pairs, _) = reader::pairs(&text)?;
- let d = reader::Drawing::read(&pairs);
+ let (tags, _) = tags(bytes)?;
+ let d = reader::Drawing::read(craft_dxf::model::Document::read(&tags));
if d.sections == 0 {
return Err("not a DXF drawing".into());
}
@@ -180,7 +182,7 @@ pub fn info(bytes: &[u8]) -> Result {
})
}
-/// Import an ASCII DXF drawing with `o`.
+/// Import a DXF drawing with `o`.
pub fn import(bytes: &[u8], o: &ImportOptions) -> Result {
let (w, h) = o.fit_to;
if !(w > 0.0 && h > 0.0 && w.max(h) <= build::MAX_EXTENT) {
@@ -191,9 +193,8 @@ pub fn import(bytes: &[u8], o: &ImportOptions) -> Result {
{
return Err(format!("scale must be a number from {} to {}, not {s}", SCALE_RANGE.start(), SCALE_RANGE.end()));
}
- let text = text(bytes)?;
- let (pairs, damaged) = reader::pairs(&text)?;
- let d = reader::Drawing::read(&pairs);
+ let (tags, damaged) = tags(bytes)?;
+ let d = reader::Drawing::read(craft_dxf::model::Document::read(&tags));
if d.sections == 0 {
return Err("not a DXF drawing".into());
}
diff --git a/crates/cad/src/import/entity.rs b/crates/cad/src/import/entity.rs
index 59e1283cce..309a4ea228 100644
--- a/crates/cad/src/import/entity.rs
+++ b/crates/cad/src/import/entity.rs
@@ -7,9 +7,10 @@ use kurbo::{BezPath, PathEl};
use vectorcraft_doc::Justify;
use vectorcraft_geom::{Affine, Point, Vec2};
+use craft_dxf::model::{self, EntityRecord, Groups, HatchEdge, HatchPath};
+
use super::curve::{self, Vertex};
-use super::reader::{Groups, Pair, Record, groups, number};
-use crate::aci::aci_rgb;
+use super::reader::{aci_paper, point};
/// How far below the baseline the lowest descender reaches, as a fraction of the cap height.
const DESCENT: f64 = 0.3;
@@ -25,21 +26,6 @@ const MAX_COORD: f64 = 1e12;
/// factor 1, the 1 to 10,000% horizontal scale the Character panel allows.
const SPAN_SCALE: (f64, f64) = (0.01, 100.0);
-/// The colour of index `i` on paper: index 7 (white on a CAD screen) is black.
-pub(crate) fn aci_paper(i: u64) -> [u8; 3] {
- match u8::try_from(i) {
- Ok(7) | Err(_) => [0; 3],
- Ok(i) => aci_rgb(i),
- }
-}
-
-/// The true colour (group 420, `0xRRGGBB`) of an entity or layer.
-pub(crate) fn true_color(g: &[Pair<'_>]) -> Option<[u8; 3]> {
- let v = g.int(420)?;
- let [_, r, gr, b] = u32::try_from(v & 0xFF_FFFF).ok()?.to_be_bytes();
- Some([r, gr, b])
-}
-
/// An entity's colour.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum Col {
@@ -79,30 +65,30 @@ pub(crate) struct Props {
}
impl Props {
- fn read(g: &[Pair<'_>]) -> Self {
- let color = match (true_color(g), g.int(62)) {
- (Some(rgb), _) => Col::Rgb(rgb),
- (None, Some(0)) => Col::ByBlock,
- (None, None | Some(256..)) => Col::ByLayer,
- (None, Some(i)) => Col::Rgb(aci_paper(i.unsigned_abs())),
+ fn read(c: &model::Common) -> Self {
+ let color = match c.color {
+ model::Color::Rgb(rgb) => Col::Rgb(rgb),
+ model::Color::ByBlock => Col::ByBlock,
+ model::Color::ByLayer => Col::ByLayer,
+ model::Color::Index(i) => Col::Rgb(aci_paper(i)),
};
- let lineweight = match g.int(370) {
- Some(-2) => Lw::ByBlock,
- Some(-3) => Lw::Default,
- Some(v @ 0..=211) => Lw::Hundredths(v as i32),
+ let lineweight = match c.lineweight {
+ model::Lineweight::ByBlock => Lw::ByBlock,
+ model::Lineweight::Default => Lw::Default,
+ model::Lineweight::Hundredths(v @ 0..=211) => Lw::Hundredths(i32::from(v)),
_ => Lw::ByLayer,
};
- let alpha = match g.int(440) {
- Some(v) if v & 0x0200_0000 != 0 => Alpha::Opacity((v & 0xFF) as f32 / 255.0),
- Some(v) if v & 0x0100_0000 != 0 => Alpha::ByBlock,
- _ => Alpha::ByLayer,
+ let alpha = match c.transparency {
+ model::Transparency::Alpha(a) => Alpha::Opacity(f32::from(a) / 255.0),
+ model::Transparency::ByBlock => Alpha::ByBlock,
+ model::Transparency::ByLayer => Alpha::ByLayer,
};
Self {
- layer: g.name(8).unwrap_or("0").to_string(),
+ layer: c.layer.clone(),
color,
lineweight,
- linetype: g.name(6).unwrap_or("BYLAYER").to_uppercase(),
- ltscale: g.num(48).filter(|v| *v > 0.0).unwrap_or(1.0),
+ linetype: c.linetype.as_deref().unwrap_or("BYLAYER").to_uppercase(),
+ ltscale: c.ltscale,
alpha,
}
}
@@ -209,90 +195,87 @@ impl Converter {
}
/// The items of `records` (an entity section or a block), in drawing order.
- pub fn items(&mut self, records: &[Record<'_>]) -> Vec- {
+ pub fn items(&mut self, records: &[EntityRecord<'_>]) -> Vec
- {
let mut out = Vec::new();
- for group in groups(records) {
- let Some(head) = group.first() else { continue };
+ for e in records {
+ let m = model::Entity::read(e);
// Invisible entities.
- if head.g.int(60) == Some(1) {
+ if !m.common.visible {
continue;
}
- match self.geom(head, group) {
+ match self.geom(e, &m) {
Some(geom) if !in_range(&geom) => self.warn(FAR_AWAY),
- Some(geom) => out.push(Item { props: Props::read(head.g), geom }),
- None if is_known(head.kind) => {}
- None => self.skip(head.kind),
+ Some(geom) => out.push(Item { props: Props::read(&m.common), geom }),
+ None if is_known(e.kind) => {}
+ None => self.skip(e.kind),
}
}
out
}
- fn geom(&mut self, head: &Record<'_>, group: &[Record<'_>]) -> Option {
- let g = head.g;
+ fn geom(&mut self, e: &EntityRecord<'_>, m: &model::Entity) -> Option {
+ let g = e.groups();
let path = |bp: BezPath| Some(Geom::Path { bp, fill: false, width: 0.0 });
- match head.kind {
- "LINE" => {
+ let ocs = ocs(m.common.extrusion);
+ match &m.kind {
+ model::EntityKind::Line { start, end } => {
let mut bp = BezPath::new();
- bp.move_to(g.point(10));
- bp.line_to(g.point(11));
+ bp.move_to(point(*start));
+ bp.line_to(point(*end));
path(bp)
}
- "CIRCLE" => path(transformed(curve::arc(g.point(10), g.num(40)?, 0.0, std::f64::consts::TAU)?, ocs(g))),
- "ARC" => {
- let (start, end) = (g.num_or(50, 0.0), g.num_or(51, 360.0));
- let sweep = ccw_sweep(start, end);
- path(transformed(curve::arc(g.point(10), g.num(40)?, start.to_radians(), sweep.to_radians())?, ocs(g)))
+ model::EntityKind::Circle { center, radius } => path(transformed(curve::arc(point(*center), *radius, 0.0, std::f64::consts::TAU)?, ocs)),
+ model::EntityKind::Arc { center, radius, start_angle, end_angle } => {
+ let sweep = ccw_sweep(*start_angle, *end_angle);
+ path(transformed(curve::arc(point(*center), *radius, start_angle.to_radians(), sweep.to_radians())?, ocs))
}
- "ELLIPSE" => path(ellipse(g)?),
- "LWPOLYLINE" => lwpolyline(g),
- "POLYLINE" => self.polyline(g, group.get(1..).unwrap_or(&[])),
- "SPLINE" => path(spline(g)?),
- "SOLID" | "TRACE" => {
- let [a, b, c, d] = [g.point(10), g.point(11), g.point(12), g.point_opt(13).unwrap_or(g.point(12))];
+ model::EntityKind::Ellipse { center, major, ratio, start, end } => {
+ path(ellipse(m.common.extrusion, *center, *major, *ratio, *start, *end)?)
+ }
+ model::EntityKind::LwPolyline(p) => lwpolyline(p, ocs),
+ model::EntityKind::Polyline(p) => self.polyline(p, ocs),
+ model::EntityKind::Spline(s) => path(spline(s)?),
+ model::EntityKind::Solid { corners } | model::EntityKind::Trace { corners } => {
+ let [a, b, c, d] = corners.map(point);
// The corners zigzag: the outline runs 1, 2, 4, 3.
- Some(Geom::Path { bp: transformed(polygon(&[a, b, d, c]), ocs(g)), fill: true, width: 0.0 })
+ Some(Geom::Path { bp: transformed(polygon(&[a, b, d, c]), ocs), fill: true, width: 0.0 })
}
- "3DFACE" => {
- let [a, b, c, d] = [g.point(10), g.point(11), g.point(12), g.point_opt(13).unwrap_or(g.point(12))];
- path(polygon(&[a, b, c, d]))
- }
- "HATCH" => self.hatch(g),
- "TEXT" | "ATTRIB" => text(g, head.kind == "ATTRIB").map(Geom::Text),
- "MTEXT" => mtext(g).map(Geom::Text),
- "INSERT" => self.insert(g, group.get(1..).unwrap_or(&[])),
+ model::EntityKind::Face3d { corners, .. } => path(polygon(&corners.map(point))),
+ model::EntityKind::Hatch(h) => self.hatch(h, ocs),
+ model::EntityKind::Text(t) => text(t, &g, false, ocs).map(Geom::Text),
+ model::EntityKind::MText(t) => mtext(t, e.tags).map(Geom::Text),
+ model::EntityKind::Insert(ins) => self.insert(ins, &e.followers, ocs),
// A dimension draws the anonymous block of its lines, arrows and text.
- "DIMENSION" => Some(Geom::Insert(InsertItem { block: g.name(2)?.to_uppercase(), cells: vec![Affine::IDENTITY], attribs: vec![] })),
+ model::EntityKind::Dimension(d) => {
+ Some(Geom::Insert(InsertItem { block: d.block.as_ref()?.to_uppercase(), cells: vec![Affine::IDENTITY], attribs: vec![] }))
+ }
_ => None,
}
}
- fn polyline(&mut self, g: &[Pair<'_>], rest: &[Record<'_>]) -> Option {
- let flags = g.int_or(70, 0);
- if flags & (16 | 64) != 0 {
+ fn polyline(&mut self, p: &model::Polyline, ocs: Affine) -> Option {
+ if p.is_mesh() || p.is_polyface() {
self.warn(MESHES);
return None;
}
// Spline frame control points and polyface faces aren't on the line.
- let verts: Vec = rest
- .iter()
- .filter(|r| r.kind == "VERTEX" && r.g.int_or(70, 0) & (16 | 128) == 0)
- .map(|r| Vertex { p: r.g.point(10), bulge: r.g.num_or(42, 0.0) })
- .collect();
- let bp = curve::polyline(&verts, flags & 1 != 0)?;
+ let verts: Vec =
+ p.vertices.iter().filter(|v| v.flags & (16 | 128) == 0).map(|v| Vertex { p: point(v.location), bulge: v.bulge }).collect();
+ let bp = curve::polyline(&verts, p.closed())?;
// 3D polylines are in world coordinates; 2D ones in their plane's.
- let bp = if flags & 8 != 0 { bp } else { transformed(bp, ocs(g)) };
- Some(Geom::Path { bp, fill: false, width: g.num_or(40, 0.0).max(0.0) })
+ let bp = if p.is_3d() { bp } else { transformed(bp, ocs) };
+ Some(Geom::Path { bp, fill: false, width: p.start_width.max(0.0) })
}
- fn insert(&mut self, g: &[Pair<'_>], rest: &[Record<'_>]) -> Option {
- let block = g.name(2)?.to_uppercase();
- let (sx, sy) = (g.num_or(41, 1.0), g.num_or(42, 1.0));
+ fn insert(&mut self, ins: &model::Insert, followers: &[model::Record<'_>], ocs: Affine) -> Option {
+ let block = Some(ins.block.trim()).filter(|b| !b.is_empty())?.to_uppercase();
+ let [sx, sy, _] = ins.scale;
if sx.abs() < 1e-12 || sy.abs() < 1e-12 {
return None;
}
- let place = ocs(g) * Affine::translate(g.point(10).to_vec2()) * Affine::rotate(g.num_or(50, 0.0).to_radians());
- let (cols, rows) = (g.int_or(70, 1).max(1), g.int_or(71, 1).max(1));
- let (cs, rs) = (g.num_or(44, 0.0), g.num_or(45, 0.0));
+ let place = ocs * Affine::translate(point(ins.insert).to_vec2()) * Affine::rotate(ins.rotation.to_radians());
+ let (cols, rows) = (ins.columns, ins.rows);
+ let (cs, rs) = (ins.column_spacing, ins.row_spacing);
let (cols, rows) = if cols.saturating_mul(rows) > MAX_CELLS {
self.warn(BIG_ARRAY);
(cols.min(MAX_CELLS), (MAX_CELLS / cols.min(MAX_CELLS)).max(1).min(rows))
@@ -312,27 +295,41 @@ impl Converter {
.flat_map(|r| (0..cols).map(move |c| (r, c)))
.map(|(r, c)| place * Affine::translate((c as f64 * cs, r as f64 * rs)) * Affine::scale_non_uniform(sx, sy))
.collect();
- let attribs = self.items(rest);
+ let attribs = self.attribs(followers);
Some(Geom::Insert(InsertItem { block, cells, attribs }))
}
- fn hatch(&mut self, g: &[Pair<'_>]) -> Option {
- let start = g.iter().position(|p| p.code == 91)?;
- let count = g.get(start).and_then(|p| number(p.value)).filter(|n| *n >= 0.0)? as usize;
- let mut c = Cursor { g, i: start + 1 };
+ /// An insert's attributes as text.
+ fn attribs(&mut self, followers: &[model::Record<'_>]) -> Vec
- {
+ let mut out = Vec::new();
+ for r in followers {
+ let c = model::Common::read(r.tags);
+ if !c.visible {
+ continue;
+ }
+ let a = model::Attrib::read("ATTRIB", r.tags);
+ match text(&a.text, &r.groups(), true, ocs(c.extrusion)) {
+ Some(t) if !in_range(&Geom::Text(t.clone())) => self.warn(FAR_AWAY),
+ Some(t) => out.push(Item { props: Props::read(&c), geom: Geom::Text(t) }),
+ None => {}
+ }
+ }
+ out
+ }
+
+ fn hatch(&mut self, h: &model::Hatch, ocs: Affine) -> Option {
let mut bp = BezPath::new();
- for _ in 0..count {
- let Some(path) = boundary(&mut c) else { break };
+ for l in &h.loops {
+ let Some(path) = boundary(&l.path) else { break };
bp.extend(path);
}
if bp.elements().is_empty() {
return None;
}
- let bp = transformed(bp, ocs(g));
- let solid = g.int_or(70, 0) == 1;
- if g.int_or(450, 0) == 1 {
+ let bp = transformed(bp, ocs);
+ if h.gradient.is_some() {
self.warn(GRADIENT_HATCH);
- } else if !solid {
+ } else if !h.solid {
self.warn(PATTERN_HATCH);
return Some(Geom::Path { bp, fill: false, width: 0.0 });
}
@@ -401,217 +398,104 @@ fn polygon(pts: &[Point]) -> BezPath {
bp
}
-/// The extrusion direction (210, 220, 230) of a planar entity, normalised.
-fn extrusion(g: &[Pair<'_>]) -> [f64; 3] {
- let n = [g.num_or(210, 0.0), g.num_or(220, 0.0), g.num_or(230, 1.0)];
- let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
- if len > 1e-12 && len.is_finite() { n.map(|v| v / len) } else { [0.0, 0.0, 1.0] }
-}
-
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
-fn unit(v: [f64; 3]) -> [f64; 3] {
- let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
- if len > 1e-12 { v.map(|x| x / len) } else { v }
-}
-
/// A planar entity's coordinate system seen from above: its x and y axes in world space by the
/// arbitrary axis algorithm (identity for drawings in the world's plane; extruded along −z it
/// mirrors x).
-fn ocs(g: &[Pair<'_>]) -> Affine {
- let n = extrusion(g);
- if n[2] > 1.0 - 1e-12 {
+fn ocs(extrusion: model::Point) -> Affine {
+ let [ax, ay, _] = model::ocs_axes(extrusion);
+ if ax == [1.0, 0.0, 0.0] && ay == [0.0, 1.0, 0.0] {
return Affine::IDENTITY;
}
- let ax = unit(if n[0].abs() < 1.0 / 64.0 && n[1].abs() < 1.0 / 64.0 { cross([0.0, 1.0, 0.0], n) } else { cross([0.0, 0.0, 1.0], n) });
- let ay = unit(cross(n, ax));
Affine::new([ax[0], ax[1], ay[0], ay[1], 0.0, 0.0])
}
-fn ellipse(g: &[Pair<'_>]) -> Option {
- let centre = g.point(10);
- let major = g.point(11).to_vec2();
- let ratio = g.num_or(40, 1.0);
+fn ellipse(extrusion: model::Point, centre: model::Point, major: model::Point, ratio: f64, start: f64, end: f64) -> Option {
+ let centre = point(centre);
+ let major = point(major).to_vec2();
if ratio <= 0.0 || major.hypot() < 1e-12 {
return None;
}
// The minor axis is the major one turned a quarter about the extrusion direction.
- let n = extrusion(g);
+ let n = model::ocs_axes(extrusion)[2];
let m = cross(n, [major.x, major.y, 0.0]);
let minor = Vec2::new(m[0], m[1]) * ratio;
- let (start, end) = (g.num_or(41, 0.0), g.num_or(42, std::f64::consts::TAU));
let sweep = ccw_sweep(start.to_degrees(), end.to_degrees()).to_radians();
curve::ellipse(Affine::new([major.x, major.y, minor.x, minor.y, centre.x, centre.y]), start, sweep)
}
-fn lwpolyline(g: &[Pair<'_>]) -> Option {
- let mut verts: Vec = Vec::new();
- let (mut widths, mut uniform) = (None, true);
- for p in g {
- match p.code {
- 10 => verts.push(Vertex { p: Point::new(number(p.value)?, 0.0), bulge: 0.0 }),
- 20 => verts.last_mut()?.p.y = number(p.value)?,
- 42 => verts.last_mut()?.bulge = number(p.value).unwrap_or(0.0),
- 40 | 41 => {
- let w = number(p.value).unwrap_or(0.0);
- uniform &= widths.is_none_or(|x: f64| (x - w).abs() < 1e-12);
- widths = Some(w);
- }
- _ => {}
- }
- }
- let bp = transformed(curve::polyline(&verts, g.int_or(70, 0) & 1 != 0)?, ocs(g));
- let width = g.num(43).filter(|w| *w > 0.0).or(widths.filter(|_| uniform)).unwrap_or(0.0).max(0.0);
+fn lwpolyline(p: &model::LwPolyline, ocs: Affine) -> Option {
+ let verts: Vec = p.vertices.iter().map(|v| Vertex { p: Point::new(v.x, v.y), bulge: v.bulge }).collect();
+ let bp = transformed(curve::polyline(&verts, p.closed)?, ocs);
+ // One width for every vertex is the line's width.
+ let widths: Vec = p.vertices.iter().flat_map(|v| [v.start_width, v.end_width]).collect();
+ let uniform = widths.first().filter(|w| widths.iter().all(|x| (x - *w).abs() < 1e-12)).copied();
+ let width = Some(p.const_width).filter(|w| *w > 0.0).or(uniform).unwrap_or(0.0).max(0.0);
Some(Geom::Path { bp, fill: false, width })
}
-/// The points at `code` (x) and `code + 10` (y), in order.
-fn points(g: &[Pair<'_>], code: i32) -> Option> {
- let mut out: Vec = Vec::new();
- for p in g {
- if p.code == code {
- out.push(Point::new(number(p.value)?, 0.0));
- } else if p.code == code + 10 {
- out.last_mut()?.y = number(p.value)?;
- }
- }
- Some(out)
-}
-
-fn spline(g: &[Pair<'_>]) -> Option {
- let flags = g.int_or(70, 0);
- let ctrl = points(g, 10)?;
- let degree = usize::try_from(g.int_or(71, 3)).ok()?;
- let knots: Vec = g.iter().filter(|p| p.code == 40).map(|p| number(p.value)).collect::