js-draw 0.0.3 → 0.0.4

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 (59) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/src/Editor.d.ts +1 -1
  3. package/dist/src/Editor.js +7 -2
  4. package/dist/src/EditorImage.d.ts +2 -0
  5. package/dist/src/EditorImage.js +29 -5
  6. package/dist/src/Pointer.js +1 -1
  7. package/dist/src/Viewport.d.ts +1 -1
  8. package/dist/src/components/AbstractComponent.d.ts +1 -1
  9. package/dist/src/components/Stroke.d.ts +1 -1
  10. package/dist/src/components/Stroke.js +1 -1
  11. package/dist/src/components/UnknownSVGObject.d.ts +1 -1
  12. package/dist/src/components/builders/ArrowBuilder.d.ts +17 -0
  13. package/dist/src/components/builders/ArrowBuilder.js +83 -0
  14. package/dist/src/{StrokeBuilder.d.ts → components/builders/FreehandLineBuilder.d.ts} +9 -13
  15. package/dist/src/{StrokeBuilder.js → components/builders/FreehandLineBuilder.js} +27 -9
  16. package/dist/src/components/builders/LineBuilder.d.ts +16 -0
  17. package/dist/src/components/builders/LineBuilder.js +57 -0
  18. package/dist/src/components/builders/RectangleBuilder.d.ts +18 -0
  19. package/dist/src/components/builders/RectangleBuilder.js +41 -0
  20. package/dist/src/components/builders/types.d.ts +12 -0
  21. package/dist/src/components/builders/types.js +1 -0
  22. package/dist/src/geometry/Path.d.ts +1 -0
  23. package/dist/src/geometry/Path.js +32 -0
  24. package/dist/src/geometry/Vec3.d.ts +2 -0
  25. package/dist/src/geometry/Vec3.js +13 -0
  26. package/dist/src/rendering/AbstractRenderer.js +3 -25
  27. package/dist/src/toolbar/HTMLToolbar.d.ts +4 -1
  28. package/dist/src/toolbar/HTMLToolbar.js +143 -16
  29. package/dist/src/toolbar/types.d.ts +6 -0
  30. package/dist/src/tools/Pen.d.ts +13 -3
  31. package/dist/src/tools/Pen.js +37 -28
  32. package/dist/src/tools/ToolController.js +3 -3
  33. package/dist/src/types.d.ts +14 -2
  34. package/dist/src/types.js +1 -0
  35. package/package.json +4 -3
  36. package/src/Editor.ts +9 -2
  37. package/src/EditorImage.ts +31 -3
  38. package/src/Pointer.ts +1 -1
  39. package/src/Viewport.ts +1 -1
  40. package/src/components/AbstractComponent.ts +1 -1
  41. package/src/components/Stroke.ts +2 -2
  42. package/src/components/UnknownSVGObject.ts +1 -1
  43. package/src/components/builders/ArrowBuilder.ts +104 -0
  44. package/src/{StrokeBuilder.ts → components/builders/FreehandLineBuilder.ts} +36 -18
  45. package/src/components/builders/LineBuilder.ts +75 -0
  46. package/src/components/builders/RectangleBuilder.ts +59 -0
  47. package/src/components/builders/types.ts +15 -0
  48. package/src/geometry/Path.ts +43 -0
  49. package/src/geometry/Vec2.test.ts +1 -0
  50. package/src/geometry/Vec3.test.ts +14 -0
  51. package/src/geometry/Vec3.ts +16 -0
  52. package/src/rendering/AbstractRenderer.ts +3 -32
  53. package/src/{editorStyles.js → styles.js} +0 -0
  54. package/src/toolbar/HTMLToolbar.ts +172 -22
  55. package/src/toolbar/toolbar.css +12 -0
  56. package/src/toolbar/types.ts +6 -0
  57. package/src/tools/Pen.ts +56 -34
  58. package/src/tools/ToolController.ts +3 -3
  59. package/src/types.ts +16 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
1
 
2
+ # 0.0.4
3
+ * Preset shapes
4
+ * Arrow
5
+ * Filled rectangle
6
+ * Outlined rectangle
7
+ * Line
8
+
2
9
  # 0.0.2
3
10
  * Adjust default editor colors based on system theme.
@@ -28,7 +28,7 @@ export declare class Editor {
28
28
  showLoadingWarning(fractionLoaded: number): void;
29
29
  hideLoadingWarning(): void;
30
30
  announceForAccessibility(message: string): void;
31
- addToolbar(): HTMLToolbar;
31
+ addToolbar(defaultLayout?: boolean): HTMLToolbar;
32
32
  private registerListeners;
33
33
  dispatch(command: Command, addToHistory?: boolean): void;
34
34
  private asyncApplyOrUnapplyCommands;
@@ -80,8 +80,13 @@ export class Editor {
80
80
  announceForAccessibility(message) {
81
81
  this.accessibilityAnnounceArea.innerText = message;
82
82
  }
83
- addToolbar() {
84
- return new HTMLToolbar(this, this.container, this.localization);
83
+ addToolbar(defaultLayout = true) {
84
+ const toolbar = new HTMLToolbar(this, this.container, this.localization);
85
+ if (defaultLayout) {
86
+ toolbar.addDefaultToolWidgets();
87
+ toolbar.addDefaultActionButtons();
88
+ }
89
+ return toolbar;
85
90
  }
86
91
  registerListeners() {
87
92
  const pointers = {};
@@ -30,6 +30,8 @@ export declare class ImageNode {
30
30
  private bbox;
31
31
  private children;
32
32
  private targetChildCount;
33
+ private minZIndex;
34
+ private maxZIndex;
33
35
  constructor(parent?: ImageNode | null);
34
36
  getContent(): AbstractComponent | null;
35
37
  getParent(): ImageNode | null;
@@ -95,6 +95,8 @@ export class ImageNode {
95
95
  this.children = [];
96
96
  this.bbox = Rect2.empty;
97
97
  this.content = null;
98
+ this.minZIndex = null;
99
+ this.maxZIndex = null;
98
100
  }
99
101
  getContent() {
100
102
  return this.content;
@@ -107,7 +109,7 @@ export class ImageNode {
107
109
  return child.getBBox().intersects(region);
108
110
  });
109
111
  }
110
- // / Returns a list of `ImageNode`s with content (and thus no children).
112
+ // Returns a list of `ImageNode`s with content (and thus no children).
111
113
  getLeavesInRegion(region, minFractionOfRegion = 0) {
112
114
  const result = [];
113
115
  // Don't render if too small
@@ -181,21 +183,41 @@ export class ImageNode {
181
183
  return this.bbox;
182
184
  }
183
185
  // Recomputes this' bounding box. If [bubbleUp], also recompute
184
- // this' ancestors bounding boxes
186
+ // this' ancestors bounding boxes. This also re-computes this' bounding box
187
+ // in the z-direction (z-indicies).
185
188
  recomputeBBox(bubbleUp) {
186
- var _a;
189
+ var _a, _b, _c;
187
190
  const oldBBox = this.bbox;
188
191
  if (this.content !== null) {
189
192
  this.bbox = this.content.getBBox();
193
+ this.minZIndex = this.content.zIndex;
194
+ this.maxZIndex = this.content.zIndex;
190
195
  }
191
196
  else {
192
197
  this.bbox = Rect2.empty;
198
+ this.minZIndex = null;
199
+ this.maxZIndex = null;
200
+ let isFirst = true;
193
201
  for (const child of this.children) {
194
- this.bbox = this.bbox.union(child.getBBox());
202
+ if (isFirst) {
203
+ this.bbox = child.getBBox();
204
+ isFirst = false;
205
+ }
206
+ else {
207
+ this.bbox = this.bbox.union(child.getBBox());
208
+ }
209
+ (_a = this.minZIndex) !== null && _a !== void 0 ? _a : (this.minZIndex = child.minZIndex);
210
+ (_b = this.maxZIndex) !== null && _b !== void 0 ? _b : (this.maxZIndex = child.maxZIndex);
211
+ if (child.minZIndex !== null && this.minZIndex !== null) {
212
+ this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
213
+ }
214
+ if (child.maxZIndex !== null && this.maxZIndex !== null) {
215
+ this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
216
+ }
195
217
  }
196
218
  }
197
219
  if (bubbleUp && !oldBBox.eq(this.bbox)) {
198
- (_a = this.parent) === null || _a === void 0 ? void 0 : _a.recomputeBBox(true);
220
+ (_c = this.parent) === null || _c === void 0 ? void 0 : _c.recomputeBBox(true);
199
221
  }
200
222
  }
201
223
  rebalance() {
@@ -221,6 +243,8 @@ export class ImageNode {
221
243
  }
222
244
  // Remove this node and all of its children
223
245
  remove() {
246
+ this.minZIndex = null;
247
+ this.maxZIndex = null;
224
248
  if (!this.parent) {
225
249
  this.content = null;
226
250
  this.children = [];
@@ -44,7 +44,7 @@ export default class Pointer {
44
44
  device = PointerDevice.Eraser;
45
45
  }
46
46
  const timeStamp = (new Date()).getTime();
47
- const canvasPos = viewport.screenToCanvas(screenPos);
47
+ const canvasPos = viewport.roundPoint(viewport.screenToCanvas(screenPos));
48
48
  return new Pointer(screenPos, canvasPos, (_b = evt.pressure) !== null && _b !== void 0 ? _b : null, evt.isPrimary, isDown, device, evt.pointerId, timeStamp);
49
49
  }
50
50
  // Create a new Pointer from a point on the canvas.
@@ -3,7 +3,7 @@ import Editor from './Editor';
3
3
  import Mat33 from './geometry/Mat33';
4
4
  import Rect2 from './geometry/Rect2';
5
5
  import { Point2, Vec2 } from './geometry/Vec2';
6
- import { StrokeDataPoint } from './StrokeBuilder';
6
+ import { StrokeDataPoint } from './types';
7
7
  import { EditorNotifier } from './types';
8
8
  declare type PointDataType<T extends Point2 | StrokeDataPoint | number> = T extends Point2 ? Point2 : number;
9
9
  export declare class Viewport {
@@ -11,7 +11,7 @@ export default abstract class AbstractComponent {
11
11
  private static zIndexCounter;
12
12
  protected constructor();
13
13
  getBBox(): Rect2;
14
- abstract render(canvas: AbstractRenderer, visibleRect: Rect2): void;
14
+ abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
15
15
  abstract intersects(lineSegment: LineSegment2): boolean;
16
16
  protected abstract applyTransformation(affineTransfm: Mat33): void;
17
17
  transformBy(affineTransfm: Mat33): Command;
@@ -9,7 +9,7 @@ export default class Stroke extends AbstractComponent {
9
9
  protected contentBBox: Rect2;
10
10
  constructor(parts: RenderablePathSpec[]);
11
11
  intersects(line: LineSegment2): boolean;
12
- render(canvas: AbstractRenderer, visibleRect: Rect2): void;
12
+ render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
13
13
  private bboxForPart;
14
14
  protected applyTransformation(affineTransfm: Mat33): void;
15
15
  description(localization: ImageComponentLocalization): string;
@@ -37,7 +37,7 @@ export default class Stroke extends AbstractComponent {
37
37
  canvas.startObject(this.getBBox());
38
38
  for (const part of this.parts) {
39
39
  const bbox = part.bbox;
40
- if (bbox.intersects(visibleRect)) {
40
+ if (!visibleRect || bbox.intersects(visibleRect)) {
41
41
  canvas.drawPath(part);
42
42
  }
43
43
  }
@@ -8,7 +8,7 @@ export default class UnknownSVGObject extends AbstractComponent {
8
8
  private svgObject;
9
9
  protected contentBBox: Rect2;
10
10
  constructor(svgObject: SVGElement);
11
- render(canvas: AbstractRenderer, _visibleRect: Rect2): void;
11
+ render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
12
12
  intersects(lineSegment: LineSegment2): boolean;
13
13
  protected applyTransformation(_affineTransfm: Mat33): void;
14
14
  description(localization: ImageComponentLocalization): string;
@@ -0,0 +1,17 @@
1
+ import Rect2 from '../../geometry/Rect2';
2
+ import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import { StrokeDataPoint } from '../../types';
4
+ import AbstractComponent from '../AbstractComponent';
5
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
6
+ export declare const makeArrowBuilder: ComponentBuilderFactory;
7
+ export default class ArrowBuilder implements ComponentBuilder {
8
+ private readonly startPoint;
9
+ private endPoint;
10
+ constructor(startPoint: StrokeDataPoint);
11
+ private getLineWidth;
12
+ getBBox(): Rect2;
13
+ private buildPreview;
14
+ build(): AbstractComponent;
15
+ preview(renderer: AbstractRenderer): void;
16
+ addPoint(point: StrokeDataPoint): void;
17
+ }
@@ -0,0 +1,83 @@
1
+ import { PathCommandType } from '../../geometry/Path';
2
+ import Stroke from '../Stroke';
3
+ export const makeArrowBuilder = (initialPoint, _viewport) => {
4
+ return new ArrowBuilder(initialPoint);
5
+ };
6
+ export default class ArrowBuilder {
7
+ constructor(startPoint) {
8
+ this.startPoint = startPoint;
9
+ this.endPoint = startPoint;
10
+ }
11
+ getLineWidth() {
12
+ return Math.max(this.endPoint.width, this.startPoint.width);
13
+ }
14
+ getBBox() {
15
+ const preview = this.buildPreview();
16
+ return preview.getBBox();
17
+ }
18
+ buildPreview() {
19
+ const startPoint = this.startPoint.pos;
20
+ const endPoint = this.endPoint.pos;
21
+ const toEnd = endPoint.minus(startPoint).normalized();
22
+ const arrowLength = endPoint.minus(startPoint).length();
23
+ // Ensure that the arrow tip is smaller than the arrow.
24
+ const arrowTipSize = Math.min(this.getLineWidth(), arrowLength / 2);
25
+ const startSize = this.startPoint.width / 2;
26
+ const endSize = this.endPoint.width / 2;
27
+ const arrowTipBase = endPoint.minus(toEnd.times(arrowTipSize));
28
+ // Scaled normal vectors.
29
+ const lineNormal = toEnd.orthog();
30
+ const scaledStartNormal = lineNormal.times(startSize);
31
+ const scaledBaseNormal = lineNormal.times(endSize);
32
+ const preview = new Stroke([
33
+ {
34
+ startPoint: arrowTipBase.minus(scaledBaseNormal),
35
+ commands: [
36
+ // Stem
37
+ {
38
+ kind: PathCommandType.LineTo,
39
+ point: startPoint.minus(scaledStartNormal),
40
+ },
41
+ {
42
+ kind: PathCommandType.LineTo,
43
+ point: startPoint.plus(scaledStartNormal),
44
+ },
45
+ {
46
+ kind: PathCommandType.LineTo,
47
+ point: arrowTipBase.plus(scaledBaseNormal),
48
+ },
49
+ // Head
50
+ {
51
+ kind: PathCommandType.LineTo,
52
+ point: arrowTipBase.plus(lineNormal.times(arrowTipSize).plus(scaledBaseNormal))
53
+ },
54
+ {
55
+ kind: PathCommandType.LineTo,
56
+ point: endPoint.plus(toEnd.times(endSize)),
57
+ },
58
+ {
59
+ kind: PathCommandType.LineTo,
60
+ point: arrowTipBase.plus(lineNormal.times(-arrowTipSize).minus(scaledBaseNormal)),
61
+ },
62
+ {
63
+ kind: PathCommandType.LineTo,
64
+ point: arrowTipBase.minus(scaledBaseNormal),
65
+ },
66
+ ],
67
+ style: {
68
+ fill: this.startPoint.color,
69
+ }
70
+ }
71
+ ]);
72
+ return preview;
73
+ }
74
+ build() {
75
+ return this.buildPreview();
76
+ }
77
+ preview(renderer) {
78
+ this.buildPreview().render(renderer);
79
+ }
80
+ addPoint(point) {
81
+ this.endPoint = point;
82
+ }
83
+ }
@@ -1,15 +1,10 @@
1
- import Color4 from './Color4';
2
- import { RenderablePathSpec } from './rendering/AbstractRenderer';
3
- import { Point2 } from './geometry/Vec2';
4
- import Rect2 from './geometry/Rect2';
5
- import Stroke from './components/Stroke';
6
- export interface StrokeDataPoint {
7
- pos: Point2;
8
- width: number;
9
- time: number;
10
- color: Color4;
11
- }
12
- export default class StrokeBuilder {
1
+ import AbstractRenderer from '../../rendering/AbstractRenderer';
2
+ import Rect2 from '../../geometry/Rect2';
3
+ import Stroke from '../Stroke';
4
+ import { StrokeDataPoint } from '../../types';
5
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
6
+ export declare const makeFreehandLineBuilder: ComponentBuilderFactory;
7
+ export default class FreehandLineBuilder implements ComponentBuilder {
13
8
  private startPoint;
14
9
  private minFitAllowed;
15
10
  private maxFitAllowed;
@@ -25,7 +20,8 @@ export default class StrokeBuilder {
25
20
  constructor(startPoint: StrokeDataPoint, minFitAllowed: number, maxFitAllowed: number);
26
21
  getBBox(): Rect2;
27
22
  private getRenderingStyle;
28
- preview(): RenderablePathSpec[];
23
+ private getPreview;
24
+ preview(renderer: AbstractRenderer): void;
29
25
  build(): Stroke;
30
26
  private roundPoint;
31
27
  private finalizeCurrentCurve;
@@ -1,12 +1,20 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import { Vec2 } from './geometry/Vec2';
3
- import Rect2 from './geometry/Rect2';
4
- import { PathCommandType } from './geometry/Path';
5
- import LineSegment2 from './geometry/LineSegment2';
6
- import Stroke from './components/Stroke';
7
- import Viewport from './Viewport';
2
+ import { Vec2 } from '../../geometry/Vec2';
3
+ import Rect2 from '../../geometry/Rect2';
4
+ import { PathCommandType } from '../../geometry/Path';
5
+ import LineSegment2 from '../../geometry/LineSegment2';
6
+ import Stroke from '../Stroke';
7
+ import Viewport from '../../Viewport';
8
+ export const makeFreehandLineBuilder = (initialPoint, viewport) => {
9
+ // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
10
+ // less than ± 2 px from the curve.
11
+ const canvasTransform = viewport.screenToCanvasTransform;
12
+ const maxSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 7;
13
+ const minSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 2;
14
+ return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist);
15
+ };
8
16
  // Handles stroke smoothing and creates Strokes from user/stylus input.
9
- export default class StrokeBuilder {
17
+ export default class FreehandLineBuilder {
10
18
  constructor(startPoint,
11
19
  // Maximum distance from the actual curve (irrespective of stroke width)
12
20
  // for which a point is considered 'part of the curve'.
@@ -34,13 +42,18 @@ export default class StrokeBuilder {
34
42
  };
35
43
  }
36
44
  // Get the segments that make up this' path. Can be called after calling build()
37
- preview() {
45
+ getPreview() {
38
46
  if (this.currentCurve && this.lastPoint) {
39
47
  const currentPath = this.currentSegmentToPath();
40
48
  return this.segments.concat(currentPath);
41
49
  }
42
50
  return this.segments;
43
51
  }
52
+ preview(renderer) {
53
+ for (const part of this.getPreview()) {
54
+ renderer.drawPath(part);
55
+ }
56
+ }
44
57
  build() {
45
58
  if (this.lastPoint) {
46
59
  this.finalizeCurrentCurve();
@@ -179,7 +192,12 @@ export default class StrokeBuilder {
179
192
  return;
180
193
  }
181
194
  const threshold = Math.min(this.lastPoint.width, newPoint.width) / 4;
182
- if (this.lastPoint.pos.minus(newPoint.pos).magnitude() < threshold) {
195
+ const shouldSnapToInitial = this.startPoint.pos.minus(newPoint.pos).magnitude() < threshold
196
+ && this.segments.length === 0;
197
+ // Snap to the starting point if the stroke is contained within a small ball centered
198
+ // at the starting point.
199
+ // This allows us to create a circle/dot at the start of the stroke.
200
+ if (shouldSnapToInitial) {
183
201
  return;
184
202
  }
185
203
  const velocity = newPoint.pos.minus(this.lastPoint.pos).times(1 / (deltaTime) * 1000);
@@ -0,0 +1,16 @@
1
+ import Rect2 from '../../geometry/Rect2';
2
+ import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import { StrokeDataPoint } from '../../types';
4
+ import AbstractComponent from '../AbstractComponent';
5
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
6
+ export declare const makeLineBuilder: ComponentBuilderFactory;
7
+ export default class LineBuilder implements ComponentBuilder {
8
+ private readonly startPoint;
9
+ private endPoint;
10
+ constructor(startPoint: StrokeDataPoint);
11
+ getBBox(): Rect2;
12
+ private buildPreview;
13
+ build(): AbstractComponent;
14
+ preview(renderer: AbstractRenderer): void;
15
+ addPoint(point: StrokeDataPoint): void;
16
+ }
@@ -0,0 +1,57 @@
1
+ import { PathCommandType } from '../../geometry/Path';
2
+ import Stroke from '../Stroke';
3
+ export const makeLineBuilder = (initialPoint, _viewport) => {
4
+ return new LineBuilder(initialPoint);
5
+ };
6
+ export default class LineBuilder {
7
+ constructor(startPoint) {
8
+ this.startPoint = startPoint;
9
+ this.endPoint = startPoint;
10
+ }
11
+ getBBox() {
12
+ const preview = this.buildPreview();
13
+ return preview.getBBox();
14
+ }
15
+ buildPreview() {
16
+ const startPoint = this.startPoint.pos;
17
+ const endPoint = this.endPoint.pos;
18
+ const toEnd = endPoint.minus(startPoint).normalized();
19
+ const startSize = this.startPoint.width / 2;
20
+ const endSize = this.endPoint.width / 2;
21
+ const lineNormal = toEnd.orthog();
22
+ const scaledStartNormal = lineNormal.times(startSize);
23
+ const scaledEndNormal = lineNormal.times(endSize);
24
+ const preview = new Stroke([
25
+ {
26
+ startPoint: startPoint.minus(scaledStartNormal),
27
+ commands: [
28
+ {
29
+ kind: PathCommandType.LineTo,
30
+ point: startPoint.plus(scaledStartNormal),
31
+ },
32
+ {
33
+ kind: PathCommandType.LineTo,
34
+ point: endPoint.plus(scaledEndNormal),
35
+ },
36
+ {
37
+ kind: PathCommandType.LineTo,
38
+ point: endPoint.minus(scaledEndNormal),
39
+ },
40
+ ],
41
+ style: {
42
+ fill: this.startPoint.color,
43
+ }
44
+ }
45
+ ]);
46
+ return preview;
47
+ }
48
+ build() {
49
+ return this.buildPreview();
50
+ }
51
+ preview(renderer) {
52
+ this.buildPreview().render(renderer);
53
+ }
54
+ addPoint(point) {
55
+ this.endPoint = point;
56
+ }
57
+ }
@@ -0,0 +1,18 @@
1
+ import Rect2 from '../../geometry/Rect2';
2
+ import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import { StrokeDataPoint } from '../../types';
4
+ import AbstractComponent from '../AbstractComponent';
5
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
6
+ export declare const makeFilledRectangleBuilder: ComponentBuilderFactory;
7
+ export declare const makeOutlinedRectangleBuilder: ComponentBuilderFactory;
8
+ export default class RectangleBuilder implements ComponentBuilder {
9
+ private readonly startPoint;
10
+ private filled;
11
+ private endPoint;
12
+ constructor(startPoint: StrokeDataPoint, filled: boolean);
13
+ getBBox(): Rect2;
14
+ private buildPreview;
15
+ build(): AbstractComponent;
16
+ preview(renderer: AbstractRenderer): void;
17
+ addPoint(point: StrokeDataPoint): void;
18
+ }
@@ -0,0 +1,41 @@
1
+ import Path from '../../geometry/Path';
2
+ import Rect2 from '../../geometry/Rect2';
3
+ import Stroke from '../Stroke';
4
+ export const makeFilledRectangleBuilder = (initialPoint, _viewport) => {
5
+ return new RectangleBuilder(initialPoint, true);
6
+ };
7
+ export const makeOutlinedRectangleBuilder = (initialPoint, _viewport) => {
8
+ return new RectangleBuilder(initialPoint, false);
9
+ };
10
+ export default class RectangleBuilder {
11
+ constructor(startPoint, filled) {
12
+ this.startPoint = startPoint;
13
+ this.filled = filled;
14
+ // Initially, the start and end points are the same.
15
+ this.endPoint = startPoint;
16
+ }
17
+ getBBox() {
18
+ const preview = this.buildPreview();
19
+ return preview.getBBox();
20
+ }
21
+ buildPreview() {
22
+ const startPoint = this.startPoint.pos;
23
+ const endPoint = this.endPoint.pos;
24
+ const path = Path.fromRect(Rect2.fromCorners(startPoint, endPoint), this.filled ? null : this.endPoint.width);
25
+ const preview = new Stroke([
26
+ path.toRenderable({
27
+ fill: this.endPoint.color
28
+ }),
29
+ ]);
30
+ return preview;
31
+ }
32
+ build() {
33
+ return this.buildPreview();
34
+ }
35
+ preview(renderer) {
36
+ this.buildPreview().render(renderer);
37
+ }
38
+ addPoint(point) {
39
+ this.endPoint = point;
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ import Rect2 from '../../geometry/Rect2';
2
+ import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import { StrokeDataPoint } from '../../types';
4
+ import Viewport from '../../Viewport';
5
+ import AbstractComponent from '../AbstractComponent';
6
+ export interface ComponentBuilder {
7
+ getBBox(): Rect2;
8
+ build(): AbstractComponent;
9
+ preview(renderer: AbstractRenderer): void;
10
+ addPoint(point: StrokeDataPoint): void;
11
+ }
12
+ export declare type ComponentBuilderFactory = (startPoint: StrokeDataPoint, viewport: Viewport) => ComponentBuilder;
@@ -0,0 +1 @@
1
+ export {};
@@ -46,6 +46,7 @@ export default class Path {
46
46
  intersection(line: LineSegment2): IntersectionResult[];
47
47
  transformedBy(affineTransfm: Mat33): Path;
48
48
  union(other: Path | null): Path;
49
+ static fromRect(rect: Rect2, lineWidth?: number | null): Path;
49
50
  static fromRenderable(renderable: RenderablePathSpec): Path;
50
51
  toRenderable(fill: RenderingStyle): RenderablePathSpec;
51
52
  toString(): string;
@@ -158,6 +158,38 @@ export default class Path {
158
158
  ...other.parts,
159
159
  ]);
160
160
  }
161
+ // Returns a path that outlines [rect]. If [lineWidth] is not given, the resultant path is
162
+ // the outline of [rect]. Otherwise, the resultant path represents a line of width [lineWidth]
163
+ // that traces [rect].
164
+ static fromRect(rect, lineWidth = null) {
165
+ const commands = [];
166
+ let corners;
167
+ let startPoint;
168
+ if (lineWidth !== null) {
169
+ // Vector from the top left corner or bottom right corner to the edge of the
170
+ // stroked region.
171
+ const cornerToEdge = Vec2.of(lineWidth, lineWidth).times(0.5);
172
+ const innerRect = Rect2.fromCorners(rect.topLeft.plus(cornerToEdge), rect.bottomRight.minus(cornerToEdge));
173
+ const outerRect = Rect2.fromCorners(rect.topLeft.minus(cornerToEdge), rect.bottomRight.plus(cornerToEdge));
174
+ corners = [
175
+ innerRect.corners[3],
176
+ ...innerRect.corners,
177
+ ...outerRect.corners.reverse(),
178
+ ];
179
+ startPoint = outerRect.corners[3];
180
+ }
181
+ else {
182
+ corners = rect.corners.slice(1);
183
+ startPoint = rect.corners[0];
184
+ }
185
+ for (const corner of corners) {
186
+ commands.push({
187
+ kind: PathCommandType.LineTo,
188
+ point: corner,
189
+ });
190
+ }
191
+ return new Path(startPoint, commands);
192
+ }
161
193
  static fromRenderable(renderable) {
162
194
  return new Path(renderable.startPoint, renderable.commands);
163
195
  }
@@ -9,6 +9,7 @@ export default class Vec3 {
9
9
  };
10
10
  static of(x: number, y: number, z: number): Vec3;
11
11
  at(idx: number): number;
12
+ length(): number;
12
13
  magnitude(): number;
13
14
  magnitudeSquared(): number;
14
15
  angle(): number;
@@ -18,6 +19,7 @@ export default class Vec3 {
18
19
  minus(v: Vec3): Vec3;
19
20
  dot(other: Vec3): number;
20
21
  cross(other: Vec3): Vec3;
22
+ orthog(): Vec3;
21
23
  extend(distance: number, direction: Vec3): Vec3;
22
24
  lerp(target: Vec3, fractionTo: number): Vec3;
23
25
  zip(other: Vec3, zip: (componentInThis: number, componentInOther: number) => number): Vec3;
@@ -26,6 +26,10 @@ export default class Vec3 {
26
26
  return this.z;
27
27
  throw new Error(`${idx} out of bounds!`);
28
28
  }
29
+ // Alias for this.magnitude
30
+ length() {
31
+ return this.magnitude();
32
+ }
29
33
  magnitude() {
30
34
  return Math.sqrt(this.dot(this));
31
35
  }
@@ -58,6 +62,15 @@ export default class Vec3 {
58
62
  // | x2 y2 z2|
59
63
  return Vec3.of(this.y * other.z - other.y * this.z, other.x * this.z - this.x * other.z, this.x * other.y - other.x * this.y);
60
64
  }
65
+ // Returns a vector orthogonal to this. If this is a Vec2, returns [this] rotated
66
+ // 90 degrees counter-clockwise.
67
+ orthog() {
68
+ // If parallel to the z-axis
69
+ if (this.dot(Vec3.unitX) === 0 && this.dot(Vec3.unitY) === 0) {
70
+ return this.dot(Vec3.unitX) === 0 ? Vec3.unitX : this.cross(Vec3.unitX).normalized();
71
+ }
72
+ return this.cross(Vec3.unitZ.times(-1)).normalized();
73
+ }
61
74
  // Returns this plus a vector of length [distance] in [direction]
62
75
  extend(distance, direction) {
63
76
  return this.plus(direction.normalized().times(distance));