diff --git a/crates/engine/src/cmd/drawing.rs b/crates/engine/src/cmd/drawing.rs index b3ca83d42..df2a041f7 100644 --- a/crates/engine/src/cmd/drawing.rs +++ b/crates/engine/src/cmd/drawing.rs @@ -20,6 +20,7 @@ use solidcraft_doc::{Document, ids}; use solidcraft_geom::cell::Warning; use solidcraft_geom::drawing_view::ProjectedSide; use solidcraft_geom::paper::line::LineGroup; +use solidcraft_sketch::PaperFonts; use super::CommandSpec; use crate::params::{bad, str_}; @@ -450,7 +451,9 @@ fn title_block_cells(doc: &Document, tree: &mut Tree<'_>, d: &Drawing, at: usize Some(sheet) => sheet.title_block.as_ref().zip(sheet.title_block_layout()?), None => None, }; - let notes = block.map(|(block, placed)| title_block_text::lay(block, &placed, &d.font, &d.language, &shown, true).notes).unwrap_or_default(); + let notes = block + .map(|(block, placed)| title_block_text::lay(block, &placed, &PaperFonts::new(&d.font, &d.language, true), &shown).notes) + .unwrap_or_default(); let note = |note: &Note| match note { Note::Fit(Warning::Chars { count, max }) => json!({"fit": "chars", "count": count, "max": max}), Note::Fit(Warning::Height) => json!({"fit": "height"}), diff --git a/crates/engine/src/drawing_dimension_paper.rs b/crates/engine/src/drawing_dimension_paper.rs index 35e29eb52..fd1e6b6dc 100644 --- a/crates/engine/src/drawing_dimension_paper.rs +++ b/crates/engine/src/drawing_dimension_paper.rs @@ -7,7 +7,6 @@ //! or outside by its real length. A text the font can't set leaves the lines and says why. use std::f64::consts::{FRAC_1_SQRT_2, PI, TAU}; -use std::path::PathBuf; use solidcore_record::{Seg2, Vec2}; use solidcraft_doc::drawing::dimension::{Dimension, DimensionKind, Resolved, TextPlace}; @@ -18,7 +17,7 @@ use solidcraft_geom::paper::line::{LineGroup, LineKind}; use solidcraft_geom::paper::text::{Font, TextRun}; use solidcraft_geom::paper::{Group, Item, ObjectKind}; use solidcraft_geom::prelude::*; -use solidcraft_sketch::{DimFrame, DimSizes, SketchError, TextStyle, dim_layout_sized, font_dirs, paper_text, paper_text_now}; +use solidcraft_sketch::{DimFrame, DimSizes, Outcome, PaperFonts, TextStyle, dim_layout_sized}; /// A dimension's sizes on paper, with its text above an unbroken line (ISO 129-1). const SIZES: DimSizes = DimSizes { arrow: 3.0, gap: 1.0, overshoot: 2.0, offset: 10.0, above: true }; @@ -50,20 +49,14 @@ struct Text { } impl Text { - /// `text` in the drawing's `font` (a family; empty: the built-in font) and `language`. With - /// `now` (the font folders), the search for fonts to fall back to is asked, not waited for. - fn set(text: Option<&str>, font: &str, language: &str, now: Option<&[PathBuf]>) -> Text { + /// `text` in the drawing's `lettering`. + fn set(text: Option<&str>, lettering: &PaperFonts) -> Text { let unset = |why, pending| Text { runs: Vec::new(), fonts: Vec::new(), half: Vec2::new(0.0, TEXT_MM * 0.5), why, pending }; - let style = TextStyle::default(); - let set = |text| match now { - Some(dirs) => paper_text_now(dirs, text, Some(font), language, Vec2::ZERO, TEXT_MM, 0.0, &style), - None => paper_text(text, Some(font), language, Vec2::ZERO, TEXT_MM, 0.0, &style), - }; - match text.map(set) { - None | Some(Ok(None)) => unset(None, false), - Some(Err(SketchError::FontPending)) => unset(None, true), - Some(Err(why)) => unset(Some(why.to_string()), false), - Some(Ok(Some((runs, fonts)))) => { + match text.map(|text| Outcome::from(lettering.text(text, Vec2::ZERO, TEXT_MM, 0.0, &TextStyle::default()))) { + None | Some(Outcome::Set(None)) => unset(None, false), + Some(Outcome::Pending) => unset(None, true), + Some(Outcome::Refused(why)) => unset(Some(why), false), + Some(Outcome::Set(Some((runs, fonts)))) => { let width = runs.iter().map(TextRun::reach).fold(0.0, f64::max); // Its lines stand below the first one's baseline. let below = runs.iter().map(|run| -run.origin.y).fold(0.0, f64::max); @@ -85,25 +78,15 @@ pub struct OnPaper { } /// `dimension`, read as `resolved`, on the view through `frame` (centred on the view), its lines in -/// `group` and its text in the drawing's `font` and `language`: its group, or `None` when what it measures has no place to draw on the -/// view: it wasn't found again, or it is a single point there. `wait`: fonts to fall back to are -/// searched for here; else the search's thread is asked, for the window's sheet. -pub fn paper_group( - dimension: &Dimension, - resolved: &Resolved, - frame: &ViewFrame, - group: LineGroup, - font: &str, - language: &str, - wait: bool, -) -> Option { +/// `group` and its text in the drawing's `lettering`: its group, or `None` when what it measures has no place to draw on the +/// view: it wasn't found again, or it is a single point there. +pub fn paper_group(dimension: &Dimension, resolved: &Resolved, frame: &ViewFrame, group: LineGroup, lettering: &PaperFonts) -> Option { let line_mm = LineKind::Thin.width_mm(group); let on: Vec = resolved.points.iter().map(|p| frame.to_paper(*p)).collect(); let mut lines: Vec> = Vec::new(); let mut arrows = Vec::new(); let (mut texts, mut fonts, mut why, mut pending) = (Vec::new(), Vec::new(), None, false); - let dirs = (!wait).then(font_dirs); - let set = |text: Option<&str>| Text::set(text, font, language, dirs.as_deref()); + let set = |text: Option<&str>| Text::set(text, lettering); for piece in pieces(dimension, resolved, &on, &set)? { let layout = dim_layout_sized(&piece.frame, Some(piece.at), &SIZES, piece.text.half, [None, None]); texts.extend(placed(piece.text.runs, piece.text.half, layout.text, layout.text_dir)); diff --git a/crates/engine/src/drawing_dimension_paper_tests.rs b/crates/engine/src/drawing_dimension_paper_tests.rs index 18f3078dc..bfa9643e4 100644 --- a/crates/engine/src/drawing_dimension_paper_tests.rs +++ b/crates/engine/src/drawing_dimension_paper_tests.rs @@ -46,7 +46,7 @@ fn length(direction: LinearDirection) -> DimensionKind { /// `dimension` on paper in the built-in font, in English. fn laid(dimension: &Dimension, resolved: &Resolved, frame: &ViewFrame) -> Option { - paper_group(dimension, resolved, frame, LineGroup::DEFAULT, "", "en", true) + paper_group(dimension, resolved, frame, LineGroup::DEFAULT, &PaperFonts::new("", "en", true)) } fn drawn(kind: DimensionKind, offset: f64, points: &[[f64; 2]], text: &str, scale: &str) -> Group { @@ -76,7 +76,8 @@ fn measured(text: &str) -> f64 { fn a_dimensions_lines_are_its_groups_thin_line() { let widths = |group| { let d = dimension(length(LinearDirection::Horizontal), 10.0, TextPlace::Centred); - let drawn = paper_group(&d, &read(&[[0.0, 0.0], [40.0, 0.0]], "40.00"), &view_at("1:1"), group, "", "en", true).unwrap().group; + let drawn = + paper_group(&d, &read(&[[0.0, 0.0], [40.0, 0.0]], "40.00"), &view_at("1:1"), group, &PaperFonts::new("", "en", true)).unwrap().group; let strokes = drawn.items.iter().filter_map(|i| match i { Item::Stroke { width_mm, .. } => Some(*width_mm), _ => None, diff --git a/crates/engine/src/drawing_scene.rs b/crates/engine/src/drawing_scene.rs index 7dd18eced..410833e4d 100644 --- a/crates/engine/src/drawing_scene.rs +++ b/crates/engine/src/drawing_scene.rs @@ -64,6 +64,7 @@ use solidcraft_geom::paper::text::{Font, TextRun, font_in}; use solidcraft_geom::paper::{Group, Item, MAX_RECORDS, ObjectKind, Picture, Scene}; use solidcraft_geom::prelude::*; use solidcraft_geom::{Aabb2, ArcMap}; +use solidcraft_sketch::PaperFonts; use crate::drawing_picture::{picture_items, view_parts}; use crate::drawing_view::{CHORD_MM, LineRequest, MIN_MODEL_CHORD_MM, Shown, ShownSet, Stats, Styles, Unmeasured, paper_lines}; @@ -465,10 +466,11 @@ fn draw_dimensions( ) -> Vec { let ids: Vec = wanted.iter().map(|record| record.id).collect(); let read = s.doc.resolve_dimensions(&s.model, d, &ids); + let lettering = PaperFonts::new(&d.font, &d.language, wait); let one = |at: usize, record: &Arc| match read.as_ref().map(|all| all.get(at)) { Ok(Some(resolved)) => { let paper = frame(record.view) - .and_then(|(frame, lines)| crate::drawing_dimension_paper::paper_group(record, resolved, &frame, lines, &d.font, &d.language, wait)); + .and_then(|(frame, lines)| crate::drawing_dimension_paper::paper_group(record, resolved, &frame, lines, &lettering)); let drawn = |group, fonts, why, pending| DrawnDimension { dimension: record.id, view: record.view, @@ -704,7 +706,7 @@ fn cell_texts(s: &Session, d: &Drawing, sheet: u64) -> Vec { fn block_text(record: &Sheet, lettering: &Lettering, cells: &[CellText], wait: bool) -> Result<(Set, bool)> { Ok(match (&record.title_block, record.title_block_layout().map_err(|e| bad(CMD, e.to_string()))?) { (Some(block), Some(placed)) => { - let laid = title_block_text::lay(block, &placed, &lettering.font, &lettering.language, cells, wait); + let laid = title_block_text::lay(block, &placed, &PaperFonts::new(&lettering.font, &lettering.language, wait), cells); (laid.set(), laid.pending) } _ => (Set::default(), false), diff --git a/crates/engine/src/title_block_text.rs b/crates/engine/src/title_block_text.rs index 1a9607422..0a26e234f 100644 --- a/crates/engine/src/title_block_text.rs +++ b/crates/engine/src/title_block_text.rs @@ -22,7 +22,7 @@ use solidcraft_geom::cell::{HAlign, Part, Source, Warning, place_lines}; use solidcraft_geom::paper::text::{Font, TextRun}; use solidcraft_geom::title_block::{CellKind, TextCell, TitleBlock, TitleBlockLayout}; use solidcraft_geom::{Aabb2, EPS}; -use solidcraft_sketch::{PaperLines, SketchError, TextAlign, TextStyle, font_dirs, paper_lines, paper_lines_now}; +use solidcraft_sketch::{Outcome, PaperFonts, PaperLines, SketchError, TextAlign, TextStyle}; /// A text cell as its sheet shows it: its name, its text, and the value typed text hides. #[derive(Clone, Debug, PartialEq)] @@ -108,33 +108,23 @@ fn format_mass(grams: f64) -> String { format!("{:.2} kg", grams / 1000.0) } -/// `block`'s labels, and its text cells' values as `shown` gives them, measured in the font `family` -/// and `language` (the drawing's; an empty family: the built-in font) where `layout` places them. -/// `wait`: fonts to fall back to are searched for here; else the search's thread is asked, for the -/// window's sheet. -pub fn lay(block: &TitleBlock, layout: &TitleBlockLayout, family: &str, language: &str, shown: &[CellText], wait: bool) -> Laid { - let dirs = if wait { Vec::new() } else { font_dirs() }; - let measure = |text: &str, cap_mm: f64, wrap: Option, style: &TextStyle| { - if wait { - paper_lines(text, Some(family), language, cap_mm, wrap, style) - } else { - paper_lines_now(&dirs, text, Some(family), language, cap_mm, wrap, style) - } - }; +/// `block`'s labels, and its text cells' values as `shown` gives them, measured in the drawing's +/// lettering (`fonts`) where `layout` places them. +pub fn lay(block: &TitleBlock, layout: &TitleBlockLayout, fonts: &PaperFonts, shown: &[CellText]) -> Laid { let mut out = Laid::default(); for (cell, placed) in block.cells.iter().zip(&layout.cells) { let mut notes = Vec::new(); let label = cell.label.as_ref().zip(placed.label_at); - let label = label.map(|(label, at)| measure(&label.text, label.cap_mm, None, &TextStyle::default()).map(|lines| (lines, at))); + let label = label.map(|(label, at)| fonts.lines(&label.text, label.cap_mm, None, &TextStyle::default()).map(|lines| (lines, at))); let value = match (&cell.kind, shown.iter().find(|shown| shown.name == cell.name)) { - (CellKind::Text(text), Some(value)) => Some(value_lines(text, placed.value_box, measure, &value.text, &mut notes)), + (CellKind::Text(text), Some(value)) => Some(value_lines(text, placed.value_box, fonts, &value.text, &mut notes)), _ => None, }; for text in [label, value].into_iter().flatten() { - match text { - Ok(text) => out.texts.push(text), - Err(SketchError::FontPending) => out.pending = true, - Err(e) => notes.push(Note::Refused(e.to_string())), + match Outcome::from(text) { + Outcome::Set(text) => out.texts.push(text), + Outcome::Pending => out.pending = true, + Outcome::Refused(why) => notes.push(Note::Refused(why)), } } out.notes.extend(notes.into_iter().map(|note| (cell.name.clone(), note))); @@ -142,15 +132,9 @@ pub fn lay(block: &TitleBlock, layout: &TitleBlockLayout, family: &str, language out } -/// `value`'s lines in `value_box`, as `measure` sets them, by the cell text rule, and where the +/// `value`'s lines in `value_box`, as `fonts` sets them, by the cell text rule, and where the /// first starts; what doesn't fit is added to `notes`. -fn value_lines( - text: &TextCell, - value_box: Aabb2, - measure: impl Fn(&str, f64, Option, &TextStyle) -> Result, - value: &str, - notes: &mut Vec, -) -> Result<(PaperLines, Vec2), SketchError> { +fn value_lines(text: &TextCell, value_box: Aabb2, fonts: &PaperFonts, value: &str, notes: &mut Vec) -> Result<(PaperLines, Vec2), SketchError> { let width = value_box.size().x; let align = match text.align { HAlign::Left => TextAlign::Left, @@ -158,7 +142,7 @@ fn value_lines( HAlign::Right => TextAlign::Right, }; let style = TextStyle { align, ..TextStyle::default() }; - let lines = measure(value, text.cap_mm, text.fit.wrap.then_some(width), &style)?; + let lines = fonts.lines(value, text.cap_mm, text.fit.wrap.then_some(width), &style)?; notes.extend(text.fit.warn_chars(value).map(Note::Fit)); if lines.lines.iter().any(|(_, w)| *w > width + EPS) { notes.push(Note::Fit(Warning::Width)); diff --git a/crates/engine/src/title_block_text_tests.rs b/crates/engine/src/title_block_text_tests.rs index a33cfcfc3..49dc5de97 100644 --- a/crates/engine/src/title_block_text_tests.rs +++ b/crates/engine/src/title_block_text_tests.rs @@ -8,6 +8,7 @@ use solidcraft_geom::paper::text::TextRun; use solidcraft_geom::paper::{Item, ObjectKind}; use solidcraft_geom::prelude::*; use solidcraft_geom::title_block::CellLayout; +use solidcraft_sketch::paper_lines; use super::*; use crate::Session; diff --git a/crates/sketch/src/lib.rs b/crates/sketch/src/lib.rs index 9ffca15fd..ece830319 100644 --- a/crates/sketch/src/lib.rs +++ b/crates/sketch/src/lib.rs @@ -70,7 +70,8 @@ pub use nurbs::Nurbs; pub use offset::OffsetChain; pub use offset_topology::{OffsetCreated, OffsetRole, OffsetRoleFlags, OffsetRoleKind, OffsetTopology}; pub use paper_text::{ - PaperLines, paper_fallback_notes, paper_font_note, paper_lines, paper_lines_in, paper_lines_now, paper_text, paper_text_in, paper_text_now, + Outcome, PaperFonts, PaperLines, paper_fallback_notes, paper_font_note, paper_lines, paper_lines_in, paper_lines_now, paper_text, paper_text_in, + paper_text_now, }; pub use pattern::{PatternAlignment, PatternInstance, SketchPattern, SketchPatternKind}; pub use periodic::periodic_beziers; diff --git a/crates/sketch/src/paper_text.rs b/crates/sketch/src/paper_text.rs index 7ccdc8890..6595f1d88 100644 --- a/crates/sketch/src/paper_text.rs +++ b/crates/sketch/src/paper_text.rs @@ -28,7 +28,8 @@ //! - The search for those fonts reads whole files for the known ones and hashes the ones it finds, //! so the window never waits for it: [`paper_text_now`] asks a thread (`font-search`) and refuses //! with [`SketchError::FontPending`] until the answers are in. Answers, misses too, are kept for -//! the process. [`paper_text`] does the search where it is called. +//! the process. [`paper_text`] does the search where it is called. A sheet sets its texts through +//! [`PaperFonts`], which makes that choice once, and reads each answer as an [`Outcome`]. //! - The browser has no font folders: there the web covers Arabic, Chinese, Japanese and Korean, a //! font each ([`fallback`]). Text is refused with [`SketchError::FontPending`], whichever way it's //! asked for, until each font it asked the web app for has come or failed. @@ -200,6 +201,57 @@ pub fn paper_lines_now( measure(dirs, text, family, language, height, wrap, style, false) } +/// A drawing's lettering for its sheet's texts: the font `family` (empty: the built-in font) and +/// `language`, with the font folders listed once. `wait`: fonts to fall back to are searched for +/// where a text is set (an export, `drawing.inspect`); else the search's thread is asked, for the +/// window's sheet, as [`paper_text_now`] does. The one place a sheet's text chooses its way in. +pub struct PaperFonts { + family: String, + language: String, + dirs: Vec, + wait: bool, +} + +impl PaperFonts { + pub fn new(family: &str, language: &str, wait: bool) -> PaperFonts { + PaperFonts::in_dirs(font_dirs(), family, language, wait) + } + + /// [`PaperFonts::new`] with the fonts installed in `dirs`. + pub fn in_dirs(dirs: Vec, family: &str, language: &str, wait: bool) -> PaperFonts { + PaperFonts { family: family.to_string(), language: language.to_string(), dirs, wait } + } + + /// `text` measured as [`paper_lines`] measures it. + pub fn lines(&self, text: &str, height: f64, wrap: Option, style: &TextStyle) -> Result { + measure(&self.dirs, text, Some(&self.family), &self.language, height, wrap, style, self.wait) + } + + /// `text` set as [`paper_text`] sets it. + pub fn text(&self, text: &str, at: Vec2, height: f64, angle: f64, style: &TextStyle) -> Result, Vec)>> { + set_text(&self.dirs, text, Some(&self.family), &self.language, (at, height, angle), style, self.wait) + } +} + +/// What became of a text set on a sheet: set; pending until the search for a font to fall back to +/// answers (the window's sheet only); or refused, with why. +#[derive(Debug)] +pub enum Outcome { + Set(T), + Pending, + Refused(String), +} + +impl From> for Outcome { + fn from(set: Result) -> Outcome { + match set { + Ok(set) => Outcome::Set(set), + Err(SketchError::FontPending) => Outcome::Pending, + Err(why) => Outcome::Refused(why.to_string()), + } + } +} + #[allow(clippy::too_many_arguments)] fn measure( dirs: &[PathBuf], diff --git a/crates/sketch/src/paper_text_tests.rs b/crates/sketch/src/paper_text_tests.rs index 8d046fd5a..0a47ab3c5 100644 --- a/crates/sketch/src/paper_text_tests.rs +++ b/crates/sketch/src/paper_text_tests.rs @@ -484,6 +484,32 @@ fn the_windows_way_in_waits_for_no_search() { assert_eq!((runs.len(), fonts.len()), (2, 2)); } +/// A sheet's texts take one of three outcomes, the same rule whichever way in: asking is pending +/// until the search answers, then sets what waiting sets; waiting is never pending; a character no +/// font has is refused with why. +#[test] +fn a_sheets_text_is_set_pending_or_refused() { + let folder = Folder::new("outcomes", &["a.ttf"]); + let (asking, waiting) = (PaperFonts::in_dirs(folder.dirs(), "", "", false), PaperFonts::in_dirs(folder.dirs(), "", "", true)); + let plain = TextStyle::default(); + assert!(matches!(Outcome::from(asking.lines("A \u{30c6}", 5.0, None, &plain)), Outcome::Pending)); + let Outcome::Set(asked) = Outcome::from(answered(|| asking.lines("A \u{30c6}", 5.0, None, &plain))) else { panic!("not set") }; + let Outcome::Set(waited) = Outcome::from(waiting.lines("A \u{30c6}", 5.0, None, &plain)) else { panic!("not set") }; + assert_eq!(asked.lines, waited.lines); + let set = |text: std::result::Result, Vec)>, SketchError>| match Outcome::from(text) { + Outcome::Set(Some((runs, _))) => runs.len(), + _ => 0, + }; + assert_eq!(set(answered(|| asking.text("A\u{30c6}", AT, 5.0, 0.0, &plain))), 2); + assert_eq!(set(waiting.text("A\u{30c6}", AT, 5.0, 0.0, &plain)), 2); + let refused = |text: std::result::Result, Vec)>, SketchError>| match Outcome::from(text) { + Outcome::Refused(why) => why, + _ => String::new(), + }; + assert!(refused(answered(|| asking.text("Size \u{1200}", AT, 5.0, 0.0, &plain))).contains("no glyph for '\u{1200}'")); + assert!(refused(waiting.text("Size \u{1200}", AT, 5.0, 0.0, &plain)).contains("no glyph for '\u{1200}'")); +} + #[test] fn a_font_fallen_back_to_takes_its_nearest_style() { // BIZ UDPGothic has no italic and no bold: it leans, and bold isn't refused for it.