js-draw 1.0.0 → 1.0.1

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 (244) hide show
  1. package/README.md +20 -6
  2. package/dist/bundle.js +1 -1
  3. package/dist/cjs/Editor.js +1 -1
  4. package/dist/cjs/Editor.loadFrom.test.d.ts +1 -0
  5. package/dist/cjs/Editor.test.d.ts +1 -0
  6. package/dist/cjs/Editor.toSVG.test.d.ts +1 -0
  7. package/dist/cjs/EditorImage.test.d.ts +1 -0
  8. package/dist/cjs/EventDispatcher.test.d.ts +1 -0
  9. package/dist/cjs/SVGLoader.test.d.ts +1 -0
  10. package/dist/cjs/UndoRedoHistory.test.d.ts +1 -0
  11. package/dist/cjs/commands/uniteCommands.test.d.ts +1 -0
  12. package/dist/cjs/components/AbstractComponent.transformBy.test.d.ts +1 -0
  13. package/dist/cjs/components/BackgroundComponent.test.d.ts +1 -0
  14. package/dist/cjs/components/Stroke.test.d.ts +1 -0
  15. package/dist/cjs/components/TextComponent.test.d.ts +1 -0
  16. package/dist/cjs/components/UnknownSVGObject.test.d.ts +1 -0
  17. package/dist/cjs/components/builders/FreehandLineBuilder.test.d.ts +1 -0
  18. package/dist/cjs/localizations/getLocalizationTable.test.d.ts +1 -0
  19. package/dist/cjs/rendering/RenderingStyle.test.d.ts +1 -0
  20. package/dist/cjs/rendering/caching/CacheRecord.test.d.ts +1 -0
  21. package/dist/cjs/rendering/caching/RenderingCache.test.d.ts +1 -0
  22. package/dist/cjs/rendering/renderers/DummyRenderer.test.d.ts +1 -0
  23. package/dist/cjs/rendering/renderers/TextOnlyRenderer.test.d.ts +1 -0
  24. package/dist/cjs/shortcuts/KeyBinding.test.d.ts +1 -0
  25. package/dist/cjs/shortcuts/KeyboardShortcutManager.test.d.ts +1 -0
  26. package/dist/cjs/toolbar/EdgeToolbar.test.d.ts +1 -0
  27. package/dist/cjs/tools/Eraser.test.d.ts +1 -0
  28. package/dist/cjs/tools/FindTool.test.d.ts +1 -0
  29. package/dist/cjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  30. package/dist/cjs/tools/PanZoom.test.d.ts +1 -0
  31. package/dist/cjs/tools/Pen.test.d.ts +1 -0
  32. package/dist/cjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  33. package/dist/cjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  34. package/dist/cjs/util/ReactiveValue.test.d.ts +1 -0
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/cjs/version.test.d.ts +1 -0
  37. package/dist/mjs/Editor.loadFrom.test.d.ts +1 -0
  38. package/dist/mjs/Editor.mjs +1 -1
  39. package/dist/mjs/Editor.test.d.ts +1 -0
  40. package/dist/mjs/Editor.toSVG.test.d.ts +1 -0
  41. package/dist/mjs/EditorImage.test.d.ts +1 -0
  42. package/dist/mjs/EventDispatcher.test.d.ts +1 -0
  43. package/dist/mjs/SVGLoader.test.d.ts +1 -0
  44. package/dist/mjs/UndoRedoHistory.test.d.ts +1 -0
  45. package/dist/mjs/commands/uniteCommands.test.d.ts +1 -0
  46. package/dist/mjs/components/AbstractComponent.transformBy.test.d.ts +1 -0
  47. package/dist/mjs/components/BackgroundComponent.test.d.ts +1 -0
  48. package/dist/mjs/components/Stroke.test.d.ts +1 -0
  49. package/dist/mjs/components/TextComponent.test.d.ts +1 -0
  50. package/dist/mjs/components/UnknownSVGObject.test.d.ts +1 -0
  51. package/dist/mjs/components/builders/FreehandLineBuilder.test.d.ts +1 -0
  52. package/dist/mjs/localizations/getLocalizationTable.test.d.ts +1 -0
  53. package/dist/mjs/rendering/RenderingStyle.test.d.ts +1 -0
  54. package/dist/mjs/rendering/caching/CacheRecord.test.d.ts +1 -0
  55. package/dist/mjs/rendering/caching/RenderingCache.test.d.ts +1 -0
  56. package/dist/mjs/rendering/renderers/DummyRenderer.test.d.ts +1 -0
  57. package/dist/mjs/rendering/renderers/TextOnlyRenderer.test.d.ts +1 -0
  58. package/dist/mjs/shortcuts/KeyBinding.test.d.ts +1 -0
  59. package/dist/mjs/shortcuts/KeyboardShortcutManager.test.d.ts +1 -0
  60. package/dist/mjs/toolbar/EdgeToolbar.test.d.ts +1 -0
  61. package/dist/mjs/tools/Eraser.test.d.ts +1 -0
  62. package/dist/mjs/tools/FindTool.test.d.ts +1 -0
  63. package/dist/mjs/tools/InputFilter/InputPipeline.test.d.ts +1 -0
  64. package/dist/mjs/tools/PanZoom.test.d.ts +1 -0
  65. package/dist/mjs/tools/Pen.test.d.ts +1 -0
  66. package/dist/mjs/tools/SelectionTool/SelectionTool.test.d.ts +1 -0
  67. package/dist/mjs/tools/UndoRedoShortcut.test.d.ts +1 -0
  68. package/dist/mjs/util/ReactiveValue.test.d.ts +1 -0
  69. package/dist/mjs/version.mjs +1 -1
  70. package/dist/mjs/version.test.d.ts +1 -0
  71. package/dist-test/test_imports/package-lock.json +13 -0
  72. package/dist-test/test_imports/package.json +12 -0
  73. package/dist-test/test_imports/test-imports.js +11 -0
  74. package/dist-test/test_imports/test-require.cjs +14 -0
  75. package/package.json +2 -2
  76. package/src/Editor.loadFrom.test.ts +24 -0
  77. package/src/Editor.test.ts +107 -0
  78. package/src/Editor.toSVG.test.ts +294 -0
  79. package/src/Editor.ts +1443 -0
  80. package/src/EditorImage.test.ts +117 -0
  81. package/src/EditorImage.ts +609 -0
  82. package/src/EventDispatcher.test.ts +123 -0
  83. package/src/EventDispatcher.ts +72 -0
  84. package/src/Pointer.ts +183 -0
  85. package/src/SVGLoader.test.ts +114 -0
  86. package/src/SVGLoader.ts +672 -0
  87. package/src/UndoRedoHistory.test.ts +34 -0
  88. package/src/UndoRedoHistory.ts +102 -0
  89. package/src/Viewport.ts +322 -0
  90. package/src/bundle/bundled.ts +7 -0
  91. package/src/commands/Command.ts +45 -0
  92. package/src/commands/Duplicate.ts +75 -0
  93. package/src/commands/Erase.ts +95 -0
  94. package/src/commands/SerializableCommand.ts +49 -0
  95. package/src/commands/UnresolvedCommand.ts +37 -0
  96. package/src/commands/invertCommand.ts +58 -0
  97. package/src/commands/lib.ts +16 -0
  98. package/src/commands/localization.ts +47 -0
  99. package/src/commands/uniteCommands.test.ts +23 -0
  100. package/src/commands/uniteCommands.ts +140 -0
  101. package/src/components/AbstractComponent.transformBy.test.ts +23 -0
  102. package/src/components/AbstractComponent.ts +383 -0
  103. package/src/components/BackgroundComponent.test.ts +44 -0
  104. package/src/components/BackgroundComponent.ts +348 -0
  105. package/src/components/ImageComponent.ts +176 -0
  106. package/src/components/RestylableComponent.ts +161 -0
  107. package/src/components/SVGGlobalAttributesObject.ts +79 -0
  108. package/src/components/Stroke.test.ts +137 -0
  109. package/src/components/Stroke.ts +294 -0
  110. package/src/components/TextComponent.test.ts +202 -0
  111. package/src/components/TextComponent.ts +429 -0
  112. package/src/components/UnknownSVGObject.test.ts +10 -0
  113. package/src/components/UnknownSVGObject.ts +60 -0
  114. package/src/components/builders/ArrowBuilder.ts +106 -0
  115. package/src/components/builders/CircleBuilder.ts +100 -0
  116. package/src/components/builders/FreehandLineBuilder.test.ts +24 -0
  117. package/src/components/builders/FreehandLineBuilder.ts +210 -0
  118. package/src/components/builders/LineBuilder.ts +77 -0
  119. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +453 -0
  120. package/src/components/builders/RectangleBuilder.ts +73 -0
  121. package/src/components/builders/types.ts +15 -0
  122. package/src/components/lib.ts +31 -0
  123. package/src/components/localization.ts +24 -0
  124. package/src/components/util/StrokeSmoother.ts +302 -0
  125. package/src/components/util/describeComponentList.ts +18 -0
  126. package/src/dialogs/makeAboutDialog.ts +82 -0
  127. package/src/inputEvents.ts +143 -0
  128. package/src/lib.ts +91 -0
  129. package/src/localization.ts +34 -0
  130. package/src/localizations/de.ts +146 -0
  131. package/src/localizations/en.ts +8 -0
  132. package/src/localizations/es.ts +74 -0
  133. package/src/localizations/getLocalizationTable.test.ts +27 -0
  134. package/src/localizations/getLocalizationTable.ts +74 -0
  135. package/src/rendering/Display.ts +247 -0
  136. package/src/rendering/RenderablePathSpec.ts +88 -0
  137. package/src/rendering/RenderingStyle.test.ts +68 -0
  138. package/src/rendering/RenderingStyle.ts +55 -0
  139. package/src/rendering/TextRenderingStyle.ts +55 -0
  140. package/src/rendering/caching/CacheRecord.test.ts +48 -0
  141. package/src/rendering/caching/CacheRecord.ts +76 -0
  142. package/src/rendering/caching/CacheRecordManager.ts +71 -0
  143. package/src/rendering/caching/RenderingCache.test.ts +43 -0
  144. package/src/rendering/caching/RenderingCache.ts +66 -0
  145. package/src/rendering/caching/RenderingCacheNode.ts +404 -0
  146. package/src/rendering/caching/testUtils.ts +35 -0
  147. package/src/rendering/caching/types.ts +34 -0
  148. package/src/rendering/lib.ts +8 -0
  149. package/src/rendering/localization.ts +20 -0
  150. package/src/rendering/renderers/AbstractRenderer.ts +232 -0
  151. package/src/rendering/renderers/CanvasRenderer.ts +312 -0
  152. package/src/rendering/renderers/DummyRenderer.test.ts +41 -0
  153. package/src/rendering/renderers/DummyRenderer.ts +142 -0
  154. package/src/rendering/renderers/SVGRenderer.ts +434 -0
  155. package/src/rendering/renderers/TextOnlyRenderer.test.ts +34 -0
  156. package/src/rendering/renderers/TextOnlyRenderer.ts +68 -0
  157. package/src/shortcuts/KeyBinding.test.ts +61 -0
  158. package/src/shortcuts/KeyBinding.ts +257 -0
  159. package/src/shortcuts/KeyboardShortcutManager.test.ts +95 -0
  160. package/src/shortcuts/KeyboardShortcutManager.ts +163 -0
  161. package/src/shortcuts/lib.ts +3 -0
  162. package/src/testing/createEditor.ts +11 -0
  163. package/src/testing/getUniquePointerId.ts +18 -0
  164. package/src/testing/lib.ts +3 -0
  165. package/src/testing/sendPenEvent.ts +36 -0
  166. package/src/testing/sendTouchEvent.ts +71 -0
  167. package/src/toolbar/AbstractToolbar.ts +542 -0
  168. package/src/toolbar/DropdownToolbar.ts +220 -0
  169. package/src/toolbar/EdgeToolbar.test.ts +54 -0
  170. package/src/toolbar/EdgeToolbar.ts +543 -0
  171. package/src/toolbar/IconProvider.ts +861 -0
  172. package/src/toolbar/constants.ts +1 -0
  173. package/src/toolbar/lib.ts +6 -0
  174. package/src/toolbar/localization.ts +136 -0
  175. package/src/toolbar/types.ts +13 -0
  176. package/src/toolbar/widgets/ActionButtonWidget.ts +39 -0
  177. package/src/toolbar/widgets/BaseToolWidget.ts +81 -0
  178. package/src/toolbar/widgets/BaseWidget.ts +495 -0
  179. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +250 -0
  180. package/src/toolbar/widgets/EraserToolWidget.ts +84 -0
  181. package/src/toolbar/widgets/HandToolWidget.ts +239 -0
  182. package/src/toolbar/widgets/InsertImageWidget.ts +248 -0
  183. package/src/toolbar/widgets/OverflowWidget.ts +92 -0
  184. package/src/toolbar/widgets/PenToolWidget.ts +369 -0
  185. package/src/toolbar/widgets/SelectionToolWidget.ts +195 -0
  186. package/src/toolbar/widgets/TextToolWidget.ts +149 -0
  187. package/src/toolbar/widgets/components/makeColorInput.ts +184 -0
  188. package/src/toolbar/widgets/components/makeFileInput.ts +128 -0
  189. package/src/toolbar/widgets/components/makeGridSelector.ts +179 -0
  190. package/src/toolbar/widgets/components/makeSeparator.ts +17 -0
  191. package/src/toolbar/widgets/components/makeThicknessSlider.ts +62 -0
  192. package/src/toolbar/widgets/keybindings.ts +19 -0
  193. package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +262 -0
  194. package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +71 -0
  195. package/src/toolbar/widgets/layout/types.ts +74 -0
  196. package/src/toolbar/widgets/lib.ts +13 -0
  197. package/src/tools/BaseTool.ts +169 -0
  198. package/src/tools/Eraser.test.ts +103 -0
  199. package/src/tools/Eraser.ts +173 -0
  200. package/src/tools/FindTool.test.ts +67 -0
  201. package/src/tools/FindTool.ts +153 -0
  202. package/src/tools/InputFilter/FunctionMapper.ts +17 -0
  203. package/src/tools/InputFilter/InputMapper.ts +41 -0
  204. package/src/tools/InputFilter/InputPipeline.test.ts +41 -0
  205. package/src/tools/InputFilter/InputPipeline.ts +34 -0
  206. package/src/tools/InputFilter/InputStabilizer.ts +254 -0
  207. package/src/tools/InputFilter/StrokeKeyboardControl.ts +104 -0
  208. package/src/tools/PanZoom.test.ts +339 -0
  209. package/src/tools/PanZoom.ts +525 -0
  210. package/src/tools/PasteHandler.ts +94 -0
  211. package/src/tools/Pen.test.ts +260 -0
  212. package/src/tools/Pen.ts +284 -0
  213. package/src/tools/PipetteTool.ts +84 -0
  214. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +29 -0
  215. package/src/tools/SelectionTool/Selection.ts +647 -0
  216. package/src/tools/SelectionTool/SelectionHandle.ts +142 -0
  217. package/src/tools/SelectionTool/SelectionTool.test.ts +370 -0
  218. package/src/tools/SelectionTool/SelectionTool.ts +510 -0
  219. package/src/tools/SelectionTool/TransformMode.ts +112 -0
  220. package/src/tools/SelectionTool/types.ts +11 -0
  221. package/src/tools/SoundUITool.ts +221 -0
  222. package/src/tools/TextTool.ts +339 -0
  223. package/src/tools/ToolController.ts +224 -0
  224. package/src/tools/ToolEnabledGroup.ts +14 -0
  225. package/src/tools/ToolSwitcherShortcut.ts +39 -0
  226. package/src/tools/ToolbarShortcutHandler.ts +39 -0
  227. package/src/tools/UndoRedoShortcut.test.ts +62 -0
  228. package/src/tools/UndoRedoShortcut.ts +24 -0
  229. package/src/tools/keybindings.ts +85 -0
  230. package/src/tools/lib.ts +22 -0
  231. package/src/tools/localization.ts +76 -0
  232. package/src/types.ts +151 -0
  233. package/src/util/ReactiveValue.test.ts +168 -0
  234. package/src/util/ReactiveValue.ts +241 -0
  235. package/src/util/assertions.ts +55 -0
  236. package/src/util/fileToBase64.ts +18 -0
  237. package/src/util/guessKeyCodeFromKey.ts +36 -0
  238. package/src/util/listPrefixMatch.ts +19 -0
  239. package/src/util/stopPropagationOfScrollingWheelEvents.ts +20 -0
  240. package/src/util/untilNextAnimationFrame.ts +9 -0
  241. package/src/util/waitForAll.ts +18 -0
  242. package/src/util/waitForTimeout.ts +9 -0
  243. package/src/version.test.ts +12 -0
  244. package/src/version.ts +3 -0
@@ -0,0 +1,429 @@
1
+ import SerializableCommand from '../commands/SerializableCommand';
2
+ import Editor from '../Editor';
3
+ import { Vec2, LineSegment2, Rect2, Mat33, Mat33Array } from '@js-draw/math';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import { cloneTextStyle, TextRenderingStyle, textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle';
6
+ import AbstractComponent from './AbstractComponent';
7
+ import { ImageComponentLocalization } from './localization';
8
+ import RestyleableComponent, { ComponentStyle, createRestyleComponentCommand } from './RestylableComponent';
9
+
10
+ const componentTypeId = 'text';
11
+
12
+ export enum TextTransformMode {
13
+ /** Absolutely positioned in both the X and Y dimensions. */
14
+ ABSOLUTE_XY,
15
+
16
+ /** Relatively positioned in both the X and Y dimensions. */
17
+ RELATIVE_XY,
18
+
19
+ /**Relatively positioned in the X direction, absolutely positioned in the Y direction. */
20
+ RELATIVE_X_ABSOLUTE_Y,
21
+
22
+ /**Relatively positioned in the Y direction, absolutely positioned in the X direction. */
23
+ RELATIVE_Y_ABSOLUTE_X,
24
+ }
25
+
26
+ type TextElement = TextComponent|string;
27
+
28
+ /**
29
+ * Displays text.
30
+ */
31
+ export default class TextComponent extends AbstractComponent implements RestyleableComponent {
32
+ protected contentBBox: Rect2;
33
+
34
+ // eslint-disable-next-line @typescript-eslint/prefer-as-const
35
+ readonly isRestylableComponent: true = true;
36
+
37
+ /**
38
+ * Creates a new text object from a list of component text or child TextComponents.
39
+ *
40
+ * @see {@link fromLines}
41
+ */
42
+ public constructor(
43
+ protected readonly textObjects: Array<TextElement>,
44
+
45
+ // Transformation relative to this component's parent element.
46
+ private transform: Mat33,
47
+ private style: TextRenderingStyle,
48
+
49
+ // @internal
50
+ private transformMode: TextTransformMode = TextTransformMode.ABSOLUTE_XY,
51
+ ) {
52
+ super(componentTypeId);
53
+ this.recomputeBBox();
54
+
55
+ // If this has no direct children, choose a style representative of this' content
56
+ // (useful for estimating the style of the TextComponent).
57
+ const hasDirectContent = textObjects.some(obj => typeof obj === 'string');
58
+ if (!hasDirectContent && textObjects.length > 0) {
59
+ this.style = (textObjects[0] as TextComponent).getTextStyle();
60
+ }
61
+ }
62
+
63
+ public static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextRenderingStyle) {
64
+ // Quote the font family if necessary.
65
+ const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
66
+
67
+ ctx.font = [
68
+ style.fontStyle ?? '',
69
+ style.fontWeight ?? '',
70
+ (style.size ?? 12) + 'px',
71
+ `${fontFamily}`
72
+ ].join(' ');
73
+
74
+ // TODO: Support RTL
75
+ ctx.textAlign = 'left';
76
+ }
77
+
78
+ private static textMeasuringCtx: CanvasRenderingContext2D|null = null;
79
+
80
+ // Roughly estimate the bounding box of `text`. Use if no CanvasRenderingContext2D is available.
81
+ private static estimateTextDimens(text: string, style: TextRenderingStyle): Rect2 {
82
+ const widthEst = text.length * style.size;
83
+ const heightEst = style.size;
84
+
85
+ // Text is drawn with (0, 0) as its baseline. As such, the majority of the text's height should
86
+ // be above (0, 0).
87
+ return new Rect2(0, -heightEst * 2/3, widthEst, heightEst);
88
+ }
89
+
90
+ // Returns a set of TextMetrics for the given text, if a canvas is available.
91
+ private static getTextMetrics(text: string, style: TextRenderingStyle): TextMetrics|null {
92
+ TextComponent.textMeasuringCtx ??= document.createElement('canvas').getContext('2d') ?? null;
93
+ if (!TextComponent.textMeasuringCtx) {
94
+ return null;
95
+ }
96
+
97
+ const ctx = TextComponent.textMeasuringCtx;
98
+ TextComponent.applyTextStyles(ctx, style);
99
+
100
+ return ctx.measureText(text);
101
+ }
102
+
103
+ // Returns the bounding box of `text`. This is approximate if no Canvas is available.
104
+ private static getTextDimens(text: string, style: TextRenderingStyle): Rect2 {
105
+ const metrics = this.getTextMetrics(text, style);
106
+
107
+ if (!metrics) {
108
+ return this.estimateTextDimens(text, style);
109
+ }
110
+
111
+ // Text is drawn with (0,0) at the bottom left of the baseline.
112
+ const textY = -metrics.actualBoundingBoxAscent;
113
+ const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
114
+ return new Rect2(0, textY, metrics.width, textHeight);
115
+ }
116
+
117
+ private static getFontHeight(style: TextRenderingStyle): number {
118
+ return style.size;
119
+ }
120
+
121
+ private computeUntransformedBBoxOfPart(part: TextElement) {
122
+ if (typeof part === 'string') {
123
+ return TextComponent.getTextDimens(part, this.style);
124
+ } else {
125
+ return part.contentBBox;
126
+ }
127
+ }
128
+
129
+ private recomputeBBox() {
130
+ let bbox: Rect2|null = null;
131
+ const cursor = new TextComponent.TextCursor(this.transform, this.style);
132
+
133
+ for (const textObject of this.textObjects) {
134
+ const transform = cursor.update(textObject);
135
+ const currentBBox = this.computeUntransformedBBoxOfPart(textObject).transformedBoundingBox(transform);
136
+
137
+ bbox ??= currentBBox;
138
+ bbox = bbox.union(currentBBox);
139
+ }
140
+
141
+ this.contentBBox = bbox ?? Rect2.empty;
142
+ }
143
+
144
+ private renderInternal(canvas: AbstractRenderer) {
145
+ const cursor = new TextComponent.TextCursor(this.transform, this.style);
146
+
147
+ for (const textObject of this.textObjects) {
148
+ const transform = cursor.update(textObject);
149
+
150
+ if (typeof textObject === 'string') {
151
+ canvas.drawText(textObject, transform, this.style);
152
+ } else {
153
+ canvas.pushTransform(transform);
154
+ textObject.renderInternal(canvas);
155
+ canvas.popTransform();
156
+ }
157
+ }
158
+ }
159
+
160
+ public override render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
161
+ canvas.startObject(this.contentBBox);
162
+ this.renderInternal(canvas);
163
+ canvas.endObject(this.getLoadSaveData());
164
+ }
165
+
166
+ public override getProportionalRenderingTime(): number {
167
+ return this.textObjects.length;
168
+ }
169
+
170
+ public override intersects(lineSegment: LineSegment2): boolean {
171
+ const cursor = new TextComponent.TextCursor(this.transform, this.style);
172
+
173
+ for (const subObject of this.textObjects) {
174
+ // Convert canvas space to internal space relative to the current object.
175
+ const invTransform = cursor.update(subObject).inverse();
176
+ const transformedLine = lineSegment.transformedBy(invTransform);
177
+
178
+ if (typeof subObject === 'string') {
179
+ const textBBox = TextComponent.getTextDimens(subObject, this.style);
180
+
181
+ // TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and
182
+ // use pixel-testing to check for intersection with its contour.
183
+ if (textBBox.getEdges().some(edge => transformedLine.intersection(edge) !== null)) {
184
+ return true;
185
+ }
186
+ } else {
187
+ if (subObject.intersects(transformedLine)) {
188
+ return true;
189
+ }
190
+ }
191
+ }
192
+
193
+ return false;
194
+ }
195
+
196
+ public getStyle(): ComponentStyle {
197
+ return {
198
+ color: this.style.renderingStyle.fill,
199
+
200
+ // Make a copy
201
+ textStyle: {
202
+ ...this.style,
203
+ renderingStyle: {
204
+ ...this.style.renderingStyle,
205
+ },
206
+ },
207
+ };
208
+ }
209
+
210
+ public updateStyle(style: ComponentStyle): SerializableCommand {
211
+ return createRestyleComponentCommand(this.getStyle(), style, this);
212
+ }
213
+
214
+ public forceStyle(style: ComponentStyle, editor: Editor|null): void {
215
+ if (style.textStyle) {
216
+ this.style = cloneTextStyle(style.textStyle);
217
+ } else if (style.color) {
218
+ this.style = {
219
+ ...this.style,
220
+ renderingStyle: {
221
+ ...this.style.renderingStyle,
222
+ fill: style.color,
223
+ },
224
+ };
225
+ } else {
226
+ return;
227
+ }
228
+
229
+ for (const child of this.textObjects) {
230
+ if (child instanceof TextComponent) {
231
+ child.forceStyle(style, editor);
232
+ }
233
+ }
234
+
235
+ if (editor) {
236
+ editor.image.queueRerenderOf(this);
237
+ editor.queueRerender();
238
+ }
239
+ }
240
+
241
+ // See {@link getStyle}
242
+ public getTextStyle(): TextRenderingStyle {
243
+ return cloneTextStyle(this.style);
244
+ }
245
+
246
+ public getBaselinePos() {
247
+ return this.transform.transformVec2(Vec2.zero);
248
+ }
249
+
250
+ public getTransform(): Mat33 {
251
+ return this.transform;
252
+ }
253
+
254
+ protected applyTransformation(affineTransfm: Mat33): void {
255
+ this.transform = affineTransfm.rightMul(this.transform);
256
+ this.recomputeBBox();
257
+ }
258
+
259
+ protected createClone(): AbstractComponent {
260
+ const clonedTextObjects = this.textObjects.map(obj => {
261
+ if (typeof obj === 'string') {
262
+ return obj;
263
+ } else {
264
+ return obj.createClone() as TextComponent;
265
+ }
266
+ });
267
+ return new TextComponent(clonedTextObjects, this.transform, this.style);
268
+ }
269
+
270
+ public getText() {
271
+ const result: string[] = [];
272
+
273
+ for (const textObject of this.textObjects) {
274
+ if (typeof textObject === 'string') {
275
+ result.push(textObject);
276
+ } else {
277
+ result.push(textObject.getText());
278
+ }
279
+ }
280
+
281
+ return result.join('\n');
282
+ }
283
+
284
+ public description(localizationTable: ImageComponentLocalization): string {
285
+ return localizationTable.text(this.getText());
286
+ }
287
+
288
+ // Do not rely on the output of `serializeToJSON` taking any particular format.
289
+ protected serializeToJSON(): Record<string, any> {
290
+ const serializableStyle = textStyleToJSON(this.style);
291
+
292
+ const serializedTextObjects = this.textObjects.map(text => {
293
+ if (typeof text === 'string') {
294
+ return {
295
+ text,
296
+ };
297
+ } else {
298
+ return {
299
+ json: text.serializeToJSON(),
300
+ };
301
+ }
302
+ });
303
+
304
+ return {
305
+ textObjects: serializedTextObjects,
306
+ transform: this.transform.toArray(),
307
+ style: serializableStyle,
308
+ };
309
+ }
310
+
311
+ // @internal
312
+ public static deserializeFromString(json: any): TextComponent {
313
+ if (typeof json === 'string') {
314
+ json = JSON.parse(json);
315
+ }
316
+
317
+ const style = textStyleFromJSON(json.style);
318
+
319
+ const textObjects: Array<TextElement> = json.textObjects.map((data: any) => {
320
+ if ((data.text ?? null) !== null) {
321
+ return data.text;
322
+ }
323
+
324
+ return TextComponent.deserializeFromString(data.json);
325
+ });
326
+
327
+ json.transform = json.transform.filter((elem: any) => typeof elem === 'number');
328
+ if (json.transform.length !== 9) {
329
+ throw new Error(`Unable to deserialize transform, ${json.transform}.`);
330
+ }
331
+
332
+ const transformData = json.transform as Mat33Array;
333
+ const transform = new Mat33(...transformData);
334
+
335
+ return new TextComponent(textObjects, transform, style);
336
+ }
337
+
338
+ /**
339
+ * Creates a `TextComponent` from `lines`.
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * const textStyle = {
344
+ * size: 12,
345
+ * fontFamily: 'serif',
346
+ * renderingStyle: { fill: Color4.black },
347
+ * };
348
+ *
349
+ * const text = TextComponent.fromLines('foo\nbar'.split('\n'), Mat33.identity, textStyle);
350
+ * ```
351
+ */
352
+ public static fromLines(lines: string[], transform: Mat33, style: TextRenderingStyle): AbstractComponent {
353
+ let lastComponent: TextComponent|null = null;
354
+ const components: TextComponent[] = [];
355
+
356
+ const lineMargin = Math.round(this.getFontHeight(style));
357
+
358
+ let position = Vec2.zero;
359
+ for (const line of lines) {
360
+ if (lastComponent) {
361
+ position = position.plus(Vec2.unitY.times(lineMargin));
362
+ }
363
+
364
+ const component = new TextComponent([ line ], Mat33.translation(position), style);
365
+ components.push(component);
366
+ lastComponent = component;
367
+ }
368
+
369
+ return new TextComponent(components, transform, style);
370
+ }
371
+
372
+ private static TextCursor = class {
373
+ public transform: Mat33 = Mat33.identity;
374
+ public constructor(
375
+ private parentTransform: Mat33 = Mat33.identity, private parentStyle: TextRenderingStyle
376
+ ) { }
377
+
378
+ /**
379
+ * Based on previous calls to `update`, returns the transformation of
380
+ * the given `element` (including the parentTransform given to this cursor's
381
+ * constructor).
382
+ *
383
+ * The result does not take into account
384
+ */
385
+ public update(elem: TextElement) {
386
+ let elementTransform = Mat33.identity;
387
+ let elemInternalTransform = Mat33.identity;
388
+ let textSize;
389
+ if (typeof(elem) === 'string') {
390
+ textSize = TextComponent.getTextDimens(elem, this.parentStyle);
391
+ } else {
392
+ // TODO: Double-check whether we need to take elem.transform into account here.
393
+ // elementTransform = elem.transform;
394
+ elemInternalTransform = elem.transform;
395
+ textSize = elem.getBBox();
396
+ }
397
+ const positioning = typeof(elem) === 'string' ? TextTransformMode.RELATIVE_XY : elem.transformMode;
398
+
399
+ if (positioning === TextTransformMode.RELATIVE_XY) {
400
+ // Position relative to the previous element's transform.
401
+ elementTransform = this.transform.rightMul(elementTransform);
402
+ } else if (positioning === TextTransformMode.RELATIVE_X_ABSOLUTE_Y || positioning === TextTransformMode.RELATIVE_Y_ABSOLUTE_X) {
403
+ // Zero the absolute component of this.transform's translation
404
+ const transform = this.transform.mapEntries((component, [row, col]) => {
405
+ if (positioning === TextTransformMode.RELATIVE_X_ABSOLUTE_Y) {
406
+ // Zero the y component of this.transform's translation
407
+ return row === 1 && col === 2 ? 0 : component;
408
+ } else if (positioning === TextTransformMode.RELATIVE_Y_ABSOLUTE_X) {
409
+ // Zero the x component of this.transform's translation
410
+ return row === 0 && col === 2 ? 0 : component;
411
+ }
412
+
413
+ throw new Error('Unreachable');
414
+ return 0;
415
+ });
416
+
417
+ elementTransform = transform.rightMul(elementTransform);
418
+ }
419
+
420
+ // Update this.transform so that future calls to update return correct values.
421
+ const endShiftTransform = Mat33.translation(Vec2.of(textSize.width, 0));
422
+ this.transform = elementTransform.rightMul(elemInternalTransform).rightMul(endShiftTransform);
423
+
424
+ return this.parentTransform.rightMul(elementTransform);
425
+ }
426
+ };
427
+ }
428
+
429
+ AbstractComponent.registerComponent(componentTypeId, (data: string) => TextComponent.deserializeFromString(data));
@@ -0,0 +1,10 @@
1
+ import AbstractComponent from './AbstractComponent';
2
+ import UnknownSVGObject from './UnknownSVGObject';
3
+
4
+ describe('UnknownSVGObject', () => {
5
+ it('should not be deserializable', () => {
6
+ const obj = new UnknownSVGObject(document.createElementNS('http://www.w3.org/2000/svg', 'circle'));
7
+ const serialized = obj.serialize();
8
+ expect(() => AbstractComponent.deserialize(serialized)).toThrow(/.*cannot be deserialized.*/);
9
+ });
10
+ });
@@ -0,0 +1,60 @@
1
+ //
2
+ // Stores objects loaded from an SVG that aren't recognised by the editor.
3
+ // @internal
4
+ // @packageDocumentation
5
+ //
6
+
7
+ import { LineSegment2, Mat33, Rect2 } from '@js-draw/math';
8
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
9
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
10
+ import AbstractComponent from './AbstractComponent';
11
+ import { ImageComponentLocalization } from './localization';
12
+
13
+ const componentId = 'unknown-svg-object';
14
+ export default class UnknownSVGObject extends AbstractComponent {
15
+ protected contentBBox: Rect2;
16
+
17
+ public constructor(private svgObject: SVGElement) {
18
+ super(componentId);
19
+ this.contentBBox = Rect2.of(svgObject.getBoundingClientRect());
20
+ }
21
+
22
+ public override render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
23
+ if (!(canvas instanceof SVGRenderer)) {
24
+ // Don't draw unrenderable objects if we can't
25
+ return;
26
+ }
27
+
28
+ canvas.startObject(this.contentBBox);
29
+ canvas.drawSVGElem(this.svgObject);
30
+ canvas.endObject(this.getLoadSaveData());
31
+ }
32
+
33
+ public override intersects(lineSegment: LineSegment2): boolean {
34
+ return this.contentBBox.getEdges().some(edge => edge.intersection(lineSegment) !== null);
35
+ }
36
+
37
+ protected applyTransformation(_affineTransfm: Mat33): void {
38
+ }
39
+
40
+ public override isSelectable() {
41
+ return false;
42
+ }
43
+
44
+ protected createClone(): AbstractComponent {
45
+ return new UnknownSVGObject(this.svgObject.cloneNode(true) as SVGElement);
46
+ }
47
+
48
+ public description(localization: ImageComponentLocalization): string {
49
+ return localization.svgObject;
50
+ }
51
+
52
+ protected serializeToJSON(): string | null {
53
+ return JSON.stringify({
54
+ html: this.svgObject.outerHTML,
55
+ });
56
+ }
57
+ }
58
+
59
+ // null: Do not deserialize UnknownSVGObjects.
60
+ AbstractComponent.registerComponent(componentId, null);
@@ -0,0 +1,106 @@
1
+ import { Path, PathCommandType, Rect2 } from '@js-draw/math';
2
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
+ import { StrokeDataPoint } from '../../types';
4
+ import Viewport from '../../Viewport';
5
+ import AbstractComponent from '../AbstractComponent';
6
+ import Stroke from '../Stroke';
7
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
8
+
9
+ export const makeArrowBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
10
+ return new ArrowBuilder(initialPoint, viewport);
11
+ };
12
+
13
+ export default class ArrowBuilder implements ComponentBuilder {
14
+ private endPoint: StrokeDataPoint;
15
+
16
+ public constructor(private readonly startPoint: StrokeDataPoint, private readonly viewport: Viewport) {
17
+ this.endPoint = startPoint;
18
+ }
19
+
20
+ private getLineWidth(): number {
21
+ return Math.max(this.endPoint.width, this.startPoint.width);
22
+ }
23
+
24
+ public getBBox(): Rect2 {
25
+ const preview = this.buildPreview();
26
+ return preview.getBBox();
27
+ }
28
+
29
+ private buildPreview(): Stroke {
30
+ const lineStartPoint = this.startPoint.pos;
31
+ const endPoint = this.endPoint.pos;
32
+ const toEnd = endPoint.minus(lineStartPoint).normalized();
33
+ const arrowLength = endPoint.minus(lineStartPoint).length();
34
+
35
+ // Ensure that the arrow tip is smaller than the arrow.
36
+ const arrowTipSize = Math.min(this.getLineWidth(), arrowLength / 2);
37
+ const startSize = this.startPoint.width / 2;
38
+ const endSize = this.endPoint.width / 2;
39
+
40
+ const arrowTipBase = endPoint.minus(toEnd.times(arrowTipSize));
41
+
42
+ // Scaled normal vectors.
43
+ const lineNormal = toEnd.orthog();
44
+ const scaledStartNormal = lineNormal.times(startSize);
45
+ const scaledBaseNormal = lineNormal.times(endSize);
46
+
47
+ const path = new Path(arrowTipBase.minus(scaledBaseNormal), [
48
+ // Stem
49
+ {
50
+ kind: PathCommandType.LineTo,
51
+ point: lineStartPoint.minus(scaledStartNormal),
52
+ },
53
+ {
54
+ kind: PathCommandType.LineTo,
55
+ point: lineStartPoint.plus(scaledStartNormal),
56
+ },
57
+ {
58
+ kind: PathCommandType.LineTo,
59
+ point: arrowTipBase.plus(scaledBaseNormal),
60
+ },
61
+
62
+ // Head
63
+ {
64
+ kind: PathCommandType.LineTo,
65
+ point: arrowTipBase.plus(lineNormal.times(arrowTipSize).plus(scaledBaseNormal)),
66
+ },
67
+ {
68
+ kind: PathCommandType.LineTo,
69
+ point: endPoint.plus(toEnd.times(endSize)),
70
+ },
71
+ {
72
+ kind: PathCommandType.LineTo,
73
+ point: arrowTipBase.plus(lineNormal.times(-arrowTipSize).minus(scaledBaseNormal)),
74
+ },
75
+ {
76
+ kind: PathCommandType.LineTo,
77
+ point: arrowTipBase.minus(scaledBaseNormal),
78
+ },
79
+ // Round all points in the arrow (to remove unnecessary decimal places)
80
+ ]).mapPoints(point => this.viewport.roundPoint(point));
81
+
82
+ const preview = new Stroke([
83
+ {
84
+ startPoint: path.startPoint,
85
+ commands: path.parts,
86
+ style: {
87
+ fill: this.startPoint.color,
88
+ }
89
+ }
90
+ ]);
91
+
92
+ return preview;
93
+ }
94
+
95
+ public build(): AbstractComponent {
96
+ return this.buildPreview();
97
+ }
98
+
99
+ public preview(renderer: AbstractRenderer): void {
100
+ this.buildPreview().render(renderer);
101
+ }
102
+
103
+ public addPoint(point: StrokeDataPoint): void {
104
+ this.endPoint = point;
105
+ }
106
+ }
@@ -0,0 +1,100 @@
1
+ import { Vec2, Path, PathCommand, PathCommandType, Rect2, Color4 } from '@js-draw/math';
2
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
+ import { pathToRenderable } from '../../rendering/RenderablePathSpec';
4
+ import { StrokeDataPoint } from '../../types';
5
+ import Viewport from '../../Viewport';
6
+ import AbstractComponent from '../AbstractComponent';
7
+ import Stroke from '../Stroke';
8
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
9
+
10
+ export const makeOutlinedCircleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
11
+ return new CircleBuilder(initialPoint, viewport);
12
+ };
13
+
14
+ class CircleBuilder implements ComponentBuilder {
15
+ private endPoint: StrokeDataPoint;
16
+
17
+ public constructor(
18
+ private readonly startPoint: StrokeDataPoint,
19
+ private readonly viewport: Viewport,
20
+ ) {
21
+ // Initially, the start and end points are the same.
22
+ this.endPoint = startPoint;
23
+ }
24
+
25
+ public getBBox(): Rect2 {
26
+ const preview = this.buildPreview();
27
+ return preview.getBBox();
28
+ }
29
+
30
+ private buildPreview(): Stroke {
31
+ const pathCommands: PathCommand[] = [];
32
+ const numDivisions = 6;
33
+ const stepSize = Math.PI * 2 / numDivisions;
34
+
35
+ // Round the stroke width so that when exported it doesn't have unnecessary trailing decimals.
36
+ const strokeWidth =
37
+ Viewport.roundPoint(this.endPoint.width, 5 / this.viewport.getScaleFactor());
38
+
39
+ const center = this.startPoint.pos.lerp(this.endPoint.pos, 0.5);
40
+ const startEndDelta = this.endPoint.pos.minus(center);
41
+ const radius = startEndDelta.length() - strokeWidth / 2;
42
+
43
+ const startPoint = center.plus(Vec2.of(radius, 0));
44
+
45
+ for (let t = stepSize; t <= Math.PI * 2; t += stepSize) {
46
+ const endPoint = Vec2.of(
47
+ radius * Math.cos(t),
48
+ -radius * Math.sin(t),
49
+ ).plus(center);
50
+
51
+ // controlPointRadiusScale is selected to make the circles appear circular and
52
+ // **does** depend on stepSize.
53
+ const controlPointRadiusScale = 1.141;
54
+ const controlPoint = Vec2.of(
55
+ Math.cos(t - stepSize / 2),
56
+ -Math.sin(t - stepSize / 2),
57
+ ).times(
58
+ radius * controlPointRadiusScale
59
+ ).plus(center);
60
+
61
+ pathCommands.push({
62
+ kind: PathCommandType.QuadraticBezierTo,
63
+ controlPoint,
64
+ endPoint,
65
+ });
66
+ }
67
+
68
+ pathCommands.push({
69
+ kind: PathCommandType.LineTo,
70
+ point: startPoint,
71
+ });
72
+
73
+ const path = new Path(startPoint, pathCommands)
74
+ .mapPoints(point => this.viewport.roundPoint(point));
75
+
76
+ const preview = new Stroke([
77
+ pathToRenderable(path, {
78
+ fill: Color4.transparent,
79
+ stroke: {
80
+ width: strokeWidth,
81
+ color: this.endPoint.color,
82
+ },
83
+ }),
84
+ ]);
85
+
86
+ return preview;
87
+ }
88
+
89
+ public build(): AbstractComponent {
90
+ return this.buildPreview();
91
+ }
92
+
93
+ public preview(renderer: AbstractRenderer): void {
94
+ this.buildPreview().render(renderer);
95
+ }
96
+
97
+ public addPoint(point: StrokeDataPoint): void {
98
+ this.endPoint = point;
99
+ }
100
+ }