forked from 1b-agent/opencadstudio-upstream
feat(web): lazy per-script font loading for international CAD text
The web build has no system-font access, so CAD text in non-Latin scripts (Cyrillic, Greek, CJK, …) rendered as nothing — build_fallback was a hard None on wasm (#141). Instead, ship per-script Noto subsets under web/fonts (one alphabet per file, SIL OFL 1.1) and fetch them lazily over HTTP the first time a drawing uses that script, then outline glyphs with ttf-parser. - web_font: Script enum + script_of (char → script font), a per-script store with lazy fetch, and a pending queue drained by the app loop. - CJK is split by language (chinese/japanese/korean). Han ideographs share code points but differ by language, so the shared block is routed by the document's $DWGCODEPAGE (932→JP, 949→KR, GB/936→CN); kana is always Japanese, Hangul always Korean. Re-tessellates when the language changes. - build_fallback (wasm) outlines from the fetched subset; clear_fallback_cache lets glyphs that missed while a font was in flight reappear on load. - PollWebFonts subscription (web only) + WebFontLoaded message drive fetches. - Trunk copy-dir serves web/fonts; nothing is bundled into the native binary (desktop keeps using system fonts). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
d36cc9786f
commit
54106ca9b0
21 files changed
+453
-5
No files matched your search
@@ -794,6 +794,11 @@ pub enum DsField {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Tick(Instant),
|
||||
/// Web: periodic check for per-script fonts a drawing needs but hasn't
|
||||
/// fetched yet (#141). Native: never emitted.
|
||||
PollWebFonts,
|
||||
/// Web: a per-script font finished fetching — `Ok(bytes)` or `Err(reason)`.
|
||||
WebFontLoaded(crate::scene::text::web_font::Script, Result<Vec<u8>, String>),
|
||||
OpenFile,
|
||||
/// File picker returned. `Some((path, size_in_bytes))` → start loading;
|
||||
/// `None` → user cancelled the dialog (no overlay shown).
|
||||
|
||||
@@ -364,6 +364,42 @@ impl OpenCADStudio {
|
||||
|
||||
fn update_inner(&mut self, msg: Message) -> Task<Message> {
|
||||
match msg {
|
||||
// Web: a drawing referenced a script whose Noto subset isn't loaded
|
||||
// yet (recorded during text tessellation). Kick off one fetch per
|
||||
// pending script; the result comes back as `WebFontLoaded`. (#141)
|
||||
Message::PollWebFonts => {
|
||||
let pending = crate::scene::text::web_font::take_pending();
|
||||
if pending.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
Task::batch(pending.into_iter().map(|script| {
|
||||
Task::perform(crate::scene::text::web_font::fetch(script), move |res| {
|
||||
Message::WebFontLoaded(script, res)
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// Web: a per-script font arrived. Store it, drop the stale fallback
|
||||
// glyph cache (entries that resolved to nothing while it loaded),
|
||||
// and re-tessellate so the text appears. (#141)
|
||||
Message::WebFontLoaded(script, res) => {
|
||||
match res {
|
||||
Ok(bytes) => {
|
||||
crate::scene::text::web_font::insert(script, Some(bytes));
|
||||
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
||||
for tab in self.tabs.iter_mut() {
|
||||
tab.scene.bump_geometry();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
crate::scene::text::web_font::insert(script, None);
|
||||
self.command_line
|
||||
.push_error(&format!("Font load failed ({script:?}): {e}"));
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::Tick(t) => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.update(t - self.start);
|
||||
@@ -500,6 +536,15 @@ impl OpenCADStudio {
|
||||
|
||||
self.tabs[i].current_path = Some(path.clone());
|
||||
self.tabs[i].scene.document = doc;
|
||||
// Route shared CJK ideographs to the language matching this
|
||||
// drawing's code page (web per-language font split). Drop the
|
||||
// glyph cache if it changed so Han re-resolves to the new
|
||||
// language's font; geometry is (re)built below regardless. (#141)
|
||||
if crate::scene::text::web_font::set_cjk_lang_from_codepage(
|
||||
&self.tabs[i].scene.document.header.code_page,
|
||||
) {
|
||||
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
||||
}
|
||||
// Current model-space annotation scale comes from the drawing's
|
||||
// CANNOSCALEVALUE (paper/drawing factor); the multiplier we use
|
||||
// for text/dim sizing is its inverse (1:50 -> 0.02 -> 50.0).
|
||||
@@ -1298,6 +1343,14 @@ impl OpenCADStudio {
|
||||
self.sync_ribbon_from_selection();
|
||||
// Grid/snap follow the newly active drawing's viewport.
|
||||
self.adopt_view_display(idx);
|
||||
// Shared CJK ideographs follow the newly active drawing's
|
||||
// language; re-tessellate if it differs from the last. (#141)
|
||||
if crate::scene::text::web_font::set_cjk_lang_from_codepage(
|
||||
&self.tabs[idx].scene.document.header.code_page,
|
||||
) {
|
||||
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
||||
self.tabs[idx].scene.bump_geometry();
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
@@ -1630,11 +1630,20 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
Subscription::none()
|
||||
};
|
||||
// Web: poll for per-script fonts that a drawing's text needs but hasn't
|
||||
// fetched yet. Cheap — `PollWebFonts` is a no-op when nothing is
|
||||
// pending. Native has system fonts, so no polling. (#141)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let web_fonts =
|
||||
iced::time::every(std::time::Duration::from_millis(300)).map(|_| Message::PollWebFonts);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let web_fonts = Subscription::none();
|
||||
iced::Subscription::batch([
|
||||
frames,
|
||||
history_tick,
|
||||
grip_dwell,
|
||||
caret_blink,
|
||||
web_fonts,
|
||||
event::listen_with(|ev, status, win_id| {
|
||||
use iced::event::Status;
|
||||
match ev {
|
||||
|
||||
Reference in new issue
Block a user