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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Fix font variant and style measurements for wrapped, clipped and vertical text.",
"type": "none",
"packageName": "@visactor/vrender-core"
}
],
"packageName": "@visactor/vrender-core",
"email": "111864431+dvd233@users.noreply.github.com"
}
120 changes: 120 additions & 0 deletions packages/vrender-core/__tests__/unit/graphic/text-font-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { application } from '../../../src/application';
import type { IGraphicUtil, TextOptionsType } from '../../../src/interface';
import { ATextMeasure } from '../../../src/core/contributions/textMeasure/AtextMeasure';
import { DefaultGraphicService } from '../../../src/graphic/graphic-service/graphic-service';
import { createText } from '../../../src/graphic/text';
import { createWrapText } from '../../../src/graphic/wrap-text';
import { createGroup } from '../../../src/graphic/group';

describe('text layout uses the rendered font', () => {
const graphicUtil = application.graphicUtil;
const graphicService = application.graphicService;

beforeEach(() => {
let font: TextOptionsType = {};
// ไฝฟ็”จ็กฎๅฎš็š„ๅญ—ไฝ“ๅฎฝๅบฆ้š”็ฆปๅฎฟไธปๅญ—ไฝ“ๅทฎๅผ‚๏ผŒไป็ป่ฟ‡็œŸๅฎž ATextMeasure ็š„ๆˆชๆ–ญๅ’Œๆต‹้‡็ฎ—ๆณ•ใ€‚
const context = {
setTextStyleWithoutAlignBaseline: (options: TextOptionsType) => (font = options),
measureText: (text: string) => {
const advance = font.fontVariant === 'small-caps' ? 20 : font.fontStyle === 'italic' ? 15 : 10;
const width = text.length * advance;
return {
width,
actualBoundingBoxAscent: 16,
actualBoundingBoxDescent: 4,
fontBoundingBoxAscent: 16,
fontBoundingBoxDescent: 4
};
}
};
const measure = new ATextMeasure();
measure.configure({ context, bindTextMeasure: (): void => undefined } as unknown as IGraphicUtil, 'browser');
application.graphicUtil = { getTextMeasureInstance: () => measure } as unknown as IGraphicUtil;
application.graphicService = new DefaultGraphicService();
});

afterEach(() => {
application.graphicUtil = graphicUtil;
application.graphicService = graphicService;
});

function wrapped(attributes = {}, create = createText) {
const text = create({
text: 'abcdefgh',
fontFamily: 'Arial',
fontSize: 20,
lineHeight: 20,
whiteSpace: 'normal',
wordBreak: 'break-all',
maxLineWidth: 55,
...attributes
});
return text;
}

test.each([createText, createWrapText])('wraps small-caps at the available width (%#)', create => {
const text = wrapped({ fontVariant: 'small-caps' }, create);
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['ab', 'cd', 'ef', 'gh']);
expect(text.multilineLayout.lines.map(line => line.width)).toEqual([40, 40, 40, 40]);
});

test('measures explicit lines with the configured font', () => {
const text = createText({ text: ['abcd', 'ab'], fontSize: 20, fontVariant: 'small-caps' });
expect(text.multilineLayout.lines.map(line => line.width)).toEqual([80, 40]);
});

test('clips a single line including its suffix using the configured font', () => {
const text = createText({
text: 'abcdefgh',
fontSize: 20,
fontVariant: 'small-caps',
maxLineWidth: 100,
ellipsis: '...'
});
expect(text.clipedText).toBe('ab...');
expect(text.clipedWidth).toBe(100);
});

test.each([createText, createWrapText])('invalidates wrapped layout when the variant changes (%#)', create => {
const text = wrapped({}, create);
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['abcde', 'fgh']);
text.setAttribute('fontVariant', 'small-caps');
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['ab', 'cd', 'ef', 'gh']);
text.setAttributes({ fontVariant: '' });
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['abcde', 'fgh']);
});

test.each([createText, createWrapText])('invalidates wrapped layout when the style changes (%#)', create => {
const text = wrapped({}, create);
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['abcde', 'fgh']);
text.setAttributes({ fontStyle: 'italic' });
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['abc', 'def', 'gh']);
expect(text.multilineLayout.lines.map(line => line.width)).toEqual([45, 45, 30]);
});

test.each([createText, createWrapText])('recomputes layout when entering and leaving a font state (%#)', create => {
const text = wrapped({}, create);
text.states = { caps: { fontVariant: 'small-caps' } };
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['abcde', 'fgh']);
text.useStates(['caps'], false);
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['ab', 'cd', 'ef', 'gh']);
text.useStates([], false);
expect(text.multilineLayout.lines.map(line => line.str)).toEqual(['abcde', 'fgh']);
});

test('inherits the font variant from the group theme while respecting an explicit override', () => {
const group = createGroup({});
group.setTheme({ text: { fontVariant: 'small-caps' } });
const inherited = wrapped();
const overridden = wrapped({ fontVariant: '' });
group.add(inherited);
group.add(overridden);
expect(inherited.multilineLayout.lines.map(line => line.str)).toEqual(['ab', 'cd', 'ef', 'gh']);
expect(overridden.multilineLayout.lines.map(line => line.str)).toEqual(['abcde', 'fgh']);
});

test('measures vertical Latin text with the configured font', () => {
const text = createText({ text: 'abcd', direction: 'vertical', fontSize: 20, fontVariant: 'small-caps' });
expect(text.AABBBounds.height()).toBe(80);
});
});
23 changes: 18 additions & 5 deletions packages/vrender-core/src/graphic/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const TEXT_UPDATE_TAG_KEY = [
'fontSize',
'fontFamily',
'fontWeight',
'fontStyle',
'fontVariant',
'ellipsis',
'lineHeight',
'direction',
Expand Down Expand Up @@ -279,6 +281,8 @@ export class Text extends Graphic<ITextGraphicAttribute> implements IText {
textBaseline = textTheme.textBaseline,
fontSize = textTheme.fontSize,
fontWeight = textTheme.fontWeight,
fontStyle = textTheme.fontStyle,
fontVariant = textTheme.fontVariant,
ellipsis = textTheme.ellipsis,
maxLineWidth,
stroke = textTheme.stroke,
Expand Down Expand Up @@ -306,7 +310,12 @@ export class Text extends Graphic<ITextGraphicAttribute> implements IText {
return this._AABBBounds;
}
const textMeasure = application.graphicUtil.getTextMeasureInstance(this.textMeasureId || this.stage?.textMeasureId);
const layoutObj = new CanvasTextLayout(fontFamily, { fontSize, fontWeight, fontFamily, lineHeight }, textMeasure);
// ๆต‹้‡ไธŽ็ป˜ๅˆถๅฟ…้กปไฝฟ็”จๅŒไธ€ๅญ—ไฝ“๏ผŒๅฆๅˆ™ small-caps ็ญ‰ๅ˜ไฝ“ไผš่ถŠ่ฟ‡ๆข่กŒๅฎฝๅบฆๅ’ŒๅŒ…ๅ›ด็›’ใ€‚
const layoutObj = new CanvasTextLayout(
fontFamily,
{ fontSize, fontWeight, fontFamily, fontStyle, fontVariant, lineHeight },
textMeasure
);
const layoutData = layoutObj.GetLayoutByLines(
text,
textAlign,
Expand Down Expand Up @@ -352,6 +361,8 @@ export class Text extends Graphic<ITextGraphicAttribute> implements IText {
lineWidth = textTheme.lineWidth,
wordBreak = textTheme.wordBreak,
fontWeight = textTheme.fontWeight,
fontStyle = textTheme.fontStyle,
fontVariant = textTheme.fontVariant,
// widthLimit,
ignoreBuf = textTheme.ignoreBuf,
measureMode = textTheme.measureMode,
Expand All @@ -374,7 +385,7 @@ export class Text extends Graphic<ITextGraphicAttribute> implements IText {
}

const textMeasure = application.graphicUtil.getTextMeasureInstance(this.textMeasureId || this.stage?.textMeasureId);
const textOptions = { fontSize, fontWeight, fontFamily, lineHeight };
const textOptions = { fontSize, fontWeight, fontFamily, fontStyle, fontVariant, lineHeight };
const layoutObj = new CanvasTextLayout(fontFamily, textOptions, textMeasure as any);

// layoutObjๅ†…้€ป่พ‘
Expand Down Expand Up @@ -555,6 +566,8 @@ export class Text extends Graphic<ITextGraphicAttribute> implements IText {
fontFamily = textTheme.fontFamily,
fontSize = textTheme.fontSize,
fontWeight = textTheme.fontWeight,
fontStyle = textTheme.fontStyle,
fontVariant = textTheme.fontVariant,
stroke = textTheme.stroke,
lineWidth = textTheme.lineWidth,
// wordBreak = textTheme.wordBreak,
Expand Down Expand Up @@ -595,7 +608,7 @@ export class Text extends Graphic<ITextGraphicAttribute> implements IText {
const strEllipsis = (ellipsis === true ? textTheme.ellipsis : ellipsis) as string;
const data = textMeasure.clipTextWithSuffixVertical(
verticalData,
{ fontSize, fontWeight, fontFamily },
{ fontSize, fontWeight, fontFamily, fontStyle, fontVariant },
maxLineWidth,
strEllipsis,
false,
Expand All @@ -606,7 +619,7 @@ export class Text extends Graphic<ITextGraphicAttribute> implements IText {
} else {
const data = textMeasure.clipTextVertical(
verticalData,
{ fontSize, fontWeight, fontFamily },
{ fontSize, fontWeight, fontFamily, fontStyle, fontVariant },
maxLineWidth,
false
);
Expand All @@ -621,7 +634,7 @@ export class Text extends Graphic<ITextGraphicAttribute> implements IText {
const w =
t.direction === TextDirection.HORIZONTAL
? fontSize
: textMeasure.measureTextWidth(t.text, { fontSize, fontWeight, fontFamily });
: textMeasure.measureTextWidth(t.text, { fontSize, fontWeight, fontFamily, fontStyle, fontVariant });

width += w;
t.width = w;
Expand Down
8 changes: 7 additions & 1 deletion packages/vrender-core/src/graphic/wrap-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export class WrapText extends Text {
lineWidth = textTheme.lineWidth,
wordBreak = textTheme.wordBreak,
fontWeight = textTheme.fontWeight,
fontStyle = textTheme.fontStyle,
fontVariant = textTheme.fontVariant,
// widthLimit,
ignoreBuf = textTheme.ignoreBuf,
heightLimit = 0,
Expand All @@ -71,7 +73,11 @@ export class WrapText extends Text {
}

const textMeasure = application.graphicUtil.getTextMeasureInstance(this.textMeasureId || this.stage?.textMeasureId);
const layoutObj = new CanvasTextLayout(fontFamily, { fontSize, fontWeight, fontFamily }, textMeasure as any) as any;
const layoutObj = new CanvasTextLayout(
fontFamily,
{ fontSize, fontWeight, fontFamily, fontStyle, fontVariant },
textMeasure as any
) as any;

// layoutObjๅ†…้€ป่พ‘
const lines = text.map(l => l.toString()) as string[];
Expand Down
Loading