From e50e7e1bf646dbbf4c8a26e2fbf5567af6fad280 Mon Sep 17 00:00:00 2001 From: CamWight <91156642+CamWight@users.noreply.github.com> Date: Sat, 7 Nov 2026 04:00:08 +0000 Subject: [PATCH] One way a sheet sets its text: wait for fonts or ask, then set, pending or refused (#4890) (#12846) The title block's text and a dimension's text each chose for themselves between waiting for the search for fonts to fall back to (an export, drawing.inspect) and asking it (the window's sheet), then each mapped the answer to set, pending or refused. That rule is now in one place, in sketch beside the fonts: PaperFonts holds the drawing's family and language with the font folders listed once, and sets lines or text the waiting or the asking way; Outcome is the one mapping of its answer. The title block's lay and the dimensions' paper_group take a PaperFonts, made once for each block and each read of dimensions. Nothing else changes: the same text in the same fonts, pending and refused in the same cases. A parts list's words on paper will use it too. Co-authored-by: Claude Opus 5.5 --- crates/engine/src/cmd/drawing.rs | 5 +- crates/engine/src/drawing_dimension_paper.rs | 41 +++++--------- .../src/drawing_dimension_paper_tests.rs | 5 +- crates/engine/src/drawing_scene.rs | 6 ++- crates/engine/src/title_block_text.rs | 42 +++++---------- crates/engine/src/title_block_text_tests.rs | 1 + crates/sketch/src/lib.rs | 3 +- crates/sketch/src/paper_text.rs | 54 ++++++++++++++++++- crates/sketch/src/paper_text_tests.rs | 26 +++++++++ 9 files changed, 118 insertions(+), 65 deletions(-) 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.