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
@@ -3,6 +3,7 @@ import { EditorEventType } from '../types';
3
3
  import DummyRenderer from './renderers/DummyRenderer';
4
4
  import { Vec2 } from '../geometry/Vec2';
5
5
  import RenderingCache from './caching/RenderingCache';
6
+ import TextOnlyRenderer from './renderers/TextOnlyRenderer';
6
7
  export var RenderingMode;
7
8
  (function (RenderingMode) {
8
9
  RenderingMode[RenderingMode["DummyRenderer"] = 0] = "DummyRenderer";
@@ -23,6 +24,8 @@ export default class Display {
23
24
  else {
24
25
  throw new Error(`Unknown rendering mode, ${mode}!`);
25
26
  }
27
+ this.textRenderer = new TextOnlyRenderer(editor.viewport, editor.localization);
28
+ this.initializeTextRendering();
26
29
  const cacheBlockResolution = Vec2.of(600, 600);
27
30
  this.cache = new RenderingCache({
28
31
  createRenderer: () => {
@@ -104,6 +107,22 @@ export default class Display {
104
107
  dryInkCtx.drawImage(wetInkCanvas, 0, 0);
105
108
  };
106
109
  }
110
+ initializeTextRendering() {
111
+ const textRendererOutputContainer = document.createElement('div');
112
+ textRendererOutputContainer.classList.add('textRendererOutputContainer');
113
+ const rerenderButton = document.createElement('button');
114
+ rerenderButton.classList.add('rerenderButton');
115
+ rerenderButton.innerText = this.editor.localization.rerenderAsText;
116
+ const rerenderOutput = document.createElement('div');
117
+ rerenderOutput.ariaLive = 'polite';
118
+ rerenderButton.onclick = () => {
119
+ this.textRenderer.clear();
120
+ this.editor.image.render(this.textRenderer, this.editor.viewport);
121
+ rerenderOutput.innerText = this.textRenderer.getDescription();
122
+ };
123
+ textRendererOutputContainer.replaceChildren(rerenderButton, rerenderOutput);
124
+ this.editor.createHTMLOverlay(textRendererOutputContainer);
125
+ }
107
126
  // Clears the drawing surfaces and otherwise prepares for a rerender.
108
127
  startRerender() {
109
128
  var _a;
@@ -0,0 +1,5 @@
1
+ export interface TextRendererLocalization {
2
+ textNode(content: string): string;
3
+ rerenderAsText: string;
4
+ }
5
+ export declare const defaultTextRendererLocalization: TextRendererLocalization;
@@ -0,0 +1,4 @@
1
+ export const defaultTextRendererLocalization = {
2
+ textNode: (content) => `Text: ${content}`,
3
+ rerenderAsText: 'Re-render as text',
4
+ };
@@ -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();
@@ -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';
@@ -10,6 +11,7 @@ export default class DummyRenderer extends AbstractRenderer {
10
11
  lastFillStyle: RenderingStyle | null;
11
12
  lastPoint: Point2 | null;
12
13
  objectNestingLevel: number;
14
+ lastText: string | null;
13
15
  pointBuffer: Point2[];
14
16
  constructor(viewport: Viewport);
15
17
  displaySize(): Vec2;
@@ -21,6 +23,7 @@ export default class DummyRenderer extends AbstractRenderer {
21
23
  protected traceCubicBezierCurve(p1: Vec3, p2: Vec3, p3: Vec3): void;
22
24
  protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
23
25
  drawPoints(..._points: Vec3[]): void;
26
+ drawText(text: string, _transform: Mat33, _style: TextStyle): void;
24
27
  startObject(boundingBox: Rect2, _clip: boolean): void;
25
28
  endObject(): void;
26
29
  isTooSmallToRender(_rect: Rect2): boolean;
@@ -10,6 +10,7 @@ export default class DummyRenderer extends AbstractRenderer {
10
10
  this.lastFillStyle = null;
11
11
  this.lastPoint = null;
12
12
  this.objectNestingLevel = 0;
13
+ this.lastText = null;
13
14
  // List of points drawn since the last clear.
14
15
  this.pointBuffer = [];
15
16
  }
@@ -28,6 +29,7 @@ export default class DummyRenderer extends AbstractRenderer {
28
29
  this.clearedCount++;
29
30
  this.renderedPathCount = 0;
30
31
  this.pointBuffer = [];
32
+ this.lastText = null;
31
33
  // Ensure all objects finished rendering
32
34
  if (this.objectNestingLevel > 0) {
33
35
  throw new Error(`Within an object while clearing! Nesting level: ${this.objectNestingLevel}`);
@@ -68,6 +70,9 @@ export default class DummyRenderer extends AbstractRenderer {
68
70
  // drawPoints is intended for debugging.
69
71
  // As such, it is unlikely to be the target of automated tests.
70
72
  }
73
+ drawText(text, _transform, _style) {
74
+ this.lastText = text;
75
+ }
71
76
  startObject(boundingBox, _clip) {
72
77
  super.startObject(boundingBox);
73
78
  this.objectNestingLevel += 1;
@@ -1,4 +1,6 @@
1
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
+ import { TextStyle } from '../../components/Text';
3
+ import Mat33 from '../../geometry/Mat33';
2
4
  import Rect2 from '../../geometry/Rect2';
3
5
  import { Point2, Vec2 } from '../../geometry/Vec2';
4
6
  import Viewport from '../../Viewport';
@@ -19,6 +21,7 @@ export default class SVGRenderer extends AbstractRenderer {
19
21
  protected beginPath(startPoint: Point2): void;
20
22
  protected endPath(style: RenderingStyle): void;
21
23
  private addPathToSVG;
24
+ drawText(text: string, transform: Mat33, style: TextStyle): void;
22
25
  startObject(boundingBox: Rect2): void;
23
26
  endObject(loaderData?: LoadSaveDataTable): void;
24
27
  protected lineTo(point: Point2): void;
@@ -1,6 +1,6 @@
1
1
  import Path, { PathCommandType } from '../../geometry/Path';
2
2
  import { Vec2 } from '../../geometry/Vec2';
3
- import { svgAttributesDataKey } from '../../SVGLoader';
3
+ import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader';
4
4
  import AbstractRenderer from './AbstractRenderer';
5
5
  const svgNameSpace = 'http://www.w3.org/2000/svg';
6
6
  export default class SVGRenderer extends AbstractRenderer {
@@ -86,6 +86,29 @@ export default class SVGRenderer extends AbstractRenderer {
86
86
  this.elem.appendChild(pathElem);
87
87
  (_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(pathElem);
88
88
  }
89
+ drawText(text, transform, style) {
90
+ var _a, _b, _c;
91
+ transform = this.getCanvasToScreenTransform().rightMul(transform);
92
+ const textElem = document.createElementNS(svgNameSpace, 'text');
93
+ textElem.appendChild(document.createTextNode(text));
94
+ textElem.style.transform = `matrix(
95
+ ${transform.a1}, ${transform.b1},
96
+ ${transform.a2}, ${transform.b2},
97
+ ${transform.a3}, ${transform.b3}
98
+ )`;
99
+ textElem.style.fontFamily = style.fontFamily;
100
+ textElem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
101
+ textElem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
102
+ textElem.style.fontSize = style.size + 'px';
103
+ textElem.style.fill = style.renderingStyle.fill.toHexString();
104
+ if (style.renderingStyle.stroke) {
105
+ const strokeStyle = style.renderingStyle.stroke;
106
+ textElem.style.stroke = strokeStyle.color.toHexString();
107
+ textElem.style.strokeWidth = strokeStyle.width + 'px';
108
+ }
109
+ this.elem.appendChild(textElem);
110
+ (_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
111
+ }
89
112
  startObject(boundingBox) {
90
113
  super.startObject(boundingBox);
91
114
  // Only accumulate a path within an object
@@ -103,11 +126,17 @@ export default class SVGRenderer extends AbstractRenderer {
103
126
  // Restore any attributes unsupported by the app.
104
127
  for (const elem of (_a = this.objectElems) !== null && _a !== void 0 ? _a : []) {
105
128
  const attrs = loaderData[svgAttributesDataKey];
129
+ const styleAttrs = loaderData[svgStyleAttributesDataKey];
106
130
  if (attrs) {
107
131
  for (const [attr, value] of attrs) {
108
132
  elem.setAttribute(attr, value);
109
133
  }
110
134
  }
135
+ if (styleAttrs) {
136
+ for (const attr of styleAttrs) {
137
+ elem.style.setProperty(attr.key, attr.value, attr.priority);
138
+ }
139
+ }
111
140
  }
112
141
  }
113
142
  }
@@ -0,0 +1,24 @@
1
+ import { TextStyle } from '../../components/Text';
2
+ import Mat33 from '../../geometry/Mat33';
3
+ import Rect2 from '../../geometry/Rect2';
4
+ import Vec3 from '../../geometry/Vec3';
5
+ import Viewport from '../../Viewport';
6
+ import { TextRendererLocalization } from '../localization';
7
+ import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
8
+ export default class TextOnlyRenderer extends AbstractRenderer {
9
+ private localizationTable;
10
+ private descriptionBuilder;
11
+ constructor(viewport: Viewport, localizationTable: TextRendererLocalization);
12
+ displaySize(): Vec3;
13
+ clear(): void;
14
+ getDescription(): string;
15
+ protected beginPath(_startPoint: Vec3): void;
16
+ protected endPath(_style: RenderingStyle): void;
17
+ protected lineTo(_point: Vec3): void;
18
+ protected moveTo(_point: Vec3): void;
19
+ protected traceCubicBezierCurve(_p1: Vec3, _p2: Vec3, _p3: Vec3): void;
20
+ protected traceQuadraticBezierCurve(_controlPoint: Vec3, _endPoint: Vec3): void;
21
+ drawText(text: string, _transform: Mat33, _style: TextStyle): void;
22
+ isTooSmallToRender(rect: Rect2): boolean;
23
+ drawPoints(..._points: Vec3[]): void;
24
+ }
@@ -0,0 +1,40 @@
1
+ import { Vec2 } from '../../geometry/Vec2';
2
+ import AbstractRenderer from './AbstractRenderer';
3
+ // Outputs a description of what was rendered.
4
+ export default class TextOnlyRenderer extends AbstractRenderer {
5
+ constructor(viewport, localizationTable) {
6
+ super(viewport);
7
+ this.localizationTable = localizationTable;
8
+ this.descriptionBuilder = [];
9
+ }
10
+ displaySize() {
11
+ // We don't have a graphical display, export a reasonable size.
12
+ return Vec2.of(500, 500);
13
+ }
14
+ clear() {
15
+ this.descriptionBuilder = [];
16
+ }
17
+ getDescription() {
18
+ return this.descriptionBuilder.join('\n');
19
+ }
20
+ beginPath(_startPoint) {
21
+ }
22
+ endPath(_style) {
23
+ }
24
+ lineTo(_point) {
25
+ }
26
+ moveTo(_point) {
27
+ }
28
+ traceCubicBezierCurve(_p1, _p2, _p3) {
29
+ }
30
+ traceQuadraticBezierCurve(_controlPoint, _endPoint) {
31
+ }
32
+ drawText(text, _transform, _style) {
33
+ this.descriptionBuilder.push(this.localizationTable.textNode(text));
34
+ }
35
+ isTooSmallToRender(rect) {
36
+ return rect.maxDimension < 10 / this.getSizeOfCanvasPixelOnScreen();
37
+ }
38
+ drawPoints(..._points) {
39
+ }
40
+ }
@@ -15,10 +15,7 @@ export const loadExpectExtensions = () => {
15
15
  return {
16
16
  pass,
17
17
  message: () => {
18
- if (pass) {
19
- return `Expected ${expected} not to .eq ${actual}. Options(${eqArgs})`;
20
- }
21
- return `Expected ${expected} to .eq ${actual}. Options(${eqArgs})`;
18
+ return `Expected ${pass ? '!' : ''}(${actual}).eq(${expected}). Options(${eqArgs})`;
22
19
  },
23
20
  };
24
21
  },
@@ -10,10 +10,11 @@ import { makeArrowBuilder } from '../components/builders/ArrowBuilder';
10
10
  import { makeLineBuilder } from '../components/builders/LineBuilder';
11
11
  import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../components/builders/RectangleBuilder';
12
12
  import { defaultToolbarLocalization } from './localization';
13
- import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon } from './icons';
13
+ import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon, makeTextIcon } from './icons';
14
14
  import PanZoom, { PanZoomMode } from '../tools/PanZoom';
15
15
  import Mat33 from '../geometry/Mat33';
16
16
  import Viewport from '../Viewport';
17
+ import TextTool from '../tools/TextTool';
17
18
  const toolbarCSSPrefix = 'toolbar-';
18
19
  class ToolbarWidget {
19
20
  constructor(editor, targetTool, localizationTable) {
@@ -316,6 +317,76 @@ class HandToolWidget extends ToolbarWidget {
316
317
  this.setDropdownVisible(!this.isDropdownVisible());
317
318
  }
318
319
  }
320
+ class TextToolWidget extends ToolbarWidget {
321
+ constructor(editor, tool, localization) {
322
+ super(editor, tool, localization);
323
+ this.tool = tool;
324
+ this.updateDropdownInputs = null;
325
+ editor.notifier.on(EditorEventType.ToolUpdated, evt => {
326
+ var _a;
327
+ if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
328
+ this.updateIcon();
329
+ (_a = this.updateDropdownInputs) === null || _a === void 0 ? void 0 : _a.call(this);
330
+ }
331
+ });
332
+ }
333
+ getTitle() {
334
+ return this.targetTool.description;
335
+ }
336
+ createIcon() {
337
+ const textStyle = this.tool.getTextStyle();
338
+ return makeTextIcon(textStyle);
339
+ }
340
+ fillDropdown(dropdown) {
341
+ const fontRow = document.createElement('div');
342
+ const colorRow = document.createElement('div');
343
+ const fontInput = document.createElement('select');
344
+ const fontLabel = document.createElement('label');
345
+ const colorInput = document.createElement('input');
346
+ const colorLabel = document.createElement('label');
347
+ const fontsInInput = new Set();
348
+ const addFontToInput = (fontName) => {
349
+ const option = document.createElement('option');
350
+ option.value = fontName;
351
+ option.textContent = fontName;
352
+ fontInput.appendChild(option);
353
+ fontsInInput.add(fontName);
354
+ };
355
+ fontLabel.innerText = this.localizationTable.fontLabel;
356
+ colorLabel.innerText = this.localizationTable.colorLabel;
357
+ colorInput.classList.add('coloris_input');
358
+ colorInput.type = 'button';
359
+ colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
360
+ colorLabel.setAttribute('for', colorInput.id);
361
+ addFontToInput('monospace');
362
+ addFontToInput('serif');
363
+ addFontToInput('sans-serif');
364
+ fontInput.id = `${toolbarCSSPrefix}-text-font-input-${TextToolWidget.idCounter++}`;
365
+ fontLabel.setAttribute('for', fontInput.id);
366
+ fontInput.onchange = () => {
367
+ this.tool.setFontFamily(fontInput.value);
368
+ };
369
+ colorInput.oninput = () => {
370
+ this.tool.setColor(Color4.fromString(colorInput.value));
371
+ };
372
+ colorRow.appendChild(colorLabel);
373
+ colorRow.appendChild(colorInput);
374
+ fontRow.appendChild(fontLabel);
375
+ fontRow.appendChild(fontInput);
376
+ this.updateDropdownInputs = () => {
377
+ const style = this.tool.getTextStyle();
378
+ colorInput.value = style.renderingStyle.fill.toHexString();
379
+ if (!fontsInInput.has(style.fontFamily)) {
380
+ addFontToInput(style.fontFamily);
381
+ }
382
+ fontInput.value = style.fontFamily;
383
+ };
384
+ this.updateDropdownInputs();
385
+ dropdown.replaceChildren(colorRow, fontRow);
386
+ return true;
387
+ }
388
+ }
389
+ TextToolWidget.idCounter = 0;
319
390
  class PenWidget extends ToolbarWidget {
320
391
  constructor(editor, tool, localization, penTypes) {
321
392
  super(editor, tool, localization);
@@ -559,6 +630,12 @@ export default class HTMLToolbar {
559
630
  }
560
631
  (new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
561
632
  }
633
+ for (const tool of toolController.getMatchingTools(ToolType.Text)) {
634
+ if (!(tool instanceof TextTool)) {
635
+ throw new Error('All text tools must have kind === ToolType.Text');
636
+ }
637
+ (new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
638
+ }
562
639
  for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
563
640
  if (!(tool instanceof PanZoom)) {
564
641
  throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
@@ -1,4 +1,5 @@
1
1
  import { ComponentBuilderFactory } from '../components/builders/types';
2
+ import { TextStyle } from '../components/Text';
2
3
  import Pen from '../tools/Pen';
3
4
  export declare const makeUndoIcon: () => SVGSVGElement;
4
5
  export declare const makeRedoIcon: (mirror?: boolean) => SVGSVGElement;
@@ -6,5 +7,6 @@ export declare const makeDropdownIcon: () => SVGSVGElement;
6
7
  export declare const makeEraserIcon: () => SVGSVGElement;
7
8
  export declare const makeSelectionIcon: () => SVGSVGElement;
8
9
  export declare const makeHandToolIcon: () => SVGSVGElement;
10
+ export declare const makeTextIcon: (textStyle: TextStyle) => SVGSVGElement;
9
11
  export declare const makePenIcon: (tipThickness: number, color: string) => SVGSVGElement;
10
12
  export declare const makeIconFromFactory: (pen: Pen, factory: ComponentBuilderFactory) => SVGSVGElement;
@@ -111,6 +111,24 @@ export const makeHandToolIcon = () => {
111
111
  icon.setAttribute('viewBox', '0 0 100 100');
112
112
  return icon;
113
113
  };
114
+ export const makeTextIcon = (textStyle) => {
115
+ var _a, _b;
116
+ const icon = document.createElementNS(svgNamespace, 'svg');
117
+ icon.setAttribute('viewBox', '0 0 100 100');
118
+ const textNode = document.createElementNS(svgNamespace, 'text');
119
+ textNode.appendChild(document.createTextNode('T'));
120
+ textNode.style.fontFamily = textStyle.fontFamily;
121
+ textNode.style.fontWeight = (_a = textStyle.fontWeight) !== null && _a !== void 0 ? _a : '';
122
+ textNode.style.fontVariant = (_b = textStyle.fontVariant) !== null && _b !== void 0 ? _b : '';
123
+ textNode.style.fill = textStyle.renderingStyle.fill.toHexString();
124
+ textNode.style.textAnchor = 'middle';
125
+ textNode.setAttribute('x', '50');
126
+ textNode.setAttribute('y', '75');
127
+ textNode.style.fontSize = '65px';
128
+ textNode.style.filter = 'drop-shadow(0px 0px 10px var(--primary-shadow-color))';
129
+ icon.appendChild(textNode);
130
+ return icon;
131
+ };
114
132
  export const makePenIcon = (tipThickness, color) => {
115
133
  const icon = document.createElementNS(svgNamespace, 'svg');
116
134
  icon.setAttribute('viewBox', '0 0 100 100');
@@ -1,4 +1,5 @@
1
1
  export interface ToolbarLocalization {
2
+ fontLabel: string;
2
3
  anyDevicePanning: string;
3
4
  touchPanning: string;
4
5
  outlinedRectanglePen: string;
@@ -5,6 +5,7 @@ export const defaultToolbarLocalization = {
5
5
  handTool: 'Pan',
6
6
  thicknessLabel: 'Thickness: ',
7
7
  colorLabel: 'Color: ',
8
+ fontLabel: 'Font: ',
8
9
  resizeImageToSelection: 'Resize image to selection',
9
10
  deleteSelection: 'Delete selection',
10
11
  undo: 'Undo',
@@ -396,7 +396,7 @@ export default class SelectionTool extends BaseTool {
396
396
  if (hasSelection) {
397
397
  this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
398
398
  const selectionRect = this.selectionBox.region;
399
- this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
399
+ this.editor.viewport.zoomTo(selectionRect, false).apply(this.editor);
400
400
  }
401
401
  }
402
402
  onPointerUp(event) {
@@ -0,0 +1,31 @@
1
+ import Color4 from '../Color4';
2
+ import { TextStyle } from '../components/Text';
3
+ import Editor from '../Editor';
4
+ import { PointerEvt } from '../types';
5
+ import BaseTool from './BaseTool';
6
+ import { ToolLocalization } from './localization';
7
+ import { ToolType } from './ToolController';
8
+ export default class TextTool extends BaseTool {
9
+ private editor;
10
+ private localizationTable;
11
+ kind: ToolType;
12
+ private textStyle;
13
+ private textEditOverlay;
14
+ private textInputElem;
15
+ private textTargetPosition;
16
+ private textMeasuringCtx;
17
+ private textRotation;
18
+ constructor(editor: Editor, description: string, localizationTable: ToolLocalization);
19
+ private getTextAscent;
20
+ private flushInput;
21
+ private updateTextInput;
22
+ private startTextInput;
23
+ setEnabled(enabled: boolean): void;
24
+ onPointerDown({ current, allPointers }: PointerEvt): boolean;
25
+ onGestureCancel(): void;
26
+ private dispatchUpdateEvent;
27
+ setFontFamily(fontFamily: string): void;
28
+ setColor(color: Color4): void;
29
+ setFontSize(size: number): void;
30
+ getTextStyle(): TextStyle;
31
+ }