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

View File

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

View File

@ -47,12 +47,9 @@ abstract class AbstractBasicSorter extends ChildrenSorterStrategy {
if (children.length === 0 || node.areChildrenShrunken()) {
result = height;
} else {
let childrenHeight = 0;
children.forEach((child) => {
childrenHeight += this._computeChildrenHeight(treeSet, child, heightCache);
});
const childrenHeight = children
.map((child) => this._computeChildrenHeight(treeSet, child, heightCache))
.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
result = Math.max(height, childrenHeight);
}
@ -63,7 +60,7 @@ abstract class AbstractBasicSorter extends ChildrenSorterStrategy {
return result;
}
protected _getSortedChildren(treeSet: RootedTreeSet, node: Node) {
protected _getSortedChildren(treeSet: RootedTreeSet, node: Node): Node[] {
const result = treeSet.getChildren(node);
result.sort((a, b) => a.getOrder() - b.getOrder());
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(node, 'node can no be null.');
@ -180,7 +180,7 @@ class BalancedSorter extends AbstractBasicSorter {
let ysum = 0;
// Calculate the offsets ...
const result = {};
const result = new Map<number, PositionType>();
for (let i = 0; i < heights.length; i++) {
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(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;
}

View File

@ -22,7 +22,7 @@ import PositionType from '../PositionType';
abstract class ChildrenSorterStrategy {
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;

View File

@ -145,7 +145,6 @@ class Node {
/** */
setSize(size: SizeType) {
$assert($defined(size), 'Size can not be null');
this._setProperty('size', { ...size });
}
@ -177,14 +176,7 @@ class Node {
setPosition(position: PositionType) {
// 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) {

View File

@ -81,15 +81,17 @@ class OriginalLayout {
// Calculate all node heights ...
const sorter = node.getSorter();
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 children = this._treeSet.getChildren(node);
const parent = this._treeSet.getParent(node);
const childrenOrderMoved = children.some((child) => child.hasOrderChanged());
const childrenSizeChanged = children.some((child) => child.hasSizeChanged());
@ -99,43 +101,24 @@ class OriginalLayout {
const parentHeightChanged = parent ? parent._heightChanged : false;
const heightChanged = node._branchHeight !== newBranchHeight;
// eslint-disable-next-line no-param-reassign
node._heightChanged = heightChanged || parentHeightChanged;
if (childrenOrderMoved || childrenSizeChanged || heightChanged || parentHeightChanged) {
const sorter = node.getSorter();
const offsetById = sorter.computeOffsets(this._treeSet, node);
const parentPosition = node.getPosition();
const me = this;
children.forEach((child) => {
const offset = offsetById[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 offset = offsetById.get(child.getId())!;
const parentX = parentPosition.x;
const parentY = parentPosition.y;
const newPos = {
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;
@ -143,15 +126,11 @@ class OriginalLayout {
// Continue reordering the children nodes ...
children.forEach((child) => {
this._layoutChildren(child, heightById);
this.layoutChildren(child, heightById);
});
}
private _calculateAlignOffset(node: Node, child: Node, heightById: Map<number, number>): number {
if (child.isFree()) {
return 0;
}
private calculateAlignOffset(node: Node, child: Node, heightById: Map<number, number>): number {
let offset = 0;
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);
if (node.isFree()) {
this._shiftBranches(node, heightById);
}
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 children = this.getChildren(node);
const me = this;
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);
}
/**
* @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.');
computeOffsets(treeSet: RootedTreeSet, node: Node): Map<number, PositionType> {
const children = this._getSortedChildren(treeSet, node);
// Compute heights ...
const heights = children
const sizeById = children
.map((child) => ({
id: child.getId(),
order: child.getOrder(),
@ -227,30 +213,26 @@ class SymmetricSorter extends AbstractBasicSorter {
.reverse();
// Compute the center of the branch ...
let totalHeight = 0;
heights.forEach((elem) => {
totalHeight += elem.height;
});
const totalHeight = sizeById
.map((e) => e.height)
.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
let ysum = totalHeight / 2;
// Calculate the offsets ...
const result = {};
for (let i = 0; i < heights.length; i++) {
ysum -= heights[i].height;
const childNode = treeSet.find(heights[i].id);
const result = new Map<number, PositionType>();
for (let i = 0; i < sizeById.length; i++) {
ysum -= sizeById[i].height;
const childNode = treeSet.find(sizeById[i].id);
const direction = this.getChildDirection(treeSet, childNode);
const yOffset = ysum + heights[i].height / 2;
const yOffset = ysum + sizeById[i].height / 2;
const xOffset =
direction *
(heights[i].width / 2 +
(sizeById[i].width / 2 +
node.getSize().width / 2 +
SymmetricSorter.INTERNODE_HORIZONTAL_PADDING);
$assert(!Number.isNaN(xOffset), 'xOffset can not be null');
$assert(!Number.isNaN(yOffset), 'yOffset can not be null');
result[heights[i].id] = { x: xOffset, y: yOffset };
result.set(sizeById[i].id, { x: xOffset, y: yOffset });
}
return result;
}

View File

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

View File

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