Reduce number of forced reflows when rendering
This commit is contained in:
parent
d283fe158c
commit
ab3d67f313
|
@ -3387,13 +3387,17 @@ define('svg/SVGUtilities',[],() => {
|
|||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
function make(type, attrs = {}, children = []) {
|
||||
const o = document.createElementNS(NS, type);
|
||||
function setAttributes(target, attrs) {
|
||||
for(const k in attrs) {
|
||||
if(attrs.hasOwnProperty(k)) {
|
||||
o.setAttribute(k, attrs[k]);
|
||||
target.setAttribute(k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function make(type, attrs = {}, children = []) {
|
||||
const o = document.createElementNS(NS, type);
|
||||
setAttributes(o, attrs);
|
||||
for(const c of children) {
|
||||
o.appendChild(c);
|
||||
}
|
||||
|
@ -3417,6 +3421,7 @@ define('svg/SVGUtilities',[],() => {
|
|||
makeText,
|
||||
make,
|
||||
makeContainer,
|
||||
setAttributes,
|
||||
empty,
|
||||
};
|
||||
});
|
||||
|
@ -3461,6 +3466,27 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
});
|
||||
}
|
||||
|
||||
function measureLine(tester, line) {
|
||||
if(!line.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const labelKey = JSON.stringify(line);
|
||||
const knownWidth = tester.widths.get(labelKey);
|
||||
if(knownWidth !== undefined) {
|
||||
return knownWidth;
|
||||
}
|
||||
|
||||
// getComputedTextLength forces a reflow, so only call it if nothing
|
||||
// else can tell us the length
|
||||
|
||||
svg.empty(tester.node);
|
||||
populateSvgTextLine(tester.node, line);
|
||||
const width = tester.node.getComputedTextLength();
|
||||
tester.widths.set(labelKey, width);
|
||||
return width;
|
||||
}
|
||||
|
||||
const EMPTY = [];
|
||||
|
||||
class SVGTextBlock {
|
||||
|
@ -3472,8 +3498,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.lines = [];
|
||||
this.set(initialState);
|
||||
}
|
||||
|
@ -3499,8 +3523,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
|
||||
_reset() {
|
||||
this._rebuildLines(0);
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
_renderText() {
|
||||
|
@ -3516,7 +3538,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
|
||||
this._rebuildLines(formatted.length);
|
||||
|
||||
let maxWidth = 0;
|
||||
this.lines.forEach((ln, i) => {
|
||||
const id = JSON.stringify(formatted[i]);
|
||||
if(id !== ln.latest) {
|
||||
|
@ -3524,9 +3545,7 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
populateSvgTextLine(ln.node, formatted[i]);
|
||||
ln.latest = id;
|
||||
}
|
||||
maxWidth = Math.max(maxWidth, ln.node.getComputedTextLength());
|
||||
});
|
||||
this.width = maxWidth;
|
||||
}
|
||||
|
||||
_updateX() {
|
||||
|
@ -3540,7 +3559,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
this.lines.forEach(({node}, i) => {
|
||||
node.setAttribute('y', this.state.y + i * lineHeight + size);
|
||||
});
|
||||
this.height = lineHeight * this.lines.length;
|
||||
}
|
||||
|
||||
firstLine() {
|
||||
|
@ -3587,47 +3605,62 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
|
|||
this.cache = new Map();
|
||||
}
|
||||
|
||||
measure(attrs, formatted) {
|
||||
if(!formatted || !formatted.length) {
|
||||
return {width: 0, height: 0};
|
||||
}
|
||||
if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
_measureHeight({attrs, formatted}) {
|
||||
return formatted.length * fontDetails(attrs).lineHeight;
|
||||
}
|
||||
|
||||
let tester = this.cache.get(attrs);
|
||||
_measureWidth({attrs, formatted}) {
|
||||
if(!formatted.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const attrKey = JSON.stringify(attrs);
|
||||
let tester = this.cache.get(attrKey);
|
||||
if(!tester) {
|
||||
tester = svg.make('text', attrs);
|
||||
this.testers.appendChild(tester);
|
||||
this.cache.set(attrs, tester);
|
||||
const node = svg.make('text', attrs);
|
||||
this.testers.appendChild(node);
|
||||
tester = {
|
||||
node,
|
||||
widths: new Map(),
|
||||
};
|
||||
this.cache.set(attrKey, tester);
|
||||
}
|
||||
|
||||
if(!this.testers.parentNode) {
|
||||
this.container.appendChild(this.testers);
|
||||
}
|
||||
|
||||
let width = 0;
|
||||
formatted.forEach((line) => {
|
||||
svg.empty(tester);
|
||||
populateSvgTextLine(tester, line);
|
||||
width = Math.max(width, tester.getComputedTextLength());
|
||||
});
|
||||
return (formatted
|
||||
.map((line) => measureLine(tester, line))
|
||||
.reduce((a, b) => Math.max(a, b), 0)
|
||||
);
|
||||
}
|
||||
|
||||
_getMeasurementOpts(attrs, formatted) {
|
||||
if(!formatted) {
|
||||
if(typeof attrs === 'object' && attrs.state) {
|
||||
formatted = attrs.state.formatted || [];
|
||||
attrs = attrs.state.attrs;
|
||||
} else {
|
||||
formatted = [];
|
||||
}
|
||||
} else if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
}
|
||||
return {attrs, formatted};
|
||||
}
|
||||
|
||||
measure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
return {
|
||||
width,
|
||||
height: formatted.length * fontDetails(attrs).lineHeight,
|
||||
width: this._measureWidth(opts),
|
||||
height: this._measureHeight(opts),
|
||||
};
|
||||
}
|
||||
|
||||
measureHeight(attrs, formatted) {
|
||||
if(!formatted) {
|
||||
return 0;
|
||||
}
|
||||
if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
}
|
||||
|
||||
return formatted.length * fontDetails(attrs).lineHeight;
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
return this._measureHeight(opts);
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
|
@ -3838,6 +3871,7 @@ define('svg/SVGShapes',[
|
|||
labelLayer,
|
||||
boxRenderer = null,
|
||||
SVGTextBlockClass = SVGTextBlock,
|
||||
textSizer,
|
||||
}) {
|
||||
if(!formatted || !formatted.length) {
|
||||
return {width: 0, height: 0, label: null, box: null};
|
||||
|
@ -3852,20 +3886,21 @@ define('svg/SVGShapes',[
|
|||
y: y + padding.top,
|
||||
});
|
||||
|
||||
const width = (label.width + padding.left + padding.right);
|
||||
const height = (label.height + padding.top + padding.bottom);
|
||||
const size = textSizer.measure(label);
|
||||
const width = (size.width + padding.left + padding.right);
|
||||
const height = (size.height + padding.top + padding.bottom);
|
||||
|
||||
let box = null;
|
||||
if(boxRenderer) {
|
||||
box = boxRenderer({
|
||||
'x': anchorX - label.width * shift - padding.left,
|
||||
'x': anchorX - size.width * shift - padding.left,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
} else {
|
||||
box = renderBox(boxAttrs, {
|
||||
'x': anchorX - label.width * shift - padding.left,
|
||||
'x': anchorX - size.width * shift - padding.left,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
|
@ -4064,6 +4099,7 @@ define('sequence/components/Block',[
|
|||
boxLayer: blockInfo.hold,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
|
||||
const labelRender = SVGShapes.renderBoxedText(label, {
|
||||
|
@ -4075,6 +4111,7 @@ define('sequence/components/Block',[
|
|||
boxLayer: env.lineMaskLayer,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
|
||||
const labelHeight = Math.max(
|
||||
|
@ -4434,6 +4471,7 @@ define('sequence/components/AgentCap',[
|
|||
boxLayer: clickable,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
clickable.insertBefore(svg.make('rect', {
|
||||
'x': x - text.width / 2,
|
||||
|
@ -5003,6 +5041,7 @@ define('sequence/components/Connect',[
|
|||
boxLayer: env.lineMaskLayer,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
const labelW = (label ? (
|
||||
renderedText.width +
|
||||
|
@ -5129,6 +5168,7 @@ define('sequence/components/Connect',[
|
|||
boxLayer: env.lineMaskLayer,
|
||||
labelLayer,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5348,15 +5388,16 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base
|
|||
formatted: label,
|
||||
y,
|
||||
});
|
||||
const size = env.textSizer.measure(labelNode);
|
||||
|
||||
const fullW = (
|
||||
labelNode.width +
|
||||
size.width +
|
||||
config.padding.left +
|
||||
config.padding.right
|
||||
);
|
||||
const fullH = (
|
||||
config.padding.top +
|
||||
labelNode.height +
|
||||
size.height +
|
||||
config.padding.bottom
|
||||
);
|
||||
if(x0 === null && xMid !== null) {
|
||||
|
@ -5638,6 +5679,7 @@ define('sequence/components/Divider',[
|
|||
boxLayer: env.fullMaskLayer,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
labelWidth = boxed.width;
|
||||
}
|
||||
|
@ -6171,12 +6213,13 @@ define('sequence/Renderer',[
|
|||
|
||||
updateBounds(stagesHeight) {
|
||||
const cx = (this.minX + this.maxX) / 2;
|
||||
const titleY = ((this.title.height > 0) ?
|
||||
(-this.theme.titleMargin - this.title.height) : 0
|
||||
const titleSize = this.sizer.measure(this.title);
|
||||
const titleY = ((titleSize.height > 0) ?
|
||||
(-this.theme.titleMargin - titleSize.height) : 0
|
||||
);
|
||||
this.title.set({x: cx, y: titleY});
|
||||
|
||||
const halfTitleWidth = this.title.width / 2;
|
||||
const halfTitleWidth = titleSize.width / 2;
|
||||
const margin = this.theme.outerMargin;
|
||||
const x0 = Math.min(this.minX, cx - halfTitleWidth) - margin;
|
||||
const x1 = Math.max(this.maxX, cx + halfTitleWidth) + margin;
|
||||
|
@ -6186,15 +6229,15 @@ define('sequence/Renderer',[
|
|||
this.width = x1 - x0;
|
||||
this.height = y1 - y0;
|
||||
|
||||
this.fullMaskReveal.setAttribute('x', x0);
|
||||
this.fullMaskReveal.setAttribute('y', y0);
|
||||
this.fullMaskReveal.setAttribute('width', this.width);
|
||||
this.fullMaskReveal.setAttribute('height', this.height);
|
||||
const fullSize = {
|
||||
'x': x0,
|
||||
'y': y0,
|
||||
'width': this.width,
|
||||
'height': this.height,
|
||||
};
|
||||
|
||||
this.lineMaskReveal.setAttribute('x', x0);
|
||||
this.lineMaskReveal.setAttribute('y', y0);
|
||||
this.lineMaskReveal.setAttribute('width', this.width);
|
||||
this.lineMaskReveal.setAttribute('height', this.height);
|
||||
svg.setAttributes(this.fullMaskReveal, fullSize);
|
||||
svg.setAttributes(this.lineMaskReveal, fullSize);
|
||||
|
||||
this.base.setAttribute('viewBox', (
|
||||
x0 + ' ' + y0 + ' ' +
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
defineDescribe('Readme', [
|
||||
define([
|
||||
'./SequenceDiagram',
|
||||
'image/ImageRegion',
|
||||
'image/ImageSimilarity',
|
||||
|
@ -70,6 +70,8 @@ defineDescribe('Readme', [
|
|||
return (fetch('README.md')
|
||||
.then((response) => response.text())
|
||||
.then(findSamples)
|
||||
.then((samples) => samples.forEach(makeSampleTests))
|
||||
.then((samples) => describe('Readme', () => {
|
||||
samples.forEach(makeSampleTests);
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
|
|
@ -494,12 +494,13 @@ define([
|
|||
|
||||
updateBounds(stagesHeight) {
|
||||
const cx = (this.minX + this.maxX) / 2;
|
||||
const titleY = ((this.title.height > 0) ?
|
||||
(-this.theme.titleMargin - this.title.height) : 0
|
||||
const titleSize = this.sizer.measure(this.title);
|
||||
const titleY = ((titleSize.height > 0) ?
|
||||
(-this.theme.titleMargin - titleSize.height) : 0
|
||||
);
|
||||
this.title.set({x: cx, y: titleY});
|
||||
|
||||
const halfTitleWidth = this.title.width / 2;
|
||||
const halfTitleWidth = titleSize.width / 2;
|
||||
const margin = this.theme.outerMargin;
|
||||
const x0 = Math.min(this.minX, cx - halfTitleWidth) - margin;
|
||||
const x1 = Math.max(this.maxX, cx + halfTitleWidth) + margin;
|
||||
|
@ -509,15 +510,15 @@ define([
|
|||
this.width = x1 - x0;
|
||||
this.height = y1 - y0;
|
||||
|
||||
this.fullMaskReveal.setAttribute('x', x0);
|
||||
this.fullMaskReveal.setAttribute('y', y0);
|
||||
this.fullMaskReveal.setAttribute('width', this.width);
|
||||
this.fullMaskReveal.setAttribute('height', this.height);
|
||||
const fullSize = {
|
||||
'x': x0,
|
||||
'y': y0,
|
||||
'width': this.width,
|
||||
'height': this.height,
|
||||
};
|
||||
|
||||
this.lineMaskReveal.setAttribute('x', x0);
|
||||
this.lineMaskReveal.setAttribute('y', y0);
|
||||
this.lineMaskReveal.setAttribute('width', this.width);
|
||||
this.lineMaskReveal.setAttribute('height', this.height);
|
||||
svg.setAttributes(this.fullMaskReveal, fullSize);
|
||||
svg.setAttributes(this.lineMaskReveal, fullSize);
|
||||
|
||||
this.base.setAttribute('viewBox', (
|
||||
x0 + ' ' + y0 + ' ' +
|
||||
|
|
|
@ -153,4 +153,35 @@ defineDescribe('SequenceDiagram', [
|
|||
expect(content).toContain('<g class="region collapsed"');
|
||||
});
|
||||
|
||||
it('measures OS fonts correctly on the first render', (done) => {
|
||||
const code = 'title message';
|
||||
const sd = new SequenceDiagram(code);
|
||||
const widthImmediate = sd.getSize().width;
|
||||
expect(widthImmediate).toBeGreaterThan(40);
|
||||
|
||||
sd.set(code);
|
||||
expect(sd.getSize().width).toEqual(widthImmediate);
|
||||
|
||||
setTimeout(() => {
|
||||
sd.set(code);
|
||||
expect(sd.getSize().width).toEqual(widthImmediate);
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('measures embedded fonts correctly on the first render', (done) => {
|
||||
const code = 'theme sketch\ntitle message';
|
||||
const sd = new SequenceDiagram(code);
|
||||
const widthImmediate = sd.getSize().width;
|
||||
expect(widthImmediate).toBeGreaterThan(40);
|
||||
|
||||
sd.set(code);
|
||||
expect(sd.getSize().width).toEqual(widthImmediate);
|
||||
|
||||
setTimeout(() => {
|
||||
sd.set(code);
|
||||
expect(sd.getSize().width).toEqual(widthImmediate);
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -50,6 +50,7 @@ define([
|
|||
boxLayer: clickable,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
clickable.insertBefore(svg.make('rect', {
|
||||
'x': x - text.width / 2,
|
||||
|
|
|
@ -56,6 +56,7 @@ define([
|
|||
boxLayer: blockInfo.hold,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
|
||||
const labelRender = SVGShapes.renderBoxedText(label, {
|
||||
|
@ -67,6 +68,7 @@ define([
|
|||
boxLayer: env.lineMaskLayer,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
|
||||
const labelHeight = Math.max(
|
||||
|
|
|
@ -227,6 +227,7 @@ define([
|
|||
boxLayer: env.lineMaskLayer,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
const labelW = (label ? (
|
||||
renderedText.width +
|
||||
|
@ -353,6 +354,7 @@ define([
|
|||
boxLayer: env.lineMaskLayer,
|
||||
labelLayer,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ define([
|
|||
boxLayer: env.fullMaskLayer,
|
||||
labelLayer: clickable,
|
||||
SVGTextBlockClass: env.SVGTextBlockClass,
|
||||
textSizer: env.textSizer,
|
||||
});
|
||||
labelWidth = boxed.width;
|
||||
}
|
||||
|
|
|
@ -42,15 +42,16 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
formatted: label,
|
||||
y,
|
||||
});
|
||||
const size = env.textSizer.measure(labelNode);
|
||||
|
||||
const fullW = (
|
||||
labelNode.width +
|
||||
size.width +
|
||||
config.padding.left +
|
||||
config.padding.right
|
||||
);
|
||||
const fullH = (
|
||||
config.padding.top +
|
||||
labelNode.height +
|
||||
size.height +
|
||||
config.padding.bottom
|
||||
);
|
||||
if(x0 === null && xMid !== null) {
|
||||
|
|
|
@ -25,8 +25,6 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.nodes = [];
|
||||
this.set(initialState);
|
||||
}
|
||||
|
@ -53,8 +51,6 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
|
||||
_reset() {
|
||||
this._rebuildNodes(0);
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
_renderText() {
|
||||
|
@ -66,13 +62,10 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
const formatted = this.state.formatted;
|
||||
this._rebuildNodes(formatted.length);
|
||||
|
||||
let maxWidth = 0;
|
||||
this.nodes.forEach(({text, element}, i) => {
|
||||
const ln = formatted[i].reduce((v, pt) => v + pt.text, '');
|
||||
text.nodeValue = ln;
|
||||
maxWidth = Math.max(maxWidth, ln.length);
|
||||
});
|
||||
this.width = maxWidth;
|
||||
}
|
||||
|
||||
_updateX() {
|
||||
|
@ -85,7 +78,6 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
this.nodes.forEach(({element}, i) => {
|
||||
element.setAttribute('y', this.state.y + i);
|
||||
});
|
||||
this.height = this.nodes.length;
|
||||
}
|
||||
|
||||
firstLine() {
|
||||
|
@ -123,6 +115,11 @@ define(['svg/SVGUtilities'], (svg) => {
|
|||
|
||||
class SizeTester {
|
||||
measure(attrs, formatted) {
|
||||
if(attrs.state) {
|
||||
formatted = attrs.state.formatted;
|
||||
attrs = attrs.state.attrs;
|
||||
}
|
||||
|
||||
if(!formatted || !formatted.length) {
|
||||
return {width: 0, height: 0};
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ define([
|
|||
labelLayer,
|
||||
boxRenderer = null,
|
||||
SVGTextBlockClass = SVGTextBlock,
|
||||
textSizer,
|
||||
}) {
|
||||
if(!formatted || !formatted.length) {
|
||||
return {width: 0, height: 0, label: null, box: null};
|
||||
|
@ -88,20 +89,21 @@ define([
|
|||
y: y + padding.top,
|
||||
});
|
||||
|
||||
const width = (label.width + padding.left + padding.right);
|
||||
const height = (label.height + padding.top + padding.bottom);
|
||||
const size = textSizer.measure(label);
|
||||
const width = (size.width + padding.left + padding.right);
|
||||
const height = (size.height + padding.top + padding.bottom);
|
||||
|
||||
let box = null;
|
||||
if(boxRenderer) {
|
||||
box = boxRenderer({
|
||||
'x': anchorX - label.width * shift - padding.left,
|
||||
'x': anchorX - size.width * shift - padding.left,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
} else {
|
||||
box = renderBox(boxAttrs, {
|
||||
'x': anchorX - label.width * shift - padding.left,
|
||||
'x': anchorX - size.width * shift - padding.left,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
|
|
|
@ -54,8 +54,17 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
});
|
||||
|
||||
describe('renderBoxedText', () => {
|
||||
let o = null;
|
||||
let sizer = null;
|
||||
|
||||
beforeEach(() => {
|
||||
o = document.createElement('p');
|
||||
sizer = {
|
||||
measure: () => ({width: 64, height: 128}),
|
||||
};
|
||||
});
|
||||
|
||||
it('renders a label', () => {
|
||||
const o = document.createElement('p');
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
|
||||
x: 1,
|
||||
y: 2,
|
||||
|
@ -64,6 +73,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
labelAttrs: {'font-size': 10, 'line-height': 1.5, 'foo': 'bar'},
|
||||
boxLayer: o,
|
||||
labelLayer: o,
|
||||
textSizer: sizer,
|
||||
});
|
||||
expect(rendered.label.state.formatted).toEqual([[{text: 'foo'}]]);
|
||||
expect(rendered.label.state.x).toEqual(5);
|
||||
|
@ -72,7 +82,6 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
});
|
||||
|
||||
it('positions a box beneath the rendered label', () => {
|
||||
const o = document.createElement('p');
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
|
||||
x: 1,
|
||||
y: 2,
|
||||
|
@ -81,16 +90,19 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
labelAttrs: {'font-size': 10, 'line-height': 1.5},
|
||||
boxLayer: o,
|
||||
labelLayer: o,
|
||||
textSizer: sizer,
|
||||
});
|
||||
expect(rendered.box.getAttribute('x')).toEqual('1');
|
||||
expect(rendered.box.getAttribute('y')).toEqual('2');
|
||||
expect(rendered.box.getAttribute('height')).toEqual('55');
|
||||
expect(rendered.box.getAttribute('width'))
|
||||
.toEqual(String(4 + 16 + 64));
|
||||
expect(rendered.box.getAttribute('height'))
|
||||
.toEqual(String(8 + 32 + 128));
|
||||
expect(rendered.box.getAttribute('foo')).toEqual('bar');
|
||||
expect(rendered.box.parentNode).toEqual(o);
|
||||
});
|
||||
|
||||
it('returns the size of the rendered box', () => {
|
||||
const o = document.createElement('p');
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
|
||||
x: 1,
|
||||
y: 2,
|
||||
|
@ -99,9 +111,10 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
|
|||
labelAttrs: {'font-size': 10, 'line-height': 1.5},
|
||||
boxLayer: o,
|
||||
labelLayer: o,
|
||||
textSizer: sizer,
|
||||
});
|
||||
expect(rendered.width).toBeGreaterThan(20 - 1);
|
||||
expect(rendered.height).toEqual(55);
|
||||
expect(rendered.width).toEqual(4 + 16 + 64);
|
||||
expect(rendered.height).toEqual(8 + 32 + 128);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,6 +38,27 @@ define(['./SVGUtilities'], (svg) => {
|
|||
});
|
||||
}
|
||||
|
||||
function measureLine(tester, line) {
|
||||
if(!line.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const labelKey = JSON.stringify(line);
|
||||
const knownWidth = tester.widths.get(labelKey);
|
||||
if(knownWidth !== undefined) {
|
||||
return knownWidth;
|
||||
}
|
||||
|
||||
// getComputedTextLength forces a reflow, so only call it if nothing
|
||||
// else can tell us the length
|
||||
|
||||
svg.empty(tester.node);
|
||||
populateSvgTextLine(tester.node, line);
|
||||
const width = tester.node.getComputedTextLength();
|
||||
tester.widths.set(labelKey, width);
|
||||
return width;
|
||||
}
|
||||
|
||||
const EMPTY = [];
|
||||
|
||||
class SVGTextBlock {
|
||||
|
@ -49,8 +70,6 @@ define(['./SVGUtilities'], (svg) => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.lines = [];
|
||||
this.set(initialState);
|
||||
}
|
||||
|
@ -76,8 +95,6 @@ define(['./SVGUtilities'], (svg) => {
|
|||
|
||||
_reset() {
|
||||
this._rebuildLines(0);
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
_renderText() {
|
||||
|
@ -93,7 +110,6 @@ define(['./SVGUtilities'], (svg) => {
|
|||
|
||||
this._rebuildLines(formatted.length);
|
||||
|
||||
let maxWidth = 0;
|
||||
this.lines.forEach((ln, i) => {
|
||||
const id = JSON.stringify(formatted[i]);
|
||||
if(id !== ln.latest) {
|
||||
|
@ -101,9 +117,7 @@ define(['./SVGUtilities'], (svg) => {
|
|||
populateSvgTextLine(ln.node, formatted[i]);
|
||||
ln.latest = id;
|
||||
}
|
||||
maxWidth = Math.max(maxWidth, ln.node.getComputedTextLength());
|
||||
});
|
||||
this.width = maxWidth;
|
||||
}
|
||||
|
||||
_updateX() {
|
||||
|
@ -117,7 +131,6 @@ define(['./SVGUtilities'], (svg) => {
|
|||
this.lines.forEach(({node}, i) => {
|
||||
node.setAttribute('y', this.state.y + i * lineHeight + size);
|
||||
});
|
||||
this.height = lineHeight * this.lines.length;
|
||||
}
|
||||
|
||||
firstLine() {
|
||||
|
@ -164,47 +177,62 @@ define(['./SVGUtilities'], (svg) => {
|
|||
this.cache = new Map();
|
||||
}
|
||||
|
||||
measure(attrs, formatted) {
|
||||
if(!formatted || !formatted.length) {
|
||||
return {width: 0, height: 0};
|
||||
}
|
||||
if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
_measureHeight({attrs, formatted}) {
|
||||
return formatted.length * fontDetails(attrs).lineHeight;
|
||||
}
|
||||
|
||||
let tester = this.cache.get(attrs);
|
||||
_measureWidth({attrs, formatted}) {
|
||||
if(!formatted.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const attrKey = JSON.stringify(attrs);
|
||||
let tester = this.cache.get(attrKey);
|
||||
if(!tester) {
|
||||
tester = svg.make('text', attrs);
|
||||
this.testers.appendChild(tester);
|
||||
this.cache.set(attrs, tester);
|
||||
const node = svg.make('text', attrs);
|
||||
this.testers.appendChild(node);
|
||||
tester = {
|
||||
node,
|
||||
widths: new Map(),
|
||||
};
|
||||
this.cache.set(attrKey, tester);
|
||||
}
|
||||
|
||||
if(!this.testers.parentNode) {
|
||||
this.container.appendChild(this.testers);
|
||||
}
|
||||
|
||||
let width = 0;
|
||||
formatted.forEach((line) => {
|
||||
svg.empty(tester);
|
||||
populateSvgTextLine(tester, line);
|
||||
width = Math.max(width, tester.getComputedTextLength());
|
||||
});
|
||||
return (formatted
|
||||
.map((line) => measureLine(tester, line))
|
||||
.reduce((a, b) => Math.max(a, b), 0)
|
||||
);
|
||||
}
|
||||
|
||||
_getMeasurementOpts(attrs, formatted) {
|
||||
if(!formatted) {
|
||||
if(typeof attrs === 'object' && attrs.state) {
|
||||
formatted = attrs.state.formatted || [];
|
||||
attrs = attrs.state.attrs;
|
||||
} else {
|
||||
formatted = [];
|
||||
}
|
||||
} else if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
}
|
||||
return {attrs, formatted};
|
||||
}
|
||||
|
||||
measure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
return {
|
||||
width,
|
||||
height: formatted.length * fontDetails(attrs).lineHeight,
|
||||
width: this._measureWidth(opts),
|
||||
height: this._measureHeight(opts),
|
||||
};
|
||||
}
|
||||
|
||||
measureHeight(attrs, formatted) {
|
||||
if(!formatted) {
|
||||
return 0;
|
||||
}
|
||||
if(!Array.isArray(formatted)) {
|
||||
throw new Error('Invalid formatted text: ' + formatted);
|
||||
}
|
||||
|
||||
return formatted.length * fontDetails(attrs).lineHeight;
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
return this._measureHeight(opts);
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
|
|
|
@ -68,12 +68,6 @@ defineDescribe('SVGTextBlock', [
|
|||
expect(hold.children[1].innerHTML).toEqual('bar');
|
||||
});
|
||||
|
||||
it('populates width and height with the size of the text', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
expect(block.width).toBeGreaterThan(0);
|
||||
expect(block.height).toEqual(30);
|
||||
});
|
||||
|
||||
it('re-uses text nodes when possible, adding more if needed', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
const line0 = hold.children[0];
|
||||
|
@ -129,8 +123,7 @@ defineDescribe('SVGTextBlock', [
|
|||
block.set({formatted: []});
|
||||
expect(hold.children.length).toEqual(0);
|
||||
expect(block.state.formatted).toEqual([]);
|
||||
expect(block.width).toEqual(0);
|
||||
expect(block.height).toEqual(0);
|
||||
expect(block.lines.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -148,6 +141,13 @@ defineDescribe('SVGTextBlock', [
|
|||
expect(size.height).toEqual(15);
|
||||
});
|
||||
|
||||
it('calculates the size of text blocks', () => {
|
||||
block.set({formatted: [[{text: 'foo'}]]});
|
||||
const size = tester.measure(block);
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
expect(size.height).toEqual(15);
|
||||
});
|
||||
|
||||
it('measures multiline text', () => {
|
||||
const size = tester.measure(attrs, [
|
||||
[{text: 'foo'}],
|
||||
|
|
|
@ -7,13 +7,17 @@ define(() => {
|
|||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
function make(type, attrs = {}, children = []) {
|
||||
const o = document.createElementNS(NS, type);
|
||||
function setAttributes(target, attrs) {
|
||||
for(const k in attrs) {
|
||||
if(attrs.hasOwnProperty(k)) {
|
||||
o.setAttribute(k, attrs[k]);
|
||||
target.setAttribute(k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function make(type, attrs = {}, children = []) {
|
||||
const o = document.createElementNS(NS, type);
|
||||
setAttributes(o, attrs);
|
||||
for(const c of children) {
|
||||
o.appendChild(c);
|
||||
}
|
||||
|
@ -37,6 +41,7 @@ define(() => {
|
|||
makeText,
|
||||
make,
|
||||
makeContainer,
|
||||
setAttributes,
|
||||
empty,
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue