js-draw 0.0.5 → 0.0.8
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/CHANGELOG.md +11 -0
- package/README.md +13 -0
- package/dist/build_tools/bundle.js +2 -2
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +9 -1
- package/dist/src/EditorImage.d.ts +2 -2
- package/dist/src/SVGLoader.d.ts +2 -0
- package/dist/src/SVGLoader.js +15 -1
- package/dist/src/UndoRedoHistory.d.ts +2 -0
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +15 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +29 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +15 -3
- package/dist/src/geometry/Path.js +11 -0
- package/dist/src/htmlUtil.d.ts +1 -0
- package/dist/src/rendering/SVGRenderer.d.ts +2 -0
- package/dist/src/rendering/SVGRenderer.js +25 -0
- package/dist/src/tools/BaseTool.d.ts +4 -4
- package/dist/src/tools/SelectionTool.js +0 -1
- package/dist/src/tools/ToolController.d.ts +2 -1
- package/dist/src/tools/UndoRedoShortcut.d.ts +10 -0
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/types.d.ts +1 -0
- package/package.json +2 -2
- package/src/Editor.ts +12 -1
- package/src/EditorImage.test.ts +1 -4
- package/src/SVGLoader.ts +18 -1
- package/src/UndoRedoHistory.ts +8 -0
- package/src/components/SVGGlobalAttributesObject.ts +39 -0
- package/src/components/builders/FreehandLineBuilder.ts +23 -4
- package/src/geometry/Path.fromString.test.ts +11 -24
- package/src/geometry/Path.ts +13 -0
- package/src/rendering/SVGRenderer.ts +27 -0
- package/src/testing/createEditor.ts +4 -0
- package/src/tools/BaseTool.ts +5 -4
- package/src/tools/ToolController.ts +3 -0
- package/src/tools/UndoRedoShortcut.test.ts +53 -0
- package/src/tools/UndoRedoShortcut.ts +28 -0
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +1 -0
package/dist/src/Editor.js
CHANGED
@@ -23,7 +23,6 @@ import SVGLoader from './SVGLoader';
|
|
23
23
|
import Pointer from './Pointer';
|
24
24
|
import Mat33 from './geometry/Mat33';
|
25
25
|
import { defaultEditorLocalization } from './localization';
|
26
|
-
;
|
27
26
|
export class Editor {
|
28
27
|
constructor(parent, settings = {}) {
|
29
28
|
var _a, _b;
|
@@ -132,6 +131,14 @@ export class Editor {
|
|
132
131
|
var _a, _b;
|
133
132
|
const pointer = Pointer.ofEvent(evt, (_b = (_a = pointers[evt.pointerId]) === null || _a === void 0 ? void 0 : _a.down) !== null && _b !== void 0 ? _b : false, this.viewport);
|
134
133
|
if (pointer.down) {
|
134
|
+
const prevData = pointers[pointer.id];
|
135
|
+
if (prevData) {
|
136
|
+
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
137
|
+
// If the pointer moved less than two pixels, don't send a new event.
|
138
|
+
if (distanceMoved < 2) {
|
139
|
+
return;
|
140
|
+
}
|
141
|
+
}
|
135
142
|
pointers[pointer.id] = pointer;
|
136
143
|
if (this.toolController.dispatchInputEvent({
|
137
144
|
kind: InputEvtType.PointerMoveEvt,
|
@@ -324,6 +331,7 @@ export class Editor {
|
|
324
331
|
result.setAttribute('viewBox', `${rect.x} ${rect.y} ${rect.w} ${rect.h}`);
|
325
332
|
result.setAttribute('width', `${rect.w}`);
|
326
333
|
result.setAttribute('height', `${rect.h}`);
|
334
|
+
console.log('res', result);
|
327
335
|
// Ensure the image can be identified as an SVG if downloaded.
|
328
336
|
// See https://jwatt.org/svg/authoring/
|
329
337
|
result.setAttribute('version', '1.1');
|
@@ -15,8 +15,8 @@ export default class EditorImage {
|
|
15
15
|
getElementsIntersectingRegion(region: Rect2): AbstractComponent[];
|
16
16
|
static AddElementCommand: {
|
17
17
|
new (element: AbstractComponent, applyByFlattening?: boolean): {
|
18
|
-
readonly "__#
|
19
|
-
"__#
|
18
|
+
readonly "__#2@#element": AbstractComponent;
|
19
|
+
"__#2@#applyByFlattening": boolean;
|
20
20
|
apply(editor: Editor): void;
|
21
21
|
unapply(editor: Editor): void;
|
22
22
|
description(localization: EditorLocalization): string;
|
package/dist/src/SVGLoader.d.ts
CHANGED
@@ -15,7 +15,9 @@ export default class SVGLoader implements ImageLoader {
|
|
15
15
|
private addPath;
|
16
16
|
private addUnknownNode;
|
17
17
|
private updateViewBox;
|
18
|
+
private updateSVGAttrs;
|
18
19
|
private visit;
|
20
|
+
private getSourceAttrs;
|
19
21
|
start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener): Promise<Rect2>;
|
20
22
|
static fromString(text: string): SVGLoader;
|
21
23
|
}
|
package/dist/src/SVGLoader.js
CHANGED
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
9
9
|
};
|
10
10
|
import Color4 from './Color4';
|
11
11
|
import Stroke from './components/Stroke';
|
12
|
+
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
12
13
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
13
14
|
import Path from './geometry/Path';
|
14
15
|
import Rect2 from './geometry/Rect2';
|
@@ -65,7 +66,9 @@ export default class SVGLoader {
|
|
65
66
|
const parts = pathData.split('M');
|
66
67
|
let isFirst = true;
|
67
68
|
for (const part of parts) {
|
68
|
-
|
69
|
+
// Skip effective no-ops -- moveTos without additional commands.
|
70
|
+
const isNoOpMoveTo = /^[0-9., \t\n]+$/.exec(part);
|
71
|
+
if (part !== '' && !isNoOpMoveTo) {
|
69
72
|
// We split the path by moveTo commands, so add the 'M' back in
|
70
73
|
// if it was present.
|
71
74
|
const current = !isFirst ? `M${part}` : part;
|
@@ -111,6 +114,10 @@ export default class SVGLoader {
|
|
111
114
|
}
|
112
115
|
this.rootViewBox = new Rect2(x, y, width, height);
|
113
116
|
}
|
117
|
+
updateSVGAttrs(node) {
|
118
|
+
var _a;
|
119
|
+
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
120
|
+
}
|
114
121
|
visit(node) {
|
115
122
|
var _a;
|
116
123
|
return __awaiter(this, void 0, void 0, function* () {
|
@@ -124,6 +131,7 @@ export default class SVGLoader {
|
|
124
131
|
break;
|
125
132
|
case 'svg':
|
126
133
|
this.updateViewBox(node);
|
134
|
+
this.updateSVGAttrs(node);
|
127
135
|
break;
|
128
136
|
default:
|
129
137
|
console.warn('Unknown SVG element,', node);
|
@@ -140,6 +148,12 @@ export default class SVGLoader {
|
|
140
148
|
yield ((_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, this.processedCount, this.totalToProcess));
|
141
149
|
});
|
142
150
|
}
|
151
|
+
// Get SVG element attributes (e.g. xlink=...)
|
152
|
+
getSourceAttrs(node) {
|
153
|
+
return node.getAttributeNames().map(attr => {
|
154
|
+
return [attr, node.getAttribute(attr)];
|
155
|
+
});
|
156
|
+
}
|
143
157
|
start(onAddComponent, onProgress) {
|
144
158
|
var _a;
|
145
159
|
return __awaiter(this, void 0, void 0, function* () {
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -10,7 +10,7 @@ export declare class Viewport {
|
|
10
10
|
private notifier;
|
11
11
|
static ViewportTransform: {
|
12
12
|
new (transform: Mat33): {
|
13
|
-
readonly "__#
|
13
|
+
readonly "__#1@#inverseTransform": Mat33;
|
14
14
|
readonly transform: Mat33;
|
15
15
|
apply(editor: Editor): void;
|
16
16
|
unapply(editor: Editor): void;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Mat33 from '../geometry/Mat33';
|
3
|
+
import Rect2 from '../geometry/Rect2';
|
4
|
+
import AbstractRenderer from '../rendering/AbstractRenderer';
|
5
|
+
import AbstractComponent from './AbstractComponent';
|
6
|
+
import { ImageComponentLocalization } from './localization';
|
7
|
+
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
8
|
+
private readonly attrs;
|
9
|
+
protected contentBBox: Rect2;
|
10
|
+
constructor(attrs: Array<[string, string | null]>);
|
11
|
+
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
12
|
+
intersects(_lineSegment: LineSegment2): boolean;
|
13
|
+
protected applyTransformation(_affineTransfm: Mat33): void;
|
14
|
+
description(localization: ImageComponentLocalization): string;
|
15
|
+
}
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import Rect2 from '../geometry/Rect2';
|
2
|
+
import SVGRenderer from '../rendering/SVGRenderer';
|
3
|
+
import AbstractComponent from './AbstractComponent';
|
4
|
+
// Stores global SVG attributes (e.g. namespace identifiers.)
|
5
|
+
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
6
|
+
constructor(attrs) {
|
7
|
+
super();
|
8
|
+
this.attrs = attrs;
|
9
|
+
this.contentBBox = Rect2.empty;
|
10
|
+
}
|
11
|
+
render(canvas, _visibleRect) {
|
12
|
+
if (!(canvas instanceof SVGRenderer)) {
|
13
|
+
// Don't draw unrenderable objects if we can't
|
14
|
+
return;
|
15
|
+
}
|
16
|
+
console.log('Rendering to SVG.', this.attrs);
|
17
|
+
for (const [attr, value] of this.attrs) {
|
18
|
+
canvas.setRootSVGAttribute(attr, value);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
intersects(_lineSegment) {
|
22
|
+
return false;
|
23
|
+
}
|
24
|
+
applyTransformation(_affineTransfm) {
|
25
|
+
}
|
26
|
+
description(localization) {
|
27
|
+
return localization.svgObject;
|
28
|
+
}
|
29
|
+
}
|
@@ -148,9 +148,21 @@ export default class FreehandLineBuilder {
|
|
148
148
|
projectionT = 0.9;
|
149
149
|
}
|
150
150
|
}
|
151
|
-
const
|
152
|
-
|
153
|
-
|
151
|
+
const halfVecT = projectionT;
|
152
|
+
let halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
|
153
|
+
.normalized().times(this.curveStartWidth / 2 * halfVecT
|
154
|
+
+ this.curveEndWidth / 2 * (1 - halfVecT));
|
155
|
+
// Computes a boundary curve. [direction] should be either +1 or -1 (determines the side
|
156
|
+
// of the center curve to place the boundary).
|
157
|
+
const computeBoundaryCurve = (direction, halfVec) => {
|
158
|
+
return new Bezier(startPt.plus(startVec.times(direction)), controlPoint.plus(halfVec.times(direction)), endPt.plus(endVec.times(direction)));
|
159
|
+
};
|
160
|
+
const upperBoundary = computeBoundaryCurve(1, halfVec);
|
161
|
+
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
162
|
+
// If the boundaries have two intersections, increasing the half vector's length could fix this.
|
163
|
+
if (upperBoundary.intersects(lowerBoundary).length === 2) {
|
164
|
+
halfVec = halfVec.times(2);
|
165
|
+
}
|
154
166
|
const pathCommands = [
|
155
167
|
{
|
156
168
|
kind: PathCommandType.QuadraticBezierTo,
|
@@ -277,14 +277,24 @@ export default class Path {
|
|
277
277
|
pathString = pathString.split('\n').join(' ');
|
278
278
|
let lastPos = Vec2.zero;
|
279
279
|
let firstPos = null;
|
280
|
+
let isFirstCommand = true;
|
280
281
|
const commands = [];
|
281
282
|
const moveTo = (point) => {
|
283
|
+
// The first moveTo/lineTo is already handled by the [startPoint] parameter of the Path constructor.
|
284
|
+
if (isFirstCommand) {
|
285
|
+
isFirstCommand = false;
|
286
|
+
return;
|
287
|
+
}
|
282
288
|
commands.push({
|
283
289
|
kind: PathCommandType.MoveTo,
|
284
290
|
point,
|
285
291
|
});
|
286
292
|
};
|
287
293
|
const lineTo = (point) => {
|
294
|
+
if (isFirstCommand) {
|
295
|
+
isFirstCommand = false;
|
296
|
+
return;
|
297
|
+
}
|
288
298
|
commands.push({
|
289
299
|
kind: PathCommandType.LineTo,
|
290
300
|
point,
|
@@ -390,6 +400,7 @@ export default class Path {
|
|
390
400
|
if (args.length > 0) {
|
391
401
|
firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = args[0]);
|
392
402
|
}
|
403
|
+
isFirstCommand = false;
|
393
404
|
}
|
394
405
|
return new Path(firstPos !== null && firstPos !== void 0 ? firstPos : Vec2.zero, commands);
|
395
406
|
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const escapeHtml: (html: string) => string;
|
@@ -10,7 +10,9 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
10
10
|
private lastPath;
|
11
11
|
private lastPathStart;
|
12
12
|
private mainGroup;
|
13
|
+
private overwrittenAttrs;
|
13
14
|
constructor(elem: SVGSVGElement, viewport: Viewport);
|
15
|
+
setRootSVGAttribute(name: string, value: string | null): void;
|
14
16
|
displaySize(): Vec2;
|
15
17
|
clear(): void;
|
16
18
|
protected beginPath(startPoint: Point2): void;
|
@@ -6,13 +6,38 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
6
6
|
constructor(elem, viewport) {
|
7
7
|
super(viewport);
|
8
8
|
this.elem = elem;
|
9
|
+
this.overwrittenAttrs = {};
|
9
10
|
this.clear();
|
10
11
|
}
|
12
|
+
// Sets an attribute on the root SVG element.
|
13
|
+
setRootSVGAttribute(name, value) {
|
14
|
+
// Make the original value of the attribute restorable on clear
|
15
|
+
if (!(name in this.overwrittenAttrs)) {
|
16
|
+
this.overwrittenAttrs[name] = this.elem.getAttribute(name);
|
17
|
+
}
|
18
|
+
if (value !== null) {
|
19
|
+
this.elem.setAttribute(name, value);
|
20
|
+
}
|
21
|
+
else {
|
22
|
+
this.elem.removeAttribute(name);
|
23
|
+
}
|
24
|
+
}
|
11
25
|
displaySize() {
|
12
26
|
return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
|
13
27
|
}
|
14
28
|
clear() {
|
15
29
|
this.mainGroup = document.createElementNS(svgNameSpace, 'g');
|
30
|
+
// Restore all alltributes
|
31
|
+
for (const attrName in this.overwrittenAttrs) {
|
32
|
+
const value = this.overwrittenAttrs[attrName];
|
33
|
+
if (value) {
|
34
|
+
this.elem.setAttribute(attrName, value);
|
35
|
+
}
|
36
|
+
else {
|
37
|
+
this.elem.removeAttribute(attrName);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
this.overwrittenAttrs = {};
|
16
41
|
// Remove all children
|
17
42
|
this.elem.replaceChildren(this.mainGroup);
|
18
43
|
}
|
@@ -6,10 +6,10 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
6
6
|
readonly description: string;
|
7
7
|
private enabled;
|
8
8
|
private group;
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
onPointerDown(_event: PointerEvt): boolean;
|
10
|
+
onPointerMove(_event: PointerEvt): void;
|
11
|
+
onPointerUp(_event: PointerEvt): void;
|
12
|
+
onGestureCancel(): void;
|
13
13
|
abstract readonly kind: ToolType;
|
14
14
|
protected constructor(notifier: EditorNotifier, description: string);
|
15
15
|
onWheel(_event: WheelEvt): boolean;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { KeyPressEvent } from '../types';
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
import { ToolType } from './ToolController';
|
5
|
+
export default class UndoRedoShortcut extends BaseTool {
|
6
|
+
private editor;
|
7
|
+
kind: ToolType.UndoRedoShortcut;
|
8
|
+
constructor(editor: Editor);
|
9
|
+
onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
|
10
|
+
}
|
package/dist/src/types.d.ts
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.8",
|
4
4
|
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
|
5
5
|
"main": "dist/src/Editor.js",
|
6
6
|
"types": "dist/src/Editor.d.ts",
|
@@ -75,7 +75,7 @@
|
|
75
75
|
"ts-jest": "^28.0.8",
|
76
76
|
"ts-loader": "^9.3.1",
|
77
77
|
"ts-node": "^10.9.1",
|
78
|
-
"typescript": "^4.
|
78
|
+
"typescript": "^4.8.2",
|
79
79
|
"webpack": "^5.74.0"
|
80
80
|
},
|
81
81
|
"bugs": {
|
package/src/Editor.ts
CHANGED
@@ -186,8 +186,18 @@ export class Editor {
|
|
186
186
|
evt, pointers[evt.pointerId]?.down ?? false, this.viewport
|
187
187
|
);
|
188
188
|
if (pointer.down) {
|
189
|
-
pointers[pointer.id]
|
189
|
+
const prevData = pointers[pointer.id];
|
190
|
+
|
191
|
+
if (prevData) {
|
192
|
+
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
190
193
|
|
194
|
+
// If the pointer moved less than two pixels, don't send a new event.
|
195
|
+
if (distanceMoved < 2) {
|
196
|
+
return;
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
pointers[pointer.id] = pointer;
|
191
201
|
if (this.toolController.dispatchInputEvent({
|
192
202
|
kind: InputEvtType.PointerMoveEvt,
|
193
203
|
current: pointer,
|
@@ -228,6 +238,7 @@ export class Editor {
|
|
228
238
|
if (this.toolController.dispatchInputEvent({
|
229
239
|
kind: InputEvtType.KeyPressEvent,
|
230
240
|
key: evt.key,
|
241
|
+
ctrlKey: evt.ctrlKey,
|
231
242
|
})) {
|
232
243
|
evt.preventDefault();
|
233
244
|
}
|
package/src/EditorImage.test.ts
CHANGED
@@ -5,12 +5,9 @@ import Stroke from './components/Stroke';
|
|
5
5
|
import { Vec2 } from './geometry/Vec2';
|
6
6
|
import Path, { PathCommandType } from './geometry/Path';
|
7
7
|
import Color4 from './Color4';
|
8
|
-
import Editor from './Editor';
|
9
|
-
import { RenderingMode } from './Display';
|
10
8
|
import DummyRenderer from './rendering/DummyRenderer';
|
11
9
|
import { RenderingStyle } from './rendering/AbstractRenderer';
|
12
|
-
|
13
|
-
const createEditor = () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
|
10
|
+
import createEditor from './testing/createEditor';
|
14
11
|
|
15
12
|
describe('EditorImage', () => {
|
16
13
|
const testStroke = new Stroke([
|
package/src/SVGLoader.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import Color4 from './Color4';
|
2
2
|
import AbstractComponent from './components/AbstractComponent';
|
3
3
|
import Stroke from './components/Stroke';
|
4
|
+
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
4
5
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
5
6
|
import Path from './geometry/Path';
|
6
7
|
import Rect2 from './geometry/Rect2';
|
@@ -66,10 +67,14 @@ export default class SVGLoader implements ImageLoader {
|
|
66
67
|
const parts = pathData.split('M');
|
67
68
|
let isFirst = true;
|
68
69
|
for (const part of parts) {
|
69
|
-
|
70
|
+
// Skip effective no-ops -- moveTos without additional commands.
|
71
|
+
const isNoOpMoveTo = /^[0-9., \t\n]+$/.exec(part);
|
72
|
+
|
73
|
+
if (part !== '' && !isNoOpMoveTo) {
|
70
74
|
// We split the path by moveTo commands, so add the 'M' back in
|
71
75
|
// if it was present.
|
72
76
|
const current = !isFirst ? `M${part}` : part;
|
77
|
+
|
73
78
|
const path = Path.fromString(current);
|
74
79
|
const spec = path.toRenderable(style);
|
75
80
|
result.push(spec);
|
@@ -123,6 +128,10 @@ export default class SVGLoader implements ImageLoader {
|
|
123
128
|
this.rootViewBox = new Rect2(x, y, width, height);
|
124
129
|
}
|
125
130
|
|
131
|
+
private updateSVGAttrs(node: SVGSVGElement) {
|
132
|
+
this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
133
|
+
}
|
134
|
+
|
126
135
|
private async visit(node: Element) {
|
127
136
|
this.totalToProcess += node.childElementCount;
|
128
137
|
|
@@ -135,6 +144,7 @@ export default class SVGLoader implements ImageLoader {
|
|
135
144
|
break;
|
136
145
|
case 'svg':
|
137
146
|
this.updateViewBox(node as SVGSVGElement);
|
147
|
+
this.updateSVGAttrs(node as SVGSVGElement);
|
138
148
|
break;
|
139
149
|
default:
|
140
150
|
console.warn('Unknown SVG element,', node);
|
@@ -154,6 +164,13 @@ export default class SVGLoader implements ImageLoader {
|
|
154
164
|
await this.onProgress?.(this.processedCount, this.totalToProcess);
|
155
165
|
}
|
156
166
|
|
167
|
+
// Get SVG element attributes (e.g. xlink=...)
|
168
|
+
private getSourceAttrs(node: SVGSVGElement): Array<[string, string|null]> {
|
169
|
+
return node.getAttributeNames().map(attr => {
|
170
|
+
return [ attr, node.getAttribute(attr) ];
|
171
|
+
});
|
172
|
+
}
|
173
|
+
|
157
174
|
public async start(
|
158
175
|
onAddComponent: ComponentAddedListener, onProgress: OnProgressListener
|
159
176
|
): Promise<Rect2> {
|
package/src/UndoRedoHistory.ts
CHANGED
@@ -56,6 +56,14 @@ class UndoRedoHistory {
|
|
56
56
|
}
|
57
57
|
this.fireUpdateEvent();
|
58
58
|
}
|
59
|
+
|
60
|
+
public get undoStackSize(): number {
|
61
|
+
return this.undoStack.length;
|
62
|
+
}
|
63
|
+
|
64
|
+
public get redoStackSize(): number {
|
65
|
+
return this.redoStack.length;
|
66
|
+
}
|
59
67
|
}
|
60
68
|
|
61
69
|
export default UndoRedoHistory;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Mat33 from '../geometry/Mat33';
|
3
|
+
import Rect2 from '../geometry/Rect2';
|
4
|
+
import AbstractRenderer from '../rendering/AbstractRenderer';
|
5
|
+
import SVGRenderer from '../rendering/SVGRenderer';
|
6
|
+
import AbstractComponent from './AbstractComponent';
|
7
|
+
import { ImageComponentLocalization } from './localization';
|
8
|
+
|
9
|
+
// Stores global SVG attributes (e.g. namespace identifiers.)
|
10
|
+
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
11
|
+
protected contentBBox: Rect2;
|
12
|
+
public constructor(private readonly attrs: Array<[string, string|null]>) {
|
13
|
+
super();
|
14
|
+
this.contentBBox = Rect2.empty;
|
15
|
+
}
|
16
|
+
|
17
|
+
public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
|
18
|
+
if (!(canvas instanceof SVGRenderer)) {
|
19
|
+
// Don't draw unrenderable objects if we can't
|
20
|
+
return;
|
21
|
+
}
|
22
|
+
|
23
|
+
console.log('Rendering to SVG.', this.attrs);
|
24
|
+
for (const [ attr, value ] of this.attrs) {
|
25
|
+
canvas.setRootSVGAttribute(attr, value);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
public intersects(_lineSegment: LineSegment2): boolean {
|
30
|
+
return false;
|
31
|
+
}
|
32
|
+
|
33
|
+
protected applyTransformation(_affineTransfm: Mat33): void {
|
34
|
+
}
|
35
|
+
|
36
|
+
public description(localization: ImageComponentLocalization): string {
|
37
|
+
return localization.svgObject;
|
38
|
+
}
|
39
|
+
}
|
@@ -183,7 +183,6 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
183
183
|
|
184
184
|
// Approximate the normal at the location of the control point
|
185
185
|
let projectionT = this.currentCurve.project(controlPoint.xy).t;
|
186
|
-
|
187
186
|
if (!projectionT) {
|
188
187
|
if (startPt.minus(controlPoint).magnitudeSquared() < endPt.minus(controlPoint).magnitudeSquared()) {
|
189
188
|
projectionT = 0.1;
|
@@ -192,11 +191,31 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
192
191
|
}
|
193
192
|
}
|
194
193
|
|
195
|
-
const
|
194
|
+
const halfVecT = projectionT;
|
195
|
+
let halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
|
196
196
|
.normalized().times(
|
197
|
-
this.curveStartWidth / 2 *
|
198
|
-
+ this.curveEndWidth / 2 * (1 -
|
197
|
+
this.curveStartWidth / 2 * halfVecT
|
198
|
+
+ this.curveEndWidth / 2 * (1 - halfVecT)
|
199
|
+
);
|
200
|
+
|
201
|
+
// Computes a boundary curve. [direction] should be either +1 or -1 (determines the side
|
202
|
+
// of the center curve to place the boundary).
|
203
|
+
const computeBoundaryCurve = (direction: number, halfVec: Vec2) => {
|
204
|
+
return new Bezier(
|
205
|
+
startPt.plus(startVec.times(direction)),
|
206
|
+
controlPoint.plus(halfVec.times(direction)),
|
207
|
+
endPt.plus(endVec.times(direction)),
|
199
208
|
);
|
209
|
+
};
|
210
|
+
|
211
|
+
const upperBoundary = computeBoundaryCurve(1, halfVec);
|
212
|
+
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
213
|
+
|
214
|
+
// If the boundaries have two intersections, increasing the half vector's length could fix this.
|
215
|
+
if (upperBoundary.intersects(lowerBoundary).length === 2) {
|
216
|
+
halfVec = halfVec.times(2);
|
217
|
+
}
|
218
|
+
|
200
219
|
|
201
220
|
const pathCommands: PathCommand[] = [
|
202
221
|
{
|
@@ -14,17 +14,13 @@ describe('Path.fromString', () => {
|
|
14
14
|
const path2 = Path.fromString('M 0 0');
|
15
15
|
const path3 = Path.fromString('M 1,1M 2,2 M 3,3');
|
16
16
|
|
17
|
-
expect(path1.parts
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
expect(path2.
|
17
|
+
expect(path1.parts.length).toBe(0);
|
18
|
+
expect(path1.startPoint).toMatchObject(Vec2.zero);
|
19
|
+
|
20
|
+
expect(path2.parts.length).toBe(0);
|
21
|
+
expect(path2.startPoint).toMatchObject(Vec2.zero);
|
22
22
|
|
23
23
|
expect(path3.parts).toMatchObject([
|
24
|
-
{
|
25
|
-
kind: PathCommandType.MoveTo,
|
26
|
-
point: Vec2.of(1, 1),
|
27
|
-
},
|
28
24
|
{
|
29
25
|
kind: PathCommandType.MoveTo,
|
30
26
|
point: Vec2.of(2, 2),
|
@@ -44,27 +40,22 @@ describe('Path.fromString', () => {
|
|
44
40
|
kind: PathCommandType.MoveTo,
|
45
41
|
point: Vec2.of(1, 1),
|
46
42
|
},
|
47
|
-
{
|
48
|
-
kind: PathCommandType.MoveTo,
|
49
|
-
point: Vec2.of(1, 1),
|
50
|
-
},
|
51
43
|
{
|
52
44
|
kind: PathCommandType.MoveTo,
|
53
45
|
point: Vec2.of(4, 4),
|
54
46
|
},
|
55
47
|
]);
|
48
|
+
expect(path.startPoint).toMatchObject(Vec2.of(1, 1));
|
56
49
|
});
|
57
50
|
|
58
51
|
it('should handle lineTo commands', () => {
|
59
52
|
const path = Path.fromString('l1,2L-1,0l0.1,-1.0');
|
53
|
+
// l is a relative lineTo, but because there
|
54
|
+
// is no previous command, it should act like an
|
55
|
+
// absolute moveTo.
|
56
|
+
expect(path.startPoint).toMatchObject(Vec2.of(1, 2));
|
57
|
+
|
60
58
|
expect(path.parts).toMatchObject([
|
61
|
-
{
|
62
|
-
kind: PathCommandType.LineTo,
|
63
|
-
// l is a relative lineTo, but because there
|
64
|
-
// is no previous command, it should act like an
|
65
|
-
// absolute moveTo.
|
66
|
-
point: Vec2.of(1, 2),
|
67
|
-
},
|
68
59
|
{
|
69
60
|
kind: PathCommandType.LineTo,
|
70
61
|
point: Vec2.of(-1, 0),
|
@@ -84,10 +75,6 @@ describe('Path.fromString', () => {
|
|
84
75
|
expect(path2.startPoint).toMatchObject(path1.startPoint);
|
85
76
|
expect(path1.parts).toMatchObject(path2.parts);
|
86
77
|
expect(path1.parts).toMatchObject([
|
87
|
-
{
|
88
|
-
kind: PathCommandType.MoveTo,
|
89
|
-
point: Vec2.of(3, 3),
|
90
|
-
},
|
91
78
|
{
|
92
79
|
kind: PathCommandType.LineTo,
|
93
80
|
point: Vec2.of(4, 5),
|