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.
Files changed (60) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.js +2 -3
  5. package/dist/src/SVGLoader.d.ts +8 -0
  6. package/dist/src/SVGLoader.js +105 -6
  7. package/dist/src/Viewport.d.ts +1 -1
  8. package/dist/src/Viewport.js +2 -2
  9. package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
  10. package/dist/src/components/Text.d.ts +30 -0
  11. package/dist/src/components/Text.js +109 -0
  12. package/dist/src/components/localization.d.ts +1 -0
  13. package/dist/src/components/localization.js +1 -0
  14. package/dist/src/geometry/Mat33.d.ts +1 -0
  15. package/dist/src/geometry/Mat33.js +30 -0
  16. package/dist/src/geometry/Path.js +8 -1
  17. package/dist/src/geometry/Rect2.d.ts +2 -0
  18. package/dist/src/geometry/Rect2.js +6 -0
  19. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +5 -0
  20. package/dist/src/rendering/renderers/AbstractRenderer.js +12 -0
  21. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
  22. package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
  23. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
  24. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  25. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -0
  26. package/dist/src/rendering/renderers/SVGRenderer.js +30 -1
  27. package/dist/src/testing/loadExpectExtensions.js +1 -4
  28. package/dist/src/toolbar/HTMLToolbar.js +52 -1
  29. package/dist/src/toolbar/icons.d.ts +2 -0
  30. package/dist/src/toolbar/icons.js +17 -0
  31. package/dist/src/tools/SelectionTool.js +1 -1
  32. package/dist/src/tools/TextTool.d.ts +29 -0
  33. package/dist/src/tools/TextTool.js +154 -0
  34. package/dist/src/tools/ToolController.d.ts +2 -1
  35. package/dist/src/tools/ToolController.js +4 -1
  36. package/dist/src/tools/localization.d.ts +3 -1
  37. package/dist/src/tools/localization.js +3 -1
  38. package/package.json +1 -1
  39. package/src/Editor.ts +3 -3
  40. package/src/SVGLoader.ts +124 -6
  41. package/src/Viewport.ts +2 -2
  42. package/src/components/SVGGlobalAttributesObject.ts +0 -1
  43. package/src/components/Text.ts +136 -0
  44. package/src/components/localization.ts +2 -0
  45. package/src/geometry/Mat33.test.ts +44 -0
  46. package/src/geometry/Mat33.ts +41 -0
  47. package/src/geometry/Path.toString.test.ts +7 -3
  48. package/src/geometry/Path.ts +11 -1
  49. package/src/geometry/Rect2.ts +8 -0
  50. package/src/rendering/renderers/AbstractRenderer.ts +16 -0
  51. package/src/rendering/renderers/CanvasRenderer.ts +34 -10
  52. package/src/rendering/renderers/DummyRenderer.ts +8 -0
  53. package/src/rendering/renderers/SVGRenderer.ts +36 -1
  54. package/src/testing/loadExpectExtensions.ts +1 -4
  55. package/src/toolbar/HTMLToolbar.ts +64 -1
  56. package/src/toolbar/icons.ts +23 -0
  57. package/src/tools/SelectionTool.ts +1 -1
  58. package/src/tools/TextTool.ts +206 -0
  59. package/src/tools/ToolController.ts +4 -0
  60. package/src/tools/localization.ts +7 -2
@@ -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 = 12;
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) {
@@ -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;
@@ -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
- this.attachUnrecognisedAttrs(elem, node, new Set(['stroke', 'fill', 'stroke-width', 'd']));
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
- for (const child of node.children) {
159
- yield this.visit(child);
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
  }
@@ -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;
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  export interface ImageComponentLocalization {
2
+ text: (text: string) => string;
2
3
  stroke: string;
3
4
  svgObject: string;
4
5
  }
@@ -1,4 +1,5 @@
1
1
  export const defaultComponentLocalization = {
2
2
  stroke: 'Stroke',
3
3
  svgObject: 'SVG Object',
4
+ text: (text) => `Text object: ${text}`,
4
5
  };
@@ -28,4 +28,5 @@ export default class Mat33 {
28
28
  static translation(amount: Vec2): Mat33;
29
29
  static zRotation(radians: number, center?: Point2): Mat33;
30
30
  static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
31
+ static fromCSSMatrix(cssString: string): Mat33;
31
32
  }
@@ -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]*)0+$/, '$1');
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
- // From MDN, transform(a,b,c,d,e,f)
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();