js-draw 0.3.2 → 0.4.1
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/pull_request_template.md +15 -0
- package/.github/workflows/firebase-hosting-merge.yml +7 -0
- package/.github/workflows/firebase-hosting-pull-request.yml +10 -0
- package/.github/workflows/github-pages.yml +2 -0
- package/CHANGELOG.md +16 -1
- package/README.md +1 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +11 -0
- package/dist/src/Editor.js +107 -77
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +8 -3
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +14 -1
- package/dist/src/components/AbstractComponent.js +1 -0
- package/dist/src/components/ImageComponent.d.ts +2 -2
- package/dist/src/components/Stroke.js +15 -9
- package/dist/src/components/Text.d.ts +1 -1
- package/dist/src/components/Text.js +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +34 -36
- package/dist/src/language/assertions.d.ts +1 -0
- package/dist/src/language/assertions.js +5 -0
- package/dist/src/math/Mat33.d.ts +38 -2
- package/dist/src/math/Mat33.js +30 -1
- package/dist/src/math/Path.d.ts +1 -1
- package/dist/src/math/Path.js +10 -8
- package/dist/src/math/Vec3.d.ts +12 -2
- package/dist/src/math/Vec3.js +16 -1
- package/dist/src/math/rounding.d.ts +1 -0
- package/dist/src/math/rounding.js +13 -6
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
- package/dist/src/testing/beforeEachFile.d.ts +1 -0
- package/dist/src/testing/beforeEachFile.js +3 -0
- package/dist/src/testing/createEditor.d.ts +1 -0
- package/dist/src/testing/createEditor.js +7 -1
- package/dist/src/testing/loadExpectExtensions.d.ts +0 -15
- package/dist/src/toolbar/HTMLToolbar.js +5 -4
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/tools/PasteHandler.js +3 -1
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
- package/dist/src/tools/SelectionTool/Selection.js +337 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +284 -0
- package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
- package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
- package/dist/src/tools/SelectionTool/types.d.ts +9 -0
- package/dist/src/tools/SelectionTool/types.js +11 -0
- package/dist/src/tools/ToolController.js +1 -1
- package/dist/src/tools/lib.d.ts +1 -1
- package/dist/src/tools/lib.js +1 -1
- package/dist/src/types.d.ts +1 -1
- package/jest.config.js +5 -0
- package/package.json +15 -14
- package/src/Editor.css +1 -0
- package/src/Editor.ts +147 -108
- package/src/Pointer.ts +8 -3
- package/src/Viewport.ts +17 -2
- package/src/components/AbstractComponent.ts +4 -6
- package/src/components/ImageComponent.ts +2 -6
- package/src/components/Stroke.test.ts +0 -3
- package/src/components/Stroke.ts +14 -7
- package/src/components/Text.test.ts +0 -3
- package/src/components/Text.ts +4 -8
- package/src/components/builders/FreehandLineBuilder.ts +37 -43
- package/src/language/assertions.ts +6 -0
- package/src/math/LineSegment2.test.ts +8 -10
- package/src/math/Mat33.test.ts +14 -2
- package/src/math/Mat33.ts +43 -2
- package/src/math/Path.toString.test.ts +12 -1
- package/src/math/Path.ts +11 -9
- package/src/math/Rect2.test.ts +0 -3
- package/src/math/Vec2.test.ts +0 -3
- package/src/math/Vec3.test.ts +0 -3
- package/src/math/Vec3.ts +23 -2
- package/src/math/rounding.test.ts +30 -5
- package/src/math/rounding.ts +16 -7
- package/src/rendering/renderers/AbstractRenderer.ts +3 -2
- package/src/testing/beforeEachFile.ts +3 -0
- package/src/testing/createEditor.ts +8 -1
- package/src/testing/global.d.ts +17 -0
- package/src/testing/loadExpectExtensions.ts +0 -15
- package/src/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/toolbar.css +3 -2
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/PasteHandler.ts +4 -1
- package/src/tools/Pen.test.ts +150 -0
- package/src/tools/Pen.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +455 -0
- package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
- package/src/tools/SelectionTool/SelectionTool.css +22 -0
- package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
- package/src/tools/SelectionTool/SelectionTool.ts +344 -0
- package/src/tools/SelectionTool/TransformMode.ts +114 -0
- package/src/tools/SelectionTool/types.ts +11 -0
- package/src/tools/ToolController.ts +1 -1
- package/src/tools/lib.ts +1 -1
- package/src/types.ts +1 -1
- package/tsconfig.json +3 -1
- package/dist/src/tools/SelectionTool.d.ts +0 -65
- package/dist/src/tools/SelectionTool.js +0 -647
- package/src/tools/SelectionTool.ts +0 -797
package/dist/src/Editor.d.ts
CHANGED
@@ -28,6 +28,8 @@ import Display, { RenderingMode } from './rendering/Display';
|
|
28
28
|
import Pointer from './Pointer';
|
29
29
|
import Rect2 from './math/Rect2';
|
30
30
|
import { EditorLocalization } from './localization';
|
31
|
+
declare type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
|
32
|
+
declare type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
|
31
33
|
export interface EditorSettings {
|
32
34
|
/** Defaults to `RenderingMode.CanvasRenderer` */
|
33
35
|
renderingMode: RenderingMode;
|
@@ -140,8 +142,16 @@ export declare class Editor {
|
|
140
142
|
*/
|
141
143
|
addToolbar(defaultLayout?: boolean): HTMLToolbar;
|
142
144
|
private registerListeners;
|
145
|
+
private pointers;
|
146
|
+
private getPointerList;
|
147
|
+
/**
|
148
|
+
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
|
149
|
+
* as the content of the editor.
|
150
|
+
*/
|
151
|
+
handleHTMLPointerEvent(eventType: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel', evt: PointerEvent): boolean;
|
143
152
|
private isEventSink;
|
144
153
|
private handlePaste;
|
154
|
+
handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter): void;
|
145
155
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
146
156
|
handleKeyEventsFrom(elem: HTMLElement): void;
|
147
157
|
/** `apply` a command. `command` will be announced for accessibility. */
|
@@ -183,6 +193,7 @@ export declare class Editor {
|
|
183
193
|
remove: () => void;
|
184
194
|
};
|
185
195
|
addStyleSheet(content: string): HTMLStyleElement;
|
196
|
+
sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean): void;
|
186
197
|
sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
|
187
198
|
toSVG(): SVGElement;
|
188
199
|
loadFrom(loader: ImageLoader): Promise<void>;
|
package/dist/src/Editor.js
CHANGED
@@ -70,6 +70,7 @@ export class Editor {
|
|
70
70
|
var _a, _b, _c, _d;
|
71
71
|
this.eventListenerTargets = [];
|
72
72
|
this.previousAccessibilityAnnouncement = '';
|
73
|
+
this.pointers = {};
|
73
74
|
this.announceUndoCallback = (command) => {
|
74
75
|
this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this, this.localization)));
|
75
76
|
};
|
@@ -182,81 +183,7 @@ export class Editor {
|
|
182
183
|
return toolbar;
|
183
184
|
}
|
184
185
|
registerListeners() {
|
185
|
-
|
186
|
-
const getPointerList = () => {
|
187
|
-
const nowTime = (new Date()).getTime();
|
188
|
-
const res = [];
|
189
|
-
for (const id in pointers) {
|
190
|
-
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
|
191
|
-
if (pointers[id] && (nowTime - pointers[id].timeStamp) < maxUnupdatedTime) {
|
192
|
-
res.push(pointers[id]);
|
193
|
-
}
|
194
|
-
}
|
195
|
-
return res;
|
196
|
-
};
|
197
|
-
// May be required to prevent text selection on iOS/Safari:
|
198
|
-
// See https://stackoverflow.com/a/70992717/17055750
|
199
|
-
this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
|
200
|
-
this.renderingRegion.addEventListener('contextmenu', evt => {
|
201
|
-
// Don't show a context menu
|
202
|
-
evt.preventDefault();
|
203
|
-
});
|
204
|
-
this.renderingRegion.addEventListener('pointerdown', evt => {
|
205
|
-
const pointer = Pointer.ofEvent(evt, true, this.viewport);
|
206
|
-
pointers[pointer.id] = pointer;
|
207
|
-
this.renderingRegion.setPointerCapture(pointer.id);
|
208
|
-
const event = {
|
209
|
-
kind: InputEvtType.PointerDownEvt,
|
210
|
-
current: pointer,
|
211
|
-
allPointers: getPointerList(),
|
212
|
-
};
|
213
|
-
this.toolController.dispatchInputEvent(event);
|
214
|
-
return true;
|
215
|
-
});
|
216
|
-
this.renderingRegion.addEventListener('pointermove', evt => {
|
217
|
-
var _a, _b;
|
218
|
-
const pointer = Pointer.ofEvent(evt, (_b = (_a = pointers[evt.pointerId]) === null || _a === void 0 ? void 0 : _a.down) !== null && _b !== void 0 ? _b : false, this.viewport);
|
219
|
-
if (pointer.down) {
|
220
|
-
const prevData = pointers[pointer.id];
|
221
|
-
if (prevData) {
|
222
|
-
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
223
|
-
// If the pointer moved less than two pixels, don't send a new event.
|
224
|
-
if (distanceMoved < 2) {
|
225
|
-
return;
|
226
|
-
}
|
227
|
-
}
|
228
|
-
pointers[pointer.id] = pointer;
|
229
|
-
if (this.toolController.dispatchInputEvent({
|
230
|
-
kind: InputEvtType.PointerMoveEvt,
|
231
|
-
current: pointer,
|
232
|
-
allPointers: getPointerList(),
|
233
|
-
})) {
|
234
|
-
evt.preventDefault();
|
235
|
-
}
|
236
|
-
}
|
237
|
-
});
|
238
|
-
const pointerEnd = (evt) => {
|
239
|
-
const pointer = Pointer.ofEvent(evt, false, this.viewport);
|
240
|
-
if (!pointers[pointer.id]) {
|
241
|
-
return;
|
242
|
-
}
|
243
|
-
pointers[pointer.id] = pointer;
|
244
|
-
this.renderingRegion.releasePointerCapture(pointer.id);
|
245
|
-
if (this.toolController.dispatchInputEvent({
|
246
|
-
kind: InputEvtType.PointerUpEvt,
|
247
|
-
current: pointer,
|
248
|
-
allPointers: getPointerList(),
|
249
|
-
})) {
|
250
|
-
evt.preventDefault();
|
251
|
-
}
|
252
|
-
delete pointers[pointer.id];
|
253
|
-
};
|
254
|
-
this.renderingRegion.addEventListener('pointerup', evt => {
|
255
|
-
pointerEnd(evt);
|
256
|
-
});
|
257
|
-
this.renderingRegion.addEventListener('pointercancel', evt => {
|
258
|
-
pointerEnd(evt);
|
259
|
-
});
|
186
|
+
this.handlePointerEventsFrom(this.renderingRegion);
|
260
187
|
this.handleKeyEventsFrom(this.renderingRegion);
|
261
188
|
this.container.addEventListener('wheel', evt => {
|
262
189
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
@@ -282,7 +209,9 @@ export class Editor {
|
|
282
209
|
if (evt.ctrlKey) {
|
283
210
|
delta = Vec3.of(0, 0, evt.deltaY);
|
284
211
|
}
|
285
|
-
|
212
|
+
// Ensure that `pos` is relative to `this.container`
|
213
|
+
const bbox = this.container.getBoundingClientRect();
|
214
|
+
const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
|
286
215
|
if (this.toolController.dispatchInputEvent({
|
287
216
|
kind: InputEvtType.WheelEvt,
|
288
217
|
delta,
|
@@ -324,6 +253,78 @@ export class Editor {
|
|
324
253
|
this.handlePaste(evt);
|
325
254
|
});
|
326
255
|
}
|
256
|
+
getPointerList() {
|
257
|
+
const nowTime = (new Date()).getTime();
|
258
|
+
const res = [];
|
259
|
+
for (const id in this.pointers) {
|
260
|
+
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
|
261
|
+
if (this.pointers[id] && (nowTime - this.pointers[id].timeStamp) < maxUnupdatedTime) {
|
262
|
+
res.push(this.pointers[id]);
|
263
|
+
}
|
264
|
+
}
|
265
|
+
return res;
|
266
|
+
}
|
267
|
+
/**
|
268
|
+
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
|
269
|
+
* as the content of the editor.
|
270
|
+
*/
|
271
|
+
handleHTMLPointerEvent(eventType, evt) {
|
272
|
+
var _a, _b, _c;
|
273
|
+
const eventsRelativeTo = this.renderingRegion;
|
274
|
+
const eventTarget = (_a = evt.target) !== null && _a !== void 0 ? _a : this.renderingRegion;
|
275
|
+
if (eventType === 'pointerdown') {
|
276
|
+
const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
|
277
|
+
this.pointers[pointer.id] = pointer;
|
278
|
+
eventTarget.setPointerCapture(pointer.id);
|
279
|
+
const event = {
|
280
|
+
kind: InputEvtType.PointerDownEvt,
|
281
|
+
current: pointer,
|
282
|
+
allPointers: this.getPointerList(),
|
283
|
+
};
|
284
|
+
this.toolController.dispatchInputEvent(event);
|
285
|
+
return true;
|
286
|
+
}
|
287
|
+
else if (eventType === 'pointermove') {
|
288
|
+
const pointer = Pointer.ofEvent(evt, (_c = (_b = this.pointers[evt.pointerId]) === null || _b === void 0 ? void 0 : _b.down) !== null && _c !== void 0 ? _c : false, this.viewport, eventsRelativeTo);
|
289
|
+
if (pointer.down) {
|
290
|
+
const prevData = this.pointers[pointer.id];
|
291
|
+
if (prevData) {
|
292
|
+
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
293
|
+
// If the pointer moved less than two pixels, don't send a new event.
|
294
|
+
if (distanceMoved < 2) {
|
295
|
+
return false;
|
296
|
+
}
|
297
|
+
}
|
298
|
+
this.pointers[pointer.id] = pointer;
|
299
|
+
if (this.toolController.dispatchInputEvent({
|
300
|
+
kind: InputEvtType.PointerMoveEvt,
|
301
|
+
current: pointer,
|
302
|
+
allPointers: this.getPointerList(),
|
303
|
+
})) {
|
304
|
+
evt.preventDefault();
|
305
|
+
}
|
306
|
+
}
|
307
|
+
return true;
|
308
|
+
}
|
309
|
+
else if (eventType === 'pointercancel' || eventType === 'pointerup') {
|
310
|
+
const pointer = Pointer.ofEvent(evt, false, this.viewport, eventsRelativeTo);
|
311
|
+
if (!this.pointers[pointer.id]) {
|
312
|
+
return false;
|
313
|
+
}
|
314
|
+
this.pointers[pointer.id] = pointer;
|
315
|
+
eventTarget.releasePointerCapture(pointer.id);
|
316
|
+
if (this.toolController.dispatchInputEvent({
|
317
|
+
kind: InputEvtType.PointerUpEvt,
|
318
|
+
current: pointer,
|
319
|
+
allPointers: this.getPointerList(),
|
320
|
+
})) {
|
321
|
+
evt.preventDefault();
|
322
|
+
}
|
323
|
+
delete this.pointers[pointer.id];
|
324
|
+
return true;
|
325
|
+
}
|
326
|
+
return eventType;
|
327
|
+
}
|
327
328
|
isEventSink(evtTarget) {
|
328
329
|
let currentElem = evtTarget;
|
329
330
|
while (currentElem !== null) {
|
@@ -411,6 +412,24 @@ export class Editor {
|
|
411
412
|
}
|
412
413
|
});
|
413
414
|
}
|
415
|
+
handlePointerEventsFrom(elem, filter) {
|
416
|
+
// May be required to prevent text selection on iOS/Safari:
|
417
|
+
// See https://stackoverflow.com/a/70992717/17055750
|
418
|
+
elem.addEventListener('touchstart', evt => evt.preventDefault());
|
419
|
+
elem.addEventListener('contextmenu', evt => {
|
420
|
+
// Don't show a context menu
|
421
|
+
evt.preventDefault();
|
422
|
+
});
|
423
|
+
const eventNames = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
|
424
|
+
for (const eventName of eventNames) {
|
425
|
+
elem.addEventListener(eventName, evt => {
|
426
|
+
if (filter && !filter(eventName, evt)) {
|
427
|
+
return true;
|
428
|
+
}
|
429
|
+
return this.handleHTMLPointerEvent(eventName, evt);
|
430
|
+
});
|
431
|
+
}
|
432
|
+
}
|
414
433
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
415
434
|
handleKeyEventsFrom(elem) {
|
416
435
|
elem.addEventListener('keydown', evt => {
|
@@ -577,9 +596,20 @@ export class Editor {
|
|
577
596
|
this.container.appendChild(styleSheet);
|
578
597
|
return styleSheet;
|
579
598
|
}
|
599
|
+
// Dispatch a keyboard event to the currently selected tool.
|
600
|
+
// Intended for unit testing
|
601
|
+
sendKeyboardEvent(eventType, key, ctrlKey = false) {
|
602
|
+
this.toolController.dispatchInputEvent({
|
603
|
+
kind: eventType,
|
604
|
+
key,
|
605
|
+
ctrlKey
|
606
|
+
});
|
607
|
+
}
|
580
608
|
// Dispatch a pen event to the currently selected tool.
|
581
609
|
// Intended primarially for unit tests.
|
582
|
-
sendPenEvent(eventType, point,
|
610
|
+
sendPenEvent(eventType, point,
|
611
|
+
// @deprecated
|
612
|
+
allPointers) {
|
583
613
|
const mainPointer = Pointer.ofCanvasPoint(point, eventType !== InputEvtType.PointerUpEvt, this.viewport);
|
584
614
|
this.toolController.dispatchInputEvent({
|
585
615
|
kind: eventType,
|
package/dist/src/Pointer.d.ts
CHANGED
@@ -18,6 +18,6 @@ export default class Pointer {
|
|
18
18
|
readonly id: number;
|
19
19
|
readonly timeStamp: number;
|
20
20
|
private constructor();
|
21
|
-
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer;
|
21
|
+
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer;
|
22
22
|
static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
|
23
23
|
}
|
package/dist/src/Pointer.js
CHANGED
@@ -31,10 +31,15 @@ export default class Pointer {
|
|
31
31
|
this.id = id;
|
32
32
|
this.timeStamp = timeStamp;
|
33
33
|
}
|
34
|
-
// Creates a Pointer from a DOM event.
|
35
|
-
|
34
|
+
// Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
|
35
|
+
// considered the top left of `relativeTo`.
|
36
|
+
static ofEvent(evt, isDown, viewport, relativeTo) {
|
36
37
|
var _a, _b;
|
37
|
-
|
38
|
+
let screenPos = Vec2.of(evt.clientX, evt.clientY);
|
39
|
+
if (relativeTo) {
|
40
|
+
const bbox = relativeTo.getBoundingClientRect();
|
41
|
+
screenPos = screenPos.minus(Vec2.of(bbox.left, bbox.top));
|
42
|
+
}
|
38
43
|
const pointerTypeToDevice = {
|
39
44
|
'mouse': PointerDevice.PrimaryButtonMouse,
|
40
45
|
'pen': PointerDevice.Pen,
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -29,6 +29,7 @@ export declare class Viewport {
|
|
29
29
|
getRotationAngle(): number;
|
30
30
|
static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
|
31
31
|
roundPoint(point: Point2): Point2;
|
32
|
+
static roundScaleRatio(scaleRatio: number, roundAmount?: number): number;
|
32
33
|
computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Mat33;
|
33
34
|
zoomTo(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Command;
|
34
35
|
}
|
package/dist/src/Viewport.js
CHANGED
@@ -73,7 +73,8 @@ export class Viewport {
|
|
73
73
|
getSizeOfPixelOnCanvas() {
|
74
74
|
return 1 / this.getScaleFactor();
|
75
75
|
}
|
76
|
-
// Returns the angle of the canvas in radians
|
76
|
+
// Returns the angle of the canvas in radians.
|
77
|
+
// This is the angle by which the canvas is rotated relative to the screen.
|
77
78
|
getRotationAngle() {
|
78
79
|
return this.transform.transformVec3(Vec3.unitX).angle();
|
79
80
|
}
|
@@ -94,6 +95,18 @@ export class Viewport {
|
|
94
95
|
roundPoint(point) {
|
95
96
|
return Viewport.roundPoint(point, 1 / this.getScaleFactor());
|
96
97
|
}
|
98
|
+
// `roundAmount`: An integer >= 0, larger numbers cause less rounding. Smaller numbers cause more
|
99
|
+
// (as such `roundAmount = 0` does the most rounding).
|
100
|
+
static roundScaleRatio(scaleRatio, roundAmount = 1) {
|
101
|
+
if (Math.abs(scaleRatio) <= 1e-12) {
|
102
|
+
return 0;
|
103
|
+
}
|
104
|
+
// Represent as k 10ⁿ for some n, k ∈ ℤ.
|
105
|
+
const decimalComponent = Math.pow(10, Math.floor(Math.log10(Math.abs(scaleRatio))));
|
106
|
+
const roundAnountFactor = Math.pow(2, roundAmount);
|
107
|
+
scaleRatio = Math.round(scaleRatio / decimalComponent * roundAnountFactor) / roundAnountFactor * decimalComponent;
|
108
|
+
return scaleRatio;
|
109
|
+
}
|
97
110
|
// Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.
|
98
111
|
computeZoomToTransform(toMakeVisible, allowZoomIn = true, allowZoomOut = true) {
|
99
112
|
let transform = Mat33.identity;
|
@@ -48,6 +48,7 @@ export default class AbstractComponent {
|
|
48
48
|
transformBy(affineTransfm) {
|
49
49
|
return new AbstractComponent.TransformElementCommand(affineTransfm, this);
|
50
50
|
}
|
51
|
+
// Returns a copy of this component.
|
51
52
|
clone() {
|
52
53
|
const clone = this.createClone();
|
53
54
|
for (const attachmentKey in this.loadSaveData) {
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import LineSegment2 from '../math/LineSegment2';
|
2
|
-
import Mat33 from '../math/Mat33';
|
2
|
+
import Mat33, { Mat33Array } from '../math/Mat33';
|
3
3
|
import Rect2 from '../math/Rect2';
|
4
4
|
import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';
|
5
5
|
import AbstractComponent from './AbstractComponent';
|
@@ -18,7 +18,7 @@ export default class ImageComponent extends AbstractComponent {
|
|
18
18
|
label: string | undefined;
|
19
19
|
width: number;
|
20
20
|
height: number;
|
21
|
-
transform:
|
21
|
+
transform: Mat33Array;
|
22
22
|
};
|
23
23
|
protected applyTransformation(affineTransfm: Mat33): void;
|
24
24
|
description(localizationTable: ImageComponentLocalization): string;
|
@@ -6,7 +6,8 @@ export default class Stroke extends AbstractComponent {
|
|
6
6
|
constructor(parts) {
|
7
7
|
var _a;
|
8
8
|
super('stroke');
|
9
|
-
this.parts =
|
9
|
+
this.parts = [];
|
10
|
+
for (const section of parts) {
|
10
11
|
const path = Path.fromRenderable(section);
|
11
12
|
const pathBBox = this.bboxForPart(path.bbox, section.style);
|
12
13
|
if (!this.contentBBox) {
|
@@ -15,14 +16,14 @@ export default class Stroke extends AbstractComponent {
|
|
15
16
|
else {
|
16
17
|
this.contentBBox = this.contentBBox.union(pathBBox);
|
17
18
|
}
|
18
|
-
|
19
|
+
this.parts.push({
|
19
20
|
path,
|
20
21
|
// To implement RenderablePathSpec
|
21
22
|
startPoint: path.startPoint,
|
22
23
|
style: section.style,
|
23
24
|
commands: path.parts,
|
24
|
-
};
|
25
|
-
}
|
25
|
+
});
|
26
|
+
}
|
26
27
|
(_a = this.contentBBox) !== null && _a !== void 0 ? _a : (this.contentBBox = Rect2.empty);
|
27
28
|
}
|
28
29
|
intersects(line) {
|
@@ -80,11 +81,16 @@ export default class Stroke extends AbstractComponent {
|
|
80
81
|
});
|
81
82
|
}
|
82
83
|
getPath() {
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
84
|
+
let result = null;
|
85
|
+
for (const part of this.parts) {
|
86
|
+
if (result) {
|
87
|
+
result = result.union(part.path);
|
88
|
+
}
|
89
|
+
else {
|
90
|
+
result !== null && result !== void 0 ? result : (result = part.path);
|
91
|
+
}
|
92
|
+
}
|
93
|
+
return result !== null && result !== void 0 ? result : Path.empty;
|
88
94
|
}
|
89
95
|
description(localization) {
|
90
96
|
return localization.stroke;
|
@@ -29,7 +29,7 @@ export default class Text extends AbstractComponent {
|
|
29
29
|
intersects(lineSegment: LineSegment2): boolean;
|
30
30
|
protected applyTransformation(affineTransfm: Mat33): void;
|
31
31
|
protected createClone(): AbstractComponent;
|
32
|
-
|
32
|
+
getText(): string;
|
33
33
|
description(localizationTable: ImageComponentLocalization): string;
|
34
34
|
protected serializeToJSON(): Record<string, any>;
|
35
35
|
static deserializeFromString(json: any, getTextDimens?: GetTextDimensCallback): Text;
|
@@ -113,7 +113,7 @@ export default class Text extends AbstractComponent {
|
|
113
113
|
result.push(textObject.getText());
|
114
114
|
}
|
115
115
|
}
|
116
|
-
return result.join('
|
116
|
+
return result.join('\n');
|
117
117
|
}
|
118
118
|
description(localizationTable) {
|
119
119
|
return localizationTable.text(this.getText());
|
@@ -29,6 +29,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
29
29
|
preview(renderer: AbstractRenderer): void;
|
30
30
|
build(): Stroke;
|
31
31
|
private roundPoint;
|
32
|
+
private approxCurrentCurveLength;
|
32
33
|
private finalizeCurrentCurve;
|
33
34
|
private currentSegmentToPath;
|
34
35
|
private computeExitingVec;
|
@@ -117,7 +117,7 @@ export default class FreehandLineBuilder {
|
|
117
117
|
}
|
118
118
|
}
|
119
119
|
build() {
|
120
|
-
if (this.lastPoint) {
|
120
|
+
if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
|
121
121
|
this.finalizeCurrentCurve();
|
122
122
|
}
|
123
123
|
return this.previewStroke();
|
@@ -129,6 +129,18 @@ export default class FreehandLineBuilder {
|
|
129
129
|
}
|
130
130
|
return Viewport.roundPoint(point, minFit);
|
131
131
|
}
|
132
|
+
// Returns the distance between the start, control, and end points of the curve.
|
133
|
+
approxCurrentCurveLength() {
|
134
|
+
if (!this.currentCurve) {
|
135
|
+
return 0;
|
136
|
+
}
|
137
|
+
const startPt = Vec2.ofXY(this.currentCurve.points[0]);
|
138
|
+
const controlPt = Vec2.ofXY(this.currentCurve.points[1]);
|
139
|
+
const endPt = Vec2.ofXY(this.currentCurve.points[2]);
|
140
|
+
const toControlDist = startPt.minus(controlPt).length();
|
141
|
+
const toEndDist = endPt.minus(controlPt).length();
|
142
|
+
return toControlDist + toEndDist;
|
143
|
+
}
|
132
144
|
finalizeCurrentCurve() {
|
133
145
|
// Case where no points have been added
|
134
146
|
if (!this.currentCurve) {
|
@@ -204,10 +216,8 @@ export default class FreehandLineBuilder {
|
|
204
216
|
let endVec = Vec2.ofXY(this.currentCurve.normal(1)).normalized();
|
205
217
|
startVec = startVec.times(this.curveStartWidth / 2);
|
206
218
|
endVec = endVec.times(this.curveEndWidth / 2);
|
207
|
-
if (
|
208
|
-
|
209
|
-
// fix.
|
210
|
-
console.error('startVec is NaN', startVec, endVec, this.currentCurve);
|
219
|
+
if (!isFinite(startVec.magnitude())) {
|
220
|
+
console.error('Warning: startVec is NaN or ∞', startVec, endVec, this.currentCurve);
|
211
221
|
startVec = endVec;
|
212
222
|
}
|
213
223
|
const startPt = Vec2.ofXY(this.currentCurve.get(0));
|
@@ -224,28 +234,18 @@ export default class FreehandLineBuilder {
|
|
224
234
|
}
|
225
235
|
}
|
226
236
|
const halfVecT = projectionT;
|
227
|
-
|
237
|
+
const halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
|
228
238
|
.normalized().times(this.curveStartWidth / 2 * halfVecT
|
229
239
|
+ this.curveEndWidth / 2 * (1 - halfVecT));
|
230
|
-
// Computes a boundary curve. [direction] should be either +1 or -1 (determines the side
|
231
|
-
// of the center curve to place the boundary).
|
232
|
-
const computeBoundaryCurve = (direction, halfVec) => {
|
233
|
-
return new Bezier(startPt.plus(startVec.times(direction)), controlPoint.plus(halfVec.times(direction)), endPt.plus(endVec.times(direction)));
|
234
|
-
};
|
235
|
-
const boundariesIntersect = () => {
|
236
|
-
const upperBoundary = computeBoundaryCurve(1, halfVec);
|
237
|
-
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
238
|
-
return upperBoundary.intersects(lowerBoundary).length > 0;
|
239
|
-
};
|
240
|
-
// If the boundaries have intersections, increasing the half vector's length could fix this.
|
241
|
-
if (boundariesIntersect()) {
|
242
|
-
halfVec = halfVec.times(1.1);
|
243
|
-
}
|
244
240
|
// Each starts at startPt ± startVec
|
241
|
+
const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
|
242
|
+
const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
|
243
|
+
const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
|
244
|
+
const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
|
245
245
|
const lowerCurve = {
|
246
246
|
kind: PathCommandType.QuadraticBezierTo,
|
247
|
-
controlPoint:
|
248
|
-
endPoint:
|
247
|
+
controlPoint: lowerCurveControlPoint,
|
248
|
+
endPoint: lowerCurveEndPoint,
|
249
249
|
};
|
250
250
|
// From the end of the upperCurve to the start of the lowerCurve:
|
251
251
|
const upperToLowerConnector = {
|
@@ -255,11 +255,11 @@ export default class FreehandLineBuilder {
|
|
255
255
|
// From the end of lowerCurve to the start of upperCurve:
|
256
256
|
const lowerToUpperConnector = {
|
257
257
|
kind: PathCommandType.LineTo,
|
258
|
-
point:
|
258
|
+
point: upperCurveStartPoint,
|
259
259
|
};
|
260
260
|
const upperCurve = {
|
261
261
|
kind: PathCommandType.QuadraticBezierTo,
|
262
|
-
controlPoint:
|
262
|
+
controlPoint: upperCurveControlPoint,
|
263
263
|
endPoint: this.roundPoint(startPt.minus(startVec)),
|
264
264
|
};
|
265
265
|
return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
|
@@ -275,7 +275,6 @@ export default class FreehandLineBuilder {
|
|
275
275
|
const fuzzEq = 1e-10;
|
276
276
|
const deltaTime = newPoint.time - this.lastPoint.time;
|
277
277
|
if (newPoint.pos.eq(this.lastPoint.pos, fuzzEq) || deltaTime === 0) {
|
278
|
-
console.warn('Discarding identical point');
|
279
278
|
return;
|
280
279
|
}
|
281
280
|
else if (isNaN(newPoint.pos.magnitude())) {
|
@@ -321,35 +320,35 @@ export default class FreehandLineBuilder {
|
|
321
320
|
}
|
322
321
|
let exitingVec = this.computeExitingVec();
|
323
322
|
// Find the intersection between the entering vector and the exiting vector
|
324
|
-
const maxRelativeLength =
|
323
|
+
const maxRelativeLength = 3;
|
325
324
|
const segmentStart = this.buffer[0];
|
326
325
|
const segmentEnd = newPoint.pos;
|
327
326
|
const startEndDist = segmentEnd.minus(segmentStart).magnitude();
|
328
327
|
const maxControlPointDist = maxRelativeLength * startEndDist;
|
329
328
|
// Exit in cases where we would divide by zero
|
330
|
-
if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 ||
|
329
|
+
if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || !isFinite(exitingVec.magnitude())) {
|
331
330
|
return;
|
332
331
|
}
|
333
|
-
console.assert(
|
332
|
+
console.assert(isFinite(enteringVec.magnitude()), 'Pre-normalized enteringVec has NaN or ∞ magnitude!');
|
334
333
|
enteringVec = enteringVec.normalized();
|
335
334
|
exitingVec = exitingVec.normalized();
|
336
|
-
console.assert(
|
335
|
+
console.assert(isFinite(enteringVec.magnitude()), 'Normalized enteringVec has NaN or ∞ magnitude!');
|
337
336
|
const lineFromStart = new LineSegment2(segmentStart, segmentStart.plus(enteringVec.times(maxControlPointDist)));
|
338
337
|
const lineFromEnd = new LineSegment2(segmentEnd.minus(exitingVec.times(maxControlPointDist)), segmentEnd);
|
339
338
|
const intersection = lineFromEnd.intersection(lineFromStart);
|
340
339
|
// Position the control point at this intersection
|
341
|
-
let controlPoint;
|
340
|
+
let controlPoint = null;
|
342
341
|
if (intersection) {
|
343
342
|
controlPoint = intersection.point;
|
344
343
|
}
|
345
|
-
|
344
|
+
// No intersection or the intersection is one of the end points?
|
345
|
+
if (!controlPoint || segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
|
346
346
|
// Position the control point closer to the first -- the connecting
|
347
347
|
// segment will be roughly a line.
|
348
348
|
controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
|
349
349
|
}
|
350
|
-
|
351
|
-
|
352
|
-
}
|
350
|
+
console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
|
351
|
+
console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
|
353
352
|
const prevCurve = this.currentCurve;
|
354
353
|
this.currentCurve = new Bezier(segmentStart.xy, controlPoint.xy, segmentEnd.xy);
|
355
354
|
if (isNaN(Vec2.ofXY(this.currentCurve.normal(0)).magnitude())) {
|
@@ -369,8 +368,7 @@ export default class FreehandLineBuilder {
|
|
369
368
|
}
|
370
369
|
return true;
|
371
370
|
};
|
372
|
-
|
373
|
-
if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
|
371
|
+
if (this.buffer.length > 3 && this.approxCurrentCurveLength() > this.curveStartWidth) {
|
374
372
|
if (!curveMatchesPoints(this.currentCurve)) {
|
375
373
|
// Use a curve that better fits the points
|
376
374
|
this.currentCurve = prevCurve;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const assertUnreachable: (key: never) => never;
|
package/dist/src/math/Mat33.d.ts
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
import { Point2, Vec2 } from './Vec2';
|
2
2
|
import Vec3 from './Vec3';
|
3
|
+
export declare type Mat33Array = [
|
4
|
+
number,
|
5
|
+
number,
|
6
|
+
number,
|
7
|
+
number,
|
8
|
+
number,
|
9
|
+
number,
|
10
|
+
number,
|
11
|
+
number,
|
12
|
+
number
|
13
|
+
];
|
3
14
|
/**
|
4
15
|
* Represents a three dimensional linear transformation or
|
5
16
|
* a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
|
@@ -68,11 +79,36 @@ export default class Mat33 {
|
|
68
79
|
* ...
|
69
80
|
* ```
|
70
81
|
*/
|
71
|
-
toArray():
|
82
|
+
toArray(): Mat33Array;
|
83
|
+
/**
|
84
|
+
* @example
|
85
|
+
* ```
|
86
|
+
* new Mat33(
|
87
|
+
* 1, 2, 3,
|
88
|
+
* 4, 5, 6,
|
89
|
+
* 7, 8, 9,
|
90
|
+
* ).mapEntries(component => component - 1);
|
91
|
+
* // → ⎡ 0, 1, 2 ⎤
|
92
|
+
* // ⎢ 3, 4, 5 ⎥
|
93
|
+
* // ⎣ 6, 7, 8 ⎦
|
94
|
+
* ```
|
95
|
+
*/
|
96
|
+
mapEntries(mapping: (component: number) => number): Mat33;
|
72
97
|
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
|
73
98
|
static translation(amount: Vec2): Mat33;
|
74
99
|
static zRotation(radians: number, center?: Point2): Mat33;
|
75
100
|
static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
|
76
|
-
/**
|
101
|
+
/** @see {@link !fromCSSMatrix} */
|
102
|
+
toCSSMatrix(): string;
|
103
|
+
/**
|
104
|
+
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
|
105
|
+
*
|
106
|
+
* Note that such a matrix has the form,
|
107
|
+
* ```
|
108
|
+
* ⎡ a c e ⎤
|
109
|
+
* ⎢ b d f ⎥
|
110
|
+
* ⎣ 0 0 1 ⎦
|
111
|
+
* ```
|
112
|
+
*/
|
77
113
|
static fromCSSMatrix(cssString: string): Mat33;
|
78
114
|
}
|