Improve text render on multi-line

This commit is contained in:
Paulo Gustavo Veiga 2023-02-10 23:45:00 -08:00
parent bfc043ebbe
commit 74d147b6f6
11 changed files with 94 additions and 127 deletions

View File

@ -59,6 +59,7 @@ class EditorComponent extends Events {
resize: 'none', resize: 'none',
overflow: 'hidden', overflow: 'hidden',
padding: '0px 0px 0px 0px', padding: '0px 0px 0px 0px',
lineHeight: '100%',
}); });
result.append(textareaElem); result.append(textareaElem);
@ -121,7 +122,7 @@ class EditorComponent extends Events {
}); });
} }
private resize(text?: string) { private resize(text?: string): void {
// Force relayout ... // Force relayout ...
EventBus.instance.fireEvent('forceLayout'); EventBus.instance.fireEvent('forceLayout');
@ -132,15 +133,12 @@ class EditorComponent extends Events {
const textValue = text || this.getTextAreaText(); const textValue = text || this.getTextAreaText();
const textElem = this.getTextareaElem(); const textElem = this.getTextareaElem();
const lines = textValue.split('\n');
let maxLineLength = 1; const rows = [...textValue].filter((x) => x === '\n').length + 1;
lines.forEach((line: string) => { const maxLineLength = Math.max(...textValue.split('\n').map((l) => l.length));
maxLineLength = Math.max(line.length, maxLineLength);
});
textElem.attr('cols', maxLineLength); textElem.attr('cols', maxLineLength);
textElem.attr('rows', lines.length); textElem.attr('rows', rows);
this._containerElem.css({ this._containerElem.css({
width: `${maxLineLength + 2}em`, width: `${maxLineLength + 2}em`,

View File

@ -1233,7 +1233,7 @@ abstract class Topic extends NodeGraph {
textShape.setFontName(fontFamily); textShape.setFontName(fontFamily);
const text = this.getText(); const text = this.getText();
textShape.setText(text.trim()); textShape.setText(text);
// Update outer shape style ... // Update outer shape style ...
const outerShape = this.getOuterShape(); const outerShape = this.getOuterShape();

View File

@ -47,12 +47,9 @@ abstract class AbstractBasicSorter extends ChildrenSorterStrategy {
if (children.length === 0 || node.areChildrenShrunken()) { if (children.length === 0 || node.areChildrenShrunken()) {
result = height; result = height;
} else { } else {
let childrenHeight = 0; const childrenHeight = children
.map((child) => this._computeChildrenHeight(treeSet, child, heightCache))
children.forEach((child) => { .reduce((accumulator, currentValue) => accumulator + currentValue, 0);
childrenHeight += this._computeChildrenHeight(treeSet, child, heightCache);
});
result = Math.max(height, childrenHeight); result = Math.max(height, childrenHeight);
} }
@ -63,7 +60,7 @@ abstract class AbstractBasicSorter extends ChildrenSorterStrategy {
return result; return result;
} }
protected _getSortedChildren(treeSet: RootedTreeSet, node: Node) { protected _getSortedChildren(treeSet: RootedTreeSet, node: Node): Node[] {
const result = treeSet.getChildren(node); const result = treeSet.getChildren(node);
result.sort((a, b) => a.getOrder() - b.getOrder()); result.sort((a, b) => a.getOrder() - b.getOrder());
return result; return result;

View File

@ -148,7 +148,7 @@ class BalancedSorter extends AbstractBasicSorter {
} }
} }
computeOffsets(treeSet: RootedTreeSet, node: Node) { computeOffsets(treeSet: RootedTreeSet, node: Node): Map<number, PositionType> {
$assert(treeSet, 'treeSet can no be null.'); $assert(treeSet, 'treeSet can no be null.');
$assert(node, 'node can no be null.'); $assert(node, 'node can no be null.');
@ -180,7 +180,7 @@ class BalancedSorter extends AbstractBasicSorter {
let ysum = 0; let ysum = 0;
// Calculate the offsets ... // Calculate the offsets ...
const result = {}; const result = new Map<number, PositionType>();
for (let i = 0; i < heights.length; i++) { for (let i = 0; i < heights.length; i++) {
const direction = heights[i].order % 2 ? -1 : 1; const direction = heights[i].order % 2 ? -1 : 1;
@ -202,7 +202,7 @@ class BalancedSorter extends AbstractBasicSorter {
$assert(!Number.isNaN(xOffset), 'xOffset can not be null'); $assert(!Number.isNaN(xOffset), 'xOffset can not be null');
$assert(!Number.isNaN(yOffset), 'yOffset can not be null'); $assert(!Number.isNaN(yOffset), 'yOffset can not be null');
result[heights[i].id] = { x: xOffset, y: yOffset }; result.set(heights[i].id, { x: xOffset, y: yOffset });
} }
return result; return result;
} }

View File

@ -22,7 +22,7 @@ import PositionType from '../PositionType';
abstract class ChildrenSorterStrategy { abstract class ChildrenSorterStrategy {
abstract computeChildrenIdByHeights(treeSet: RootedTreeSet, node: Node): Map<number, number>; abstract computeChildrenIdByHeights(treeSet: RootedTreeSet, node: Node): Map<number, number>;
abstract computeOffsets(treeSet: RootedTreeSet, node: Node): void; abstract computeOffsets(treeSet: RootedTreeSet, node: Node): Map<number, PositionType>;
abstract insert(treeSet: RootedTreeSet, parent: Node, child: Node, order: number): void; abstract insert(treeSet: RootedTreeSet, parent: Node, child: Node, order: number): void;

View File

@ -145,7 +145,6 @@ class Node {
/** */ /** */
setSize(size: SizeType) { setSize(size: SizeType) {
$assert($defined(size), 'Size can not be null');
this._setProperty('size', { ...size }); this._setProperty('size', { ...size });
} }
@ -177,15 +176,8 @@ class Node {
setPosition(position: PositionType) { setPosition(position: PositionType) {
// This is a performance improvement to avoid movements that really could be avoided. // This is a performance improvement to avoid movements that really could be avoided.
const currentPos = this.getPosition();
if (
currentPos == null ||
Math.abs(currentPos.x - position.x) > 2 ||
Math.abs(currentPos.y - position.y) > 2
) {
this._setProperty('position', position); this._setProperty('position', position);
} }
}
_setProperty(key: string, value) { _setProperty(key: string, value) {
let prop = this._properties[key]; let prop = this._properties[key];

View File

@ -81,15 +81,17 @@ class OriginalLayout {
// Calculate all node heights ... // Calculate all node heights ...
const sorter = node.getSorter(); const sorter = node.getSorter();
const heightById = sorter.computeChildrenIdByHeights(this._treeSet, node); const heightById = sorter.computeChildrenIdByHeights(this._treeSet, node);
this._layoutChildren(node, heightById);
this._fixOverlapping(node, heightById); this.layoutChildren(node, heightById);
// this.fixOverlapping(node, heightById);
}); });
} }
private _layoutChildren(node: Node, heightById: Map<number, number>): void { private layoutChildren(node: Node, heightById: Map<number, number>): void {
const nodeId = node.getId(); const nodeId = node.getId();
const children = this._treeSet.getChildren(node); const children = this._treeSet.getChildren(node);
const parent = this._treeSet.getParent(node); const parent = this._treeSet.getParent(node);
const childrenOrderMoved = children.some((child) => child.hasOrderChanged()); const childrenOrderMoved = children.some((child) => child.hasOrderChanged());
const childrenSizeChanged = children.some((child) => child.hasSizeChanged()); const childrenSizeChanged = children.some((child) => child.hasSizeChanged());
@ -99,43 +101,24 @@ class OriginalLayout {
const parentHeightChanged = parent ? parent._heightChanged : false; const parentHeightChanged = parent ? parent._heightChanged : false;
const heightChanged = node._branchHeight !== newBranchHeight; const heightChanged = node._branchHeight !== newBranchHeight;
// eslint-disable-next-line no-param-reassign
node._heightChanged = heightChanged || parentHeightChanged; node._heightChanged = heightChanged || parentHeightChanged;
if (childrenOrderMoved || childrenSizeChanged || heightChanged || parentHeightChanged) { if (childrenOrderMoved || childrenSizeChanged || heightChanged || parentHeightChanged) {
const sorter = node.getSorter(); const sorter = node.getSorter();
const offsetById = sorter.computeOffsets(this._treeSet, node); const offsetById = sorter.computeOffsets(this._treeSet, node);
const parentPosition = node.getPosition(); const parentPosition = node.getPosition();
const me = this;
children.forEach((child) => { children.forEach((child) => {
const offset = offsetById[child.getId()]; const offset = offsetById.get(child.getId())!;
const childFreeDisplacement = child.getFreeDisplacement();
const direction = node.getSorter().getChildDirection(me._treeSet, child);
if (
(direction > 0 && childFreeDisplacement.x < 0) ||
(direction < 0 && childFreeDisplacement.x > 0)
) {
child.resetFreeDisplacement();
child.setFreeDisplacement({
x: -childFreeDisplacement.x,
y: childFreeDisplacement.y,
});
}
offset.x += child.getFreeDisplacement().x;
offset.y += child.getFreeDisplacement().y;
const parentX = parentPosition.x; const parentX = parentPosition.x;
const parentY = parentPosition.y; const parentY = parentPosition.y;
const newPos = { const newPos = {
x: parentX + offset.x, x: parentX + offset.x,
y: parentY + offset.y + me._calculateAlignOffset(node, child, heightById), y: parentY + offset.y + this.calculateAlignOffset(node, child, heightById),
}; };
me._treeSet.updateBranchPosition(child, newPos); this._treeSet.updateBranchPosition(child, newPos);
}); });
node._branchHeight = newBranchHeight; node._branchHeight = newBranchHeight;
@ -143,15 +126,11 @@ class OriginalLayout {
// Continue reordering the children nodes ... // Continue reordering the children nodes ...
children.forEach((child) => { children.forEach((child) => {
this._layoutChildren(child, heightById); this.layoutChildren(child, heightById);
}); });
} }
private _calculateAlignOffset(node: Node, child: Node, heightById: Map<number, number>): number { private calculateAlignOffset(node: Node, child: Node, heightById: Map<number, number>): number {
if (child.isFree()) {
return 0;
}
let offset = 0; let offset = 0;
const nodeHeight = node.getSize().height; const nodeHeight = node.getSize().height;
@ -192,14 +171,11 @@ class OriginalLayout {
); );
} }
private _fixOverlapping(node: Node, heightById: Map<number, number>): void { private fixOverlapping(node: Node, heightById: Map<number, number>): void {
const children = this._treeSet.getChildren(node); const children = this._treeSet.getChildren(node);
if (node.isFree()) {
this._shiftBranches(node, heightById);
}
children.forEach((child) => { children.forEach((child) => {
this._fixOverlapping(child, heightById); this.fixOverlapping(child, heightById);
}); });
} }

View File

@ -334,9 +334,8 @@ class RootedTreeSet {
const yOffset = oldPos.y - position.y; const yOffset = oldPos.y - position.y;
const children = this.getChildren(node); const children = this.getChildren(node);
const me = this;
children.forEach((child) => { children.forEach((child) => {
me.shiftBranchPosition(child, xOffset, yOffset); this.shiftBranchPosition(child, xOffset, yOffset);
}); });
} }

View File

@ -198,25 +198,11 @@ class SymmetricSorter extends AbstractBasicSorter {
node.setOrder(0); node.setOrder(0);
} }
/** computeOffsets(treeSet: RootedTreeSet, node: Node): Map<number, PositionType> {
* @param treeSet
* @param node
* @throws will throw an error if treeSet is null or undefined
* @throws will throw an error if node is null or undefined
* @throws will throw an error if the calculated x offset cannot be converted to a numeric
* value, is null or undefined
* @throws will throw an error if the calculated y offset cannot be converted to a numeric
* value, is null or undefined
* @return offsets
*/
computeOffsets(treeSet: RootedTreeSet, node: Node) {
$assert(treeSet, 'treeSet can no be null.');
$assert(node, 'node can no be null.');
const children = this._getSortedChildren(treeSet, node); const children = this._getSortedChildren(treeSet, node);
// Compute heights ... // Compute heights ...
const heights = children const sizeById = children
.map((child) => ({ .map((child) => ({
id: child.getId(), id: child.getId(),
order: child.getOrder(), order: child.getOrder(),
@ -227,30 +213,26 @@ class SymmetricSorter extends AbstractBasicSorter {
.reverse(); .reverse();
// Compute the center of the branch ... // Compute the center of the branch ...
let totalHeight = 0; const totalHeight = sizeById
heights.forEach((elem) => { .map((e) => e.height)
totalHeight += elem.height; .reduce((accumulator, currentValue) => accumulator + currentValue, 0);
});
let ysum = totalHeight / 2; let ysum = totalHeight / 2;
// Calculate the offsets ... // Calculate the offsets ...
const result = {}; const result = new Map<number, PositionType>();
for (let i = 0; i < heights.length; i++) { for (let i = 0; i < sizeById.length; i++) {
ysum -= heights[i].height; ysum -= sizeById[i].height;
const childNode = treeSet.find(heights[i].id); const childNode = treeSet.find(sizeById[i].id);
const direction = this.getChildDirection(treeSet, childNode); const direction = this.getChildDirection(treeSet, childNode);
const yOffset = ysum + heights[i].height / 2; const yOffset = ysum + sizeById[i].height / 2;
const xOffset = const xOffset =
direction * direction *
(heights[i].width / 2 + (sizeById[i].width / 2 +
node.getSize().width / 2 + node.getSize().width / 2 +
SymmetricSorter.INTERNODE_HORIZONTAL_PADDING); SymmetricSorter.INTERNODE_HORIZONTAL_PADDING);
$assert(!Number.isNaN(xOffset), 'xOffset can not be null'); result.set(sizeById[i].id, { x: xOffset, y: yOffset });
$assert(!Number.isNaN(yOffset), 'yOffset can not be null');
result[heights[i].id] = { x: xOffset, y: yOffset };
} }
return result; return result;
} }

View File

@ -96,8 +96,7 @@ class Text extends WorkspaceElement<TextPeer> {
} }
getFontHeight(): number { getFontHeight(): number {
const lines = this.peer.getText().split('\n').length; return this.getShapeHeight() / this.peer.getTextLines().length;
return this.getShapeHeight() / lines;
} }
getPosition(): PositionType { getPosition(): PositionType {

View File

@ -20,53 +20,77 @@ import FontPeer, { FontStyle } from './FontPeer';
import ElementPeer from './ElementPeer'; import ElementPeer from './ElementPeer';
import { getPosition } from '../utils/DomUtils'; import { getPosition } from '../utils/DomUtils';
import SizeType from '../../SizeType'; import SizeType from '../../SizeType';
import PositionType from '../../PositionType';
class TextPeer extends ElementPeer { class TextPeer extends ElementPeer {
private _position: { x: number; y: number }; private _position: { x: number; y: number };
private _font: FontPeer; private _font: FontPeer;
private _textAlign: any; private _textAlign: string;
private _text: string | undefined; private _text: string;
constructor(fontPeer: FontPeer) { constructor(fontPeer: FontPeer) {
const svgElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'text'); const svgElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'text');
super(svgElement); super(svgElement);
this._position = { x: 0, y: 0 }; this._position = { x: 0, y: 0 };
this._font = fontPeer; this._font = fontPeer;
this._text = '';
this._textAlign = 'left';
} }
append(element: ElementPeer) { append(element: ElementPeer): void {
this._native.appendChild(element._native); this._native.appendChild(element._native);
} }
setTextAlignment(align: string) { setTextAlignment(align: string): void {
this._textAlign = align; this._textAlign = align;
} }
getTextAlignment() { getTextAlignment(): 'left' | 'right' | 'center' {
return $defined(this._textAlign) ? this._textAlign : 'left'; return this._textAlign ? (this._textAlign as 'left' | 'right' | 'center') : 'left';
} }
setText(text: string) { setText(text: string) {
this._text = text;
// Remove all previous nodes ... // Remove all previous nodes ...
while (this._native.firstChild) { while (this._native.firstChild) {
this._native.removeChild(this._native.firstChild); this._native.removeChild(this._native.firstChild);
} }
this._text = text; // Add nodes ...
if (text) { this.getTextLines().forEach((l) => {
const lines = text.split('\n'); // Append a new line ...
lines.forEach((line) => {
const tspan = window.document.createElementNS(ElementPeer.svgNamespace, 'tspan'); const tspan = window.document.createElementNS(ElementPeer.svgNamespace, 'tspan');
tspan.setAttribute('dy', '1em'); tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', String(this.getPosition().x)); tspan.setAttribute('x', this.getPosition().x.toFixed(1));
tspan.textContent = line.length === 0 ? ' ' : line; // Add new line ...
tspan.textContent = l || ' ';
this._native.appendChild(tspan); this._native.appendChild(tspan);
}); });
} }
getTextLines(): string[] {
const result: string[] = [];
if (this._text) {
const text = this._text;
let line = '';
let i = 0;
do {
const c = text[i];
if (c === '\n' || i === text.length) {
result.push(line);
line = '';
} else {
line = line + c;
}
i = i + 1;
} while (i < text.length + 1);
}
return result;
} }
getText(): any { getText(): string {
return this._text; return this._text;
} }
@ -81,20 +105,20 @@ class TextPeer extends ElementPeer {
}); });
} }
getPosition() { getPosition(): PositionType {
return this._position; return this._position;
} }
getNativePosition() { getNativePosition(): { left: number; top: number } {
return getPosition(this._native); return getPosition(this._native);
} }
setFont(fontName: string, size: number, style: string, weight: string): void { setFont(fontName: string, size: number, style: string, weight: string): void {
if ($defined(fontName)) { if (fontName) {
this._font = new FontPeer(fontName); this._font = new FontPeer(fontName);
} }
if ($defined(style)) { if (style) {
this._font.setStyle(style); this._font.setStyle(style);
} }
if ($defined(weight)) { if ($defined(weight)) {
@ -103,10 +127,10 @@ class TextPeer extends ElementPeer {
if ($defined(size)) { if ($defined(size)) {
this._font.setSize(size); this._font.setSize(size);
} }
this._updateFontStyle(); this.updateFontStyle();
} }
_updateFontStyle() { private updateFontStyle() {
this._native.setAttribute('font-family', this._font.getFontName()); this._native.setAttribute('font-family', this._font.getFontName());
this._native.setAttribute('font-size', this._font.getGraphSize()); this._native.setAttribute('font-size', this._font.getGraphSize());
this._native.setAttribute('font-style', this._font.getStyle()); this._native.setAttribute('font-style', this._font.getStyle());
@ -123,17 +147,17 @@ class TextPeer extends ElementPeer {
setTextSize(size: number) { setTextSize(size: number) {
this._font.setSize(size); this._font.setSize(size);
this._updateFontStyle(); this.updateFontStyle();
} }
setStyle(style: string) { setStyle(style: string) {
this._font.setStyle(style); this._font.setStyle(style);
this._updateFontStyle(); this.updateFontStyle();
} }
setWeight(weight: string) { setWeight(weight: string) {
this._font.setWeight(weight); this._font.setWeight(weight);
this._updateFontStyle(); this.updateFontStyle();
} }
setFontName(fontName: string): void { setFontName(fontName: string): void {
@ -142,7 +166,7 @@ class TextPeer extends ElementPeer {
this._font.setSize(oldFont.getSize()); this._font.setSize(oldFont.getSize());
this._font.setStyle(oldFont.getStyle()); this._font.setStyle(oldFont.getStyle());
this._font.setWeight(oldFont.getWeight()); this._font.setWeight(oldFont.getWeight());
this._updateFontStyle(); this.updateFontStyle();
} }
getFontStyle(): FontStyle { getFontStyle(): FontStyle {
@ -159,7 +183,7 @@ class TextPeer extends ElementPeer {
setFontSize(size: number): void { setFontSize(size: number): void {
this._font.setSize(size); this._font.setSize(size);
this._updateFontStyle(); this.updateFontStyle();
} }
getShapeWidth(): number { getShapeWidth(): number {