js-draw 0.15.0 → 0.15.2
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.
- package/.github/ISSUE_TEMPLATE/translation.yml +16 -0
- package/CHANGELOG.md +13 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +1 -1
- package/dist/src/Color4.js +5 -1
- package/dist/src/Editor.d.ts +0 -2
- package/dist/src/Editor.js +15 -30
- package/dist/src/EditorImage.d.ts +25 -0
- package/dist/src/EditorImage.js +57 -2
- package/dist/src/EventDispatcher.d.ts +4 -3
- package/dist/src/SVGLoader.d.ts +1 -0
- package/dist/src/SVGLoader.js +15 -1
- package/dist/src/Viewport.d.ts +3 -3
- package/dist/src/Viewport.js +4 -8
- package/dist/src/components/AbstractComponent.d.ts +5 -1
- package/dist/src/components/AbstractComponent.js +22 -8
- package/dist/src/components/ImageBackground.d.ts +41 -0
- package/dist/src/components/ImageBackground.js +132 -0
- package/dist/src/components/ImageComponent.js +2 -0
- package/dist/src/components/builders/ArrowBuilder.d.ts +3 -1
- package/dist/src/components/builders/ArrowBuilder.js +43 -40
- package/dist/src/components/builders/LineBuilder.d.ts +3 -1
- package/dist/src/components/builders/LineBuilder.js +25 -28
- package/dist/src/components/builders/RectangleBuilder.js +1 -1
- package/dist/src/components/lib.d.ts +2 -1
- package/dist/src/components/lib.js +2 -1
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/math/Mat33.js +43 -5
- package/dist/src/math/Path.d.ts +5 -0
- package/dist/src/math/Path.js +80 -28
- package/dist/src/math/Vec3.js +1 -1
- package/dist/src/rendering/Display.js +1 -1
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
- package/dist/src/testing/sendTouchEvent.d.ts +6 -0
- package/dist/src/testing/sendTouchEvent.js +26 -0
- package/dist/src/toolbar/IconProvider.js +1 -2
- package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
- package/dist/src/tools/Eraser.js +5 -2
- package/dist/src/tools/PanZoom.js +12 -0
- package/dist/src/tools/SelectionTool/Selection.js +1 -1
- package/dist/src/tools/SelectionTool/SelectionTool.js +8 -2
- package/package.json +1 -1
- package/src/Color4.test.ts +6 -0
- package/src/Color4.ts +6 -1
- package/src/Editor.ts +15 -36
- package/src/EditorImage.ts +74 -2
- package/src/EventDispatcher.ts +4 -1
- package/src/SVGLoader.ts +12 -1
- package/src/Viewport.ts +4 -7
- package/src/components/AbstractComponent.transformBy.test.ts +22 -0
- package/src/components/AbstractComponent.ts +21 -4
- package/src/components/ImageBackground.ts +167 -0
- package/src/components/ImageComponent.ts +2 -0
- package/src/components/builders/ArrowBuilder.ts +44 -41
- package/src/components/builders/LineBuilder.ts +26 -28
- package/src/components/builders/RectangleBuilder.ts +1 -1
- package/src/components/lib.ts +2 -0
- package/src/components/localization.ts +4 -0
- package/src/math/Mat33.test.ts +20 -1
- package/src/math/Mat33.ts +47 -5
- package/src/math/Path.ts +87 -28
- package/src/math/Vec3.test.ts +4 -0
- package/src/math/Vec3.ts +1 -1
- package/src/rendering/Display.ts +1 -1
- package/src/rendering/renderers/AbstractRenderer.ts +20 -3
- package/src/rendering/renderers/CanvasRenderer.ts +16 -3
- package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
- package/src/rendering/renderers/SVGRenderer.ts +8 -1
- package/src/testing/sendTouchEvent.ts +43 -0
- package/src/toolbar/IconProvider.ts +1 -2
- package/src/toolbar/toolbar.css +7 -0
- package/src/toolbar/widgets/HandToolWidget.ts +1 -1
- package/src/tools/Eraser.test.ts +24 -1
- package/src/tools/Eraser.ts +6 -2
- package/src/tools/PanZoom.test.ts +267 -23
- package/src/tools/PanZoom.ts +15 -1
- package/src/tools/SelectionTool/Selection.ts +1 -1
- package/src/tools/SelectionTool/SelectionTool.ts +8 -1
- package/src/types.ts +1 -0
package/src/Viewport.ts
CHANGED
@@ -6,7 +6,6 @@ import Rect2 from './math/Rect2';
|
|
6
6
|
import { Point2, Vec2 } from './math/Vec2';
|
7
7
|
import Vec3 from './math/Vec3';
|
8
8
|
import { StrokeDataPoint } from './types';
|
9
|
-
import { EditorEventType, EditorNotifier } from './types';
|
10
9
|
|
11
10
|
// Returns the base type of some type of point/number
|
12
11
|
type PointDataType<T extends Point2|StrokeDataPoint|number> = T extends Point2 ? Point2 : number;
|
@@ -15,6 +14,8 @@ export abstract class ViewportTransform extends Command {
|
|
15
14
|
public abstract readonly transform: Mat33;
|
16
15
|
}
|
17
16
|
|
17
|
+
type TransformChangeCallback = (oldTransform: Mat33, newTransform: Mat33)=> void;
|
18
|
+
|
18
19
|
export class Viewport {
|
19
20
|
// Command that translates/scales the viewport.
|
20
21
|
private static ViewportTransform = class extends ViewportTransform {
|
@@ -82,7 +83,7 @@ export class Viewport {
|
|
82
83
|
private screenRect: Rect2;
|
83
84
|
|
84
85
|
// @internal
|
85
|
-
public constructor(private
|
86
|
+
public constructor(private onTransformChangeCallback: TransformChangeCallback) {
|
86
87
|
this.resetTransform(Mat33.identity);
|
87
88
|
this.screenRect = Rect2.empty;
|
88
89
|
}
|
@@ -120,11 +121,7 @@ export class Viewport {
|
|
120
121
|
const oldTransform = this.transform;
|
121
122
|
this.transform = newTransform;
|
122
123
|
this.inverseTransform = newTransform.inverse();
|
123
|
-
this.
|
124
|
-
kind: EditorEventType.ViewportChanged,
|
125
|
-
newTransform,
|
126
|
-
oldTransform,
|
127
|
-
});
|
124
|
+
this.onTransformChangeCallback?.(oldTransform, newTransform);
|
128
125
|
}
|
129
126
|
|
130
127
|
public get screenToCanvasTransform(): Mat33 {
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { Color4, EditorImage, Mat33, Path, Rect2, Vec2 } from '../lib';
|
2
|
+
import createEditor from '../testing/createEditor';
|
3
|
+
import Stroke from './Stroke';
|
4
|
+
|
5
|
+
describe('AbstractComponent.transformBy', () => {
|
6
|
+
it('should restore the component\'s z-index on undo', () => {
|
7
|
+
const editor = createEditor();
|
8
|
+
const component = new Stroke([ Path.fromRect(Rect2.unitSquare).toRenderable({ fill: Color4.red }) ]);
|
9
|
+
EditorImage.addElement(component).apply(editor);
|
10
|
+
|
11
|
+
const origZIndex = component.getZIndex();
|
12
|
+
|
13
|
+
const transformCommand = component.transformBy(Mat33.translation(Vec2.unitX));
|
14
|
+
transformCommand.apply(editor);
|
15
|
+
|
16
|
+
// Should increase the z-index on applying a transform
|
17
|
+
expect(component.getZIndex()).toBeGreaterThan(origZIndex);
|
18
|
+
|
19
|
+
transformCommand.unapply(editor);
|
20
|
+
expect(component.getZIndex()).toBe(origZIndex);
|
21
|
+
});
|
22
|
+
});
|
@@ -37,9 +37,15 @@ export default abstract class AbstractComponent {
|
|
37
37
|
protected constructor(
|
38
38
|
// A unique identifier for the type of component
|
39
39
|
private readonly componentKind: string,
|
40
|
+
initialZIndex?: number,
|
40
41
|
) {
|
41
42
|
this.lastChangedTime = (new Date()).getTime();
|
42
|
-
|
43
|
+
|
44
|
+
if (initialZIndex !== undefined) {
|
45
|
+
this.zIndex = initialZIndex;
|
46
|
+
} else {
|
47
|
+
this.zIndex = AbstractComponent.zIndexCounter++;
|
48
|
+
}
|
43
49
|
|
44
50
|
// Create a unique ID.
|
45
51
|
this.id = `${new Date().getTime()}-${Math.random()}`;
|
@@ -96,6 +102,10 @@ export default abstract class AbstractComponent {
|
|
96
102
|
return this.contentBBox;
|
97
103
|
}
|
98
104
|
|
105
|
+
/** Called when this component is added to the given image. */
|
106
|
+
public onAddToImage(_image: EditorImage): void { }
|
107
|
+
public onRemoveFromImage(): void { }
|
108
|
+
|
99
109
|
public abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
100
110
|
|
101
111
|
/** @return true if `lineSegment` intersects this component. */
|
@@ -138,7 +148,7 @@ export default abstract class AbstractComponent {
|
|
138
148
|
|
139
149
|
// Returns a command that updates this component's z-index.
|
140
150
|
public setZIndex(newZIndex: number): SerializableCommand {
|
141
|
-
return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex);
|
151
|
+
return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex, this.getZIndex());
|
142
152
|
}
|
143
153
|
|
144
154
|
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
@@ -156,7 +166,6 @@ export default abstract class AbstractComponent {
|
|
156
166
|
private static transformElementCommandId = 'transform-element';
|
157
167
|
|
158
168
|
private static TransformElementCommand = class extends UnresolvedSerializableCommand {
|
159
|
-
private origZIndex: number|null = null;
|
160
169
|
private targetZIndex: number;
|
161
170
|
|
162
171
|
// Construct a new TransformElementCommand. `component`, while optional, should
|
@@ -167,6 +176,7 @@ export default abstract class AbstractComponent {
|
|
167
176
|
componentID: string,
|
168
177
|
component?: AbstractComponent,
|
169
178
|
targetZIndex?: number,
|
179
|
+
private origZIndex?: number,
|
170
180
|
) {
|
171
181
|
super(AbstractComponent.transformElementCommandId, componentID, component);
|
172
182
|
this.targetZIndex = targetZIndex ?? AbstractComponent.zIndexCounter++;
|
@@ -175,6 +185,10 @@ export default abstract class AbstractComponent {
|
|
175
185
|
if (this.targetZIndex >= AbstractComponent.zIndexCounter) {
|
176
186
|
AbstractComponent.zIndexCounter = this.targetZIndex + 1;
|
177
187
|
}
|
188
|
+
|
189
|
+
if (component && origZIndex === undefined) {
|
190
|
+
this.origZIndex = component.getZIndex();
|
191
|
+
}
|
178
192
|
}
|
179
193
|
|
180
194
|
protected resolveComponent(image: EditorImage): void {
|
@@ -183,7 +197,7 @@ export default abstract class AbstractComponent {
|
|
183
197
|
}
|
184
198
|
|
185
199
|
super.resolveComponent(image);
|
186
|
-
this.origZIndex
|
200
|
+
this.origZIndex ??= this.component!.getZIndex();
|
187
201
|
}
|
188
202
|
|
189
203
|
private updateTransform(editor: Editor, newTransfm: Mat33) {
|
@@ -233,12 +247,14 @@ export default abstract class AbstractComponent {
|
|
233
247
|
const elem = editor.image.lookupElement(json.id) ?? undefined;
|
234
248
|
const transform = new Mat33(...(json.transfm as Mat33Array));
|
235
249
|
const targetZIndex = json.targetZIndex;
|
250
|
+
const origZIndex = json.origZIndex ?? undefined;
|
236
251
|
|
237
252
|
return new AbstractComponent.TransformElementCommand(
|
238
253
|
transform,
|
239
254
|
json.id,
|
240
255
|
elem,
|
241
256
|
targetZIndex,
|
257
|
+
origZIndex,
|
242
258
|
);
|
243
259
|
});
|
244
260
|
}
|
@@ -248,6 +264,7 @@ export default abstract class AbstractComponent {
|
|
248
264
|
id: this.componentID,
|
249
265
|
transfm: this.affineTransfm.toArray(),
|
250
266
|
targetZIndex: this.targetZIndex,
|
267
|
+
origZIndex: this.origZIndex,
|
251
268
|
};
|
252
269
|
}
|
253
270
|
};
|
@@ -0,0 +1,167 @@
|
|
1
|
+
import Color4 from '../Color4';
|
2
|
+
import Editor from '../Editor';
|
3
|
+
import EditorImage, { EditorImageEventType } from '../EditorImage';
|
4
|
+
import { DispatcherEventListener } from '../EventDispatcher';
|
5
|
+
import SerializableCommand from '../commands/SerializableCommand';
|
6
|
+
import LineSegment2 from '../math/LineSegment2';
|
7
|
+
import Mat33 from '../math/Mat33';
|
8
|
+
import Rect2 from '../math/Rect2';
|
9
|
+
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
10
|
+
import AbstractComponent from './AbstractComponent';
|
11
|
+
import { ImageComponentLocalization } from './localization';
|
12
|
+
import RestyleableComponent, { ComponentStyle, createRestyleComponentCommand } from './RestylableComponent';
|
13
|
+
|
14
|
+
export enum BackgroundType {
|
15
|
+
SolidColor,
|
16
|
+
None,
|
17
|
+
}
|
18
|
+
|
19
|
+
export const imageBackgroundCSSClassName = 'js-draw-image-background';
|
20
|
+
|
21
|
+
// Represents the background of an image in the editor.
|
22
|
+
export default class ImageBackground extends AbstractComponent implements RestyleableComponent {
|
23
|
+
protected contentBBox: Rect2;
|
24
|
+
private viewportSizeChangeListener: DispatcherEventListener|null = null;
|
25
|
+
|
26
|
+
// eslint-disable-next-line @typescript-eslint/prefer-as-const
|
27
|
+
readonly isRestylableComponent: true = true;
|
28
|
+
|
29
|
+
public constructor(
|
30
|
+
private backgroundType: BackgroundType, private mainColor: Color4
|
31
|
+
) {
|
32
|
+
super('image-background', 0);
|
33
|
+
this.contentBBox = Rect2.empty;
|
34
|
+
}
|
35
|
+
|
36
|
+
public getStyle(): ComponentStyle {
|
37
|
+
let color: Color4|undefined = this.mainColor;
|
38
|
+
|
39
|
+
if (this.backgroundType === BackgroundType.None) {
|
40
|
+
color = undefined;
|
41
|
+
}
|
42
|
+
|
43
|
+
return {
|
44
|
+
color,
|
45
|
+
};
|
46
|
+
}
|
47
|
+
|
48
|
+
public updateStyle(style: ComponentStyle): SerializableCommand {
|
49
|
+
return createRestyleComponentCommand(this.getStyle(), style, this);
|
50
|
+
}
|
51
|
+
|
52
|
+
// @internal
|
53
|
+
public forceStyle(style: ComponentStyle, _editor: Editor | null): void {
|
54
|
+
const fill = style.color;
|
55
|
+
|
56
|
+
if (!fill) {
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
|
60
|
+
this.mainColor = fill;
|
61
|
+
if (fill.eq(Color4.transparent)) {
|
62
|
+
this.backgroundType = BackgroundType.None;
|
63
|
+
} else {
|
64
|
+
this.backgroundType = BackgroundType.SolidColor;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
public onAddToImage(image: EditorImage) {
|
69
|
+
if (this.viewportSizeChangeListener) {
|
70
|
+
console.warn('onAddToImage called when background is already in an image');
|
71
|
+
this.onRemoveFromImage();
|
72
|
+
}
|
73
|
+
|
74
|
+
this.viewportSizeChangeListener = image.notifier.on(
|
75
|
+
EditorImageEventType.ExportViewportChanged, () => {
|
76
|
+
this.recomputeBBox(image);
|
77
|
+
});
|
78
|
+
this.recomputeBBox(image);
|
79
|
+
}
|
80
|
+
|
81
|
+
public onRemoveFromImage(): void {
|
82
|
+
this.viewportSizeChangeListener?.remove();
|
83
|
+
this.viewportSizeChangeListener = null;
|
84
|
+
}
|
85
|
+
|
86
|
+
private recomputeBBox(image: EditorImage) {
|
87
|
+
const importExportRect = image.getImportExportViewport().visibleRect;
|
88
|
+
if (!this.contentBBox.eq(importExportRect)) {
|
89
|
+
this.contentBBox = importExportRect;
|
90
|
+
|
91
|
+
// Re-render this if already added to the EditorImage.
|
92
|
+
image.queueRerenderOf(this);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
public render(canvas: AbstractRenderer, visibleRect?: Rect2) {
|
97
|
+
if (this.backgroundType === BackgroundType.None) {
|
98
|
+
return;
|
99
|
+
}
|
100
|
+
canvas.startObject(this.contentBBox);
|
101
|
+
|
102
|
+
if (this.backgroundType === BackgroundType.SolidColor) {
|
103
|
+
// If the rectangle for this region contains the visible rect,
|
104
|
+
// we can fill the entire visible rectangle (which may be more efficient than
|
105
|
+
// filling the entire region for this.)
|
106
|
+
if (visibleRect) {
|
107
|
+
const intersection = visibleRect.intersection(this.contentBBox);
|
108
|
+
if (intersection) {
|
109
|
+
canvas.fillRect(intersection, this.mainColor);
|
110
|
+
}
|
111
|
+
} else {
|
112
|
+
canvas.fillRect(this.contentBBox, this.mainColor);
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
canvas.endObject(this.getLoadSaveData(), [ imageBackgroundCSSClassName ]);
|
117
|
+
}
|
118
|
+
|
119
|
+
public intersects(lineSegment: LineSegment2): boolean {
|
120
|
+
return this.contentBBox.getEdges().some(edge => edge.intersects(lineSegment));
|
121
|
+
}
|
122
|
+
|
123
|
+
public isSelectable(): boolean {
|
124
|
+
return false;
|
125
|
+
}
|
126
|
+
|
127
|
+
protected serializeToJSON() {
|
128
|
+
return {
|
129
|
+
mainColor: this.mainColor.toHexString(),
|
130
|
+
backgroundType: this.backgroundType,
|
131
|
+
};
|
132
|
+
}
|
133
|
+
|
134
|
+
protected applyTransformation(_affineTransfm: Mat33) {
|
135
|
+
// Do nothing — it doesn't make sense to transform the background.
|
136
|
+
}
|
137
|
+
|
138
|
+
public description(localizationTable: ImageComponentLocalization) {
|
139
|
+
if (this.backgroundType === BackgroundType.SolidColor) {
|
140
|
+
return localizationTable.filledBackgroundWithColor(this.mainColor.toString());
|
141
|
+
} else {
|
142
|
+
return localizationTable.emptyBackground;
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
protected createClone(): AbstractComponent {
|
147
|
+
return new ImageBackground(this.backgroundType, this.mainColor);
|
148
|
+
}
|
149
|
+
|
150
|
+
// @internal
|
151
|
+
public static deserializeFromJSON(json: any) {
|
152
|
+
if (typeof json === 'string') {
|
153
|
+
json = JSON.parse(json);
|
154
|
+
}
|
155
|
+
|
156
|
+
if (typeof json.mainColor !== 'string') {
|
157
|
+
throw new Error('Error deserializing — mainColor must be of type string.');
|
158
|
+
}
|
159
|
+
|
160
|
+
const backgroundType = json.backgroundType === BackgroundType.SolidColor ? BackgroundType.SolidColor : BackgroundType.None;
|
161
|
+
const mainColor = Color4.fromHex(json.mainColor);
|
162
|
+
|
163
|
+
return new ImageBackground(backgroundType, mainColor);
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
AbstractComponent.registerComponent('image-background', ImageBackground.deserializeFromJSON);
|
@@ -88,7 +88,9 @@ export default class ImageComponent extends AbstractComponent {
|
|
88
88
|
}
|
89
89
|
|
90
90
|
public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
|
91
|
+
canvas.startObject(this.contentBBox);
|
91
92
|
canvas.drawImage(this.image);
|
93
|
+
canvas.endObject(this.getLoadSaveData());
|
92
94
|
}
|
93
95
|
|
94
96
|
public getProportionalRenderingTime(): number {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { PathCommandType } from '../../math/Path';
|
1
|
+
import Path, { PathCommandType } from '../../math/Path';
|
2
2
|
import Rect2 from '../../math/Rect2';
|
3
3
|
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
4
4
|
import { StrokeDataPoint } from '../../types';
|
@@ -7,14 +7,14 @@ import AbstractComponent from '../AbstractComponent';
|
|
7
7
|
import Stroke from '../Stroke';
|
8
8
|
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
9
9
|
|
10
|
-
export const makeArrowBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint,
|
11
|
-
return new ArrowBuilder(initialPoint);
|
10
|
+
export const makeArrowBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
|
11
|
+
return new ArrowBuilder(initialPoint, viewport);
|
12
12
|
};
|
13
13
|
|
14
14
|
export default class ArrowBuilder implements ComponentBuilder {
|
15
15
|
private endPoint: StrokeDataPoint;
|
16
16
|
|
17
|
-
public constructor(private readonly startPoint: StrokeDataPoint) {
|
17
|
+
public constructor(private readonly startPoint: StrokeDataPoint, private readonly viewport: Viewport) {
|
18
18
|
this.endPoint = startPoint;
|
19
19
|
}
|
20
20
|
|
@@ -28,10 +28,10 @@ export default class ArrowBuilder implements ComponentBuilder {
|
|
28
28
|
}
|
29
29
|
|
30
30
|
private buildPreview(): Stroke {
|
31
|
-
const
|
31
|
+
const lineStartPoint = this.startPoint.pos;
|
32
32
|
const endPoint = this.endPoint.pos;
|
33
|
-
const toEnd = endPoint.minus(
|
34
|
-
const arrowLength = endPoint.minus(
|
33
|
+
const toEnd = endPoint.minus(lineStartPoint).normalized();
|
34
|
+
const arrowLength = endPoint.minus(lineStartPoint).length();
|
35
35
|
|
36
36
|
// Ensure that the arrow tip is smaller than the arrow.
|
37
37
|
const arrowTipSize = Math.min(this.getLineWidth(), arrowLength / 2);
|
@@ -45,42 +45,45 @@ export default class ArrowBuilder implements ComponentBuilder {
|
|
45
45
|
const scaledStartNormal = lineNormal.times(startSize);
|
46
46
|
const scaledBaseNormal = lineNormal.times(endSize);
|
47
47
|
|
48
|
+
const path = new Path(arrowTipBase.minus(scaledBaseNormal), [
|
49
|
+
// Stem
|
50
|
+
{
|
51
|
+
kind: PathCommandType.LineTo,
|
52
|
+
point: lineStartPoint.minus(scaledStartNormal),
|
53
|
+
},
|
54
|
+
{
|
55
|
+
kind: PathCommandType.LineTo,
|
56
|
+
point: lineStartPoint.plus(scaledStartNormal),
|
57
|
+
},
|
58
|
+
{
|
59
|
+
kind: PathCommandType.LineTo,
|
60
|
+
point: arrowTipBase.plus(scaledBaseNormal),
|
61
|
+
},
|
62
|
+
|
63
|
+
// Head
|
64
|
+
{
|
65
|
+
kind: PathCommandType.LineTo,
|
66
|
+
point: arrowTipBase.plus(lineNormal.times(arrowTipSize).plus(scaledBaseNormal)),
|
67
|
+
},
|
68
|
+
{
|
69
|
+
kind: PathCommandType.LineTo,
|
70
|
+
point: endPoint.plus(toEnd.times(endSize)),
|
71
|
+
},
|
72
|
+
{
|
73
|
+
kind: PathCommandType.LineTo,
|
74
|
+
point: arrowTipBase.plus(lineNormal.times(-arrowTipSize).minus(scaledBaseNormal)),
|
75
|
+
},
|
76
|
+
{
|
77
|
+
kind: PathCommandType.LineTo,
|
78
|
+
point: arrowTipBase.minus(scaledBaseNormal),
|
79
|
+
},
|
80
|
+
// Round all points in the arrow (to remove unnecessary decimal places)
|
81
|
+
]).mapPoints(point => this.viewport.roundPoint(point));
|
82
|
+
|
48
83
|
const preview = new Stroke([
|
49
84
|
{
|
50
|
-
startPoint:
|
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
|
-
],
|
85
|
+
startPoint: path.startPoint,
|
86
|
+
commands: path.parts,
|
84
87
|
style: {
|
85
88
|
fill: this.startPoint.color,
|
86
89
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { PathCommandType } from '../../math/Path';
|
1
|
+
import Path, { PathCommandType } from '../../math/Path';
|
2
2
|
import Rect2 from '../../math/Rect2';
|
3
3
|
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
4
4
|
import { StrokeDataPoint } from '../../types';
|
@@ -7,14 +7,14 @@ import AbstractComponent from '../AbstractComponent';
|
|
7
7
|
import Stroke from '../Stroke';
|
8
8
|
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
9
9
|
|
10
|
-
export const makeLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint,
|
11
|
-
return new LineBuilder(initialPoint);
|
10
|
+
export const makeLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
|
11
|
+
return new LineBuilder(initialPoint, viewport);
|
12
12
|
};
|
13
13
|
|
14
14
|
export default class LineBuilder implements ComponentBuilder {
|
15
15
|
private endPoint: StrokeDataPoint;
|
16
16
|
|
17
|
-
public constructor(private readonly startPoint: StrokeDataPoint) {
|
17
|
+
public constructor(private readonly startPoint: StrokeDataPoint, private readonly viewport: Viewport) {
|
18
18
|
this.endPoint = startPoint;
|
19
19
|
}
|
20
20
|
|
@@ -35,31 +35,29 @@ export default class LineBuilder implements ComponentBuilder {
|
|
35
35
|
const scaledStartNormal = lineNormal.times(startSize);
|
36
36
|
const scaledEndNormal = lineNormal.times(endSize);
|
37
37
|
|
38
|
-
const
|
38
|
+
const strokeStartPoint = startPoint.minus(scaledStartNormal);
|
39
|
+
|
40
|
+
const path = new Path(strokeStartPoint, [
|
41
|
+
{
|
42
|
+
kind: PathCommandType.LineTo,
|
43
|
+
point: startPoint.plus(scaledStartNormal),
|
44
|
+
},
|
45
|
+
{
|
46
|
+
kind: PathCommandType.LineTo,
|
47
|
+
point: endPoint.plus(scaledEndNormal),
|
48
|
+
},
|
39
49
|
{
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
kind: PathCommandType.LineTo,
|
52
|
-
point: endPoint.minus(scaledEndNormal),
|
53
|
-
},
|
54
|
-
{
|
55
|
-
kind: PathCommandType.LineTo,
|
56
|
-
point: startPoint.minus(scaledStartNormal),
|
57
|
-
},
|
58
|
-
],
|
59
|
-
style: {
|
60
|
-
fill: this.startPoint.color,
|
61
|
-
}
|
62
|
-
}
|
50
|
+
kind: PathCommandType.LineTo,
|
51
|
+
point: endPoint.minus(scaledEndNormal),
|
52
|
+
},
|
53
|
+
{
|
54
|
+
kind: PathCommandType.LineTo,
|
55
|
+
point: startPoint.minus(scaledStartNormal),
|
56
|
+
},
|
57
|
+
]).mapPoints(point => this.viewport.roundPoint(point));
|
58
|
+
|
59
|
+
const preview = new Stroke([
|
60
|
+
path.toRenderable({ fill: this.startPoint.color })
|
63
61
|
]);
|
64
62
|
|
65
63
|
return preview;
|
@@ -49,7 +49,7 @@ export default class RectangleBuilder implements ComponentBuilder {
|
|
49
49
|
).transformedBy(
|
50
50
|
// Rotate the canvas rectangle so that its rotation matches the screen
|
51
51
|
rotationMat
|
52
|
-
);
|
52
|
+
).mapPoints(point => this.viewport.roundPoint(point));
|
53
53
|
|
54
54
|
const preview = new Stroke([
|
55
55
|
path.toRenderable({
|
package/src/components/lib.ts
CHANGED
@@ -9,6 +9,7 @@ import Stroke from './Stroke';
|
|
9
9
|
import TextComponent from './TextComponent';
|
10
10
|
import ImageComponent from './ImageComponent';
|
11
11
|
import RestyleableComponent, { createRestyleComponentCommand } from './RestylableComponent';
|
12
|
+
import ImageBackground from './ImageBackground';
|
12
13
|
|
13
14
|
export {
|
14
15
|
Stroke,
|
@@ -18,5 +19,6 @@ export {
|
|
18
19
|
|
19
20
|
TextComponent,
|
20
21
|
Stroke as StrokeComponent,
|
22
|
+
ImageBackground as BackgroundComponent,
|
21
23
|
ImageComponent,
|
22
24
|
};
|
@@ -4,6 +4,8 @@ export interface ImageComponentLocalization {
|
|
4
4
|
imageNode: (description: string)=> string;
|
5
5
|
stroke: string;
|
6
6
|
svgObject: string;
|
7
|
+
emptyBackground: string;
|
8
|
+
filledBackgroundWithColor: (color: string)=> string;
|
7
9
|
|
8
10
|
restyledElements: string;
|
9
11
|
}
|
@@ -13,6 +15,8 @@ export const defaultComponentLocalization: ImageComponentLocalization = {
|
|
13
15
|
stroke: 'Stroke',
|
14
16
|
svgObject: 'SVG Object',
|
15
17
|
restyledElements: 'Restyled elements',
|
18
|
+
emptyBackground: 'Empty background',
|
19
|
+
filledBackgroundWithColor: (color) => `Filled background (${color})`,
|
16
20
|
text: (text) => `Text object: ${text}`,
|
17
21
|
imageNode: (description: string) => `Image: ${description}`,
|
18
22
|
};
|
package/src/math/Mat33.test.ts
CHANGED
@@ -72,7 +72,7 @@ describe('Mat33 tests', () => {
|
|
72
72
|
});
|
73
73
|
|
74
74
|
it('90 degree z-rotation matricies should rotate 90 degrees counter clockwise', () => {
|
75
|
-
const fuzz = 0.
|
75
|
+
const fuzz = 0.001;
|
76
76
|
|
77
77
|
const M = Mat33.zRotation(Math.PI / 2);
|
78
78
|
const rotated = M.transformVec2(Vec2.unitX);
|
@@ -80,6 +80,18 @@ describe('Mat33 tests', () => {
|
|
80
80
|
expect(M.transformVec2(rotated)).objEq(Vec2.unitX.times(-1), fuzz);
|
81
81
|
});
|
82
82
|
|
83
|
+
it('z-rotation matricies should preserve the given origin', () => {
|
84
|
+
const testPairs: Array<[number, Vec2]> = [
|
85
|
+
[ Math.PI / 2, Vec2.zero ],
|
86
|
+
[ -Math.PI / 2, Vec2.zero ],
|
87
|
+
[ -Math.PI / 2, Vec2.of(10, 10) ],
|
88
|
+
];
|
89
|
+
|
90
|
+
for (const [ angle, center ] of testPairs) {
|
91
|
+
expect(Mat33.zRotation(angle, center).transformVec2(center)).objEq(center);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
|
83
95
|
it('translation matricies should translate Vec2s', () => {
|
84
96
|
const fuzz = 0.01;
|
85
97
|
|
@@ -140,6 +152,13 @@ describe('Mat33 tests', () => {
|
|
140
152
|
).objEq(Vec2.unitX, fuzz);
|
141
153
|
});
|
142
154
|
|
155
|
+
it('z-rotation should preserve given origin', () => {
|
156
|
+
const rotationOrigin = Vec2.of(75.16363373235318, 104.29870408043762);
|
157
|
+
const angle = 6.205048847547065;
|
158
|
+
|
159
|
+
expect(Mat33.zRotation(angle, rotationOrigin).transformVec2(rotationOrigin)).objEq(rotationOrigin);
|
160
|
+
});
|
161
|
+
|
143
162
|
it('should correctly apply a mapping to all components', () => {
|
144
163
|
expect(
|
145
164
|
new Mat33(
|
package/src/math/Mat33.ts
CHANGED
@@ -240,11 +240,49 @@ export default class Mat33 {
|
|
240
240
|
}
|
241
241
|
|
242
242
|
public toString(): string {
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
243
|
+
let result = '';
|
244
|
+
const maxColumnLens = [ 0, 0, 0 ];
|
245
|
+
|
246
|
+
// Determine the longest item in each column so we can pad the others to that
|
247
|
+
// length.
|
248
|
+
for (const row of this.rows) {
|
249
|
+
for (let i = 0; i < 3; i++) {
|
250
|
+
maxColumnLens[i] = Math.max(maxColumnLens[0], `${row.at(i)}`.length);
|
251
|
+
}
|
252
|
+
}
|
253
|
+
|
254
|
+
for (let i = 0; i < 3; i++) {
|
255
|
+
if (i === 0) {
|
256
|
+
result += '⎡ ';
|
257
|
+
} else if (i === 1) {
|
258
|
+
result += '⎢ ';
|
259
|
+
} else {
|
260
|
+
result += '⎣ ';
|
261
|
+
}
|
262
|
+
|
263
|
+
// Add each component of the ith row (after padding it)
|
264
|
+
for (let j = 0; j < 3; j++) {
|
265
|
+
const val = this.rows[i].at(j).toString();
|
266
|
+
|
267
|
+
let padding = '';
|
268
|
+
for (let i = val.length; i < maxColumnLens[j]; i++) {
|
269
|
+
padding += ' ';
|
270
|
+
}
|
271
|
+
|
272
|
+
result += val + ', ' + padding;
|
273
|
+
}
|
274
|
+
|
275
|
+
if (i === 0) {
|
276
|
+
result += ' ⎤';
|
277
|
+
} else if (i === 1) {
|
278
|
+
result += ' ⎥';
|
279
|
+
} else {
|
280
|
+
result += ' ⎦';
|
281
|
+
}
|
282
|
+
result += '\n';
|
283
|
+
}
|
284
|
+
|
285
|
+
return result.trimEnd();
|
248
286
|
}
|
249
287
|
|
250
288
|
/**
|
@@ -302,6 +340,10 @@ export default class Mat33 {
|
|
302
340
|
}
|
303
341
|
|
304
342
|
public static zRotation(radians: number, center: Point2 = Vec2.zero): Mat33 {
|
343
|
+
if (radians === 0) {
|
344
|
+
return Mat33.identity;
|
345
|
+
}
|
346
|
+
|
305
347
|
const cos = Math.cos(radians);
|
306
348
|
const sin = Math.sin(radians);
|
307
349
|
|