feat(text): add TextSprite with dirty-check caching
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* TextSprite — wraps PixiJS Text with dirty-check caching.
|
||||||
|
*
|
||||||
|
* Prevents unnecessary PixiJS text invalidation by comparing
|
||||||
|
* incoming data against last-applied values. PixiJS Text treats
|
||||||
|
* any property assignment as an invalidation trigger, even if
|
||||||
|
* the value is identical — so we must guard every mutation.
|
||||||
|
*
|
||||||
|
* Structure:
|
||||||
|
* Container (this)
|
||||||
|
* └─ _text: Text
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Container, Text, TextStyle } from 'pixi.js';
|
||||||
|
import type { TextObject } from '../scene-format';
|
||||||
|
|
||||||
|
export class TextSprite extends Container {
|
||||||
|
private _text: Text;
|
||||||
|
private _style: TextStyle;
|
||||||
|
|
||||||
|
// Dirty-check cache
|
||||||
|
private _lastContent = '';
|
||||||
|
private _lastFontSize = 24;
|
||||||
|
private _lastFontFamily = 'sans-serif';
|
||||||
|
private _lastFill = '#ffffff';
|
||||||
|
|
||||||
|
/** Measured width after last text change. */
|
||||||
|
get measuredWidth(): number {
|
||||||
|
return this._text.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Measured height after last text change. */
|
||||||
|
get measuredHeight(): number {
|
||||||
|
return this._text.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show/hide the text (used by TextEditor during editing). */
|
||||||
|
showText(visible: boolean): void {
|
||||||
|
this._text.visible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(data: TextObject) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const fontSize = data.fontSize || 24;
|
||||||
|
const fontFamily = data.fontFamily || 'sans-serif';
|
||||||
|
const fill = data.fill || '#ffffff';
|
||||||
|
|
||||||
|
this._style = new TextStyle({ fontSize, fontFamily, fill });
|
||||||
|
this._text = new Text({ text: data.text || '', style: this._style });
|
||||||
|
this.addChild(this._text);
|
||||||
|
|
||||||
|
this._lastContent = data.text || '';
|
||||||
|
this._lastFontSize = fontSize;
|
||||||
|
this._lastFontFamily = fontFamily;
|
||||||
|
this._lastFill = fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update from TextObject data. Only touches PixiJS properties
|
||||||
|
* when the corresponding input has actually changed.
|
||||||
|
* Returns true if dimensions may have changed (text/style updated).
|
||||||
|
*/
|
||||||
|
updateFromData(data: TextObject): boolean {
|
||||||
|
const content = data.text ?? '';
|
||||||
|
const fontSize = data.fontSize || 24;
|
||||||
|
const fontFamily = data.fontFamily || 'sans-serif';
|
||||||
|
const fill = data.fill || '#ffffff';
|
||||||
|
|
||||||
|
const contentChanged = content !== this._lastContent;
|
||||||
|
const styleChanged =
|
||||||
|
fontSize !== this._lastFontSize ||
|
||||||
|
fontFamily !== this._lastFontFamily ||
|
||||||
|
fill !== this._lastFill;
|
||||||
|
|
||||||
|
if (!contentChanged && !styleChanged) return false;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
this._lastContent = content;
|
||||||
|
this._lastFontSize = fontSize;
|
||||||
|
this._lastFontFamily = fontFamily;
|
||||||
|
this._lastFill = fill;
|
||||||
|
|
||||||
|
if (contentChanged) {
|
||||||
|
this._text.text = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (styleChanged) {
|
||||||
|
this._style.fontSize = fontSize;
|
||||||
|
this._style.fontFamily = fontFamily;
|
||||||
|
this._style.fill = fill;
|
||||||
|
// Do NOT reassign this._text.style = this._style — it's already the live
|
||||||
|
// reference, and reassigning triggers a redundant PixiJS invalidation.
|
||||||
|
// Mutating style properties is sufficient.
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user