@fulate/ui 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/index.js +70 -49
- package/dist/text/index.d.ts +1 -1
- package/dist/text/layout.d.ts +31 -3
- package/dist/text/paint.d.ts +11 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,10 +159,13 @@ import { measureTextRun } from "@fulate/ui";
|
|
|
159
159
|
const metrics = measureTextRun(ctx, text, root.skin.text, 14, 500);
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
每次调用只执行一次 `ctx.measureText()`,返回 `font`、`width`、`lineHeight`、`drawY
|
|
163
|
-
`alphabeticBaseline
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
每次调用只执行一次 `ctx.measureText()`,返回 `font`、`width`、`lineHeight`、`drawY`、
|
|
163
|
+
`alphabeticBaseline`、`textBaseline` 和 `letterSpacing`。`drawY` 根据原生 Canvas 的
|
|
164
|
+
`fontBoundingBoxAscent`/`fontBoundingBoxDescent` 把字体行框放到行框中心;`alphabeticBaseline`
|
|
165
|
+
是同一行框内的规范 alphabetic baseline,因此不同字号的 run 可以直接对齐。空文本的 `width`
|
|
166
|
+
为 `0`,但仍测量一个空格以取得稳定字体度量。函数会把本次测量所需的文字状态写入 `ctx`,不会
|
|
167
|
+
调用 `save()` 或 `restore()`;Canvas 生命周期以及业务特有的文字顺序、对齐和绘制仍由 Shape
|
|
168
|
+
自己拥有。
|
|
166
169
|
|
|
167
170
|
### 组合型多行文本
|
|
168
171
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clearCache, layoutNextLineRange, layoutWithLines, materializeLineRange, measureLineStats, measureNaturalWidth, prepareWithSegments } from "@chenglou/pretext";
|
|
2
2
|
import { ChangeImpact, CustomEvent, Element, ElementProperties, ElementTransform, SKIN_DEFAULTS, Shape, ShapeProperties, ShapeTransform, createAuthoredLineSubpath, createGeometrySnapshot, createLineSubpath, createPathLocalId, evaluatePathSegmentPoint, getElementConnectionPort, getElementConnectionPortPoint, isReparentTransformIntent, resolveAuthoredGeometryFramePatch, resolveAuthoredSubpath, resolveReferenceTransform, resolveReferenceTransformTarget, splitPathSegment } from "@fulate/core";
|
|
3
|
-
import { clamp, isFunction, isNil, isNull, isString, isUndefined } from "lodash-es";
|
|
3
|
+
import { clamp, has, isFunction, isNil, isNull, isString, isUndefined, last } from "lodash-es";
|
|
4
4
|
import { Bound, Intersection, createCanvasGradient, createPointView, isGradient, isIdentityMatrix, isPointWithinDistance, rectEdgePosition, transformPoint } from "@fulate/share";
|
|
5
5
|
import { computePosition as computePosition$1, flip, offset, shift } from "@floating-ui/core";
|
|
6
6
|
import { Easing, Tween } from "@tweenjs/tween.js";
|
|
@@ -8,8 +8,10 @@ import { Easing, Tween } from "@tweenjs/tween.js";
|
|
|
8
8
|
var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
9
9
|
var measurementListeners = /* @__PURE__ */ new Set();
|
|
10
10
|
var observesFontLoading = false;
|
|
11
|
+
var textLineBoxMetricsCache = /* @__PURE__ */ new Map();
|
|
11
12
|
function invalidateFontMeasurements() {
|
|
12
13
|
clearCache();
|
|
14
|
+
textLineBoxMetricsCache.clear();
|
|
13
15
|
for (const listener of measurementListeners) listener();
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
@@ -50,14 +52,35 @@ function measurePlainText(text, font) {
|
|
|
50
52
|
return context.measureText(text).width;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
+
* Resolve one line box from a native Canvas measurement. The font bounding
|
|
56
|
+
* box is stable for the font state, while `alphabeticBaseline` translates the
|
|
57
|
+
* selected Canvas baseline into the same canonical alphabetic coordinate used
|
|
58
|
+
* by TextRow and composite owners.
|
|
55
59
|
*/
|
|
56
|
-
function
|
|
60
|
+
function resolveTextLinePlacement(metrics, lineHeight) {
|
|
61
|
+
const drawY = (lineHeight + metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2;
|
|
62
|
+
return {
|
|
63
|
+
drawY,
|
|
64
|
+
alphabeticBaseline: drawY - metrics.alphabeticBaseline,
|
|
65
|
+
height: lineHeight
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Measure the font line box once and reuse it for Text painting, placeholders,
|
|
70
|
+
* and composite TextBlock owners. The representative space supplies the
|
|
71
|
+
* font-level Canvas metrics without making placement depend on the content of
|
|
72
|
+
* an individual line.
|
|
73
|
+
*/
|
|
74
|
+
function measureTextLinePlacement(font, textBaseline, lineHeight) {
|
|
75
|
+
const key = `${font}\u0000${textBaseline}\u0000${lineHeight}`;
|
|
76
|
+
const cached = textLineBoxMetricsCache.get(key);
|
|
77
|
+
if (cached) return cached;
|
|
57
78
|
const context = getPlainMeasureContext();
|
|
58
79
|
context.font = font;
|
|
59
80
|
context.textBaseline = textBaseline;
|
|
60
|
-
|
|
81
|
+
const metrics = resolveTextLinePlacement(context.measureText(" "), lineHeight);
|
|
82
|
+
textLineBoxMetricsCache.set(key, metrics);
|
|
83
|
+
return metrics;
|
|
61
84
|
}
|
|
62
85
|
/** A no-wrap run can stay one Canvas string until rich layout is requested. */
|
|
63
86
|
function canUsePlainText(text, letterSpacing) {
|
|
@@ -809,40 +832,39 @@ function clearTextShadow(ctx) {
|
|
|
809
832
|
ctx.shadowOffsetX = 0;
|
|
810
833
|
ctx.shadowOffsetY = 0;
|
|
811
834
|
}
|
|
812
|
-
function getTextLineStartY(textBaseline, lineHeight) {
|
|
813
|
-
return textBaseline === "middle" ? lineHeight / 2 : textBaseline === "bottom" ? lineHeight : 0;
|
|
814
|
-
}
|
|
815
|
-
function getTextLineDrawY(textBaseline, fontSize, lineHeight) {
|
|
816
|
-
return getTextLineStartY(textBaseline, lineHeight) + (lineHeight - fontSize) / 2;
|
|
817
|
-
}
|
|
818
835
|
/**
|
|
819
836
|
* Measure one native Canvas text run with Fulate's resolved font and line-box
|
|
820
837
|
* semantics. This performs one `measureText()` call and writes the run's font,
|
|
821
838
|
* baseline, and letter spacing to the supplied context without saving or
|
|
822
|
-
* restoring it.
|
|
839
|
+
* restoring it. The font bounding box determines the centered draw position;
|
|
840
|
+
* the returned alphabetic baseline is the canonical coordinate for row
|
|
841
|
+
* alignment. Empty text measures a space for baseline data but reports zero
|
|
823
842
|
* width.
|
|
824
843
|
*/
|
|
825
844
|
function measureTextRun(ctx, text, style, fontSize = style.fontSize, fontWeight = style.fontWeight) {
|
|
826
845
|
const font = buildFontString(style, fontSize, fontWeight);
|
|
827
846
|
const lineHeight = fontSize * style.lineHeight;
|
|
828
|
-
const drawY = getTextLineDrawY(style.textBaseline, fontSize, lineHeight);
|
|
829
847
|
setTextState(ctx, font, style.letterSpacing);
|
|
830
848
|
ctx.textBaseline = style.textBaseline;
|
|
831
|
-
const
|
|
849
|
+
const measured = ctx.measureText(text || " ");
|
|
850
|
+
const lineMetrics = resolveTextLinePlacement(measured, lineHeight);
|
|
832
851
|
return {
|
|
833
852
|
font,
|
|
834
|
-
width: text ?
|
|
853
|
+
width: text ? measured.width : 0,
|
|
835
854
|
lineHeight,
|
|
836
|
-
drawY,
|
|
837
|
-
alphabeticBaseline:
|
|
855
|
+
drawY: lineMetrics.drawY,
|
|
856
|
+
alphabeticBaseline: lineMetrics.alphabeticBaseline,
|
|
857
|
+
textBaseline: style.textBaseline,
|
|
858
|
+
letterSpacing: style.letterSpacing
|
|
838
859
|
};
|
|
839
860
|
}
|
|
840
|
-
/** Measure the line box
|
|
841
|
-
function measureTextLineBoxMetrics(
|
|
861
|
+
/** Measure the line box shared by Text, Span, and composite text owners. */
|
|
862
|
+
function measureTextLineBoxMetrics(style) {
|
|
842
863
|
const lineHeight = style.fontSize * style.lineHeight;
|
|
864
|
+
const { alphabeticBaseline, height } = measureTextLinePlacement(buildFontString(style), style.textBaseline, lineHeight);
|
|
843
865
|
return {
|
|
844
|
-
alphabeticBaseline
|
|
845
|
-
height
|
|
866
|
+
alphabeticBaseline,
|
|
867
|
+
height
|
|
846
868
|
};
|
|
847
869
|
}
|
|
848
870
|
/**
|
|
@@ -856,7 +878,7 @@ function paintTextBlock(ctx, layout, style, width, height) {
|
|
|
856
878
|
if (layout.lines.length === 0) return;
|
|
857
879
|
const font = buildFontString(style);
|
|
858
880
|
const lineHeight = style.fontSize * style.lineHeight;
|
|
859
|
-
const
|
|
881
|
+
const lineMetrics = measureTextLinePlacement(font, style.textBaseline, lineHeight);
|
|
860
882
|
const blockOffset = layout.getBlockVerticalOffset(height, style.verticalAlign);
|
|
861
883
|
setTextState(ctx, font, style.letterSpacing);
|
|
862
884
|
ctx.textAlign = "left";
|
|
@@ -871,7 +893,7 @@ function paintTextBlock(ctx, layout, style, width, height) {
|
|
|
871
893
|
for (let index = 0; index < layout.lines.length; index++) {
|
|
872
894
|
const line = layout.lines[index];
|
|
873
895
|
const x = isLeftAligned ? 0 : layout.getLineX(index, width, style.textAlign);
|
|
874
|
-
const y = drawY + index * lineHeight + blockOffset;
|
|
896
|
+
const y = lineMetrics.drawY + index * lineHeight + blockOffset;
|
|
875
897
|
if (hasTextShadow) applyTextShadow(ctx, style.textShadow);
|
|
876
898
|
if (hasStroke) {
|
|
877
899
|
ctx.strokeStyle = strokeColor;
|
|
@@ -898,7 +920,6 @@ function paintTextContent(self, ctx, offsetY = 0) {
|
|
|
898
920
|
const w = self.width || 0;
|
|
899
921
|
const h = self.height || 0;
|
|
900
922
|
const lineHeightPx = style.fontSize * style.lineHeight;
|
|
901
|
-
const drawY = getTextLineDrawY(style.textBaseline, style.fontSize, lineHeightPx);
|
|
902
923
|
const ls = style.letterSpacing;
|
|
903
924
|
const layout = self.textLayout;
|
|
904
925
|
const blockOffset = layout.getBlockVerticalOffset(h, style.verticalAlign);
|
|
@@ -944,7 +965,8 @@ function paintTextContent(self, ctx, offsetY = 0) {
|
|
|
944
965
|
const phWidth = measureTextWidth(self.placeholder, font, ls);
|
|
945
966
|
const phX = style.textAlign === "center" ? (w - phWidth) / 2 : style.textAlign === "right" ? w - phWidth : 0;
|
|
946
967
|
const phBlockOffset = layout.getBlockVerticalOffset(h, style.verticalAlign, lineHeightPx);
|
|
947
|
-
|
|
968
|
+
const lineMetrics = measureTextLinePlacement(font, style.textBaseline, lineHeightPx);
|
|
969
|
+
drawTextLine(ctx, self.placeholder, phX, lineMetrics.drawY + phBlockOffset, font, ls, true, false);
|
|
948
970
|
ctx.fillStyle = fillStyle;
|
|
949
971
|
}
|
|
950
972
|
if (self.isEditing && self.caretVisible && self.textarea) {
|
|
@@ -1567,7 +1589,7 @@ function resolveConnectionPortChanges(root, changes) {
|
|
|
1567
1589
|
let sources;
|
|
1568
1590
|
let index;
|
|
1569
1591
|
for (const { owner, patch } of changes) {
|
|
1570
|
-
if (owner.activeRoot !== root || !
|
|
1592
|
+
if (owner.activeRoot !== root || !has(patch, "ports")) continue;
|
|
1571
1593
|
const next = resolveConfiguredPorts(patch.ports);
|
|
1572
1594
|
if (Object.is(owner.ports, next)) continue;
|
|
1573
1595
|
const nextIds = new Set(next === false ? [] : next === true ? owner.resolveConnectionPorts(true).map(({ ref }) => ref.portId) : next.map(({ id }) => id));
|
|
@@ -1590,7 +1612,7 @@ function resolveConnectionPortChanges(root, changes) {
|
|
|
1590
1612
|
*/
|
|
1591
1613
|
function commitConnectionPortPatch(owner, patch) {
|
|
1592
1614
|
const root = owner.activeRoot;
|
|
1593
|
-
if (!root || !
|
|
1615
|
+
if (!root || !has(patch, "ports")) return false;
|
|
1594
1616
|
if (Object.is(owner.ports, resolveConfiguredPorts(patch.ports))) return false;
|
|
1595
1617
|
root.applyChangeBatch(resolveConnectionPortChanges(root, [{
|
|
1596
1618
|
owner,
|
|
@@ -2465,7 +2487,7 @@ var Polygon = class extends EditorShape {
|
|
|
2465
2487
|
super(prepared, new PolygonProperties(prepared), PolygonTransform);
|
|
2466
2488
|
}
|
|
2467
2489
|
resolveOptions(options) {
|
|
2468
|
-
if (!
|
|
2490
|
+
if (!has(options, "contour") || options.contour === void 0 || options.contour === this.contour) return options;
|
|
2469
2491
|
return {
|
|
2470
2492
|
...options,
|
|
2471
2493
|
contour: resolveContour(options.contour)
|
|
@@ -2774,7 +2796,7 @@ var VectorPath = class extends EditorShape {
|
|
|
2774
2796
|
super(prepared, new VectorPathProperties(prepared), VectorPathTransform);
|
|
2775
2797
|
}
|
|
2776
2798
|
resolveOptions(options) {
|
|
2777
|
-
if (!
|
|
2799
|
+
if (!has(options, "path") || options.path === void 0 || options.path === this.path) return options;
|
|
2778
2800
|
return {
|
|
2779
2801
|
...options,
|
|
2780
2802
|
path: resolveVectorPath(options.path)
|
|
@@ -3140,13 +3162,12 @@ var Text = class extends Rectangle {
|
|
|
3140
3162
|
return this.textBlock.layout;
|
|
3141
3163
|
}
|
|
3142
3164
|
/** @internal Shared by TextRow layout and the final Text painter. */
|
|
3143
|
-
getTextLineBoxMetrics(
|
|
3165
|
+
getTextLineBoxMetrics() {
|
|
3144
3166
|
const style = this.getResolvedTextStyle();
|
|
3145
3167
|
const cached = this.textLineBoxMetricsCache;
|
|
3146
|
-
if (cached?.
|
|
3147
|
-
const metrics = measureTextLineBoxMetrics(
|
|
3168
|
+
if (cached?.style === style) return cached.metrics;
|
|
3169
|
+
const metrics = measureTextLineBoxMetrics(style);
|
|
3148
3170
|
this.textLineBoxMetricsCache = {
|
|
3149
|
-
text,
|
|
3150
3171
|
style,
|
|
3151
3172
|
metrics
|
|
3152
3173
|
};
|
|
@@ -3155,7 +3176,7 @@ var Text = class extends Rectangle {
|
|
|
3155
3176
|
/** @internal Alphabetic baseline relative to this Text box. */
|
|
3156
3177
|
getTextBlockFirstAlphabeticBaseline() {
|
|
3157
3178
|
const style = this.getResolvedTextStyle();
|
|
3158
|
-
return this.textLayout.getBlockVerticalOffset(this.height ?? 0, style.verticalAlign) + this.getTextLineBoxMetrics(
|
|
3179
|
+
return this.textLayout.getBlockVerticalOffset(this.height ?? 0, style.verticalAlign) + this.getTextLineBoxMetrics().alphabeticBaseline;
|
|
3159
3180
|
}
|
|
3160
3181
|
/** @internal The editor frame follows the Text box, not visible overflow. */
|
|
3161
3182
|
getSelectionFrameBounds() {
|
|
@@ -4557,7 +4578,7 @@ function getTransformRelationPatch(line) {
|
|
|
4557
4578
|
};
|
|
4558
4579
|
}
|
|
4559
4580
|
function applyRelationPatch(options) {
|
|
4560
|
-
if (!
|
|
4581
|
+
if (!has(options, "relations")) return options;
|
|
4561
4582
|
const relations = options.relations;
|
|
4562
4583
|
const resolved = { ...options };
|
|
4563
4584
|
delete resolved.relations;
|
|
@@ -4566,13 +4587,13 @@ function applyRelationPatch(options) {
|
|
|
4566
4587
|
resolved.tailRelation = void 0;
|
|
4567
4588
|
return resolved;
|
|
4568
4589
|
}
|
|
4569
|
-
if (
|
|
4570
|
-
if (
|
|
4590
|
+
if (has(relations, "head")) resolved.headRelation = relations.head;
|
|
4591
|
+
if (has(relations, "tail")) resolved.tailRelation = relations.tail;
|
|
4571
4592
|
return resolved;
|
|
4572
4593
|
}
|
|
4573
4594
|
function resolveLinePatch(options) {
|
|
4574
4595
|
let resolved = options;
|
|
4575
|
-
if (
|
|
4596
|
+
if (has(options, "path") && options.path !== void 0) {
|
|
4576
4597
|
const path = options.path;
|
|
4577
4598
|
if (Array.isArray(path)) resolved = {
|
|
4578
4599
|
...options,
|
|
@@ -4617,7 +4638,7 @@ var LineProperties = class extends ElementProperties {
|
|
|
4617
4638
|
this.headRelation = void 0;
|
|
4618
4639
|
this.tailRelation = void 0;
|
|
4619
4640
|
} else if (this.path.anchors.length === 1) {
|
|
4620
|
-
this.headRelation =
|
|
4641
|
+
this.headRelation = has(options, "headRelation") ? options.headRelation : options.tailRelation;
|
|
4621
4642
|
this.tailRelation = void 0;
|
|
4622
4643
|
} else {
|
|
4623
4644
|
this.headRelation = options.headRelation;
|
|
@@ -4916,7 +4937,7 @@ var BaseLine = class extends Element {
|
|
|
4916
4937
|
}
|
|
4917
4938
|
resolveOptions(options) {
|
|
4918
4939
|
const resolved = resolveLinePatch(options);
|
|
4919
|
-
if (!
|
|
4940
|
+
if (!has(options, "path") && !has(options, "relations")) return resolved;
|
|
4920
4941
|
const path = resolved.path ?? this.path;
|
|
4921
4942
|
if (path.anchors.length > 1) return resolved;
|
|
4922
4943
|
if (path.anchors.length === 0) return {
|
|
@@ -4924,8 +4945,8 @@ var BaseLine = class extends Element {
|
|
|
4924
4945
|
headRelation: void 0,
|
|
4925
4946
|
tailRelation: void 0
|
|
4926
4947
|
};
|
|
4927
|
-
const hasHead =
|
|
4928
|
-
const hasTail =
|
|
4948
|
+
const hasHead = has(resolved, "headRelation");
|
|
4949
|
+
const hasTail = has(resolved, "tailRelation");
|
|
4929
4950
|
const head = hasHead ? resolved.headRelation : this.headRelation;
|
|
4930
4951
|
const tail = hasTail ? resolved.tailRelation : this.tailRelation;
|
|
4931
4952
|
const anchorId = path.anchors[0].anchorId;
|
|
@@ -4985,7 +5006,7 @@ var BaseLine = class extends Element {
|
|
|
4985
5006
|
const vertices = this.getEffectiveLineVerticesView();
|
|
4986
5007
|
let path = this.path;
|
|
4987
5008
|
if (detachHead) path = replaceAuthoredAnchorPoint(path, head.anchorId, localPositions?.get(head.anchorId) ?? vertices[0].point);
|
|
4988
|
-
if (detachTail && tail.anchorId !== head?.anchorId) path = replaceAuthoredAnchorPoint(path, tail.anchorId, localPositions?.get(tail.anchorId) ?? vertices
|
|
5009
|
+
if (detachTail && tail.anchorId !== head?.anchorId) path = replaceAuthoredAnchorPoint(path, tail.anchorId, localPositions?.get(tail.anchorId) ?? last(vertices).point);
|
|
4989
5010
|
return {
|
|
4990
5011
|
owner: this,
|
|
4991
5012
|
patch: {
|
|
@@ -5065,7 +5086,7 @@ var BaseLine = class extends Element {
|
|
|
5065
5086
|
}
|
|
5066
5087
|
setLocalPoint(anchorId, point, endpointUpdate) {
|
|
5067
5088
|
const patch = { path: replaceAuthoredAnchorPoint(this.path, anchorId, point) };
|
|
5068
|
-
if (endpointUpdate &&
|
|
5089
|
+
if (endpointUpdate && has(endpointUpdate, "target")) {
|
|
5069
5090
|
const head = this.headAnchor?.anchorId === anchorId;
|
|
5070
5091
|
const tail = this.tailAnchor?.anchorId === anchorId;
|
|
5071
5092
|
const relation = endpointUpdate.target && {
|
|
@@ -5112,8 +5133,8 @@ var BaseLine = class extends Element {
|
|
|
5112
5133
|
const root = this.activeRoot;
|
|
5113
5134
|
const previousPath = changes.get("path");
|
|
5114
5135
|
const currentHeadId = current.path.anchors[0]?.anchorId;
|
|
5115
|
-
const currentTailId = current.path.anchors
|
|
5116
|
-
const endpointIdsChanged = previousPath !== void 0 && (previousPath.anchors[0]?.anchorId !== currentHeadId || previousPath.anchors
|
|
5136
|
+
const currentTailId = last(current.path.anchors)?.anchorId;
|
|
5137
|
+
const endpointIdsChanged = previousPath !== void 0 && (previousPath.anchors[0]?.anchorId !== currentHeadId || last(previousPath.anchors)?.anchorId !== currentTailId);
|
|
5117
5138
|
if (!root || !endpointIdsChanged && !changes.has("headRelation") && !changes.has("tailRelation")) return;
|
|
5118
5139
|
this.syncEndpointRelations(root);
|
|
5119
5140
|
}
|
|
@@ -5428,7 +5449,7 @@ function createChildBranchRecords(parentSubpathId, sourceAnchorId, points, style
|
|
|
5428
5449
|
};
|
|
5429
5450
|
}
|
|
5430
5451
|
function addLineTreeBranchAtTail(topology, parentSubpathId, points, style = DEFAULT_LINE_TREE_BRANCH_STYLE, tailRelation) {
|
|
5431
|
-
const sourceAnchorId = getLineTreeBranch(topology, parentSubpathId).vertices
|
|
5452
|
+
const sourceAnchorId = last(getLineTreeBranch(topology, parentSubpathId).vertices).anchorId;
|
|
5432
5453
|
const created = createChildBranchRecords(parentSubpathId, sourceAnchorId, points, style, tailRelation);
|
|
5433
5454
|
return {
|
|
5434
5455
|
topology: {
|
|
@@ -5714,7 +5735,7 @@ var LineTreeTransform = class extends ElementTransform {
|
|
|
5714
5735
|
return this.worldSnapPoints;
|
|
5715
5736
|
}
|
|
5716
5737
|
getBranchTailSource(subpathId) {
|
|
5717
|
-
const anchorId = this.getBranch(subpathId).vertices
|
|
5738
|
+
const anchorId = last(this.getBranch(subpathId).vertices).anchorId;
|
|
5718
5739
|
return {
|
|
5719
5740
|
subpathId,
|
|
5720
5741
|
anchorId,
|
|
@@ -5757,7 +5778,7 @@ var LineTreeTransform = class extends ElementTransform {
|
|
|
5757
5778
|
//#endregion
|
|
5758
5779
|
//#region packages/ui/src/line-tree/index.ts
|
|
5759
5780
|
function prepareTopologyOption(options) {
|
|
5760
|
-
return
|
|
5781
|
+
return has(options, "topology") && options.topology === void 0 ? {
|
|
5761
5782
|
...options,
|
|
5762
5783
|
topology: EMPTY_LINE_TREE_TOPOLOGY
|
|
5763
5784
|
} : options;
|
package/dist/text/index.d.ts
CHANGED
|
@@ -118,7 +118,7 @@ export declare class Text<P extends TextProperties = TextProperties, O extends T
|
|
|
118
118
|
getFontString(style?: Required<TextStyleConfig>): string;
|
|
119
119
|
get textLayout(): import("./layout").TextLayoutSnapshot;
|
|
120
120
|
/** @internal Shared by TextRow layout and the final Text painter. */
|
|
121
|
-
getTextLineBoxMetrics(
|
|
121
|
+
getTextLineBoxMetrics(): TextLineBoxMetrics;
|
|
122
122
|
/** @internal Alphabetic baseline relative to this Text box. */
|
|
123
123
|
getTextBlockFirstAlphabeticBaseline(): number;
|
|
124
124
|
/** @internal The editor frame follows the Text box, not visible overflow. */
|
package/dist/text/layout.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { type LayoutLine } from "@chenglou/pretext";
|
|
2
|
+
/**
|
|
3
|
+
* The vertical placement of a Canvas text run is a font property, not a
|
|
4
|
+
* `fontSize` approximation. Keep one resolved line box per native font state
|
|
5
|
+
* so painting can reuse the measurement without measuring on every frame.
|
|
6
|
+
*/
|
|
7
|
+
export interface TextLineBoxMetrics {
|
|
8
|
+
/** Alphabetic baseline relative to the line-box top. */
|
|
9
|
+
readonly alphabeticBaseline: number;
|
|
10
|
+
/** Line-box height in logical pixels. */
|
|
11
|
+
readonly height: number;
|
|
12
|
+
}
|
|
13
|
+
export interface TextLinePlacementMetrics extends TextLineBoxMetrics {
|
|
14
|
+
/** Canvas draw Y relative to the line-box top. */
|
|
15
|
+
readonly drawY: number;
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
18
|
* Subscribe a non-Text owner to browser font measurement completion.
|
|
4
19
|
*
|
|
@@ -29,10 +44,23 @@ export interface TextLayoutMetrics {
|
|
|
29
44
|
readonly truncated: boolean;
|
|
30
45
|
}
|
|
31
46
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
47
|
+
* Resolve one line box from a native Canvas measurement. The font bounding
|
|
48
|
+
* box is stable for the font state, while `alphabeticBaseline` translates the
|
|
49
|
+
* selected Canvas baseline into the same canonical alphabetic coordinate used
|
|
50
|
+
* by TextRow and composite owners.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveTextLinePlacement(metrics: Pick<TextMetrics, "fontBoundingBoxAscent" | "fontBoundingBoxDescent" | "alphabeticBaseline">, lineHeight: number): {
|
|
53
|
+
drawY: number;
|
|
54
|
+
alphabeticBaseline: number;
|
|
55
|
+
height: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Measure the font line box once and reuse it for Text painting, placeholders,
|
|
59
|
+
* and composite TextBlock owners. The representative space supplies the
|
|
60
|
+
* font-level Canvas metrics without making placement depend on the content of
|
|
61
|
+
* an individual line.
|
|
34
62
|
*/
|
|
35
|
-
export declare function
|
|
63
|
+
export declare function measureTextLinePlacement(font: string, textBaseline: CanvasTextBaseline, lineHeight: number): TextLinePlacementMetrics;
|
|
36
64
|
/**
|
|
37
65
|
* Measure one text run through the same engine used by Text and Span.
|
|
38
66
|
* `white-space: pre-wrap` keeps the caller's spaces and hard breaks intact.
|
package/dist/text/paint.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type TextLayoutSnapshot } from "./layout";
|
|
1
|
+
import { type TextLayoutSnapshot, type TextLineBoxMetrics } from "./layout";
|
|
2
2
|
import type { Text } from "./index";
|
|
3
3
|
import { type ResolvedTextStyle } from "./style";
|
|
4
4
|
import type { ShadowOption, TextStyleConfig } from "@fulate/core";
|
|
5
|
+
export type { TextLineBoxMetrics } from "./layout";
|
|
5
6
|
export declare function resolveColorFill(ctx: CanvasRenderingContext2D, color: ResolvedTextStyle["color"], w: number, h: number): string | CanvasGradient;
|
|
6
7
|
export declare function drawTextLine(ctx: CanvasRenderingContext2D, line: string, x: number, y: number, font: string, letterSpacing: number, doFill: boolean, doStroke: boolean): void;
|
|
7
8
|
export interface TextDecorationOptions {
|
|
@@ -29,21 +30,23 @@ export interface TextRunMetrics {
|
|
|
29
30
|
readonly drawY: number;
|
|
30
31
|
/** Alphabetic baseline relative to the run's line-box top. */
|
|
31
32
|
readonly alphabeticBaseline: number;
|
|
33
|
+
/** Canvas baseline used when the run was measured. */
|
|
34
|
+
readonly textBaseline: CanvasTextBaseline;
|
|
35
|
+
/** Letter spacing used when the run was measured. */
|
|
36
|
+
readonly letterSpacing: number;
|
|
32
37
|
}
|
|
33
38
|
/**
|
|
34
39
|
* Measure one native Canvas text run with Fulate's resolved font and line-box
|
|
35
40
|
* semantics. This performs one `measureText()` call and writes the run's font,
|
|
36
41
|
* baseline, and letter spacing to the supplied context without saving or
|
|
37
|
-
* restoring it.
|
|
42
|
+
* restoring it. The font bounding box determines the centered draw position;
|
|
43
|
+
* the returned alphabetic baseline is the canonical coordinate for row
|
|
44
|
+
* alignment. Empty text measures a space for baseline data but reports zero
|
|
38
45
|
* width.
|
|
39
46
|
*/
|
|
40
47
|
export declare function measureTextRun(ctx: CanvasRenderingContext2D, text: string, style: Readonly<Required<TextStyleConfig>>, fontSize?: number, fontWeight?: string | number): TextRunMetrics;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
readonly height: number;
|
|
44
|
-
}
|
|
45
|
-
/** Measure the line box produced by the existing Text painter. */
|
|
46
|
-
export declare function measureTextLineBoxMetrics(text: string, style: ResolvedTextStyle): TextLineBoxMetrics;
|
|
48
|
+
/** Measure the line box shared by Text, Span, and composite text owners. */
|
|
49
|
+
export declare function measureTextLineBoxMetrics(style: ResolvedTextStyle): TextLineBoxMetrics;
|
|
47
50
|
/**
|
|
48
51
|
* Paint a previously synchronized text block.
|
|
49
52
|
*
|