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.
- package/LICENSE +21 -0
- package/dist/Editor.css +1 -0
- package/dist/bundle.js +1 -1
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/toolbar/AbstractToolbar.d.ts +9 -13
- package/dist/cjs/toolbar/AbstractToolbar.js +14 -19
- package/dist/cjs/toolbar/widgets/SaveActionWidget.d.ts +10 -0
- package/dist/cjs/toolbar/widgets/SaveActionWidget.js +26 -0
- package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/toolbar/AbstractToolbar.d.ts +9 -13
- package/dist/mjs/toolbar/AbstractToolbar.mjs +14 -19
- package/dist/mjs/toolbar/widgets/SaveActionWidget.d.ts +10 -0
- package/dist/mjs/toolbar/widgets/SaveActionWidget.mjs +21 -0
- package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
- package/dist/mjs/version.mjs +1 -1
- package/docs/img/readme-images/js-draw.jpg +0 -0
- package/docs/img/readme-images/unsupported-elements--in-editor.png +0 -0
- package/package.json +5 -4
- package/src/toolbar/EdgeToolbar.scss +1 -0
- package/dist-test/test_imports/package-lock.json +0 -13
- package/dist-test/test_imports/package.json +0 -12
- package/dist-test/test_imports/test-imports.js +0 -11
- package/dist-test/test_imports/test-require.cjs +0 -14
- package/src/Editor.loadFrom.test.ts +0 -24
- package/src/Editor.test.ts +0 -107
- package/src/Editor.toSVG.test.ts +0 -294
- package/src/Editor.ts +0 -1443
- package/src/EditorImage.test.ts +0 -117
- package/src/EditorImage.ts +0 -609
- package/src/EventDispatcher.test.ts +0 -123
- package/src/EventDispatcher.ts +0 -72
- package/src/Pointer.ts +0 -183
- package/src/SVGLoader.test.ts +0 -114
- package/src/SVGLoader.ts +0 -672
- package/src/UndoRedoHistory.test.ts +0 -34
- package/src/UndoRedoHistory.ts +0 -102
- package/src/Viewport.ts +0 -322
- package/src/bundle/bundled.ts +0 -7
- package/src/commands/Command.ts +0 -45
- package/src/commands/Duplicate.ts +0 -75
- package/src/commands/Erase.ts +0 -95
- package/src/commands/SerializableCommand.ts +0 -49
- package/src/commands/UnresolvedCommand.ts +0 -37
- package/src/commands/invertCommand.ts +0 -58
- package/src/commands/lib.ts +0 -16
- package/src/commands/localization.ts +0 -47
- package/src/commands/uniteCommands.test.ts +0 -23
- package/src/commands/uniteCommands.ts +0 -140
- package/src/components/AbstractComponent.transformBy.test.ts +0 -23
- package/src/components/AbstractComponent.ts +0 -383
- package/src/components/BackgroundComponent.test.ts +0 -44
- package/src/components/BackgroundComponent.ts +0 -348
- package/src/components/ImageComponent.ts +0 -176
- package/src/components/RestylableComponent.ts +0 -161
- package/src/components/SVGGlobalAttributesObject.ts +0 -79
- package/src/components/Stroke.test.ts +0 -137
- package/src/components/Stroke.ts +0 -294
- package/src/components/TextComponent.test.ts +0 -202
- package/src/components/TextComponent.ts +0 -429
- package/src/components/UnknownSVGObject.test.ts +0 -10
- package/src/components/UnknownSVGObject.ts +0 -60
- package/src/components/builders/ArrowBuilder.ts +0 -106
- package/src/components/builders/CircleBuilder.ts +0 -100
- package/src/components/builders/FreehandLineBuilder.test.ts +0 -24
- package/src/components/builders/FreehandLineBuilder.ts +0 -210
- package/src/components/builders/LineBuilder.ts +0 -77
- package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +0 -453
- package/src/components/builders/RectangleBuilder.ts +0 -73
- package/src/components/builders/types.ts +0 -15
- package/src/components/lib.ts +0 -31
- package/src/components/localization.ts +0 -24
- package/src/components/util/StrokeSmoother.ts +0 -302
- package/src/components/util/describeComponentList.ts +0 -18
- package/src/dialogs/makeAboutDialog.ts +0 -82
- package/src/inputEvents.ts +0 -143
- package/src/lib.ts +0 -91
- package/src/localization.ts +0 -34
- package/src/localizations/de.ts +0 -146
- package/src/localizations/en.ts +0 -8
- package/src/localizations/es.ts +0 -74
- package/src/localizations/getLocalizationTable.test.ts +0 -27
- package/src/localizations/getLocalizationTable.ts +0 -74
- package/src/rendering/Display.ts +0 -247
- package/src/rendering/RenderablePathSpec.ts +0 -88
- package/src/rendering/RenderingStyle.test.ts +0 -68
- package/src/rendering/RenderingStyle.ts +0 -55
- package/src/rendering/TextRenderingStyle.ts +0 -55
- package/src/rendering/caching/CacheRecord.test.ts +0 -48
- package/src/rendering/caching/CacheRecord.ts +0 -76
- package/src/rendering/caching/CacheRecordManager.ts +0 -71
- package/src/rendering/caching/RenderingCache.test.ts +0 -43
- package/src/rendering/caching/RenderingCache.ts +0 -66
- package/src/rendering/caching/RenderingCacheNode.ts +0 -404
- package/src/rendering/caching/testUtils.ts +0 -35
- package/src/rendering/caching/types.ts +0 -34
- package/src/rendering/lib.ts +0 -8
- package/src/rendering/localization.ts +0 -20
- package/src/rendering/renderers/AbstractRenderer.ts +0 -232
- package/src/rendering/renderers/CanvasRenderer.ts +0 -312
- package/src/rendering/renderers/DummyRenderer.test.ts +0 -41
- package/src/rendering/renderers/DummyRenderer.ts +0 -142
- package/src/rendering/renderers/SVGRenderer.ts +0 -434
- package/src/rendering/renderers/TextOnlyRenderer.test.ts +0 -34
- package/src/rendering/renderers/TextOnlyRenderer.ts +0 -68
- package/src/shortcuts/KeyBinding.test.ts +0 -61
- package/src/shortcuts/KeyBinding.ts +0 -257
- package/src/shortcuts/KeyboardShortcutManager.test.ts +0 -95
- package/src/shortcuts/KeyboardShortcutManager.ts +0 -163
- package/src/shortcuts/lib.ts +0 -3
- package/src/testing/createEditor.ts +0 -11
- package/src/testing/getUniquePointerId.ts +0 -18
- package/src/testing/lib.ts +0 -3
- package/src/testing/sendPenEvent.ts +0 -36
- package/src/testing/sendTouchEvent.ts +0 -71
- package/src/toolbar/AbstractToolbar.ts +0 -542
- package/src/toolbar/DropdownToolbar.ts +0 -220
- package/src/toolbar/EdgeToolbar.test.ts +0 -54
- package/src/toolbar/EdgeToolbar.ts +0 -543
- package/src/toolbar/IconProvider.ts +0 -861
- package/src/toolbar/constants.ts +0 -1
- package/src/toolbar/lib.ts +0 -6
- package/src/toolbar/localization.ts +0 -136
- package/src/toolbar/types.ts +0 -13
- package/src/toolbar/widgets/ActionButtonWidget.ts +0 -39
- package/src/toolbar/widgets/BaseToolWidget.ts +0 -81
- package/src/toolbar/widgets/BaseWidget.ts +0 -495
- package/src/toolbar/widgets/DocumentPropertiesWidget.ts +0 -250
- package/src/toolbar/widgets/EraserToolWidget.ts +0 -84
- package/src/toolbar/widgets/HandToolWidget.ts +0 -239
- package/src/toolbar/widgets/InsertImageWidget.ts +0 -248
- package/src/toolbar/widgets/OverflowWidget.ts +0 -92
- package/src/toolbar/widgets/PenToolWidget.ts +0 -369
- package/src/toolbar/widgets/SelectionToolWidget.ts +0 -195
- package/src/toolbar/widgets/TextToolWidget.ts +0 -149
- package/src/toolbar/widgets/components/makeColorInput.ts +0 -184
- package/src/toolbar/widgets/components/makeFileInput.ts +0 -128
- package/src/toolbar/widgets/components/makeGridSelector.ts +0 -179
- package/src/toolbar/widgets/components/makeSeparator.ts +0 -17
- package/src/toolbar/widgets/components/makeThicknessSlider.ts +0 -62
- package/src/toolbar/widgets/keybindings.ts +0 -19
- package/src/toolbar/widgets/layout/DropdownLayoutManager.ts +0 -262
- package/src/toolbar/widgets/layout/EdgeToolbarLayoutManager.ts +0 -71
- package/src/toolbar/widgets/layout/types.ts +0 -74
- package/src/toolbar/widgets/lib.ts +0 -13
- package/src/tools/BaseTool.ts +0 -169
- package/src/tools/Eraser.test.ts +0 -103
- package/src/tools/Eraser.ts +0 -173
- package/src/tools/FindTool.test.ts +0 -67
- package/src/tools/FindTool.ts +0 -153
- package/src/tools/InputFilter/FunctionMapper.ts +0 -17
- package/src/tools/InputFilter/InputMapper.ts +0 -41
- package/src/tools/InputFilter/InputPipeline.test.ts +0 -41
- package/src/tools/InputFilter/InputPipeline.ts +0 -34
- package/src/tools/InputFilter/InputStabilizer.ts +0 -254
- package/src/tools/InputFilter/StrokeKeyboardControl.ts +0 -104
- package/src/tools/PanZoom.test.ts +0 -339
- package/src/tools/PanZoom.ts +0 -525
- package/src/tools/PasteHandler.ts +0 -94
- package/src/tools/Pen.test.ts +0 -260
- package/src/tools/Pen.ts +0 -284
- package/src/tools/PipetteTool.ts +0 -84
- package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +0 -29
- package/src/tools/SelectionTool/Selection.ts +0 -647
- package/src/tools/SelectionTool/SelectionHandle.ts +0 -142
- package/src/tools/SelectionTool/SelectionTool.test.ts +0 -370
- package/src/tools/SelectionTool/SelectionTool.ts +0 -510
- package/src/tools/SelectionTool/TransformMode.ts +0 -112
- package/src/tools/SelectionTool/types.ts +0 -11
- package/src/tools/SoundUITool.ts +0 -221
- package/src/tools/TextTool.ts +0 -339
- package/src/tools/ToolController.ts +0 -224
- package/src/tools/ToolEnabledGroup.ts +0 -14
- package/src/tools/ToolSwitcherShortcut.ts +0 -39
- package/src/tools/ToolbarShortcutHandler.ts +0 -39
- package/src/tools/UndoRedoShortcut.test.ts +0 -62
- package/src/tools/UndoRedoShortcut.ts +0 -24
- package/src/tools/keybindings.ts +0 -85
- package/src/tools/lib.ts +0 -22
- package/src/tools/localization.ts +0 -76
- package/src/types.ts +0 -151
- package/src/util/ReactiveValue.test.ts +0 -168
- package/src/util/ReactiveValue.ts +0 -241
- package/src/util/assertions.ts +0 -55
- package/src/util/fileToBase64.ts +0 -18
- package/src/util/guessKeyCodeFromKey.ts +0 -36
- package/src/util/listPrefixMatch.ts +0 -19
- package/src/util/stopPropagationOfScrollingWheelEvents.ts +0 -20
- package/src/util/untilNextAnimationFrame.ts +0 -9
- package/src/util/waitForAll.ts +0 -18
- package/src/util/waitForTimeout.ts +0 -9
- package/src/version.test.ts +0 -12
- package/src/version.ts +0 -3
- package/tools/allLocales.js +0 -4
- 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
|
-
});
|
package/src/components/Stroke.ts
DELETED
@@ -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
|
-
});
|