Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/mtext-renderer/src/font/defaultFontsPresets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand All @@ -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']
}

/**
Expand Down
39 changes: 35 additions & 4 deletions packages/mtext-renderer/src/font/fontManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/mtext-renderer/src/renderer/mtextProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/mtext-renderer/test/font/fontManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
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<string, unknown> }
).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
)
})