js-draw 0.1.0 → 0.1.3
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 +12 -0
- package/README.md +2 -2
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +6 -3
- package/dist/src/EditorImage.d.ts +1 -1
- package/dist/src/EditorImage.js +6 -3
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +11 -0
- package/dist/src/SVGLoader.js +113 -4
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +12 -2
- package/dist/src/components/AbstractComponent.d.ts +6 -0
- package/dist/src/components/AbstractComponent.js +11 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/Text.d.ts +30 -0
- package/dist/src/components/Text.js +109 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
- package/dist/src/components/localization.d.ts +1 -0
- package/dist/src/components/localization.js +1 -0
- package/dist/src/geometry/Mat33.d.ts +1 -0
- package/dist/src/geometry/Mat33.js +30 -0
- package/dist/src/geometry/Path.js +105 -67
- package/dist/src/geometry/Rect2.d.ts +2 -0
- package/dist/src/geometry/Rect2.js +25 -8
- package/dist/src/rendering/Display.js +4 -3
- package/dist/src/rendering/caching/CacheRecord.js +2 -1
- package/dist/src/rendering/caching/CacheRecordManager.js +2 -10
- package/dist/src/rendering/caching/RenderingCache.js +10 -4
- package/dist/src/rendering/caching/RenderingCacheNode.js +10 -3
- package/dist/src/rendering/caching/testUtils.js +1 -1
- package/dist/src/rendering/caching/types.d.ts +1 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
- package/dist/src/testing/loadExpectExtensions.js +1 -4
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +216 -154
- package/dist/src/toolbar/icons.d.ts +12 -0
- package/dist/src/toolbar/icons.js +197 -0
- package/dist/src/toolbar/localization.d.ts +4 -1
- package/dist/src/toolbar/localization.js +4 -1
- package/dist/src/toolbar/types.d.ts +4 -0
- package/dist/src/tools/PanZoom.d.ts +9 -6
- package/dist/src/tools/PanZoom.js +30 -21
- package/dist/src/tools/Pen.js +8 -3
- package/dist/src/tools/SelectionTool.js +1 -1
- package/dist/src/tools/TextTool.d.ts +29 -0
- package/dist/src/tools/TextTool.js +154 -0
- package/dist/src/tools/ToolController.d.ts +5 -5
- package/dist/src/tools/ToolController.js +10 -9
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/package.json +1 -1
- package/src/Editor.ts +7 -3
- package/src/EditorImage.ts +7 -3
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +146 -5
- package/src/Viewport.ts +15 -3
- package/src/components/AbstractComponent.ts +16 -1
- package/src/components/SVGGlobalAttributesObject.ts +0 -1
- package/src/components/Stroke.ts +1 -1
- package/src/components/Text.ts +136 -0
- package/src/components/builders/FreehandLineBuilder.ts +1 -1
- package/src/components/localization.ts +2 -0
- package/src/geometry/Mat33.test.ts +44 -0
- package/src/geometry/Mat33.ts +41 -0
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.toString.test.ts +7 -3
- package/src/geometry/Path.ts +110 -68
- package/src/geometry/Rect2.test.ts +9 -0
- package/src/geometry/Rect2.ts +33 -8
- package/src/rendering/Display.ts +4 -3
- package/src/rendering/caching/CacheRecord.ts +2 -1
- package/src/rendering/caching/CacheRecordManager.ts +2 -12
- package/src/rendering/caching/RenderingCache.test.ts +1 -1
- package/src/rendering/caching/RenderingCache.ts +11 -4
- package/src/rendering/caching/RenderingCacheNode.ts +16 -3
- package/src/rendering/caching/testUtils.ts +1 -0
- package/src/rendering/caching/types.ts +4 -0
- package/src/rendering/renderers/AbstractRenderer.ts +18 -1
- package/src/rendering/renderers/CanvasRenderer.ts +34 -10
- package/src/rendering/renderers/DummyRenderer.ts +8 -0
- package/src/rendering/renderers/SVGRenderer.ts +57 -10
- package/src/testing/loadExpectExtensions.ts +1 -4
- package/src/toolbar/HTMLToolbar.ts +262 -170
- package/src/toolbar/icons.ts +226 -0
- package/src/toolbar/localization.ts +9 -2
- package/src/toolbar/toolbar.css +21 -8
- package/src/toolbar/types.ts +5 -0
- package/src/tools/PanZoom.ts +37 -27
- package/src/tools/Pen.ts +7 -3
- package/src/tools/SelectionTool.ts +1 -1
- package/src/tools/TextTool.ts +206 -0
- package/src/tools/ToolController.ts +7 -5
- package/src/tools/localization.ts +7 -0
package/dist/src/Editor.js
CHANGED
@@ -115,6 +115,10 @@ export class Editor {
|
|
115
115
|
// May be required to prevent text selection on iOS/Safari:
|
116
116
|
// See https://stackoverflow.com/a/70992717/17055750
|
117
117
|
this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
|
118
|
+
this.renderingRegion.addEventListener('contextmenu', evt => {
|
119
|
+
// Don't show a context menu
|
120
|
+
evt.preventDefault();
|
121
|
+
});
|
118
122
|
this.renderingRegion.addEventListener('pointerdown', evt => {
|
119
123
|
const pointer = Pointer.ofEvent(evt, true, this.viewport);
|
120
124
|
pointers[pointer.id] = pointer;
|
@@ -277,13 +281,12 @@ export class Editor {
|
|
277
281
|
this.display.startRerender();
|
278
282
|
// Draw a rectangle around the region that will be visible on save
|
279
283
|
const renderer = this.display.getDryInkRenderer();
|
284
|
+
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
|
280
285
|
if (showImageBounds) {
|
281
286
|
const exportRectFill = { fill: Color4.fromHex('#44444455') };
|
282
|
-
const exportRectStrokeWidth =
|
287
|
+
const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
|
283
288
|
renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
|
284
289
|
}
|
285
|
-
//this.image.render(renderer, this.viewport);
|
286
|
-
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
|
287
290
|
this.rerenderQueued = false;
|
288
291
|
}
|
289
292
|
drawWetInk(...path) {
|
@@ -40,7 +40,7 @@ export declare class ImageNode {
|
|
40
40
|
onContentChange(): void;
|
41
41
|
getContent(): AbstractComponent | null;
|
42
42
|
getParent(): ImageNode | null;
|
43
|
-
|
43
|
+
private getChildrenIntersectingRegion;
|
44
44
|
getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[];
|
45
45
|
getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[];
|
46
46
|
getLeaves(): ImageNode[];
|
package/dist/src/EditorImage.js
CHANGED
@@ -62,6 +62,9 @@ EditorImage.AddElementCommand = (_a = class {
|
|
62
62
|
_applyByFlattening.set(this, false);
|
63
63
|
__classPrivateFieldSet(this, _element, element, "f");
|
64
64
|
__classPrivateFieldSet(this, _applyByFlattening, applyByFlattening, "f");
|
65
|
+
if (isNaN(__classPrivateFieldGet(this, _element, "f").getBBox().area)) {
|
66
|
+
throw new Error('Elements in the image cannot have NaN bounding boxes');
|
67
|
+
}
|
65
68
|
}
|
66
69
|
apply(editor) {
|
67
70
|
editor.image.addElement(__classPrivateFieldGet(this, _element, "f"));
|
@@ -107,7 +110,7 @@ export class ImageNode {
|
|
107
110
|
getParent() {
|
108
111
|
return this.parent;
|
109
112
|
}
|
110
|
-
|
113
|
+
getChildrenIntersectingRegion(region) {
|
111
114
|
return this.children.filter(child => {
|
112
115
|
return child.getBBox().intersects(region);
|
113
116
|
});
|
@@ -116,7 +119,7 @@ export class ImageNode {
|
|
116
119
|
if (this.content) {
|
117
120
|
return [this];
|
118
121
|
}
|
119
|
-
return this.
|
122
|
+
return this.getChildrenIntersectingRegion(region);
|
120
123
|
}
|
121
124
|
// Returns a list of `ImageNode`s with content (and thus no children).
|
122
125
|
getLeavesIntersectingRegion(region, isTooSmall) {
|
@@ -128,7 +131,7 @@ export class ImageNode {
|
|
128
131
|
if (this.content !== null && this.getBBox().intersects(region)) {
|
129
132
|
result.push(this);
|
130
133
|
}
|
131
|
-
const children = this.
|
134
|
+
const children = this.getChildrenIntersectingRegion(region);
|
132
135
|
for (const child of children) {
|
133
136
|
result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
|
134
137
|
}
|
package/dist/src/Pointer.d.ts
CHANGED
package/dist/src/Pointer.js
CHANGED
@@ -4,8 +4,9 @@ export var PointerDevice;
|
|
4
4
|
PointerDevice[PointerDevice["Pen"] = 0] = "Pen";
|
5
5
|
PointerDevice[PointerDevice["Eraser"] = 1] = "Eraser";
|
6
6
|
PointerDevice[PointerDevice["Touch"] = 2] = "Touch";
|
7
|
-
PointerDevice[PointerDevice["
|
8
|
-
PointerDevice[PointerDevice["
|
7
|
+
PointerDevice[PointerDevice["PrimaryButtonMouse"] = 3] = "PrimaryButtonMouse";
|
8
|
+
PointerDevice[PointerDevice["RightButtonMouse"] = 4] = "RightButtonMouse";
|
9
|
+
PointerDevice[PointerDevice["Other"] = 5] = "Other";
|
9
10
|
})(PointerDevice || (PointerDevice = {}));
|
10
11
|
// Provides a snapshot containing information about a pointer. A Pointer
|
11
12
|
// object is immutable --- it will not be updated when the pointer's information changes.
|
@@ -34,7 +35,7 @@ export default class Pointer {
|
|
34
35
|
var _a, _b;
|
35
36
|
const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
|
36
37
|
const pointerTypeToDevice = {
|
37
|
-
'mouse': PointerDevice.
|
38
|
+
'mouse': PointerDevice.PrimaryButtonMouse,
|
38
39
|
'pen': PointerDevice.Pen,
|
39
40
|
'touch': PointerDevice.Touch,
|
40
41
|
};
|
@@ -45,6 +46,14 @@ export default class Pointer {
|
|
45
46
|
}
|
46
47
|
const timeStamp = (new Date()).getTime();
|
47
48
|
const canvasPos = viewport.roundPoint(viewport.screenToCanvas(screenPos));
|
49
|
+
if (device === PointerDevice.PrimaryButtonMouse) {
|
50
|
+
if (evt.buttons & 0x2) {
|
51
|
+
device = PointerDevice.RightButtonMouse;
|
52
|
+
}
|
53
|
+
else if (!(evt.buttons & 0x1)) {
|
54
|
+
device = PointerDevice.Other;
|
55
|
+
}
|
56
|
+
}
|
48
57
|
return new Pointer(screenPos, canvasPos, (_b = evt.pressure) !== null && _b !== void 0 ? _b : null, evt.isPrimary, isDown, device, evt.pointerId, timeStamp);
|
49
58
|
}
|
50
59
|
// Create a new Pointer from a point on the canvas.
|
package/dist/src/SVGLoader.d.ts
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
import Rect2 from './geometry/Rect2';
|
2
2
|
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
|
3
3
|
export declare const defaultSVGViewRect: Rect2;
|
4
|
+
export declare const svgAttributesDataKey = "svgAttrs";
|
5
|
+
export declare const svgStyleAttributesDataKey = "svgStyleAttrs";
|
6
|
+
export declare type SVGLoaderUnknownAttribute = [string, string];
|
7
|
+
export declare type SVGLoaderUnknownStyleAttribute = {
|
8
|
+
key: string;
|
9
|
+
value: string;
|
10
|
+
priority?: string;
|
11
|
+
};
|
4
12
|
export default class SVGLoader implements ImageLoader {
|
5
13
|
private source;
|
6
14
|
private onFinish?;
|
@@ -13,7 +21,10 @@ export default class SVGLoader implements ImageLoader {
|
|
13
21
|
private constructor();
|
14
22
|
private getStyle;
|
15
23
|
private strokeDataFromElem;
|
24
|
+
private attachUnrecognisedAttrs;
|
16
25
|
private addPath;
|
26
|
+
private makeText;
|
27
|
+
private addText;
|
17
28
|
private addUnknownNode;
|
18
29
|
private updateViewBox;
|
19
30
|
private updateSVGAttrs;
|
package/dist/src/SVGLoader.js
CHANGED
@@ -10,11 +10,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
10
10
|
import Color4 from './Color4';
|
11
11
|
import Stroke from './components/Stroke';
|
12
12
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
13
|
+
import Text from './components/Text';
|
13
14
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
15
|
+
import Mat33 from './geometry/Mat33';
|
14
16
|
import Path from './geometry/Path';
|
15
17
|
import Rect2 from './geometry/Rect2';
|
18
|
+
import { Vec2 } from './geometry/Vec2';
|
16
19
|
// Size of a loaded image if no size is specified.
|
17
20
|
export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
|
21
|
+
// Key to retrieve unrecognised attributes from an AbstractComponent
|
22
|
+
export const svgAttributesDataKey = 'svgAttrs';
|
23
|
+
export const svgStyleAttributesDataKey = 'svgStyleAttrs';
|
18
24
|
export default class SVGLoader {
|
19
25
|
constructor(source, onFinish) {
|
20
26
|
this.source = source;
|
@@ -81,6 +87,30 @@ export default class SVGLoader {
|
|
81
87
|
}
|
82
88
|
return result;
|
83
89
|
}
|
90
|
+
attachUnrecognisedAttrs(elem, node, supportedAttrs, supportedStyleAttrs) {
|
91
|
+
for (const attr of node.getAttributeNames()) {
|
92
|
+
if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
|
93
|
+
continue;
|
94
|
+
}
|
95
|
+
elem.attachLoadSaveData(svgAttributesDataKey, [attr, node.getAttribute(attr)]);
|
96
|
+
}
|
97
|
+
if (supportedStyleAttrs) {
|
98
|
+
for (const attr of node.style) {
|
99
|
+
if (attr === '' || !attr) {
|
100
|
+
continue;
|
101
|
+
}
|
102
|
+
if (supportedStyleAttrs.has(attr)) {
|
103
|
+
continue;
|
104
|
+
}
|
105
|
+
// TODO: Do we need special logic for !important properties?
|
106
|
+
elem.attachLoadSaveData(svgStyleAttributesDataKey, {
|
107
|
+
key: attr,
|
108
|
+
value: node.style.getPropertyValue(attr),
|
109
|
+
priority: node.style.getPropertyPriority(attr)
|
110
|
+
});
|
111
|
+
}
|
112
|
+
}
|
113
|
+
}
|
84
114
|
// Adds a stroke with a single path
|
85
115
|
addPath(node) {
|
86
116
|
var _a;
|
@@ -88,6 +118,8 @@ export default class SVGLoader {
|
|
88
118
|
try {
|
89
119
|
const strokeData = this.strokeDataFromElem(node);
|
90
120
|
elem = new Stroke(strokeData);
|
121
|
+
const supportedStyleAttrs = ['stroke', 'fill', 'stroke-width'];
|
122
|
+
this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStyleAttrs, 'd']), new Set(supportedStyleAttrs));
|
91
123
|
}
|
92
124
|
catch (e) {
|
93
125
|
console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
|
@@ -95,6 +127,74 @@ export default class SVGLoader {
|
|
95
127
|
}
|
96
128
|
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
|
97
129
|
}
|
130
|
+
makeText(elem) {
|
131
|
+
var _a;
|
132
|
+
const contentList = [];
|
133
|
+
for (const child of elem.childNodes) {
|
134
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
135
|
+
contentList.push((_a = child.nodeValue) !== null && _a !== void 0 ? _a : '');
|
136
|
+
}
|
137
|
+
else if (child.nodeType === Node.ELEMENT_NODE) {
|
138
|
+
const subElem = child;
|
139
|
+
if (subElem.tagName.toLowerCase() === 'tspan') {
|
140
|
+
contentList.push(this.makeText(subElem));
|
141
|
+
}
|
142
|
+
else {
|
143
|
+
throw new Error(`Unrecognized text child element: ${subElem}`);
|
144
|
+
}
|
145
|
+
}
|
146
|
+
else {
|
147
|
+
throw new Error(`Unrecognized text child node: ${child}.`);
|
148
|
+
}
|
149
|
+
}
|
150
|
+
// Compute styles.
|
151
|
+
const computedStyles = window.getComputedStyle(elem);
|
152
|
+
const fontSizeMatch = /^([-0-9.e]+)px/i.exec(computedStyles.fontSize);
|
153
|
+
const supportedStyleAttrs = [
|
154
|
+
'fontFamily',
|
155
|
+
'fill',
|
156
|
+
'transform'
|
157
|
+
];
|
158
|
+
let fontSize = 12;
|
159
|
+
if (fontSizeMatch) {
|
160
|
+
supportedStyleAttrs.push('fontSize');
|
161
|
+
fontSize = parseFloat(fontSizeMatch[1]);
|
162
|
+
}
|
163
|
+
const style = {
|
164
|
+
size: fontSize,
|
165
|
+
fontFamily: computedStyles.fontFamily || 'sans',
|
166
|
+
renderingStyle: {
|
167
|
+
fill: Color4.fromString(computedStyles.fill)
|
168
|
+
},
|
169
|
+
};
|
170
|
+
// Compute transform matrix
|
171
|
+
let transform = Mat33.fromCSSMatrix(computedStyles.transform);
|
172
|
+
const supportedAttrs = [];
|
173
|
+
const elemX = elem.getAttribute('x');
|
174
|
+
const elemY = elem.getAttribute('y');
|
175
|
+
if (elemX && elemY) {
|
176
|
+
const x = parseFloat(elemX);
|
177
|
+
const y = parseFloat(elemY);
|
178
|
+
if (!isNaN(x) && !isNaN(y)) {
|
179
|
+
supportedAttrs.push('x', 'y');
|
180
|
+
transform = transform.rightMul(Mat33.translation(Vec2.of(x, y)));
|
181
|
+
}
|
182
|
+
}
|
183
|
+
const result = new Text(contentList, transform, style);
|
184
|
+
this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
|
185
|
+
return result;
|
186
|
+
}
|
187
|
+
addText(elem) {
|
188
|
+
var _a;
|
189
|
+
try {
|
190
|
+
const textElem = this.makeText(elem);
|
191
|
+
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, textElem);
|
192
|
+
}
|
193
|
+
catch (e) {
|
194
|
+
console.error('Invalid text object in node', elem, '. Adding as an unknown object. Error:', e);
|
195
|
+
this.addUnknownNode(elem);
|
196
|
+
}
|
197
|
+
}
|
98
198
|
addUnknownNode(node) {
|
99
199
|
var _a;
|
100
200
|
const component = new UnknownSVGObject(node);
|
@@ -106,12 +206,13 @@ export default class SVGLoader {
|
|
106
206
|
if (this.rootViewBox || !viewBoxAttr) {
|
107
207
|
return;
|
108
208
|
}
|
109
|
-
const components = viewBoxAttr.split(/[ \t,]
|
209
|
+
const components = viewBoxAttr.split(/[ \t\n,]+/);
|
110
210
|
const x = parseFloat(components[0]);
|
111
211
|
const y = parseFloat(components[1]);
|
112
212
|
const width = parseFloat(components[2]);
|
113
213
|
const height = parseFloat(components[3]);
|
114
214
|
if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
|
215
|
+
console.warn(`node ${node} has an unparsable viewbox. Viewbox: ${viewBoxAttr}. Match: ${components}.`);
|
115
216
|
return;
|
116
217
|
}
|
117
218
|
this.rootViewBox = new Rect2(x, y, width, height);
|
@@ -125,6 +226,7 @@ export default class SVGLoader {
|
|
125
226
|
var _a;
|
126
227
|
return __awaiter(this, void 0, void 0, function* () {
|
127
228
|
this.totalToProcess += node.childElementCount;
|
229
|
+
let visitChildren = true;
|
128
230
|
switch (node.tagName.toLowerCase()) {
|
129
231
|
case 'g':
|
130
232
|
// Continue -- visit the node's children.
|
@@ -132,6 +234,10 @@ export default class SVGLoader {
|
|
132
234
|
case 'path':
|
133
235
|
this.addPath(node);
|
134
236
|
break;
|
237
|
+
case 'text':
|
238
|
+
this.addText(node);
|
239
|
+
visitChildren = false;
|
240
|
+
break;
|
135
241
|
case 'svg':
|
136
242
|
this.updateViewBox(node);
|
137
243
|
this.updateSVGAttrs(node);
|
@@ -144,8 +250,10 @@ export default class SVGLoader {
|
|
144
250
|
this.addUnknownNode(node);
|
145
251
|
return;
|
146
252
|
}
|
147
|
-
|
148
|
-
|
253
|
+
if (visitChildren) {
|
254
|
+
for (const child of node.children) {
|
255
|
+
yield this.visit(child);
|
256
|
+
}
|
149
257
|
}
|
150
258
|
this.processedCount++;
|
151
259
|
yield ((_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, this.processedCount, this.totalToProcess));
|
@@ -189,7 +297,6 @@ export default class SVGLoader {
|
|
189
297
|
sandbox.remove();
|
190
298
|
throw new Error('SVG loading iframe is not sandboxed.');
|
191
299
|
}
|
192
|
-
// Try running JavaScript within the iframe
|
193
300
|
const sandboxDoc = (_b = (_a = sandbox.contentWindow) === null || _a === void 0 ? void 0 : _a.document) !== null && _b !== void 0 ? _b : sandbox.contentDocument;
|
194
301
|
if (sandboxDoc == null)
|
195
302
|
throw new Error('Unable to open a sandboxed iframe!');
|
@@ -213,7 +320,9 @@ export default class SVGLoader {
|
|
213
320
|
sandboxDoc.close();
|
214
321
|
const svgElem = sandboxDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
215
322
|
svgElem.innerHTML = text;
|
323
|
+
sandboxDoc.body.appendChild(svgElem);
|
216
324
|
return new SVGLoader(svgElem, () => {
|
325
|
+
svgElem.remove();
|
217
326
|
sandbox.remove();
|
218
327
|
});
|
219
328
|
}
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -35,7 +35,7 @@ export declare class Viewport {
|
|
35
35
|
getRotationAngle(): number;
|
36
36
|
static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
|
37
37
|
roundPoint(point: Point2): Point2;
|
38
|
-
zoomTo(toMakeVisible: Rect2): Command;
|
38
|
+
zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;
|
39
39
|
}
|
40
40
|
export declare namespace Viewport {
|
41
41
|
type ViewportTransform = typeof Viewport.ViewportTransform.prototype;
|
package/dist/src/Viewport.js
CHANGED
@@ -85,8 +85,14 @@ export class Viewport {
|
|
85
85
|
// Returns a Command that transforms the view such that [rect] is visible, and perhaps
|
86
86
|
// centered in the viewport.
|
87
87
|
// Returns null if no transformation is necessary
|
88
|
-
zoomTo(toMakeVisible) {
|
88
|
+
zoomTo(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
|
89
89
|
let transform = Mat33.identity;
|
90
|
+
if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
|
91
|
+
throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
|
92
|
+
}
|
93
|
+
if (isNaN(toMakeVisible.size.magnitude())) {
|
94
|
+
throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
|
95
|
+
}
|
90
96
|
// Try to move the selection within the center 2/3rds of the viewport.
|
91
97
|
const recomputeTargetRect = () => {
|
92
98
|
// transform transforms objects on the canvas. As such, we need to invert it
|
@@ -98,7 +104,7 @@ export class Viewport {
|
|
98
104
|
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
|
99
105
|
// Ensure that toMakeVisible is at least 1/8th of the visible region.
|
100
106
|
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
|
101
|
-
if (largerThanTarget || muchSmallerThanTarget) {
|
107
|
+
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
|
102
108
|
// If larger than the target, ensure that the longest axis is visible.
|
103
109
|
// If smaller, shrink the visible rectangle as much as possible
|
104
110
|
const multiplier = (largerThanTarget ? Math.max : Math.min)(toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h);
|
@@ -115,6 +121,10 @@ export class Viewport {
|
|
115
121
|
const viewportContentTransform = visibleRectTransform.inverse();
|
116
122
|
transform = transform.rightMul(viewportContentTransform);
|
117
123
|
}
|
124
|
+
if (!transform.invertable()) {
|
125
|
+
console.warn('Unable to zoom to ', toMakeVisible, '! Computed transform', transform, 'is singular.');
|
126
|
+
transform = Mat33.identity;
|
127
|
+
}
|
118
128
|
return new Viewport.ViewportTransform(transform);
|
119
129
|
}
|
120
130
|
}
|
@@ -4,12 +4,17 @@ import Mat33 from '../geometry/Mat33';
|
|
4
4
|
import Rect2 from '../geometry/Rect2';
|
5
5
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
6
6
|
import { ImageComponentLocalization } from './localization';
|
7
|
+
declare type LoadSaveData = unknown;
|
8
|
+
export declare type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
|
7
9
|
export default abstract class AbstractComponent {
|
8
10
|
protected lastChangedTime: number;
|
9
11
|
protected abstract contentBBox: Rect2;
|
10
12
|
private zIndex;
|
11
13
|
private static zIndexCounter;
|
12
14
|
protected constructor();
|
15
|
+
private loadSaveData;
|
16
|
+
attachLoadSaveData(key: string, data: LoadSaveData): void;
|
17
|
+
getLoadSaveData(): LoadSaveDataTable;
|
13
18
|
getZIndex(): number;
|
14
19
|
getBBox(): Rect2;
|
15
20
|
abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
@@ -18,3 +23,4 @@ export default abstract class AbstractComponent {
|
|
18
23
|
transformBy(affineTransfm: Mat33): Command;
|
19
24
|
abstract description(localizationTable: ImageComponentLocalization): string;
|
20
25
|
}
|
26
|
+
export {};
|
@@ -1,9 +1,20 @@
|
|
1
1
|
import EditorImage from '../EditorImage';
|
2
2
|
export default class AbstractComponent {
|
3
3
|
constructor() {
|
4
|
+
// Get and manage data attached by a loader.
|
5
|
+
this.loadSaveData = {};
|
4
6
|
this.lastChangedTime = (new Date()).getTime();
|
5
7
|
this.zIndex = AbstractComponent.zIndexCounter++;
|
6
8
|
}
|
9
|
+
attachLoadSaveData(key, data) {
|
10
|
+
if (!this.loadSaveData[key]) {
|
11
|
+
this.loadSaveData[key] = [];
|
12
|
+
}
|
13
|
+
this.loadSaveData[key].push(data);
|
14
|
+
}
|
15
|
+
getLoadSaveData() {
|
16
|
+
return this.loadSaveData;
|
17
|
+
}
|
7
18
|
getZIndex() {
|
8
19
|
return this.zIndex;
|
9
20
|
}
|
@@ -13,7 +13,6 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
|
|
13
13
|
// Don't draw unrenderable objects if we can't
|
14
14
|
return;
|
15
15
|
}
|
16
|
-
console.log('Rendering to SVG.', this.attrs);
|
17
16
|
for (const [attr, value] of this.attrs) {
|
18
17
|
canvas.setRootSVGAttribute(attr, value);
|
19
18
|
}
|
@@ -41,7 +41,7 @@ export default class Stroke extends AbstractComponent {
|
|
41
41
|
canvas.drawPath(part);
|
42
42
|
}
|
43
43
|
}
|
44
|
-
canvas.endObject();
|
44
|
+
canvas.endObject(this.getLoadSaveData());
|
45
45
|
}
|
46
46
|
// Grows the bounding box for a given stroke part based on that part's style.
|
47
47
|
bboxForPart(origBBox, style) {
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Mat33 from '../geometry/Mat33';
|
3
|
+
import Rect2 from '../geometry/Rect2';
|
4
|
+
import AbstractRenderer, { RenderingStyle } from '../rendering/renderers/AbstractRenderer';
|
5
|
+
import AbstractComponent from './AbstractComponent';
|
6
|
+
import { ImageComponentLocalization } from './localization';
|
7
|
+
export interface TextStyle {
|
8
|
+
size: number;
|
9
|
+
fontFamily: string;
|
10
|
+
fontWeight?: string;
|
11
|
+
fontVariant?: string;
|
12
|
+
renderingStyle: RenderingStyle;
|
13
|
+
}
|
14
|
+
export default class Text extends AbstractComponent {
|
15
|
+
protected textObjects: Array<string | Text>;
|
16
|
+
private transform;
|
17
|
+
private style;
|
18
|
+
protected contentBBox: Rect2;
|
19
|
+
constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle);
|
20
|
+
static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
|
21
|
+
private static textMeasuringCtx;
|
22
|
+
private static getTextDimens;
|
23
|
+
private computeBBoxOfPart;
|
24
|
+
private recomputeBBox;
|
25
|
+
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
26
|
+
intersects(lineSegment: LineSegment2): boolean;
|
27
|
+
protected applyTransformation(affineTransfm: Mat33): void;
|
28
|
+
private getText;
|
29
|
+
description(localizationTable: ImageComponentLocalization): string;
|
30
|
+
}
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Rect2 from '../geometry/Rect2';
|
3
|
+
import AbstractComponent from './AbstractComponent';
|
4
|
+
export default class Text extends AbstractComponent {
|
5
|
+
constructor(textObjects, transform, style) {
|
6
|
+
super();
|
7
|
+
this.textObjects = textObjects;
|
8
|
+
this.transform = transform;
|
9
|
+
this.style = style;
|
10
|
+
this.recomputeBBox();
|
11
|
+
}
|
12
|
+
static applyTextStyles(ctx, style) {
|
13
|
+
var _a, _b;
|
14
|
+
ctx.font = [
|
15
|
+
((_a = style.size) !== null && _a !== void 0 ? _a : 12) + 'px',
|
16
|
+
(_b = style.fontWeight) !== null && _b !== void 0 ? _b : '',
|
17
|
+
`"${style.fontFamily.replace(/["]/g, '\\"')}"`,
|
18
|
+
style.fontWeight
|
19
|
+
].join(' ');
|
20
|
+
ctx.textAlign = 'left';
|
21
|
+
}
|
22
|
+
static getTextDimens(text, style) {
|
23
|
+
var _a;
|
24
|
+
(_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = document.createElement('canvas').getContext('2d'));
|
25
|
+
const ctx = Text.textMeasuringCtx;
|
26
|
+
Text.applyTextStyles(ctx, style);
|
27
|
+
const measure = ctx.measureText(text);
|
28
|
+
// Text is drawn with (0,0) at the bottom left of the baseline.
|
29
|
+
const textY = -measure.actualBoundingBoxAscent;
|
30
|
+
const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
|
31
|
+
return new Rect2(0, textY, measure.width, textHeight);
|
32
|
+
}
|
33
|
+
computeBBoxOfPart(part) {
|
34
|
+
if (typeof part === 'string') {
|
35
|
+
const textBBox = Text.getTextDimens(part, this.style);
|
36
|
+
return textBBox.transformedBoundingBox(this.transform);
|
37
|
+
}
|
38
|
+
else {
|
39
|
+
const bbox = part.contentBBox.transformedBoundingBox(this.transform);
|
40
|
+
return bbox;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
recomputeBBox() {
|
44
|
+
let bbox = null;
|
45
|
+
for (const textObject of this.textObjects) {
|
46
|
+
const currentBBox = this.computeBBoxOfPart(textObject);
|
47
|
+
bbox !== null && bbox !== void 0 ? bbox : (bbox = currentBBox);
|
48
|
+
bbox = bbox.union(currentBBox);
|
49
|
+
}
|
50
|
+
this.contentBBox = bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
|
51
|
+
}
|
52
|
+
render(canvas, _visibleRect) {
|
53
|
+
const cursor = this.transform;
|
54
|
+
canvas.startObject(this.contentBBox);
|
55
|
+
for (const textObject of this.textObjects) {
|
56
|
+
if (typeof textObject === 'string') {
|
57
|
+
canvas.drawText(textObject, cursor, this.style);
|
58
|
+
}
|
59
|
+
else {
|
60
|
+
canvas.pushTransform(cursor);
|
61
|
+
textObject.render(canvas);
|
62
|
+
canvas.popTransform();
|
63
|
+
}
|
64
|
+
}
|
65
|
+
canvas.endObject(this.getLoadSaveData());
|
66
|
+
}
|
67
|
+
intersects(lineSegment) {
|
68
|
+
// Convert canvas space to internal space.
|
69
|
+
const invTransform = this.transform.inverse();
|
70
|
+
const p1InThisSpace = invTransform.transformVec2(lineSegment.p1);
|
71
|
+
const p2InThisSpace = invTransform.transformVec2(lineSegment.p2);
|
72
|
+
lineSegment = new LineSegment2(p1InThisSpace, p2InThisSpace);
|
73
|
+
for (const subObject of this.textObjects) {
|
74
|
+
if (typeof subObject === 'string') {
|
75
|
+
const textBBox = Text.getTextDimens(subObject, this.style);
|
76
|
+
// TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and
|
77
|
+
// use pixel-testing to check for intersection with its contour.
|
78
|
+
if (textBBox.getEdges().some(edge => lineSegment.intersection(edge) !== null)) {
|
79
|
+
return true;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
else {
|
83
|
+
if (subObject.intersects(lineSegment)) {
|
84
|
+
return true;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
88
|
+
return false;
|
89
|
+
}
|
90
|
+
applyTransformation(affineTransfm) {
|
91
|
+
this.transform = affineTransfm.rightMul(this.transform);
|
92
|
+
this.recomputeBBox();
|
93
|
+
}
|
94
|
+
getText() {
|
95
|
+
const result = [];
|
96
|
+
for (const textObject of this.textObjects) {
|
97
|
+
if (typeof textObject === 'string') {
|
98
|
+
result.push(textObject);
|
99
|
+
}
|
100
|
+
else {
|
101
|
+
result.push(textObject.getText());
|
102
|
+
}
|
103
|
+
}
|
104
|
+
return result.join(' ');
|
105
|
+
}
|
106
|
+
description(localizationTable) {
|
107
|
+
return localizationTable.text(this.getText());
|
108
|
+
}
|
109
|
+
}
|
@@ -160,7 +160,7 @@ export default class FreehandLineBuilder {
|
|
160
160
|
const upperBoundary = computeBoundaryCurve(1, halfVec);
|
161
161
|
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
162
162
|
// If the boundaries have two intersections, increasing the half vector's length could fix this.
|
163
|
-
if (upperBoundary.intersects(lowerBoundary).length
|
163
|
+
if (upperBoundary.intersects(lowerBoundary).length > 0) {
|
164
164
|
halfVec = halfVec.times(2);
|
165
165
|
}
|
166
166
|
const pathCommands = [
|
@@ -186,5 +186,35 @@ export default class Mat33 {
|
|
186
186
|
// Translate such that [center] goes to (0, 0)
|
187
187
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
188
188
|
}
|
189
|
+
// Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
|
190
|
+
static fromCSSMatrix(cssString) {
|
191
|
+
if (cssString === '' || cssString === 'none') {
|
192
|
+
return Mat33.identity;
|
193
|
+
}
|
194
|
+
const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
|
195
|
+
const numberSepExp = '[, \\t\\n]';
|
196
|
+
const regExpSource = `^\\s*matrix\\s*\\(${[
|
197
|
+
// According to MDN, matrix(a,b,c,d,e,f) has form:
|
198
|
+
// ⎡ a c e ⎤
|
199
|
+
// ⎢ b d f ⎥
|
200
|
+
// ⎣ 0 0 1 ⎦
|
201
|
+
numberExp, numberExp, numberExp,
|
202
|
+
numberExp, numberExp, numberExp, // b, d, f
|
203
|
+
].join(`${numberSepExp}+`)}${numberSepExp}*\\)\\s*$`;
|
204
|
+
const matrixExp = new RegExp(regExpSource, 'i');
|
205
|
+
const match = matrixExp.exec(cssString);
|
206
|
+
if (!match) {
|
207
|
+
throw new Error(`Unsupported transformation: ${cssString}`);
|
208
|
+
}
|
209
|
+
const matrixData = match.slice(1).map(entry => parseFloat(entry));
|
210
|
+
const a = matrixData[0];
|
211
|
+
const b = matrixData[1];
|
212
|
+
const c = matrixData[2];
|
213
|
+
const d = matrixData[3];
|
214
|
+
const e = matrixData[4];
|
215
|
+
const f = matrixData[5];
|
216
|
+
const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
|
217
|
+
return transform;
|
218
|
+
}
|
189
219
|
}
|
190
220
|
Mat33.identity = new Mat33(1, 0, 0, 0, 1, 0, 0, 0, 1);
|