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 <noreply@anthropic.com>
This commit is contained in:
CamWightandClaude Opus 5.5 authored and GitHub committed 2026-11-06 23:00:08 -05:00
1 parent d506dd6552
commit e50e7e1bf6
9 files changed
+118 -65

No files matched your search

+4 -1
View File
@@ -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"}),
+12 -29
View File
@@ -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<OnPaper> {
/// `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<OnPaper> {
let line_mm = LineKind::Thin.width_mm(group);
let on: Vec<Vec2> = resolved.points.iter().map(|p| frame.to_paper(*p)).collect();
let mut lines: Vec<Vec<Vec2>> = 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));
@@ -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<OnPaper> {
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,
+4 -2
View File
@@ -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<DrawnDimension> {
let ids: Vec<u64> = 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<Dimension>| 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<CellText> {
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),
+13 -29
View File
@@ -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<f64>, 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<f64>, &TextStyle) -> Result<PaperLines, SketchError>,
value: &str,
notes: &mut Vec<Note>,
) -> Result<(PaperLines, Vec2), SketchError> {
fn value_lines(text: &TextCell, value_box: Aabb2, fonts: &PaperFonts, value: &str, notes: &mut Vec<Note>) -> 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));
@@ -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;
+2 -1
View File
@@ -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;
+53 -1
View File
@@ -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<PathBuf>,
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<PathBuf>, 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<f64>, style: &TextStyle) -> Result<PaperLines> {
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<Option<(Vec<TextRun>, Vec<Font>)>> {
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<T> {
Set(T),
Pending,
Refused(String),
}
impl<T> From<Result<T>> for Outcome<T> {
fn from(set: Result<T>) -> Outcome<T> {
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],
+26
View File
@@ -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<Option<(Vec<TextRun>, Vec<Font>)>, 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<Option<(Vec<TextRun>, Vec<Font>)>, 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.