@@ -561,29 +561,41 @@ export class FontManager {
561561
562562 /**
563563 * Tries to find the specified font. If not found, uses a replacement font and returns its name.
564+ *
565+ * When the requested face is not loaded, the original name is recorded in
566+ * {@link missedFonts} and {@link events.fontNotFound} is dispatched (once per
567+ * miss cycle) so hosts can surface missing-font UI. Lazy loading still
568+ * schedules {@link requestFont} for the face that will be fetched.
569+ *
564570 * @param fontName - The font name to find
565571 * @returns The original font name if found, or the replacement font name if not found
566572 */
567573 findAndReplaceFont ( fontName : string ) {
568574 const requested = fontName == null ? '' : String ( fontName )
575+ const missName = this . stripFontFileExtension ( requested )
569576 let font = this . loadedFontMap . get ( requested . toLowerCase ( ) )
577+ if ( font == null && missName && missName !== requested ) {
578+ font = this . loadedFontMap . get ( missName . toLowerCase ( ) )
579+ }
570580 if ( font == null ) {
571581 const mappedFontName = this . fontMapping [ requested ]
572582 if ( mappedFontName ) {
573583 font = this . loadedFontMap . get ( mappedFontName . toLowerCase ( ) )
574584 if ( ! font && this . lazyFontLoading ) {
575585 void this . requestFont ( mappedFontName )
576586 }
587+ // Drawing still asked for the original face — report it even when a
588+ // mapping supplies a replacement (do not also request the original).
589+ this . recordMissedFonts ( missName , false )
577590 // Prefer the mapped face even while it is still loading (legacy behavior).
578591 return mappedFontName
579592 }
580593 }
581594 if ( font ) {
582595 return requested
583596 }
584- if ( this . lazyFontLoading && requested ) {
585- void this . requestFont ( requested )
586- }
597+ // Not loaded and no mapping — record miss (schedules lazy load once).
598+ this . recordMissedFonts ( missName , true )
587599 // Prefer non-BIGFONT defaults as the primary face. BIGFONT SHX files (hztxt,
588600 // gbcbig, …) map ASCII to GBK fullwidth cells (0xA3xx), which makes Latin
589601 // runs much wider than AutoCAD and triggers false MTEXT wrapping when the
@@ -815,11 +827,96 @@ export class FontManager {
815827 return this . loadedFontMap . has ( fontName . toLowerCase ( ) )
816828 }
817829
830+ /**
831+ * Applies a missed-font report from another isolate (e.g. a web worker).
832+ * Updates {@link missedFonts} and dispatches {@link events.fontNotFound} on
833+ * first sighting. Does not call {@link requestFont} — the remote isolate owns loading.
834+ */
835+ applyRemoteFontNotFound ( fontName : string , count : number = 1 ) : void {
836+ if ( ! fontName ) {
837+ return
838+ }
839+ const prev = this . missedFonts [ fontName ] ?? 0
840+ this . missedFonts [ fontName ] = Math . max ( prev , count )
841+ if ( prev === 0 ) {
842+ this . events . fontNotFound . dispatch ( {
843+ fontName,
844+ count : this . missedFonts [ fontName ]
845+ } )
846+ }
847+ }
848+
849+ /**
850+ * Clears a missed-font entry after a remote isolate reports the font loaded.
851+ * Call before dispatching {@link events.fontLoaded} on the main thread.
852+ */
853+ applyRemoteFontLoaded ( fontName : string ) : void {
854+ if ( ! fontName ) {
855+ return
856+ }
857+ this . clearMissedFontEntry ( fontName )
858+ this . fontRequestFailed . delete ( fontName . toLowerCase ( ) )
859+ }
860+
861+ /**
862+ * Replaces session-scoped missed-font bookkeeping without dispatching events.
863+ * Entries for faces that are already loaded are dropped.
864+ */
865+ replaceMissedFonts ( fonts : Record < string , number > ) : void {
866+ const next : Record < string , number > = { }
867+ for ( const [ name , count ] of Object . entries ( fonts ?? { } ) ) {
868+ if ( ! name || this . isFontLoaded ( name ) ) {
869+ continue
870+ }
871+ next [ name ] = count
872+ }
873+ this . missedFonts = next
874+ }
875+
876+ /**
877+ * Clears {@link missedFonts}. Does not unload faces or cancel in-flight loads.
878+ */
879+ clearMissedFonts ( ) : void {
880+ this . missedFonts = { }
881+ }
882+
883+ /**
884+ * Strips a trailing `.ttf` / `.otf` / `.woff` / `.shx` extension when present.
885+ * Matches the normalization used by {@link getFontByName}.
886+ */
887+ private stripFontFileExtension ( fontName : string ) : string {
888+ if ( ! fontName ) {
889+ return fontName
890+ }
891+ const dotIndex = fontName . lastIndexOf ( '.' )
892+ if (
893+ ( dotIndex > 0 && dotIndex == fontName . length - 4 ) ||
894+ dotIndex == fontName . length - 5
895+ ) {
896+ return fontName . substring ( 0 , dotIndex )
897+ }
898+ return fontName
899+ }
900+
901+ /**
902+ * Removes {@link missedFonts} entries whose name matches `fontName`
903+ * case-insensitively (CAD style names and loaded face names often differ in case).
904+ */
905+ private clearMissedFontEntry ( fontName : string ) : void {
906+ const key = fontName . toLowerCase ( )
907+ for ( const name of Object . keys ( this . missedFonts ) ) {
908+ if ( name . toLowerCase ( ) === key ) {
909+ delete this . missedFonts [ name ]
910+ }
911+ }
912+ }
913+
818914 /**
819915 * Records a font that was requested but not found
820916 * @param fontName - The name of the font that was not found
917+ * @param scheduleLoad - When true (default), also {@link requestFont} on first miss if lazy loading is on
821918 */
822- private recordMissedFonts ( fontName : string ) {
919+ private recordMissedFonts ( fontName : string , scheduleLoad : boolean = true ) {
823920 if ( fontName ) {
824921 if ( ! this . missedFonts [ fontName ] ) {
825922 this . missedFonts [ fontName ] = 0
@@ -831,7 +928,7 @@ export class FontManager {
831928 fontName : fontName ,
832929 count : this . missedFonts [ fontName ]
833930 } )
834- if ( this . lazyFontLoading ) {
931+ if ( scheduleLoad && this . lazyFontLoading ) {
835932 void this . requestFont ( fontName )
836933 }
837934 }
@@ -892,7 +989,7 @@ export class FontManager {
892989 if ( epoch !== this . loadEpoch || ! this . isFontLoaded ( fontName ) ) {
893990 return
894991 }
895- delete this . missedFonts [ fontName ]
992+ this . clearMissedFontEntry ( fontName )
896993 this . fontRequestFailed . delete ( fontName . toLowerCase ( ) )
897994 this . events . fontLoaded . dispatch ( {
898995 fontName : fontName
0 commit comments