js-draw 0.1.2 → 0.1.5

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 (79) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +21 -12
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +2 -1
  5. package/dist/src/Editor.js +20 -6
  6. package/dist/src/SVGLoader.d.ts +8 -0
  7. package/dist/src/SVGLoader.js +105 -6
  8. package/dist/src/Viewport.d.ts +1 -1
  9. package/dist/src/Viewport.js +5 -5
  10. package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
  11. package/dist/src/components/Text.d.ts +30 -0
  12. package/dist/src/components/Text.js +111 -0
  13. package/dist/src/components/localization.d.ts +1 -0
  14. package/dist/src/components/localization.js +1 -0
  15. package/dist/src/geometry/Mat33.d.ts +1 -0
  16. package/dist/src/geometry/Mat33.js +30 -0
  17. package/dist/src/geometry/Path.js +8 -1
  18. package/dist/src/geometry/Rect2.d.ts +2 -0
  19. package/dist/src/geometry/Rect2.js +6 -0
  20. package/dist/src/localization.d.ts +2 -1
  21. package/dist/src/localization.js +2 -1
  22. package/dist/src/rendering/Display.d.ts +2 -0
  23. package/dist/src/rendering/Display.js +19 -0
  24. package/dist/src/rendering/localization.d.ts +5 -0
  25. package/dist/src/rendering/localization.js +4 -0
  26. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +5 -0
  27. package/dist/src/rendering/renderers/AbstractRenderer.js +12 -0
  28. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
  29. package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
  30. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
  31. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  32. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -0
  33. package/dist/src/rendering/renderers/SVGRenderer.js +30 -1
  34. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +24 -0
  35. package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
  36. package/dist/src/testing/loadExpectExtensions.js +1 -4
  37. package/dist/src/toolbar/HTMLToolbar.js +78 -1
  38. package/dist/src/toolbar/icons.d.ts +2 -0
  39. package/dist/src/toolbar/icons.js +18 -0
  40. package/dist/src/toolbar/localization.d.ts +1 -0
  41. package/dist/src/toolbar/localization.js +1 -0
  42. package/dist/src/tools/SelectionTool.js +1 -1
  43. package/dist/src/tools/TextTool.d.ts +31 -0
  44. package/dist/src/tools/TextTool.js +174 -0
  45. package/dist/src/tools/ToolController.d.ts +2 -1
  46. package/dist/src/tools/ToolController.js +4 -1
  47. package/dist/src/tools/localization.d.ts +3 -1
  48. package/dist/src/tools/localization.js +3 -1
  49. package/dist-test/test-dist-bundle.html +8 -1
  50. package/package.json +1 -1
  51. package/src/Editor.css +12 -0
  52. package/src/Editor.ts +22 -7
  53. package/src/SVGLoader.ts +124 -6
  54. package/src/Viewport.ts +5 -5
  55. package/src/components/SVGGlobalAttributesObject.ts +0 -1
  56. package/src/components/Text.ts +140 -0
  57. package/src/components/localization.ts +2 -0
  58. package/src/geometry/Mat33.test.ts +44 -0
  59. package/src/geometry/Mat33.ts +41 -0
  60. package/src/geometry/Path.toString.test.ts +7 -3
  61. package/src/geometry/Path.ts +11 -1
  62. package/src/geometry/Rect2.ts +8 -0
  63. package/src/localization.ts +3 -1
  64. package/src/rendering/Display.ts +26 -0
  65. package/src/rendering/localization.ts +10 -0
  66. package/src/rendering/renderers/AbstractRenderer.ts +16 -0
  67. package/src/rendering/renderers/CanvasRenderer.ts +34 -10
  68. package/src/rendering/renderers/DummyRenderer.ts +8 -0
  69. package/src/rendering/renderers/SVGRenderer.ts +36 -1
  70. package/src/rendering/renderers/TextOnlyRenderer.ts +51 -0
  71. package/src/testing/loadExpectExtensions.ts +1 -4
  72. package/src/toolbar/HTMLToolbar.ts +96 -1
  73. package/src/toolbar/icons.ts +24 -0
  74. package/src/toolbar/localization.ts +2 -0
  75. package/src/toolbar/toolbar.css +6 -3
  76. package/src/tools/SelectionTool.ts +1 -1
  77. package/src/tools/TextTool.ts +229 -0
  78. package/src/tools/ToolController.ts +4 -0
  79. package/src/tools/localization.ts +7 -2
@@ -14,7 +14,7 @@ import { EditorLocalization } from './localization';
14
14
  export interface EditorSettings {
15
15
  renderingMode: RenderingMode;
16
16
  localization: Partial<EditorLocalization>;
17
- wheelEventsEnabled: boolean;
17
+ wheelEventsEnabled: boolean | 'only-if-focused';
18
18
  }
19
19
  export declare class Editor {
20
20
  private container;
@@ -48,6 +48,7 @@ export declare class Editor {
48
48
  rerender(showImageBounds?: boolean): void;
49
49
  drawWetInk(...path: RenderablePathSpec[]): void;
50
50
  clearWetInk(): void;
51
+ focus(): void;
51
52
  createHTMLOverlay(overlay: HTMLElement): {
52
53
  remove: () => void;
53
54
  };
@@ -183,13 +183,24 @@ export class Editor {
183
183
  })) {
184
184
  evt.preventDefault();
185
185
  }
186
+ else if (evt.key === 'Escape') {
187
+ this.renderingRegion.blur();
188
+ }
186
189
  });
187
190
  this.container.addEventListener('wheel', evt => {
188
191
  let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
189
- // Process wheel events if the ctrl key is down -- we do want to handle
192
+ // Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
190
193
  // pinch-zooming.
191
- if (!this.settings.wheelEventsEnabled && !evt.ctrlKey) {
192
- return;
194
+ if (!evt.ctrlKey) {
195
+ if (!this.settings.wheelEventsEnabled) {
196
+ return;
197
+ }
198
+ else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
199
+ const focusedChild = this.container.querySelector(':focus');
200
+ if (!focusedChild) {
201
+ return;
202
+ }
203
+ }
193
204
  }
194
205
  if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
195
206
  delta = delta.times(15);
@@ -281,13 +292,12 @@ export class Editor {
281
292
  this.display.startRerender();
282
293
  // Draw a rectangle around the region that will be visible on save
283
294
  const renderer = this.display.getDryInkRenderer();
295
+ this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
284
296
  if (showImageBounds) {
285
297
  const exportRectFill = { fill: Color4.fromHex('#44444455') };
286
- const exportRectStrokeWidth = 12;
298
+ const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
287
299
  renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
288
300
  }
289
- //this.image.render(renderer, this.viewport);
290
- this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
291
301
  this.rerenderQueued = false;
292
302
  }
293
303
  drawWetInk(...path) {
@@ -298,6 +308,10 @@ export class Editor {
298
308
  clearWetInk() {
299
309
  this.display.getWetInkRenderer().clear();
300
310
  }
311
+ // Focuses the region used for text input
312
+ focus() {
313
+ this.renderingRegion.focus();
314
+ }
301
315
  createHTMLOverlay(overlay) {
302
316
  overlay.classList.add('overlay');
303
317
  this.container.appendChild(overlay);
@@ -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-serif',
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!`);
@@ -93,18 +93,18 @@ export class Viewport {
93
93
  if (isNaN(toMakeVisible.size.magnitude())) {
94
94
  throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
95
95
  }
96
- // Try to move the selection within the center 2/3rds of the viewport.
96
+ // Try to move the selection within the center 3/4ths of the viewport.
97
97
  const recomputeTargetRect = () => {
98
98
  // transform transforms objects on the canvas. As such, we need to invert it
99
99
  // to transform the viewport.
100
100
  const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
101
- return visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
101
+ return visibleRect.transformedBoundingBox(Mat33.scaling2D(3 / 4, visibleRect.center));
102
102
  };
103
103
  let targetRect = recomputeTargetRect();
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
- const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
107
- if (largerThanTarget || muchSmallerThanTarget) {
106
+ const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.25;
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,111 @@
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
+ // Quote the font family if necessary.
15
+ const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
16
+ ctx.font = [
17
+ ((_a = style.size) !== null && _a !== void 0 ? _a : 12) + 'px',
18
+ (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '',
19
+ `${fontFamily}`,
20
+ style.fontWeight
21
+ ].join(' ');
22
+ ctx.textAlign = 'left';
23
+ }
24
+ static getTextDimens(text, style) {
25
+ var _a;
26
+ (_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = document.createElement('canvas').getContext('2d'));
27
+ const ctx = Text.textMeasuringCtx;
28
+ Text.applyTextStyles(ctx, style);
29
+ const measure = ctx.measureText(text);
30
+ // Text is drawn with (0,0) at the bottom left of the baseline.
31
+ const textY = -measure.actualBoundingBoxAscent;
32
+ const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
33
+ return new Rect2(0, textY, measure.width, textHeight);
34
+ }
35
+ computeBBoxOfPart(part) {
36
+ if (typeof part === 'string') {
37
+ const textBBox = Text.getTextDimens(part, this.style);
38
+ return textBBox.transformedBoundingBox(this.transform);
39
+ }
40
+ else {
41
+ const bbox = part.contentBBox.transformedBoundingBox(this.transform);
42
+ return bbox;
43
+ }
44
+ }
45
+ recomputeBBox() {
46
+ let bbox = null;
47
+ for (const textObject of this.textObjects) {
48
+ const currentBBox = this.computeBBoxOfPart(textObject);
49
+ bbox !== null && bbox !== void 0 ? bbox : (bbox = currentBBox);
50
+ bbox = bbox.union(currentBBox);
51
+ }
52
+ this.contentBBox = bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
53
+ }
54
+ render(canvas, _visibleRect) {
55
+ const cursor = this.transform;
56
+ canvas.startObject(this.contentBBox);
57
+ for (const textObject of this.textObjects) {
58
+ if (typeof textObject === 'string') {
59
+ canvas.drawText(textObject, cursor, this.style);
60
+ }
61
+ else {
62
+ canvas.pushTransform(cursor);
63
+ textObject.render(canvas);
64
+ canvas.popTransform();
65
+ }
66
+ }
67
+ canvas.endObject(this.getLoadSaveData());
68
+ }
69
+ intersects(lineSegment) {
70
+ // Convert canvas space to internal space.
71
+ const invTransform = this.transform.inverse();
72
+ const p1InThisSpace = invTransform.transformVec2(lineSegment.p1);
73
+ const p2InThisSpace = invTransform.transformVec2(lineSegment.p2);
74
+ lineSegment = new LineSegment2(p1InThisSpace, p2InThisSpace);
75
+ for (const subObject of this.textObjects) {
76
+ if (typeof subObject === 'string') {
77
+ const textBBox = Text.getTextDimens(subObject, this.style);
78
+ // TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and
79
+ // use pixel-testing to check for intersection with its contour.
80
+ if (textBBox.getEdges().some(edge => lineSegment.intersection(edge) !== null)) {
81
+ return true;
82
+ }
83
+ }
84
+ else {
85
+ if (subObject.intersects(lineSegment)) {
86
+ return true;
87
+ }
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+ applyTransformation(affineTransfm) {
93
+ this.transform = affineTransfm.rightMul(this.transform);
94
+ this.recomputeBBox();
95
+ }
96
+ getText() {
97
+ const result = [];
98
+ for (const textObject of this.textObjects) {
99
+ if (typeof textObject === 'string') {
100
+ result.push(textObject);
101
+ }
102
+ else {
103
+ result.push(textObject.getText());
104
+ }
105
+ }
106
+ return result.join(' ');
107
+ }
108
+ description(localizationTable) {
109
+ return localizationTable.text(this.getText());
110
+ }
111
+ }
@@ -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,8 +1,9 @@
1
1
  import { CommandLocalization } from './commands/localization';
2
2
  import { ImageComponentLocalization } from './components/localization';
3
+ import { TextRendererLocalization } from './rendering/localization';
3
4
  import { ToolbarLocalization } from './toolbar/localization';
4
5
  import { ToolLocalization } from './tools/localization';
5
- export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
6
+ export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization, TextRendererLocalization {
6
7
  undoAnnouncement: (actionDescription: string) => string;
7
8
  redoAnnouncement: (actionDescription: string) => string;
8
9
  doneLoading: string;
@@ -1,5 +1,6 @@
1
1
  import { defaultCommandLocalization } from './commands/localization';
2
2
  import { defaultComponentLocalization } from './components/localization';
3
+ import { defaultTextRendererLocalization } from './rendering/localization';
3
4
  import { defaultToolbarLocalization } from './toolbar/localization';
4
5
  import { defaultToolLocalization } from './tools/localization';
5
- export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
6
+ export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), defaultTextRendererLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
@@ -10,6 +10,7 @@ export default class Display {
10
10
  private parent;
11
11
  private dryInkRenderer;
12
12
  private wetInkRenderer;
13
+ private textRenderer;
13
14
  private cache;
14
15
  private resizeSurfacesCallback?;
15
16
  private flattenCallback?;
@@ -18,6 +19,7 @@ export default class Display {
18
19
  get height(): number;
19
20
  getCache(): RenderingCache;
20
21
  private initializeCanvasRendering;
22
+ private initializeTextRendering;
21
23
  startRerender(): AbstractRenderer;
22
24
  setDraftMode(draftMode: boolean): void;
23
25
  getDryInkRenderer(): AbstractRenderer;