@mocanvas/mocanvas 1.0.0 → 4.0.0
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/ARCHITECTURE.md +421 -0
- package/BENCHMARK.md +519 -0
- package/CLEAN_ROOM.md +50 -0
- package/COMPAT.md +282 -0
- package/CUSTOM_SHAPES.md +880 -0
- package/LICENSE +110 -16
- package/MIGRATION.md +807 -0
- package/README.md +122 -6
- package/UI.md +256 -0
- package/dist/chunk-DMCI6V5Y.js +3370 -0
- package/dist/chunk-DMCI6V5Y.js.map +1 -0
- package/dist/chunk-GYF6AY4N.js +54 -0
- package/dist/chunk-GYF6AY4N.js.map +1 -0
- package/dist/chunk-OCYJAMXT.js +1117 -0
- package/dist/chunk-OCYJAMXT.js.map +1 -0
- package/dist/chunk-OMUOD53T.js +370 -0
- package/dist/chunk-OMUOD53T.js.map +1 -0
- package/dist/chunk-QDUQZXE4.js +1340 -0
- package/dist/chunk-QDUQZXE4.js.map +1 -0
- package/dist/chunk-TWCVRVK2.js +296 -0
- package/dist/chunk-TWCVRVK2.js.map +1 -0
- package/dist/export-VQGOVONC.js +5 -0
- package/dist/export-VQGOVONC.js.map +1 -0
- package/dist/file-GEHTVI2G.js +3 -0
- package/dist/file-GEHTVI2G.js.map +1 -0
- package/dist/index.d.ts +9593 -1659
- package/dist/index.js +9005 -6114
- package/dist/index.js.map +1 -1
- package/dist/panel-shortcuts-R2PDTDUN.js +4 -0
- package/dist/panel-shortcuts-R2PDTDUN.js.map +1 -0
- package/dist/{ui-62NV5N7K.css → ui-4C5V5GYT.css} +385 -0
- package/dist/urlContent-GC44EU52.js +4 -0
- package/dist/urlContent-GC44EU52.js.map +1 -0
- package/package.json +17 -16
|
@@ -0,0 +1,3370 @@
|
|
|
1
|
+
import { tipTapDefaultExtensions, richTextToHtml, isRichText, richTextToText, getRichTextEditorFactory, applyPlainTextToRichText, propsOf, readNumber, readText, readRichText, readStyle, readEnum, readPoint, arrowShapeMigrations, arrowShapeProps, toRichText, svgPath, frameShapeProps, frameShapeMigrations, readString, rectPath, readBoolean, geoShapeProps, noteShapeProps, noteShapeMigrations, richTextToBlocks, textShapeProps, textShapeMigrations, videoShapeProps, videoShapeMigrations, safeHref, getBookmarkCard, getBookmarkLayout, BOOKMARK_STROKE_WIDTH, BOOKMARK_STROKE, BOOKMARK_FILL, BOOKMARK_RADIUS, BOOKMARK_BANNER_FILL, BOOKMARK_TITLE_FONT_SIZE, BOOKMARK_TITLE_COLOR, BOOKMARK_META_COLOR, BOOKMARK_TEXT_FONT_SIZE, BOOKMARK_TEXT_COLOR, BOOKMARK_META_FONT_SIZE, getEmbedDefinition, EMBED_PLACEHOLDER_STROKE, EMBED_PLACEHOLDER_FILL, EMBED_RADIUS, EMBED_PLACEHOLDER_PADDING, EMBED_PLACEHOLDER_FONT_SIZE, EMBED_PLACEHOLDER_TEXT } from './chunk-OCYJAMXT.js';
|
|
2
|
+
import { getGeoGeometry, arcToCubicSegments, pathWordsToSvgD } from './chunk-OMUOD53T.js';
|
|
3
|
+
import { DEFAULT_THEME, GEO_SHAPE_KINDS, createBuiltInShapePropsMigrationIds, createShapePropsMigrationSequence, ARROW_SHAPE_KINDS, Vec, Polyline2d, CubicSpline2d, Polygon2d, Box, Group2d, STROKE_SIZES, hexToRgba, getColorValue, DEFAULT_FILL_TOKENS, DefaultFontFaces, useEditor, DefaultFontStyle, ARROWHEAD_KINDS, DefaultSizeStyle, DefaultDashStyle, DefaultFillStyle, DefaultLabelColorStyle, DefaultColorStyle, getDefaultDisplayValues, ShapeUtil, FONT_SIZES, Rectangle2d, getDisplayValues, BaseFrameLikeShapeUtil, DefaultVerticalAlignStyle, DefaultHorizontalAlignStyle, GeoShapeGeoStyle, BaseBoxShapeUtil, GEO_KIND, LIGHT_THEME, PATH_OP } from '@mocanvas/editor';
|
|
4
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
5
|
+
import { useMemo, useRef, useState, useCallback, useEffect, useLayoutEffect } from 'react';
|
|
6
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var EPS = 1e-6;
|
|
9
|
+
function normalizeAngle(a) {
|
|
10
|
+
let r = a % (Math.PI * 2);
|
|
11
|
+
if (r > Math.PI) r -= Math.PI * 2;
|
|
12
|
+
if (r <= -Math.PI) r += Math.PI * 2;
|
|
13
|
+
return r;
|
|
14
|
+
}
|
|
15
|
+
function getArrowBody(start, end, bend) {
|
|
16
|
+
const s = Vec.From(start);
|
|
17
|
+
const e = Vec.From(end);
|
|
18
|
+
const chord = Vec.Sub(e, s);
|
|
19
|
+
const halfChord = Vec.Len(chord) / 2;
|
|
20
|
+
if (Math.abs(bend) < EPS || halfChord < EPS) return { kind: "straight", start: s, end: e };
|
|
21
|
+
const u = Vec.Uni(chord);
|
|
22
|
+
const n = Vec.Per(u);
|
|
23
|
+
const mid = Vec.Lrp(s, e, 0.5);
|
|
24
|
+
const b = Math.abs(bend);
|
|
25
|
+
const sign = Math.sign(bend);
|
|
26
|
+
const radius = (halfChord * halfChord + b * b) / (2 * b);
|
|
27
|
+
const center = Vec.Add(mid, Vec.Mul(n, bend - sign * radius));
|
|
28
|
+
const arcMid = Vec.Add(mid, Vec.Mul(n, bend));
|
|
29
|
+
const startAngle = Vec.Angle(center, s);
|
|
30
|
+
const endAngle = Vec.Angle(center, e);
|
|
31
|
+
let sweep = normalizeAngle(endAngle - startAngle);
|
|
32
|
+
const midAngle = startAngle + sweep / 2;
|
|
33
|
+
const candidate = new Vec(center.x + radius * Math.cos(midAngle), center.y + radius * Math.sin(midAngle));
|
|
34
|
+
if (Vec.Dist(candidate, arcMid) > 1e-3 * Math.max(1, radius)) {
|
|
35
|
+
sweep = sweep - Math.sign(sweep || 1) * Math.PI * 2;
|
|
36
|
+
}
|
|
37
|
+
return { kind: "arc", center, radius, startAngle, sweep };
|
|
38
|
+
}
|
|
39
|
+
function cumulativeLengths(points) {
|
|
40
|
+
const out = [0];
|
|
41
|
+
for (let i = 1; i < points.length; i++) out.push(out[i - 1] + Vec.Dist(points[i - 1], points[i]));
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function segmentAt(lengths, distance) {
|
|
45
|
+
for (let i = 1; i < lengths.length; i++) {
|
|
46
|
+
if (distance <= lengths[i] || i === lengths.length - 1) return i - 1;
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
function pointAtDistance(points, lengths, distance) {
|
|
51
|
+
if (points.length === 0) return new Vec();
|
|
52
|
+
if (points.length === 1) return points[0].clone();
|
|
53
|
+
const i = segmentAt(lengths, distance);
|
|
54
|
+
const segment = lengths[i + 1] - lengths[i];
|
|
55
|
+
const t = segment < EPS ? 0 : (distance - lengths[i]) / segment;
|
|
56
|
+
return Vec.Lrp(points[i], points[i + 1], Math.max(0, Math.min(1, t)));
|
|
57
|
+
}
|
|
58
|
+
function getBodyLength(body) {
|
|
59
|
+
if (body.kind === "straight") return Vec.Dist(body.start, body.end);
|
|
60
|
+
if (body.kind === "elbow") return cumulativeLengths(body.points).at(-1) ?? 0;
|
|
61
|
+
return Math.abs(body.sweep) * body.radius;
|
|
62
|
+
}
|
|
63
|
+
function getPointOnBody(body, t) {
|
|
64
|
+
if (body.kind === "straight") return Vec.Lrp(body.start, body.end, t);
|
|
65
|
+
if (body.kind === "elbow") {
|
|
66
|
+
const lengths = cumulativeLengths(body.points);
|
|
67
|
+
return pointAtDistance(body.points, lengths, (lengths.at(-1) ?? 0) * t);
|
|
68
|
+
}
|
|
69
|
+
const a = body.startAngle + body.sweep * t;
|
|
70
|
+
return new Vec(body.center.x + body.radius * Math.cos(a), body.center.y + body.radius * Math.sin(a));
|
|
71
|
+
}
|
|
72
|
+
function getTangentOnBody(body, t) {
|
|
73
|
+
if (body.kind === "straight") return Vec.Uni(Vec.Sub(body.end, body.start));
|
|
74
|
+
if (body.kind === "elbow") {
|
|
75
|
+
const points = body.points;
|
|
76
|
+
if (points.length < 2) return new Vec(1, 0);
|
|
77
|
+
const lengths = cumulativeLengths(points);
|
|
78
|
+
const i = segmentAt(lengths, (lengths.at(-1) ?? 0) * Math.max(0, Math.min(1, t)));
|
|
79
|
+
for (let step = 0; step < points.length; step++) {
|
|
80
|
+
for (const j of [i + step, i - step]) {
|
|
81
|
+
if (j < 0 || j >= points.length - 1) continue;
|
|
82
|
+
const d = Vec.Sub(points[j + 1], points[j]);
|
|
83
|
+
if (Vec.Len(d) > EPS) return Vec.Uni(d);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return new Vec(1, 0);
|
|
87
|
+
}
|
|
88
|
+
const a = body.startAngle + body.sweep * t;
|
|
89
|
+
const dir = Math.sign(body.sweep) || 1;
|
|
90
|
+
return new Vec(-Math.sin(a) * dir, Math.cos(a) * dir);
|
|
91
|
+
}
|
|
92
|
+
function shortenBody(body, startBy, endBy) {
|
|
93
|
+
const length = getBodyLength(body);
|
|
94
|
+
if (length < EPS) return body;
|
|
95
|
+
const total = startBy + endBy;
|
|
96
|
+
const budget = length * 0.9;
|
|
97
|
+
const scale = total > budget ? budget / total : 1;
|
|
98
|
+
const s = startBy * scale;
|
|
99
|
+
const e = endBy * scale;
|
|
100
|
+
if (body.kind === "straight") {
|
|
101
|
+
return { kind: "straight", start: getPointOnBody(body, s / length), end: getPointOnBody(body, 1 - e / length) };
|
|
102
|
+
}
|
|
103
|
+
if (body.kind === "elbow") {
|
|
104
|
+
const lengths = cumulativeLengths(body.points);
|
|
105
|
+
const from = s;
|
|
106
|
+
const to = length - e;
|
|
107
|
+
const points = [pointAtDistance(body.points, lengths, from)];
|
|
108
|
+
for (let i = 1; i < body.points.length - 1; i++) {
|
|
109
|
+
const at = lengths[i];
|
|
110
|
+
if (at > from + EPS && at < to - EPS) points.push(body.points[i].clone());
|
|
111
|
+
}
|
|
112
|
+
points.push(pointAtDistance(body.points, lengths, to));
|
|
113
|
+
return { kind: "elbow", points };
|
|
114
|
+
}
|
|
115
|
+
const dir = Math.sign(body.sweep) || 1;
|
|
116
|
+
const dStart = s / body.radius * dir;
|
|
117
|
+
const dEnd = e / body.radius * dir;
|
|
118
|
+
return {
|
|
119
|
+
kind: "arc",
|
|
120
|
+
center: body.center,
|
|
121
|
+
radius: body.radius,
|
|
122
|
+
startAngle: body.startAngle + dStart,
|
|
123
|
+
sweep: body.sweep - dStart - dEnd
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function bodyToGeometry(body) {
|
|
127
|
+
if (body.kind === "straight") return new Polyline2d({ points: [body.start, body.end] });
|
|
128
|
+
if (body.kind === "elbow") return new Polyline2d({ points: body.points });
|
|
129
|
+
const count = Math.max(2, Math.ceil(Math.abs(body.sweep) / (Math.PI / 2)));
|
|
130
|
+
return new CubicSpline2d({
|
|
131
|
+
segments: arcToCubicSegments(body.center, body.radius, body.startAngle, body.sweep, count),
|
|
132
|
+
isClosed: false,
|
|
133
|
+
isFilled: false
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function getArrowheadLength(strokeWidth, bodyLength) {
|
|
137
|
+
return Math.max(0, Math.min(Math.max(12, strokeWidth * 4), bodyLength / 2.5));
|
|
138
|
+
}
|
|
139
|
+
function getArrowheadInset(kind, length) {
|
|
140
|
+
switch (kind) {
|
|
141
|
+
case "triangle":
|
|
142
|
+
case "inverted":
|
|
143
|
+
case "diamond":
|
|
144
|
+
case "square":
|
|
145
|
+
case "dot":
|
|
146
|
+
return length;
|
|
147
|
+
case "arrow":
|
|
148
|
+
case "bar":
|
|
149
|
+
case "pipe":
|
|
150
|
+
case "none":
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
var CHEVRON_HALF_WIDTH = Math.tan(Math.PI / 6);
|
|
155
|
+
function getArrowheadGeometry(kind, tip, dir, length) {
|
|
156
|
+
if (kind === "none" || length <= 0) return null;
|
|
157
|
+
const t = Vec.From(tip);
|
|
158
|
+
const d = Vec.From(dir);
|
|
159
|
+
const p = Vec.Per(d);
|
|
160
|
+
const back = Vec.Sub(t, Vec.Mul(d, length));
|
|
161
|
+
const wing = Vec.Mul(p, length * CHEVRON_HALF_WIDTH);
|
|
162
|
+
switch (kind) {
|
|
163
|
+
case "arrow":
|
|
164
|
+
return new Polyline2d({ points: [Vec.Add(back, wing), t, Vec.Sub(back, wing)] });
|
|
165
|
+
case "triangle":
|
|
166
|
+
return new Polygon2d({ points: [t, Vec.Add(back, wing), Vec.Sub(back, wing)], isFilled: true });
|
|
167
|
+
case "inverted":
|
|
168
|
+
return new Polygon2d({ points: [back, Vec.Add(t, wing), Vec.Sub(t, wing)], isFilled: true });
|
|
169
|
+
case "square": {
|
|
170
|
+
const half = Vec.Mul(p, length / 2);
|
|
171
|
+
return new Polygon2d({
|
|
172
|
+
points: [Vec.Add(t, half), Vec.Sub(t, half), Vec.Sub(back, half), Vec.Add(back, half)],
|
|
173
|
+
isFilled: true
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
case "diamond": {
|
|
177
|
+
const mid = Vec.Lrp(t, back, 0.5);
|
|
178
|
+
const half = Vec.Mul(p, length / 2);
|
|
179
|
+
return new Polygon2d({ points: [t, Vec.Add(mid, half), back, Vec.Sub(mid, half)], isFilled: true });
|
|
180
|
+
}
|
|
181
|
+
case "dot": {
|
|
182
|
+
const center = Vec.Lrp(t, back, 0.5);
|
|
183
|
+
return new CubicSpline2d({
|
|
184
|
+
segments: arcToCubicSegments(center, length / 2, 0, Math.PI * 2, 4),
|
|
185
|
+
isClosed: true,
|
|
186
|
+
isFilled: true
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
case "bar": {
|
|
190
|
+
const half = Vec.Mul(p, length / 2);
|
|
191
|
+
return new Polyline2d({ points: [Vec.Add(t, half), Vec.Sub(t, half)] });
|
|
192
|
+
}
|
|
193
|
+
case "pipe": {
|
|
194
|
+
const half = Vec.Mul(p, length * 0.35);
|
|
195
|
+
return new Polyline2d({ points: [Vec.Add(t, half), Vec.Sub(t, half)] });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function labelGapOnBody(body, box) {
|
|
200
|
+
const SAMPLES = 192;
|
|
201
|
+
const minX = box.x;
|
|
202
|
+
const maxX = box.x + box.w;
|
|
203
|
+
const minY = box.y;
|
|
204
|
+
const maxY = box.y + box.h;
|
|
205
|
+
let from = -1;
|
|
206
|
+
let to = -1;
|
|
207
|
+
for (let i = 0; i <= SAMPLES; i++) {
|
|
208
|
+
const t = i / SAMPLES;
|
|
209
|
+
const p = getPointOnBody(body, t);
|
|
210
|
+
if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) continue;
|
|
211
|
+
if (from < 0) from = t;
|
|
212
|
+
to = t;
|
|
213
|
+
}
|
|
214
|
+
if (from < 0) return null;
|
|
215
|
+
const half = 0.5 / SAMPLES;
|
|
216
|
+
const gap = { from: Math.max(0, from - half), to: Math.min(1, to + half) };
|
|
217
|
+
if (gap.to - gap.from > 0.9) return null;
|
|
218
|
+
return gap;
|
|
219
|
+
}
|
|
220
|
+
function getBendFromPoint(start, end, point) {
|
|
221
|
+
const chord = Vec.Sub(end, start);
|
|
222
|
+
if (Vec.Len(chord) < EPS) return 0;
|
|
223
|
+
const n = Vec.Per(Vec.Uni(chord));
|
|
224
|
+
const mid = Vec.Lrp(start, end, 0.5);
|
|
225
|
+
const bend = Vec.Dot(Vec.Sub(point, mid), n);
|
|
226
|
+
return Math.abs(bend) < 1 ? 0 : bend;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/bindings/arrow-terminals.ts
|
|
230
|
+
function applyTransform(m, p) {
|
|
231
|
+
return new Vec(m.a * p.x + m.c * p.y + m.e, m.b * p.x + m.d * p.y + m.f);
|
|
232
|
+
}
|
|
233
|
+
function getArrowBindings(editor, arrow) {
|
|
234
|
+
const out = {};
|
|
235
|
+
for (const binding of editor.getBindingsFromShape(arrow, "arrow")) {
|
|
236
|
+
out[binding.props.terminal] = binding;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
function getAnchorInShapeSpace(editor, shape, normalizedAnchor) {
|
|
241
|
+
const b = editor.getShapeGeometry(shape).bounds;
|
|
242
|
+
return new Vec(b.x + b.w * normalizedAnchor.x, b.y + b.h * normalizedAnchor.y);
|
|
243
|
+
}
|
|
244
|
+
function getArrowBindingTargetAtPoint(editor, arrow, pagePoint) {
|
|
245
|
+
const margin = editor.options.hitTestMargin / editor.getZoomLevel();
|
|
246
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
247
|
+
for (let p = editor.getShapeParent(arrow); p; p = editor.getShapeParent(p)) ancestors.add(p.id);
|
|
248
|
+
const shapes = editor.getCurrentPageShapesSorted();
|
|
249
|
+
for (let i = shapes.length - 1; i >= 0; i--) {
|
|
250
|
+
const shape = shapes[i];
|
|
251
|
+
if (shape.id === arrow.id || shape.type === "arrow" || shape.isLocked || ancestors.has(shape.id)) continue;
|
|
252
|
+
if (!editor.getShapeUtil(shape).canBind({ fromShape: arrow, toShape: shape, bindingType: "arrow" })) continue;
|
|
253
|
+
const bounds = editor.getShapePageBounds(shape);
|
|
254
|
+
if (!bounds || !Box.ContainsPoint(bounds, pagePoint, margin)) continue;
|
|
255
|
+
const local = editor.getPointInShapeSpace(shape, pagePoint);
|
|
256
|
+
if (editor.getShapeGeometry(shape).hitTestPoint(local, margin, true)) return shape;
|
|
257
|
+
}
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
function getNormalizedAnchor(editor, shape, pagePoint) {
|
|
261
|
+
const local = editor.getPointInShapeSpace(shape, pagePoint);
|
|
262
|
+
const b = editor.getShapeGeometry(shape).bounds;
|
|
263
|
+
const clamp = (v) => Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0.5;
|
|
264
|
+
return new Vec(clamp(b.w === 0 ? 0.5 : (local.x - b.x) / b.w), clamp(b.h === 0 ? 0.5 : (local.y - b.y) / b.h));
|
|
265
|
+
}
|
|
266
|
+
function getOutlineSegments(geometry) {
|
|
267
|
+
if (geometry.isLabel) return [];
|
|
268
|
+
if (geometry instanceof Group2d) return geometry.children.flatMap(getOutlineSegments);
|
|
269
|
+
const v = geometry.vertices;
|
|
270
|
+
const out = [];
|
|
271
|
+
if (v.length < 2) return out;
|
|
272
|
+
const n = geometry.isClosed ? v.length : v.length - 1;
|
|
273
|
+
for (let i = 0; i < n; i++) out.push([v[i], v[(i + 1) % v.length]]);
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
function intersectSegments(a1, a2, b1, b2) {
|
|
277
|
+
const rx = a2.x - a1.x;
|
|
278
|
+
const ry = a2.y - a1.y;
|
|
279
|
+
const sx = b2.x - b1.x;
|
|
280
|
+
const sy = b2.y - b1.y;
|
|
281
|
+
const denom = rx * sy - ry * sx;
|
|
282
|
+
if (Math.abs(denom) < 1e-12) return null;
|
|
283
|
+
const qx = b1.x - a1.x;
|
|
284
|
+
const qy = b1.y - a1.y;
|
|
285
|
+
const t = (qx * sy - qy * sx) / denom;
|
|
286
|
+
const u = (qx * ry - qy * rx) / denom;
|
|
287
|
+
if (t < 0 || t > 1 || u < 0 || u > 1) return null;
|
|
288
|
+
return new Vec(a1.x + rx * t, a1.y + ry * t);
|
|
289
|
+
}
|
|
290
|
+
function firstCrossing(path, segments) {
|
|
291
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
292
|
+
const p0 = path[i];
|
|
293
|
+
const p1 = path[i + 1];
|
|
294
|
+
let best = null;
|
|
295
|
+
let bestD = Infinity;
|
|
296
|
+
for (const [s0, s1] of segments) {
|
|
297
|
+
const hit = intersectSegments(p0, p1, s0, s1);
|
|
298
|
+
if (!hit) continue;
|
|
299
|
+
const d = Vec.Dist2(p0, hit);
|
|
300
|
+
if (d < bestD) {
|
|
301
|
+
bestD = d;
|
|
302
|
+
best = hit;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (best) return best;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
function sampleBody(from, to, bend, reverse) {
|
|
310
|
+
const body = reverse ? getArrowBody(to, from, bend) : getArrowBody(from, to, bend);
|
|
311
|
+
if (body.kind !== "arc") return [from, to];
|
|
312
|
+
const count = Math.max(8, Math.min(128, Math.ceil(Math.abs(body.sweep) * body.radius / 4)));
|
|
313
|
+
const pts = [];
|
|
314
|
+
for (let i = 0; i <= count; i++) {
|
|
315
|
+
const t = reverse ? 1 - i / count : i / count;
|
|
316
|
+
pts.push(getPointOnBody(body, t));
|
|
317
|
+
}
|
|
318
|
+
return pts;
|
|
319
|
+
}
|
|
320
|
+
function getBoundElbowAxes(editor, arrow, terminals) {
|
|
321
|
+
const bindings = getArrowBindings(editor, arrow);
|
|
322
|
+
const out = {};
|
|
323
|
+
for (const terminal of ["start", "end"]) {
|
|
324
|
+
const binding = bindings[terminal];
|
|
325
|
+
const shape = binding ? editor.getShape(binding.toId) : void 0;
|
|
326
|
+
if (!shape) continue;
|
|
327
|
+
const m = editor.getShapePageTransform(shape);
|
|
328
|
+
const corners = editor.getShapeGeometry(shape).bounds.corners.map((p2) => editor.getPointInShapeSpace(arrow, applyTransform(m, p2)));
|
|
329
|
+
const box = Box.FromPoints(corners);
|
|
330
|
+
if (box.w < 1e-6 && box.h < 1e-6) continue;
|
|
331
|
+
const p = terminals[terminal];
|
|
332
|
+
const dx = Math.min(Math.abs(p.x - box.minX), Math.abs(p.x - box.maxX));
|
|
333
|
+
const dy = Math.min(Math.abs(p.y - box.minY), Math.abs(p.y - box.maxY));
|
|
334
|
+
out[terminal] = dx <= dy ? "x" : "y";
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
var ARROW_TERMINAL_GAP_STROKES = 2.7;
|
|
339
|
+
function getArrowTerminalGap(size, scale = 1) {
|
|
340
|
+
return STROKE_SIZES[size] * scale * ARROW_TERMINAL_GAP_STROKES;
|
|
341
|
+
}
|
|
342
|
+
function pullBack(point, from, distance) {
|
|
343
|
+
const d = Vec.Dist(point, from);
|
|
344
|
+
if (d <= 1e-6) return point;
|
|
345
|
+
return Vec.Lrp(point, from, Math.min(distance, d) / d);
|
|
346
|
+
}
|
|
347
|
+
function getArrowTerminalsInArrowSpace(editor, arrow) {
|
|
348
|
+
const bindings = getArrowBindings(editor, arrow);
|
|
349
|
+
const startShape = bindings.start ? editor.getShape(bindings.start.toId) : void 0;
|
|
350
|
+
const endShape = bindings.end ? editor.getShape(bindings.end.toId) : void 0;
|
|
351
|
+
const anchorFor2 = (terminal, binding, shape) => {
|
|
352
|
+
if (!binding || !shape) return Vec.From(arrow.props[terminal]);
|
|
353
|
+
const local = getAnchorInShapeSpace(editor, shape, binding.props.normalizedAnchor);
|
|
354
|
+
const page = applyTransform(editor.getShapePageTransform(shape), local);
|
|
355
|
+
return editor.getPointInShapeSpace(arrow, page);
|
|
356
|
+
};
|
|
357
|
+
const startAnchor = anchorFor2("start", bindings.start, startShape);
|
|
358
|
+
const endAnchor = anchorFor2("end", bindings.end, endShape);
|
|
359
|
+
const outlineInArrowSpace = (shape) => {
|
|
360
|
+
const m = editor.getShapePageTransform(shape);
|
|
361
|
+
const toArrow = (p) => editor.getPointInShapeSpace(arrow, applyTransform(m, p));
|
|
362
|
+
return getOutlineSegments(editor.getShapeGeometry(shape)).map(([a, b]) => [toArrow(a), toArrow(b)]);
|
|
363
|
+
};
|
|
364
|
+
const snap = (terminal, binding, shape, anchor, other) => {
|
|
365
|
+
if (!binding || !shape || binding.props.isExact) return anchor;
|
|
366
|
+
if (Vec.Dist2(anchor, other) < 1e-12) return anchor;
|
|
367
|
+
const path = sampleBody(other, anchor, arrow.props.bend, terminal === "start");
|
|
368
|
+
const crossing = firstCrossing(path, outlineInArrowSpace(shape));
|
|
369
|
+
if (!crossing) return anchor;
|
|
370
|
+
return pullBack(crossing, other, getArrowTerminalGap(arrow.props.size, arrow.props.scale));
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
start: snap("start", bindings.start, startShape, startAnchor, endAnchor),
|
|
374
|
+
end: snap("end", bindings.end, endShape, endAnchor, startAnchor)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
var FALLBACK_THEME_COLORS = DEFAULT_THEME.colors.light;
|
|
378
|
+
function getTheme(source) {
|
|
379
|
+
return source?.getCurrentTheme?.() ?? DEFAULT_THEME;
|
|
380
|
+
}
|
|
381
|
+
function getThemeColors(source) {
|
|
382
|
+
const theme = getTheme(source);
|
|
383
|
+
const mode = source?.getColorMode?.() ?? "light";
|
|
384
|
+
return theme.colors[mode] ?? theme.colors.light;
|
|
385
|
+
}
|
|
386
|
+
function getStrokeRgba(color, colors = FALLBACK_THEME_COLORS) {
|
|
387
|
+
return hexToRgba(getColorValue(colors, color, "solid"));
|
|
388
|
+
}
|
|
389
|
+
function getFillRgba(color, fill, colors = FALLBACK_THEME_COLORS) {
|
|
390
|
+
const token = DEFAULT_FILL_TOKENS[fill] ?? "none";
|
|
391
|
+
if (token === "none") return 0;
|
|
392
|
+
if (token === "paper") return hexToRgba(colors.solid);
|
|
393
|
+
return hexToRgba(getColorValue(colors, color, token));
|
|
394
|
+
}
|
|
395
|
+
function getNoteFillRgba(color, colors = FALLBACK_THEME_COLORS) {
|
|
396
|
+
return hexToRgba(getColorValue(colors, color, "noteFill"));
|
|
397
|
+
}
|
|
398
|
+
function getTextCssColor(color, colors = FALLBACK_THEME_COLORS) {
|
|
399
|
+
return getColorValue(colors, color, "solid");
|
|
400
|
+
}
|
|
401
|
+
function getNoteTextCssColor(color, colors = FALLBACK_THEME_COLORS) {
|
|
402
|
+
return getColorValue(colors, color, "noteText");
|
|
403
|
+
}
|
|
404
|
+
function getFontFamily(font, theme = DEFAULT_THEME) {
|
|
405
|
+
return theme.fonts[font] ?? theme.fonts.draw;
|
|
406
|
+
}
|
|
407
|
+
function getLabelFontFaces(font) {
|
|
408
|
+
const set = DefaultFontFaces[`tldraw_${font}`];
|
|
409
|
+
if (set === void 0) return [];
|
|
410
|
+
const faces = [];
|
|
411
|
+
for (const byWeight of Object.values(set)) {
|
|
412
|
+
for (const face of Object.values(byWeight)) faces.push(face);
|
|
413
|
+
}
|
|
414
|
+
return faces;
|
|
415
|
+
}
|
|
416
|
+
function getDashId(dash) {
|
|
417
|
+
switch (dash) {
|
|
418
|
+
case "dashed":
|
|
419
|
+
return 1;
|
|
420
|
+
case "dotted":
|
|
421
|
+
return 2;
|
|
422
|
+
case "draw":
|
|
423
|
+
return 3;
|
|
424
|
+
default:
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
var FRAME_FILL = "#ffffff";
|
|
429
|
+
var FRAME_STROKE = "#717171";
|
|
430
|
+
var FRAME_STROKE_WIDTH = 1;
|
|
431
|
+
var FRAME_NAME_COLOR = "#5f5f5f";
|
|
432
|
+
var FRAME_NAME_FONT_SIZE = 12;
|
|
433
|
+
var FRAME_NAME_OFFSET = 24;
|
|
434
|
+
var FRAME_NAME_GAP = 6;
|
|
435
|
+
var FRAME_NAME_HEIGHT = FRAME_NAME_OFFSET - FRAME_NAME_GAP;
|
|
436
|
+
var NOTE_GRADIENT_TOP_SCALE = 0.9785;
|
|
437
|
+
var NOTE_SHADOW_COLOR = "#152223";
|
|
438
|
+
var NOTE_SHADOW_OPACITY = 0.36;
|
|
439
|
+
var NOTE_SHADOW_OFFSET_Y = 12;
|
|
440
|
+
var NOTE_SHADOW_BLUR = 13;
|
|
441
|
+
var NOTE_SHADOW_SPREAD = -9;
|
|
442
|
+
function scaleHexColor(hex, factor) {
|
|
443
|
+
const n = Number.parseInt(hex.slice(1), 16);
|
|
444
|
+
const ch = (shift) => Math.max(0, Math.min(255, Math.round((n >> shift & 255) * factor)));
|
|
445
|
+
return `#${((ch(16) << 16 | ch(8) << 8 | ch(0)) >>> 0).toString(16).padStart(6, "0")}`;
|
|
446
|
+
}
|
|
447
|
+
function hexToCssRgba(hex, alpha) {
|
|
448
|
+
const n = Number.parseInt(hex.slice(1), 16);
|
|
449
|
+
return `rgba(${n >> 16 & 255}, ${n >> 8 & 255}, ${n & 255}, ${alpha})`;
|
|
450
|
+
}
|
|
451
|
+
function getNoteFillCssColor(color, colors = FALLBACK_THEME_COLORS) {
|
|
452
|
+
return getColorValue(colors, color, "noteFill");
|
|
453
|
+
}
|
|
454
|
+
function getNoteGradientTopFrom(fill) {
|
|
455
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(fill)) return fill;
|
|
456
|
+
return scaleHexColor(fill, NOTE_GRADIENT_TOP_SCALE);
|
|
457
|
+
}
|
|
458
|
+
function getNoteGradientTopCssColor(color, colors = FALLBACK_THEME_COLORS) {
|
|
459
|
+
return getNoteGradientTopFrom(getNoteFillCssColor(color, colors));
|
|
460
|
+
}
|
|
461
|
+
function getNoteBodyGradientCss(color, colors = FALLBACK_THEME_COLORS) {
|
|
462
|
+
return `linear-gradient(to bottom, ${getNoteGradientTopCssColor(color, colors)} 0%, ${getNoteFillCssColor(color, colors)} 100%)`;
|
|
463
|
+
}
|
|
464
|
+
function getNoteShadowCss(scale = 1) {
|
|
465
|
+
const dy = NOTE_SHADOW_OFFSET_Y * scale;
|
|
466
|
+
const blur = NOTE_SHADOW_BLUR * scale;
|
|
467
|
+
const spread = NOTE_SHADOW_SPREAD * scale;
|
|
468
|
+
return `0 ${dy}px ${blur}px ${spread}px ${hexToCssRgba(NOTE_SHADOW_COLOR, NOTE_SHADOW_OPACITY)}`;
|
|
469
|
+
}
|
|
470
|
+
function getNoteShadowSvgRect(w, h, scale = 1) {
|
|
471
|
+
const inset = -NOTE_SHADOW_SPREAD * scale;
|
|
472
|
+
return {
|
|
473
|
+
x: inset,
|
|
474
|
+
y: NOTE_SHADOW_OFFSET_Y * scale + inset,
|
|
475
|
+
w: Math.max(0, w - inset * 2),
|
|
476
|
+
h: Math.max(0, h - inset * 2),
|
|
477
|
+
// CSS blur radius is twice the Gaussian standard deviation.
|
|
478
|
+
stdDeviation: NOTE_SHADOW_BLUR * scale / 2
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/shapes/text-helpers.ts
|
|
483
|
+
var LINE_HEIGHT = 1.3;
|
|
484
|
+
var AVG_CHAR_WIDTH = 0.6;
|
|
485
|
+
function estimateTextSize(text, fontSize, maxWidth = Infinity) {
|
|
486
|
+
const charW = fontSize * AVG_CHAR_WIDTH;
|
|
487
|
+
const charsPerLine = Number.isFinite(maxWidth) && charW > 0 ? Math.max(1, Math.floor(maxWidth / charW)) : Infinity;
|
|
488
|
+
let lines = 0;
|
|
489
|
+
let widest = 0;
|
|
490
|
+
for (const paragraph of text.split("\n")) {
|
|
491
|
+
const n = paragraph.length;
|
|
492
|
+
lines += Math.max(1, Math.ceil(n / charsPerLine));
|
|
493
|
+
widest = Math.max(widest, Math.min(n, charsPerLine) * charW);
|
|
494
|
+
}
|
|
495
|
+
return { w: widest, h: lines * fontSize * LINE_HEIGHT, lines };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/text/TextMeasure.ts
|
|
499
|
+
var MAX_CACHE_ENTRIES = 2e3;
|
|
500
|
+
var ZERO_WIDTH_SPACE = "\u200B";
|
|
501
|
+
function toDisplayText(text) {
|
|
502
|
+
if (text.length === 0) return ZERO_WIDTH_SPACE;
|
|
503
|
+
return text.endsWith("\n") ? text + ZERO_WIDTH_SPACE : text;
|
|
504
|
+
}
|
|
505
|
+
var TextMeasure = class {
|
|
506
|
+
element = null;
|
|
507
|
+
htmlElement = null;
|
|
508
|
+
cache = /* @__PURE__ */ new Map();
|
|
509
|
+
htmlCache = /* @__PURE__ */ new Map();
|
|
510
|
+
measureText(text, opts) {
|
|
511
|
+
const key = cacheKey(text, opts);
|
|
512
|
+
const hit = this.cache.get(key);
|
|
513
|
+
if (hit) return hit;
|
|
514
|
+
const result = typeof document === "undefined" ? this.estimate(text, opts) : this.measureDom(text, opts);
|
|
515
|
+
if (this.cache.size >= MAX_CACHE_ENTRIES) {
|
|
516
|
+
const oldest = this.cache.keys().next().value;
|
|
517
|
+
if (oldest !== void 0) this.cache.delete(oldest);
|
|
518
|
+
}
|
|
519
|
+
this.cache.set(key, result);
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Measure a run of HTML — a rich-text label — laid out the way the DOM
|
|
524
|
+
* overlay renders it.
|
|
525
|
+
*
|
|
526
|
+
* Only pass HTML this package generated (`richTextToHtml` escapes text and
|
|
527
|
+
* scheme-checks link hrefs); the probe is inert but it is still a live DOM
|
|
528
|
+
* subtree.
|
|
529
|
+
*/
|
|
530
|
+
measureHtml(html, opts) {
|
|
531
|
+
const key = htmlCacheKey(html, opts);
|
|
532
|
+
const hit = this.htmlCache.get(key);
|
|
533
|
+
if (hit) return hit;
|
|
534
|
+
const result = typeof document === "undefined" ? this.estimateHtml(html, opts) : this.measureHtmlDom(html, opts);
|
|
535
|
+
if (this.htmlCache.size >= MAX_CACHE_ENTRIES) {
|
|
536
|
+
const oldest = this.htmlCache.keys().next().value;
|
|
537
|
+
if (oldest !== void 0) this.htmlCache.delete(oldest);
|
|
538
|
+
}
|
|
539
|
+
this.htmlCache.set(key, result);
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Measure several HTML runs against one probe. Nothing is shared between the
|
|
544
|
+
* items beyond the element itself; the win is one style write and one layout
|
|
545
|
+
* flush per item instead of one per call site.
|
|
546
|
+
*/
|
|
547
|
+
measureHtmlBatch(items) {
|
|
548
|
+
return items.map((item) => this.measureHtml(item.html, item.opts));
|
|
549
|
+
}
|
|
550
|
+
/** Number of cached measurements (for tests and debugging). */
|
|
551
|
+
get cacheSize() {
|
|
552
|
+
return this.cache.size;
|
|
553
|
+
}
|
|
554
|
+
clearCache() {
|
|
555
|
+
this.cache.clear();
|
|
556
|
+
this.htmlCache.clear();
|
|
557
|
+
}
|
|
558
|
+
dispose() {
|
|
559
|
+
this.cache.clear();
|
|
560
|
+
this.htmlCache.clear();
|
|
561
|
+
this.element?.remove();
|
|
562
|
+
this.element = null;
|
|
563
|
+
this.htmlElement?.remove();
|
|
564
|
+
this.htmlElement = null;
|
|
565
|
+
}
|
|
566
|
+
estimate(text, opts) {
|
|
567
|
+
const padding = opts.padding ?? 0;
|
|
568
|
+
const inner = opts.maxWidth === void 0 ? Infinity : Math.max(1, opts.maxWidth - padding * 2);
|
|
569
|
+
const est = estimateTextSize(text, opts.fontSize, inner);
|
|
570
|
+
return {
|
|
571
|
+
w: est.w + padding * 2,
|
|
572
|
+
h: est.lines * opts.fontSize * opts.lineHeight + padding * 2,
|
|
573
|
+
lineCount: est.lines
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
measureDom(text, opts) {
|
|
577
|
+
const el = this.getElement();
|
|
578
|
+
const padding = opts.padding ?? 0;
|
|
579
|
+
const s = el.style;
|
|
580
|
+
s.fontFamily = opts.fontFamily;
|
|
581
|
+
s.fontSize = `${opts.fontSize}px`;
|
|
582
|
+
s.fontWeight = opts.fontWeight === void 0 ? "normal" : String(opts.fontWeight);
|
|
583
|
+
s.lineHeight = String(opts.lineHeight);
|
|
584
|
+
s.padding = `${padding}px`;
|
|
585
|
+
s.maxWidth = opts.maxWidth === void 0 ? "none" : `${Math.max(1, opts.maxWidth)}px`;
|
|
586
|
+
el.textContent = toDisplayText(text);
|
|
587
|
+
const rect = el.getBoundingClientRect();
|
|
588
|
+
const lineHeightPx = opts.fontSize * opts.lineHeight;
|
|
589
|
+
const contentH = Math.max(0, rect.height - padding * 2);
|
|
590
|
+
const lineCount = lineHeightPx > 0 ? Math.max(1, Math.round(contentH / lineHeightPx)) : 1;
|
|
591
|
+
return { w: Math.ceil(rect.width * 100) / 100, h: Math.ceil(rect.height * 100) / 100, lineCount };
|
|
592
|
+
}
|
|
593
|
+
estimateHtml(html, opts) {
|
|
594
|
+
const text = htmlToProbeText(html);
|
|
595
|
+
const padding = paddingPx(opts.padding);
|
|
596
|
+
const base = this.estimate(text, {
|
|
597
|
+
fontFamily: opts.fontFamily,
|
|
598
|
+
fontSize: opts.fontSize,
|
|
599
|
+
lineHeight: opts.lineHeight,
|
|
600
|
+
...opts.fontWeight === void 0 ? {} : { fontWeight: opts.fontWeight },
|
|
601
|
+
...opts.maxWidth === void 0 || opts.maxWidth === null ? {} : { maxWidth: opts.maxWidth },
|
|
602
|
+
padding
|
|
603
|
+
});
|
|
604
|
+
const unwrapped = this.estimate(text, { fontFamily: opts.fontFamily, fontSize: opts.fontSize, lineHeight: opts.lineHeight, padding });
|
|
605
|
+
return { ...base, scrollWidth: opts.measureScrollWidth ? unwrapped.w : base.w };
|
|
606
|
+
}
|
|
607
|
+
measureHtmlDom(html, opts) {
|
|
608
|
+
const el = this.getHtmlElement();
|
|
609
|
+
const s = el.style;
|
|
610
|
+
s.fontFamily = opts.fontFamily;
|
|
611
|
+
s.fontSize = `${opts.fontSize}px`;
|
|
612
|
+
s.fontWeight = opts.fontWeight === void 0 ? "normal" : String(opts.fontWeight);
|
|
613
|
+
s.fontStyle = opts.fontStyle ?? "normal";
|
|
614
|
+
s.lineHeight = String(opts.lineHeight);
|
|
615
|
+
s.padding = typeof opts.padding === "number" ? `${opts.padding}px` : opts.padding ?? "0px";
|
|
616
|
+
s.maxWidth = opts.maxWidth === void 0 || opts.maxWidth === null ? "none" : `${Math.max(1, opts.maxWidth)}px`;
|
|
617
|
+
for (const name of readCustomStyleNames(el)) s.removeProperty(name);
|
|
618
|
+
for (const [name, value] of Object.entries(opts.otherStyles ?? {})) s.setProperty(name, value);
|
|
619
|
+
writeCustomStyleNames(el, Object.keys(opts.otherStyles ?? {}));
|
|
620
|
+
el.innerHTML = html;
|
|
621
|
+
const rect = el.getBoundingClientRect();
|
|
622
|
+
const padding = paddingPx(opts.padding);
|
|
623
|
+
const lineHeightPx = opts.fontSize * opts.lineHeight;
|
|
624
|
+
const contentH = Math.max(0, rect.height - padding * 2);
|
|
625
|
+
const lineCount = lineHeightPx > 0 ? Math.max(1, Math.round(contentH / lineHeightPx)) : 1;
|
|
626
|
+
let scrollWidth = rect.width;
|
|
627
|
+
if (opts.measureScrollWidth) {
|
|
628
|
+
const previousMax = s.maxWidth;
|
|
629
|
+
s.maxWidth = "none";
|
|
630
|
+
scrollWidth = el.getBoundingClientRect().width;
|
|
631
|
+
s.maxWidth = previousMax;
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
w: Math.ceil(rect.width * 100) / 100,
|
|
635
|
+
h: Math.ceil(rect.height * 100) / 100,
|
|
636
|
+
lineCount,
|
|
637
|
+
scrollWidth: Math.ceil(scrollWidth * 100) / 100
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
getHtmlElement() {
|
|
641
|
+
if (this.htmlElement && this.htmlElement.isConnected) return this.htmlElement;
|
|
642
|
+
installHtmlProbeStyles();
|
|
643
|
+
const el = document.createElement("div");
|
|
644
|
+
el.setAttribute("aria-hidden", "true");
|
|
645
|
+
el.className = "mocanvas-text-measure-html";
|
|
646
|
+
Object.assign(el.style, {
|
|
647
|
+
position: "fixed",
|
|
648
|
+
top: "-10000px",
|
|
649
|
+
left: "-10000px",
|
|
650
|
+
visibility: "hidden",
|
|
651
|
+
pointerEvents: "none",
|
|
652
|
+
whiteSpace: "pre-wrap",
|
|
653
|
+
overflowWrap: "break-word",
|
|
654
|
+
wordBreak: "normal",
|
|
655
|
+
width: "max-content",
|
|
656
|
+
boxSizing: "border-box",
|
|
657
|
+
margin: "0",
|
|
658
|
+
border: "0",
|
|
659
|
+
zIndex: "-1"
|
|
660
|
+
});
|
|
661
|
+
document.body.appendChild(el);
|
|
662
|
+
this.htmlElement = el;
|
|
663
|
+
return el;
|
|
664
|
+
}
|
|
665
|
+
getElement() {
|
|
666
|
+
if (this.element && this.element.isConnected) return this.element;
|
|
667
|
+
const el = document.createElement("div");
|
|
668
|
+
el.setAttribute("aria-hidden", "true");
|
|
669
|
+
el.className = "mocanvas-text-measure";
|
|
670
|
+
Object.assign(el.style, {
|
|
671
|
+
position: "fixed",
|
|
672
|
+
top: "-10000px",
|
|
673
|
+
left: "-10000px",
|
|
674
|
+
visibility: "hidden",
|
|
675
|
+
pointerEvents: "none",
|
|
676
|
+
whiteSpace: "pre-wrap",
|
|
677
|
+
overflowWrap: "break-word",
|
|
678
|
+
wordBreak: "normal",
|
|
679
|
+
width: "max-content",
|
|
680
|
+
boxSizing: "border-box",
|
|
681
|
+
margin: "0",
|
|
682
|
+
border: "0",
|
|
683
|
+
zIndex: "-1"
|
|
684
|
+
});
|
|
685
|
+
document.body.appendChild(el);
|
|
686
|
+
this.element = el;
|
|
687
|
+
return el;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
function cacheKey(text, o) {
|
|
691
|
+
return `${o.fontFamily}|${o.fontSize}|${o.fontWeight ?? ""}|${o.lineHeight}|${o.maxWidth ?? ""}|${o.padding ?? 0}|${text}`;
|
|
692
|
+
}
|
|
693
|
+
function htmlCacheKey(html, o) {
|
|
694
|
+
const other = Object.entries(o.otherStyles ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${k}:${v}`).join(";");
|
|
695
|
+
return `${o.fontFamily}|${o.fontSize}|${o.fontWeight ?? ""}|${o.fontStyle ?? ""}|${o.lineHeight}|${o.maxWidth ?? ""}|${o.padding ?? 0}|${o.measureScrollWidth ? 1 : 0}|${other}|${html}`;
|
|
696
|
+
}
|
|
697
|
+
function paddingPx(padding) {
|
|
698
|
+
if (typeof padding === "number") return Number.isFinite(padding) ? padding : 0;
|
|
699
|
+
if (typeof padding !== "string") return 0;
|
|
700
|
+
const first = padding.trim().split(/\s+/)[0] ?? "";
|
|
701
|
+
const value = Number.parseFloat(first);
|
|
702
|
+
return Number.isFinite(value) ? value : 0;
|
|
703
|
+
}
|
|
704
|
+
var BLOCK_TAG = /<\/?(?:p|div|li|ul|ol|h[1-6]|blockquote|pre|tr|br|hr)\b[^>]*>/gi;
|
|
705
|
+
function htmlToProbeText(html) {
|
|
706
|
+
return html.replace(BLOCK_TAG, "\n").replace(/<[^>]*>/g, "").replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").replace(/\n{2,}/g, "\n").replace(/^\n|\n$/g, "");
|
|
707
|
+
}
|
|
708
|
+
function readCustomStyleNames(el) {
|
|
709
|
+
const raw = el.dataset["mocanvasOtherStyles"];
|
|
710
|
+
return raw === void 0 || raw.length === 0 ? [] : raw.split(",");
|
|
711
|
+
}
|
|
712
|
+
function writeCustomStyleNames(el, names) {
|
|
713
|
+
if (names.length === 0) delete el.dataset["mocanvasOtherStyles"];
|
|
714
|
+
else el.dataset["mocanvasOtherStyles"] = names.join(",");
|
|
715
|
+
}
|
|
716
|
+
var htmlProbeStylesInstalled = false;
|
|
717
|
+
function installHtmlProbeStyles() {
|
|
718
|
+
if (htmlProbeStylesInstalled || typeof document === "undefined") return;
|
|
719
|
+
htmlProbeStylesInstalled = true;
|
|
720
|
+
const style = document.createElement("style");
|
|
721
|
+
style.setAttribute("data-mocanvas", "text-measure");
|
|
722
|
+
style.textContent = RICH_TEXT_BLOCK_CSS.replace(/__SCOPE__/g, ".mocanvas-text-measure-html");
|
|
723
|
+
document.head.appendChild(style);
|
|
724
|
+
}
|
|
725
|
+
var RICH_TEXT_BLOCK_CSS = [
|
|
726
|
+
"__SCOPE__ p,__SCOPE__ h1,__SCOPE__ h2,__SCOPE__ h3,__SCOPE__ h4,__SCOPE__ h5,__SCOPE__ h6,__SCOPE__ blockquote,__SCOPE__ pre,__SCOPE__ ul,__SCOPE__ ol{margin:0;padding:0;font-size:inherit;font-weight:inherit;line-height:inherit;}",
|
|
727
|
+
"__SCOPE__ ul,__SCOPE__ ol{padding-inline-start:1.4em;}",
|
|
728
|
+
"__SCOPE__ li{margin:0;}",
|
|
729
|
+
"__SCOPE__ pre,__SCOPE__ code{font-family:inherit;white-space:pre-wrap;}",
|
|
730
|
+
"__SCOPE__ hr{margin:0;border:0;border-top:1px solid currentColor;}",
|
|
731
|
+
"__SCOPE__ a{color:inherit;}"
|
|
732
|
+
].join("");
|
|
733
|
+
var singleton = null;
|
|
734
|
+
function getTextMeasure() {
|
|
735
|
+
if (!singleton) singleton = new TextMeasure();
|
|
736
|
+
return singleton;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/text/measure-html.ts
|
|
740
|
+
function getRichTextExtensions(editor) {
|
|
741
|
+
const host = editor;
|
|
742
|
+
const configured = host?.options?.text?.tipTapConfig?.extensions ?? host?.textOptions?.tipTapConfig?.extensions;
|
|
743
|
+
return configured && configured.length > 0 ? configured : tipTapDefaultExtensions;
|
|
744
|
+
}
|
|
745
|
+
function renderHtmlFromRichTextForMeasurement(editor, richText) {
|
|
746
|
+
return richTextToHtml(richText, { extensions: getRichTextExtensions(editor) });
|
|
747
|
+
}
|
|
748
|
+
function renderHtmlFromRichTextWithExtensions(richText, extensions) {
|
|
749
|
+
return richTextToHtml(richText, { extensions });
|
|
750
|
+
}
|
|
751
|
+
function measureRichText(editor, richText, opts) {
|
|
752
|
+
return getTextMeasure().measureHtml(renderHtmlFromRichTextForMeasurement(editor, richText), opts);
|
|
753
|
+
}
|
|
754
|
+
function labelFontFamily(props) {
|
|
755
|
+
return props.fontFamily ?? props.font ?? "draw";
|
|
756
|
+
}
|
|
757
|
+
function labelTextAlign(props) {
|
|
758
|
+
return props.textAlign ?? props.align ?? "middle";
|
|
759
|
+
}
|
|
760
|
+
function alignToJustify(align) {
|
|
761
|
+
switch (align) {
|
|
762
|
+
case "start":
|
|
763
|
+
case "start-legacy":
|
|
764
|
+
return "flex-start";
|
|
765
|
+
case "end":
|
|
766
|
+
case "end-legacy":
|
|
767
|
+
return "flex-end";
|
|
768
|
+
default:
|
|
769
|
+
return "center";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function alignToTextAlign(align) {
|
|
773
|
+
switch (align) {
|
|
774
|
+
case "start":
|
|
775
|
+
case "start-legacy":
|
|
776
|
+
return "left";
|
|
777
|
+
case "end":
|
|
778
|
+
case "end-legacy":
|
|
779
|
+
return "right";
|
|
780
|
+
default:
|
|
781
|
+
return "center";
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function verticalAlignToAlignItems(align) {
|
|
785
|
+
switch (align) {
|
|
786
|
+
case "start":
|
|
787
|
+
return "flex-start";
|
|
788
|
+
case "end":
|
|
789
|
+
return "flex-end";
|
|
790
|
+
default:
|
|
791
|
+
return "center";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
var FLUSH_FALLBACK_MS = 32;
|
|
795
|
+
var stop = (e) => {
|
|
796
|
+
e.stopPropagation();
|
|
797
|
+
};
|
|
798
|
+
function TextLabel(props) {
|
|
799
|
+
const { text, richText, isEditing, fontSize, color, verticalAlign, wrap, width, height, padding = 0, placeholder, singleLine } = props;
|
|
800
|
+
const align = labelTextAlign(props);
|
|
801
|
+
const font = labelFontFamily(props);
|
|
802
|
+
const editor = useEditor();
|
|
803
|
+
const rich = isRichText(richText) ? richText : null;
|
|
804
|
+
const plain = rich ? richTextToText(rich) : text;
|
|
805
|
+
const html = useMemo(() => rich ? renderHtmlFromRichTextForMeasurement(editor, rich) : null, [editor, rich]);
|
|
806
|
+
const textStyle = {
|
|
807
|
+
position: "relative",
|
|
808
|
+
fontFamily: getFontFamily(font),
|
|
809
|
+
fontSize,
|
|
810
|
+
fontWeight: "normal",
|
|
811
|
+
lineHeight: LINE_HEIGHT,
|
|
812
|
+
color,
|
|
813
|
+
textAlign: alignToTextAlign(align),
|
|
814
|
+
whiteSpace: wrap ? "pre-wrap" : "pre",
|
|
815
|
+
overflowWrap: wrap ? "break-word" : "normal",
|
|
816
|
+
wordBreak: "normal",
|
|
817
|
+
padding,
|
|
818
|
+
boxSizing: "border-box",
|
|
819
|
+
maxWidth: "100%",
|
|
820
|
+
minWidth: isEditing ? fontSize + padding * 2 : void 0,
|
|
821
|
+
width: wrap ? "100%" : void 0,
|
|
822
|
+
margin: 0
|
|
823
|
+
};
|
|
824
|
+
const outerStyle = {
|
|
825
|
+
position: "absolute",
|
|
826
|
+
left: 0,
|
|
827
|
+
top: 0,
|
|
828
|
+
width: width ?? "max-content",
|
|
829
|
+
height: height ?? "auto",
|
|
830
|
+
display: "flex",
|
|
831
|
+
justifyContent: alignToJustify(align),
|
|
832
|
+
alignItems: verticalAlignToAlignItems(verticalAlign),
|
|
833
|
+
pointerEvents: isEditing ? "auto" : "none",
|
|
834
|
+
userSelect: isEditing ? "text" : "none",
|
|
835
|
+
WebkitUserSelect: isEditing ? "text" : "none"
|
|
836
|
+
};
|
|
837
|
+
if (!isEditing) {
|
|
838
|
+
const empty = plain.length === 0;
|
|
839
|
+
if (rich && !(empty && placeholder)) {
|
|
840
|
+
return /* @__PURE__ */ jsxs("div", { className: "mocanvas-text-label", style: outerStyle, children: [
|
|
841
|
+
/* @__PURE__ */ jsx(RichTextBlockStyles, {}),
|
|
842
|
+
/* @__PURE__ */ jsx("div", { className: "mocanvas-rich-text", style: textStyle, dangerouslySetInnerHTML: { __html: html ?? "" } })
|
|
843
|
+
] });
|
|
844
|
+
}
|
|
845
|
+
const shown = empty && placeholder ? placeholder : toDisplayText(plain);
|
|
846
|
+
return /* @__PURE__ */ jsx("div", { className: "mocanvas-text-label", style: outerStyle, children: /* @__PURE__ */ jsx("div", { style: { ...textStyle, opacity: empty && placeholder ? 0.5 : 1 }, children: shown }) });
|
|
847
|
+
}
|
|
848
|
+
const factory = getRichTextEditorFactory();
|
|
849
|
+
const useRichSurface = factory !== null && props.onChangeRichText !== void 0 && rich !== null;
|
|
850
|
+
return /* @__PURE__ */ jsx("div", { className: "mocanvas-text-label mocanvas-text-label-editing", style: outerStyle, children: useRichSurface ? /* @__PURE__ */ jsx(RichTextEditorSurface, { ...props, richText: rich, onChangeRichText: props.onChangeRichText, factory, textStyle, singleLine: singleLine ?? false }) : /* @__PURE__ */ jsx(TextEditor, { ...props, text: plain, textStyle, singleLine: singleLine ?? false }) });
|
|
851
|
+
}
|
|
852
|
+
function RichTextBlockStyles() {
|
|
853
|
+
return /* @__PURE__ */ jsx("style", { children: RICH_TEXT_BLOCK_CSS.replace(/__SCOPE__/g, ".mocanvas-rich-text") });
|
|
854
|
+
}
|
|
855
|
+
function TextEditor(props) {
|
|
856
|
+
const { text, onChange, onChangeRichText, richText, textStyle, singleLine, wrap, color } = props;
|
|
857
|
+
const editor = useEditor();
|
|
858
|
+
const ref = useRef(null);
|
|
859
|
+
const [value, setValue] = useState(text);
|
|
860
|
+
const valueRef = useRef(text);
|
|
861
|
+
const pendingRef = useRef(null);
|
|
862
|
+
const rafRef = useRef(0);
|
|
863
|
+
const timerRef = useRef(0);
|
|
864
|
+
const richRef = useRef(richText);
|
|
865
|
+
richRef.current = richText;
|
|
866
|
+
const onChangeRef = useRef(() => {
|
|
867
|
+
});
|
|
868
|
+
onChangeRef.current = (next) => {
|
|
869
|
+
onChange(next);
|
|
870
|
+
onChangeRichText?.(applyPlainTextToRichText(richRef.current, next));
|
|
871
|
+
};
|
|
872
|
+
const flush = useCallback(() => {
|
|
873
|
+
if (rafRef.current) {
|
|
874
|
+
cancelAnimationFrame(rafRef.current);
|
|
875
|
+
rafRef.current = 0;
|
|
876
|
+
}
|
|
877
|
+
if (timerRef.current) {
|
|
878
|
+
clearTimeout(timerRef.current);
|
|
879
|
+
timerRef.current = 0;
|
|
880
|
+
}
|
|
881
|
+
if (pendingRef.current !== null) {
|
|
882
|
+
const next = pendingRef.current;
|
|
883
|
+
pendingRef.current = null;
|
|
884
|
+
onChangeRef.current(next);
|
|
885
|
+
}
|
|
886
|
+
}, []);
|
|
887
|
+
const commit = useCallback(
|
|
888
|
+
(next) => {
|
|
889
|
+
valueRef.current = next;
|
|
890
|
+
setValue(next);
|
|
891
|
+
pendingRef.current = next;
|
|
892
|
+
if (!rafRef.current) rafRef.current = requestAnimationFrame(flush);
|
|
893
|
+
if (!timerRef.current) timerRef.current = window.setTimeout(flush, FLUSH_FALLBACK_MS);
|
|
894
|
+
},
|
|
895
|
+
[flush]
|
|
896
|
+
);
|
|
897
|
+
useEffect(() => {
|
|
898
|
+
if (pendingRef.current === null && text !== valueRef.current) {
|
|
899
|
+
valueRef.current = text;
|
|
900
|
+
setValue(text);
|
|
901
|
+
}
|
|
902
|
+
}, [text]);
|
|
903
|
+
useLayoutEffect(() => {
|
|
904
|
+
const el = ref.current;
|
|
905
|
+
if (el) {
|
|
906
|
+
el.focus({ preventScroll: true });
|
|
907
|
+
const end = el.value.length;
|
|
908
|
+
el.setSelectionRange(end, end);
|
|
909
|
+
}
|
|
910
|
+
return () => flush();
|
|
911
|
+
}, [flush]);
|
|
912
|
+
const finish = useCallback(() => {
|
|
913
|
+
flush();
|
|
914
|
+
editor.setEditingShape(null);
|
|
915
|
+
editor.complete();
|
|
916
|
+
}, [editor, flush]);
|
|
917
|
+
const onKeyDown = (e) => {
|
|
918
|
+
e.stopPropagation();
|
|
919
|
+
if (e.key === "Escape") {
|
|
920
|
+
e.preventDefault();
|
|
921
|
+
finish();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (e.key === "Tab") {
|
|
925
|
+
e.preventDefault();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (e.key === "Enter" && singleLine && !e.shiftKey) {
|
|
929
|
+
e.preventDefault();
|
|
930
|
+
finish();
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
const displayed = toDisplayText(value);
|
|
934
|
+
return /* @__PURE__ */ jsxs("div", { style: textStyle, onPointerDown: stop, onPointerMove: stop, onPointerUp: stop, children: [
|
|
935
|
+
/* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { visibility: "hidden", pointerEvents: "none" }, children: displayed }),
|
|
936
|
+
/* @__PURE__ */ jsx(
|
|
937
|
+
"textarea",
|
|
938
|
+
{
|
|
939
|
+
ref,
|
|
940
|
+
className: "mocanvas-text-editor",
|
|
941
|
+
value,
|
|
942
|
+
rows: 1,
|
|
943
|
+
wrap: wrap ? "soft" : "off",
|
|
944
|
+
spellCheck: false,
|
|
945
|
+
autoCapitalize: "off",
|
|
946
|
+
autoCorrect: "off",
|
|
947
|
+
"data-gramm": "false",
|
|
948
|
+
onChange: (e) => {
|
|
949
|
+
const raw = e.currentTarget.value;
|
|
950
|
+
commit(singleLine ? raw.replace(/[\r\n]+/g, "") : raw);
|
|
951
|
+
},
|
|
952
|
+
onBlur: flush,
|
|
953
|
+
onKeyDown,
|
|
954
|
+
onKeyUp: stop,
|
|
955
|
+
onPointerDown: stop,
|
|
956
|
+
onPointerMove: stop,
|
|
957
|
+
onPointerUp: stop,
|
|
958
|
+
onContextMenu: stop,
|
|
959
|
+
style: {
|
|
960
|
+
position: "absolute",
|
|
961
|
+
inset: 0,
|
|
962
|
+
width: "100%",
|
|
963
|
+
height: "100%",
|
|
964
|
+
display: "block",
|
|
965
|
+
margin: 0,
|
|
966
|
+
border: "none",
|
|
967
|
+
outline: "none",
|
|
968
|
+
background: "transparent",
|
|
969
|
+
resize: "none",
|
|
970
|
+
overflow: "hidden",
|
|
971
|
+
boxSizing: "border-box",
|
|
972
|
+
padding: textStyle.padding,
|
|
973
|
+
fontFamily: textStyle.fontFamily,
|
|
974
|
+
fontSize: textStyle.fontSize,
|
|
975
|
+
fontWeight: textStyle.fontWeight,
|
|
976
|
+
lineHeight: textStyle.lineHeight,
|
|
977
|
+
color,
|
|
978
|
+
caretColor: color,
|
|
979
|
+
textAlign: textStyle.textAlign,
|
|
980
|
+
whiteSpace: textStyle.whiteSpace,
|
|
981
|
+
overflowWrap: textStyle.overflowWrap,
|
|
982
|
+
wordBreak: textStyle.wordBreak,
|
|
983
|
+
letterSpacing: "inherit",
|
|
984
|
+
userSelect: "text",
|
|
985
|
+
WebkitUserSelect: "text",
|
|
986
|
+
pointerEvents: "auto",
|
|
987
|
+
cursor: "text",
|
|
988
|
+
touchAction: "auto"
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
)
|
|
992
|
+
] });
|
|
993
|
+
}
|
|
994
|
+
function RichTextEditorSurface({ richText, onChangeRichText, factory, textStyle, singleLine }) {
|
|
995
|
+
const editor = useEditor();
|
|
996
|
+
const ref = useRef(null);
|
|
997
|
+
const onChangeRef = useRef(onChangeRichText);
|
|
998
|
+
onChangeRef.current = onChangeRichText;
|
|
999
|
+
const initial = useRef(richText);
|
|
1000
|
+
useLayoutEffect(() => {
|
|
1001
|
+
const container = ref.current;
|
|
1002
|
+
if (!container) return;
|
|
1003
|
+
const handle = factory({
|
|
1004
|
+
container,
|
|
1005
|
+
initialValue: initial.current,
|
|
1006
|
+
extensions: getRichTextExtensions(editor),
|
|
1007
|
+
singleLine,
|
|
1008
|
+
onChange: (value) => onChangeRef.current(value),
|
|
1009
|
+
onFinish: () => {
|
|
1010
|
+
editor.setEditingShape(null);
|
|
1011
|
+
editor.complete();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
handle.focus();
|
|
1015
|
+
return () => handle.destroy();
|
|
1016
|
+
}, [editor, factory, singleLine]);
|
|
1017
|
+
return /* @__PURE__ */ jsx(
|
|
1018
|
+
"div",
|
|
1019
|
+
{
|
|
1020
|
+
ref,
|
|
1021
|
+
className: "mocanvas-rich-text mocanvas-rich-text-editor",
|
|
1022
|
+
style: { ...textStyle, userSelect: "text", WebkitUserSelect: "text", pointerEvents: "auto", cursor: "text" },
|
|
1023
|
+
onPointerDown: stop,
|
|
1024
|
+
onPointerMove: stop,
|
|
1025
|
+
onPointerUp: stop,
|
|
1026
|
+
onKeyDown: stop,
|
|
1027
|
+
onKeyUp: stop,
|
|
1028
|
+
onContextMenu: stop
|
|
1029
|
+
}
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/text/text-layout.ts
|
|
1034
|
+
function computeGrowY(labelHeight, boxHeight) {
|
|
1035
|
+
if (!Number.isFinite(labelHeight) || !Number.isFinite(boxHeight)) return 0;
|
|
1036
|
+
return Math.max(0, labelHeight - boxHeight);
|
|
1037
|
+
}
|
|
1038
|
+
function trimTrailingWhitespace(text) {
|
|
1039
|
+
return text.replace(/\s+$/u, "");
|
|
1040
|
+
}
|
|
1041
|
+
function labelFontStyle(opts) {
|
|
1042
|
+
return opts.fontFamily ?? opts.font ?? "draw";
|
|
1043
|
+
}
|
|
1044
|
+
function measureLabel(source, opts) {
|
|
1045
|
+
const fontFamily = getFontFamily(labelFontStyle(opts));
|
|
1046
|
+
if (isRichText(source)) {
|
|
1047
|
+
const html = renderHtmlFromRichTextForMeasurement(opts.editor ?? null, source);
|
|
1048
|
+
return getTextMeasure().measureHtml(html, {
|
|
1049
|
+
fontFamily,
|
|
1050
|
+
fontSize: opts.fontSize,
|
|
1051
|
+
lineHeight: LINE_HEIGHT,
|
|
1052
|
+
...opts.maxWidth === void 0 ? {} : { maxWidth: opts.maxWidth },
|
|
1053
|
+
padding: opts.padding ?? 0
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
return getTextMeasure().measureText(richTextToText(source), {
|
|
1057
|
+
fontFamily,
|
|
1058
|
+
fontSize: opts.fontSize,
|
|
1059
|
+
lineHeight: LINE_HEIGHT,
|
|
1060
|
+
...opts.maxWidth === void 0 ? {} : { maxWidth: opts.maxWidth },
|
|
1061
|
+
padding: opts.padding ?? 0
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
var OPTICAL_LIFT_CACHE = /* @__PURE__ */ new Map();
|
|
1065
|
+
function getLabelOpticalLift(opts) {
|
|
1066
|
+
return getOpticalCentreLift(getFontFamily(labelFontStyle(opts))) * opts.fontSize;
|
|
1067
|
+
}
|
|
1068
|
+
function getOpticalCentreLift(fontFamily) {
|
|
1069
|
+
const cached = OPTICAL_LIFT_CACHE.get(fontFamily);
|
|
1070
|
+
if (cached !== void 0) return cached;
|
|
1071
|
+
let lift = 0;
|
|
1072
|
+
try {
|
|
1073
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
1074
|
+
if (ctx) {
|
|
1075
|
+
const em = 100;
|
|
1076
|
+
ctx.font = `${em}px ${fontFamily}`;
|
|
1077
|
+
const caps = ctx.measureText("H");
|
|
1078
|
+
const ex = ctx.measureText("x");
|
|
1079
|
+
const capHeight = caps.actualBoundingBoxAscent;
|
|
1080
|
+
const xHeight = ex.actualBoundingBoxAscent;
|
|
1081
|
+
const ascent = caps.fontBoundingBoxAscent;
|
|
1082
|
+
const descent = caps.fontBoundingBoxDescent;
|
|
1083
|
+
if (capHeight > 0 && xHeight > 0 && ascent > 0) {
|
|
1084
|
+
const lineHeight = LINE_HEIGHT * em;
|
|
1085
|
+
const baseline = (lineHeight - (ascent + descent)) / 2 + ascent;
|
|
1086
|
+
const optical = baseline - (capHeight + xHeight) / 4;
|
|
1087
|
+
lift = (optical - lineHeight / 2) / em;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
} catch {
|
|
1091
|
+
}
|
|
1092
|
+
if (!Number.isFinite(lift) || lift < 0 || lift > 0.25) lift = 0;
|
|
1093
|
+
OPTICAL_LIFT_CACHE.set(fontFamily, lift);
|
|
1094
|
+
return lift;
|
|
1095
|
+
}
|
|
1096
|
+
var TEXT_SHAPE_MIN_WIDTH = 8;
|
|
1097
|
+
function getTextShapeSize(input) {
|
|
1098
|
+
const { text, fontSize, autoSize, w, editor } = input;
|
|
1099
|
+
const fontFamily = labelFontStyle(input);
|
|
1100
|
+
if (autoSize) {
|
|
1101
|
+
const m2 = measureLabel(text, { fontFamily, fontSize, editor: editor ?? null });
|
|
1102
|
+
return { w: Math.max(TEXT_SHAPE_MIN_WIDTH, m2.w), h: m2.h, lineCount: m2.lineCount };
|
|
1103
|
+
}
|
|
1104
|
+
const width = Math.max(1, w);
|
|
1105
|
+
const m = measureLabel(text, { fontFamily, fontSize, maxWidth: width, editor: editor ?? null });
|
|
1106
|
+
return { w: width, h: m.h, lineCount: m.lineCount };
|
|
1107
|
+
}
|
|
1108
|
+
var EPS2 = 1e-6;
|
|
1109
|
+
var ELBOW_CORNER_STROKES = 2.5;
|
|
1110
|
+
var CORNER_SEGMENTS = 4;
|
|
1111
|
+
function clamp01(value) {
|
|
1112
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
1113
|
+
}
|
|
1114
|
+
function getDominantAxis(start, end) {
|
|
1115
|
+
return Math.abs(end.x - start.x) >= Math.abs(end.y - start.y) ? "x" : "y";
|
|
1116
|
+
}
|
|
1117
|
+
function simplify(points) {
|
|
1118
|
+
const out = [];
|
|
1119
|
+
for (const p of points) {
|
|
1120
|
+
const last = out.at(-1);
|
|
1121
|
+
if (last && Vec.Dist2(last, p) < EPS2 * EPS2) continue;
|
|
1122
|
+
out.push(p);
|
|
1123
|
+
}
|
|
1124
|
+
for (let i = out.length - 2; i > 0; i--) {
|
|
1125
|
+
const a = out[i - 1];
|
|
1126
|
+
const b = out[i];
|
|
1127
|
+
const c = out[i + 1];
|
|
1128
|
+
const cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
1129
|
+
if (Math.abs(cross) < EPS2) out.splice(i, 1);
|
|
1130
|
+
}
|
|
1131
|
+
return out;
|
|
1132
|
+
}
|
|
1133
|
+
function roundCorner(a, p, b, radius) {
|
|
1134
|
+
const lenA = Vec.Dist(a, p);
|
|
1135
|
+
const lenB = Vec.Dist(b, p);
|
|
1136
|
+
const r = Math.min(radius, lenA / 2, lenB / 2);
|
|
1137
|
+
if (r < EPS2) return [p];
|
|
1138
|
+
const da = Vec.Div(Vec.Sub(a, p), lenA);
|
|
1139
|
+
const db = Vec.Div(Vec.Sub(b, p), lenB);
|
|
1140
|
+
if (Math.abs(Vec.Dot(da, db)) > 1e-3) return [p];
|
|
1141
|
+
const from = Vec.Add(p, Vec.Mul(da, r));
|
|
1142
|
+
const to = Vec.Add(p, Vec.Mul(db, r));
|
|
1143
|
+
const center = Vec.Add(p, Vec.Mul(Vec.Add(da, db), r));
|
|
1144
|
+
const a0 = Math.atan2(from.y - center.y, from.x - center.x);
|
|
1145
|
+
const a1 = Math.atan2(to.y - center.y, to.x - center.x);
|
|
1146
|
+
let sweep = a1 - a0;
|
|
1147
|
+
while (sweep > Math.PI) sweep -= Math.PI * 2;
|
|
1148
|
+
while (sweep <= -Math.PI) sweep += Math.PI * 2;
|
|
1149
|
+
const out = [];
|
|
1150
|
+
for (let i = 0; i <= CORNER_SEGMENTS; i++) {
|
|
1151
|
+
const angle = a0 + sweep * i / CORNER_SEGMENTS;
|
|
1152
|
+
out.push(new Vec(center.x + r * Math.cos(angle), center.y + r * Math.sin(angle)));
|
|
1153
|
+
}
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1156
|
+
function roundElbowCorners(corners, radius) {
|
|
1157
|
+
if (corners.length < 3 || radius < EPS2) return corners.map((p) => p.clone());
|
|
1158
|
+
const out = [corners[0].clone()];
|
|
1159
|
+
for (let i = 1; i < corners.length - 1; i++) {
|
|
1160
|
+
for (const p of roundCorner(corners[i - 1], corners[i], corners[i + 1], radius)) out.push(p);
|
|
1161
|
+
}
|
|
1162
|
+
out.push(corners.at(-1).clone());
|
|
1163
|
+
return simplify(out);
|
|
1164
|
+
}
|
|
1165
|
+
function getElbowRoute(start, end, options = {}) {
|
|
1166
|
+
const s = Vec.From(start);
|
|
1167
|
+
const e = Vec.From(end);
|
|
1168
|
+
const dominant = getDominantAxis(s, e);
|
|
1169
|
+
const startAxis = options.startAxis ?? dominant;
|
|
1170
|
+
const endAxis = options.endAxis ?? dominant;
|
|
1171
|
+
const t = clamp01(options.midPoint ?? 0.5);
|
|
1172
|
+
let corners;
|
|
1173
|
+
let midLeg = null;
|
|
1174
|
+
let slideAxis = null;
|
|
1175
|
+
if (startAxis === endAxis) {
|
|
1176
|
+
if (startAxis === "x") {
|
|
1177
|
+
const mx = s.x + (e.x - s.x) * t;
|
|
1178
|
+
const a = new Vec(mx, s.y);
|
|
1179
|
+
const b = new Vec(mx, e.y);
|
|
1180
|
+
corners = [s, a, b, e];
|
|
1181
|
+
if (Math.abs(e.y - s.y) > EPS2) {
|
|
1182
|
+
midLeg = [a, b];
|
|
1183
|
+
slideAxis = "x";
|
|
1184
|
+
}
|
|
1185
|
+
} else {
|
|
1186
|
+
const my = s.y + (e.y - s.y) * t;
|
|
1187
|
+
const a = new Vec(s.x, my);
|
|
1188
|
+
const b = new Vec(e.x, my);
|
|
1189
|
+
corners = [s, a, b, e];
|
|
1190
|
+
if (Math.abs(e.x - s.x) > EPS2) {
|
|
1191
|
+
midLeg = [a, b];
|
|
1192
|
+
slideAxis = "y";
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
} else {
|
|
1196
|
+
corners = startAxis === "x" ? [s, new Vec(e.x, s.y), e] : [s, new Vec(s.x, e.y), e];
|
|
1197
|
+
}
|
|
1198
|
+
const simplified = simplify(corners);
|
|
1199
|
+
return {
|
|
1200
|
+
corners: simplified,
|
|
1201
|
+
points: roundElbowCorners(simplified, options.cornerRadius ?? 0),
|
|
1202
|
+
midLeg,
|
|
1203
|
+
slideAxis
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function getElbowBody(start, end, options = {}) {
|
|
1207
|
+
return { kind: "elbow", points: getElbowRoute(start, end, options).points };
|
|
1208
|
+
}
|
|
1209
|
+
function getElbowMidPointFromPoint(start, end, point, axis) {
|
|
1210
|
+
const span = end[axis] - start[axis];
|
|
1211
|
+
if (Math.abs(span) < EPS2) return 0.5;
|
|
1212
|
+
return clamp01((point[axis] - start[axis]) / span);
|
|
1213
|
+
}
|
|
1214
|
+
var ARROW_LABEL_PADDING = 8;
|
|
1215
|
+
var LABEL_PADDING = ARROW_LABEL_PADDING;
|
|
1216
|
+
var ARROW_KINDS = ARROW_SHAPE_KINDS;
|
|
1217
|
+
function readArrowShape(shape) {
|
|
1218
|
+
return { ...shape, props: readArrowProps(shape) };
|
|
1219
|
+
}
|
|
1220
|
+
function readArrowProps(shape) {
|
|
1221
|
+
const p = propsOf(shape);
|
|
1222
|
+
return {
|
|
1223
|
+
kind: readEnum(p, "kind", ARROW_KINDS, "arc"),
|
|
1224
|
+
start: readPoint(p, "start", { x: 0, y: 0 }),
|
|
1225
|
+
end: readPoint(p, "end", { x: 2, y: 0 }),
|
|
1226
|
+
bend: readNumber(p, "bend", 0),
|
|
1227
|
+
elbowMidPoint: readNumber(p, "elbowMidPoint", 0.5),
|
|
1228
|
+
color: readStyle(p, "color", DefaultColorStyle),
|
|
1229
|
+
labelColor: readStyle(p, "labelColor", DefaultLabelColorStyle),
|
|
1230
|
+
fill: readStyle(p, "fill", DefaultFillStyle),
|
|
1231
|
+
dash: readStyle(p, "dash", DefaultDashStyle),
|
|
1232
|
+
size: readStyle(p, "size", DefaultSizeStyle),
|
|
1233
|
+
arrowheadStart: readEnum(p, "arrowheadStart", ARROWHEAD_KINDS, "none"),
|
|
1234
|
+
arrowheadEnd: readEnum(p, "arrowheadEnd", ARROWHEAD_KINDS, "arrow"),
|
|
1235
|
+
font: readStyle(p, "font", DefaultFontStyle),
|
|
1236
|
+
richText: readRichText(p),
|
|
1237
|
+
text: readText(p),
|
|
1238
|
+
labelPosition: readNumber(p, "labelPosition", 0.5),
|
|
1239
|
+
scale: readNumber(p, "scale", 1)
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function getArrowDisplayValues(editor, shape, theme, colorMode) {
|
|
1243
|
+
const base = getDefaultDisplayValues(editor, shape, theme, colorMode);
|
|
1244
|
+
const scale = readNumber(propsOf(shape), "scale", 1);
|
|
1245
|
+
return {
|
|
1246
|
+
...base,
|
|
1247
|
+
scaledStrokeWidth: base.strokeWidth * scale,
|
|
1248
|
+
labelFontSize: base.fontSize * scale,
|
|
1249
|
+
labelPadding: ARROW_LABEL_PADDING * scale,
|
|
1250
|
+
labelFontFamily: base.fontFamily
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
var ArrowShapeUtil = class extends ShapeUtil {
|
|
1254
|
+
static type = "arrow";
|
|
1255
|
+
static migrations = arrowShapeMigrations;
|
|
1256
|
+
static options = { getDefaultDisplayValues: getArrowDisplayValues };
|
|
1257
|
+
/**
|
|
1258
|
+
* `kind` is deliberately not among these. A style is shared across shape
|
|
1259
|
+
* types, remembered for the next shape and applied to a whole selection at
|
|
1260
|
+
* once; arc-versus-elbow is routing that belongs to the one arrow, and
|
|
1261
|
+
* declaring it a style would put it in every mixed selection's shared styles
|
|
1262
|
+
* (and in the style panel) with nothing else to share it with.
|
|
1263
|
+
*/
|
|
1264
|
+
static props = arrowShapeProps;
|
|
1265
|
+
getDefaultProps() {
|
|
1266
|
+
return {
|
|
1267
|
+
kind: "arc",
|
|
1268
|
+
start: { x: 0, y: 0 },
|
|
1269
|
+
end: { x: 2, y: 0 },
|
|
1270
|
+
bend: 0,
|
|
1271
|
+
elbowMidPoint: 0.5,
|
|
1272
|
+
color: "black",
|
|
1273
|
+
labelColor: "black",
|
|
1274
|
+
fill: "none",
|
|
1275
|
+
dash: "draw",
|
|
1276
|
+
size: "m",
|
|
1277
|
+
arrowheadStart: "none",
|
|
1278
|
+
arrowheadEnd: "arrow",
|
|
1279
|
+
font: "draw",
|
|
1280
|
+
richText: toRichText(""),
|
|
1281
|
+
text: "",
|
|
1282
|
+
labelPosition: 0.5,
|
|
1283
|
+
scale: 1
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* The arrow's terminals and the body running between them, in arrow-local
|
|
1288
|
+
* space. `"arc"` bows by `bend`; `"elbow"` routes axis-aligned legs, leaving
|
|
1289
|
+
* a bound shape along its nearest edge's normal, with corners rounded in
|
|
1290
|
+
* proportion to the stroke. The elbow's route is handed back as well, for the
|
|
1291
|
+
* midpoint handle.
|
|
1292
|
+
*/
|
|
1293
|
+
resolveBody(shape) {
|
|
1294
|
+
const normalized = readArrowShape(shape);
|
|
1295
|
+
const props = normalized.props;
|
|
1296
|
+
const terminals = getArrowTerminalsInArrowSpace(this.editor, normalized);
|
|
1297
|
+
if (props.kind !== "elbow") {
|
|
1298
|
+
return { props, terminals, body: getArrowBody(terminals.start, terminals.end, props.bend), route: null };
|
|
1299
|
+
}
|
|
1300
|
+
const axes = getBoundElbowAxes(this.editor, normalized, terminals);
|
|
1301
|
+
const route = getElbowRoute(terminals.start, terminals.end, {
|
|
1302
|
+
midPoint: props.elbowMidPoint,
|
|
1303
|
+
startAxis: axes.start,
|
|
1304
|
+
endAxis: axes.end,
|
|
1305
|
+
cornerRadius: STROKE_SIZES[props.size] * props.scale * ELBOW_CORNER_STROKES
|
|
1306
|
+
});
|
|
1307
|
+
return { props, terminals, body: { kind: "elbow", points: route.points }, route };
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* The label's box in shape space, or `null` when there is no label.
|
|
1311
|
+
*
|
|
1312
|
+
* Three things need it and they must agree: the geometry (which is what gets
|
|
1313
|
+
* hit-tested), the DOM element that paints the text, and the indicator that
|
|
1314
|
+
* outlines it on selection. They were computing it separately.
|
|
1315
|
+
*/
|
|
1316
|
+
labelBox(shape) {
|
|
1317
|
+
const { text, labelPosition, font, size, scale } = readArrowProps(shape);
|
|
1318
|
+
if (!text) return null;
|
|
1319
|
+
const m = measureLabel(readRichText(shape.props), {
|
|
1320
|
+
fontFamily: font,
|
|
1321
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
1322
|
+
padding: LABEL_PADDING * scale,
|
|
1323
|
+
editor: this.editor
|
|
1324
|
+
});
|
|
1325
|
+
const c = getPointOnBody(this.resolveBody(shape).body, Math.max(0, Math.min(1, labelPosition)));
|
|
1326
|
+
return { x: c.x - m.w / 2, y: c.y - m.h / 2, w: m.w, h: m.h };
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* `gapAtLabel` splits the body around the label's box. Only the selection
|
|
1330
|
+
* outline asks for it; the geometry the editor hit-tests keeps the body whole,
|
|
1331
|
+
* so the stroke the label covers is still there to be clicked.
|
|
1332
|
+
*/
|
|
1333
|
+
getGeometry(shape, opts) {
|
|
1334
|
+
const { props, terminals, body: full } = this.resolveBody(shape);
|
|
1335
|
+
const { arrowheadStart, arrowheadEnd, size, scale, text, labelPosition, font } = props;
|
|
1336
|
+
const { start, end } = terminals;
|
|
1337
|
+
const strokeWidth = STROKE_SIZES[size] * scale;
|
|
1338
|
+
const length = getBodyLength(full);
|
|
1339
|
+
const headLength = getArrowheadLength(strokeWidth, length);
|
|
1340
|
+
const body = shortenBody(full, getArrowheadInset(arrowheadStart, headLength), getArrowheadInset(arrowheadEnd, headLength));
|
|
1341
|
+
const children = [];
|
|
1342
|
+
const gap = opts?.gapAtLabel ? labelGapOnBody(body, opts.gapAtLabel) : null;
|
|
1343
|
+
if (gap) {
|
|
1344
|
+
const len = getBodyLength(body);
|
|
1345
|
+
if (gap.from > 0) children.push(bodyToGeometry(shortenBody(body, 0, len * (1 - gap.from))));
|
|
1346
|
+
if (gap.to < 1) children.push(bodyToGeometry(shortenBody(body, len * gap.to, 0)));
|
|
1347
|
+
} else {
|
|
1348
|
+
children.push(bodyToGeometry(body));
|
|
1349
|
+
}
|
|
1350
|
+
const startHead = getArrowheadGeometry(arrowheadStart, start, Vec.Mul(getTangentOnBody(full, 0), -1), headLength);
|
|
1351
|
+
if (startHead) children.push(startHead);
|
|
1352
|
+
const endHead = getArrowheadGeometry(arrowheadEnd, end, getTangentOnBody(full, 1), headLength);
|
|
1353
|
+
if (endHead) children.push(endHead);
|
|
1354
|
+
const label = this.labelBox(shape);
|
|
1355
|
+
if (label) {
|
|
1356
|
+
children.push(new Rectangle2d({ x: label.x, y: label.y, width: label.w, height: label.h, isFilled: false, isLabel: true }));
|
|
1357
|
+
}
|
|
1358
|
+
return new Group2d({ children });
|
|
1359
|
+
}
|
|
1360
|
+
/** An arrow needs its family's faces only while it carries a label. */
|
|
1361
|
+
getFontFaces(shape) {
|
|
1362
|
+
return readText(shape.props) ? getLabelFontFaces(readArrowProps(shape).font) : [];
|
|
1363
|
+
}
|
|
1364
|
+
getRenderStyle(shape) {
|
|
1365
|
+
const { color, dash, size, scale } = readArrowProps(shape);
|
|
1366
|
+
const stroke = getStrokeRgba(color, getThemeColors(this.editor));
|
|
1367
|
+
return { stroke, strokeWidth: STROKE_SIZES[size] * scale, fill: stroke, dash: getDashId(dash), opacity: 1 };
|
|
1368
|
+
}
|
|
1369
|
+
component(shape) {
|
|
1370
|
+
const { richText, text, font, size, scale, labelPosition } = readArrowProps(shape);
|
|
1371
|
+
const isEditing = this.editor.getEditingShapeId() === shape.id;
|
|
1372
|
+
if (!text && !isEditing) return null;
|
|
1373
|
+
const display = getDisplayValues(this, shape);
|
|
1374
|
+
const box = this.labelBox(shape) ?? { x: 0, y: 0, w: 0, h: 0 };
|
|
1375
|
+
const background = getThemeColors(this.editor).background;
|
|
1376
|
+
return /* @__PURE__ */ jsx(
|
|
1377
|
+
"div",
|
|
1378
|
+
{
|
|
1379
|
+
style: {
|
|
1380
|
+
position: "absolute",
|
|
1381
|
+
left: box.x,
|
|
1382
|
+
top: box.y,
|
|
1383
|
+
width: box.w,
|
|
1384
|
+
height: box.h,
|
|
1385
|
+
background,
|
|
1386
|
+
borderRadius: LABEL_PADDING * scale * 0.5,
|
|
1387
|
+
pointerEvents: "none"
|
|
1388
|
+
},
|
|
1389
|
+
children: /* @__PURE__ */ jsx(
|
|
1390
|
+
"div",
|
|
1391
|
+
{
|
|
1392
|
+
style: {
|
|
1393
|
+
position: "relative",
|
|
1394
|
+
width: "max-content",
|
|
1395
|
+
transform: `translateY(${-getLabelOpticalLift({ fontFamily: font, fontSize: FONT_SIZES[size] * scale })}px)`
|
|
1396
|
+
},
|
|
1397
|
+
children: /* @__PURE__ */ jsx(
|
|
1398
|
+
TextLabel,
|
|
1399
|
+
{
|
|
1400
|
+
shape,
|
|
1401
|
+
text,
|
|
1402
|
+
richText,
|
|
1403
|
+
isEditing,
|
|
1404
|
+
fontFamily: font,
|
|
1405
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
1406
|
+
color: display.labelColor,
|
|
1407
|
+
textAlign: "middle",
|
|
1408
|
+
verticalAlign: "middle",
|
|
1409
|
+
wrap: false,
|
|
1410
|
+
padding: LABEL_PADDING * scale,
|
|
1411
|
+
onChange: (next) => this.editor.updateShape({
|
|
1412
|
+
id: shape.id,
|
|
1413
|
+
type: "arrow",
|
|
1414
|
+
props: { text: next, richText: applyPlainTextToRichText(richText, next) }
|
|
1415
|
+
}),
|
|
1416
|
+
onChangeRichText: (next) => this.editor.updateShape({
|
|
1417
|
+
id: shape.id,
|
|
1418
|
+
type: "arrow",
|
|
1419
|
+
props: { richText: next, text: richTextToText(next) }
|
|
1420
|
+
})
|
|
1421
|
+
}
|
|
1422
|
+
)
|
|
1423
|
+
}
|
|
1424
|
+
)
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
getIndicatorPath(shape) {
|
|
1429
|
+
const label = this.labelBox(shape);
|
|
1430
|
+
const labelRadius = () => Math.min(LABEL_PADDING * readArrowProps(shape).scale * 0.5, label.w / 2, label.h / 2);
|
|
1431
|
+
if (label && this.editor.getEditingShapeId() === shape.id) {
|
|
1432
|
+
const p2 = new Path2D();
|
|
1433
|
+
p2.roundRect(label.x, label.y, label.w, label.h, labelRadius());
|
|
1434
|
+
return p2;
|
|
1435
|
+
}
|
|
1436
|
+
const words = label ? this.getGeometry(shape, { gapAtLabel: label }).toPathWords() : this.getGeometry(shape).toPathWords();
|
|
1437
|
+
const p = svgPath(pathWordsToSvgD(words));
|
|
1438
|
+
if (label) p.roundRect(label.x, label.y, label.w, label.h, labelRadius());
|
|
1439
|
+
return p;
|
|
1440
|
+
}
|
|
1441
|
+
/** The GPU keeps drawing the arrow while its label is edited. */
|
|
1442
|
+
needsOverlay(_shape) {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
hasOverlayLabel(shape) {
|
|
1446
|
+
return readText(shape.props).trim().length > 0 || this.editor.getEditingShapeId() === shape.id;
|
|
1447
|
+
}
|
|
1448
|
+
canEdit(_shape) {
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
/** Nothing binds to an arrow (no arrow-to-arrow bindings). */
|
|
1452
|
+
canBind(_opts) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
/** A selected arrow shows its handles instead of a selection box. */
|
|
1456
|
+
hideSelectionBoundsBg(_shape) {
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* …but the arrow's *own* outline is drawn. This flag suppresses a lone selected
|
|
1461
|
+
* shape's indicator, and the arrow used to set it because the selection frame
|
|
1462
|
+
* was the cue instead. With the frame gone there was no cue at all — a selected
|
|
1463
|
+
* arrow showed nothing. Its indicator is the curve and the label's box, which
|
|
1464
|
+
* is the cue worth having.
|
|
1465
|
+
*/
|
|
1466
|
+
hideSelectionBoundsFg(_shape) {
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
hideResizeHandles(_shape) {
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* …and no rotate handle either, which is what actually suppresses the box.
|
|
1474
|
+
* `selectionHandles` only drops the frame when resize *and* rotate are both
|
|
1475
|
+
* hidden, so hiding one of the two left a selected arrow wrapped in a
|
|
1476
|
+
* rectangle with a lone rotate dot above it. An arrow has nothing to rotate
|
|
1477
|
+
* about: its ends are the two handles, and turning it means moving them.
|
|
1478
|
+
*/
|
|
1479
|
+
hideRotateHandle(_shape) {
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
getText(shape) {
|
|
1483
|
+
return readText(shape.props);
|
|
1484
|
+
}
|
|
1485
|
+
onEditEnd(shape) {
|
|
1486
|
+
const text = readText(shape.props);
|
|
1487
|
+
const trimmed = trimTrailingWhitespace(text);
|
|
1488
|
+
if (trimmed === text) return;
|
|
1489
|
+
this.editor.updateShape({
|
|
1490
|
+
id: shape.id,
|
|
1491
|
+
type: "arrow",
|
|
1492
|
+
props: { text: trimmed, richText: applyPlainTextToRichText(readRichText(shape.props), trimmed) }
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Start and end handles move (and bind) the terminals. Between them sits one
|
|
1497
|
+
* virtual handle: an arc's `bend` handle rides the middle of the curve, while
|
|
1498
|
+
* an elbow's `midpoint` handle sits on its middle leg and slides that leg.
|
|
1499
|
+
* An elbow with no middle leg (an L route, or a straight run) has neither.
|
|
1500
|
+
*/
|
|
1501
|
+
getHandles(shape) {
|
|
1502
|
+
const { terminals, body, route } = this.resolveBody(shape);
|
|
1503
|
+
const { start, end } = terminals;
|
|
1504
|
+
const handles = [{ id: "start", type: "vertex", index: "a1", x: start.x, y: start.y }];
|
|
1505
|
+
if (route) {
|
|
1506
|
+
if (route.midLeg) {
|
|
1507
|
+
const mid = Vec.Lrp(route.midLeg[0], route.midLeg[1], 0.5);
|
|
1508
|
+
handles.push({ id: "midpoint", type: "vertex", index: "a2", x: mid.x, y: mid.y });
|
|
1509
|
+
}
|
|
1510
|
+
} else {
|
|
1511
|
+
const mid = getPointOnBody(body, 0.5);
|
|
1512
|
+
handles.push({ id: "bend", type: "vertex", index: "a2", x: mid.x, y: mid.y });
|
|
1513
|
+
}
|
|
1514
|
+
handles.push({ id: "end", type: "vertex", index: "a3", x: end.x, y: end.y });
|
|
1515
|
+
return handles;
|
|
1516
|
+
}
|
|
1517
|
+
onHandleDrag(shape, info) {
|
|
1518
|
+
const { handle } = info;
|
|
1519
|
+
switch (handle.id) {
|
|
1520
|
+
case "start":
|
|
1521
|
+
case "end":
|
|
1522
|
+
return this.dragTerminal(shape, handle.id, handle, info.isPrecise);
|
|
1523
|
+
case "bend": {
|
|
1524
|
+
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, readArrowShape(shape));
|
|
1525
|
+
return { props: { ...shape.props, bend: getBendFromPoint(start, end, handle) } };
|
|
1526
|
+
}
|
|
1527
|
+
case "midpoint": {
|
|
1528
|
+
const { terminals, route } = this.resolveBody(shape);
|
|
1529
|
+
if (!route?.slideAxis) return;
|
|
1530
|
+
const elbowMidPoint = getElbowMidPointFromPoint(terminals.start, terminals.end, handle, route.slideAxis);
|
|
1531
|
+
return { props: { ...shape.props, elbowMidPoint } };
|
|
1532
|
+
}
|
|
1533
|
+
default:
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Move a terminal handle. When the handle lands on a bindable shape the
|
|
1539
|
+
* terminal is bound to it (centered, or at the precise point under the
|
|
1540
|
+
* pointer); otherwise any existing binding is dropped. The static point is
|
|
1541
|
+
* always written so the arrow renders sensibly if the binding goes away.
|
|
1542
|
+
* Holding Ctrl suppresses binding. Targets are found by hit-testing shape
|
|
1543
|
+
* geometry directly (not `editor.getShapeAtPoint`) because the engine treats
|
|
1544
|
+
* unfilled shapes as hollow, and arrows must bind into hollow shapes too.
|
|
1545
|
+
*/
|
|
1546
|
+
dragTerminal(shape, terminal, point, isPrecise) {
|
|
1547
|
+
const editor = this.editor;
|
|
1548
|
+
const local = { x: point.x, y: point.y };
|
|
1549
|
+
const pagePoint = applyTransform(editor.getShapePageTransform(shape), local);
|
|
1550
|
+
const existing = getArrowBindings(editor, shape)[terminal];
|
|
1551
|
+
const target = editor.inputs.ctrlKey ? void 0 : getArrowBindingTargetAtPoint(editor, shape, pagePoint);
|
|
1552
|
+
if (target) {
|
|
1553
|
+
const bindingProps = {
|
|
1554
|
+
terminal,
|
|
1555
|
+
normalizedAnchor: isPrecise ? getNormalizedAnchor(editor, target, pagePoint).toJson() : { x: 0.5, y: 0.5 },
|
|
1556
|
+
isPrecise,
|
|
1557
|
+
isExact: false
|
|
1558
|
+
};
|
|
1559
|
+
if (existing && existing.toId === target.id) {
|
|
1560
|
+
editor.updateBinding({ id: existing.id, type: "arrow", props: bindingProps });
|
|
1561
|
+
} else {
|
|
1562
|
+
if (existing) editor.deleteBinding(existing.id);
|
|
1563
|
+
editor.createBinding({ type: "arrow", fromId: shape.id, toId: target.id, props: bindingProps });
|
|
1564
|
+
}
|
|
1565
|
+
} else if (existing) {
|
|
1566
|
+
editor.deleteBinding(existing.id);
|
|
1567
|
+
}
|
|
1568
|
+
return { props: { ...shape.props, [terminal]: local } };
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Dragging the arrow on its own detaches it: bindings to shapes that are not
|
|
1572
|
+
* part of the selection are dropped and their terminals frozen in place.
|
|
1573
|
+
* Bindings to shapes moving along with the arrow are kept.
|
|
1574
|
+
*/
|
|
1575
|
+
onTranslateStart(shape) {
|
|
1576
|
+
const editor = this.editor;
|
|
1577
|
+
const selected = new Set(editor.getSelectedShapeIds());
|
|
1578
|
+
const stale = editor.getBindingsFromShape(shape, "arrow").filter((b) => !selected.has(b.toId));
|
|
1579
|
+
if (stale.length) editor.deleteBindings(stale, { isolateShapes: true });
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
function getFrameDisplayValues(editor, shape, theme, colorMode) {
|
|
1583
|
+
return {
|
|
1584
|
+
...getDefaultDisplayValues(editor, shape, theme, colorMode),
|
|
1585
|
+
frameFill: FRAME_FILL,
|
|
1586
|
+
frameStroke: FRAME_STROKE,
|
|
1587
|
+
frameStrokeWidth: FRAME_STROKE_WIDTH,
|
|
1588
|
+
nameFontSize: FRAME_NAME_FONT_SIZE,
|
|
1589
|
+
nameColor: FRAME_NAME_COLOR,
|
|
1590
|
+
nameHeight: FRAME_NAME_HEIGHT
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
var FrameShapeUtil = class extends BaseFrameLikeShapeUtil {
|
|
1594
|
+
static type = "frame";
|
|
1595
|
+
static props = frameShapeProps;
|
|
1596
|
+
static migrations = frameShapeMigrations;
|
|
1597
|
+
static options = { getDefaultDisplayValues: getFrameDisplayValues };
|
|
1598
|
+
getDefaultProps() {
|
|
1599
|
+
return { w: 160, h: 90, name: "" };
|
|
1600
|
+
}
|
|
1601
|
+
getGeometry(shape) {
|
|
1602
|
+
const p = propsOf(shape);
|
|
1603
|
+
return new Rectangle2d({ width: readNumber(p, "w", 160), height: readNumber(p, "h", 90), isFilled: true });
|
|
1604
|
+
}
|
|
1605
|
+
getRenderStyle(_shape) {
|
|
1606
|
+
return { fill: hexToRgba(FRAME_FILL), stroke: hexToRgba(FRAME_STROKE), strokeWidth: FRAME_STROKE_WIDTH, dash: 0, opacity: 1 };
|
|
1607
|
+
}
|
|
1608
|
+
component(shape) {
|
|
1609
|
+
const p = propsOf(shape);
|
|
1610
|
+
const name = readString(p, "name", "");
|
|
1611
|
+
const w = readNumber(p, "w", 160);
|
|
1612
|
+
return /* @__PURE__ */ jsx(
|
|
1613
|
+
"div",
|
|
1614
|
+
{
|
|
1615
|
+
style: {
|
|
1616
|
+
position: "absolute",
|
|
1617
|
+
top: -FRAME_NAME_OFFSET,
|
|
1618
|
+
left: 0,
|
|
1619
|
+
width: w,
|
|
1620
|
+
height: FRAME_NAME_HEIGHT,
|
|
1621
|
+
overflow: "hidden",
|
|
1622
|
+
textOverflow: "ellipsis",
|
|
1623
|
+
pointerEvents: "none"
|
|
1624
|
+
},
|
|
1625
|
+
children: /* @__PURE__ */ jsx(
|
|
1626
|
+
TextLabel,
|
|
1627
|
+
{
|
|
1628
|
+
shape,
|
|
1629
|
+
text: name,
|
|
1630
|
+
isEditing: this.editor.getEditingShapeId() === shape.id,
|
|
1631
|
+
fontFamily: "sans",
|
|
1632
|
+
fontSize: FRAME_NAME_FONT_SIZE,
|
|
1633
|
+
color: FRAME_NAME_COLOR,
|
|
1634
|
+
textAlign: "start",
|
|
1635
|
+
verticalAlign: "end",
|
|
1636
|
+
wrap: false,
|
|
1637
|
+
width: w,
|
|
1638
|
+
height: FRAME_NAME_HEIGHT,
|
|
1639
|
+
placeholder: "Frame",
|
|
1640
|
+
singleLine: true,
|
|
1641
|
+
onChange: (next) => this.editor.updateShape({ id: shape.id, type: "frame", props: { name: next } })
|
|
1642
|
+
}
|
|
1643
|
+
)
|
|
1644
|
+
}
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
getIndicatorPath(shape) {
|
|
1648
|
+
const p = propsOf(shape);
|
|
1649
|
+
return rectPath(readNumber(p, "w", 160), readNumber(p, "h", 90));
|
|
1650
|
+
}
|
|
1651
|
+
/** The GPU draws the frame body while its name is edited. */
|
|
1652
|
+
needsOverlay(_shape) {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
hasOverlayLabel(_shape) {
|
|
1656
|
+
return true;
|
|
1657
|
+
}
|
|
1658
|
+
/** A frame draws its name in the sans family, whatever its children use. */
|
|
1659
|
+
getFontFaces(_shape) {
|
|
1660
|
+
return getLabelFontFaces("sans");
|
|
1661
|
+
}
|
|
1662
|
+
canEdit(_shape) {
|
|
1663
|
+
return true;
|
|
1664
|
+
}
|
|
1665
|
+
getText(shape) {
|
|
1666
|
+
return readString(propsOf(shape), "name", "");
|
|
1667
|
+
}
|
|
1668
|
+
onEditEnd(shape) {
|
|
1669
|
+
const name = readString(propsOf(shape), "name", "");
|
|
1670
|
+
const trimmed = name.trim();
|
|
1671
|
+
if (trimmed !== name) this.editor.updateShape({ id: shape.id, type: "frame", props: { name: trimmed } });
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
var BLOBBY_KINDS = /* @__PURE__ */ new Set(["ellipse", "oval", "cloud", "heart"]);
|
|
1675
|
+
var DEFAULT_GEO_TYPE_DEFINITIONS = Object.freeze(
|
|
1676
|
+
Object.fromEntries(
|
|
1677
|
+
GEO_SHAPE_KINDS.map((kind) => [
|
|
1678
|
+
kind,
|
|
1679
|
+
{
|
|
1680
|
+
id: kind,
|
|
1681
|
+
getPath: (w, h, opts) => getGeoGeometry(kind, w, h, opts?.isFilled ?? false, { flipX: opts?.flipX, flipY: opts?.flipY }, opts?.strokeWidth ?? 0),
|
|
1682
|
+
snapType: BLOBBY_KINDS.has(kind) ? "blobby" : "polygon",
|
|
1683
|
+
// The icon set names its geo icons after the kind itself.
|
|
1684
|
+
icon: `geo-${kind}`
|
|
1685
|
+
}
|
|
1686
|
+
])
|
|
1687
|
+
)
|
|
1688
|
+
);
|
|
1689
|
+
function getGeoTypeDefinition(geo, customGeoTypes) {
|
|
1690
|
+
if (typeof geo !== "string" || geo.length === 0) return void 0;
|
|
1691
|
+
const custom = customGeoTypes?.[geo];
|
|
1692
|
+
if (custom !== void 0) return custom;
|
|
1693
|
+
return DEFAULT_GEO_TYPE_DEFINITIONS[geo];
|
|
1694
|
+
}
|
|
1695
|
+
var GEO_DEFAULT_SIZE = { w: 100, h: 100 };
|
|
1696
|
+
function getGeoDisplayValues(editor, shape, theme, colorMode) {
|
|
1697
|
+
const base = getDefaultDisplayValues(editor, shape, theme, colorMode);
|
|
1698
|
+
const scale = readNumber(propsOf(shape), "scale", 1);
|
|
1699
|
+
return {
|
|
1700
|
+
...base,
|
|
1701
|
+
labelFontSize: base.fontSize * scale,
|
|
1702
|
+
labelPadding: GEO_LABEL_PADDING * scale,
|
|
1703
|
+
labelFontFamily: base.fontFamily,
|
|
1704
|
+
scaledStrokeWidth: base.strokeWidth * scale
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
var GEO_LABEL_PADDING = 16;
|
|
1708
|
+
var LABEL_PADDING2 = GEO_LABEL_PADDING;
|
|
1709
|
+
function horizontalAlignToFlex(align) {
|
|
1710
|
+
return alignToJustify(align);
|
|
1711
|
+
}
|
|
1712
|
+
function horizontalAlignToTextAlign(align) {
|
|
1713
|
+
return alignToTextAlign(align);
|
|
1714
|
+
}
|
|
1715
|
+
function verticalAlignToFlex(align) {
|
|
1716
|
+
return verticalAlignToAlignItems(align);
|
|
1717
|
+
}
|
|
1718
|
+
function readGeoProps(shape) {
|
|
1719
|
+
const p = propsOf(shape);
|
|
1720
|
+
return {
|
|
1721
|
+
geo: readStyle(p, "geo", GeoShapeGeoStyle),
|
|
1722
|
+
w: readNumber(p, "w", 100),
|
|
1723
|
+
h: readNumber(p, "h", 100),
|
|
1724
|
+
color: readStyle(p, "color", DefaultColorStyle),
|
|
1725
|
+
labelColor: readStyle(p, "labelColor", DefaultLabelColorStyle),
|
|
1726
|
+
fill: readStyle(p, "fill", DefaultFillStyle),
|
|
1727
|
+
dash: readStyle(p, "dash", DefaultDashStyle),
|
|
1728
|
+
size: readStyle(p, "size", DefaultSizeStyle),
|
|
1729
|
+
font: readStyle(p, "font", DefaultFontStyle),
|
|
1730
|
+
align: readStyle(p, "align", DefaultHorizontalAlignStyle),
|
|
1731
|
+
verticalAlign: readStyle(p, "verticalAlign", DefaultVerticalAlignStyle),
|
|
1732
|
+
growY: readNumber(p, "growY", 0),
|
|
1733
|
+
url: readString(p, "url", ""),
|
|
1734
|
+
richText: readRichText(p),
|
|
1735
|
+
text: readText(p),
|
|
1736
|
+
scale: readNumber(p, "scale", 1),
|
|
1737
|
+
flipX: readBoolean(p, "flipX", false),
|
|
1738
|
+
flipY: readBoolean(p, "flipY", false)
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function measureGeoLabel(props, editor) {
|
|
1742
|
+
const richText = readRichText(props);
|
|
1743
|
+
const font = readStyle(props, "font", DefaultFontStyle);
|
|
1744
|
+
const size = readStyle(props, "size", DefaultSizeStyle);
|
|
1745
|
+
const scale = readNumber(props, "scale", 1);
|
|
1746
|
+
const w = readNumber(props, "w", 100);
|
|
1747
|
+
return measureLabel(richText, {
|
|
1748
|
+
fontFamily: font,
|
|
1749
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
1750
|
+
maxWidth: Math.max(1, w),
|
|
1751
|
+
padding: LABEL_PADDING2 * scale,
|
|
1752
|
+
editor: editor ?? null
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
function getGeoGrowY(props, editor) {
|
|
1756
|
+
if (!readText(props)) return 0;
|
|
1757
|
+
return computeGrowY(measureGeoLabel(props, editor).h, readNumber(props, "h", 100));
|
|
1758
|
+
}
|
|
1759
|
+
var LABEL_KEYS = ["richText", "text", "font", "size", "scale", "w", "h"];
|
|
1760
|
+
var geoShapeVersions = createBuiltInShapePropsMigrationIds("geo", {
|
|
1761
|
+
AddFlipProps: 1
|
|
1762
|
+
});
|
|
1763
|
+
var geoShapeMigrations = createShapePropsMigrationSequence({
|
|
1764
|
+
sequence: [
|
|
1765
|
+
{
|
|
1766
|
+
id: geoShapeVersions.AddFlipProps,
|
|
1767
|
+
up(props) {
|
|
1768
|
+
props["flipX"] ??= false;
|
|
1769
|
+
props["flipY"] ??= false;
|
|
1770
|
+
},
|
|
1771
|
+
down(props) {
|
|
1772
|
+
delete props["flipX"];
|
|
1773
|
+
delete props["flipY"];
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
]
|
|
1777
|
+
});
|
|
1778
|
+
var GeoShapeUtil = class extends BaseBoxShapeUtil {
|
|
1779
|
+
static type = "geo";
|
|
1780
|
+
static migrations = geoShapeMigrations;
|
|
1781
|
+
static options = { getDefaultDisplayValues: getGeoDisplayValues };
|
|
1782
|
+
static props = geoShapeProps;
|
|
1783
|
+
getDefaultProps() {
|
|
1784
|
+
return {
|
|
1785
|
+
geo: "rectangle",
|
|
1786
|
+
w: 100,
|
|
1787
|
+
h: 100,
|
|
1788
|
+
color: "black",
|
|
1789
|
+
labelColor: "black",
|
|
1790
|
+
fill: "none",
|
|
1791
|
+
dash: "draw",
|
|
1792
|
+
size: "m",
|
|
1793
|
+
font: "draw",
|
|
1794
|
+
align: "middle",
|
|
1795
|
+
verticalAlign: "middle",
|
|
1796
|
+
growY: 0,
|
|
1797
|
+
url: "",
|
|
1798
|
+
richText: toRichText(""),
|
|
1799
|
+
text: "",
|
|
1800
|
+
scale: 1,
|
|
1801
|
+
flipX: false,
|
|
1802
|
+
flipY: false
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* The silhouette for `geo`, taking this util's `customGeoTypes` into account.
|
|
1807
|
+
* Falls back to the rectangle so an unknown value still draws something.
|
|
1808
|
+
*/
|
|
1809
|
+
getGeoTypeDefinition(geo) {
|
|
1810
|
+
return getGeoTypeDefinition(geo, this.options.customGeoTypes) ?? getGeoTypeDefinition("rectangle", this.options.customGeoTypes);
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* The `geo` value this util will actually draw.
|
|
1814
|
+
*
|
|
1815
|
+
* `props.geo` is a *style* prop, and its validator only knows the built-in
|
|
1816
|
+
* silhouettes — so reading it through the style would turn every custom geo
|
|
1817
|
+
* type back into a rectangle before it ever reached the table. A stored value
|
|
1818
|
+
* this util has a definition for is therefore kept as it is; anything else
|
|
1819
|
+
* falls back to the validated style, which is what keeps a value from another
|
|
1820
|
+
* editor's vocabulary drawing something rather than nothing.
|
|
1821
|
+
*/
|
|
1822
|
+
getGeoValue(shape) {
|
|
1823
|
+
const p = propsOf(shape);
|
|
1824
|
+
const raw = readString(p, "geo", "");
|
|
1825
|
+
if (raw !== "" && getGeoTypeDefinition(raw, this.options.customGeoTypes) !== void 0) return raw;
|
|
1826
|
+
return readStyle(p, "geo", GeoShapeGeoStyle);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* The engine has a generator for every built-in geo kind, so a built-in
|
|
1830
|
+
* travels as `(kind, w, h, flips)` rather than as its vertices. A custom geo
|
|
1831
|
+
* type returns `undefined` and keeps uploading whatever its `getPath` builds.
|
|
1832
|
+
*/
|
|
1833
|
+
getEngineGeometry(shape) {
|
|
1834
|
+
const geo = this.getGeoValue(shape);
|
|
1835
|
+
if (this.options.customGeoTypes?.[geo]) return void 0;
|
|
1836
|
+
const kind = GEO_KIND[geo];
|
|
1837
|
+
if (kind === void 0) return void 0;
|
|
1838
|
+
const { w, h, growY, fill, flipX, flipY, size, scale } = readGeoProps(shape);
|
|
1839
|
+
return {
|
|
1840
|
+
type: "geo",
|
|
1841
|
+
kind,
|
|
1842
|
+
w,
|
|
1843
|
+
h: h + growY,
|
|
1844
|
+
isClosed: true,
|
|
1845
|
+
isFilled: fill !== "none",
|
|
1846
|
+
flipX,
|
|
1847
|
+
flipY,
|
|
1848
|
+
// Only the marks inside an outline use it — the X of an x-box, whose ends
|
|
1849
|
+
// sit on the corners. `getGeometry` has always shortened them by this; the
|
|
1850
|
+
// engine draws its own copy of the path and needs to be told the same.
|
|
1851
|
+
strokeWidth: STROKE_SIZES[size] * scale
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
getGeometry(shape) {
|
|
1855
|
+
const props = readGeoProps(shape);
|
|
1856
|
+
const { w, h, growY, fill, text, flipX, flipY, size, scale } = props;
|
|
1857
|
+
const height = h + growY;
|
|
1858
|
+
const body = this.getGeoTypeDefinition(this.getGeoValue(shape)).getPath(w, height, {
|
|
1859
|
+
isFilled: fill !== "none",
|
|
1860
|
+
flipX,
|
|
1861
|
+
flipY,
|
|
1862
|
+
strokeWidth: STROKE_SIZES[size] * scale
|
|
1863
|
+
});
|
|
1864
|
+
if (!text) return body;
|
|
1865
|
+
return new Group2d({ children: [body, this.getLabelRect(props)] });
|
|
1866
|
+
}
|
|
1867
|
+
/** Where the text label sits inside the body, in shape-local space. */
|
|
1868
|
+
getLabelRect(props) {
|
|
1869
|
+
const { w, h, growY, align, verticalAlign } = props;
|
|
1870
|
+
const height = h + growY;
|
|
1871
|
+
const m = measureGeoLabel(props);
|
|
1872
|
+
const lw = Math.min(w, m.w);
|
|
1873
|
+
const lh = Math.min(height, m.h);
|
|
1874
|
+
const x = align === "start" || align === "start-legacy" ? 0 : align === "end" || align === "end-legacy" ? w - lw : (w - lw) / 2;
|
|
1875
|
+
const y = verticalAlign === "start" ? 0 : verticalAlign === "end" ? height - lh : (height - lh) / 2;
|
|
1876
|
+
return new Rectangle2d({ x, y, width: lw, height: lh, isFilled: false, isLabel: true });
|
|
1877
|
+
}
|
|
1878
|
+
getRenderStyle(shape) {
|
|
1879
|
+
const { color, fill, dash, size, scale } = readGeoProps(shape);
|
|
1880
|
+
const colors = getThemeColors(this.editor);
|
|
1881
|
+
return {
|
|
1882
|
+
stroke: getStrokeRgba(color, colors),
|
|
1883
|
+
strokeWidth: STROKE_SIZES[size] * scale,
|
|
1884
|
+
fill: getFillRgba(color, fill, colors),
|
|
1885
|
+
dash: getDashId(dash),
|
|
1886
|
+
opacity: 1
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
/** A geo shape needs its family's faces only while it carries a label. */
|
|
1890
|
+
getFontFaces(shape) {
|
|
1891
|
+
return readText(shape.props) ? getLabelFontFaces(readGeoProps(shape).font) : [];
|
|
1892
|
+
}
|
|
1893
|
+
component(shape) {
|
|
1894
|
+
const { richText, text, font, size, scale, align, verticalAlign, w, h, growY } = readGeoProps(shape);
|
|
1895
|
+
const isEditing = this.editor.getEditingShapeId() === shape.id;
|
|
1896
|
+
if (!text && !isEditing) return null;
|
|
1897
|
+
const display = getDisplayValues(this, shape);
|
|
1898
|
+
return /* @__PURE__ */ jsx(
|
|
1899
|
+
TextLabel,
|
|
1900
|
+
{
|
|
1901
|
+
shape,
|
|
1902
|
+
text,
|
|
1903
|
+
richText,
|
|
1904
|
+
isEditing,
|
|
1905
|
+
fontFamily: font,
|
|
1906
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
1907
|
+
color: display.labelColor,
|
|
1908
|
+
textAlign: align,
|
|
1909
|
+
verticalAlign,
|
|
1910
|
+
wrap: true,
|
|
1911
|
+
width: w,
|
|
1912
|
+
height: h + growY,
|
|
1913
|
+
padding: LABEL_PADDING2 * scale,
|
|
1914
|
+
onChange: (next) => this.editor.updateShape({
|
|
1915
|
+
id: shape.id,
|
|
1916
|
+
type: "geo",
|
|
1917
|
+
props: { text: next, richText: applyPlainTextToRichText(richText, next) }
|
|
1918
|
+
}),
|
|
1919
|
+
onChangeRichText: (next) => this.editor.updateShape({
|
|
1920
|
+
id: shape.id,
|
|
1921
|
+
type: "geo",
|
|
1922
|
+
props: { richText: next, text: richTextToText(next) }
|
|
1923
|
+
})
|
|
1924
|
+
}
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
getIndicatorPath(shape) {
|
|
1928
|
+
return svgPath(pathWordsToSvgD(this.getGeometry(shape).toPathWords()));
|
|
1929
|
+
}
|
|
1930
|
+
/** The GPU keeps drawing the body while editing; only the label lives in the DOM. */
|
|
1931
|
+
needsOverlay(_shape) {
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
hasOverlayLabel(shape) {
|
|
1935
|
+
return readText(shape.props).trim().length > 0 || this.editor.getEditingShapeId() === shape.id;
|
|
1936
|
+
}
|
|
1937
|
+
canEdit(_shape) {
|
|
1938
|
+
return true;
|
|
1939
|
+
}
|
|
1940
|
+
getText(shape) {
|
|
1941
|
+
return readText(shape.props);
|
|
1942
|
+
}
|
|
1943
|
+
onBeforeCreate(next) {
|
|
1944
|
+
const growY = getGeoGrowY(next.props, this.editor);
|
|
1945
|
+
if (growY !== next.props.growY) return { ...next, props: { ...next.props, growY } };
|
|
1946
|
+
}
|
|
1947
|
+
onBeforeUpdate(prev, next) {
|
|
1948
|
+
if (!LABEL_KEYS.some((k) => propsOf(prev)[k] !== propsOf(next)[k])) return;
|
|
1949
|
+
const growY = getGeoGrowY(next.props, this.editor);
|
|
1950
|
+
if (growY !== next.props.growY) return { ...next, props: { ...next.props, growY } };
|
|
1951
|
+
}
|
|
1952
|
+
onEditEnd(shape) {
|
|
1953
|
+
const text = readText(shape.props);
|
|
1954
|
+
const trimmed = trimTrailingWhitespace(text);
|
|
1955
|
+
if (trimmed === text) return;
|
|
1956
|
+
this.editor.updateShape({
|
|
1957
|
+
id: shape.id,
|
|
1958
|
+
type: "geo",
|
|
1959
|
+
props: { text: trimmed, richText: applyPlainTextToRichText(readRichText(shape.props), trimmed) }
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
var NOTE_SIZE = 200;
|
|
1964
|
+
var NOTE_PADDING = 16;
|
|
1965
|
+
function readNoteProps(shape) {
|
|
1966
|
+
const p = propsOf(shape);
|
|
1967
|
+
return {
|
|
1968
|
+
color: readStyle(p, "color", DefaultColorStyle),
|
|
1969
|
+
labelColor: readStyle(p, "labelColor", DefaultLabelColorStyle),
|
|
1970
|
+
size: readStyle(p, "size", DefaultSizeStyle),
|
|
1971
|
+
font: readStyle(p, "font", DefaultFontStyle),
|
|
1972
|
+
fontSizeAdjustment: readNumber(p, "fontSizeAdjustment", 0),
|
|
1973
|
+
align: readStyle(p, "align", DefaultHorizontalAlignStyle),
|
|
1974
|
+
verticalAlign: readStyle(p, "verticalAlign", DefaultVerticalAlignStyle),
|
|
1975
|
+
growY: readNumber(p, "growY", 0),
|
|
1976
|
+
url: readString(p, "url", ""),
|
|
1977
|
+
richText: readRichText(p),
|
|
1978
|
+
text: readText(p),
|
|
1979
|
+
textFirstEditedBy: readString(p, "textFirstEditedBy", "") || null,
|
|
1980
|
+
scale: readNumber(p, "scale", 1)
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
var MIN_NOTE_FONT_SIZE = 4;
|
|
1984
|
+
function getNoteFontSize(shape, theme = DEFAULT_THEME) {
|
|
1985
|
+
const { size, scale, fontSizeAdjustment } = readNoteProps(shape);
|
|
1986
|
+
const styled = theme.fontSize[size] ?? DEFAULT_THEME.fontSize[size];
|
|
1987
|
+
return (fontSizeAdjustment >= MIN_NOTE_FONT_SIZE ? fontSizeAdjustment : styled) * scale;
|
|
1988
|
+
}
|
|
1989
|
+
function getNoteDisplayValues(editor, shape, theme, colorMode, options = {}) {
|
|
1990
|
+
const base = getDefaultDisplayValues(editor, shape, theme, colorMode);
|
|
1991
|
+
const props = readNoteProps(shape);
|
|
1992
|
+
const colors = theme.colors[colorMode] ?? theme.colors.light;
|
|
1993
|
+
const side = (options.noteSize ?? NOTE_SIZE) * props.scale;
|
|
1994
|
+
const fontSize = getNoteFontSize(shape, theme);
|
|
1995
|
+
const labelColor = props.labelColor === "black" ? getColorValue(colors, props.color, "noteText") : getColorValue(colors, props.labelColor, "solid");
|
|
1996
|
+
return {
|
|
1997
|
+
...base,
|
|
1998
|
+
labelColor,
|
|
1999
|
+
fill: getColorValue(colors, props.color, "noteFill"),
|
|
2000
|
+
fontSize,
|
|
2001
|
+
lineHeight: fontSize * theme.lineHeight,
|
|
2002
|
+
noteWidth: side,
|
|
2003
|
+
noteHeight: side,
|
|
2004
|
+
labelPadding: (options.labelPadding ?? NOTE_PADDING) * props.scale,
|
|
2005
|
+
labelFontSize: fontSize,
|
|
2006
|
+
labelFontFamily: theme.fonts[props.font] ?? theme.fonts.draw,
|
|
2007
|
+
// SEMANTICS-ASSUMED: a note's label is upright, regular and unshaped. Rich
|
|
2008
|
+
// text carries its own bold/italic runs as marks, so the shape-level
|
|
2009
|
+
// typography is the base every run is measured relative to.
|
|
2010
|
+
labelFontStyle: "normal",
|
|
2011
|
+
labelFontWeight: "normal",
|
|
2012
|
+
labelFontVariant: "normal",
|
|
2013
|
+
labelLineHeight: theme.lineHeight
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
function getNoteGrowY(shape, editor) {
|
|
2017
|
+
const { richText, text, font, scale } = readNoteProps(shape);
|
|
2018
|
+
if (!text) return 0;
|
|
2019
|
+
const side = NOTE_SIZE * scale;
|
|
2020
|
+
const m = measureLabel(richText, {
|
|
2021
|
+
fontFamily: font,
|
|
2022
|
+
fontSize: getNoteFontSize(shape),
|
|
2023
|
+
maxWidth: side,
|
|
2024
|
+
padding: NOTE_PADDING * scale,
|
|
2025
|
+
editor: editor ?? null
|
|
2026
|
+
});
|
|
2027
|
+
return computeGrowY(m.h, side);
|
|
2028
|
+
}
|
|
2029
|
+
var LABEL_KEYS2 = ["richText", "text", "font", "size", "scale", "fontSizeAdjustment"];
|
|
2030
|
+
var NoteShapeUtil = class extends ShapeUtil {
|
|
2031
|
+
static type = "note";
|
|
2032
|
+
// A method, not an arrow: `getDisplayValues` calls it on the options bag, so
|
|
2033
|
+
// `this` is the bag a `configure()` copy actually carries. Closing over
|
|
2034
|
+
// `NoteShapeUtil.options` instead would make every configured copy resolve
|
|
2035
|
+
// against the unconfigured defaults.
|
|
2036
|
+
static options = {
|
|
2037
|
+
getDefaultDisplayValues(editor, shape, theme, colorMode) {
|
|
2038
|
+
return getNoteDisplayValues(editor, shape, theme, colorMode, this);
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
static props = noteShapeProps;
|
|
2042
|
+
static migrations = noteShapeMigrations;
|
|
2043
|
+
getDefaultProps() {
|
|
2044
|
+
return {
|
|
2045
|
+
color: "black",
|
|
2046
|
+
labelColor: "black",
|
|
2047
|
+
size: "m",
|
|
2048
|
+
font: "draw",
|
|
2049
|
+
fontSizeAdjustment: 0,
|
|
2050
|
+
align: "middle",
|
|
2051
|
+
verticalAlign: "middle",
|
|
2052
|
+
growY: 0,
|
|
2053
|
+
url: "",
|
|
2054
|
+
richText: toRichText(""),
|
|
2055
|
+
text: "",
|
|
2056
|
+
// Deliberately absent: `textFirstEditedBy` is read and round-tripped but
|
|
2057
|
+
// never written by mocanvas (only the host knows who "a person" is), and
|
|
2058
|
+
// defaulting it would make every file that predates attribution warn on
|
|
2059
|
+
// load about a prop nothing here ever sets.
|
|
2060
|
+
scale: 1
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
getGeometry(shape) {
|
|
2064
|
+
const { scale, growY } = readNoteProps(shape);
|
|
2065
|
+
return new Rectangle2d({ width: NOTE_SIZE * scale, height: NOTE_SIZE * scale + growY, isFilled: true });
|
|
2066
|
+
}
|
|
2067
|
+
getRenderStyle(shape) {
|
|
2068
|
+
const colors = getThemeColors(this.editor);
|
|
2069
|
+
return { fill: getNoteFillRgba(readNoteProps(shape).color, colors), stroke: 0, strokeWidth: 0, dash: 0, opacity: 1 };
|
|
2070
|
+
}
|
|
2071
|
+
/** A note is nothing but a label, so it needs its family's faces. */
|
|
2072
|
+
getFontFaces(shape) {
|
|
2073
|
+
return getLabelFontFaces(readNoteProps(shape).font);
|
|
2074
|
+
}
|
|
2075
|
+
component(shape) {
|
|
2076
|
+
const { richText, text, font, color, align, verticalAlign, scale, growY } = readNoteProps(shape);
|
|
2077
|
+
const display = getDisplayValues(this, shape);
|
|
2078
|
+
const colors = getThemeColors(this.editor);
|
|
2079
|
+
const textColor = display.labelColor;
|
|
2080
|
+
const w = NOTE_SIZE * scale;
|
|
2081
|
+
const h = NOTE_SIZE * scale + growY;
|
|
2082
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2083
|
+
/* @__PURE__ */ jsx(
|
|
2084
|
+
"div",
|
|
2085
|
+
{
|
|
2086
|
+
"aria-hidden": "true",
|
|
2087
|
+
style: {
|
|
2088
|
+
position: "absolute",
|
|
2089
|
+
left: 0,
|
|
2090
|
+
top: 0,
|
|
2091
|
+
width: w,
|
|
2092
|
+
height: h,
|
|
2093
|
+
background: getNoteBodyGradientCss(color, colors),
|
|
2094
|
+
boxShadow: getNoteShadowCss(scale),
|
|
2095
|
+
pointerEvents: "none"
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
),
|
|
2099
|
+
/* @__PURE__ */ jsx(
|
|
2100
|
+
TextLabel,
|
|
2101
|
+
{
|
|
2102
|
+
shape,
|
|
2103
|
+
text,
|
|
2104
|
+
richText,
|
|
2105
|
+
isEditing: this.editor.getEditingShapeId() === shape.id,
|
|
2106
|
+
fontFamily: font,
|
|
2107
|
+
fontSize: display.labelFontSize,
|
|
2108
|
+
color: textColor,
|
|
2109
|
+
textAlign: align,
|
|
2110
|
+
verticalAlign,
|
|
2111
|
+
wrap: true,
|
|
2112
|
+
width: w,
|
|
2113
|
+
height: h,
|
|
2114
|
+
padding: display.labelPadding,
|
|
2115
|
+
onChange: (next) => this.editor.updateShape({
|
|
2116
|
+
id: shape.id,
|
|
2117
|
+
type: "note",
|
|
2118
|
+
props: { text: next, richText: applyPlainTextToRichText(richText, next) }
|
|
2119
|
+
}),
|
|
2120
|
+
onChangeRichText: (next) => this.editor.updateShape({
|
|
2121
|
+
id: shape.id,
|
|
2122
|
+
type: "note",
|
|
2123
|
+
props: { richText: next, text: richTextToText(next) }
|
|
2124
|
+
})
|
|
2125
|
+
}
|
|
2126
|
+
)
|
|
2127
|
+
] });
|
|
2128
|
+
}
|
|
2129
|
+
getIndicatorPath(shape) {
|
|
2130
|
+
const { scale, growY } = readNoteProps(shape);
|
|
2131
|
+
return rectPath(NOTE_SIZE * scale, NOTE_SIZE * scale + growY);
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* The GPU draws the sticky background even while editing; the overlay adds
|
|
2135
|
+
* the gradient and the shadow over it (see `component`).
|
|
2136
|
+
*/
|
|
2137
|
+
needsOverlay(_shape) {
|
|
2138
|
+
return false;
|
|
2139
|
+
}
|
|
2140
|
+
hasOverlayLabel(_shape) {
|
|
2141
|
+
return true;
|
|
2142
|
+
}
|
|
2143
|
+
canEdit(_shape) {
|
|
2144
|
+
return true;
|
|
2145
|
+
}
|
|
2146
|
+
hideResizeHandles(_shape) {
|
|
2147
|
+
return true;
|
|
2148
|
+
}
|
|
2149
|
+
getText(shape) {
|
|
2150
|
+
return readText(shape.props);
|
|
2151
|
+
}
|
|
2152
|
+
onBeforeCreate(next) {
|
|
2153
|
+
const growY = getNoteGrowY(next, this.editor);
|
|
2154
|
+
if (growY !== next.props.growY) return { ...next, props: { ...next.props, growY } };
|
|
2155
|
+
}
|
|
2156
|
+
onBeforeUpdate(prev, next) {
|
|
2157
|
+
if (!LABEL_KEYS2.some((k) => propsOf(prev)[k] !== propsOf(next)[k])) return;
|
|
2158
|
+
const growY = getNoteGrowY(next, this.editor);
|
|
2159
|
+
if (growY !== next.props.growY) return { ...next, props: { ...next.props, growY } };
|
|
2160
|
+
}
|
|
2161
|
+
onEditEnd(shape) {
|
|
2162
|
+
const text = readText(shape.props);
|
|
2163
|
+
const trimmed = trimTrailingWhitespace(text);
|
|
2164
|
+
if (trimmed === text) return;
|
|
2165
|
+
this.editor.updateShape({
|
|
2166
|
+
id: shape.id,
|
|
2167
|
+
type: "note",
|
|
2168
|
+
props: { text: trimmed, richText: applyPlainTextToRichText(readRichText(shape.props), trimmed) }
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
// src/text/TextTexture.ts
|
|
2174
|
+
var MAX_TEXT_TEXTURE_PX = 4096;
|
|
2175
|
+
function textTextureAlign(spec) {
|
|
2176
|
+
return spec.textAlign ?? spec.align ?? "start";
|
|
2177
|
+
}
|
|
2178
|
+
function textTextureText(spec) {
|
|
2179
|
+
return spec.richText === void 0 || spec.richText === null ? spec.text : richTextToText(spec.richText);
|
|
2180
|
+
}
|
|
2181
|
+
function getTextTextureKey(spec) {
|
|
2182
|
+
return [
|
|
2183
|
+
"text",
|
|
2184
|
+
spec.fontFamily,
|
|
2185
|
+
spec.fontSize,
|
|
2186
|
+
spec.lineHeight,
|
|
2187
|
+
spec.color,
|
|
2188
|
+
textTextureAlign(spec),
|
|
2189
|
+
spec.verticalAlign,
|
|
2190
|
+
round(spec.width),
|
|
2191
|
+
round(spec.height),
|
|
2192
|
+
spec.maxWidth === void 0 ? "" : round(spec.maxWidth),
|
|
2193
|
+
spec.padding ?? 0,
|
|
2194
|
+
spec.resolution,
|
|
2195
|
+
spec.richText === void 0 || spec.richText === null ? spec.text : JSON.stringify(spec.richText)
|
|
2196
|
+
].join("|");
|
|
2197
|
+
}
|
|
2198
|
+
function runWidth(text, spec, style) {
|
|
2199
|
+
if (text.length === 0) return 0;
|
|
2200
|
+
return getTextMeasure().measureText(text, {
|
|
2201
|
+
fontFamily: spec.fontFamily,
|
|
2202
|
+
fontSize: spec.fontSize,
|
|
2203
|
+
lineHeight: spec.lineHeight,
|
|
2204
|
+
...style?.bold ? { fontWeight: "bold" } : {}
|
|
2205
|
+
}).w;
|
|
2206
|
+
}
|
|
2207
|
+
function wrapTextTextureLines(spec) {
|
|
2208
|
+
const padding = spec.padding ?? 0;
|
|
2209
|
+
const max = spec.maxWidth === void 0 ? void 0 : Math.max(1, spec.maxWidth - padding * 2);
|
|
2210
|
+
const out = [];
|
|
2211
|
+
for (const paragraph of textTextureText(spec).split("\n")) {
|
|
2212
|
+
if (max === void 0 || paragraph.length === 0) {
|
|
2213
|
+
out.push(paragraph);
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
let line = "";
|
|
2217
|
+
for (const word of paragraph.split(" ")) {
|
|
2218
|
+
const candidate = line.length === 0 ? word : `${line} ${word}`;
|
|
2219
|
+
if (runWidth(candidate, spec) <= max || line.length === 0) {
|
|
2220
|
+
line = candidate;
|
|
2221
|
+
if (line.length > 0 && runWidth(line, spec) > max) {
|
|
2222
|
+
const parts = breakLongWord(line, spec, max);
|
|
2223
|
+
out.push(...parts.slice(0, -1));
|
|
2224
|
+
line = parts[parts.length - 1] ?? "";
|
|
2225
|
+
}
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
out.push(line);
|
|
2229
|
+
line = word;
|
|
2230
|
+
}
|
|
2231
|
+
out.push(line);
|
|
2232
|
+
}
|
|
2233
|
+
return out;
|
|
2234
|
+
}
|
|
2235
|
+
function breakLongWord(word, spec, max) {
|
|
2236
|
+
const parts = [];
|
|
2237
|
+
let current = "";
|
|
2238
|
+
for (const char of word) {
|
|
2239
|
+
const next = current + char;
|
|
2240
|
+
if (current.length > 0 && runWidth(next, spec) > max) {
|
|
2241
|
+
parts.push(current);
|
|
2242
|
+
current = char;
|
|
2243
|
+
} else {
|
|
2244
|
+
current = next;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
parts.push(current);
|
|
2248
|
+
return parts;
|
|
2249
|
+
}
|
|
2250
|
+
function sameStyle(a, b) {
|
|
2251
|
+
return a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.strike === b.strike && a.code === b.code && a.href === b.href;
|
|
2252
|
+
}
|
|
2253
|
+
function wrapTextTextureRuns(spec) {
|
|
2254
|
+
const padding = spec.padding ?? 0;
|
|
2255
|
+
const max = spec.maxWidth === void 0 ? void 0 : Math.max(1, spec.maxWidth - padding * 2);
|
|
2256
|
+
const source = spec.richText === void 0 || spec.richText === null ? spec.text : spec.richText;
|
|
2257
|
+
const out = [];
|
|
2258
|
+
for (const block of richTextToBlocks(source)) {
|
|
2259
|
+
let line = [];
|
|
2260
|
+
let width = 0;
|
|
2261
|
+
const push = () => {
|
|
2262
|
+
while (line.length > 0 && /^\s+$/u.test(line[line.length - 1].text)) {
|
|
2263
|
+
const dropped = line.pop();
|
|
2264
|
+
width -= runWidth(dropped.text, spec, dropped);
|
|
2265
|
+
}
|
|
2266
|
+
out.push({ runs: mergeRuns(line), width: Math.max(0, width) });
|
|
2267
|
+
line = [];
|
|
2268
|
+
width = 0;
|
|
2269
|
+
};
|
|
2270
|
+
for (const run of block.runs) {
|
|
2271
|
+
for (const token of run.text.split(/(\s+)/u).filter((t) => t.length > 0)) {
|
|
2272
|
+
const isSpace = /^\s+$/u.test(token);
|
|
2273
|
+
const tokenWidth = runWidth(token, spec, run);
|
|
2274
|
+
if (max !== void 0 && !isSpace && line.length > 0 && width + tokenWidth > max) {
|
|
2275
|
+
push();
|
|
2276
|
+
}
|
|
2277
|
+
if (isSpace && line.length === 0) continue;
|
|
2278
|
+
if (max !== void 0 && !isSpace && tokenWidth > max) {
|
|
2279
|
+
for (const piece of breakLongToken(token, spec, run, max, width, line.length > 0)) {
|
|
2280
|
+
if (piece === null) push();
|
|
2281
|
+
else {
|
|
2282
|
+
line.push({ ...run, text: piece });
|
|
2283
|
+
width += runWidth(piece, spec, run);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
line.push({ ...run, text: token });
|
|
2289
|
+
width += tokenWidth;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
push();
|
|
2293
|
+
}
|
|
2294
|
+
return out;
|
|
2295
|
+
}
|
|
2296
|
+
function breakLongToken(token, spec, style, max, startWidth, canBreakFirst) {
|
|
2297
|
+
const out = [];
|
|
2298
|
+
if (canBreakFirst && startWidth > 0) out.push(null);
|
|
2299
|
+
let current = "";
|
|
2300
|
+
for (const char of token) {
|
|
2301
|
+
const next = current + char;
|
|
2302
|
+
if (current.length > 0 && runWidth(next, spec, style) > max) {
|
|
2303
|
+
out.push(current, null);
|
|
2304
|
+
current = char;
|
|
2305
|
+
} else {
|
|
2306
|
+
current = next;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if (current.length > 0) out.push(current);
|
|
2310
|
+
return out;
|
|
2311
|
+
}
|
|
2312
|
+
function mergeRuns(runs) {
|
|
2313
|
+
const out = [];
|
|
2314
|
+
for (const run of runs) {
|
|
2315
|
+
const last = out[out.length - 1];
|
|
2316
|
+
if (last && sameStyle(last, run)) last.text += run.text;
|
|
2317
|
+
else out.push({ ...run });
|
|
2318
|
+
}
|
|
2319
|
+
return out;
|
|
2320
|
+
}
|
|
2321
|
+
function getTextTextureScale(spec) {
|
|
2322
|
+
const longest = Math.max(1, spec.width, spec.height);
|
|
2323
|
+
return Math.max(0.05, Math.min(spec.resolution, MAX_TEXT_TEXTURE_PX / longest));
|
|
2324
|
+
}
|
|
2325
|
+
function renderTextToCanvas(spec) {
|
|
2326
|
+
if (typeof document === "undefined") throw new Error("mocanvas: no document to rasterize text with");
|
|
2327
|
+
const scale = getTextTextureScale(spec);
|
|
2328
|
+
const canvas = document.createElement("canvas");
|
|
2329
|
+
canvas.width = Math.max(1, Math.ceil(spec.width * scale));
|
|
2330
|
+
canvas.height = Math.max(1, Math.ceil(spec.height * scale));
|
|
2331
|
+
const ctx = canvas.getContext("2d");
|
|
2332
|
+
if (!ctx) throw new Error("mocanvas: no 2d context to rasterize text with");
|
|
2333
|
+
const padding = spec.padding ?? 0;
|
|
2334
|
+
const lines = wrapTextTextureRuns(spec);
|
|
2335
|
+
const lineHeightPx = spec.fontSize * spec.lineHeight;
|
|
2336
|
+
const blockHeight = lines.length * lineHeightPx;
|
|
2337
|
+
const align = textTextureAlign(spec);
|
|
2338
|
+
ctx.scale(scale, scale);
|
|
2339
|
+
ctx.fillStyle = spec.color;
|
|
2340
|
+
ctx.textBaseline = "middle";
|
|
2341
|
+
ctx.textAlign = "left";
|
|
2342
|
+
const innerH = Math.max(0, spec.height - padding * 2);
|
|
2343
|
+
const innerW = Math.max(0, spec.width - padding * 2);
|
|
2344
|
+
const top = padding + (spec.verticalAlign === "start" ? 0 : spec.verticalAlign === "end" ? innerH - blockHeight : (innerH - blockHeight) / 2);
|
|
2345
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2346
|
+
const line = lines[i];
|
|
2347
|
+
const baseline = top + i * lineHeightPx + lineHeightPx / 2;
|
|
2348
|
+
let x = padding + (align === "start" ? 0 : align === "end" ? innerW - line.width : (innerW - line.width) / 2);
|
|
2349
|
+
for (const run of line.runs) {
|
|
2350
|
+
ctx.font = runFont(run, spec);
|
|
2351
|
+
ctx.fillText(run.text, x, baseline);
|
|
2352
|
+
const width = runWidth(run.text, spec, run);
|
|
2353
|
+
if (run.underline || run.strike) {
|
|
2354
|
+
const thickness = Math.max(1, spec.fontSize / 14);
|
|
2355
|
+
const y = run.underline ? baseline + spec.fontSize * 0.4 : baseline;
|
|
2356
|
+
ctx.fillRect(x, y - thickness / 2, width, thickness);
|
|
2357
|
+
}
|
|
2358
|
+
x += width;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return canvas;
|
|
2362
|
+
}
|
|
2363
|
+
function runFont(style, spec) {
|
|
2364
|
+
const parts = [];
|
|
2365
|
+
if (style.italic) parts.push("italic");
|
|
2366
|
+
if (style.bold) parts.push("bold");
|
|
2367
|
+
parts.push(`${spec.fontSize}px`, spec.fontFamily);
|
|
2368
|
+
return parts.join(" ");
|
|
2369
|
+
}
|
|
2370
|
+
function round(v) {
|
|
2371
|
+
return Math.round(v * 100) / 100;
|
|
2372
|
+
}
|
|
2373
|
+
var TEXT_ALIGNS = ["start", "middle", "end"];
|
|
2374
|
+
function readTextProps(shape) {
|
|
2375
|
+
const p = propsOf(shape);
|
|
2376
|
+
return {
|
|
2377
|
+
color: readStyle(p, "color", DefaultColorStyle),
|
|
2378
|
+
size: readStyle(p, "size", DefaultSizeStyle),
|
|
2379
|
+
font: readStyle(p, "font", DefaultFontStyle),
|
|
2380
|
+
textAlign: readEnum(p, "textAlign", TEXT_ALIGNS, "start"),
|
|
2381
|
+
w: readNumber(p, "w", 100),
|
|
2382
|
+
richText: readRichText(p),
|
|
2383
|
+
text: readText(p),
|
|
2384
|
+
scale: readNumber(p, "scale", 1),
|
|
2385
|
+
autoSize: readBoolean(p, "autoSize", true)
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
function getTextShapeSizeFor(shape, editor) {
|
|
2389
|
+
const { richText, size, scale, w, font, autoSize } = readTextProps(shape);
|
|
2390
|
+
return getTextShapeSize({
|
|
2391
|
+
text: richText,
|
|
2392
|
+
fontFamily: font,
|
|
2393
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
2394
|
+
autoSize,
|
|
2395
|
+
w,
|
|
2396
|
+
editor: editor ?? null
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
function getTextShapeHeight(shape, editor) {
|
|
2400
|
+
return getTextShapeSizeFor(shape, editor).h;
|
|
2401
|
+
}
|
|
2402
|
+
function getTextShapeBox(shape, editor) {
|
|
2403
|
+
const { w, h } = getTextShapeSizeFor(shape, editor);
|
|
2404
|
+
const props = readTextProps(shape);
|
|
2405
|
+
return { w: Math.max(1, props.autoSize ? Math.max(w, props.w) : props.w), h };
|
|
2406
|
+
}
|
|
2407
|
+
function getTextShapeTextureSpec(editor, shape) {
|
|
2408
|
+
const { text, font, size, scale, color, textAlign, autoSize } = readTextProps(shape);
|
|
2409
|
+
const box = getTextShapeBox(shape, editor);
|
|
2410
|
+
return {
|
|
2411
|
+
text,
|
|
2412
|
+
fontFamily: getFontFamily(font, getTheme(editor)),
|
|
2413
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
2414
|
+
color: getTextCssColor(color, getThemeColors(editor)),
|
|
2415
|
+
align: textAlign,
|
|
2416
|
+
verticalAlign: "start",
|
|
2417
|
+
lineHeight: LINE_HEIGHT,
|
|
2418
|
+
width: box.w,
|
|
2419
|
+
height: box.h,
|
|
2420
|
+
...autoSize ? {} : { maxWidth: box.w },
|
|
2421
|
+
resolution: editor.getTextureResolution()
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
function canRasterizeText() {
|
|
2425
|
+
return typeof document !== "undefined";
|
|
2426
|
+
}
|
|
2427
|
+
var SIZE_KEYS = ["richText", "text", "font", "size", "scale", "autoSize"];
|
|
2428
|
+
function getTextDisplayValues(editor, shape, theme, colorMode) {
|
|
2429
|
+
const base = getDefaultDisplayValues(editor, shape, theme, colorMode);
|
|
2430
|
+
const scale = readNumber(propsOf(shape), "scale", 1);
|
|
2431
|
+
return {
|
|
2432
|
+
...base,
|
|
2433
|
+
labelFontSize: base.fontSize * scale,
|
|
2434
|
+
labelLineHeight: base.fontSize * scale * LINE_HEIGHT,
|
|
2435
|
+
labelFontFamily: base.fontFamily
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
var TextShapeUtil = class extends ShapeUtil {
|
|
2439
|
+
static type = "text";
|
|
2440
|
+
static props = textShapeProps;
|
|
2441
|
+
static migrations = textShapeMigrations;
|
|
2442
|
+
static options = { getDefaultDisplayValues: getTextDisplayValues };
|
|
2443
|
+
getDefaultProps() {
|
|
2444
|
+
return {
|
|
2445
|
+
color: "black",
|
|
2446
|
+
size: "m",
|
|
2447
|
+
font: "draw",
|
|
2448
|
+
textAlign: "start",
|
|
2449
|
+
w: 100,
|
|
2450
|
+
richText: toRichText(""),
|
|
2451
|
+
text: "",
|
|
2452
|
+
scale: 1,
|
|
2453
|
+
autoSize: true
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
getGeometry(shape) {
|
|
2457
|
+
const box = getTextShapeBox(shape, this.editor);
|
|
2458
|
+
return new Rectangle2d({ width: box.w, height: box.h, isFilled: true });
|
|
2459
|
+
}
|
|
2460
|
+
/** A text shape is nothing but a label, so it always needs its family's faces. */
|
|
2461
|
+
getFontFaces(shape) {
|
|
2462
|
+
return getLabelFontFaces(readTextProps(shape).font);
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* A texture of the rasterized label, so the GPU draws the text instead of the
|
|
2466
|
+
* DOM. The shape being edited (and any environment without a canvas) keeps
|
|
2467
|
+
* the DOM path; `fill` is the text colour so the level-of-detail quad the
|
|
2468
|
+
* engine draws below a few pixels still looks right.
|
|
2469
|
+
*/
|
|
2470
|
+
getRenderStyle(shape) {
|
|
2471
|
+
const key = this.getTextureKey(shape);
|
|
2472
|
+
if (!key) return null;
|
|
2473
|
+
const texture = this.editor.textures.acquire(key.key, async () => renderTextToCanvas(key.spec));
|
|
2474
|
+
if (!texture) return null;
|
|
2475
|
+
return {
|
|
2476
|
+
fill: getStrokeRgba(readTextProps(shape).color, getThemeColors(this.editor)),
|
|
2477
|
+
stroke: 0,
|
|
2478
|
+
strokeWidth: 0,
|
|
2479
|
+
dash: 0,
|
|
2480
|
+
opacity: 1,
|
|
2481
|
+
texture
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
/** The DOM label stands in while editing and until the texture is ready. */
|
|
2485
|
+
needsOverlay(shape) {
|
|
2486
|
+
const key = this.getTextureKey(shape);
|
|
2487
|
+
return key === null || !this.editor.textures.isReady(key.key);
|
|
2488
|
+
}
|
|
2489
|
+
getTextureKey(shape) {
|
|
2490
|
+
if (this.editor.getEditingShapeId() === shape.id) return null;
|
|
2491
|
+
if (readText(shape.props).length === 0) return null;
|
|
2492
|
+
if (!canRasterizeText()) return null;
|
|
2493
|
+
const spec = getTextShapeTextureSpec(this.editor, shape);
|
|
2494
|
+
return { key: getTextTextureKey(spec), spec };
|
|
2495
|
+
}
|
|
2496
|
+
component(shape) {
|
|
2497
|
+
const { richText, text, font, size, scale, textAlign, w, autoSize } = readTextProps(shape);
|
|
2498
|
+
const display = getDisplayValues(this, shape);
|
|
2499
|
+
return /* @__PURE__ */ jsx(
|
|
2500
|
+
TextLabel,
|
|
2501
|
+
{
|
|
2502
|
+
shape,
|
|
2503
|
+
text,
|
|
2504
|
+
richText,
|
|
2505
|
+
isEditing: this.editor.getEditingShapeId() === shape.id,
|
|
2506
|
+
fontFamily: font,
|
|
2507
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
2508
|
+
color: display.color,
|
|
2509
|
+
textAlign,
|
|
2510
|
+
verticalAlign: "start",
|
|
2511
|
+
wrap: !autoSize,
|
|
2512
|
+
width: Math.max(1, w),
|
|
2513
|
+
onChange: (next) => this.editor.updateShape({
|
|
2514
|
+
id: shape.id,
|
|
2515
|
+
type: "text",
|
|
2516
|
+
props: { text: next, richText: applyPlainTextToRichText(richText, next) }
|
|
2517
|
+
}),
|
|
2518
|
+
onChangeRichText: (next) => this.editor.updateShape({
|
|
2519
|
+
id: shape.id,
|
|
2520
|
+
type: "text",
|
|
2521
|
+
props: { richText: next, text: richTextToText(next) }
|
|
2522
|
+
})
|
|
2523
|
+
}
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
getIndicatorPath(shape) {
|
|
2527
|
+
const b = this.getGeometry(shape).bounds;
|
|
2528
|
+
return rectPath(b.w, b.h);
|
|
2529
|
+
}
|
|
2530
|
+
canEdit(_shape) {
|
|
2531
|
+
return true;
|
|
2532
|
+
}
|
|
2533
|
+
isAspectRatioLocked(_shape) {
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
getText(shape) {
|
|
2537
|
+
return readText(shape.props);
|
|
2538
|
+
}
|
|
2539
|
+
/** Auto-sized text keeps `w` in sync with its measured width. */
|
|
2540
|
+
onBeforeCreate(next) {
|
|
2541
|
+
return this.fitWidth(next);
|
|
2542
|
+
}
|
|
2543
|
+
onBeforeUpdate(prev, next) {
|
|
2544
|
+
if (!readTextProps(next).autoSize) return;
|
|
2545
|
+
if (!SIZE_KEYS.some((k) => propsOf(prev)[k] !== propsOf(next)[k])) return;
|
|
2546
|
+
return this.fitWidth(next);
|
|
2547
|
+
}
|
|
2548
|
+
fitWidth(shape) {
|
|
2549
|
+
const props = readTextProps(shape);
|
|
2550
|
+
if (!props.autoSize) return;
|
|
2551
|
+
const { w } = getTextShapeSizeFor(shape, this.editor);
|
|
2552
|
+
if (w !== props.w) return { ...shape, props: { ...shape.props, w } };
|
|
2553
|
+
}
|
|
2554
|
+
/** A text shape left empty after editing is removed. */
|
|
2555
|
+
onEditEnd(shape) {
|
|
2556
|
+
if (readText(shape.props).trim().length === 0) this.editor.deleteShapes([shape.id]);
|
|
2557
|
+
}
|
|
2558
|
+
/** Resizing a text shape changes its wrap width and turns auto-size off. */
|
|
2559
|
+
onResize(shape, info) {
|
|
2560
|
+
const { scaleX, initialShape, newPoint } = info;
|
|
2561
|
+
const w = Math.max(1, Math.abs(readTextProps(initialShape).w * scaleX));
|
|
2562
|
+
return { x: newPoint.x, y: newPoint.y, props: { ...shape.props, w, autoSize: false } };
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
var VIDEO_WIDTH = 640;
|
|
2566
|
+
var VIDEO_HEIGHT = 360;
|
|
2567
|
+
var VIDEO_PLACEHOLDER_FILL = "#eceff3";
|
|
2568
|
+
var VIDEO_PLACEHOLDER_STROKE = "#9fa8b2";
|
|
2569
|
+
var VIDEO_PLAY_COLOR = "#5f6670";
|
|
2570
|
+
var VIDEO_PLAY_SIZE = 48;
|
|
2571
|
+
var VIDEO_TIME_EPSILON = 0.1;
|
|
2572
|
+
function isVideoAutoplayAllowed(editor) {
|
|
2573
|
+
return editor?.options?.allowVideoAutoplay !== false;
|
|
2574
|
+
}
|
|
2575
|
+
function getVideoSource(editor, shape) {
|
|
2576
|
+
const assetId = readString(propsOf(shape), "assetId", "");
|
|
2577
|
+
if (!assetId) return null;
|
|
2578
|
+
const asset = editor.getAsset(assetId);
|
|
2579
|
+
if (!asset || asset.type !== "video") return null;
|
|
2580
|
+
return asset.props.src ?? null;
|
|
2581
|
+
}
|
|
2582
|
+
function readVideoBox(shape) {
|
|
2583
|
+
const p = propsOf(shape);
|
|
2584
|
+
return { w: readNumber(p, "w", VIDEO_WIDTH), h: readNumber(p, "h", VIDEO_HEIGHT) };
|
|
2585
|
+
}
|
|
2586
|
+
function getVideoPlayTriangle(w, h, size = VIDEO_PLAY_SIZE) {
|
|
2587
|
+
const s = Math.max(0, Math.min(size, w * 0.6, h * 0.6));
|
|
2588
|
+
const cx = w / 2;
|
|
2589
|
+
const cy = h / 2;
|
|
2590
|
+
const half = s / 2;
|
|
2591
|
+
return [
|
|
2592
|
+
{ x: cx - half * 0.6, y: cy - half },
|
|
2593
|
+
{ x: cx - half * 0.6, y: cy + half },
|
|
2594
|
+
{ x: cx + half * 0.9, y: cy }
|
|
2595
|
+
];
|
|
2596
|
+
}
|
|
2597
|
+
function VideoPlayer({ src, time, playing, controls, altText }) {
|
|
2598
|
+
const ref = useRef(null);
|
|
2599
|
+
useEffect(() => {
|
|
2600
|
+
const el = ref.current;
|
|
2601
|
+
if (!el) return;
|
|
2602
|
+
const seek = () => {
|
|
2603
|
+
if (Math.abs(el.currentTime - time) <= VIDEO_TIME_EPSILON) return;
|
|
2604
|
+
try {
|
|
2605
|
+
el.currentTime = time;
|
|
2606
|
+
} catch {
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
seek();
|
|
2610
|
+
el.addEventListener("loadedmetadata", seek);
|
|
2611
|
+
return () => el.removeEventListener("loadedmetadata", seek);
|
|
2612
|
+
}, [time, src]);
|
|
2613
|
+
useEffect(() => {
|
|
2614
|
+
const el = ref.current;
|
|
2615
|
+
if (!el) return;
|
|
2616
|
+
if (playing) void el.play().catch(() => {
|
|
2617
|
+
});
|
|
2618
|
+
else el.pause();
|
|
2619
|
+
}, [playing, src]);
|
|
2620
|
+
return /* @__PURE__ */ jsx(
|
|
2621
|
+
"video",
|
|
2622
|
+
{
|
|
2623
|
+
ref,
|
|
2624
|
+
src,
|
|
2625
|
+
controls,
|
|
2626
|
+
playsInline: true,
|
|
2627
|
+
muted: true,
|
|
2628
|
+
loop: true,
|
|
2629
|
+
preload: "metadata",
|
|
2630
|
+
"aria-label": altText || void 0,
|
|
2631
|
+
draggable: false,
|
|
2632
|
+
style: {
|
|
2633
|
+
display: "block",
|
|
2634
|
+
width: "100%",
|
|
2635
|
+
height: "100%",
|
|
2636
|
+
objectFit: "cover",
|
|
2637
|
+
// Controls are only reachable while editing; otherwise the canvas
|
|
2638
|
+
// keeps the pointer so a drag over a video still pans.
|
|
2639
|
+
pointerEvents: controls ? "auto" : "none",
|
|
2640
|
+
userSelect: "none"
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
function getVideoDisplayValues(editor, shape, theme, colorMode) {
|
|
2646
|
+
return {
|
|
2647
|
+
...getDefaultDisplayValues(editor, shape, theme, colorMode),
|
|
2648
|
+
placeholderFill: VIDEO_PLACEHOLDER_FILL,
|
|
2649
|
+
placeholderStroke: VIDEO_PLACEHOLDER_STROKE,
|
|
2650
|
+
playColor: VIDEO_PLAY_COLOR,
|
|
2651
|
+
playSize: VIDEO_PLAY_SIZE
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
var VideoShapeUtil = class extends BaseBoxShapeUtil {
|
|
2655
|
+
static type = "video";
|
|
2656
|
+
static props = videoShapeProps;
|
|
2657
|
+
static migrations = videoShapeMigrations;
|
|
2658
|
+
static options = { getDefaultDisplayValues: getVideoDisplayValues };
|
|
2659
|
+
getDefaultProps() {
|
|
2660
|
+
return { w: VIDEO_WIDTH, h: VIDEO_HEIGHT, assetId: null, time: 0, playing: true, url: "", altText: "" };
|
|
2661
|
+
}
|
|
2662
|
+
getGeometry(shape) {
|
|
2663
|
+
const { w, h } = readVideoBox(shape);
|
|
2664
|
+
return new Rectangle2d({ width: Math.max(1, w), height: Math.max(1, h), isFilled: true });
|
|
2665
|
+
}
|
|
2666
|
+
/** A moving picture is not a texture the engine owns: video renders in the DOM overlay. */
|
|
2667
|
+
getRenderStyle(_shape) {
|
|
2668
|
+
return null;
|
|
2669
|
+
}
|
|
2670
|
+
component(shape) {
|
|
2671
|
+
const { w, h } = readVideoBox(shape);
|
|
2672
|
+
const p = propsOf(shape);
|
|
2673
|
+
const altText = readString(p, "altText", "");
|
|
2674
|
+
const src = getVideoSource(this.editor, shape);
|
|
2675
|
+
const box = { position: "absolute", left: 0, top: 0, width: w, height: h, overflow: "hidden", boxSizing: "border-box" };
|
|
2676
|
+
if (!src) {
|
|
2677
|
+
const [a, b, c] = getVideoPlayTriangle(w, h);
|
|
2678
|
+
return /* @__PURE__ */ jsx(
|
|
2679
|
+
"div",
|
|
2680
|
+
{
|
|
2681
|
+
style: { ...box, background: VIDEO_PLACEHOLDER_FILL, border: `1px dashed ${VIDEO_PLACEHOLDER_STROKE}`, pointerEvents: "none" },
|
|
2682
|
+
"aria-label": altText || "video",
|
|
2683
|
+
children: /* @__PURE__ */ jsx("svg", { width: w, height: h, viewBox: `0 0 ${Math.max(1, w)} ${Math.max(1, h)}`, style: { display: "block" }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("polygon", { points: `${a.x},${a.y} ${b.x},${b.y} ${c.x},${c.y}`, fill: VIDEO_PLAY_COLOR }) })
|
|
2684
|
+
}
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
return /* @__PURE__ */ jsx("div", { style: box, children: /* @__PURE__ */ jsx(
|
|
2688
|
+
VideoPlayer,
|
|
2689
|
+
{
|
|
2690
|
+
src,
|
|
2691
|
+
time: readNumber(p, "time", 0),
|
|
2692
|
+
playing: readBoolean(p, "playing", true) && isVideoAutoplayAllowed(this.editor),
|
|
2693
|
+
controls: this.editor.getEditingShapeId() === shape.id,
|
|
2694
|
+
altText
|
|
2695
|
+
}
|
|
2696
|
+
) });
|
|
2697
|
+
}
|
|
2698
|
+
getIndicatorPath(shape) {
|
|
2699
|
+
const { w, h } = readVideoBox(shape);
|
|
2700
|
+
return rectPath(w, h);
|
|
2701
|
+
}
|
|
2702
|
+
/** Editing a video means taking its controls, not typing into it. */
|
|
2703
|
+
canEdit(_shape) {
|
|
2704
|
+
return true;
|
|
2705
|
+
}
|
|
2706
|
+
isAspectRatioLocked(_shape) {
|
|
2707
|
+
return true;
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
function svgNum(v) {
|
|
2711
|
+
if (!Number.isFinite(v)) return "0";
|
|
2712
|
+
const r = Math.round(v * 100) / 100;
|
|
2713
|
+
return String(r === 0 ? 0 : r);
|
|
2714
|
+
}
|
|
2715
|
+
function escapeXml(text) {
|
|
2716
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2717
|
+
}
|
|
2718
|
+
function rgbaToHex(word) {
|
|
2719
|
+
const w = word >>> 0;
|
|
2720
|
+
const a = w & 255;
|
|
2721
|
+
if (a === 0) return void 0;
|
|
2722
|
+
const r = w >>> 24 & 255;
|
|
2723
|
+
const g = w >>> 16 & 255;
|
|
2724
|
+
const b = w >>> 8 & 255;
|
|
2725
|
+
const hex = (n) => n.toString(16).padStart(2, "0");
|
|
2726
|
+
return a === 255 ? `#${hex(r)}${hex(g)}${hex(b)}` : `#${hex(r)}${hex(g)}${hex(b)}${hex(a)}`;
|
|
2727
|
+
}
|
|
2728
|
+
function matrixAttr(m) {
|
|
2729
|
+
return `matrix(${svgNum(m.a)} ${svgNum(m.b)} ${svgNum(m.c)} ${svgNum(m.d)} ${svgNum(m.e)} ${svgNum(m.f)})`;
|
|
2730
|
+
}
|
|
2731
|
+
function dashArray(dash, strokeWidth) {
|
|
2732
|
+
const sw = Math.max(0.5, strokeWidth);
|
|
2733
|
+
switch (dash) {
|
|
2734
|
+
case 1:
|
|
2735
|
+
return `${svgNum(sw * 2)} ${svgNum(sw * 2)}`;
|
|
2736
|
+
case 2:
|
|
2737
|
+
return `0.1 ${svgNum(sw * 2)}`;
|
|
2738
|
+
default:
|
|
2739
|
+
return void 0;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
function attrs(values) {
|
|
2743
|
+
const parts = [];
|
|
2744
|
+
for (const [k, v] of Object.entries(values)) {
|
|
2745
|
+
if (v === void 0) continue;
|
|
2746
|
+
parts.push(`${k}="${typeof v === "number" ? svgNum(v) : escapeXml(v)}"`);
|
|
2747
|
+
}
|
|
2748
|
+
return parts.join(" ");
|
|
2749
|
+
}
|
|
2750
|
+
function strokeAttrs(style) {
|
|
2751
|
+
const stroke = rgbaToHex(style.stroke);
|
|
2752
|
+
if (!stroke || style.strokeWidth <= 0) return { stroke: "none" };
|
|
2753
|
+
return {
|
|
2754
|
+
stroke,
|
|
2755
|
+
"stroke-width": style.strokeWidth,
|
|
2756
|
+
"stroke-linejoin": "round",
|
|
2757
|
+
"stroke-linecap": "round",
|
|
2758
|
+
"stroke-dasharray": dashArray(style.dash, style.strokeWidth)
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
function isGroup(g) {
|
|
2762
|
+
return Array.isArray(g.children);
|
|
2763
|
+
}
|
|
2764
|
+
function geometryLeaves(geometry) {
|
|
2765
|
+
if (!isGroup(geometry)) return geometry.isLabel ? [] : [geometry];
|
|
2766
|
+
return geometry.children.flatMap((c) => geometryLeaves(c));
|
|
2767
|
+
}
|
|
2768
|
+
function geometryLabels(geometry) {
|
|
2769
|
+
if (!isGroup(geometry)) return geometry.isLabel ? [geometry] : [];
|
|
2770
|
+
return geometry.children.flatMap((c) => geometryLabels(c));
|
|
2771
|
+
}
|
|
2772
|
+
function hasPathWords(words) {
|
|
2773
|
+
return words.length > 0 && words[0] === PATH_OP.MOVE;
|
|
2774
|
+
}
|
|
2775
|
+
function geometryToSvgPaths(geometry, style) {
|
|
2776
|
+
const fill = rgbaToHex(style.fill);
|
|
2777
|
+
const stroke = strokeAttrs(style);
|
|
2778
|
+
const out = [];
|
|
2779
|
+
for (const leaf of geometryLeaves(geometry)) {
|
|
2780
|
+
const words = leaf.toPathWords();
|
|
2781
|
+
if (!hasPathWords(words)) continue;
|
|
2782
|
+
const filled = fill !== void 0 && leaf.isClosed && leaf.isFilled;
|
|
2783
|
+
out.push(`<path ${attrs({ d: pathWordsToSvgD(words), fill: filled ? fill : "none", ...stroke })}/>`);
|
|
2784
|
+
}
|
|
2785
|
+
return out.join("");
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// src/export/text-svg.ts
|
|
2789
|
+
function alignOf(opts) {
|
|
2790
|
+
return opts.textAlign ?? opts.align ?? "middle";
|
|
2791
|
+
}
|
|
2792
|
+
function wrapTextLines(text, fontSize, maxWidth = Infinity) {
|
|
2793
|
+
const charW = fontSize * AVG_CHAR_WIDTH;
|
|
2794
|
+
const limit = Number.isFinite(maxWidth) && charW > 0 ? Math.max(1, Math.floor(maxWidth / charW)) : Infinity;
|
|
2795
|
+
const lines = [];
|
|
2796
|
+
for (const paragraph of text.split("\n")) {
|
|
2797
|
+
if (paragraph.length <= limit) {
|
|
2798
|
+
lines.push(paragraph);
|
|
2799
|
+
continue;
|
|
2800
|
+
}
|
|
2801
|
+
let current = "";
|
|
2802
|
+
for (const word of paragraph.split(" ")) {
|
|
2803
|
+
const candidate = current.length === 0 ? word : `${current} ${word}`;
|
|
2804
|
+
if (candidate.length <= limit) {
|
|
2805
|
+
current = candidate;
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
if (current.length > 0) lines.push(current);
|
|
2809
|
+
let rest = word;
|
|
2810
|
+
while (rest.length > limit) {
|
|
2811
|
+
lines.push(rest.slice(0, limit));
|
|
2812
|
+
rest = rest.slice(limit);
|
|
2813
|
+
}
|
|
2814
|
+
current = rest;
|
|
2815
|
+
}
|
|
2816
|
+
lines.push(current);
|
|
2817
|
+
}
|
|
2818
|
+
return lines;
|
|
2819
|
+
}
|
|
2820
|
+
function anchorFor(align) {
|
|
2821
|
+
switch (align) {
|
|
2822
|
+
case "start":
|
|
2823
|
+
case "start-legacy":
|
|
2824
|
+
return "start";
|
|
2825
|
+
case "end":
|
|
2826
|
+
case "end-legacy":
|
|
2827
|
+
return "end";
|
|
2828
|
+
default:
|
|
2829
|
+
return "middle";
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
function wrapRichTextLines(source, fontSize, maxWidth = Infinity) {
|
|
2833
|
+
const charW = fontSize * AVG_CHAR_WIDTH;
|
|
2834
|
+
const limit = Number.isFinite(maxWidth) && charW > 0 ? Math.max(1, Math.floor(maxWidth / charW)) : Infinity;
|
|
2835
|
+
const lines = [];
|
|
2836
|
+
for (const block of richTextToBlocks(source)) {
|
|
2837
|
+
let line = [];
|
|
2838
|
+
let length = 0;
|
|
2839
|
+
const push = () => {
|
|
2840
|
+
while (line.length > 0 && /^\s+$/u.test(line[line.length - 1].text)) line.pop();
|
|
2841
|
+
lines.push(mergeSvgRuns(line));
|
|
2842
|
+
line = [];
|
|
2843
|
+
length = 0;
|
|
2844
|
+
};
|
|
2845
|
+
for (const run of block.runs) {
|
|
2846
|
+
for (const token of run.text.split(/(\s+)/u).filter((t) => t.length > 0)) {
|
|
2847
|
+
const isSpace = /^\s+$/u.test(token);
|
|
2848
|
+
if (!isSpace && length > 0 && length + token.length > limit) push();
|
|
2849
|
+
if (isSpace && length === 0) continue;
|
|
2850
|
+
if (!isSpace && token.length > limit) {
|
|
2851
|
+
let rest = token;
|
|
2852
|
+
while (rest.length > limit) {
|
|
2853
|
+
if (length > 0) push();
|
|
2854
|
+
line.push({ ...run, text: rest.slice(0, limit) });
|
|
2855
|
+
length = limit;
|
|
2856
|
+
rest = rest.slice(limit);
|
|
2857
|
+
push();
|
|
2858
|
+
}
|
|
2859
|
+
if (rest.length > 0) {
|
|
2860
|
+
line.push({ ...run, text: rest });
|
|
2861
|
+
length += rest.length;
|
|
2862
|
+
}
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
line.push({ ...run, text: token });
|
|
2866
|
+
length += token.length;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
push();
|
|
2870
|
+
}
|
|
2871
|
+
return lines;
|
|
2872
|
+
}
|
|
2873
|
+
function mergeSvgRuns(runs) {
|
|
2874
|
+
const out = [];
|
|
2875
|
+
for (const run of runs) {
|
|
2876
|
+
const last = out[out.length - 1];
|
|
2877
|
+
if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.strike === run.strike && last.code === run.code && last.href === run.href) last.text += run.text;
|
|
2878
|
+
else out.push({ ...run });
|
|
2879
|
+
}
|
|
2880
|
+
return out;
|
|
2881
|
+
}
|
|
2882
|
+
function runAttrs(run, opts) {
|
|
2883
|
+
const decorations = [];
|
|
2884
|
+
if (run.underline) decorations.push("underline");
|
|
2885
|
+
if (run.strike) decorations.push("line-through");
|
|
2886
|
+
return {
|
|
2887
|
+
"font-weight": run.bold ? "bold" : opts.fontWeight === void 0 ? void 0 : String(opts.fontWeight),
|
|
2888
|
+
"font-style": run.italic ? "italic" : void 0,
|
|
2889
|
+
"text-decoration": decorations.length === 0 ? void 0 : decorations.join(" ")
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
function textToSvg(source, box, opts) {
|
|
2893
|
+
const rich = isRichText(source);
|
|
2894
|
+
const plain = richTextToText(source);
|
|
2895
|
+
if (plain.length === 0) return "";
|
|
2896
|
+
const padding = opts.padding ?? 0;
|
|
2897
|
+
const innerX = box.x + padding;
|
|
2898
|
+
const innerY = box.y + padding;
|
|
2899
|
+
const innerW = Math.max(0, box.w - padding * 2);
|
|
2900
|
+
const innerH = Math.max(0, box.h - padding * 2);
|
|
2901
|
+
const wrapWidth = opts.wrap ? innerW : Infinity;
|
|
2902
|
+
const lineCount = rich ? wrapRichTextLines(source, opts.fontSize, wrapWidth).length : wrapTextLines(plain, opts.fontSize, wrapWidth).length;
|
|
2903
|
+
const lineH = opts.fontSize * LINE_HEIGHT;
|
|
2904
|
+
const blockH = lineCount * lineH;
|
|
2905
|
+
const anchor = anchorFor(alignOf(opts));
|
|
2906
|
+
const x = anchor === "start" ? innerX : anchor === "end" ? innerX + innerW : innerX + innerW / 2;
|
|
2907
|
+
const top = opts.verticalAlign === "start" ? innerY : opts.verticalAlign === "end" ? innerY + innerH - blockH : innerY + (innerH - blockH) / 2;
|
|
2908
|
+
const lineY = (i) => top + lineH * i + lineH / 2;
|
|
2909
|
+
const spans = rich ? wrapRichTextLines(source, opts.fontSize, wrapWidth).map((runs, i) => {
|
|
2910
|
+
const content = runs.length === 0 ? "\u200B" : runs.map((run) => {
|
|
2911
|
+
const a2 = attrs(runAttrs(run, opts));
|
|
2912
|
+
const inner = `<tspan${a2.length === 0 ? "" : ` ${a2}`}>${escapeXml(run.text)}</tspan>`;
|
|
2913
|
+
const href = safeHref(run.href);
|
|
2914
|
+
return href === null ? inner : `<a ${attrs({ href })}>${inner}</a>`;
|
|
2915
|
+
}).join("");
|
|
2916
|
+
return `<tspan x="${svgNum(x)}" y="${svgNum(lineY(i))}">${content}</tspan>`;
|
|
2917
|
+
}).join("") : wrapTextLines(plain, opts.fontSize, wrapWidth).map((line, i) => `<tspan x="${svgNum(x)}" y="${svgNum(lineY(i))}">${line.length === 0 ? "\u200B" : escapeXml(line)}</tspan>`).join("");
|
|
2918
|
+
const a = attrs({
|
|
2919
|
+
"font-family": opts.fontFamily,
|
|
2920
|
+
"font-size": opts.fontSize,
|
|
2921
|
+
"font-weight": opts.fontWeight === void 0 ? void 0 : String(opts.fontWeight),
|
|
2922
|
+
fill: opts.color,
|
|
2923
|
+
"text-anchor": anchor,
|
|
2924
|
+
"dominant-baseline": "central",
|
|
2925
|
+
"xml:space": "preserve"
|
|
2926
|
+
});
|
|
2927
|
+
return `<text ${a}>${spans}</text>`;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/export/shape-svg.ts
|
|
2931
|
+
function geometryFallbackSvg(editor, shape) {
|
|
2932
|
+
const geometry = editor.getShapeGeometry(shape);
|
|
2933
|
+
const style = editor.getShapeUtil(shape).getRenderStyle(shape) ?? {
|
|
2934
|
+
fill: 0,
|
|
2935
|
+
stroke: hexToRgba(LIGHT_THEME.text),
|
|
2936
|
+
strokeWidth: 1,
|
|
2937
|
+
dash: 0};
|
|
2938
|
+
return geometryToSvgPaths(geometry, style);
|
|
2939
|
+
}
|
|
2940
|
+
var defaultShapeSvgRenderer = (editor, shape) => geometryFallbackSvg(editor, shape);
|
|
2941
|
+
function svgId(id) {
|
|
2942
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
2943
|
+
}
|
|
2944
|
+
function labelBox(editor, shape) {
|
|
2945
|
+
const label = geometryLabels(editor.getShapeGeometry(shape))[0];
|
|
2946
|
+
if (!label) return void 0;
|
|
2947
|
+
const b = label.bounds;
|
|
2948
|
+
return { x: b.x, y: b.y, w: b.w, h: b.h };
|
|
2949
|
+
}
|
|
2950
|
+
var geoSvg = (editor, shape) => {
|
|
2951
|
+
const { text, font, size, scale, labelColor, align, verticalAlign } = shape.props;
|
|
2952
|
+
let out = geometryFallbackSvg(editor, shape);
|
|
2953
|
+
if (!text) return out;
|
|
2954
|
+
const box = labelBox(editor, shape);
|
|
2955
|
+
if (!box) return out;
|
|
2956
|
+
out += textToSvg(text, box, {
|
|
2957
|
+
fontFamily: getFontFamily(font),
|
|
2958
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
2959
|
+
color: getTextCssColor(labelColor),
|
|
2960
|
+
align,
|
|
2961
|
+
verticalAlign,
|
|
2962
|
+
padding: GEO_LABEL_PADDING * scale,
|
|
2963
|
+
wrap: true
|
|
2964
|
+
});
|
|
2965
|
+
return out;
|
|
2966
|
+
};
|
|
2967
|
+
var drawSvg = (editor, shape) => geometryFallbackSvg(editor, shape);
|
|
2968
|
+
var lineSvg = (editor, shape) => geometryFallbackSvg(editor, shape);
|
|
2969
|
+
var arrowSvg = (editor, shape, ctx) => {
|
|
2970
|
+
const { text, font, size, scale, labelColor } = shape.props;
|
|
2971
|
+
let out = geometryFallbackSvg(editor, shape);
|
|
2972
|
+
if (!text) return out;
|
|
2973
|
+
const box = labelBox(editor, shape);
|
|
2974
|
+
if (!box) return out;
|
|
2975
|
+
out += `<rect ${attrs({ x: box.x, y: box.y, width: box.w, height: box.h, rx: 4, fill: ctx.background })}/>`;
|
|
2976
|
+
out += textToSvg(text, box, {
|
|
2977
|
+
fontFamily: getFontFamily(font),
|
|
2978
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
2979
|
+
color: getTextCssColor(labelColor),
|
|
2980
|
+
align: "middle",
|
|
2981
|
+
verticalAlign: "middle",
|
|
2982
|
+
padding: ARROW_LABEL_PADDING * scale,
|
|
2983
|
+
wrap: false
|
|
2984
|
+
});
|
|
2985
|
+
return out;
|
|
2986
|
+
};
|
|
2987
|
+
var textSvg = (editor, shape) => {
|
|
2988
|
+
const { text, font, size, scale, color, textAlign, autoSize } = shape.props;
|
|
2989
|
+
const b = editor.getShapeGeometry(shape).bounds;
|
|
2990
|
+
return textToSvg(text, { x: b.x, y: b.y, w: b.w, h: b.h }, {
|
|
2991
|
+
fontFamily: getFontFamily(font),
|
|
2992
|
+
fontSize: FONT_SIZES[size] * scale,
|
|
2993
|
+
color: getTextCssColor(color),
|
|
2994
|
+
align: textAlign,
|
|
2995
|
+
verticalAlign: "start",
|
|
2996
|
+
wrap: !autoSize
|
|
2997
|
+
});
|
|
2998
|
+
};
|
|
2999
|
+
function noteTrimDefs(shape, bottom, ids) {
|
|
3000
|
+
const scale = shape.props.scale;
|
|
3001
|
+
const gradient = `<linearGradient ${attrs({ id: ids.gradient, x1: 0, y1: 0, x2: 0, y2: 1 })}><stop ${attrs({ offset: 0, "stop-color": getNoteGradientTopFrom(bottom) })}/><stop ${attrs({ offset: 1, "stop-color": bottom })}/></linearGradient>`;
|
|
3002
|
+
const filter = `<filter ${attrs({ id: ids.shadow, x: "-50%", y: "-50%", width: "200%", height: "200%" })}><feGaussianBlur ${attrs({ stdDeviation: getNoteShadowSvgRect(0, 0, scale).stdDeviation })}/></filter>`;
|
|
3003
|
+
return `<defs>${gradient}${filter}</defs>`;
|
|
3004
|
+
}
|
|
3005
|
+
var noteSvg = (editor, shape) => {
|
|
3006
|
+
const { text, font, color, labelColor, align, verticalAlign, scale, growY } = shape.props;
|
|
3007
|
+
const w = NOTE_SIZE * scale;
|
|
3008
|
+
const h = NOTE_SIZE * scale + growY;
|
|
3009
|
+
const style = editor.getShapeUtil(shape).getRenderStyle(shape);
|
|
3010
|
+
const bottom = (style && rgbaToHex(style.fill)) ?? getNoteFillCssColor(color);
|
|
3011
|
+
const suffix = svgId(shape.id);
|
|
3012
|
+
const ids = { gradient: `mc-note-fill-${suffix}`, shadow: `mc-note-shadow-${suffix}` };
|
|
3013
|
+
let out = noteTrimDefs(shape, bottom, ids);
|
|
3014
|
+
const sh = getNoteShadowSvgRect(w, h, scale);
|
|
3015
|
+
if (sh.w > 0 && sh.h > 0) {
|
|
3016
|
+
out += `<rect ${attrs({
|
|
3017
|
+
x: sh.x,
|
|
3018
|
+
y: sh.y,
|
|
3019
|
+
width: sh.w,
|
|
3020
|
+
height: sh.h,
|
|
3021
|
+
fill: NOTE_SHADOW_COLOR,
|
|
3022
|
+
"fill-opacity": NOTE_SHADOW_OPACITY,
|
|
3023
|
+
filter: `url(#${ids.shadow})`
|
|
3024
|
+
})}/>`;
|
|
3025
|
+
}
|
|
3026
|
+
out += `<rect ${attrs({ x: 0, y: 0, width: w, height: h, fill: `url(#${ids.gradient})` })}/>`;
|
|
3027
|
+
if (!text) return out;
|
|
3028
|
+
const textColor = labelColor === "black" ? getNoteTextCssColor(color) : LIGHT_THEME[labelColor].solid;
|
|
3029
|
+
out += textToSvg(text, { x: 0, y: 0, w, h }, {
|
|
3030
|
+
fontFamily: getFontFamily(font),
|
|
3031
|
+
fontSize: getNoteFontSize(shape),
|
|
3032
|
+
color: textColor,
|
|
3033
|
+
align,
|
|
3034
|
+
verticalAlign,
|
|
3035
|
+
padding: NOTE_PADDING * scale,
|
|
3036
|
+
wrap: true
|
|
3037
|
+
});
|
|
3038
|
+
return out;
|
|
3039
|
+
};
|
|
3040
|
+
var frameSvg = (_editor, shape) => {
|
|
3041
|
+
const { w, h, name } = shape.props;
|
|
3042
|
+
let out = `<rect ${attrs({ x: 0, y: 0, width: w, height: h, fill: FRAME_FILL, stroke: FRAME_STROKE, "stroke-width": FRAME_STROKE_WIDTH })}/>`;
|
|
3043
|
+
if (name) {
|
|
3044
|
+
const [firstLine = ""] = name.split("\n");
|
|
3045
|
+
out += textToSvg(firstLine, { x: 0, y: -FRAME_NAME_OFFSET, w, h: FRAME_NAME_HEIGHT }, {
|
|
3046
|
+
fontFamily: getFontFamily("sans"),
|
|
3047
|
+
fontSize: FRAME_NAME_FONT_SIZE,
|
|
3048
|
+
color: FRAME_NAME_COLOR,
|
|
3049
|
+
align: "start",
|
|
3050
|
+
verticalAlign: "end",
|
|
3051
|
+
wrap: false
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
return out;
|
|
3055
|
+
};
|
|
3056
|
+
var bookmarkSvg = (editor, shape) => {
|
|
3057
|
+
const { w, h } = shape.props;
|
|
3058
|
+
const card = getBookmarkCard(editor, shape);
|
|
3059
|
+
const layout = getBookmarkLayout(w, h);
|
|
3060
|
+
const sans = getFontFamily("sans");
|
|
3061
|
+
let out = `<rect ${attrs({
|
|
3062
|
+
x: 0,
|
|
3063
|
+
y: 0,
|
|
3064
|
+
width: w,
|
|
3065
|
+
height: h,
|
|
3066
|
+
rx: BOOKMARK_RADIUS,
|
|
3067
|
+
fill: BOOKMARK_FILL,
|
|
3068
|
+
stroke: BOOKMARK_STROKE,
|
|
3069
|
+
"stroke-width": BOOKMARK_STROKE_WIDTH
|
|
3070
|
+
})}/>`;
|
|
3071
|
+
if (layout.banner.h > 0) {
|
|
3072
|
+
out += `<rect ${attrs({ x: 0, y: 0, width: layout.banner.w, height: layout.banner.h, fill: BOOKMARK_BANNER_FILL })}/>`;
|
|
3073
|
+
}
|
|
3074
|
+
const title = card.hasAsset ? card.title : card.hostname || card.url;
|
|
3075
|
+
out += textToSvg(title, layout.title, {
|
|
3076
|
+
fontFamily: sans,
|
|
3077
|
+
fontSize: BOOKMARK_TITLE_FONT_SIZE,
|
|
3078
|
+
color: card.hasAsset ? BOOKMARK_TITLE_COLOR : BOOKMARK_META_COLOR,
|
|
3079
|
+
align: "start",
|
|
3080
|
+
verticalAlign: "start",
|
|
3081
|
+
...card.hasAsset ? { fontWeight: 600 } : {},
|
|
3082
|
+
wrap: true
|
|
3083
|
+
});
|
|
3084
|
+
if (!card.hasAsset) return out;
|
|
3085
|
+
out += textToSvg(card.description, layout.description, {
|
|
3086
|
+
fontFamily: sans,
|
|
3087
|
+
fontSize: BOOKMARK_TEXT_FONT_SIZE,
|
|
3088
|
+
color: BOOKMARK_TEXT_COLOR,
|
|
3089
|
+
align: "start",
|
|
3090
|
+
verticalAlign: "start",
|
|
3091
|
+
wrap: true
|
|
3092
|
+
});
|
|
3093
|
+
if (card.favicon) {
|
|
3094
|
+
out += `<rect ${attrs({ ...boxAttrs(layout.favicon), rx: 2, fill: BOOKMARK_BANNER_FILL })}/>`;
|
|
3095
|
+
}
|
|
3096
|
+
out += textToSvg(card.hostname, layout.hostname, {
|
|
3097
|
+
fontFamily: sans,
|
|
3098
|
+
fontSize: BOOKMARK_META_FONT_SIZE,
|
|
3099
|
+
color: BOOKMARK_META_COLOR,
|
|
3100
|
+
align: "start",
|
|
3101
|
+
verticalAlign: "middle",
|
|
3102
|
+
wrap: false
|
|
3103
|
+
});
|
|
3104
|
+
return out;
|
|
3105
|
+
};
|
|
3106
|
+
function boxAttrs(box) {
|
|
3107
|
+
return { x: box.x, y: box.y, width: box.w, height: box.h };
|
|
3108
|
+
}
|
|
3109
|
+
var embedSvg = (_editor, shape) => {
|
|
3110
|
+
const { w, h, url } = shape.props;
|
|
3111
|
+
const match = getEmbedDefinition(url);
|
|
3112
|
+
const label = match ? `${match.definition.title}
|
|
3113
|
+
${url}` : url;
|
|
3114
|
+
let out = `<rect ${attrs({
|
|
3115
|
+
x: 0,
|
|
3116
|
+
y: 0,
|
|
3117
|
+
width: w,
|
|
3118
|
+
height: h,
|
|
3119
|
+
rx: EMBED_RADIUS,
|
|
3120
|
+
fill: EMBED_PLACEHOLDER_FILL,
|
|
3121
|
+
stroke: EMBED_PLACEHOLDER_STROKE,
|
|
3122
|
+
"stroke-width": 1,
|
|
3123
|
+
"stroke-dasharray": "4 4"
|
|
3124
|
+
})}/>`;
|
|
3125
|
+
out += textToSvg(label, { x: 0, y: 0, w, h }, {
|
|
3126
|
+
fontFamily: getFontFamily("sans"),
|
|
3127
|
+
fontSize: EMBED_PLACEHOLDER_FONT_SIZE,
|
|
3128
|
+
color: EMBED_PLACEHOLDER_TEXT,
|
|
3129
|
+
align: "middle",
|
|
3130
|
+
verticalAlign: "middle",
|
|
3131
|
+
padding: EMBED_PLACEHOLDER_PADDING,
|
|
3132
|
+
wrap: true
|
|
3133
|
+
});
|
|
3134
|
+
return out;
|
|
3135
|
+
};
|
|
3136
|
+
var videoSvg = (editor, shape) => {
|
|
3137
|
+
const { w, h, altText } = shape.props;
|
|
3138
|
+
const hasSource = getVideoSource(editor, shape) !== null;
|
|
3139
|
+
let out = `<rect ${attrs({
|
|
3140
|
+
x: 0,
|
|
3141
|
+
y: 0,
|
|
3142
|
+
width: w,
|
|
3143
|
+
height: h,
|
|
3144
|
+
fill: VIDEO_PLACEHOLDER_FILL,
|
|
3145
|
+
stroke: VIDEO_PLACEHOLDER_STROKE,
|
|
3146
|
+
"stroke-width": 1,
|
|
3147
|
+
"stroke-dasharray": hasSource ? void 0 : "4 4"
|
|
3148
|
+
})}/>`;
|
|
3149
|
+
const points = getVideoPlayTriangle(w, h).map((p) => `${svgNum(p.x)},${svgNum(p.y)}`).join(" ");
|
|
3150
|
+
out += `<polygon ${attrs({ points, fill: VIDEO_PLAY_COLOR })}/>`;
|
|
3151
|
+
if (altText) {
|
|
3152
|
+
out += `<title>${escapeXml(altText)}</title>`;
|
|
3153
|
+
}
|
|
3154
|
+
return out;
|
|
3155
|
+
};
|
|
3156
|
+
var shapeSvgRenderers = /* @__PURE__ */ new Map([
|
|
3157
|
+
["geo", geoSvg],
|
|
3158
|
+
["draw", drawSvg],
|
|
3159
|
+
["line", lineSvg],
|
|
3160
|
+
["arrow", arrowSvg],
|
|
3161
|
+
["text", textSvg],
|
|
3162
|
+
["note", noteSvg],
|
|
3163
|
+
["frame", frameSvg],
|
|
3164
|
+
["bookmark", bookmarkSvg],
|
|
3165
|
+
["embed", embedSvg],
|
|
3166
|
+
["video", videoSvg]
|
|
3167
|
+
]);
|
|
3168
|
+
function registerShapeSvgRenderer(type, renderer) {
|
|
3169
|
+
shapeSvgRenderers.set(type, renderer);
|
|
3170
|
+
}
|
|
3171
|
+
function svgResultToMarkup(value) {
|
|
3172
|
+
if (value === void 0 || value === null || typeof value === "boolean") return void 0;
|
|
3173
|
+
if (typeof value === "string") return value;
|
|
3174
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
3175
|
+
return renderToStaticMarkup(value);
|
|
3176
|
+
}
|
|
3177
|
+
function shapeToSvg(editor, shape, ctx) {
|
|
3178
|
+
const util = editor.getShapeUtil(shape);
|
|
3179
|
+
const own = svgResultToMarkup(util.toSvg?.(shape, ctx));
|
|
3180
|
+
if (own !== void 0) return own;
|
|
3181
|
+
const renderer = shapeSvgRenderers.get(shape.type) ?? defaultShapeSvgRenderer;
|
|
3182
|
+
return renderer(editor, shape, ctx);
|
|
3183
|
+
}
|
|
3184
|
+
function shapeToBackgroundSvg(editor, shape, ctx) {
|
|
3185
|
+
return svgResultToMarkup(editor.getShapeUtil(shape).toBackgroundSvg?.(shape, ctx));
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// src/export/svg.ts
|
|
3189
|
+
var SVG_EXPORT_DEFAULT_PADDING = 32;
|
|
3190
|
+
var SVG_LIGHT_BACKGROUND = "#f9fafb";
|
|
3191
|
+
var SVG_DARK_BACKGROUND = "#101011";
|
|
3192
|
+
function getExportShapes(editor, ids) {
|
|
3193
|
+
const sorted = editor.getCurrentPageShapesSorted();
|
|
3194
|
+
let roots = ids;
|
|
3195
|
+
if (!roots) {
|
|
3196
|
+
const selected = editor.getSelectedShapeIds();
|
|
3197
|
+
if (selected.length === 0) return sorted;
|
|
3198
|
+
roots = selected;
|
|
3199
|
+
}
|
|
3200
|
+
const included = /* @__PURE__ */ new Set();
|
|
3201
|
+
const visit = (id) => {
|
|
3202
|
+
if (included.has(id)) return;
|
|
3203
|
+
included.add(id);
|
|
3204
|
+
for (const child of editor.getSortedChildIdsForParent(id)) visit(child);
|
|
3205
|
+
};
|
|
3206
|
+
for (const id of roots) if (editor.getShape(id)) visit(id);
|
|
3207
|
+
return sorted.filter((s) => included.has(s.id));
|
|
3208
|
+
}
|
|
3209
|
+
function getExportBounds(editor, shapes) {
|
|
3210
|
+
const boxes = [];
|
|
3211
|
+
for (const shape of shapes) {
|
|
3212
|
+
const b = editor.getShapePageBounds(shape);
|
|
3213
|
+
if (b) boxes.push(b);
|
|
3214
|
+
}
|
|
3215
|
+
return boxes.length === 0 ? void 0 : Box.Common(boxes);
|
|
3216
|
+
}
|
|
3217
|
+
var clipCounter = 0;
|
|
3218
|
+
function getSvgString(editor, ids, opts = {}) {
|
|
3219
|
+
const padding = typeof opts.padding === "number" ? opts.padding : SVG_EXPORT_DEFAULT_PADDING;
|
|
3220
|
+
const scale = opts.scale ?? 1;
|
|
3221
|
+
const darkMode = opts.darkMode ?? false;
|
|
3222
|
+
const shapes = getExportShapes(editor, ids);
|
|
3223
|
+
if (shapes.length === 0) return void 0;
|
|
3224
|
+
const bounds = getExportBounds(editor, shapes);
|
|
3225
|
+
if (!bounds) return void 0;
|
|
3226
|
+
const view = Box.Expand(bounds, padding);
|
|
3227
|
+
const width = Math.max(1, Math.ceil(view.w * scale));
|
|
3228
|
+
const height = Math.max(1, Math.ceil(view.h * scale));
|
|
3229
|
+
const background = darkMode ? SVG_DARK_BACKGROUND : SVG_LIGHT_BACKGROUND;
|
|
3230
|
+
const ctx = { darkMode, background };
|
|
3231
|
+
const included = new Set(shapes.map((s) => s.id));
|
|
3232
|
+
const defs = [];
|
|
3233
|
+
const backdrop = [];
|
|
3234
|
+
const body = [];
|
|
3235
|
+
const prefix = `mc${(clipCounter++).toString(36)}`;
|
|
3236
|
+
const emit = (shape) => {
|
|
3237
|
+
const transform = editor.getShapePageTransform(shape);
|
|
3238
|
+
const g = attrs({
|
|
3239
|
+
transform: matrixAttr(transform),
|
|
3240
|
+
opacity: shape.opacity < 1 ? shape.opacity : void 0,
|
|
3241
|
+
"data-shape-type": shape.type
|
|
3242
|
+
});
|
|
3243
|
+
const behind = shapeToBackgroundSvg(editor, shape, ctx);
|
|
3244
|
+
if (behind !== void 0) backdrop.push(`<g ${g} data-shape-background="true">${behind}</g>`);
|
|
3245
|
+
const inner = shapeToSvg(editor, shape, ctx);
|
|
3246
|
+
body.push(`<g ${g}>${inner}</g>`);
|
|
3247
|
+
const children = editor.getSortedChildIdsForParent(shape.id).filter((id) => included.has(id));
|
|
3248
|
+
if (children.length === 0) return;
|
|
3249
|
+
if (shape.type === "frame") {
|
|
3250
|
+
const b = editor.getShapeGeometry(shape).bounds;
|
|
3251
|
+
const clipId = `${prefix}-clip-${defs.length}`;
|
|
3252
|
+
defs.push(
|
|
3253
|
+
`<clipPath id="${clipId}"><rect ${attrs({ transform: matrixAttr(transform), x: b.x, y: b.y, width: b.w, height: b.h })}/></clipPath>`
|
|
3254
|
+
);
|
|
3255
|
+
body.push(`<g clip-path="url(#${clipId})">`);
|
|
3256
|
+
for (const id of children) {
|
|
3257
|
+
const child = editor.getShape(id);
|
|
3258
|
+
if (child) emit(child);
|
|
3259
|
+
}
|
|
3260
|
+
body.push(`</g>`);
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
for (const id of children) {
|
|
3264
|
+
const child = editor.getShape(id);
|
|
3265
|
+
if (child) emit(child);
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
for (const shape of shapes) {
|
|
3269
|
+
const parentIncluded = included.has(shape.parentId);
|
|
3270
|
+
if (!parentIncluded) emit(shape);
|
|
3271
|
+
}
|
|
3272
|
+
const viewBox = `${svgNum(view.x)} ${svgNum(view.y)} ${svgNum(view.w)} ${svgNum(view.h)}`;
|
|
3273
|
+
const parts = [];
|
|
3274
|
+
parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${viewBox}">`);
|
|
3275
|
+
if (defs.length > 0) parts.push(`<defs>${defs.join("")}</defs>`);
|
|
3276
|
+
if (opts.background) {
|
|
3277
|
+
parts.push(`<rect ${attrs({ x: view.x, y: view.y, width: view.w, height: view.h, fill: background })}/>`);
|
|
3278
|
+
}
|
|
3279
|
+
parts.push(...backdrop);
|
|
3280
|
+
parts.push(...body);
|
|
3281
|
+
parts.push(`</svg>`);
|
|
3282
|
+
return { svg: parts.join(""), width, height };
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
// src/export/image.ts
|
|
3286
|
+
var MIME = {
|
|
3287
|
+
svg: "image/svg+xml",
|
|
3288
|
+
png: "image/png",
|
|
3289
|
+
jpeg: "image/jpeg",
|
|
3290
|
+
webp: "image/webp"
|
|
3291
|
+
};
|
|
3292
|
+
function hasDom() {
|
|
3293
|
+
return typeof document !== "undefined" && typeof window !== "undefined";
|
|
3294
|
+
}
|
|
3295
|
+
async function exportToBlob(editor, opts) {
|
|
3296
|
+
const { ids, format, quality, pixelRatio, ...svgOpts } = opts;
|
|
3297
|
+
const result = getSvgString(editor, ids, svgOpts);
|
|
3298
|
+
if (!result) throw new Error("Nothing to export");
|
|
3299
|
+
const svgBlob = new Blob([result.svg], { type: MIME.svg });
|
|
3300
|
+
if (format === "svg") return svgBlob;
|
|
3301
|
+
if (!hasDom()) {
|
|
3302
|
+
throw new Error(`Raster export ("${format}") needs a browser environment; only "svg" is available here`);
|
|
3303
|
+
}
|
|
3304
|
+
const ratio = pixelRatio ?? (window.devicePixelRatio || 1);
|
|
3305
|
+
const url = URL.createObjectURL(svgBlob);
|
|
3306
|
+
try {
|
|
3307
|
+
const image = await loadImage(url);
|
|
3308
|
+
const canvas = document.createElement("canvas");
|
|
3309
|
+
canvas.width = Math.max(1, Math.round(result.width * ratio));
|
|
3310
|
+
canvas.height = Math.max(1, Math.round(result.height * ratio));
|
|
3311
|
+
const ctx = canvas.getContext("2d");
|
|
3312
|
+
if (!ctx) throw new Error("Could not create a 2D canvas context");
|
|
3313
|
+
if (format === "jpeg" && !svgOpts.background) {
|
|
3314
|
+
ctx.fillStyle = svgOpts.darkMode ? "#101011" : "#f9fafb";
|
|
3315
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
3316
|
+
}
|
|
3317
|
+
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
3318
|
+
return await canvasToBlob(canvas, MIME[format], quality);
|
|
3319
|
+
} finally {
|
|
3320
|
+
URL.revokeObjectURL(url);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
function loadImage(url) {
|
|
3324
|
+
return new Promise((resolve, reject) => {
|
|
3325
|
+
const image = new Image();
|
|
3326
|
+
image.decoding = "sync";
|
|
3327
|
+
image.onload = () => resolve(image);
|
|
3328
|
+
image.onerror = () => reject(new Error("Could not rasterize the SVG"));
|
|
3329
|
+
image.src = url;
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
function canvasToBlob(canvas, type, quality) {
|
|
3333
|
+
return new Promise((resolve, reject) => {
|
|
3334
|
+
canvas.toBlob(
|
|
3335
|
+
(blob) => {
|
|
3336
|
+
if (blob) resolve(blob);
|
|
3337
|
+
else reject(new Error(`Canvas could not encode "${type}"`));
|
|
3338
|
+
},
|
|
3339
|
+
type,
|
|
3340
|
+
quality
|
|
3341
|
+
);
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
function downloadBlob(blob, filename) {
|
|
3345
|
+
if (!hasDom()) return;
|
|
3346
|
+
const url = URL.createObjectURL(blob);
|
|
3347
|
+
const anchor = document.createElement("a");
|
|
3348
|
+
anchor.href = url;
|
|
3349
|
+
anchor.download = filename;
|
|
3350
|
+
anchor.rel = "noopener";
|
|
3351
|
+
anchor.style.display = "none";
|
|
3352
|
+
document.body.appendChild(anchor);
|
|
3353
|
+
anchor.click();
|
|
3354
|
+
anchor.remove();
|
|
3355
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
3356
|
+
}
|
|
3357
|
+
async function copyBlobToClipboard(blob) {
|
|
3358
|
+
const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
|
|
3359
|
+
if (!clipboard) throw new Error("Clipboard API is not available");
|
|
3360
|
+
if (blob.type === MIME.svg || !("write" in clipboard) || typeof ClipboardItem === "undefined") {
|
|
3361
|
+
if (blob.type !== MIME.svg && !("writeText" in clipboard)) throw new Error("Clipboard does not accept images");
|
|
3362
|
+
await clipboard.writeText(await blob.text());
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
await clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
export { ARROW_KINDS, ARROW_LABEL_PADDING, ARROW_TERMINAL_GAP_STROKES, AVG_CHAR_WIDTH, ArrowShapeUtil, DEFAULT_GEO_TYPE_DEFINITIONS, ELBOW_CORNER_STROKES, FALLBACK_THEME_COLORS, FRAME_FILL, FRAME_NAME_COLOR, FRAME_NAME_FONT_SIZE, FRAME_NAME_GAP, FRAME_NAME_HEIGHT, FRAME_NAME_OFFSET, FRAME_STROKE, FRAME_STROKE_WIDTH, FrameShapeUtil, GEO_DEFAULT_SIZE, GEO_LABEL_PADDING, GeoShapeUtil, LINE_HEIGHT, MAX_TEXT_TEXTURE_PX, NOTE_GRADIENT_TOP_SCALE, NOTE_PADDING, NOTE_SHADOW_BLUR, NOTE_SHADOW_COLOR, NOTE_SHADOW_OFFSET_Y, NOTE_SHADOW_OPACITY, NOTE_SHADOW_SPREAD, NOTE_SIZE, NoteShapeUtil, RICH_TEXT_BLOCK_CSS, SVG_DARK_BACKGROUND, SVG_EXPORT_DEFAULT_PADDING, SVG_LIGHT_BACKGROUND, TEXT_SHAPE_MIN_WIDTH, TextLabel, TextMeasure, TextShapeUtil, VIDEO_HEIGHT, VIDEO_PLACEHOLDER_FILL, VIDEO_PLACEHOLDER_STROKE, VIDEO_PLAY_COLOR, VIDEO_PLAY_SIZE, VIDEO_TIME_EPSILON, VIDEO_WIDTH, VideoShapeUtil, alignToJustify, alignToTextAlign, applyTransform, bodyToGeometry, computeGrowY, copyBlobToClipboard, dashArray, defaultShapeSvgRenderer, downloadBlob, escapeXml, estimateTextSize, exportToBlob, geoShapeMigrations, geoShapeVersions, geometryFallbackSvg, geometryToSvgPaths, getAnchorInShapeSpace, getArrowBindingTargetAtPoint, getArrowBindings, getArrowBody, getArrowDisplayValues, getArrowTerminalGap, getArrowTerminalsInArrowSpace, getArrowheadGeometry, getArrowheadInset, getArrowheadLength, getBendFromPoint, getBodyLength, getBoundElbowAxes, getDashId, getDominantAxis, getElbowBody, getElbowMidPointFromPoint, getElbowRoute, getExportBounds, getExportShapes, getFillRgba, getFontFamily, getFrameDisplayValues, getGeoDisplayValues, getGeoGrowY, getGeoTypeDefinition, getLabelFontFaces, getLabelOpticalLift, getNormalizedAnchor, getNoteBodyGradientCss, getNoteDisplayValues, getNoteFillCssColor, getNoteFillRgba, getNoteFontSize, getNoteGradientTopCssColor, getNoteGradientTopFrom, getNoteGrowY, getNoteShadowCss, getNoteShadowSvgRect, getNoteTextCssColor, getOutlineSegments, getPointOnBody, getRichTextExtensions, getStrokeRgba, getSvgString, getTangentOnBody, getTextCssColor, getTextDisplayValues, getTextMeasure, getTextShapeBox, getTextShapeHeight, getTextShapeSize, getTextShapeSizeFor, getTextShapeTextureSpec, getTextTextureKey, getTextTextureScale, getTheme, getThemeColors, getVideoDisplayValues, getVideoPlayTriangle, getVideoSource, hexToCssRgba, horizontalAlignToFlex, horizontalAlignToTextAlign, intersectSegments, isVideoAutoplayAllowed, labelFontFamily, labelFontStyle, labelGapOnBody, labelTextAlign, matrixAttr, measureGeoLabel, measureLabel, measureRichText, readArrowProps, readArrowShape, readGeoProps, readNoteProps, readTextProps, registerShapeSvgRenderer, renderHtmlFromRichTextForMeasurement, renderHtmlFromRichTextWithExtensions, renderTextToCanvas, rgbaToHex, roundElbowCorners, shapeSvgRenderers, shapeToBackgroundSvg, shapeToSvg, shortenBody, svgResultToMarkup, textTextureAlign, textTextureText, textToSvg, toDisplayText, trimTrailingWhitespace, verticalAlignToAlignItems, verticalAlignToFlex, wrapTextLines, wrapTextTextureLines, wrapTextTextureRuns };
|
|
3369
|
+
//# sourceMappingURL=chunk-DMCI6V5Y.js.map
|
|
3370
|
+
//# sourceMappingURL=chunk-DMCI6V5Y.js.map
|