js-draw 0.0.1 → 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 (68) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +5 -3
  3. package/dist/src/Editor.d.ts +1 -1
  4. package/dist/src/Editor.js +7 -2
  5. package/dist/src/EditorImage.d.ts +2 -0
  6. package/dist/src/EditorImage.js +29 -5
  7. package/dist/src/Pointer.js +1 -1
  8. package/dist/src/Viewport.d.ts +1 -1
  9. package/dist/src/components/AbstractComponent.d.ts +1 -1
  10. package/dist/src/components/Stroke.d.ts +1 -1
  11. package/dist/src/components/Stroke.js +1 -1
  12. package/dist/src/components/UnknownSVGObject.d.ts +1 -1
  13. package/dist/src/components/builders/ArrowBuilder.d.ts +17 -0
  14. package/dist/src/components/builders/ArrowBuilder.js +83 -0
  15. package/dist/src/{StrokeBuilder.d.ts → components/builders/FreehandLineBuilder.d.ts} +9 -13
  16. package/dist/src/{StrokeBuilder.js → components/builders/FreehandLineBuilder.js} +27 -9
  17. package/dist/src/components/builders/LineBuilder.d.ts +16 -0
  18. package/dist/src/components/builders/LineBuilder.js +57 -0
  19. package/dist/src/components/builders/RectangleBuilder.d.ts +18 -0
  20. package/dist/src/components/builders/RectangleBuilder.js +41 -0
  21. package/dist/src/components/builders/types.d.ts +12 -0
  22. package/dist/{scripts/bundle.d.ts → src/components/builders/types.js} +0 -0
  23. package/dist/src/geometry/Path.d.ts +1 -0
  24. package/dist/src/geometry/Path.js +32 -0
  25. package/dist/src/geometry/Vec3.d.ts +2 -0
  26. package/dist/src/geometry/Vec3.js +13 -0
  27. package/dist/src/rendering/AbstractRenderer.js +3 -25
  28. package/dist/src/toolbar/HTMLToolbar.d.ts +4 -1
  29. package/dist/src/toolbar/HTMLToolbar.js +143 -16
  30. package/dist/src/toolbar/types.d.ts +6 -0
  31. package/dist/src/tools/Pen.d.ts +13 -3
  32. package/dist/src/tools/Pen.js +37 -28
  33. package/dist/src/tools/ToolController.js +3 -3
  34. package/dist/src/types.d.ts +14 -2
  35. package/dist/src/types.js +1 -0
  36. package/package.json +8 -4
  37. package/src/Editor.css +11 -0
  38. package/src/Editor.ts +9 -2
  39. package/src/EditorImage.ts +31 -3
  40. package/src/Pointer.ts +1 -1
  41. package/src/Viewport.ts +1 -1
  42. package/src/components/AbstractComponent.ts +1 -1
  43. package/src/components/Stroke.ts +2 -2
  44. package/src/components/UnknownSVGObject.ts +1 -1
  45. package/src/components/builders/ArrowBuilder.ts +104 -0
  46. package/src/{StrokeBuilder.ts → components/builders/FreehandLineBuilder.ts} +36 -18
  47. package/src/components/builders/LineBuilder.ts +75 -0
  48. package/src/components/builders/RectangleBuilder.ts +59 -0
  49. package/src/components/builders/types.ts +15 -0
  50. package/src/geometry/Path.ts +43 -0
  51. package/src/geometry/Vec2.test.ts +1 -0
  52. package/src/geometry/Vec3.test.ts +14 -0
  53. package/src/geometry/Vec3.ts +16 -0
  54. package/src/rendering/AbstractRenderer.ts +3 -32
  55. package/src/{editorStyles.js → styles.js} +0 -0
  56. package/src/toolbar/HTMLToolbar.ts +172 -22
  57. package/src/toolbar/toolbar.css +12 -0
  58. package/src/toolbar/types.ts +6 -0
  59. package/src/tools/Pen.ts +56 -34
  60. package/src/tools/ToolController.ts +3 -3
  61. package/src/types.ts +16 -1
  62. package/dist/build_tools/BundledFile.d.ts +0 -12
  63. package/dist/build_tools/BundledFile.js +0 -153
  64. package/dist/scripts/bundle.js +0 -19
  65. package/dist/scripts/watchBundle.d.ts +0 -1
  66. package/dist/scripts/watchBundle.js +0 -9
  67. package/dist/src/main.d.ts +0 -3
  68. package/dist/src/main.js +0 -4
@@ -107,6 +107,8 @@ export class ImageNode {
107
107
  private bbox: Rect2;
108
108
  private children: ImageNode[];
109
109
  private targetChildCount: number = 30;
110
+ private minZIndex: number|null;
111
+ private maxZIndex: number|null;
110
112
 
111
113
  public constructor(
112
114
  private parent: ImageNode|null = null
@@ -114,6 +116,9 @@ export class ImageNode {
114
116
  this.children = [];
115
117
  this.bbox = Rect2.empty;
116
118
  this.content = null;
119
+
120
+ this.minZIndex = null;
121
+ this.maxZIndex = null;
117
122
  }
118
123
 
119
124
  public getContent(): AbstractComponent|null {
@@ -130,7 +135,7 @@ export class ImageNode {
130
135
  });
131
136
  }
132
137
 
133
- // / Returns a list of `ImageNode`s with content (and thus no children).
138
+ // Returns a list of `ImageNode`s with content (and thus no children).
134
139
  public getLeavesInRegion(region: Rect2, minFractionOfRegion: number = 0): ImageNode[] {
135
140
  const result: ImageNode[] = [];
136
141
 
@@ -228,16 +233,36 @@ export class ImageNode {
228
233
  }
229
234
 
230
235
  // Recomputes this' bounding box. If [bubbleUp], also recompute
231
- // this' ancestors bounding boxes
236
+ // this' ancestors bounding boxes. This also re-computes this' bounding box
237
+ // in the z-direction (z-indicies).
232
238
  public recomputeBBox(bubbleUp: boolean) {
233
239
  const oldBBox = this.bbox;
234
240
  if (this.content !== null) {
235
241
  this.bbox = this.content.getBBox();
242
+ this.minZIndex = this.content.zIndex;
243
+ this.maxZIndex = this.content.zIndex;
236
244
  } else {
237
245
  this.bbox = Rect2.empty;
246
+ this.minZIndex = null;
247
+ this.maxZIndex = null;
248
+ let isFirst = true;
238
249
 
239
250
  for (const child of this.children) {
240
- this.bbox = this.bbox.union(child.getBBox());
251
+ if (isFirst) {
252
+ this.bbox = child.getBBox();
253
+ isFirst = false;
254
+ } else {
255
+ this.bbox = this.bbox.union(child.getBBox());
256
+ }
257
+
258
+ this.minZIndex ??= child.minZIndex;
259
+ this.maxZIndex ??= child.maxZIndex;
260
+ if (child.minZIndex !== null && this.minZIndex !== null) {
261
+ this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
262
+ }
263
+ if (child.maxZIndex !== null && this.maxZIndex !== null) {
264
+ this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
265
+ }
241
266
  }
242
267
  }
243
268
 
@@ -270,6 +295,9 @@ export class ImageNode {
270
295
 
271
296
  // Remove this node and all of its children
272
297
  public remove() {
298
+ this.minZIndex = null;
299
+ this.maxZIndex = null;
300
+
273
301
  if (!this.parent) {
274
302
  this.content = null;
275
303
  this.children = [];
package/src/Pointer.ts CHANGED
@@ -51,7 +51,7 @@ export default class Pointer {
51
51
  }
52
52
 
53
53
  const timeStamp = (new Date()).getTime();
54
- const canvasPos = viewport.screenToCanvas(screenPos);
54
+ const canvasPos = viewport.roundPoint(viewport.screenToCanvas(screenPos));
55
55
 
56
56
  return new Pointer(
57
57
  screenPos,
package/src/Viewport.ts CHANGED
@@ -5,7 +5,7 @@ import Mat33 from './geometry/Mat33';
5
5
  import Rect2 from './geometry/Rect2';
6
6
  import { Point2, Vec2 } from './geometry/Vec2';
7
7
  import Vec3 from './geometry/Vec3';
8
- import { StrokeDataPoint } from './StrokeBuilder';
8
+ import { StrokeDataPoint } from './types';
9
9
  import { EditorEventType, EditorNotifier } from './types';
10
10
 
11
11
  // Returns the base type of some type of point/number
@@ -23,7 +23,7 @@ export default abstract class AbstractComponent {
23
23
  public getBBox(): Rect2 {
24
24
  return this.contentBBox;
25
25
  }
26
- public abstract render(canvas: AbstractRenderer, visibleRect: Rect2): void;
26
+ public abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
27
27
  public abstract intersects(lineSegment: LineSegment2): boolean;
28
28
 
29
29
  // Private helper for transformBy: Apply the given transformation to all points of this.
@@ -50,11 +50,11 @@ export default class Stroke extends AbstractComponent {
50
50
  return false;
51
51
  }
52
52
 
53
- public render(canvas: AbstractRenderer, visibleRect: Rect2): void {
53
+ public render(canvas: AbstractRenderer, visibleRect?: Rect2): void {
54
54
  canvas.startObject(this.getBBox());
55
55
  for (const part of this.parts) {
56
56
  const bbox = part.bbox;
57
- if (bbox.intersects(visibleRect)) {
57
+ if (!visibleRect || bbox.intersects(visibleRect)) {
58
58
  canvas.drawPath(part);
59
59
  }
60
60
  }
@@ -14,7 +14,7 @@ export default class UnknownSVGObject extends AbstractComponent {
14
14
  this.contentBBox = Rect2.of(svgObject.getBoundingClientRect());
15
15
  }
16
16
 
17
- public render(canvas: AbstractRenderer, _visibleRect: Rect2): void {
17
+ public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
18
18
  if (!(canvas instanceof SVGRenderer)) {
19
19
  // Don't draw unrenderable objects if we can't
20
20
  return;
@@ -0,0 +1,104 @@
1
+ import { PathCommandType } from '../../geometry/Path';
2
+ import Rect2 from '../../geometry/Rect2';
3
+ import AbstractRenderer from '../../rendering/AbstractRenderer';
4
+ import { StrokeDataPoint } from '../../types';
5
+ import Viewport from '../../Viewport';
6
+ import AbstractComponent from '../AbstractComponent';
7
+ import Stroke from '../Stroke';
8
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
9
+
10
+ export const makeArrowBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
11
+ return new ArrowBuilder(initialPoint);
12
+ };
13
+
14
+ export default class ArrowBuilder implements ComponentBuilder {
15
+ private endPoint: StrokeDataPoint;
16
+
17
+ public constructor(private readonly startPoint: StrokeDataPoint) {
18
+ this.endPoint = startPoint;
19
+ }
20
+
21
+ private getLineWidth(): number {
22
+ return Math.max(this.endPoint.width, this.startPoint.width);
23
+ }
24
+
25
+ public getBBox(): Rect2 {
26
+ const preview = this.buildPreview();
27
+ return preview.getBBox();
28
+ }
29
+
30
+ private buildPreview(): Stroke {
31
+ const startPoint = this.startPoint.pos;
32
+ const endPoint = this.endPoint.pos;
33
+ const toEnd = endPoint.minus(startPoint).normalized();
34
+ const arrowLength = endPoint.minus(startPoint).length();
35
+
36
+ // Ensure that the arrow tip is smaller than the arrow.
37
+ const arrowTipSize = Math.min(this.getLineWidth(), arrowLength / 2);
38
+ const startSize = this.startPoint.width / 2;
39
+ const endSize = this.endPoint.width / 2;
40
+
41
+ const arrowTipBase = endPoint.minus(toEnd.times(arrowTipSize));
42
+
43
+ // Scaled normal vectors.
44
+ const lineNormal = toEnd.orthog();
45
+ const scaledStartNormal = lineNormal.times(startSize);
46
+ const scaledBaseNormal = lineNormal.times(endSize);
47
+
48
+ const preview = new Stroke([
49
+ {
50
+ startPoint: arrowTipBase.minus(scaledBaseNormal),
51
+ commands: [
52
+ // Stem
53
+ {
54
+ kind: PathCommandType.LineTo,
55
+ point: startPoint.minus(scaledStartNormal),
56
+ },
57
+ {
58
+ kind: PathCommandType.LineTo,
59
+ point: startPoint.plus(scaledStartNormal),
60
+ },
61
+ {
62
+ kind: PathCommandType.LineTo,
63
+ point: arrowTipBase.plus(scaledBaseNormal),
64
+ },
65
+
66
+ // Head
67
+ {
68
+ kind: PathCommandType.LineTo,
69
+ point: arrowTipBase.plus(lineNormal.times(arrowTipSize).plus(scaledBaseNormal))
70
+ },
71
+ {
72
+ kind: PathCommandType.LineTo,
73
+ point: endPoint.plus(toEnd.times(endSize)),
74
+ },
75
+ {
76
+ kind: PathCommandType.LineTo,
77
+ point: arrowTipBase.plus(lineNormal.times(-arrowTipSize).minus(scaledBaseNormal)),
78
+ },
79
+ {
80
+ kind: PathCommandType.LineTo,
81
+ point: arrowTipBase.minus(scaledBaseNormal),
82
+ },
83
+ ],
84
+ style: {
85
+ fill: this.startPoint.color,
86
+ }
87
+ }
88
+ ]);
89
+
90
+ return preview;
91
+ }
92
+
93
+ public build(): AbstractComponent {
94
+ return this.buildPreview();
95
+ }
96
+
97
+ public preview(renderer: AbstractRenderer): void {
98
+ this.buildPreview().render(renderer);
99
+ }
100
+
101
+ public addPoint(point: StrokeDataPoint): void {
102
+ this.endPoint = point;
103
+ }
104
+ }
@@ -1,22 +1,28 @@
1
- import Color4 from './Color4';
2
1
  import { Bezier } from 'bezier-js';
3
- import { RenderingStyle, RenderablePathSpec } from './rendering/AbstractRenderer';
4
- import { Point2, Vec2 } from './geometry/Vec2';
5
- import Rect2 from './geometry/Rect2';
6
- import { PathCommand, PathCommandType } from './geometry/Path';
7
- import LineSegment2 from './geometry/LineSegment2';
8
- import Stroke from './components/Stroke';
9
- import Viewport from './Viewport';
10
-
11
- export interface StrokeDataPoint {
12
- pos: Point2;
13
- width: number;
14
- time: number;
15
- color: Color4;
16
- }
2
+ import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/AbstractRenderer';
3
+ import { Point2, Vec2 } from '../../geometry/Vec2';
4
+ import Rect2 from '../../geometry/Rect2';
5
+ import { PathCommand, PathCommandType } from '../../geometry/Path';
6
+ import LineSegment2 from '../../geometry/LineSegment2';
7
+ import Stroke from '../Stroke';
8
+ import Viewport from '../../Viewport';
9
+ import { StrokeDataPoint } from '../../types';
10
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
11
+
12
+ export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
13
+ // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
14
+ // less than ± 2 px from the curve.
15
+ const canvasTransform = viewport.screenToCanvasTransform;
16
+ const maxSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 7;
17
+ const minSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 2;
18
+
19
+ return new FreehandLineBuilder(
20
+ initialPoint, minSmoothingDist, maxSmoothingDist
21
+ );
22
+ };
17
23
 
18
24
  // Handles stroke smoothing and creates Strokes from user/stylus input.
19
- export default class StrokeBuilder {
25
+ export default class FreehandLineBuilder implements ComponentBuilder {
20
26
  private segments: RenderablePathSpec[];
21
27
  private buffer: Point2[];
22
28
  private lastPoint: StrokeDataPoint;
@@ -59,7 +65,7 @@ export default class StrokeBuilder {
59
65
  }
60
66
 
61
67
  // Get the segments that make up this' path. Can be called after calling build()
62
- public preview(): RenderablePathSpec[] {
68
+ private getPreview(): RenderablePathSpec[] {
63
69
  if (this.currentCurve && this.lastPoint) {
64
70
  const currentPath = this.currentSegmentToPath();
65
71
  return this.segments.concat(currentPath);
@@ -68,6 +74,12 @@ export default class StrokeBuilder {
68
74
  return this.segments;
69
75
  }
70
76
 
77
+ public preview(renderer: AbstractRenderer) {
78
+ for (const part of this.getPreview()) {
79
+ renderer.drawPath(part);
80
+ }
81
+ }
82
+
71
83
  public build(): Stroke {
72
84
  if (this.lastPoint) {
73
85
  this.finalizeCurrentCurve();
@@ -231,7 +243,13 @@ export default class StrokeBuilder {
231
243
  }
232
244
 
233
245
  const threshold = Math.min(this.lastPoint.width, newPoint.width) / 4;
234
- if (this.lastPoint.pos.minus(newPoint.pos).magnitude() < threshold) {
246
+ const shouldSnapToInitial = this.startPoint.pos.minus(newPoint.pos).magnitude() < threshold
247
+ && this.segments.length === 0;
248
+
249
+ // Snap to the starting point if the stroke is contained within a small ball centered
250
+ // at the starting point.
251
+ // This allows us to create a circle/dot at the start of the stroke.
252
+ if (shouldSnapToInitial) {
235
253
  return;
236
254
  }
237
255
 
@@ -0,0 +1,75 @@
1
+ import { PathCommandType } from '../../geometry/Path';
2
+ import Rect2 from '../../geometry/Rect2';
3
+ import AbstractRenderer from '../../rendering/AbstractRenderer';
4
+ import { StrokeDataPoint } from '../../types';
5
+ import Viewport from '../../Viewport';
6
+ import AbstractComponent from '../AbstractComponent';
7
+ import Stroke from '../Stroke';
8
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
9
+
10
+ export const makeLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
11
+ return new LineBuilder(initialPoint);
12
+ };
13
+
14
+ export default class LineBuilder implements ComponentBuilder {
15
+ private endPoint: StrokeDataPoint;
16
+
17
+ public constructor(private readonly startPoint: StrokeDataPoint) {
18
+ this.endPoint = startPoint;
19
+ }
20
+
21
+ public getBBox(): Rect2 {
22
+ const preview = this.buildPreview();
23
+ return preview.getBBox();
24
+ }
25
+
26
+ private buildPreview(): Stroke {
27
+ const startPoint = this.startPoint.pos;
28
+ const endPoint = this.endPoint.pos;
29
+ const toEnd = endPoint.minus(startPoint).normalized();
30
+
31
+ const startSize = this.startPoint.width / 2;
32
+ const endSize = this.endPoint.width / 2;
33
+
34
+ const lineNormal = toEnd.orthog();
35
+ const scaledStartNormal = lineNormal.times(startSize);
36
+ const scaledEndNormal = lineNormal.times(endSize);
37
+
38
+ const preview = new Stroke([
39
+ {
40
+ startPoint: startPoint.minus(scaledStartNormal),
41
+ commands: [
42
+ {
43
+ kind: PathCommandType.LineTo,
44
+ point: startPoint.plus(scaledStartNormal),
45
+ },
46
+ {
47
+ kind: PathCommandType.LineTo,
48
+ point: endPoint.plus(scaledEndNormal),
49
+ },
50
+ {
51
+ kind: PathCommandType.LineTo,
52
+ point: endPoint.minus(scaledEndNormal),
53
+ },
54
+ ],
55
+ style: {
56
+ fill: this.startPoint.color,
57
+ }
58
+ }
59
+ ]);
60
+
61
+ return preview;
62
+ }
63
+
64
+ public build(): AbstractComponent {
65
+ return this.buildPreview();
66
+ }
67
+
68
+ public preview(renderer: AbstractRenderer): void {
69
+ this.buildPreview().render(renderer);
70
+ }
71
+
72
+ public addPoint(point: StrokeDataPoint): void {
73
+ this.endPoint = point;
74
+ }
75
+ }
@@ -0,0 +1,59 @@
1
+ import Path from '../../geometry/Path';
2
+ import Rect2 from '../../geometry/Rect2';
3
+ import AbstractRenderer from '../../rendering/AbstractRenderer';
4
+ import { StrokeDataPoint } from '../../types';
5
+ import Viewport from '../../Viewport';
6
+ import AbstractComponent from '../AbstractComponent';
7
+ import Stroke from '../Stroke';
8
+ import { ComponentBuilder, ComponentBuilderFactory } from './types';
9
+
10
+ export const makeFilledRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
11
+ return new RectangleBuilder(initialPoint, true);
12
+ };
13
+
14
+ export const makeOutlinedRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
15
+ return new RectangleBuilder(initialPoint, false);
16
+ };
17
+
18
+ export default class RectangleBuilder implements ComponentBuilder {
19
+ private endPoint: StrokeDataPoint;
20
+
21
+ public constructor(private readonly startPoint: StrokeDataPoint, private filled: boolean) {
22
+ // Initially, the start and end points are the same.
23
+ this.endPoint = startPoint;
24
+ }
25
+
26
+ public getBBox(): Rect2 {
27
+ const preview = this.buildPreview();
28
+ return preview.getBBox();
29
+ }
30
+
31
+ private buildPreview(): Stroke {
32
+ const startPoint = this.startPoint.pos;
33
+ const endPoint = this.endPoint.pos;
34
+ const path = Path.fromRect(
35
+ Rect2.fromCorners(startPoint, endPoint),
36
+ this.filled ? null : this.endPoint.width,
37
+ );
38
+
39
+ const preview = new Stroke([
40
+ path.toRenderable({
41
+ fill: this.endPoint.color
42
+ }),
43
+ ]);
44
+
45
+ return preview;
46
+ }
47
+
48
+ public build(): AbstractComponent {
49
+ return this.buildPreview();
50
+ }
51
+
52
+ public preview(renderer: AbstractRenderer): void {
53
+ this.buildPreview().render(renderer);
54
+ }
55
+
56
+ public addPoint(point: StrokeDataPoint): void {
57
+ this.endPoint = point;
58
+ }
59
+ }
@@ -0,0 +1,15 @@
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
+
7
+ export interface ComponentBuilder {
8
+ getBBox(): Rect2;
9
+ build(): AbstractComponent;
10
+ preview(renderer: AbstractRenderer): void;
11
+
12
+ addPoint(point: StrokeDataPoint): void;
13
+ }
14
+
15
+ export type ComponentBuilderFactory = (startPoint: StrokeDataPoint, viewport: Viewport)=> ComponentBuilder;
@@ -223,6 +223,49 @@ export default class Path {
223
223
  ]);
224
224
  }
225
225
 
226
+ // Returns a path that outlines [rect]. If [lineWidth] is not given, the resultant path is
227
+ // the outline of [rect]. Otherwise, the resultant path represents a line of width [lineWidth]
228
+ // that traces [rect].
229
+ public static fromRect(rect: Rect2, lineWidth: number|null = null): Path {
230
+ const commands: PathCommand[] = [];
231
+
232
+ let corners;
233
+ let startPoint;
234
+
235
+ if (lineWidth !== null) {
236
+ // Vector from the top left corner or bottom right corner to the edge of the
237
+ // stroked region.
238
+ const cornerToEdge = Vec2.of(lineWidth, lineWidth).times(0.5);
239
+ const innerRect = Rect2.fromCorners(
240
+ rect.topLeft.plus(cornerToEdge),
241
+ rect.bottomRight.minus(cornerToEdge)
242
+ );
243
+ const outerRect = Rect2.fromCorners(
244
+ rect.topLeft.minus(cornerToEdge),
245
+ rect.bottomRight.plus(cornerToEdge)
246
+ );
247
+
248
+ corners = [
249
+ innerRect.corners[3],
250
+ ...innerRect.corners,
251
+ ...outerRect.corners.reverse(),
252
+ ];
253
+ startPoint = outerRect.corners[3];
254
+ } else {
255
+ corners = rect.corners.slice(1);
256
+ startPoint = rect.corners[0];
257
+ }
258
+
259
+ for (const corner of corners) {
260
+ commands.push({
261
+ kind: PathCommandType.LineTo,
262
+ point: corner,
263
+ });
264
+ }
265
+
266
+ return new Path(startPoint, commands);
267
+ }
268
+
226
269
  public static fromRenderable(renderable: RenderablePathSpec): Path {
227
270
  return new Path(renderable.startPoint, renderable.commands);
228
271
  }
@@ -28,5 +28,6 @@ describe('Vec2', () => {
28
28
  it('Perpindicular', () => {
29
29
  const fuzz = 0.001;
30
30
  expect(Vec2.unitX.cross(Vec3.unitZ)).objEq(Vec2.unitY.times(-1), fuzz);
31
+ expect(Vec2.unitX.orthog()).objEq(Vec2.unitY, fuzz);
31
32
  });
32
33
  });
@@ -26,4 +26,18 @@ describe('Vec3', () => {
26
26
  expect(vec1.cross(vec2)).objEq(Vec3.unitZ);
27
27
  expect(vec2.cross(vec1)).objEq(Vec3.unitZ.times(-1));
28
28
  });
29
+
30
+ it('.orthog should return an orthogonal vector', () => {
31
+ expect(Vec3.unitZ.orthog().dot(Vec3.unitZ)).toBe(0);
32
+
33
+ // Should return some orthogonal vector, even if given the zero vector
34
+ expect(Vec3.zero.orthog().dot(Vec3.zero)).toBe(0);
35
+ });
36
+
37
+ it('.orthog should return a unit vector', () => {
38
+ expect(Vec3.zero.orthog().magnitude()).toBe(1);
39
+ expect(Vec3.unitZ.orthog().magnitude()).toBe(1);
40
+ expect(Vec3.unitX.orthog().magnitude()).toBe(1);
41
+ expect(Vec3.unitY.orthog().magnitude()).toBe(1);
42
+ });
29
43
  });
@@ -31,6 +31,11 @@ export default class Vec3 {
31
31
  throw new Error(`${idx} out of bounds!`);
32
32
  }
33
33
 
34
+ // Alias for this.magnitude
35
+ public length(): number {
36
+ return this.magnitude();
37
+ }
38
+
34
39
  public magnitude(): number {
35
40
  return Math.sqrt(this.dot(this));
36
41
  }
@@ -76,6 +81,17 @@ export default class Vec3 {
76
81
  );
77
82
  }
78
83
 
84
+ // Returns a vector orthogonal to this. If this is a Vec2, returns [this] rotated
85
+ // 90 degrees counter-clockwise.
86
+ public orthog(): Vec3 {
87
+ // If parallel to the z-axis
88
+ if (this.dot(Vec3.unitX) === 0 && this.dot(Vec3.unitY) === 0) {
89
+ return this.dot(Vec3.unitX) === 0 ? Vec3.unitX : this.cross(Vec3.unitX).normalized();
90
+ }
91
+
92
+ return this.cross(Vec3.unitZ.times(-1)).normalized();
93
+ }
94
+
79
95
  // Returns this plus a vector of length [distance] in [direction]
80
96
  public extend(distance: number, direction: Vec3): Vec3 {
81
97
  return this.plus(direction.normalized().times(distance));
@@ -1,5 +1,5 @@
1
1
  import Color4 from '../Color4';
2
- import { PathCommand, PathCommandType } from '../geometry/Path';
2
+ import Path, { PathCommand, PathCommandType } from '../geometry/Path';
3
3
  import Rect2 from '../geometry/Rect2';
4
4
  import { Point2, Vec2 } from '../geometry/Vec2';
5
5
  import Viewport from '../Viewport';
@@ -103,37 +103,8 @@ export default abstract class AbstractRenderer {
103
103
 
104
104
  // Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]
105
105
  public drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void {
106
- const commands: PathCommand[] = [];
107
-
108
- // Vector from the top left corner or bottom right corner to the edge of the
109
- // stroked region.
110
- const cornerToEdge = Vec2.of(lineWidth, lineWidth).times(0.5);
111
- const innerRect = Rect2.fromCorners(
112
- rect.topLeft.plus(cornerToEdge),
113
- rect.bottomRight.minus(cornerToEdge)
114
- );
115
- const outerRect = Rect2.fromCorners(
116
- rect.topLeft.minus(cornerToEdge),
117
- rect.bottomRight.plus(cornerToEdge)
118
- );
119
-
120
- const corners = [
121
- innerRect.corners[3],
122
- ...innerRect.corners,
123
- ...outerRect.corners.reverse(),
124
- ];
125
- for (const corner of corners) {
126
- commands.push({
127
- kind: PathCommandType.LineTo,
128
- point: corner,
129
- });
130
- }
131
-
132
- this.drawPath({
133
- startPoint: outerRect.corners[3],
134
- commands,
135
- style: lineFill,
136
- });
106
+ const path = Path.fromRect(rect, lineWidth);
107
+ this.drawPath(path.toRenderable(lineFill));
137
108
  }
138
109
 
139
110
  // Note the start/end of an object with the given bounding box.
File without changes