From 9ddc103bcce41e8dd44f12022e6e53105bdccf87 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:12:47 +0300 Subject: [PATCH] WMF: arcs on whole units, text by its font's ascent, no chords Arcs and pies are cut at whole multiples of 1/99 turn round the ellipse's whole-unit centre and radii, rounded down, from the start ray to the end ray. Text sits the font's ascent below its reference point and is four fifths of it tall; its style is WMF- on the installed file of that face. CHORD records are not read. Co-Authored-By: Claude Opus 5.5 --- src/app/commands/model_import.rs | 29 ++-- src/io/wmf_import.rs | 220 ++++++++++++++++++++++++++----- src/scene/text/sysfont.rs | 16 +++ 3 files changed, 215 insertions(+), 50 deletions(-) diff --git a/src/app/commands/model_import.rs b/src/app/commands/model_import.rs index 94b86d8f3..f5a23b5c9 100644 --- a/src/app/commands/model_import.rs +++ b/src/app/commands/model_import.rs @@ -145,22 +145,21 @@ impl OpenCADStudio { }; self.push_undo_snapshot(i, if prompt { "WMFIN" } else { "IMPORT" }); let scene = &mut self.tabs[i].scene; - // A text style per font, named WMF- after the first free number. + // A text style per face, `WMF-` with `n` counting the + // face's bold and italic variants; the style's font is the + // installed file of that face, none when it is not installed. A + // style the drawing already has is used as it is. let mut styles: Vec = Vec::new(); - for _ in &picture.fonts { - let mut n = 0; - let name = loop { - let name = format!("WMF-{n}"); - if !scene.document.text_styles.contains(&name) && !styles.contains(&name) { - break name; - } - n += 1; - }; - let mut style = codec::tables::TextStyle::new(name.as_str()); - style.handle = scene.document.allocate_handle(); - // No font of its own, as the metafile names the face only. - style.font_file = String::new(); - let _ = scene.document.text_styles.add(style); + for (k, face) in picture.fonts.iter().enumerate() { + let n = picture.fonts[..k].iter().filter(|f| f.name == face.name).count(); + let name = format!("WMF-{}{n}", face.name); + if !scene.document.text_styles.contains(&name) { + let mut style = codec::tables::TextStyle::new(name.as_str()); + style.handle = scene.document.allocate_handle(); + style.font_file = + crate::scene::text::sysfont::face_file(&face.name, face.bold, face.italic).unwrap_or_default(); + let _ = scene.document.text_styles.add(style); + } styles.push(name); } for (at, font) in &picture.text_fonts { diff --git a/src/io/wmf_import.rs b/src/io/wmf_import.rs index 352f79696..2c0721e6e 100644 --- a/src/io/wmf_import.rs +++ b/src/io/wmf_import.rs @@ -1,7 +1,9 @@ //! Windows metafile (WMF) reading: the records a metafile plays, as the //! drawing objects of a block — outlines as polylines in their pen's colour //! and width, filled shapes as closed polylines in their brush's colour, -//! ellipses as 99-segment polylines, text as single-line text. +//! ellipses as 99-segment polylines, arcs and pies on whole units, text as +//! single-line text placed and sized by its font's own ascent. Chords are +//! not read. //! //! The picture is scaled to fit half the view it is imported into, with its //! top-left corner at the block's origin and y running down the screen as @@ -11,13 +13,21 @@ use codec::entities::{Polyline2D, Text, Vertex2D}; use codec::types::{Color, Vector3}; use codec::EntityType; +/// A font face as the text uses it: its name, and whether bold and italic. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Face { + pub name: String, + pub bold: bool, + pub italic: bool, +} + /// A picture: its objects, in block coordinates, and the font of each /// text (an index into `fonts`). pub struct Picture { pub entities: Vec, - /// The face name of each font the text uses; text styles are - /// `WMF-` for each, in this order. - pub fonts: Vec, + /// Each face the text uses, in the order first used; each becomes a + /// text style. + pub fonts: Vec, /// For each text in `entities` (by position), its font index. pub text_fonts: Vec<(usize, usize)>, } @@ -37,11 +47,62 @@ struct Brush { #[derive(Clone)] struct Font { + /// The logical height: negative for the character height, positive for + /// the cell height. height: f64, escapement: f64, + weight: i32, + italic: bool, face: String, } +/// A font's ascent, in metafile units, as the system lays the font out at +/// its logical height: the baseline sits this far below a text's reference +/// point. Without the system's font engine it is nine tenths of the height. +#[cfg(target_os = "windows")] +fn ascent(font: &Font) -> f64 { + use windows_sys::Win32::Graphics::Gdi::{ + CreateCompatibleDC, CreateFontIndirectW, DeleteDC, DeleteObject, GetTextMetricsW, + SelectObject, LOGFONTW, TEXTMETRICW, + }; + // SAFETY: a private memory device context and font, released before + // returning; the structures are plain data the calls fill. + unsafe { + let mut logfont: LOGFONTW = std::mem::zeroed(); + logfont.lfHeight = font.height as i32; + logfont.lfWeight = font.weight; + logfont.lfItalic = u8::from(font.italic); + for (slot, unit) in logfont + .lfFaceName + .iter_mut() + .zip(font.face.encode_utf16().take(31)) + { + *slot = unit; + } + let dc = CreateCompatibleDC(std::ptr::null_mut()); + if dc.is_null() { + return 0.9 * font.height.abs(); + } + let handle = CreateFontIndirectW(&logfont); + let previous = SelectObject(dc, handle); + let mut metrics: TEXTMETRICW = std::mem::zeroed(); + let ok = GetTextMetricsW(dc, &mut metrics) != 0; + SelectObject(dc, previous); + DeleteObject(handle); + DeleteDC(dc); + if ok { + f64::from(metrics.tmAscent) + } else { + 0.9 * font.height.abs() + } + } +} + +#[cfg(not(target_os = "windows"))] +fn ascent(font: &Font) -> f64 { + 0.9 * font.height.abs() +} + #[derive(Clone)] enum Object { Pen(Pen), @@ -70,7 +131,7 @@ struct Player { origin: [f64; 2], scale: [f64; 2], entities: Vec, - fonts: Vec, + fonts: Vec, text_fonts: Vec<(usize, usize)>, } @@ -129,12 +190,94 @@ fn ellipse_points(bounds: [f64; 4], start: f64, sweep: f64) -> Vec<[f64; 2]> { .collect() } -/// The angle of a radial point round an ellipse's centre, in drawing -/// orientation. -fn angle(bounds: [f64; 4], x: f64, y: f64) -> f64 { +/// Turns an arc is cut into: its points lie on whole multiples of this +/// share of a turn. +const ARC_STEPS: i64 = 99; + +/// The angle of a radial point round an ellipse, in degrees in `(0, 360]` +/// counter-clockwise in drawing orientation — or `None` for a point inside +/// the ellipse, which marks no angle. +fn radial_angle(bounds: [f64; 4], x: f64, y: f64) -> Option { let [left, top, right, bottom] = bounds; let (cx, cy) = (0.5 * (left + right), 0.5 * (top + bottom)); - (-(y - cy)).atan2(x - cx) + let (rx, ry) = (0.5 * (right - left).abs(), 0.5 * (bottom - top).abs()); + let (u, v) = ((x - cx) / rx, -(y - cy) / ry); + if u.hypot(v) < 1.0 { + return None; + } + let degrees = v.atan2(u).to_degrees().rem_euclid(360.0); + Some(if degrees == 0.0 { 360.0 } else { degrees }) +} + +/// An arc as the reference reader plays it: points round the ellipse's +/// whole-unit centre and radii at whole multiples of 1/99 turn, rounded +/// down to whole units, from the start angle up to the end angle — past +/// 360 degrees back from zero when the end comes first. A start inside the +/// ellipse starts at zero; an end inside it runs on to a whole turn; with +/// both inside there is no arc. +fn arc_points(bounds: [f64; 4], start: [f64; 2], end: [f64; 2]) -> Vec<[f64; 2]> { + let [left, top, right, bottom] = bounds.map(|v| v as i64); + let (cx, cy) = ((left + right).div_euclid(2), (top + bottom).div_euclid(2)); + let (rx, ry) = ((right - left).abs() / 2, (bottom - top).abs() / 2); + let (from, to) = (radial_angle(bounds, start[0], start[1]), radial_angle(bounds, end[0], end[1])); + let (start_angle, end_angle) = match (from, to) { + (None, None) => return Vec::new(), + (None, to) => (0.0, to), + (Some(from), to) => (from, to.filter(|to| *to != from)), + }; + // The angles are stepped out from zero one step at a time in degrees, + // rounding and all, as the points come out. + let mut degrees = vec![0.0f64; ARC_STEPS as usize + 1]; + for k in 1..degrees.len() { + degrees[k] = degrees[k - 1] + 360.0 / ARC_STEPS as f64; + } + let point = |k: i64| { + let a = degrees[k as usize] * (std::f64::consts::PI / 180.0); + [ + (cx as f64 + rx as f64 * a.cos()).floor(), + (cy as f64 + ry as f64 * -a.sin()).floor(), + ] + }; + // `k` steps lie below an angle when k·360 < angle·99, kept exact. + let under = |k: &i64, angle: f64| ((k * 360) as f64) < angle * ARC_STEPS as f64; + let first = (start_angle * ARC_STEPS as f64 / 360.0).ceil() as i64; + let mut steps: Vec = match end_angle { + // Up to the end. + Some(to) if to > start_angle => (first..).take_while(|k| under(k, to)).collect(), + // Round to a whole turn, then from zero up to the end. + Some(to) => (first..=ARC_STEPS).chain((0..).take_while(|k| under(k, to))).collect(), + // Round to a whole turn. + None => (first..=ARC_STEPS).collect(), + }; + // The whole-unit points next to the start and end rays are judged by + // which side of the ray they fall: a point on the start ray is left + // out, one on the end ray kept. + let (fx, fy) = (0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3])); + let side = |ray: [f64; 2], k: i64| { + let p = point(k); + let (ux, uy) = (ray[0] - fx, -(ray[1] - fy)); + let (vx, vy) = (p[0] - fx, -(p[1] - fy)); + ux * vy - uy * vx + }; + if from.is_some() { + if let Some(&k) = steps.first() { + if k < ARC_STEPS && side(start, k) <= 0.0 { + steps.remove(0); + } else if k > 0 && k < ARC_STEPS && side(start, k - 1) > 0.0 { + steps.insert(0, k - 1); + } + } + } + if end_angle.is_some() { + if let Some(&k) = steps.last() { + if k > 0 && side(end, k) > 0.0 { + steps.pop(); + } else if k + 1 < ARC_STEPS && side(end, k + 1) <= 0.0 { + steps.push(k + 1); + } + } + } + steps.into_iter().map(point).collect() } /// Reads a metafile, scaled so the picture fits within `fit` (width and @@ -206,6 +349,8 @@ pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option { let mut font = Font { height: 12.0, escapement: 0.0, + weight: 400, + italic: false, face: String::new(), }; let mut text_rgb = [0, 0, 0]; @@ -251,8 +396,10 @@ pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option { create( &mut table, Object::Font(Font { - height: s(0).abs(), + height: s(0), escapement: s(2) / 10.0, + weight: s(4) as i32, + italic: p.get(5).is_some_and(|w| w & 0xff != 0), face: String::from_utf8_lossy(&face[..end]).into_owned(), }), ) @@ -321,29 +468,26 @@ pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option { player.polyline(&ring, false, pen.width, pen.rgb); } } - // ARC, CHORD, PIE: (yEnd, xEnd, yStart, xStart, bottom, - // right, top, left), drawn counter-clockwise - 0x0817 | 0x0830 | 0x081A => { + // ARC, PIE: (yEnd, xEnd, yStart, xStart, bottom, right, top, + // left), drawn counter-clockwise; a pie's outline starts at the + // centre. (CHORD records are not read.) + 0x0817 | 0x081A => { let bounds = [s(7), s(6), s(5), s(4)]; - let start = angle(bounds, s(3), s(2)); - let end = angle(bounds, s(1), s(0)); - let mut sweep = end - start; - if sweep <= 0.0 { - sweep += std::f64::consts::TAU; + let arc = arc_points(bounds, [s(3), s(2)], [s(1), s(0)]); + if arc.is_empty() { + continue; } - let mut ring = ellipse_points(bounds, start, sweep); - match function { - 0x0817 => { - if !pen.null { - player.polyline(&ring, false, pen.width, pen.rgb); - } - } - _ => { - if *function == 0x081A { - ring.push([0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3])]); - } - player.shape(&ring, &pen, &brush); + if *function == 0x0817 { + if !pen.null { + player.polyline(&arc, false, pen.width, pen.rgb); } + } else { + let centre = [ + ((bounds[0] + bounds[2]) as i64).div_euclid(2) as f64, + ((bounds[1] + bounds[3]) as i64).div_euclid(2) as f64, + ]; + let ring: Vec<[f64; 2]> = std::iter::once(centre).chain(arc).collect(); + player.shape(&ring, &pen, &brush); } } // TEXTOUT (count, string, y, x) and EXTTEXTOUT (y, x, count, @@ -368,21 +512,27 @@ pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option { continue; } // The reference point is the cell's top; the baseline sits - // nine tenths of the font height below it. + // the font's ascent below it, and the text is four fifths + // of that ascent tall. let rotation = font.escapement.to_radians(); - let drop = 0.9 * font.height; + let drop = ascent(&font); let base = player.at(x + drop * rotation.sin(), y + drop * rotation.cos()); - let index = match player.fonts.iter().position(|f| *f == font.face) { + let face = Face { + name: font.face.clone(), + bold: font.weight >= 600, + italic: font.italic, + }; + let index = match player.fonts.iter().position(|f| *f == face) { Some(k) => k, None => { - player.fonts.push(font.face.clone()); + player.fonts.push(face); player.fonts.len() - 1 } }; let mut text = Text::new(); text.value = value; text.insertion_point = base; - text.height = 0.72 * font.height * scale; + text.height = 0.8 * drop * scale; text.rotation = rotation; text.common.color = color(text_rgb); player.text_fonts.push((player.entities.len(), index)); diff --git a/src/scene/text/sysfont.rs b/src/scene/text/sysfont.rs index a74de6122..ae0b3ed96 100644 --- a/src/scene/text/sysfont.rs +++ b/src/scene/text/sysfont.rs @@ -292,6 +292,22 @@ fn face_id_weighted(family: &str, weight: fontdb::Weight) -> Option fonts().db.lock().unwrap().query(&query) } +/// The file name (lower case) of the installed face of exactly `family` — +/// no stand-in family — in the bold and italic asked for, or `None`. +pub fn face_file(family: &str, bold: bool, italic: bool) -> Option { + let db = fonts().db.lock().unwrap(); + let id = db.query(&fontdb::Query { + families: &[fontdb::Family::Name(family)], + weight: if bold { fontdb::Weight::BOLD } else { fontdb::Weight::NORMAL }, + style: if italic { fontdb::Style::Italic } else { fontdb::Style::Normal }, + ..Default::default() + })?; + match &db.face(id)?.source { + fontdb::Source::File(path) => Some(path.file_name()?.to_string_lossy().to_ascii_lowercase()), + _ => None, + } +} + /// Borrow the raw face bytes for `family` and run `f` over them. The byte slice /// is only valid inside the closure, so callers extract everything they need /// (e.g. flattened glyph outlines) before returning. `index` is the face index