js-draw 1.0.1 → 1.0.2

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/cjs/version.js +1 -1
  4. package/dist/mjs/version.mjs +1 -1
  5. package/docs/img/readme-images/js-draw.jpg +0 -0
  6. package/docs/img/readme-images/unsupported-elements--in-editor.png +0 -0
  7. package/package.json +5 -4
  8. package/dist-test/test_imports/package-lock.json +0 -13
  9. package/dist-test/test_imports/package.json +0 -12
  10. package/dist-test/test_imports/test-imports.js +0 -11
  11. package/dist-test/test_imports/test-require.cjs +0 -14
  12. package/src/Editor.loadFrom.test.ts +0 -24
  13. package/src/Editor.test.ts +0 -107
  14. package/src/Editor.toSVG.test.ts +0 -294
  15. package/src/Editor.ts +0 -1443
  16. package/src/EditorImage.test.ts +0 -117
  17. package/src/EditorImage.ts +0 -609
  18. package/src/EventDispatcher.test.ts +0 -123
  19. package/src/EventDispatcher.ts +0 -72
  20. package/src/Pointer.ts +0 -183
  21. package/src/SVGLoader.test.ts +0 -114
  22. package/src/SVGLoader.ts +0 -672
  23. package/src/UndoRedoHistory.test.ts +0 -34
  24. package/src/UndoRedoHistory.ts +0 -102
  25. package/src/Viewport.ts +0 -322
  26. package/src/bundle/bundled.ts +0 -7
  27. package/src/commands/Command.ts +0 -45
  28. package/src/commands/Duplicate.ts +0 -75
  29. package/src/commands/Erase.ts +0 -95
  30. package/src/commands/SerializableCommand.ts +0 -49
  31. package/src/commands/UnresolvedCommand.ts +0 -37
  32. package/src/commands/invertCommand.ts +0 -58
  33. package/src/commands/lib.ts +0 -16
  34. package/src/commands/localization.ts +0 -47
  35. package/src/commands/uniteCommands.test.ts +0 -23
  36. package/src/commands/uniteCommands.ts +0 -140
  37. package/src/components/AbstractComponent.transformBy.test.ts +0 -23
  38. package/src/components/AbstractComponent.ts +0 -383
  39. package/src/components/BackgroundComponent.test.ts +0 -44
  40. package/src/components/BackgroundComponent.ts +0 -348
  41. package/src/components/ImageComponent.ts +0 -176
  42. package/src/components/RestylableComponent.ts +0 -161
  43. package/src/components/SVGGlobalAttributesObject.ts +0 -79
  44. package/src/components/Stroke.test.ts +0 -137
  45. package/src/components/Stroke.ts +0 -294
  46. package/src/components/TextComponent.test.ts +0 -202
  47. package/src/components/TextComponent.ts +0 -429
  48. package/src/components/UnknownSVGObject.test.ts +0 -10
  49. package/src/components/UnknownSVGObject.ts +0 -60
  50. package/src/components/builders/ArrowBuilder.ts +0 -106
  51. package/src/components/builders/CircleBuilder.ts +0 -100
  52. package/src/components/builders/FreehandLineBuilder.test.ts +0 -24
  53. package/src/components/builders/FreehandLineBuilder.ts +0 -210
  54. package/src/components/builders/LineBuilder.ts +0 -77
  55. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +0 -453
  56. package/src/components/builders/RectangleBuilder.ts +0 -73
  57. package/src/components/builders/types.ts +0 -15
  58. package/src/components/lib.ts +0 -31
  59. package/src/components/localization.ts +0 -24
  60. package/src/components/util/StrokeSmoother.ts +0 -302
  61. package/src/components/util/describeComponentList.ts +0 -18
  62. package/src/dialogs/makeAboutDialog.ts +0 -82
  63. package/src/inputEvents.ts +0 -143
  64. package/src/lib.ts +0 -91
  65. package/src/localization.ts +0 -34
  66. package/src/localizations/de.ts +0 -146
  67. package/src/localizations/en.ts +0 -8
  68. package/src/localizations/es.ts +0 -74
  69. package/src/localizations/getLocalizationTable.test.ts +0 -27
  70. package/src/localizations/getLocalizationTable.ts +0 -74
  71. package/src/rendering/Display.ts +0 -247
  72. package/src/rendering/RenderablePathSpec.ts +0 -88
  73. package/src/rendering/RenderingStyle.test.ts +0 -68
  74. package/src/rendering/RenderingStyle.ts +0 -55
  75. package/src/rendering/TextRenderingStyle.ts +0 -55
  76. package/src/rendering/caching/CacheRecord.test.ts +0 -48
  77. package/src/rendering/caching/CacheRecord.ts +0 -76
  78. package/src/rendering/caching/CacheRecordManager.ts +0 -71
  79. package/src/rendering/caching/RenderingCache.test.ts +0 -43
  80. package/src/rendering/caching/RenderingCache.ts +0 -66
  81. package/src/rendering/caching/RenderingCacheNode.ts +0 -404
  82. package/src/rendering/caching/testUtils.ts +0 -35
  83. package/src/rendering/caching/types.ts +0 -34
  84. package/src/rendering/lib.ts +0 -8
  85. package/src/rendering/localization.ts +0 -20
  86. package/src/rendering/renderers/AbstractRenderer.ts +0 -232
  87. package/src/rendering/renderers/CanvasRenderer.ts +0 -312
  88. package/src/rendering/renderers/DummyRenderer.test.ts +0 -41
  89. package/src/rendering/renderers/DummyRenderer.ts +0 -142
  90. package/src/rendering/renderers/SVGRenderer.ts +0 -434
  91. package/src/rendering/renderers/TextOnlyRenderer.test.ts +0 -34
  92. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -68
  93. package/src/shortcuts/KeyBinding.test.ts +0 -61
  94. package/src/shortcuts/KeyBinding.ts +0 -257
  95. package/src/shortcuts/KeyboardShortcutManager.test.ts +0 -95
  96. package/src/shortcuts/KeyboardShortcutManager.ts +0 -163
  97. package/src/shortcuts/lib.ts +0 -3
  98. package/src/testing/createEditor.ts +0 -11
  99. package/src/testing/getUniquePointerId.ts +0 -18
  100. package/src/testing/lib.ts +0 -3
  101. package/src/testing/sendPenEvent.ts +0 -36
  102. package/src/testing/sendTouchEvent.ts +0 -71
  103. package/src/toolbar/AbstractToolbar.ts +0 -542
  104. package/src/toolbar/DropdownToolbar.ts +0 -220
  105. package/src/toolbar/EdgeToolbar.test.ts +0 -54
  106. package/src/toolbar/EdgeToolbar.ts +0 -543
  107. package/src/toolbar/IconProvider.ts +0 -861
  108. package/src/toolbar/constants.ts +0 -1
  109. package/src/toolbar/lib.ts +0 -6
  110. package/src/toolbar/localization.ts +0 -136
  111. package/src/toolbar/types.ts +0 -13
  112. package/src/toolbar/widgets/ActionButtonWidget.ts +0 -39
  113. package/src/toolbar/widgets/BaseToolWidget.ts +0 -81
  114. package/src/toolbar/widgets/BaseWidget.ts +0 -495
  115. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +0 -250
  116. package/src/toolbar/widgets/EraserToolWidget.ts +0 -84
  117. package/src/toolbar/widgets/HandToolWidget.ts +0 -239
  118. package/src/toolbar/widgets/InsertImageWidget.ts +0 -248
  119. package/src/toolbar/widgets/OverflowWidget.ts +0 -92
  120. package/src/toolbar/widgets/PenToolWidget.ts +0 -369
  121. package/src/toolbar/widgets/SelectionToolWidget.ts +0 -195
  122. package/src/toolbar/widgets/TextToolWidget.ts +0 -149
  123. package/src/toolbar/widgets/components/makeColorInput.ts +0 -184
  124. package/src/toolbar/widgets/components/makeFileInput.ts +0 -128
  125. package/src/toolbar/widgets/components/makeGridSelector.ts +0 -179
  126. package/src/toolbar/widgets/components/makeSeparator.ts +0 -17
  127. package/src/toolbar/widgets/components/makeThicknessSlider.ts +0 -62
  128. package/src/toolbar/widgets/keybindings.ts +0 -19
  129. package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +0 -262
  130. package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +0 -71
  131. package/src/toolbar/widgets/layout/types.ts +0 -74
  132. package/src/toolbar/widgets/lib.ts +0 -13
  133. package/src/tools/BaseTool.ts +0 -169
  134. package/src/tools/Eraser.test.ts +0 -103
  135. package/src/tools/Eraser.ts +0 -173
  136. package/src/tools/FindTool.test.ts +0 -67
  137. package/src/tools/FindTool.ts +0 -153
  138. package/src/tools/InputFilter/FunctionMapper.ts +0 -17
  139. package/src/tools/InputFilter/InputMapper.ts +0 -41
  140. package/src/tools/InputFilter/InputPipeline.test.ts +0 -41
  141. package/src/tools/InputFilter/InputPipeline.ts +0 -34
  142. package/src/tools/InputFilter/InputStabilizer.ts +0 -254
  143. package/src/tools/InputFilter/StrokeKeyboardControl.ts +0 -104
  144. package/src/tools/PanZoom.test.ts +0 -339
  145. package/src/tools/PanZoom.ts +0 -525
  146. package/src/tools/PasteHandler.ts +0 -94
  147. package/src/tools/Pen.test.ts +0 -260
  148. package/src/tools/Pen.ts +0 -284
  149. package/src/tools/PipetteTool.ts +0 -84
  150. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +0 -29
  151. package/src/tools/SelectionTool/Selection.ts +0 -647
  152. package/src/tools/SelectionTool/SelectionHandle.ts +0 -142
  153. package/src/tools/SelectionTool/SelectionTool.test.ts +0 -370
  154. package/src/tools/SelectionTool/SelectionTool.ts +0 -510
  155. package/src/tools/SelectionTool/TransformMode.ts +0 -112
  156. package/src/tools/SelectionTool/types.ts +0 -11
  157. package/src/tools/SoundUITool.ts +0 -221
  158. package/src/tools/TextTool.ts +0 -339
  159. package/src/tools/ToolController.ts +0 -224
  160. package/src/tools/ToolEnabledGroup.ts +0 -14
  161. package/src/tools/ToolSwitcherShortcut.ts +0 -39
  162. package/src/tools/ToolbarShortcutHandler.ts +0 -39
  163. package/src/tools/UndoRedoShortcut.test.ts +0 -62
  164. package/src/tools/UndoRedoShortcut.ts +0 -24
  165. package/src/tools/keybindings.ts +0 -85
  166. package/src/tools/lib.ts +0 -22
  167. package/src/tools/localization.ts +0 -76
  168. package/src/types.ts +0 -151
  169. package/src/util/ReactiveValue.test.ts +0 -168
  170. package/src/util/ReactiveValue.ts +0 -241
  171. package/src/util/assertions.ts +0 -55
  172. package/src/util/fileToBase64.ts +0 -18
  173. package/src/util/guessKeyCodeFromKey.ts +0 -36
  174. package/src/util/listPrefixMatch.ts +0 -19
  175. package/src/util/stopPropagationOfScrollingWheelEvents.ts +0 -20
  176. package/src/util/untilNextAnimationFrame.ts +0 -9
  177. package/src/util/waitForAll.ts +0 -18
  178. package/src/util/waitForTimeout.ts +0 -9
  179. package/src/version.test.ts +0 -12
  180. package/src/version.ts +0 -3
  181. package/tools/allLocales.js +0 -4
  182. 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
- });