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
6 changes: 5 additions & 1 deletion packages/example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,10 @@ <h3 class="panel-title">Rendering</h3>
<input type="checkbox" id="lazy-font-loading" checked>
<label for="lazy-font-loading">Lazy Font Loading</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="await-fonts-before-draw">
<label for="await-fonts-before-draw">Await Fonts Before Draw</label>
</div>
</div>
</div>
</section>
Expand Down Expand Up @@ -564,7 +568,7 @@ <h3 class="panel-title">Actions</h3>
</div>
<p class="hint" style="margin-top: 6px;">Accepts .shx / .ttf / .otf / .woff. Cached entries show <code>[cached]</code> in the lists.</p>
</div>
<p class="hint">Lazy Font Loading skips preload and redraws on <code>fontLoaded</code>. Use Release Fonts, then Render to compare.</p>
<p class="hint">Lazy Font Loading skips preload. Unchecked Await Fonts redraws on <code>fontLoaded</code>; checked waits for fonts before the first draw. Use Release Fonts, then Render to compare.</p>
</div>
</details>

Expand Down
18 changes: 18 additions & 0 deletions packages/example/src/exampleFontManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ export class ExampleFontManager {
return FontManager.instance.lazyFontLoading
}

/**
* @returns Whether {@link FontManager.awaitFontsBeforeDraw} is currently
* enabled.
*/
isAwaitFontsBeforeDraw(): boolean {
return FontManager.instance.awaitFontsBeforeDraw
}

/**
* Mirrors the UI checkbox onto {@link FontManager.lazyFontLoading} and any
* existing worker pool.
Expand All @@ -72,6 +80,16 @@ export class ExampleFontManager {
await this.unifiedRenderer.setLazyFontLoading(enabled)
}

/**
* Mirrors the UI checkbox onto {@link FontManager.awaitFontsBeforeDraw} and
* any existing worker pool.
*
* @param enabled - When true with lazy loading, draw waits for referenced fonts.
*/
async setAwaitFontsBeforeDraw(enabled: boolean): Promise<void> {
await this.unifiedRenderer.setAwaitFontsBeforeDraw(enabled)
}

/**
* Fonts that non-lazy mode should preload for the active preset and selects.
*/
Expand Down
65 changes: 62 additions & 3 deletions packages/example/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ class MTextRendererExample {
private readonly defaultFontsPresetSelect: HTMLSelectElement
/** `#lazy-font-loading` — toggles {@link FontManager.lazyFontLoading}. */
private readonly lazyFontLoadingCheckbox: HTMLInputElement
/**
* `#await-fonts-before-draw` — toggles {@link FontManager.awaitFontsBeforeDraw}
* (only meaningful when lazy font loading is on).
*/
private readonly awaitFontsBeforeDrawCheckbox: HTMLInputElement
/** DXF layer name passed in {@link getColorSettings} for ByLayer color resolution. */
private readonly defaultLayerName = '0'

Expand Down Expand Up @@ -211,6 +216,9 @@ class MTextRendererExample {
this.lazyFontLoadingCheckbox = document.getElementById(
'lazy-font-loading'
) as HTMLInputElement
this.awaitFontsBeforeDrawCheckbox = document.getElementById(
'await-fonts-before-draw'
) as HTMLInputElement
this.fontManager = new ExampleFontManager(
this.unifiedRenderer,
this.statusDiv,
Expand All @@ -224,6 +232,11 @@ class MTextRendererExample {
void this.fontManager.setLazyFontLoading(
this.lazyFontLoadingCheckbox.checked
)
this.syncAwaitFontsBeforeDrawCheckbox()
void this.fontManager.setAwaitFontsBeforeDraw(
this.lazyFontLoadingCheckbox.checked &&
this.awaitFontsBeforeDrawCheckbox.checked
)
FontManager.instance.events.fontLoaded.addEventListener(payload => {
this.onLazyFontLoaded(payload.fontName)
})
Expand Down Expand Up @@ -411,6 +424,12 @@ class MTextRendererExample {
await this.applyLazyFontLoadingMode(this.lazyFontLoadingCheckbox.checked)
})

this.awaitFontsBeforeDrawCheckbox.addEventListener('change', async () => {
await this.applyAwaitFontsBeforeDrawMode(
this.awaitFontsBeforeDrawCheckbox.checked
)
})

document.querySelectorAll('.example-btn').forEach(button => {
button.addEventListener('click', async () => {
const exampleType = (button as HTMLElement).dataset
Expand Down Expand Up @@ -523,12 +542,18 @@ class MTextRendererExample {
*/
private async applyLazyFontLoadingMode(enabled: boolean): Promise<void> {
await this.fontManager.setLazyFontLoading(enabled)
this.syncAwaitFontsBeforeDrawCheckbox()
await this.fontManager.setAwaitFontsBeforeDraw(
enabled && this.awaitFontsBeforeDrawCheckbox.checked
)
if (enabled) {
FontManager.instance.release()
this.unifiedRenderer.terminateWorkers()
await this.fontManager.applyDefaultFontsPreset()
this.statusDiv.textContent =
'Lazy font loading ON — fonts released; next render loads on demand'
const awaitFonts = this.awaitFontsBeforeDrawCheckbox.checked
this.statusDiv.textContent = awaitFonts
? 'Lazy font loading ON + await — next render waits for fonts, then draws once'
: 'Lazy font loading ON — fonts released; next render loads on demand'
this.statusDiv.style.color = '#0f0'
} else {
await this.fontManager.applyDefaultFontsPreset()
Expand All @@ -540,6 +565,32 @@ class MTextRendererExample {
await this.renderCurrentContent()
}

/**
* Enables/disables the Await Fonts checkbox to match Lazy Font Loading, and
* keeps {@link FontManager.awaitFontsBeforeDraw} in sync.
*/
private syncAwaitFontsBeforeDrawCheckbox(): void {
const lazy = this.lazyFontLoadingCheckbox.checked
this.awaitFontsBeforeDrawCheckbox.disabled = !lazy
}

/**
* Applies the Await Fonts Before Draw checkbox when lazy loading is enabled.
*/
private async applyAwaitFontsBeforeDrawMode(enabled: boolean): Promise<void> {
if (!this.lazyFontLoadingCheckbox.checked) {
this.syncAwaitFontsBeforeDrawCheckbox()
return
}
await this.fontManager.setAwaitFontsBeforeDraw(enabled)
this.statusDiv.textContent = enabled
? 'Await fonts ON — render waits for download/load, then draws once'
: 'Await fonts OFF — render draws with fallbacks, then redraws on fontLoaded'
this.statusDiv.style.color = '#0f0'
this.beginUserRender()
await this.renderCurrentContent()
}

/**
* Handles {@link FontManager.events.fontLoaded} by scheduling a debounced redraw
* so glyph fallbacks are replaced once background loads finish.
Expand All @@ -548,6 +599,10 @@ class MTextRendererExample {
if (!this.fontManager.isLazyFontLoading()) {
return
}
// Draw already waited for referenced fonts — no progressive redraw needed.
if (this.fontManager.isAwaitFontsBeforeDraw()) {
return
}
if (!this.lazyFontsLoadedSinceRender.includes(fontName)) {
this.lazyFontsLoadedSinceRender.push(fontName)
}
Expand Down Expand Up @@ -1042,8 +1097,12 @@ class MTextRendererExample {

/** Appends lazy/non-lazy and thread mode details to a render status line. */
private formatRenderStatus(base: string, lazy: boolean): string {
const mode = lazy ? 'lazy' : 'non-lazy'
const awaitFonts = this.fontManager.isAwaitFontsBeforeDraw()
const mode = lazy ? (awaitFonts ? 'lazy+await' : 'lazy') : 'non-lazy'
const thread = this.renderModeSelect.value
if (lazy && awaitFonts) {
return `${base} (${mode}, ${thread}) · waited for fonts before draw`
}
if (lazy && this.lazyFontsLoadedSinceRender.length > 0) {
return `${base} (${mode}, ${thread}) · loaded [${this.lazyFontsLoadedSinceRender.join(', ')}]`
}
Expand Down
2 changes: 1 addition & 1 deletion packages/mtext-renderer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mlightcad/mtext-renderer",
"version": "0.12.2",
"version": "0.12.3",
"description": "AutoCAD MText renderer based on Three.js",
"license": "MIT",
"author": "MLight Lee <mlight.lee@outlook.com>",
Expand Down
24 changes: 18 additions & 6 deletions packages/mtext-renderer/src/font/fontManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,19 @@ export class FontManager {
/**
* When true (default), missing fonts are fetched/parsed in the background
* via {@link requestFont} instead of requiring an open-time preload.
* Drawing continues with temporary fallbacks until {@link events.fontLoaded}.
* Drawing continues with temporary fallbacks until {@link events.fontLoaded}
* unless {@link awaitFontsBeforeDraw} (or a per-draw override) waits first.
*/
public lazyFontLoading = true
/**
* When true, {@link MText.asyncDraw} / {@link Shape.asyncDraw} wait for fonts
* referenced by the content and style to finish loading before building
* geometry. Useful with {@link lazyFontLoading} when callers prefer a single
* draw pass instead of redrawing on {@link events.fontLoaded}.
*
* Has no effect when {@link lazyFontLoading} is false (draw already awaits).
*/
public awaitFontsBeforeDraw = false
/**
* Default fonts to use when a requested font is not found or lacks a glyph.
* Insertion order is preserved; earlier entries are tried first.
Expand Down Expand Up @@ -360,12 +370,14 @@ export class FontManager {
}

/**
* Fire-and-forget {@link requestFont} for each name (deduped per name).
* Requests each font name via {@link requestFont} (deduped per name).
* Callers may ignore the returned promise for fire-and-forget loads, or
* await it when they need fonts before drawing.
*/
requestFonts(fontNames: readonly string[]): void {
for (const name of fontNames) {
void this.requestFont(name)
}
requestFonts(fontNames: readonly string[]): Promise<FontLoadStatus[]> {
return Promise.all(fontNames.map(name => this.requestFont(name))).then(
results => results.flat()
)
}

private normalizeFontName(fontName: string): string {
Expand Down
38 changes: 32 additions & 6 deletions packages/mtext-renderer/src/renderer/mtext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ const tempPoint2 = /*@__PURE__*/ new THREE.Vector3()
const tempPoint3 = /*@__PURE__*/ new THREE.Vector3()
const AxisX = /*@__PURE__*/ new THREE.Vector3(1, 0, 0)

/**
* Options for {@link MText.asyncDraw} / {@link Shape.asyncDraw}.
*/
export interface MTextDrawOptions {
/**
* Wait for fonts referenced by the content/style to finish loading before
* building geometry.
*
* Defaults to `true` when {@link FontManager.lazyFontLoading} is false, or
* when {@link FontManager.awaitFontsBeforeDraw} is true. Otherwise fonts are
* scheduled in the background and the first draw may use fallbacks.
*/
awaitFonts?: boolean
}

/**
* Axis-aligned rectangle (plus baseline) describing the **logical MText frame**
* used to compute attachment-point offsets in {@link MText.loadMText}.
Expand Down Expand Up @@ -182,13 +197,15 @@ export class MText extends THREE.Object3D {
}

/**
* Draw the MText object. Schedules required fonts for background load and
* builds the object graph immediately with current fallbacks.
* Draw the MText object.
*
* Does not await downloads — callers that need glyphs from newly loaded fonts
* should redraw after {@link FontManager.events.fontLoaded}.
* With {@link FontManager.lazyFontLoading} and without awaiting fonts, this
* schedules downloads in the background and builds geometry immediately with
* current fallbacks — redraw after {@link FontManager.events.fontLoaded} if
* you need the final faces. Pass `{ awaitFonts: true }` or set
* {@link FontManager.awaitFontsBeforeDraw} to wait for referenced fonts first.
*/
async asyncDraw() {
async asyncDraw(options?: MTextDrawOptions) {
// Determine fonts used in the mtext string (without extensions)
const fonts = Array.from(MText.getFonts(this._mtextData.text || '', true))

Expand All @@ -208,8 +225,17 @@ export class MText extends THREE.Object3D {
}
}
if (fonts.length > 0) {
const awaitFonts =
options?.awaitFonts ??
(!this._fontManager.lazyFontLoading ||
this._fontManager.awaitFontsBeforeDraw)

if (this._fontManager.lazyFontLoading) {
this._fontManager.requestFonts(fonts)
if (awaitFonts) {
await this._fontManager.requestFonts(fonts)
} else {
void this._fontManager.requestFonts(fonts)
}
} else {
await this._fontManager.loadFontsByNames(fonts)
}
Expand Down
15 changes: 12 additions & 3 deletions packages/mtext-renderer/src/renderer/shape.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as THREE from 'three'

import { FontManager } from '../font'
import { MText } from './mtext'
import { MText, MTextDrawOptions } from './mtext'
import { StyleManager } from './styleManager'
import {
ColorSettings,
Expand Down Expand Up @@ -62,7 +62,7 @@ export class Shape extends THREE.Object3D {
return { lines: [], chars: [] }
}

async asyncDraw() {
async asyncDraw(options?: MTextDrawOptions) {
const fonts: string[] = []
if (!this._fontsInStyleLoaded) {
for (const key of ['font', 'bigFont', 'extendedFont'] as const) {
Expand All @@ -71,8 +71,17 @@ export class Shape extends THREE.Object3D {
}
}
if (fonts.length > 0) {
const awaitFonts =
options?.awaitFonts ??
(!this._fontManager.lazyFontLoading ||
this._fontManager.awaitFontsBeforeDraw)

if (this._fontManager.lazyFontLoading) {
this._fontManager.requestFonts(fonts)
if (awaitFonts) {
await this._fontManager.requestFonts(fonts)
} else {
void this._fontManager.requestFonts(fonts)
}
} else {
await this._fontManager.loadFontsByNames(fonts)
}
Expand Down
4 changes: 3 additions & 1 deletion packages/mtext-renderer/src/worker/mainThreadRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export class MainThreadRenderer implements MTextBaseRenderer {
/**
* Render MText directly in the main thread asynchronously. Fonts referenced by
* the text/style are scheduled via {@link FontManager.requestFonts} when
* {@link FontManager.lazyFontLoading} is enabled; otherwise they are awaited.
* {@link FontManager.lazyFontLoading} is enabled (unless
* {@link FontManager.awaitFontsBeforeDraw} waits for them first); otherwise
* they are awaited.
*/
async asyncRenderMText(
mtextContent: MTextData,
Expand Down
17 changes: 17 additions & 0 deletions packages/mtext-renderer/src/worker/mtextWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface WorkerMessage {
| 'loadFonts'
| 'setDefaultFonts'
| 'setLazyFontLoading'
| 'setAwaitFontsBeforeDraw'
| 'setFontUrl'
| 'getAvailableFonts'
| 'getMemoryStats'
Expand All @@ -43,6 +44,7 @@ interface WorkerResponse {
| 'loadFonts'
| 'setDefaultFonts'
| 'setLazyFontLoading'
| 'setAwaitFontsBeforeDraw'
| 'setFontUrl'
| 'getAvailableFonts'
| 'getMemoryStats'
Expand Down Expand Up @@ -162,6 +164,21 @@ self.addEventListener('message', async (event: MessageEvent<WorkerMessage>) => {
break
}

case 'setAwaitFontsBeforeDraw': {
if (!data) {
throw new Error('Missing data for setAwaitFontsBeforeDraw message')
}
const { enabled } = data as { enabled: boolean }
fontManager.awaitFontsBeforeDraw = enabled
self.postMessage({
type: 'setAwaitFontsBeforeDraw',
id,
success: true,
data: { enabled: fontManager.awaitFontsBeforeDraw }
} as WorkerResponse)
break
}

case 'setFontUrl': {
if (!data) throw new Error('Missing data for setFontUrl message')
const { url } = data as { url: string }
Expand Down
Loading