refactor: extract TextCore shared text engine + sticky width-only resize
TextCore.ts: shared text rendering with dirty-check caching, zoom-bucket resolution, and optional word-wrap. TextSprite and StickySprite now compose TextCore instead of duplicating text logic. Sticky resize now bakes width only (fontSize stays stable), so dragging a sticky wider/narrower reflows text with auto-adjusted height — like a resizable text box.
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* StickySprite — renders a sticky note card.
|
* StickySprite — renders a sticky note card.
|
||||||
*
|
*
|
||||||
|
* Composes TextCore for dirty-checked text rendering with word-wrap.
|
||||||
|
* Adds a rounded-rect background that auto-sizes to fit text.
|
||||||
|
*
|
||||||
* Structure:
|
* Structure:
|
||||||
* Container (this)
|
* Container (this)
|
||||||
* └─ _bg: Graphics (rounded rect background)
|
* └─ _bg: Graphics (rounded rect background)
|
||||||
* └─ _text: Text (word-wrapped content)
|
* └─ TextCore.pixiText (word-wrapped content)
|
||||||
*
|
*
|
||||||
* Dimensions:
|
* Dimensions:
|
||||||
* - width is fixed (from data.w)
|
* - width is fixed (from data.w)
|
||||||
@@ -17,8 +20,9 @@
|
|||||||
* - showText() never triggers layout or redraw
|
* - showText() never triggers layout or redraw
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Container, Graphics, Text, TextStyle } from 'pixi.js';
|
import { Container, Graphics } from 'pixi.js';
|
||||||
import type { StickyObject } from '../scene-format';
|
import type { StickyObject } from '../scene-format';
|
||||||
|
import { TextCore } from './TextCore';
|
||||||
|
|
||||||
const DEFAULT_PADDING = 16;
|
const DEFAULT_PADDING = 16;
|
||||||
const DEFAULT_CORNER_RADIUS = 8;
|
const DEFAULT_CORNER_RADIUS = 8;
|
||||||
@@ -27,8 +31,7 @@ const MIN_HEIGHT = 60;
|
|||||||
|
|
||||||
export class StickySprite extends Container {
|
export class StickySprite extends Container {
|
||||||
private _bg: Graphics;
|
private _bg: Graphics;
|
||||||
private _text: Text;
|
private _core: TextCore;
|
||||||
private _style: TextStyle;
|
|
||||||
|
|
||||||
// Cached layout state
|
// Cached layout state
|
||||||
private _cardW = 200;
|
private _cardW = 200;
|
||||||
@@ -37,11 +40,7 @@ export class StickySprite extends Container {
|
|||||||
private _cornerRadius = DEFAULT_CORNER_RADIUS;
|
private _cornerRadius = DEFAULT_CORNER_RADIUS;
|
||||||
private _bgColor = '#ffd43b';
|
private _bgColor = '#ffd43b';
|
||||||
|
|
||||||
// Dirty-check cache: last known inputs to avoid redundant work
|
// Dirty-check cache for bg-only props (text props are in TextCore)
|
||||||
private _lastText = '';
|
|
||||||
private _lastFontSize = 14;
|
|
||||||
private _lastFontFamily = 'Inter, system-ui, sans-serif';
|
|
||||||
private _lastTextColor = '#1a1a1a';
|
|
||||||
private _lastFill = '#ffd43b';
|
private _lastFill = '#ffd43b';
|
||||||
private _lastW = 200;
|
private _lastW = 200;
|
||||||
private _lastPadding = DEFAULT_PADDING;
|
private _lastPadding = DEFAULT_PADDING;
|
||||||
@@ -49,7 +48,6 @@ export class StickySprite extends Container {
|
|||||||
|
|
||||||
// Background redraw cache: skip Graphics.clear()+redraw when shape is unchanged
|
// Background redraw cache: skip Graphics.clear()+redraw when shape is unchanged
|
||||||
private _bgCacheKey = '';
|
private _bgCacheKey = '';
|
||||||
private _zoomBucket = 1;
|
|
||||||
|
|
||||||
/** After layout, the computed card height. Parent should sync to data.h. */
|
/** After layout, the computed card height. Parent should sync to data.h. */
|
||||||
get computedHeight(): number {
|
get computedHeight(): number {
|
||||||
@@ -58,7 +56,7 @@ export class StickySprite extends Container {
|
|||||||
|
|
||||||
/** Show/hide the text child (used by TextEditor during editing). */
|
/** Show/hide the text child (used by TextEditor during editing). */
|
||||||
showText(visible: boolean): void {
|
showText(visible: boolean): void {
|
||||||
this._text.visible = visible;
|
this._core.setVisible(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,9 +64,7 @@ export class StickySprite extends Container {
|
|||||||
* Only re-rasterizes when the bucket actually changes.
|
* Only re-rasterizes when the bucket actually changes.
|
||||||
*/
|
*/
|
||||||
setZoomBucket(bucket: number): void {
|
setZoomBucket(bucket: number): void {
|
||||||
if (bucket === this._zoomBucket) return;
|
this._core.setZoomBucket(bucket);
|
||||||
this._zoomBucket = bucket;
|
|
||||||
this._text.resolution = bucket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(data: StickyObject) {
|
constructor(data: StickyObject) {
|
||||||
@@ -83,26 +79,20 @@ export class StickySprite extends Container {
|
|||||||
this._bg = new Graphics();
|
this._bg = new Graphics();
|
||||||
this.addChild(this._bg);
|
this.addChild(this._bg);
|
||||||
|
|
||||||
// Text style
|
// Text via shared TextCore (word-wrapped)
|
||||||
const fontSize = data.fontSize || 14;
|
const fontSize = data.fontSize || 14;
|
||||||
this._style = new TextStyle({
|
this._core = new TextCore({
|
||||||
|
text: data.text || '',
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily: data.fontFamily || 'Inter, system-ui, sans-serif',
|
fontFamily: data.fontFamily || 'Inter, system-ui, sans-serif',
|
||||||
fill: data.textColor || '#1a1a1a',
|
fill: data.textColor || '#1a1a1a',
|
||||||
wordWrap: true,
|
|
||||||
wordWrapWidth: Math.max(data.w - this._padding * 2, 1),
|
wordWrapWidth: Math.max(data.w - this._padding * 2, 1),
|
||||||
lineHeight: fontSize * DEFAULT_LINE_HEIGHT,
|
lineHeightMultiplier: DEFAULT_LINE_HEIGHT,
|
||||||
});
|
});
|
||||||
|
this._core.pixiText.position.set(this._padding, this._padding);
|
||||||
|
this.addChild(this._core.pixiText);
|
||||||
|
|
||||||
this._text = new Text({ text: data.text || '', style: this._style });
|
// Snapshot initial state for bg-only dirty checks
|
||||||
this._text.position.set(this._padding, this._padding);
|
|
||||||
this.addChild(this._text);
|
|
||||||
|
|
||||||
// Snapshot initial state
|
|
||||||
this._lastText = data.text || '';
|
|
||||||
this._lastFontSize = fontSize;
|
|
||||||
this._lastFontFamily = data.fontFamily || 'Inter, system-ui, sans-serif';
|
|
||||||
this._lastTextColor = data.textColor || '#1a1a1a';
|
|
||||||
this._lastFill = this._bgColor;
|
this._lastFill = this._bgColor;
|
||||||
this._lastW = data.w;
|
this._lastW = data.w;
|
||||||
this._lastPadding = this._padding;
|
this._lastPadding = this._padding;
|
||||||
@@ -120,17 +110,8 @@ export class StickySprite extends Container {
|
|||||||
const cornerRadius = data.cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
const cornerRadius = data.cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
||||||
const bgColor = data.fill || '#ffd43b';
|
const bgColor = data.fill || '#ffd43b';
|
||||||
const w = data.w;
|
const w = data.w;
|
||||||
const text = data.text || '';
|
|
||||||
const fontSize = data.fontSize || 14;
|
|
||||||
const fontFamily = data.fontFamily || 'Inter, system-ui, sans-serif';
|
|
||||||
const textColor = data.textColor || '#1a1a1a';
|
|
||||||
|
|
||||||
// Check if anything actually changed
|
// Check bg/layout-only changes
|
||||||
const textChanged = text !== this._lastText;
|
|
||||||
const styleChanged =
|
|
||||||
fontSize !== this._lastFontSize ||
|
|
||||||
fontFamily !== this._lastFontFamily ||
|
|
||||||
textColor !== this._lastTextColor;
|
|
||||||
const layoutChanged =
|
const layoutChanged =
|
||||||
w !== this._lastW ||
|
w !== this._lastW ||
|
||||||
padding !== this._lastPadding;
|
padding !== this._lastPadding;
|
||||||
@@ -138,41 +119,35 @@ export class StickySprite extends Container {
|
|||||||
bgColor !== this._lastFill ||
|
bgColor !== this._lastFill ||
|
||||||
cornerRadius !== this._lastCornerRadius;
|
cornerRadius !== this._lastCornerRadius;
|
||||||
|
|
||||||
|
// Delegate text+style dirty-checking to TextCore
|
||||||
|
const textChanged = this._core.update({
|
||||||
|
text: data.text || '',
|
||||||
|
fontSize: data.fontSize || 14,
|
||||||
|
fontFamily: data.fontFamily || 'Inter, system-ui, sans-serif',
|
||||||
|
fill: data.textColor || '#1a1a1a',
|
||||||
|
wordWrapWidth: Math.max(w - padding * 2, 1),
|
||||||
|
lineHeightMultiplier: DEFAULT_LINE_HEIGHT,
|
||||||
|
});
|
||||||
|
|
||||||
// Nothing changed — skip all work
|
// Nothing changed — skip all work
|
||||||
if (!textChanged && !styleChanged && !layoutChanged && !bgChanged) return;
|
if (!textChanged && !layoutChanged && !bgChanged) return;
|
||||||
|
|
||||||
// Update cached state
|
// Update cached state
|
||||||
this._padding = padding;
|
this._padding = padding;
|
||||||
this._cornerRadius = cornerRadius;
|
this._cornerRadius = cornerRadius;
|
||||||
this._bgColor = bgColor;
|
this._bgColor = bgColor;
|
||||||
this._cardW = w;
|
this._cardW = w;
|
||||||
this._lastText = text;
|
|
||||||
this._lastFontSize = fontSize;
|
|
||||||
this._lastFontFamily = fontFamily;
|
|
||||||
this._lastTextColor = textColor;
|
|
||||||
this._lastFill = bgColor;
|
this._lastFill = bgColor;
|
||||||
this._lastW = w;
|
this._lastW = w;
|
||||||
this._lastPadding = padding;
|
this._lastPadding = padding;
|
||||||
this._lastCornerRadius = cornerRadius;
|
this._lastCornerRadius = cornerRadius;
|
||||||
|
|
||||||
// Only update text if content changed
|
if (layoutChanged) {
|
||||||
if (textChanged) {
|
this._core.pixiText.position.set(padding, padding);
|
||||||
this._text.text = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update style if style/layout props changed
|
|
||||||
if (styleChanged || layoutChanged) {
|
|
||||||
this._style.fontSize = fontSize;
|
|
||||||
this._style.fontFamily = fontFamily;
|
|
||||||
this._style.fill = textColor;
|
|
||||||
this._style.wordWrapWidth = Math.max(w - padding * 2, 1);
|
|
||||||
this._style.lineHeight = fontSize * DEFAULT_LINE_HEIGHT;
|
|
||||||
this._text.style = this._style;
|
|
||||||
this._text.position.set(padding, padding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relayout if text, style, or dimensions changed (need remeasure)
|
// Relayout if text, style, or dimensions changed (need remeasure)
|
||||||
if (textChanged || styleChanged || layoutChanged) {
|
if (textChanged || layoutChanged) {
|
||||||
this._layout();
|
this._layout();
|
||||||
} else if (bgChanged) {
|
} else if (bgChanged) {
|
||||||
// Only bg color/radius changed — redraw background without remeasuring text
|
// Only bg color/radius changed — redraw background without remeasuring text
|
||||||
@@ -184,7 +159,7 @@ export class StickySprite extends Container {
|
|||||||
* Recompute layout: measure text height, resize card, redraw background.
|
* Recompute layout: measure text height, resize card, redraw background.
|
||||||
*/
|
*/
|
||||||
private _layout(): void {
|
private _layout(): void {
|
||||||
const textBounds = this._text.getLocalBounds();
|
const textBounds = this._core.pixiText.getLocalBounds();
|
||||||
const textH = textBounds.height;
|
const textH = textBounds.height;
|
||||||
|
|
||||||
this._cardH = Math.max(textH + this._padding * 2, MIN_HEIGHT);
|
this._cardH = Math.max(textH + this._padding * 2, MIN_HEIGHT);
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* TextCore — shared text rendering engine with dirty-check caching.
|
||||||
|
*
|
||||||
|
* Owns a PixiJS Text instance and guards every property mutation
|
||||||
|
* against redundant invalidation. Used by both TextSprite and
|
||||||
|
* StickySprite so text rendering logic lives in one place.
|
||||||
|
*
|
||||||
|
* PixiJS Text treats any property assignment as an invalidation
|
||||||
|
* trigger (re-rasterizes the text texture), even if the value is
|
||||||
|
* identical — so we must guard every mutation ourselves.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Text, TextStyle } from 'pixi.js';
|
||||||
|
|
||||||
|
export interface TextCoreOptions {
|
||||||
|
text: string;
|
||||||
|
fontSize: number;
|
||||||
|
fontFamily: string;
|
||||||
|
fill: string;
|
||||||
|
/** Enable word-wrap at this width. Omit or 0 for no wrap. */
|
||||||
|
wordWrapWidth?: number;
|
||||||
|
/** Line height multiplier (default 1). Only used when wordWrap is enabled. */
|
||||||
|
lineHeightMultiplier?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextCore {
|
||||||
|
readonly pixiText: Text;
|
||||||
|
readonly style: TextStyle;
|
||||||
|
|
||||||
|
// Dirty-check cache
|
||||||
|
private _lastContent: string;
|
||||||
|
private _lastFontSize: number;
|
||||||
|
private _lastFontFamily: string;
|
||||||
|
private _lastFill: string;
|
||||||
|
private _lastWordWrapWidth: number;
|
||||||
|
private _lastLineHeightMultiplier: number;
|
||||||
|
private _zoomBucket = 1;
|
||||||
|
|
||||||
|
get measuredWidth(): number {
|
||||||
|
return this.pixiText.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
get measuredHeight(): number {
|
||||||
|
return this.pixiText.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(opts: TextCoreOptions) {
|
||||||
|
const { text, fontSize, fontFamily, fill } = opts;
|
||||||
|
const wordWrapWidth = opts.wordWrapWidth || 0;
|
||||||
|
const lineHeightMultiplier = opts.lineHeightMultiplier || 1;
|
||||||
|
|
||||||
|
const styleOpts: ConstructorParameters<typeof TextStyle>[0] = {
|
||||||
|
fontSize,
|
||||||
|
fontFamily,
|
||||||
|
fill,
|
||||||
|
};
|
||||||
|
if (wordWrapWidth > 0) {
|
||||||
|
styleOpts.wordWrap = true;
|
||||||
|
styleOpts.wordWrapWidth = wordWrapWidth;
|
||||||
|
styleOpts.lineHeight = fontSize * lineHeightMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.style = new TextStyle(styleOpts);
|
||||||
|
this.pixiText = new Text({ text, style: this.style });
|
||||||
|
|
||||||
|
this._lastContent = text;
|
||||||
|
this._lastFontSize = fontSize;
|
||||||
|
this._lastFontFamily = fontFamily;
|
||||||
|
this._lastFill = fill;
|
||||||
|
this._lastWordWrapWidth = wordWrapWidth;
|
||||||
|
this._lastLineHeightMultiplier = lineHeightMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show/hide the text (used by TextEditor during editing). */
|
||||||
|
setVisible(visible: boolean): void {
|
||||||
|
this.pixiText.visible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update text rasterization resolution for zoom-bucket crisp rendering.
|
||||||
|
* Only re-rasterizes when the bucket actually changes.
|
||||||
|
*/
|
||||||
|
setZoomBucket(bucket: number): void {
|
||||||
|
if (bucket === this._zoomBucket) return;
|
||||||
|
this._zoomBucket = bucket;
|
||||||
|
this.pixiText.resolution = bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update text content and style. Only touches PixiJS properties
|
||||||
|
* when the corresponding input has actually changed.
|
||||||
|
* Returns true if text content or style changed (dimensions may differ).
|
||||||
|
*/
|
||||||
|
update(opts: TextCoreOptions): boolean {
|
||||||
|
const { text, fontSize, fontFamily, fill } = opts;
|
||||||
|
const wordWrapWidth = opts.wordWrapWidth || 0;
|
||||||
|
const lineHeightMultiplier = opts.lineHeightMultiplier || 1;
|
||||||
|
|
||||||
|
const contentChanged = text !== this._lastContent;
|
||||||
|
const styleChanged =
|
||||||
|
fontSize !== this._lastFontSize ||
|
||||||
|
fontFamily !== this._lastFontFamily ||
|
||||||
|
fill !== this._lastFill;
|
||||||
|
const wrapChanged =
|
||||||
|
wordWrapWidth !== this._lastWordWrapWidth ||
|
||||||
|
lineHeightMultiplier !== this._lastLineHeightMultiplier;
|
||||||
|
|
||||||
|
if (!contentChanged && !styleChanged && !wrapChanged) return false;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
this._lastContent = text;
|
||||||
|
this._lastFontSize = fontSize;
|
||||||
|
this._lastFontFamily = fontFamily;
|
||||||
|
this._lastFill = fill;
|
||||||
|
this._lastWordWrapWidth = wordWrapWidth;
|
||||||
|
this._lastLineHeightMultiplier = lineHeightMultiplier;
|
||||||
|
|
||||||
|
if (contentChanged) {
|
||||||
|
this.pixiText.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (styleChanged || wrapChanged) {
|
||||||
|
this.style.fontSize = fontSize;
|
||||||
|
this.style.fontFamily = fontFamily;
|
||||||
|
this.style.fill = fill;
|
||||||
|
if (wordWrapWidth > 0) {
|
||||||
|
this.style.wordWrap = true;
|
||||||
|
this.style.wordWrapWidth = wordWrapWidth;
|
||||||
|
this.style.lineHeight = fontSize * lineHeightMultiplier;
|
||||||
|
} else {
|
||||||
|
this.style.wordWrap = false;
|
||||||
|
}
|
||||||
|
// Do NOT reassign pixiText.style — it's already the live reference.
|
||||||
|
// Mutating style properties is sufficient; reassigning triggers
|
||||||
|
// a redundant PixiJS invalidation.
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* TextSprite — wraps PixiJS Text with dirty-check caching.
|
* TextSprite — free-flowing text display object.
|
||||||
*
|
*
|
||||||
* Prevents unnecessary PixiJS text invalidation by comparing
|
* Composes TextCore for dirty-checked text rendering.
|
||||||
* incoming data against last-applied values. PixiJS Text treats
|
* No word-wrap — text flows freely, measured width/height
|
||||||
* any property assignment as an invalidation trigger, even if
|
* reflect actual rendered bounds.
|
||||||
* the value is identical — so we must guard every mutation.
|
|
||||||
*
|
*
|
||||||
* Structure:
|
* Structure:
|
||||||
* Container (this)
|
* Container (this)
|
||||||
* └─ _text: Text
|
* └─ TextCore.pixiText
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Container, Text, TextStyle } from 'pixi.js';
|
import { Container } from 'pixi.js';
|
||||||
import type { TextObject } from '../scene-format';
|
import type { TextObject } from '../scene-format';
|
||||||
|
import { TextCore } from './TextCore';
|
||||||
|
|
||||||
export class TextSprite extends Container {
|
export class TextSprite extends Container {
|
||||||
private _text: Text;
|
private _core: TextCore;
|
||||||
private _style: TextStyle;
|
|
||||||
|
|
||||||
// Dirty-check cache
|
|
||||||
private _lastContent = '';
|
|
||||||
private _lastFontSize = 24;
|
|
||||||
private _lastFontFamily = 'sans-serif';
|
|
||||||
private _lastFill = '#ffffff';
|
|
||||||
private _zoomBucket = 1;
|
|
||||||
|
|
||||||
/** Measured width after last text change. */
|
/** Measured width after last text change. */
|
||||||
get measuredWidth(): number {
|
get measuredWidth(): number {
|
||||||
return this._text.width;
|
return this._core.measuredWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Measured height after last text change. */
|
/** Measured height after last text change. */
|
||||||
get measuredHeight(): number {
|
get measuredHeight(): number {
|
||||||
return this._text.height;
|
return this._core.measuredHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Show/hide the text (used by TextEditor during editing). */
|
/** Show/hide the text (used by TextEditor during editing). */
|
||||||
showText(visible: boolean): void {
|
showText(visible: boolean): void {
|
||||||
this._text.visible = visible;
|
this._core.setVisible(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,26 +37,19 @@ export class TextSprite extends Container {
|
|||||||
* Only re-rasterizes when the bucket actually changes.
|
* Only re-rasterizes when the bucket actually changes.
|
||||||
*/
|
*/
|
||||||
setZoomBucket(bucket: number): void {
|
setZoomBucket(bucket: number): void {
|
||||||
if (bucket === this._zoomBucket) return;
|
this._core.setZoomBucket(bucket);
|
||||||
this._zoomBucket = bucket;
|
|
||||||
this._text.resolution = bucket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(data: TextObject) {
|
constructor(data: TextObject) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const fontSize = data.fontSize || 24;
|
this._core = new TextCore({
|
||||||
const fontFamily = data.fontFamily || 'sans-serif';
|
text: data.text || '',
|
||||||
const fill = data.fill || '#ffffff';
|
fontSize: data.fontSize || 24,
|
||||||
|
fontFamily: data.fontFamily || 'sans-serif',
|
||||||
this._style = new TextStyle({ fontSize, fontFamily, fill });
|
fill: data.fill || '#ffffff',
|
||||||
this._text = new Text({ text: data.text || '', style: this._style });
|
});
|
||||||
this.addChild(this._text);
|
this.addChild(this._core.pixiText);
|
||||||
|
|
||||||
this._lastContent = data.text || '';
|
|
||||||
this._lastFontSize = fontSize;
|
|
||||||
this._lastFontFamily = fontFamily;
|
|
||||||
this._lastFill = fill;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,38 +58,11 @@ export class TextSprite extends Container {
|
|||||||
* Returns true if dimensions may have changed (text/style updated).
|
* Returns true if dimensions may have changed (text/style updated).
|
||||||
*/
|
*/
|
||||||
updateFromData(data: TextObject): boolean {
|
updateFromData(data: TextObject): boolean {
|
||||||
const content = data.text ?? '';
|
return this._core.update({
|
||||||
const fontSize = data.fontSize || 24;
|
text: data.text ?? '',
|
||||||
const fontFamily = data.fontFamily || 'sans-serif';
|
fontSize: data.fontSize || 24,
|
||||||
const fill = data.fill || '#ffffff';
|
fontFamily: data.fontFamily || 'sans-serif',
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,9 +231,9 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
item.displayObject.scale.set(d.sx, d.sy);
|
item.displayObject.scale.set(d.sx, d.sy);
|
||||||
} else if (item.type === 'sticky') {
|
} else if (item.type === 'sticky') {
|
||||||
const d = item.data as any;
|
const d = item.data as any;
|
||||||
// Scale card width and fontSize, then reset sx/sy so text re-rasterizes crisp
|
// Bake width only — text re-wraps and height auto-adjusts.
|
||||||
|
// fontSize stays stable so the card acts like a resizable text box.
|
||||||
d.w = Math.round(d.w * absSx);
|
d.w = Math.round(d.w * absSx);
|
||||||
d.fontSize = Math.round((d.fontSize || 14) * ((absSx + absSy) / 2));
|
|
||||||
d.sx = item.data.sx > 0 ? 1 : -1;
|
d.sx = item.data.sx > 0 ? 1 : -1;
|
||||||
d.sy = item.data.sy > 0 ? 1 : -1;
|
d.sy = item.data.sy > 0 ? 1 : -1;
|
||||||
if (item.displayObject instanceof StickySprite) {
|
if (item.displayObject instanceof StickySprite) {
|
||||||
|
|||||||
Reference in New Issue
Block a user