js-draw 1.0.1 → 1.1.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 (197) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Editor.css +1 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/toolbar/AbstractToolbar.d.ts +9 -13
  6. package/dist/cjs/toolbar/AbstractToolbar.js +14 -19
  7. package/dist/cjs/toolbar/widgets/SaveActionWidget.d.ts +10 -0
  8. package/dist/cjs/toolbar/widgets/SaveActionWidget.js +26 -0
  9. package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
  10. package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
  11. package/dist/cjs/version.js +1 -1
  12. package/dist/mjs/toolbar/AbstractToolbar.d.ts +9 -13
  13. package/dist/mjs/toolbar/AbstractToolbar.mjs +14 -19
  14. package/dist/mjs/toolbar/widgets/SaveActionWidget.d.ts +10 -0
  15. package/dist/mjs/toolbar/widgets/SaveActionWidget.mjs +21 -0
  16. package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
  17. package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
  18. package/dist/mjs/version.mjs +1 -1
  19. package/docs/img/readme-images/js-draw.jpg +0 -0
  20. package/docs/img/readme-images/unsupported-elements--in-editor.png +0 -0
  21. package/package.json +5 -4
  22. package/src/toolbar/EdgeToolbar.scss +1 -0
  23. package/dist-test/test_imports/package-lock.json +0 -13
  24. package/dist-test/test_imports/package.json +0 -12
  25. package/dist-test/test_imports/test-imports.js +0 -11
  26. package/dist-test/test_imports/test-require.cjs +0 -14
  27. package/src/Editor.loadFrom.test.ts +0 -24
  28. package/src/Editor.test.ts +0 -107
  29. package/src/Editor.toSVG.test.ts +0 -294
  30. package/src/Editor.ts +0 -1443
  31. package/src/EditorImage.test.ts +0 -117
  32. package/src/EditorImage.ts +0 -609
  33. package/src/EventDispatcher.test.ts +0 -123
  34. package/src/EventDispatcher.ts +0 -72
  35. package/src/Pointer.ts +0 -183
  36. package/src/SVGLoader.test.ts +0 -114
  37. package/src/SVGLoader.ts +0 -672
  38. package/src/UndoRedoHistory.test.ts +0 -34
  39. package/src/UndoRedoHistory.ts +0 -102
  40. package/src/Viewport.ts +0 -322
  41. package/src/bundle/bundled.ts +0 -7
  42. package/src/commands/Command.ts +0 -45
  43. package/src/commands/Duplicate.ts +0 -75
  44. package/src/commands/Erase.ts +0 -95
  45. package/src/commands/SerializableCommand.ts +0 -49
  46. package/src/commands/UnresolvedCommand.ts +0 -37
  47. package/src/commands/invertCommand.ts +0 -58
  48. package/src/commands/lib.ts +0 -16
  49. package/src/commands/localization.ts +0 -47
  50. package/src/commands/uniteCommands.test.ts +0 -23
  51. package/src/commands/uniteCommands.ts +0 -140
  52. package/src/components/AbstractComponent.transformBy.test.ts +0 -23
  53. package/src/components/AbstractComponent.ts +0 -383
  54. package/src/components/BackgroundComponent.test.ts +0 -44
  55. package/src/components/BackgroundComponent.ts +0 -348
  56. package/src/components/ImageComponent.ts +0 -176
  57. package/src/components/RestylableComponent.ts +0 -161
  58. package/src/components/SVGGlobalAttributesObject.ts +0 -79
  59. package/src/components/Stroke.test.ts +0 -137
  60. package/src/components/Stroke.ts +0 -294
  61. package/src/components/TextComponent.test.ts +0 -202
  62. package/src/components/TextComponent.ts +0 -429
  63. package/src/components/UnknownSVGObject.test.ts +0 -10
  64. package/src/components/UnknownSVGObject.ts +0 -60
  65. package/src/components/builders/ArrowBuilder.ts +0 -106
  66. package/src/components/builders/CircleBuilder.ts +0 -100
  67. package/src/components/builders/FreehandLineBuilder.test.ts +0 -24
  68. package/src/components/builders/FreehandLineBuilder.ts +0 -210
  69. package/src/components/builders/LineBuilder.ts +0 -77
  70. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +0 -453
  71. package/src/components/builders/RectangleBuilder.ts +0 -73
  72. package/src/components/builders/types.ts +0 -15
  73. package/src/components/lib.ts +0 -31
  74. package/src/components/localization.ts +0 -24
  75. package/src/components/util/StrokeSmoother.ts +0 -302
  76. package/src/components/util/describeComponentList.ts +0 -18
  77. package/src/dialogs/makeAboutDialog.ts +0 -82
  78. package/src/inputEvents.ts +0 -143
  79. package/src/lib.ts +0 -91
  80. package/src/localization.ts +0 -34
  81. package/src/localizations/de.ts +0 -146
  82. package/src/localizations/en.ts +0 -8
  83. package/src/localizations/es.ts +0 -74
  84. package/src/localizations/getLocalizationTable.test.ts +0 -27
  85. package/src/localizations/getLocalizationTable.ts +0 -74
  86. package/src/rendering/Display.ts +0 -247
  87. package/src/rendering/RenderablePathSpec.ts +0 -88
  88. package/src/rendering/RenderingStyle.test.ts +0 -68
  89. package/src/rendering/RenderingStyle.ts +0 -55
  90. package/src/rendering/TextRenderingStyle.ts +0 -55
  91. package/src/rendering/caching/CacheRecord.test.ts +0 -48
  92. package/src/rendering/caching/CacheRecord.ts +0 -76
  93. package/src/rendering/caching/CacheRecordManager.ts +0 -71
  94. package/src/rendering/caching/RenderingCache.test.ts +0 -43
  95. package/src/rendering/caching/RenderingCache.ts +0 -66
  96. package/src/rendering/caching/RenderingCacheNode.ts +0 -404
  97. package/src/rendering/caching/testUtils.ts +0 -35
  98. package/src/rendering/caching/types.ts +0 -34
  99. package/src/rendering/lib.ts +0 -8
  100. package/src/rendering/localization.ts +0 -20
  101. package/src/rendering/renderers/AbstractRenderer.ts +0 -232
  102. package/src/rendering/renderers/CanvasRenderer.ts +0 -312
  103. package/src/rendering/renderers/DummyRenderer.test.ts +0 -41
  104. package/src/rendering/renderers/DummyRenderer.ts +0 -142
  105. package/src/rendering/renderers/SVGRenderer.ts +0 -434
  106. package/src/rendering/renderers/TextOnlyRenderer.test.ts +0 -34
  107. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -68
  108. package/src/shortcuts/KeyBinding.test.ts +0 -61
  109. package/src/shortcuts/KeyBinding.ts +0 -257
  110. package/src/shortcuts/KeyboardShortcutManager.test.ts +0 -95
  111. package/src/shortcuts/KeyboardShortcutManager.ts +0 -163
  112. package/src/shortcuts/lib.ts +0 -3
  113. package/src/testing/createEditor.ts +0 -11
  114. package/src/testing/getUniquePointerId.ts +0 -18
  115. package/src/testing/lib.ts +0 -3
  116. package/src/testing/sendPenEvent.ts +0 -36
  117. package/src/testing/sendTouchEvent.ts +0 -71
  118. package/src/toolbar/AbstractToolbar.ts +0 -542
  119. package/src/toolbar/DropdownToolbar.ts +0 -220
  120. package/src/toolbar/EdgeToolbar.test.ts +0 -54
  121. package/src/toolbar/EdgeToolbar.ts +0 -543
  122. package/src/toolbar/IconProvider.ts +0 -861
  123. package/src/toolbar/constants.ts +0 -1
  124. package/src/toolbar/lib.ts +0 -6
  125. package/src/toolbar/localization.ts +0 -136
  126. package/src/toolbar/types.ts +0 -13
  127. package/src/toolbar/widgets/ActionButtonWidget.ts +0 -39
  128. package/src/toolbar/widgets/BaseToolWidget.ts +0 -81
  129. package/src/toolbar/widgets/BaseWidget.ts +0 -495
  130. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +0 -250
  131. package/src/toolbar/widgets/EraserToolWidget.ts +0 -84
  132. package/src/toolbar/widgets/HandToolWidget.ts +0 -239
  133. package/src/toolbar/widgets/InsertImageWidget.ts +0 -248
  134. package/src/toolbar/widgets/OverflowWidget.ts +0 -92
  135. package/src/toolbar/widgets/PenToolWidget.ts +0 -369
  136. package/src/toolbar/widgets/SelectionToolWidget.ts +0 -195
  137. package/src/toolbar/widgets/TextToolWidget.ts +0 -149
  138. package/src/toolbar/widgets/components/makeColorInput.ts +0 -184
  139. package/src/toolbar/widgets/components/makeFileInput.ts +0 -128
  140. package/src/toolbar/widgets/components/makeGridSelector.ts +0 -179
  141. package/src/toolbar/widgets/components/makeSeparator.ts +0 -17
  142. package/src/toolbar/widgets/components/makeThicknessSlider.ts +0 -62
  143. package/src/toolbar/widgets/keybindings.ts +0 -19
  144. package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +0 -262
  145. package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +0 -71
  146. package/src/toolbar/widgets/layout/types.ts +0 -74
  147. package/src/toolbar/widgets/lib.ts +0 -13
  148. package/src/tools/BaseTool.ts +0 -169
  149. package/src/tools/Eraser.test.ts +0 -103
  150. package/src/tools/Eraser.ts +0 -173
  151. package/src/tools/FindTool.test.ts +0 -67
  152. package/src/tools/FindTool.ts +0 -153
  153. package/src/tools/InputFilter/FunctionMapper.ts +0 -17
  154. package/src/tools/InputFilter/InputMapper.ts +0 -41
  155. package/src/tools/InputFilter/InputPipeline.test.ts +0 -41
  156. package/src/tools/InputFilter/InputPipeline.ts +0 -34
  157. package/src/tools/InputFilter/InputStabilizer.ts +0 -254
  158. package/src/tools/InputFilter/StrokeKeyboardControl.ts +0 -104
  159. package/src/tools/PanZoom.test.ts +0 -339
  160. package/src/tools/PanZoom.ts +0 -525
  161. package/src/tools/PasteHandler.ts +0 -94
  162. package/src/tools/Pen.test.ts +0 -260
  163. package/src/tools/Pen.ts +0 -284
  164. package/src/tools/PipetteTool.ts +0 -84
  165. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +0 -29
  166. package/src/tools/SelectionTool/Selection.ts +0 -647
  167. package/src/tools/SelectionTool/SelectionHandle.ts +0 -142
  168. package/src/tools/SelectionTool/SelectionTool.test.ts +0 -370
  169. package/src/tools/SelectionTool/SelectionTool.ts +0 -510
  170. package/src/tools/SelectionTool/TransformMode.ts +0 -112
  171. package/src/tools/SelectionTool/types.ts +0 -11
  172. package/src/tools/SoundUITool.ts +0 -221
  173. package/src/tools/TextTool.ts +0 -339
  174. package/src/tools/ToolController.ts +0 -224
  175. package/src/tools/ToolEnabledGroup.ts +0 -14
  176. package/src/tools/ToolSwitcherShortcut.ts +0 -39
  177. package/src/tools/ToolbarShortcutHandler.ts +0 -39
  178. package/src/tools/UndoRedoShortcut.test.ts +0 -62
  179. package/src/tools/UndoRedoShortcut.ts +0 -24
  180. package/src/tools/keybindings.ts +0 -85
  181. package/src/tools/lib.ts +0 -22
  182. package/src/tools/localization.ts +0 -76
  183. package/src/types.ts +0 -151
  184. package/src/util/ReactiveValue.test.ts +0 -168
  185. package/src/util/ReactiveValue.ts +0 -241
  186. package/src/util/assertions.ts +0 -55
  187. package/src/util/fileToBase64.ts +0 -18
  188. package/src/util/guessKeyCodeFromKey.ts +0 -36
  189. package/src/util/listPrefixMatch.ts +0 -19
  190. package/src/util/stopPropagationOfScrollingWheelEvents.ts +0 -20
  191. package/src/util/untilNextAnimationFrame.ts +0 -9
  192. package/src/util/waitForAll.ts +0 -18
  193. package/src/util/waitForTimeout.ts +0 -9
  194. package/src/version.test.ts +0 -12
  195. package/src/version.ts +0 -3
  196. package/tools/allLocales.js +0 -4
  197. package/tools/copyREADME.ts +0 -62
@@ -1,79 +0,0 @@
1
- //
2
- // Used by `SVGLoader`s to store unrecognised global attributes
3
- // (e.g. unrecognised XML namespace declarations).
4
- // @internal
5
- // @packageDocumentation
6
- //
7
-
8
- import { LineSegment2, Mat33, Rect2 } from '@js-draw/math';
9
- import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
10
- import SVGRenderer from '../rendering/renderers/SVGRenderer';
11
- import AbstractComponent from './AbstractComponent';
12
- import { ImageComponentLocalization } from './localization';
13
-
14
- type GlobalAttrsList = Array<[string, string|null]>;
15
-
16
- const componentKind = 'svg-global-attributes';
17
-
18
- // Stores global SVG attributes (e.g. namespace identifiers.)
19
- export default class SVGGlobalAttributesObject extends AbstractComponent {
20
- protected contentBBox: Rect2;
21
- public constructor(private readonly attrs: GlobalAttrsList) {
22
- super(componentKind);
23
- this.contentBBox = Rect2.empty;
24
- }
25
-
26
- public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
27
- if (!(canvas instanceof SVGRenderer)) {
28
- // Don't draw unrenderable objects if we can't
29
- return;
30
- }
31
-
32
- for (const [ attr, value ] of this.attrs) {
33
- canvas.setRootSVGAttribute(attr, value);
34
- }
35
- }
36
-
37
- public intersects(_lineSegment: LineSegment2): boolean {
38
- return false;
39
- }
40
-
41
- protected applyTransformation(_affineTransfm: Mat33): void {
42
- }
43
-
44
- public override isSelectable() {
45
- return false;
46
- }
47
-
48
- protected createClone() {
49
- return new SVGGlobalAttributesObject(this.attrs);
50
- }
51
-
52
- public description(localization: ImageComponentLocalization): string {
53
- return localization.svgObject;
54
- }
55
-
56
- protected serializeToJSON(): string | null {
57
- return JSON.stringify(this.attrs);
58
- }
59
-
60
- public static deserializeFromString(data: string): AbstractComponent {
61
- const json = JSON.parse(data) as GlobalAttrsList;
62
- const attrs: GlobalAttrsList = [];
63
-
64
- const numericAndSpaceContentExp = /^[ \t\n0-9.-eE]+$/;
65
-
66
- // Don't deserialize all attributes, just those that should be safe.
67
- for (const [ key, val ] of json) {
68
- if (key === 'viewBox' || key === 'width' || key === 'height') {
69
- if (val && numericAndSpaceContentExp.exec(val)) {
70
- attrs.push([key, val]);
71
- }
72
- }
73
- }
74
-
75
- return new SVGGlobalAttributesObject(attrs);
76
- }
77
- }
78
-
79
- AbstractComponent.registerComponent(componentKind, SVGGlobalAttributesObject.deserializeFromString);
@@ -1,137 +0,0 @@
1
- import { Path, Vec2, Mat33, Color4 } from '@js-draw/math';
2
- import Stroke from './Stroke';
3
- import createEditor from '../testing/createEditor';
4
- import EditorImage from '../EditorImage';
5
- import AbstractComponent from './AbstractComponent';
6
- import { DummyRenderer, SerializableCommand } from '../lib';
7
- import { pathToRenderable } from '../rendering/RenderablePathSpec';
8
-
9
- describe('Stroke', () => {
10
- it('empty stroke should have an empty bounding box', () => {
11
- const stroke = new Stroke([{
12
- startPoint: Vec2.zero,
13
- commands: [],
14
- style: {
15
- fill: Color4.blue,
16
- },
17
- }]);
18
- expect(stroke.getBBox()).toMatchObject({
19
- x: 0, y: 0, w: 0, h: 0,
20
- });
21
- });
22
-
23
- it('cloned strokes should have the same points', () => {
24
- const stroke = new Stroke([
25
- pathToRenderable(Path.fromString('m1,1 2,2 3,3 z'), { fill: Color4.red })
26
- ]);
27
- const clone = stroke.clone();
28
-
29
- expect(
30
- (clone as Stroke).getPath().toString()
31
- ).toBe(
32
- stroke.getPath().toString()
33
- );
34
- });
35
-
36
- it('transforming a cloned stroke should not affect the original', () => {
37
- const editor = createEditor();
38
- const stroke = new Stroke([
39
- pathToRenderable(Path.fromString('m1,1 2,2 3,3 z'), { fill: Color4.red })
40
- ]);
41
- const origBBox = stroke.getBBox();
42
- expect(origBBox).toMatchObject({
43
- x: 1, y: 1,
44
- w: 5, h: 5,
45
- });
46
-
47
- const copy = stroke.clone();
48
- expect(copy.getBBox()).objEq(origBBox);
49
-
50
- stroke.transformBy(
51
- Mat33.scaling2D(Vec2.of(10, 10))
52
- ).apply(editor);
53
-
54
- expect(stroke.getBBox()).not.objEq(origBBox);
55
- expect(copy.getBBox()).objEq(origBBox);
56
- });
57
-
58
- it('strokes should deserialize from JSON data', () => {
59
- const deserialized = Stroke.deserializeFromJSON(`[
60
- {
61
- "style": { "fill": "#f00" },
62
- "path": "m0,0 l10,10z"
63
- }
64
- ]`);
65
- const path = deserialized.getPath();
66
-
67
- // Should cache the original string representation.
68
- expect(deserialized.getPath().toString()).toBe('m0,0 l10,10z');
69
- path['cachedStringVersion'] = null;
70
- expect(deserialized.getPath().toString()).toBe('M0,0L10,10L0,0');
71
- });
72
-
73
- it('strokes should load from just-serialized JSON data', () => {
74
- const deserialized = Stroke.deserializeFromJSON(`[
75
- {
76
- "style": { "fill": "#f00" },
77
- "path": "m0,0 l10,10z"
78
- }
79
- ]`);
80
-
81
- const redeserialized = AbstractComponent.deserialize(deserialized.serialize()) as Stroke;
82
- expect(redeserialized.getPath().toString()).toBe(deserialized.getPath().toString());
83
- expect(redeserialized.getStyle().color).objEq(deserialized.getStyle().color!);
84
- });
85
-
86
- it('strokes should be restylable', () => {
87
- const stroke = Stroke.deserializeFromJSON(`[
88
- {
89
- "style": { "fill": "#f00" },
90
- "path": "m0,0 l10,10z"
91
- },
92
- {
93
- "style": { "fill": "#f00" },
94
- "path": "m10,10 l100,10z"
95
- }
96
- ]`);
97
-
98
- expect(stroke.getStyle().color).objEq(Color4.fromHex('#f00'));
99
-
100
- // Should restyle even if no editor
101
- stroke.forceStyle({
102
- color: Color4.fromHex('#0f0')
103
- }, null);
104
-
105
- expect(stroke.getStyle().color).objEq(Color4.fromHex('#0f0'));
106
-
107
- const editor = createEditor();
108
- EditorImage.addElement(stroke).apply(editor);
109
-
110
- // Re-rendering should render with the new color
111
- const renderer = new DummyRenderer(editor.viewport);
112
- stroke.render(renderer);
113
- expect(renderer.lastFillStyle!.fill).toBe(stroke.getStyle().color);
114
-
115
- // Calling updateStyle should have similar results.
116
- const updateStyleCommand = stroke.updateStyle({
117
- color: Color4.fromHex('#00f'),
118
- });
119
- expect(stroke.getStyle().color).objEq(Color4.fromHex('#0f0'));
120
-
121
- updateStyleCommand.apply(editor);
122
- expect(editor.isRerenderQueued()).toBe(true);
123
-
124
- // Should do and undo correclty
125
- expect(stroke.getStyle().color).objEq(Color4.fromHex('#00f'));
126
- updateStyleCommand.unapply(editor);
127
- expect(stroke.getStyle().color).objEq(Color4.fromHex('#0f0'));
128
-
129
- // As should a deserialized updateStyle.
130
- const deserializedUpdateStyle = SerializableCommand.deserialize(updateStyleCommand.serialize(), editor);
131
- deserializedUpdateStyle.apply(editor);
132
-
133
- expect(stroke.getStyle().color).objEq(Color4.fromHex('#00f'));
134
- updateStyleCommand.unapply(editor);
135
- expect(stroke.getStyle().color).objEq(Color4.fromHex('#0f0'));
136
- });
137
- });
@@ -1,294 +0,0 @@
1
- import SerializableCommand from '../commands/SerializableCommand';
2
- import { Mat33, Path, Rect2, LineSegment2 } from '@js-draw/math';
3
- import Editor from '../Editor';
4
- import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
- import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
6
- import AbstractComponent from './AbstractComponent';
7
- import { ImageComponentLocalization } from './localization';
8
- import RestyleableComponent, { ComponentStyle, createRestyleComponentCommand } from './RestylableComponent';
9
- import RenderablePathSpec, { pathFromRenderable, pathToRenderable } from '../rendering/RenderablePathSpec';
10
-
11
- interface StrokePart extends RenderablePathSpec {
12
- path: Path;
13
- }
14
-
15
- /**
16
- * Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
17
- *
18
- * @example
19
- * For some {@link Editor} editor and `Stroke` stroke,
20
- *
21
- * **Restyling**:
22
- * ```ts
23
- * editor.dispatch(stroke.updateStyle({ color: Color4.red }));
24
- * ```
25
- *
26
- * **Transforming**:
27
- * ```ts
28
- * editor.dispatch(stroke.transformBy(Mat33.translation(Vec2.of(10, 0))));
29
- * ```
30
- */
31
- export default class Stroke extends AbstractComponent implements RestyleableComponent {
32
- private parts: StrokePart[];
33
- protected contentBBox: Rect2;
34
-
35
- // @internal
36
- // eslint-disable-next-line @typescript-eslint/prefer-as-const
37
- readonly isRestylableComponent: true = true;
38
-
39
- // See `getProportionalRenderingTime`
40
- private approximateRenderingTime: number;
41
-
42
- /**
43
- * Creates a `Stroke` from the given `parts`. All parts should have the
44
- * same color.
45
- *
46
- * @example
47
- * ```ts
48
- * // A path that starts at (1,1), moves to the right by (2, 0),
49
- * // then moves down and right by (3, 3)
50
- * const path = Path.fromString('m1,1 2,0 3,3');
51
- *
52
- * const stroke = new Stroke([
53
- * // Fill with red
54
- * path.toRenderable({ fill: Color4.red })
55
- * ]);
56
- * ```
57
- */
58
- public constructor(parts: RenderablePathSpec[]) {
59
- super('stroke');
60
-
61
- this.approximateRenderingTime = 0;
62
- this.parts = [];
63
-
64
- for (const section of parts) {
65
- const path = pathFromRenderable(section);
66
- const pathBBox = this.bboxForPart(path.bbox, section.style);
67
-
68
- if (!this.contentBBox) {
69
- this.contentBBox = pathBBox;
70
- } else {
71
- this.contentBBox = this.contentBBox.union(pathBBox);
72
- }
73
-
74
- this.parts.push({
75
- path,
76
-
77
- // To implement RenderablePathSpec
78
- startPoint: path.startPoint,
79
- style: section.style,
80
- commands: path.parts,
81
- });
82
-
83
- this.approximateRenderingTime += path.parts.length;
84
- }
85
- this.contentBBox ??= Rect2.empty;
86
- }
87
-
88
- public getStyle(): ComponentStyle {
89
- if (this.parts.length === 0) {
90
- return { };
91
- }
92
- const firstPart = this.parts[0];
93
-
94
- if (
95
- firstPart.style.stroke === undefined
96
- || firstPart.style.stroke.width === 0
97
- ) {
98
- return {
99
- color: firstPart.style.fill,
100
- };
101
- }
102
-
103
- return {
104
- color: firstPart.style.stroke.color,
105
- };
106
- }
107
-
108
- public updateStyle(style: ComponentStyle): SerializableCommand {
109
- return createRestyleComponentCommand(this.getStyle(), style, this);
110
- }
111
-
112
- public forceStyle(style: ComponentStyle, editor: Editor|null): void {
113
- if (!style.color) {
114
- return;
115
- }
116
-
117
- this.parts = this.parts.map((part) => {
118
- const newStyle = {
119
- ...part.style,
120
- stroke: part.style.stroke ? {
121
- ...part.style.stroke,
122
- } : undefined,
123
- };
124
-
125
- // Change the stroke color if a stroked shape. Else,
126
- // change the fill.
127
- if (newStyle.stroke && newStyle.stroke.width > 0) {
128
- newStyle.stroke.color = style.color!;
129
- } else {
130
- newStyle.fill = style.color!;
131
- }
132
-
133
- return {
134
- path: part.path,
135
- startPoint: part.startPoint,
136
- commands: part.commands,
137
- style: newStyle,
138
- };
139
- });
140
-
141
- if (editor) {
142
- editor.image.queueRerenderOf(this);
143
- editor.queueRerender();
144
- }
145
- }
146
-
147
- public override intersects(line: LineSegment2): boolean {
148
- for (const part of this.parts) {
149
- const strokeWidth = part.style.stroke?.width;
150
- const strokeRadius = strokeWidth ? strokeWidth / 2 : undefined;
151
-
152
- if (part.path.intersection(line, strokeRadius).length > 0) {
153
- return true;
154
- }
155
- }
156
- return false;
157
- }
158
-
159
- public override render(canvas: AbstractRenderer, visibleRect?: Rect2): void {
160
- canvas.startObject(this.getBBox());
161
- for (const part of this.parts) {
162
- const bbox = this.bboxForPart(part.path.bbox, part.style);
163
- if (visibleRect) {
164
- if (!bbox.intersects(visibleRect)) {
165
- continue;
166
- }
167
-
168
- const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 3 || bbox.size.y > visibleRect.size.y * 3;
169
- if (muchBiggerThanVisible && !part.path.roughlyIntersects(visibleRect, part.style.stroke?.width ?? 0)) {
170
- continue;
171
- }
172
- }
173
-
174
- canvas.drawPath(part);
175
- }
176
- canvas.endObject(this.getLoadSaveData());
177
- }
178
-
179
- public override getProportionalRenderingTime(): number {
180
- return this.approximateRenderingTime;
181
- }
182
-
183
- // Grows the bounding box for a given stroke part based on that part's style.
184
- private bboxForPart(origBBox: Rect2, style: RenderingStyle) {
185
- if (!style.stroke) {
186
- return origBBox;
187
- }
188
-
189
- return origBBox.grownBy(style.stroke.width / 2);
190
- }
191
-
192
- public override getExactBBox(): Rect2 {
193
- let bbox: Rect2|null = null;
194
-
195
- for (const { path, style } of this.parts) {
196
- // Paths' default .bbox can be
197
- const partBBox = this.bboxForPart(path.getExactBBox(), style);
198
- bbox ??= partBBox;
199
-
200
- bbox = bbox.union(partBBox);
201
- }
202
-
203
- return bbox ?? Rect2.empty;
204
- }
205
-
206
- protected applyTransformation(affineTransfm: Mat33): void {
207
- this.contentBBox = Rect2.empty;
208
- let isFirstPart = true;
209
-
210
- // Update each part
211
- this.parts = this.parts.map((part) => {
212
- const newPath = part.path.transformedBy(affineTransfm);
213
- const newStyle = {
214
- ...part.style,
215
- stroke: part.style.stroke ? {
216
- ...part.style.stroke,
217
- } : undefined,
218
- };
219
-
220
- // Approximate the scale factor.
221
- if (newStyle.stroke) {
222
- const scaleFactor = affineTransfm.getScaleFactor();
223
- newStyle.stroke.width *= scaleFactor;
224
- }
225
-
226
- const newBBox = this.bboxForPart(newPath.bbox, newStyle);
227
-
228
- if (isFirstPart) {
229
- this.contentBBox = newBBox;
230
- isFirstPart = false;
231
- } else {
232
- this.contentBBox = this.contentBBox.union(newBBox);
233
- }
234
-
235
- return {
236
- path: newPath,
237
- startPoint: newPath.startPoint,
238
- commands: newPath.parts,
239
- style: newStyle,
240
- };
241
- });
242
- }
243
-
244
- /**
245
- * @returns the {@link Path.union} of all paths that make up this stroke.
246
- */
247
- public getPath() {
248
- let result: Path|null = null;
249
- for (const part of this.parts) {
250
- if (result) {
251
- result = result.union(part.path);
252
- } else {
253
- result ??= part.path;
254
- }
255
- }
256
- return result ?? Path.empty;
257
- }
258
-
259
- public override description(localization: ImageComponentLocalization): string {
260
- return localization.stroke;
261
- }
262
-
263
- protected override createClone(): AbstractComponent {
264
- return new Stroke(this.parts);
265
- }
266
-
267
- protected override serializeToJSON() {
268
- return this.parts.map(part => {
269
- return {
270
- style: styleToJSON(part.style),
271
- path: part.path.serialize(),
272
- };
273
- });
274
- }
275
-
276
- /** @internal */
277
- public static deserializeFromJSON(json: any): Stroke {
278
- if (typeof json === 'string') {
279
- json = JSON.parse(json);
280
- }
281
-
282
- if (typeof json !== 'object' || typeof json.length !== 'number') {
283
- throw new Error(`${json} is missing required field, parts, or parts is of the wrong type.`);
284
- }
285
-
286
- const pathSpec: RenderablePathSpec[] = json.map((part: any) => {
287
- const style = styleFromJSON(part.style);
288
- return pathToRenderable(Path.fromString(part.path), style);
289
- });
290
- return new Stroke(pathSpec);
291
- }
292
- }
293
-
294
- AbstractComponent.registerComponent('stroke', Stroke.deserializeFromJSON);
@@ -1,202 +0,0 @@
1
- import EditorImage from '../EditorImage';
2
- import { Vec2, Mat33, Color4 } from '@js-draw/math';
3
- import TextRenderingStyle from '../rendering/TextRenderingStyle';
4
- import createEditor from '../testing/createEditor';
5
- import AbstractComponent from './AbstractComponent';
6
- import TextComponent, { TextTransformMode } from './TextComponent';
7
-
8
-
9
- describe('TextComponent', () => {
10
- it('should be serializable', () => {
11
- const style: TextRenderingStyle = {
12
- size: 12,
13
- fontFamily: 'serif',
14
- renderingStyle: { fill: Color4.black },
15
- };
16
- const text = new TextComponent([ 'Foo' ], Mat33.identity, style);
17
- const serialized = text.serialize();
18
- const deserialized = AbstractComponent.deserialize(serialized) as TextComponent;
19
- expect(deserialized.getBBox()).objEq(text.getBBox());
20
- expect(deserialized['getText']()).toContain('Foo');
21
- });
22
-
23
- it('should be deserializable', () => {
24
- const textComponent = TextComponent.deserializeFromString(`{
25
- "textObjects": [ { "text": "Foo" } ],
26
- "transform": [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ],
27
- "style": {
28
- "fontFamily": "sans",
29
- "size": 10,
30
- "renderingStyle": { "fill": "#000" }
31
- }
32
- }`);
33
-
34
- expect(textComponent.getText()).toBe('Foo');
35
- expect(textComponent.getTransform()).objEq(Mat33.identity);
36
- expect(textComponent.getStyle().color!).objEq(Color4.black);
37
- expect(textComponent.getTextStyle().fontFamily!).toBe('sans');
38
- });
39
-
40
- it('should be restylable', () => {
41
- const style: TextRenderingStyle = {
42
- size: 10,
43
- fontFamily: 'sans',
44
- renderingStyle: { fill: Color4.red },
45
- };
46
- const text = new TextComponent([ 'Foo' ], Mat33.identity, style);
47
-
48
- expect(text.getStyle().color).objEq(Color4.red);
49
- text.forceStyle({
50
- color: Color4.green,
51
- }, null);
52
- expect(text.getStyle().color).objEq(Color4.green);
53
- expect(text.getTextStyle().renderingStyle.fill).objEq(Color4.green);
54
-
55
- const restyleCommand = text.updateStyle({
56
- color: Color4.purple,
57
- });
58
-
59
- // Should queue a re-render after restyling.
60
- const editor = createEditor();
61
- EditorImage.addElement(text).apply(editor);
62
-
63
- editor.rerender();
64
- expect(editor.isRerenderQueued()).toBe(false);
65
- editor.dispatch(restyleCommand);
66
- expect(editor.isRerenderQueued()).toBe(true);
67
-
68
- // Undoing should reset to the correct color.
69
- expect(text.getStyle().color).objEq(Color4.purple);
70
- editor.history.undo();
71
- expect(text.getStyle().color).objEq(Color4.green);
72
- });
73
-
74
- it('calling forceStyle on the duplicate of a TextComponent should preserve the original\'s style', () => {
75
- const originalStyle: TextRenderingStyle = {
76
- size: 11,
77
- fontFamily: 'sans-serif',
78
- renderingStyle: { fill: Color4.purple, },
79
- };
80
-
81
- const text1 = new TextComponent([ 'Test' ], Mat33.identity, originalStyle);
82
- const text2 = text1.clone() as TextComponent;
83
-
84
- text1.forceStyle({
85
- color: Color4.red,
86
- }, null);
87
-
88
- expect(text2.getStyle().color).objEq(Color4.purple);
89
- expect(text1.getStyle().color).objEq(Color4.red);
90
-
91
- text2.forceStyle({
92
- textStyle: originalStyle,
93
- }, null);
94
-
95
- expect(text1.getStyle().color).objEq(Color4.red);
96
- expect(text2.getTextStyle()).toMatchObject(originalStyle);
97
- });
98
-
99
- describe('should position text components relatively or absolutely (bounding box tests)', () => {
100
- const baseStyle: TextRenderingStyle = {
101
- size: 12,
102
- fontFamily: 'sans-serif',
103
- renderingStyle: { fill: Color4.red },
104
- };
105
-
106
- it('strings should be placed relative to one another', () => {
107
- const str1 = 'test';
108
- const str2 = 'test2';
109
-
110
- const container = new TextComponent([ str1, str2 ], Mat33.identity, baseStyle);
111
-
112
- // Create separate components for str1 and str2 so we can check their individual bounding boxes
113
- const str1Component = new TextComponent([ str1 ], Mat33.identity, baseStyle);
114
- const str2Component = new TextComponent([ str2 ], Mat33.identity, baseStyle);
115
-
116
- const widthSum = str1Component.getBBox().width + str2Component.getBBox().width;
117
- const maxHeight = Math.max(str1Component.getBBox().height, str2Component.getBBox().height);
118
- expect(container.getBBox().size).objEq(Vec2.of(widthSum, maxHeight));
119
- });
120
-
121
- it('RELATIVE_X_ABSOLUTE_Y should work (relatively positioned along x, absolutely along y)', () => {
122
- const component1 = new TextComponent([ 'test' ], Mat33.identity, baseStyle);
123
-
124
- const componentTranslation = Vec2.of(10, 10);
125
- const component2 = new TextComponent(
126
- [ 'relatively' ],
127
- Mat33.translation(componentTranslation),
128
- baseStyle,
129
- TextTransformMode.RELATIVE_X_ABSOLUTE_Y
130
- );
131
-
132
- const component3 = new TextComponent(
133
- [ 'more of a test...' ],
134
- Mat33.translation(componentTranslation),
135
- baseStyle,
136
- TextTransformMode.RELATIVE_X_ABSOLUTE_Y
137
- );
138
-
139
-
140
- const container = new TextComponent([ component1, component2, component3 ], Mat33.identity, baseStyle);
141
- const expectedWidth =
142
- component1.getBBox().width
143
- // x should take the translation from each component into account.
144
- + componentTranslation.x + component2.getBBox().width
145
- + componentTranslation.x + component3.getBBox().width;
146
- const expectedHeight = Math.max(
147
- component1.getBBox().height,
148
-
149
- // Absolute y: Should *not* take into account both components' y translations
150
- componentTranslation.y + component3.getBBox().height
151
- );
152
- expect(container.getBBox().size).objEq(Vec2.of(expectedWidth, expectedHeight));
153
- });
154
-
155
- it('RELATIVE_Y_ABSOLUTE_X should work (relatively positioned along y, absolutely along x)', () => {
156
- const firstComponentTranslation = Vec2.of(1000, 1000);
157
- const component1 = new TextComponent(
158
- [ '...' ],
159
-
160
- // The translation of the first component shouldn't affect the Y size of the bounding box.
161
- Mat33.translation(firstComponentTranslation),
162
-
163
- baseStyle);
164
-
165
- const componentTranslation = Vec2.of(10, 20);
166
- const component2 = new TextComponent(
167
- [ 'Test!' ],
168
- Mat33.translation(componentTranslation),
169
- baseStyle,
170
- TextTransformMode.RELATIVE_Y_ABSOLUTE_X
171
- );
172
-
173
- const component3 = new TextComponent(
174
- [ 'Even more of a test.' ],
175
- Mat33.translation(componentTranslation),
176
- baseStyle,
177
- TextTransformMode.RELATIVE_Y_ABSOLUTE_X
178
- );
179
-
180
-
181
- const container = new TextComponent([ component1, component2, component3 ], Mat33.identity, baseStyle);
182
- const expectedWidth =
183
- component1.getBBox().width
184
-
185
- // Space between the start of components 2 and 3 and the start of component 1
186
- + firstComponentTranslation.x - componentTranslation.x;
187
-
188
- const expectedHeight =
189
- // Don't include component1.bbox.height: component1 overlaps with component 2 completely in y
190
- // similarly, component 2 overlaps completely with component3 in y.
191
- //
192
- // Note that while relative positioning is relative to the right edge of the baseline of the previous
193
- // item (when in left-to-right mode). Thus, x is adjusted automatically by the text width, while
194
- // y remains the same (if there is no additional translation).
195
- + componentTranslation.y
196
- + componentTranslation.y
197
- + component3.getBBox().height;
198
-
199
- expect(container.getBBox().size).objEq(Vec2.of(expectedWidth, expectedHeight));
200
- });
201
- });
202
- });