fabricjs-document-engine 1.2.0 → 1.3.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.
Files changed (81) hide show
  1. package/README.md +31 -0
  2. package/README.zh-CN.md +32 -0
  3. package/dist/commands/text-edit.cjs +223 -0
  4. package/dist/commands/text-edit.d.cts +23 -0
  5. package/dist/commands/text-edit.d.ts +23 -0
  6. package/dist/commands/text-edit.js +218 -0
  7. package/dist/fabric/object-registry.cjs +7 -1
  8. package/dist/fabric/object-registry.js +7 -1
  9. package/dist/filters/filter-worker.cjs +281 -0
  10. package/dist/filters/filter-worker.d.cts +26 -0
  11. package/dist/filters/filter-worker.d.ts +26 -0
  12. package/dist/filters/filter-worker.js +281 -0
  13. package/dist/filters/pipeline.cjs +91 -0
  14. package/dist/filters/pipeline.d.cts +35 -0
  15. package/dist/filters/pipeline.d.ts +35 -0
  16. package/dist/filters/pipeline.js +87 -0
  17. package/dist/filters/worker-entry.cjs +78 -0
  18. package/dist/filters/worker-entry.d.cts +29 -0
  19. package/dist/filters/worker-entry.d.ts +29 -0
  20. package/dist/filters/worker-entry.js +78 -0
  21. package/dist/filters.cjs +9 -0
  22. package/dist/filters.d.cts +3 -0
  23. package/dist/filters.d.ts +3 -0
  24. package/dist/filters.js +3 -0
  25. package/dist/import/svg-import.cjs +102 -0
  26. package/dist/import/svg-import.d.cts +1 -0
  27. package/dist/import/svg-import.d.ts +1 -0
  28. package/dist/import/svg-import.js +103 -1
  29. package/dist/index.cjs +7 -0
  30. package/dist/index.d.cts +2 -1
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js +2 -1
  33. package/dist/performance/batch.cjs +31 -0
  34. package/dist/performance/batch.d.cts +4 -0
  35. package/dist/performance/batch.d.ts +4 -0
  36. package/dist/performance/batch.js +31 -0
  37. package/dist/performance/dirty-regions.cjs +378 -0
  38. package/dist/performance/dirty-regions.d.cts +23 -0
  39. package/dist/performance/dirty-regions.d.ts +23 -0
  40. package/dist/performance/dirty-regions.js +378 -0
  41. package/dist/performance/monitor.cjs +60 -0
  42. package/dist/performance/monitor.d.cts +20 -0
  43. package/dist/performance/monitor.d.ts +20 -0
  44. package/dist/performance/monitor.js +60 -0
  45. package/dist/performance/spatial-index.cjs +78 -0
  46. package/dist/performance/spatial-index.d.cts +21 -0
  47. package/dist/performance/spatial-index.d.ts +21 -0
  48. package/dist/performance/spatial-index.js +78 -0
  49. package/dist/performance.cjs +9 -0
  50. package/dist/performance.d.cts +5 -0
  51. package/dist/performance.d.ts +5 -0
  52. package/dist/performance.js +5 -0
  53. package/dist/text/bounded-textbox.cjs +306 -0
  54. package/dist/text/bounded-textbox.d.cts +53 -0
  55. package/dist/text/bounded-textbox.d.ts +53 -0
  56. package/dist/text/bounded-textbox.js +306 -0
  57. package/dist/text/mobile-input.cjs +146 -0
  58. package/dist/text/mobile-input.d.cts +19 -0
  59. package/dist/text/mobile-input.d.ts +19 -0
  60. package/dist/text/mobile-input.js +145 -0
  61. package/dist/text/register.cjs +17 -0
  62. package/dist/text/register.d.cts +5 -0
  63. package/dist/text/register.d.ts +5 -0
  64. package/dist/text/register.js +16 -0
  65. package/dist/text/shaping.cjs +125 -0
  66. package/dist/text/shaping.d.cts +28 -0
  67. package/dist/text/shaping.d.ts +28 -0
  68. package/dist/text/shaping.js +121 -0
  69. package/dist/text/vertical-orientation.cjs +192 -0
  70. package/dist/text/vertical-orientation.d.cts +10 -0
  71. package/dist/text/vertical-orientation.d.ts +10 -0
  72. package/dist/text/vertical-orientation.js +190 -0
  73. package/dist/text/vertical-text.cjs +336 -0
  74. package/dist/text/vertical-text.d.cts +54 -0
  75. package/dist/text/vertical-text.d.ts +54 -0
  76. package/dist/text/vertical-text.js +336 -0
  77. package/dist/text.cjs +21 -0
  78. package/dist/text.d.cts +7 -0
  79. package/dist/text.d.ts +7 -0
  80. package/dist/text.js +7 -0
  81. package/package.json +31 -1
package/README.md CHANGED
@@ -74,6 +74,8 @@ That is the whole setup for a first test. localStorage is fine here; in a real a
74
74
  - **Versions and migration.** Keep named versions, restore any of them as a new revision, and open plain Fabric JSON or documents saved by older versions of this package.
75
75
  - **Large documents.** Objects are created in chunks so the page stays responsive. Loads report progress and can be cancelled with an `AbortSignal`, and a cancelled load leaves the canvas as it was.
76
76
  - **Copy, paste and layers.** A clipboard that keeps group transforms and custom properties and gives every pasted object a new id, plus bring-to-front and send-to-back commands that keep a pinned background in place. Each is one undo step.
77
+ - **Text that behaves.** A Textbox that keeps its width and can clip, add "…" or shrink to fit; cursor positions that match Arabic and ligatures; editable vertical Chinese, Japanese and Korean text; correct typing with Android and iOS keyboards; and text edits in code that keep each letter's style.
78
+ - **Fast with many objects.** Dirty-region rendering redraws only what changed, and image filters run in a Web Worker with progress and cancel.
77
79
  - **Undo and redo.** One user action is one undo step. Transactions group several code changes into one labelled step, and ids survive undo and redo.
78
80
  - **React ready, framework free.** Hooks for React, and a small state store for any other framework.
79
81
  - **Hardened.** Imported documents and SVG files are cleaned and size-limited, undo history has a memory budget, and every feature is tested on Fabric 6 and 7 in Chromium, Firefox and WebKit. Exports are checked pixel by pixel against the canvas.
@@ -428,6 +430,7 @@ const { objects, viewport, warnings } = await engine.importSvg(svgText, {
428
430
  - The result is one group with a fixed layout the size of the viewport, or separate objects with `as: 'objects'`. Either way it is one undo step, and every object gets an id.
429
431
  - Scripts, event handlers, `foreignObject`, links to other files and image addresses that `limits.isAllowedUrl` refuses are removed, and `warnings` says what was removed.
430
432
  - Size limits apply as for documents, so a huge or deeply nested SVG is refused with `UNSAFE_DOCUMENT`.
433
+ - `preserveGroups: true` keeps the SVG's `<g>` groups as Fabric groups, with their ids, opacity, clip paths, classes and `data-*` attributes (Fabric flattens them).
431
434
 
432
435
  ## Version history
433
436
 
@@ -593,6 +596,34 @@ getLayers(engine); // [{ id, type, name, index, visible, locked }], top first
593
596
 
594
597
  The engine saves each object's `name`, so a layers panel keeps its labels. In React, `useLayers(engine)` from `fabricjs-document-engine/react` returns the same list and updates on every change.
595
598
 
599
+ ## Text, rendering speed and filters
600
+
601
+ These fix long-standing Fabric.js issues. Each was reproduced on Fabric 6 and 7 first, and each fix is tested in Chromium, Firefox and WebKit.
602
+
603
+ ```ts
604
+ import { BoundedTextbox, VerticalText, ShapedIText, attachMobileTextInput, textObjects } from 'fabricjs-document-engine/text';
605
+ import { enableDirtyRegionRendering } from 'fabricjs-document-engine/performance';
606
+ import { createFilterWorker } from 'fabricjs-document-engine/filters';
607
+ import { createTextCommands } from 'fabricjs-document-engine';
608
+
609
+ const engine = createDocumentEngine({ canvas, customObjects: textObjects });
610
+ canvas.add(new BoundedTextbox(longText, { width: 240, maxHeight: 120, overflow: 'ellipsis' }));
611
+ canvas.add(new VerticalText('縦書きのテキスト', { fontSize: 32 }));
612
+ canvas.add(new ShapedIText('مرحبا بالعالم', { direction: 'rtl' }));
613
+ attachMobileTextInput(canvas);
614
+ createTextCommands(engine).insertText(title, 0, 'New: ');
615
+ enableDirtyRegionRendering(canvas);
616
+ await createFilterWorker({ engine }).apply(image, [new filters.Blur({ blur: 0.2 })], { onProgress });
617
+ ```
618
+
619
+ - **`BoundedTextbox` (#2376).** Fabric widens a Textbox to its longest word. This one breaks such a word between letters, as CSS `overflow-wrap: anywhere` does. With `maxHeight`, extra lines are clipped or end in "…", or `fit: 'shrink'` makes the font smaller until the text fits. The saved font size does not change.
620
+ - **`ShapedIText` and `ShapedTextbox` (#4815).** Fabric measures each letter alone but draws joined letters, so in Arabic the cursor can land two letters away. These measure from the text as the browser shapes it.
621
+ - **`VerticalText` (#511).** Columns from right to left, upright CJK characters, turned Latin words (Unicode UAX #50), and two-digit numbers side by side. Arrow keys, selection, IME, SVG and PDF all work.
622
+ - **`attachMobileTextInput` (#6588).** Reads each edit from the text itself, so Android autocorrect, suggestions and cursor swipes put letters and styles in the right place. It also opens the keyboard on tap and stops iOS from zooming.
623
+ - **`createTextCommands` (#6133).** Insert, delete, replace and restyle text from code with styles that stay on their letters, one undo step each.
624
+ - **`enableDirtyRegionRendering` (#9847).** Moving one of 5,000 shapes draws about 25 of them instead of all 5,000: under 2 ms per frame in place of nearly a second in headless Chromium.
625
+ - **`createFilterWorker` (#9532).** Fabric's filters run in a worker on transferred bitmaps, with the same pixels as Fabric, progress, cancel, and only the newest run applied while a slider moves.
626
+
596
627
  ## Edits that are never lost
597
628
 
598
629
  These guarantees hold however the user and your code interleave work:
package/README.zh-CN.md CHANGED
@@ -74,6 +74,8 @@ await engine.loadDocument(JSON.parse(localStorage.getItem(document.id)!));
74
74
  - **版本和迁移。** 保存命名版本,把任意版本恢复为新的修订,还能打开纯 Fabric JSON 或旧版本这个包保存的文档。
75
75
  - **大文档。** 对象分批创建,页面保持响应。加载会报告进度,并能用 `AbortSignal` 取消;取消后画布保持原样。
76
76
  - **复制、粘贴和图层。** 剪贴板会保留编组的变换和自定义属性,并给每个粘贴出的对象一个新 id;置顶、置底等图层命令可以让固定的背景保持不动。每个操作都是一步撤销。
77
+ - **好用的文字。** 保持宽度、可以裁剪、加“…”或自动缩小字号的文本框;与阿拉伯文和连字一致的光标位置;可编辑的中日韩竖排文字;在 Android 和 iOS 键盘上正确输入;在代码里修改文字时每个字的样式保持不变。
78
+ - **对象多也流畅。** 脏区域渲染只重绘变化的部分,图片滤镜在 Web Worker 中运行,支持进度和取消。
77
79
  - **撤销和重做。** 用户的一次操作就是一步撤销。事务可以把代码里的多处改动合成一个带标签的步骤,撤销和重做后 id 保持不变。
78
80
  - **支持 React,不绑定框架。** 为 React 提供 hooks,为其他框架提供一个小的状态 store。
79
81
  - **经过加固。** 导入的文档和 SVG 文件会被清理并限制大小,撤销历史有内存上限,每个功能都在 Fabric 6 和 7、Chromium、Firefox 和 WebKit 中测试过。导出结果会和画布逐像素比对。
@@ -428,6 +430,7 @@ const { objects, viewport, warnings } = await engine.importSvg(svgText, {
428
430
  - 结果是一个固定布局、大小等于视口的编组;使用 `as: 'objects'` 时则是分开的对象。两种方式都只算一步撤销,每个对象都有 id。
429
431
  - 脚本、事件处理器、`foreignObject`、指向其他文件的链接,以及 `limits.isAllowedUrl` 拒绝的图片地址都会被移除,`warnings` 会说明移除了什么。
430
432
  - 大小限制和文档相同,过大或嵌套过深的 SVG 会以 `UNSAFE_DOCUMENT` 被拒绝。
433
+ - 使用 `preserveGroups: true` 时,SVG 的 `<g>` 编组会保留为 Fabric 编组,连同 id、透明度、裁剪路径、class 和 `data-*` 属性(Fabric 自己会把它们拍平)。
431
434
 
432
435
  ## 版本历史
433
436
 
@@ -593,6 +596,35 @@ getLayers(engine); // [{ id, type, name, index, visible, locked }], top first
593
596
 
594
597
  引擎会保存每个对象的 `name`,所以图层面板的名称不会丢。在 React 中,`fabricjs-document-engine/react` 的 `useLayers(engine)` 返回同样的列表,并在每次改动后更新。
595
598
 
599
+ ## 文字、渲染速度和滤镜
600
+
601
+ 这些功能修复了 Fabric.js 中长期存在的问题。每个问题都先在 Fabric 6 和 7 上复现,每个修复都在 Chromium、Firefox 和 WebKit 中测试过。
602
+
603
+ ```ts
604
+ import { BoundedTextbox, VerticalText, ShapedIText, attachMobileTextInput, textObjects } from 'fabricjs-document-engine/text';
605
+ import { enableDirtyRegionRendering } from 'fabricjs-document-engine/performance';
606
+ import { createFilterWorker } from 'fabricjs-document-engine/filters';
607
+ import { createTextCommands } from 'fabricjs-document-engine';
608
+
609
+ const engine = createDocumentEngine({ canvas, customObjects: textObjects });
610
+
611
+ canvas.add(new BoundedTextbox(longText, { width: 240, maxHeight: 120, overflow: 'ellipsis' }));
612
+ canvas.add(new VerticalText('縦書きのテキスト', { fontSize: 32 }));
613
+ canvas.add(new ShapedIText('مرحبا بالعالم', { direction: 'rtl' }));
614
+ attachMobileTextInput(canvas);
615
+ createTextCommands(engine).insertText(title, 0, 'New: ');
616
+ enableDirtyRegionRendering(canvas);
617
+ await createFilterWorker({ engine }).apply(image, [new filters.Blur({ blur: 0.2 })], { onProgress });
618
+ ```
619
+
620
+ - **`BoundedTextbox`(#2376)。** Fabric 会把文本框撑到最长单词的宽度。它会像 CSS `overflow-wrap: anywhere` 一样在字母之间断开过长的单词。设置 `maxHeight` 后,多出的行可以被裁剪或以“…”结尾,或者用 `fit: 'shrink'` 缩小字号直到放得下。保存的字号不会改变。
621
+ - **`ShapedIText` 和 `ShapedTextbox`(#4815)。** Fabric 单独测量每个字母,但绘制时字母是连在一起的,所以阿拉伯文的光标可能偏两个字母。它们按浏览器实际排版的文字来测量。
622
+ - **`VerticalText`(#511)。** 从右到左的列,直立的中日韩字符,旋转的拉丁单词(Unicode UAX #50),两位数字并排放在一个方格里。方向键、选择、输入法、SVG 和 PDF 都可以使用。
623
+ - **`attachMobileTextInput`(#6588)。** 从文字本身读取每次修改,所以 Android 的自动更正、联想和光标滑动都会把文字和样式放在正确的位置。它还会在点击时打开键盘,并防止 iOS 缩放页面。
624
+ - **`createTextCommands`(#6133)。** 在代码中插入、删除、替换文字和修改样式,样式跟着字母走,每次一步撤销。
625
+ - **`enableDirtyRegionRendering`(#9847)。** 在 5,000 个图形中移动一个时,只绘制大约 25 个,而不是全部 5,000 个:在无头 Chromium 中每帧不到 2 毫秒,原来接近一秒。
626
+ - **`createFilterWorker`(#9532)。** Fabric 的滤镜在 Worker 中处理转移过去的位图,像素和 Fabric 相同,支持进度和取消,拖动滑块时只应用最新的一次。
627
+
596
628
  ## 不会丢失的修改
597
629
 
598
630
  无论用户的操作和你的代码怎样交错,下面这些保证都成立:
@@ -0,0 +1,223 @@
1
+ //#region src/commands/text-edit.ts
2
+ function sameStyle(a, b) {
3
+ if (a === b) return true;
4
+ const keys = Object.keys(a);
5
+ if (keys.length !== Object.keys(b).length) return false;
6
+ return keys.every((key) => a[key] === b[key]);
7
+ }
8
+ function hasStyle(style) {
9
+ return style !== void 0 && style !== null && Object.keys(style).length > 0;
10
+ }
11
+ function readStyleRuns(graphemes, styles) {
12
+ const runs = [];
13
+ let line = 0;
14
+ let char = 0;
15
+ for (let index = 0; index < graphemes.length; index += 1) {
16
+ if (graphemes[index] === "\n") {
17
+ line += 1;
18
+ char = 0;
19
+ continue;
20
+ }
21
+ const style = styles?.[line]?.[char];
22
+ char += 1;
23
+ if (!hasStyle(style)) continue;
24
+ const last = runs[runs.length - 1];
25
+ if (last && last.end === index && sameStyle(last.style, style)) last.end = index + 1;
26
+ else runs.push({
27
+ start: index,
28
+ end: index + 1,
29
+ style
30
+ });
31
+ }
32
+ return runs;
33
+ }
34
+ function writeStyleRuns(graphemes, runs) {
35
+ const styles = {};
36
+ let line = 0;
37
+ let char = 0;
38
+ let next = 0;
39
+ for (let index = 0; index < graphemes.length; index += 1) {
40
+ var _line;
41
+ if (graphemes[index] === "\n") {
42
+ line += 1;
43
+ char = 0;
44
+ continue;
45
+ }
46
+ while (next < runs.length && runs[next].end <= index) next += 1;
47
+ const run = runs[next];
48
+ if (run && run.start <= index) (styles[_line = line] ?? (styles[_line] = {}))[char] = { ...run.style };
49
+ char += 1;
50
+ }
51
+ return styles;
52
+ }
53
+ function shiftStyleRuns(runs, start, end, insertedLength, style) {
54
+ const delta = insertedLength - (end - start);
55
+ const shifted = [];
56
+ let inherited;
57
+ for (const run of runs) if (run.end <= start) {
58
+ const copy = { ...run };
59
+ if (copy.end === start) inherited = copy;
60
+ shifted.push(copy);
61
+ } else if (run.start >= end) shifted.push({
62
+ start: run.start + delta,
63
+ end: run.end + delta,
64
+ style: run.style
65
+ });
66
+ else {
67
+ if (run.start < start) {
68
+ inherited = {
69
+ start: run.start,
70
+ end: start,
71
+ style: run.style
72
+ };
73
+ shifted.push(inherited);
74
+ }
75
+ if (run.end > end) shifted.push({
76
+ start: end + delta,
77
+ end: run.end + delta,
78
+ style: run.style
79
+ });
80
+ }
81
+ if (insertedLength === 0) return shifted;
82
+ if (style === void 0) {
83
+ if (inherited) inherited.end += insertedLength;
84
+ return shifted;
85
+ }
86
+ if (!hasStyle(style)) return shifted;
87
+ const inserted = {
88
+ start,
89
+ end: start + insertedLength,
90
+ style
91
+ };
92
+ const at = shifted.findIndex((run) => run.start >= start);
93
+ if (at === -1) shifted.push(inserted);
94
+ else shifted.splice(at, 0, inserted);
95
+ return shifted;
96
+ }
97
+ function clamp(value, low, high) {
98
+ return Math.min(Math.max(value, low), high);
99
+ }
100
+ function moveIndex(index, start, end, insertedLength) {
101
+ if (index <= start) return index;
102
+ if (index >= end) return index + insertedLength - (end - start);
103
+ return start + insertedLength;
104
+ }
105
+ function replaceTextRange(target, start, end, insert, style) {
106
+ const text = target;
107
+ const graphemes = text._text ?? text.graphemeSplit(text.text);
108
+ const from = clamp(Math.floor(start), 0, graphemes.length);
109
+ const to = clamp(Math.floor(end), from, graphemes.length);
110
+ const inserted = insert ? text.graphemeSplit(insert) : [];
111
+ const runs = shiftStyleRuns(readStyleRuns(graphemes, text.styles), from, to, inserted.length, style);
112
+ const next = [
113
+ ...graphemes.slice(0, from),
114
+ ...inserted,
115
+ ...graphemes.slice(to)
116
+ ];
117
+ const selectionStart = moveIndex(text.selectionStart ?? 0, from, to, inserted.length);
118
+ const selectionEnd = moveIndex(text.selectionEnd ?? 0, from, to, inserted.length);
119
+ applyTextAndStyles(text, next, runs);
120
+ text.selectionStart = selectionStart;
121
+ text.selectionEnd = Math.max(selectionStart, selectionEnd);
122
+ announceTextChange(text);
123
+ }
124
+ function setTextRangeStyle(target, start, end, style) {
125
+ const text = target;
126
+ const graphemes = text._text ?? text.graphemeSplit(text.text);
127
+ const from = clamp(Math.floor(start), 0, graphemes.length);
128
+ const to = clamp(Math.floor(end), from, graphemes.length);
129
+ if (from === to) return;
130
+ const runs = readStyleRuns(graphemes, text.styles);
131
+ const result = [];
132
+ let cursor = from;
133
+ for (const run of runs) {
134
+ if (run.end <= from || run.start >= to) {
135
+ if (run.start >= to && cursor < to) {
136
+ result.push({
137
+ start: cursor,
138
+ end: to,
139
+ style: { ...style }
140
+ });
141
+ cursor = to;
142
+ }
143
+ result.push(run);
144
+ continue;
145
+ }
146
+ if (run.start < from) result.push({
147
+ start: run.start,
148
+ end: from,
149
+ style: run.style
150
+ });
151
+ const overlapStart = Math.max(run.start, from);
152
+ if (cursor < overlapStart) result.push({
153
+ start: cursor,
154
+ end: overlapStart,
155
+ style: { ...style }
156
+ });
157
+ const overlapEnd = Math.min(run.end, to);
158
+ result.push({
159
+ start: overlapStart,
160
+ end: overlapEnd,
161
+ style: {
162
+ ...run.style,
163
+ ...style
164
+ }
165
+ });
166
+ cursor = overlapEnd;
167
+ if (run.end > to) result.push({
168
+ start: to,
169
+ end: run.end,
170
+ style: run.style
171
+ });
172
+ }
173
+ if (cursor < to) result.push({
174
+ start: cursor,
175
+ end: to,
176
+ style: { ...style }
177
+ });
178
+ applyTextAndStyles(text, graphemes, result);
179
+ announceTextChange(text);
180
+ }
181
+ function applyTextAndStyles(text, graphemes, runs) {
182
+ const value = graphemes.join("");
183
+ text.styles = writeStyleRuns(graphemes, runs);
184
+ if (text.text !== value) text.set("text", value);
185
+ else text.initDimensions();
186
+ text.set("dirty", true);
187
+ text.setCoords();
188
+ }
189
+ function announceTextChange(text) {
190
+ if (text.isEditing && text.hiddenTextarea && !text.inCompositionMode) {
191
+ text.hiddenTextarea.value = text.text;
192
+ text._updateTextarea?.();
193
+ }
194
+ text.fire("changed");
195
+ text.canvas?.fire("text:changed", { target: text });
196
+ text.canvas?.requestRenderAll();
197
+ }
198
+ function createTextCommands(engine) {
199
+ return {
200
+ insertText(target, index, text, style) {
201
+ if (!text) return;
202
+ engine.transaction("Insert text", () => replaceTextRange(target, index, index, text, style));
203
+ },
204
+ deleteText(target, start, end) {
205
+ if (end <= start) return;
206
+ engine.transaction("Delete text", () => replaceTextRange(target, start, end, ""));
207
+ },
208
+ replaceText(target, start, end, text, style) {
209
+ engine.transaction("Replace text", () => replaceTextRange(target, start, end, text, style));
210
+ },
211
+ setTextStyle(target, start, end, style) {
212
+ if (end <= start) return;
213
+ engine.transaction("Style text", () => setTextRangeStyle(target, start, end, style));
214
+ }
215
+ };
216
+ }
217
+ //#endregion
218
+ exports.createTextCommands = createTextCommands;
219
+ exports.readStyleRuns = readStyleRuns;
220
+ exports.replaceTextRange = replaceTextRange;
221
+ exports.setTextRangeStyle = setTextRangeStyle;
222
+ exports.shiftStyleRuns = shiftStyleRuns;
223
+ exports.writeStyleRuns = writeStyleRuns;
@@ -0,0 +1,23 @@
1
+ import { DocumentEngine } from "../engine/create-document-engine.cjs";
2
+ import { IText, TextStyleDeclaration, Textbox } from "fabric";
3
+ //#region src/commands/text-edit.d.ts
4
+ export type EditableTextObject = IText | Textbox;
5
+ export interface StyleRun {
6
+ start: number;
7
+ end: number;
8
+ style: TextStyleDeclaration;
9
+ }
10
+ export type InsertedStyle = TextStyleDeclaration | null | undefined;
11
+ export declare function readStyleRuns(graphemes: readonly string[], styles: IText["styles"]): StyleRun[];
12
+ export declare function writeStyleRuns(graphemes: readonly string[], runs: readonly StyleRun[]): IText["styles"];
13
+ export declare function shiftStyleRuns(runs: readonly StyleRun[], start: number, end: number, insertedLength: number, style?: InsertedStyle): StyleRun[];
14
+ export declare function replaceTextRange(target: EditableTextObject, start: number, end: number, insert: string, style?: InsertedStyle): void;
15
+ export declare function setTextRangeStyle(target: EditableTextObject, start: number, end: number, style: TextStyleDeclaration): void;
16
+ export interface TextCommands {
17
+ insertText(target: EditableTextObject, index: number, text: string, style?: InsertedStyle): void;
18
+ deleteText(target: EditableTextObject, start: number, end: number): void;
19
+ replaceText(target: EditableTextObject, start: number, end: number, text: string, style?: InsertedStyle): void;
20
+ setTextStyle(target: EditableTextObject, start: number, end: number, style: TextStyleDeclaration): void;
21
+ }
22
+ export declare function createTextCommands(engine: DocumentEngine): TextCommands;
23
+ //#endregion
@@ -0,0 +1,23 @@
1
+ import { DocumentEngine } from "../engine/create-document-engine.js";
2
+ import { IText, TextStyleDeclaration, Textbox } from "fabric";
3
+ //#region src/commands/text-edit.d.ts
4
+ export type EditableTextObject = IText | Textbox;
5
+ export interface StyleRun {
6
+ start: number;
7
+ end: number;
8
+ style: TextStyleDeclaration;
9
+ }
10
+ export type InsertedStyle = TextStyleDeclaration | null | undefined;
11
+ export declare function readStyleRuns(graphemes: readonly string[], styles: IText["styles"]): StyleRun[];
12
+ export declare function writeStyleRuns(graphemes: readonly string[], runs: readonly StyleRun[]): IText["styles"];
13
+ export declare function shiftStyleRuns(runs: readonly StyleRun[], start: number, end: number, insertedLength: number, style?: InsertedStyle): StyleRun[];
14
+ export declare function replaceTextRange(target: EditableTextObject, start: number, end: number, insert: string, style?: InsertedStyle): void;
15
+ export declare function setTextRangeStyle(target: EditableTextObject, start: number, end: number, style: TextStyleDeclaration): void;
16
+ export interface TextCommands {
17
+ insertText(target: EditableTextObject, index: number, text: string, style?: InsertedStyle): void;
18
+ deleteText(target: EditableTextObject, start: number, end: number): void;
19
+ replaceText(target: EditableTextObject, start: number, end: number, text: string, style?: InsertedStyle): void;
20
+ setTextStyle(target: EditableTextObject, start: number, end: number, style: TextStyleDeclaration): void;
21
+ }
22
+ export declare function createTextCommands(engine: DocumentEngine): TextCommands;
23
+ //#endregion
@@ -0,0 +1,218 @@
1
+ //#region src/commands/text-edit.ts
2
+ function sameStyle(a, b) {
3
+ if (a === b) return true;
4
+ const keys = Object.keys(a);
5
+ if (keys.length !== Object.keys(b).length) return false;
6
+ return keys.every((key) => a[key] === b[key]);
7
+ }
8
+ function hasStyle(style) {
9
+ return style !== void 0 && style !== null && Object.keys(style).length > 0;
10
+ }
11
+ function readStyleRuns(graphemes, styles) {
12
+ const runs = [];
13
+ let line = 0;
14
+ let char = 0;
15
+ for (let index = 0; index < graphemes.length; index += 1) {
16
+ if (graphemes[index] === "\n") {
17
+ line += 1;
18
+ char = 0;
19
+ continue;
20
+ }
21
+ const style = styles?.[line]?.[char];
22
+ char += 1;
23
+ if (!hasStyle(style)) continue;
24
+ const last = runs[runs.length - 1];
25
+ if (last && last.end === index && sameStyle(last.style, style)) last.end = index + 1;
26
+ else runs.push({
27
+ start: index,
28
+ end: index + 1,
29
+ style
30
+ });
31
+ }
32
+ return runs;
33
+ }
34
+ function writeStyleRuns(graphemes, runs) {
35
+ const styles = {};
36
+ let line = 0;
37
+ let char = 0;
38
+ let next = 0;
39
+ for (let index = 0; index < graphemes.length; index += 1) {
40
+ var _line;
41
+ if (graphemes[index] === "\n") {
42
+ line += 1;
43
+ char = 0;
44
+ continue;
45
+ }
46
+ while (next < runs.length && runs[next].end <= index) next += 1;
47
+ const run = runs[next];
48
+ if (run && run.start <= index) (styles[_line = line] ?? (styles[_line] = {}))[char] = { ...run.style };
49
+ char += 1;
50
+ }
51
+ return styles;
52
+ }
53
+ function shiftStyleRuns(runs, start, end, insertedLength, style) {
54
+ const delta = insertedLength - (end - start);
55
+ const shifted = [];
56
+ let inherited;
57
+ for (const run of runs) if (run.end <= start) {
58
+ const copy = { ...run };
59
+ if (copy.end === start) inherited = copy;
60
+ shifted.push(copy);
61
+ } else if (run.start >= end) shifted.push({
62
+ start: run.start + delta,
63
+ end: run.end + delta,
64
+ style: run.style
65
+ });
66
+ else {
67
+ if (run.start < start) {
68
+ inherited = {
69
+ start: run.start,
70
+ end: start,
71
+ style: run.style
72
+ };
73
+ shifted.push(inherited);
74
+ }
75
+ if (run.end > end) shifted.push({
76
+ start: end + delta,
77
+ end: run.end + delta,
78
+ style: run.style
79
+ });
80
+ }
81
+ if (insertedLength === 0) return shifted;
82
+ if (style === void 0) {
83
+ if (inherited) inherited.end += insertedLength;
84
+ return shifted;
85
+ }
86
+ if (!hasStyle(style)) return shifted;
87
+ const inserted = {
88
+ start,
89
+ end: start + insertedLength,
90
+ style
91
+ };
92
+ const at = shifted.findIndex((run) => run.start >= start);
93
+ if (at === -1) shifted.push(inserted);
94
+ else shifted.splice(at, 0, inserted);
95
+ return shifted;
96
+ }
97
+ function clamp(value, low, high) {
98
+ return Math.min(Math.max(value, low), high);
99
+ }
100
+ function moveIndex(index, start, end, insertedLength) {
101
+ if (index <= start) return index;
102
+ if (index >= end) return index + insertedLength - (end - start);
103
+ return start + insertedLength;
104
+ }
105
+ function replaceTextRange(target, start, end, insert, style) {
106
+ const text = target;
107
+ const graphemes = text._text ?? text.graphemeSplit(text.text);
108
+ const from = clamp(Math.floor(start), 0, graphemes.length);
109
+ const to = clamp(Math.floor(end), from, graphemes.length);
110
+ const inserted = insert ? text.graphemeSplit(insert) : [];
111
+ const runs = shiftStyleRuns(readStyleRuns(graphemes, text.styles), from, to, inserted.length, style);
112
+ const next = [
113
+ ...graphemes.slice(0, from),
114
+ ...inserted,
115
+ ...graphemes.slice(to)
116
+ ];
117
+ const selectionStart = moveIndex(text.selectionStart ?? 0, from, to, inserted.length);
118
+ const selectionEnd = moveIndex(text.selectionEnd ?? 0, from, to, inserted.length);
119
+ applyTextAndStyles(text, next, runs);
120
+ text.selectionStart = selectionStart;
121
+ text.selectionEnd = Math.max(selectionStart, selectionEnd);
122
+ announceTextChange(text);
123
+ }
124
+ function setTextRangeStyle(target, start, end, style) {
125
+ const text = target;
126
+ const graphemes = text._text ?? text.graphemeSplit(text.text);
127
+ const from = clamp(Math.floor(start), 0, graphemes.length);
128
+ const to = clamp(Math.floor(end), from, graphemes.length);
129
+ if (from === to) return;
130
+ const runs = readStyleRuns(graphemes, text.styles);
131
+ const result = [];
132
+ let cursor = from;
133
+ for (const run of runs) {
134
+ if (run.end <= from || run.start >= to) {
135
+ if (run.start >= to && cursor < to) {
136
+ result.push({
137
+ start: cursor,
138
+ end: to,
139
+ style: { ...style }
140
+ });
141
+ cursor = to;
142
+ }
143
+ result.push(run);
144
+ continue;
145
+ }
146
+ if (run.start < from) result.push({
147
+ start: run.start,
148
+ end: from,
149
+ style: run.style
150
+ });
151
+ const overlapStart = Math.max(run.start, from);
152
+ if (cursor < overlapStart) result.push({
153
+ start: cursor,
154
+ end: overlapStart,
155
+ style: { ...style }
156
+ });
157
+ const overlapEnd = Math.min(run.end, to);
158
+ result.push({
159
+ start: overlapStart,
160
+ end: overlapEnd,
161
+ style: {
162
+ ...run.style,
163
+ ...style
164
+ }
165
+ });
166
+ cursor = overlapEnd;
167
+ if (run.end > to) result.push({
168
+ start: to,
169
+ end: run.end,
170
+ style: run.style
171
+ });
172
+ }
173
+ if (cursor < to) result.push({
174
+ start: cursor,
175
+ end: to,
176
+ style: { ...style }
177
+ });
178
+ applyTextAndStyles(text, graphemes, result);
179
+ announceTextChange(text);
180
+ }
181
+ function applyTextAndStyles(text, graphemes, runs) {
182
+ const value = graphemes.join("");
183
+ text.styles = writeStyleRuns(graphemes, runs);
184
+ if (text.text !== value) text.set("text", value);
185
+ else text.initDimensions();
186
+ text.set("dirty", true);
187
+ text.setCoords();
188
+ }
189
+ function announceTextChange(text) {
190
+ if (text.isEditing && text.hiddenTextarea && !text.inCompositionMode) {
191
+ text.hiddenTextarea.value = text.text;
192
+ text._updateTextarea?.();
193
+ }
194
+ text.fire("changed");
195
+ text.canvas?.fire("text:changed", { target: text });
196
+ text.canvas?.requestRenderAll();
197
+ }
198
+ function createTextCommands(engine) {
199
+ return {
200
+ insertText(target, index, text, style) {
201
+ if (!text) return;
202
+ engine.transaction("Insert text", () => replaceTextRange(target, index, index, text, style));
203
+ },
204
+ deleteText(target, start, end) {
205
+ if (end <= start) return;
206
+ engine.transaction("Delete text", () => replaceTextRange(target, start, end, ""));
207
+ },
208
+ replaceText(target, start, end, text, style) {
209
+ engine.transaction("Replace text", () => replaceTextRange(target, start, end, text, style));
210
+ },
211
+ setTextStyle(target, start, end, style) {
212
+ if (end <= start) return;
213
+ engine.transaction("Style text", () => setTextRangeStyle(target, start, end, style));
214
+ }
215
+ };
216
+ }
217
+ //#endregion
218
+ export { createTextCommands, readStyleRuns, replaceTextRange, setTextRangeStyle, shiftStyleRuns, writeStyleRuns };
@@ -2,7 +2,13 @@ const require_errors = require("../engine/errors.cjs");
2
2
  let fabric = require("fabric");
3
3
  //#region src/fabric/object-registry.ts
4
4
  function createObjectRegistry(initialDefinitions = []) {
5
- const extraProperties = /* @__PURE__ */ new Set(["id", "name"]);
5
+ const extraProperties = /* @__PURE__ */ new Set([
6
+ "id",
7
+ "name",
8
+ "svgId",
9
+ "svgClass",
10
+ "svgData"
11
+ ]);
6
12
  function register(definition) {
7
13
  const { fabricClass } = definition;
8
14
  if (typeof fabricClass?.type !== "string" || fabricClass.type.length === 0) throw new require_errors.DocumentEngineError("INVALID_CUSTOM_OBJECT", "A custom object class needs a static \"type\" string, for example: static type = \"Sticker\"");
@@ -2,7 +2,13 @@ import { DocumentEngineError } from "../engine/errors.js";
2
2
  import { classRegistry } from "fabric";
3
3
  //#region src/fabric/object-registry.ts
4
4
  function createObjectRegistry(initialDefinitions = []) {
5
- const extraProperties = /* @__PURE__ */ new Set(["id", "name"]);
5
+ const extraProperties = /* @__PURE__ */ new Set([
6
+ "id",
7
+ "name",
8
+ "svgId",
9
+ "svgClass",
10
+ "svgData"
11
+ ]);
6
12
  function register(definition) {
7
13
  const { fabricClass } = definition;
8
14
  if (typeof fabricClass?.type !== "string" || fabricClass.type.length === 0) throw new DocumentEngineError("INVALID_CUSTOM_OBJECT", "A custom object class needs a static \"type\" string, for example: static type = \"Sticker\"");