From 39ec25102451ec051d626d650e64a8a1ee8e2e39 Mon Sep 17 00:00:00 2001 From: mlight lee Date: Mon, 7 Sep 2026 08:19:41 +0800 Subject: [PATCH] fix: prefer mesh defaults over BIGFONT and correct MTEXT width factor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Missing style fonts (e.g. 标准) were falling back to hztxt BIGFONT, which maps ASCII to fullwidth cells and falsely wraps Latin MTEXT. Also align absolute/relative \W with AutoCAD semantics. --- .../src/font/defaultFontsPresets.ts | 10 +- .../mtext-renderer/src/font/fontManager.ts | 39 ++++- .../src/renderer/mtextProcessor.ts | 8 +- .../test/font/fontManager.test.ts | 2 +- .../mtext.missing-font-latin-wrap.test.ts | 137 ++++++++++++++++++ 5 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 packages/mtext-renderer/test/renderer/mtext.missing-font-latin-wrap.test.ts diff --git a/packages/mtext-renderer/src/font/defaultFontsPresets.ts b/packages/mtext-renderer/src/font/defaultFontsPresets.ts index 9c58ec2..f3f4107 100644 --- a/packages/mtext-renderer/src/font/defaultFontsPresets.ts +++ b/packages/mtext-renderer/src/font/defaultFontsPresets.ts @@ -9,11 +9,11 @@ export type DefaultFontsPreset = | 'minimal' /** Classic R12/R14 stack: SHX basics, GB big font, then mesh CJK and AMGDT. */ | 'r12r14' - /** Later-era stack: hztxt big font with simsun and AMGDT symbols. */ + /** Later-era stack: simsun first for Latin metrics, then hztxt big font. */ | 'modern' /** Western SHX fonts plus simsun and AMGDT; no CJK-specific SHX big fonts. */ | 'international' - /** Broad CJK coverage: both GB big-font SHX files plus common mesh fallbacks. */ + /** Broad CJK coverage: simsun first, then GB big-font SHX files. */ | 'cjk' /** @@ -27,9 +27,11 @@ export const DEFAULT_FONTS_PRESETS: Record< > = { minimal: ['txt', 'simsun'], r12r14: ['txt', 'simplex', 'romans', 'gbcbig', 'simsun'], - modern: ['hztxt', 'simsun'], + // Mesh/TTF first so missing style fonts (e.g. "标准") get correct Latin metrics. + // BIGFONT SHX stays available via glyph fallback for CJK coverage. + modern: ['simsun', 'hztxt'], international: ['txt', 'simplex', 'romans', 'simsun'], - cjk: ['gbcbig', 'hztxt', 'simsun'] + cjk: ['simsun', 'gbcbig', 'hztxt'] } /** diff --git a/packages/mtext-renderer/src/font/fontManager.ts b/packages/mtext-renderer/src/font/fontManager.ts index ab06016..8dd5cf9 100644 --- a/packages/mtext-renderer/src/font/fontManager.ts +++ b/packages/mtext-renderer/src/font/fontManager.ts @@ -584,10 +584,17 @@ export class FontManager { if (this.lazyFontLoading && requested) { void this.requestFont(requested) } - for (const defaultFontName of this.defaultFonts) { - if (this.loadedFontMap.has(defaultFontName.toLowerCase())) { - return defaultFontName - } + // Prefer non-BIGFONT defaults as the primary face. BIGFONT SHX files (hztxt, + // gbcbig, …) map ASCII to GBK fullwidth cells (0xA3xx), which makes Latin + // runs much wider than AutoCAD and triggers false MTEXT wrapping when the + // style font is missing (e.g. Chinese styles named "标准"). + const loadedDefault = this.pickLoadedDefaultFont(true) + if (loadedDefault) { + return loadedDefault + } + const anyLoadedDefault = this.pickLoadedDefaultFont(false) + if (anyLoadedDefault) { + return anyLoadedDefault } const firstDefault = [...this.defaultFonts][0] ?? '' if (firstDefault && this.lazyFontLoading) { @@ -596,6 +603,30 @@ export class FontManager { return firstDefault } + /** + * Returns the first loaded font from {@link defaultFonts}. + * + * @param skipBigfont When true, ignores SHX BIGFONT faces so ASCII/Latin text + * is not forced through fullwidth CJK glyph cells. + */ + private pickLoadedDefaultFont(skipBigfont: boolean): string | undefined { + for (const defaultFontName of this.defaultFonts) { + const loaded = this.loadedFontMap.get(defaultFontName.toLowerCase()) + if (!loaded) { + continue + } + if ( + skipBigfont && + loaded.type === 'shx' && + (loaded.data as ShxFontData).header?.fontType === ShxFontType.BIGFONT + ) { + continue + } + return defaultFontName + } + return undefined + } + /** * Gets font by font name. Return undefined if not found. * @param fontName - The font name to find diff --git a/packages/mtext-renderer/src/renderer/mtextProcessor.ts b/packages/mtext-renderer/src/renderer/mtextProcessor.ts index 70547d3..f4c3e35 100644 --- a/packages/mtext-renderer/src/renderer/mtextProcessor.ts +++ b/packages/mtext-renderer/src/renderer/mtextProcessor.ts @@ -729,6 +729,10 @@ export class MTextProcessor { /** * Apply width factor changes, resolving relative factors to absolute values. + * + * Absolute `\Wvalue;` replaces the current width factor. Relative `\Wvaluex;` + * multiplies the current width factor (AutoCAD / ezdxf semantics). + * * @param widthFactor Width factor change data. */ private applyWidthFactorChange( @@ -737,12 +741,12 @@ export class MTextProcessor { if (!widthFactor) return if (widthFactor.isRelative) { this._currentContext.widthFactor = { - value: widthFactor.value * this.maxWidth, + value: widthFactor.value * this._currentContext.widthFactor.value, isRelative: false } } else { this._currentContext.widthFactor = { - value: widthFactor.value * 0.85, + value: widthFactor.value, isRelative: false } } diff --git a/packages/mtext-renderer/test/font/fontManager.test.ts b/packages/mtext-renderer/test/font/fontManager.test.ts index f76fa9f..8ee1522 100644 --- a/packages/mtext-renderer/test/font/fontManager.test.ts +++ b/packages/mtext-renderer/test/font/fontManager.test.ts @@ -444,7 +444,7 @@ describe('FontManager', () => { await manager.loadDefaultFont() - expect(loader.load).toHaveBeenCalledWith(['hztxt', 'simsun', 'simplex', 'amgdt']) + expect(loader.load).toHaveBeenCalledWith(['simsun', 'hztxt', 'simplex', 'amgdt']) }) it('resolves fonts by alias names after loading', async () => { diff --git a/packages/mtext-renderer/test/renderer/mtext.missing-font-latin-wrap.test.ts b/packages/mtext-renderer/test/renderer/mtext.missing-font-latin-wrap.test.ts new file mode 100644 index 0000000..2e36988 --- /dev/null +++ b/packages/mtext-renderer/test/renderer/mtext.missing-font-latin-wrap.test.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three' +import { afterEach, describe, expect, it } from 'vitest' + +import { FontData } from '../../src/font/font' +import { FontFactory } from '../../src/font/fontFactory' +import { FontManager } from '../../src/font/fontManager' +import { MText } from '../../src/renderer/mtext' +import { + createDefaultColorSettings, + MTextAttachmentPoint, + TextStyle +} from '../../src/renderer/types' + +const FONT_BASE = 'https://cdn.jsdelivr.net/gh/mlightcad/cad-data/fonts/' + +async function loadFont( + name: string, + file: string, + encoding?: string +): Promise { + const response = await fetch(FONT_BASE + file) + if (!response.ok) throw new Error(`Failed to fetch ${file}`) + const type = file.endsWith('.shx') ? 'shx' : 'mesh' + const fontData: FontData = { + name, + type, + data: await response.arrayBuffer(), + encoding, + alias: [name] + } + const font = FontFactory.instance.createFont(fontData) + font.names.add(name) + ;( + FontManager.instance as unknown as { loadedFontMap: Map } + ).loadedFontMap.set(name, font) +} + +const styleManager = { + unsupportedTextStyles: {}, + getMeshBasicMaterial: () => new THREE.MeshBasicMaterial(), + getLineBasicMaterial: () => new THREE.LineBasicMaterial() +} + +/** + * Regression for drawings whose text style font name is missing from the CDN + * (common Chinese styles such as "标准"). Falling back to hztxt BIGFONT mapped + * ASCII to fullwidth cells and made Latin MTEXT wrap incorrectly. + */ +describe('missing style font Latin wrap width', () => { + const style: TextStyle = { + name: '标准', + standardFlag: 0, + fixedTextHeight: 0, + widthFactor: 0.667, + obliqueAngle: 0, + textGenerationFlag: 0, + lastHeight: 3, + font: '标准', + bigFont: '' + } + + afterEach(() => { + FontManager.instance.release() + FontManager.instance.enableFontCache = true + }) + + it( + 'keeps FYA/G-AE-01-01-2018 on one line inside AutoCAD defined width', + async () => { + FontManager.instance.release() + FontManager.instance.enableFontCache = false + FontManager.instance.setDefaultFonts('modern') + await loadFont('hztxt', 'hztxt.shx', 'gbk') + await loadFont('simsun', 'simsun.woff') + + expect(FontManager.instance.findAndReplaceFont('标准')).toBe('simsun') + + const wrapWidth = 31.945271 + const autocadExtents = 29.527606 + + const unconstrained = new MText( + { + text: '{\\W0.667;\\T1.1;FYA/G-AE-01-01-2018}', + height: 3, + width: 0, + position: { x: 0, y: 0, z: 0 }, + attachmentPoint: MTextAttachmentPoint.TopLeft, + collectCharBoxes: true + }, + style, + styleManager as any, + FontManager.instance as any, + createDefaultColorSettings() + ) + unconstrained.syncDraw() + const contentWidth = + unconstrained.box.max.x - unconstrained.box.min.x + expect(contentWidth).toBeCloseTo(autocadExtents, 1) + expect(contentWidth).toBeLessThan(wrapWidth) + + const wrapped = new MText( + { + text: '{\\W0.667;\\T1.1;FYA/G-AE-01-01-2018}', + height: 3, + width: wrapWidth, + position: { x: 0, y: 0, z: 0 }, + attachmentPoint: MTextAttachmentPoint.MiddleRight, + collectCharBoxes: true + }, + style, + styleManager as any, + FontManager.instance as any, + createDefaultColorSettings() + ) + wrapped.syncDraw() + + const height = wrapped.box.max.y - wrapped.box.min.y + // Single visual line: height stays near one cap-height, not ~2× after wrap. + expect(height).toBeLessThan(5) + }, + 120_000 + ) + + it( + 'prefers simsun over hztxt even when hztxt is listed first in defaults', + async () => { + FontManager.instance.release() + FontManager.instance.enableFontCache = false + FontManager.instance.setDefaultFonts(['hztxt', 'simsun']) + await loadFont('hztxt', 'hztxt.shx', 'gbk') + await loadFont('simsun', 'simsun.woff') + + expect(FontManager.instance.findAndReplaceFont('标准')).toBe('simsun') + }, + 120_000 + ) +})