js-draw 0.1.2 → 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 +5 -0
- package/README.md +2 -2
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +2 -3
- package/dist/src/SVGLoader.d.ts +8 -0
- package/dist/src/SVGLoader.js +105 -6
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +2 -2
- package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
- package/dist/src/components/Text.d.ts +30 -0
- package/dist/src/components/Text.js +109 -0
- 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 +8 -1
- package/dist/src/geometry/Rect2.d.ts +2 -0
- package/dist/src/geometry/Rect2.js +6 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +5 -0
- package/dist/src/rendering/renderers/AbstractRenderer.js +12 -0
- 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 +3 -0
- package/dist/src/rendering/renderers/SVGRenderer.js +30 -1
- package/dist/src/testing/loadExpectExtensions.js +1 -4
- package/dist/src/toolbar/HTMLToolbar.js +52 -1
- package/dist/src/toolbar/icons.d.ts +2 -0
- package/dist/src/toolbar/icons.js +17 -0
- 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 +2 -1
- package/dist/src/tools/ToolController.js +4 -1
- package/dist/src/tools/localization.d.ts +3 -1
- package/dist/src/tools/localization.js +3 -1
- package/package.json +1 -1
- package/src/Editor.ts +3 -3
- package/src/SVGLoader.ts +124 -6
- package/src/Viewport.ts +2 -2
- package/src/components/SVGGlobalAttributesObject.ts +0 -1
- package/src/components/Text.ts +136 -0
- 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.toString.test.ts +7 -3
- package/src/geometry/Path.ts +11 -1
- package/src/geometry/Rect2.ts +8 -0
- package/src/rendering/renderers/AbstractRenderer.ts +16 -0
- package/src/rendering/renderers/CanvasRenderer.ts +34 -10
- package/src/rendering/renderers/DummyRenderer.ts +8 -0
- package/src/rendering/renderers/SVGRenderer.ts +36 -1
- package/src/testing/loadExpectExtensions.ts +1 -4
- package/src/toolbar/HTMLToolbar.ts +64 -1
- package/src/toolbar/icons.ts +23 -0
- package/src/tools/SelectionTool.ts +1 -1
- package/src/tools/TextTool.ts +206 -0
- package/src/tools/ToolController.ts +4 -0
- package/src/tools/localization.ts +7 -2
package/dist/src/Editor.js
CHANGED
@@ -281,13 +281,12 @@ export class Editor {
|
|
281
281
|
this.display.startRerender();
|
282
282
|
// Draw a rectangle around the region that will be visible on save
|
283
283
|
const renderer = this.display.getDryInkRenderer();
|
284
|
+
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
|
284
285
|
if (showImageBounds) {
|
285
286
|
const exportRectFill = { fill: Color4.fromHex('#44444455') };
|
286
|
-
const exportRectStrokeWidth =
|
287
|
+
const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
|
287
288
|
renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
|
288
289
|
}
|
289
|
-
//this.image.render(renderer, this.viewport);
|
290
|
-
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
|
291
290
|
this.rerenderQueued = false;
|
292
291
|
}
|
293
292
|
drawWetInk(...path) {
|
package/dist/src/SVGLoader.d.ts
CHANGED
@@ -2,7 +2,13 @@ import Rect2 from './geometry/Rect2';
|
|
2
2
|
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
|
3
3
|
export declare const defaultSVGViewRect: Rect2;
|
4
4
|
export declare const svgAttributesDataKey = "svgAttrs";
|
5
|
+
export declare const svgStyleAttributesDataKey = "svgStyleAttrs";
|
5
6
|
export declare type SVGLoaderUnknownAttribute = [string, string];
|
7
|
+
export declare type SVGLoaderUnknownStyleAttribute = {
|
8
|
+
key: string;
|
9
|
+
value: string;
|
10
|
+
priority?: string;
|
11
|
+
};
|
6
12
|
export default class SVGLoader implements ImageLoader {
|
7
13
|
private source;
|
8
14
|
private onFinish?;
|
@@ -17,6 +23,8 @@ export default class SVGLoader implements ImageLoader {
|
|
17
23
|
private strokeDataFromElem;
|
18
24
|
private attachUnrecognisedAttrs;
|
19
25
|
private addPath;
|
26
|
+
private makeText;
|
27
|
+
private addText;
|
20
28
|
private addUnknownNode;
|
21
29
|
private updateViewBox;
|
22
30
|
private updateSVGAttrs;
|
package/dist/src/SVGLoader.js
CHANGED
@@ -10,13 +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);
|
18
21
|
// Key to retrieve unrecognised attributes from an AbstractComponent
|
19
22
|
export const svgAttributesDataKey = 'svgAttrs';
|
23
|
+
export const svgStyleAttributesDataKey = 'svgStyleAttrs';
|
20
24
|
export default class SVGLoader {
|
21
25
|
constructor(source, onFinish) {
|
22
26
|
this.source = source;
|
@@ -83,13 +87,29 @@ export default class SVGLoader {
|
|
83
87
|
}
|
84
88
|
return result;
|
85
89
|
}
|
86
|
-
attachUnrecognisedAttrs(elem, node, supportedAttrs) {
|
90
|
+
attachUnrecognisedAttrs(elem, node, supportedAttrs, supportedStyleAttrs) {
|
87
91
|
for (const attr of node.getAttributeNames()) {
|
88
|
-
if (supportedAttrs.has(attr)) {
|
92
|
+
if (supportedAttrs.has(attr) || (attr === 'style' && supportedStyleAttrs)) {
|
89
93
|
continue;
|
90
94
|
}
|
91
95
|
elem.attachLoadSaveData(svgAttributesDataKey, [attr, node.getAttribute(attr)]);
|
92
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
|
+
}
|
93
113
|
}
|
94
114
|
// Adds a stroke with a single path
|
95
115
|
addPath(node) {
|
@@ -98,7 +118,8 @@ export default class SVGLoader {
|
|
98
118
|
try {
|
99
119
|
const strokeData = this.strokeDataFromElem(node);
|
100
120
|
elem = new Stroke(strokeData);
|
101
|
-
|
121
|
+
const supportedStyleAttrs = ['stroke', 'fill', 'stroke-width'];
|
122
|
+
this.attachUnrecognisedAttrs(elem, node, new Set([...supportedStyleAttrs, 'd']), new Set(supportedStyleAttrs));
|
102
123
|
}
|
103
124
|
catch (e) {
|
104
125
|
console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
|
@@ -106,6 +127,74 @@ export default class SVGLoader {
|
|
106
127
|
}
|
107
128
|
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
|
108
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
|
+
}
|
109
198
|
addUnknownNode(node) {
|
110
199
|
var _a;
|
111
200
|
const component = new UnknownSVGObject(node);
|
@@ -117,12 +206,13 @@ export default class SVGLoader {
|
|
117
206
|
if (this.rootViewBox || !viewBoxAttr) {
|
118
207
|
return;
|
119
208
|
}
|
120
|
-
const components = viewBoxAttr.split(/[ \t,]
|
209
|
+
const components = viewBoxAttr.split(/[ \t\n,]+/);
|
121
210
|
const x = parseFloat(components[0]);
|
122
211
|
const y = parseFloat(components[1]);
|
123
212
|
const width = parseFloat(components[2]);
|
124
213
|
const height = parseFloat(components[3]);
|
125
214
|
if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
|
215
|
+
console.warn(`node ${node} has an unparsable viewbox. Viewbox: ${viewBoxAttr}. Match: ${components}.`);
|
126
216
|
return;
|
127
217
|
}
|
128
218
|
this.rootViewBox = new Rect2(x, y, width, height);
|
@@ -136,6 +226,7 @@ export default class SVGLoader {
|
|
136
226
|
var _a;
|
137
227
|
return __awaiter(this, void 0, void 0, function* () {
|
138
228
|
this.totalToProcess += node.childElementCount;
|
229
|
+
let visitChildren = true;
|
139
230
|
switch (node.tagName.toLowerCase()) {
|
140
231
|
case 'g':
|
141
232
|
// Continue -- visit the node's children.
|
@@ -143,6 +234,10 @@ export default class SVGLoader {
|
|
143
234
|
case 'path':
|
144
235
|
this.addPath(node);
|
145
236
|
break;
|
237
|
+
case 'text':
|
238
|
+
this.addText(node);
|
239
|
+
visitChildren = false;
|
240
|
+
break;
|
146
241
|
case 'svg':
|
147
242
|
this.updateViewBox(node);
|
148
243
|
this.updateSVGAttrs(node);
|
@@ -155,8 +250,10 @@ export default class SVGLoader {
|
|
155
250
|
this.addUnknownNode(node);
|
156
251
|
return;
|
157
252
|
}
|
158
|
-
|
159
|
-
|
253
|
+
if (visitChildren) {
|
254
|
+
for (const child of node.children) {
|
255
|
+
yield this.visit(child);
|
256
|
+
}
|
160
257
|
}
|
161
258
|
this.processedCount++;
|
162
259
|
yield ((_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, this.processedCount, this.totalToProcess));
|
@@ -223,7 +320,9 @@ export default class SVGLoader {
|
|
223
320
|
sandboxDoc.close();
|
224
321
|
const svgElem = sandboxDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
225
322
|
svgElem.innerHTML = text;
|
323
|
+
sandboxDoc.body.appendChild(svgElem);
|
226
324
|
return new SVGLoader(svgElem, () => {
|
325
|
+
svgElem.remove();
|
227
326
|
sandbox.remove();
|
228
327
|
});
|
229
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,7 +85,7 @@ 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
90
|
if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
|
91
91
|
throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
|
@@ -104,7 +104,7 @@ export class Viewport {
|
|
104
104
|
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
|
105
105
|
// Ensure that toMakeVisible is at least 1/8th of the visible region.
|
106
106
|
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
|
107
|
-
if (largerThanTarget || muchSmallerThanTarget) {
|
107
|
+
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
|
108
108
|
// If larger than the target, ensure that the longest axis is visible.
|
109
109
|
// If smaller, shrink the visible rectangle as much as possible
|
110
110
|
const multiplier = (largerThanTarget ? Math.max : Math.min)(toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h);
|
@@ -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
|
}
|
@@ -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
|
+
}
|
@@ -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);
|
@@ -220,6 +220,7 @@ export default class Path {
|
|
220
220
|
const lastDigit = parseInt(text.charAt(text.length - 1), 10);
|
221
221
|
const postDecimal = parseInt(roundingDownMatch[3], 10);
|
222
222
|
const preDecimal = parseInt(roundingDownMatch[2], 10);
|
223
|
+
const origPostDecimalString = roundingDownMatch[3];
|
223
224
|
let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
|
224
225
|
let carry = 0;
|
225
226
|
if (newPostDecimal.length > postDecimal.toString().length) {
|
@@ -227,11 +228,17 @@ export default class Path {
|
|
227
228
|
newPostDecimal = newPostDecimal.substring(1);
|
228
229
|
carry = 1;
|
229
230
|
}
|
231
|
+
// parseInt(...).toString() removes leading zeroes. Add them back.
|
232
|
+
while (newPostDecimal.length < origPostDecimalString.length) {
|
233
|
+
newPostDecimal = carry.toString(10) + newPostDecimal;
|
234
|
+
carry = 0;
|
235
|
+
}
|
230
236
|
text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
|
231
237
|
}
|
232
238
|
text = text.replace(fixRoundingUpExp, '$1');
|
233
239
|
// Remove trailing zeroes
|
234
|
-
text = text.replace(/([.][^0]
|
240
|
+
text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
|
241
|
+
text = text.replace(/[.]0+$/, '.');
|
235
242
|
// Remove trailing period
|
236
243
|
return text.replace(/[.]$/, '');
|
237
244
|
};
|
@@ -34,6 +34,8 @@ export default class Rect2 {
|
|
34
34
|
get maxDimension(): number;
|
35
35
|
get topRight(): import("./Vec3").default;
|
36
36
|
get bottomLeft(): import("./Vec3").default;
|
37
|
+
get width(): number;
|
38
|
+
get height(): number;
|
37
39
|
getEdges(): LineSegment2[];
|
38
40
|
transformedBoundingBox(affineTransform: Mat33): Rect2;
|
39
41
|
/** @return true iff this is equal to [other] ± fuzz */
|
@@ -126,6 +126,12 @@ export default class Rect2 {
|
|
126
126
|
get bottomLeft() {
|
127
127
|
return this.topLeft.plus(Vec2.of(0, this.h));
|
128
128
|
}
|
129
|
+
get width() {
|
130
|
+
return this.w;
|
131
|
+
}
|
132
|
+
get height() {
|
133
|
+
return this.h;
|
134
|
+
}
|
129
135
|
// Returns edges in the order
|
130
136
|
// [ rightEdge, topEdge, leftEdge, bottomEdge ]
|
131
137
|
getEdges() {
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
2
|
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
3
|
+
import { TextStyle } from '../../components/Text';
|
3
4
|
import Mat33 from '../../geometry/Mat33';
|
4
5
|
import { PathCommand } from '../../geometry/Path';
|
5
6
|
import Rect2 from '../../geometry/Rect2';
|
@@ -20,6 +21,7 @@ export interface RenderablePathSpec {
|
|
20
21
|
export default abstract class AbstractRenderer {
|
21
22
|
private viewport;
|
22
23
|
private selfTransform;
|
24
|
+
private transformStack;
|
23
25
|
protected constructor(viewport: Viewport);
|
24
26
|
protected getViewport(): Viewport;
|
25
27
|
abstract displaySize(): Vec2;
|
@@ -30,6 +32,7 @@ export default abstract class AbstractRenderer {
|
|
30
32
|
protected abstract moveTo(point: Point2): void;
|
31
33
|
protected abstract traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
32
34
|
protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
|
35
|
+
abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
|
33
36
|
abstract isTooSmallToRender(rect: Rect2): boolean;
|
34
37
|
setDraftMode(_draftMode: boolean): void;
|
35
38
|
protected objectLevel: number;
|
@@ -44,6 +47,8 @@ export default abstract class AbstractRenderer {
|
|
44
47
|
canRenderFromWithoutDataLoss(_other: AbstractRenderer): boolean;
|
45
48
|
renderFromOtherOfSameType(_renderTo: Mat33, other: AbstractRenderer): void;
|
46
49
|
setTransform(transform: Mat33 | null): void;
|
50
|
+
pushTransform(transform: Mat33): void;
|
51
|
+
popTransform(): void;
|
47
52
|
getCanvasToScreenTransform(): Mat33;
|
48
53
|
canvasToScreen(vec: Vec2): Vec2;
|
49
54
|
getSizeOfCanvasPixelOnScreen(): number;
|
@@ -11,6 +11,7 @@ export default class AbstractRenderer {
|
|
11
11
|
this.viewport = viewport;
|
12
12
|
// If null, this' transformation is linked to the Viewport
|
13
13
|
this.selfTransform = null;
|
14
|
+
this.transformStack = [];
|
14
15
|
this.objectLevel = 0;
|
15
16
|
this.currentPaths = null;
|
16
17
|
}
|
@@ -104,6 +105,17 @@ export default class AbstractRenderer {
|
|
104
105
|
setTransform(transform) {
|
105
106
|
this.selfTransform = transform;
|
106
107
|
}
|
108
|
+
pushTransform(transform) {
|
109
|
+
this.transformStack.push(this.selfTransform);
|
110
|
+
this.setTransform(this.getCanvasToScreenTransform().rightMul(transform));
|
111
|
+
}
|
112
|
+
popTransform() {
|
113
|
+
var _a;
|
114
|
+
if (this.transformStack.length === 0) {
|
115
|
+
throw new Error('Unable to pop more transforms than have been pushed!');
|
116
|
+
}
|
117
|
+
this.setTransform((_a = this.transformStack.pop()) !== null && _a !== void 0 ? _a : null);
|
118
|
+
}
|
107
119
|
// Get the matrix that transforms a vector on the canvas to a vector on this'
|
108
120
|
// rendering target.
|
109
121
|
getCanvasToScreenTransform() {
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { TextStyle } from '../../components/Text';
|
1
2
|
import Mat33 from '../../geometry/Mat33';
|
2
3
|
import Rect2 from '../../geometry/Rect2';
|
3
4
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
@@ -12,6 +13,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
12
13
|
private minRenderSizeAnyDimen;
|
13
14
|
private minRenderSizeBothDimens;
|
14
15
|
constructor(ctx: CanvasRenderingContext2D, viewport: Viewport);
|
16
|
+
private transformBy;
|
15
17
|
canRenderFromWithoutDataLoss(other: AbstractRenderer): boolean;
|
16
18
|
renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void;
|
17
19
|
setDraftMode(draftMode: boolean): void;
|
@@ -24,6 +26,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
24
26
|
protected traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
25
27
|
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
26
28
|
drawPath(path: RenderablePathSpec): void;
|
29
|
+
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
27
30
|
private clipLevels;
|
28
31
|
startObject(boundingBox: Rect2, clip: boolean): void;
|
29
32
|
endObject(): void;
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
+
import Text from '../../components/Text';
|
2
3
|
import { Vec2 } from '../../geometry/Vec2';
|
3
4
|
import AbstractRenderer from './AbstractRenderer';
|
4
5
|
export default class CanvasRenderer extends AbstractRenderer {
|
@@ -10,6 +11,16 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
10
11
|
this.clipLevels = [];
|
11
12
|
this.setDraftMode(false);
|
12
13
|
}
|
14
|
+
transformBy(transformBy) {
|
15
|
+
// From MDN, transform(a,b,c,d,e,f)
|
16
|
+
// takes input such that
|
17
|
+
// ⎡ a c e ⎤
|
18
|
+
// ⎢ b d f ⎥ transforms content drawn to [ctx].
|
19
|
+
// ⎣ 0 0 1 ⎦
|
20
|
+
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
|
21
|
+
transformBy.a2, transformBy.b2, // c, d
|
22
|
+
transformBy.a3, transformBy.b3);
|
23
|
+
}
|
13
24
|
canRenderFromWithoutDataLoss(other) {
|
14
25
|
return other instanceof CanvasRenderer;
|
15
26
|
}
|
@@ -19,14 +30,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
19
30
|
}
|
20
31
|
transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
|
21
32
|
this.ctx.save();
|
22
|
-
|
23
|
-
// takes input such that
|
24
|
-
// ⎡ a c e ⎤
|
25
|
-
// ⎢ b d f ⎥ transforms content drawn to [ctx].
|
26
|
-
// ⎣ 0 0 1 ⎦
|
27
|
-
this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
|
28
|
-
transformBy.a2, transformBy.b2, // c, d
|
29
|
-
transformBy.a3, transformBy.b3);
|
33
|
+
this.transformBy(transformBy);
|
30
34
|
this.ctx.drawImage(other.ctx.canvas, 0, 0);
|
31
35
|
this.ctx.restore();
|
32
36
|
}
|
@@ -105,6 +109,22 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
105
109
|
}
|
106
110
|
super.drawPath(path);
|
107
111
|
}
|
112
|
+
drawText(text, transform, style) {
|
113
|
+
this.ctx.save();
|
114
|
+
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
115
|
+
this.transformBy(transform);
|
116
|
+
Text.applyTextStyles(this.ctx, style);
|
117
|
+
if (style.renderingStyle.fill.a !== 0) {
|
118
|
+
this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
|
119
|
+
this.ctx.fillText(text, 0, 0);
|
120
|
+
}
|
121
|
+
if (style.renderingStyle.stroke) {
|
122
|
+
this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
|
123
|
+
this.ctx.lineWidth = style.renderingStyle.stroke.width;
|
124
|
+
this.ctx.strokeText(text, 0, 0);
|
125
|
+
}
|
126
|
+
this.ctx.restore();
|
127
|
+
}
|
108
128
|
startObject(boundingBox, clip) {
|
109
129
|
if (this.isTooSmallToRender(boundingBox)) {
|
110
130
|
this.ignoreObjectsAboveLevel = this.getNestingLevel();
|