forked from 1b-agent/opencadstudio-upstream
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-<face><n> on the installed file of that face. CHORD records are not read. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8e9bd7db69
commit
9ddc103bcc
3 files changed
+215
-50
No files matched your search
@@ -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-<n> after the first free number.
|
||||
// A text style per face, `WMF-<face><n>` 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<String> = 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 {
|
||||
|
||||
+185
-35
@@ -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<EntityType>,
|
||||
/// The face name of each font the text uses; text styles are
|
||||
/// `WMF-<n>` for each, in this order.
|
||||
pub fonts: Vec<String>,
|
||||
/// Each face the text uses, in the order first used; each becomes a
|
||||
/// text style.
|
||||
pub fonts: Vec<Face>,
|
||||
/// 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<EntityType>,
|
||||
fonts: Vec<String>,
|
||||
fonts: Vec<Face>,
|
||||
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<f64> {
|
||||
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<i64> = 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<Picture> {
|
||||
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<Picture> {
|
||||
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<Picture> {
|
||||
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<Picture> {
|
||||
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));
|
||||
|
||||
@@ -292,6 +292,22 @@ fn face_id_weighted(family: &str, weight: fontdb::Weight) -> Option<fontdb::ID>
|
||||
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<String> {
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user