js-draw 0.3.0 → 0.3.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/CHANGELOG.md +7 -0
- package/dist/bundle.js +1 -1
- package/dist/src/components/Stroke.js +11 -6
- package/dist/src/components/builders/FreehandLineBuilder.js +5 -5
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +6 -0
- package/dist/src/math/Path.d.ts +5 -1
- package/dist/src/math/Path.js +89 -7
- package/dist/src/math/Rect2.js +1 -1
- package/dist/src/math/Triangle.d.ts +11 -0
- package/dist/src/math/Triangle.js +19 -0
- package/dist/src/rendering/Display.js +2 -2
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +10 -11
- package/dist/src/rendering/renderers/SVGRenderer.js +27 -68
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
- package/dist/src/toolbar/HTMLToolbar.js +1 -0
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
- package/dist/src/tools/BaseTool.d.ts +1 -0
- package/dist/src/tools/BaseTool.js +6 -0
- package/dist/src/tools/Pen.d.ts +2 -1
- package/dist/src/tools/Pen.js +16 -0
- package/dist/src/tools/ToolController.d.ts +1 -0
- package/dist/src/tools/ToolController.js +9 -2
- package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
- package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +8 -2
- package/dist/src/types.js +1 -0
- package/package.json +2 -2
- package/src/components/Stroke.test.ts +5 -0
- package/src/components/Stroke.ts +13 -7
- package/src/components/builders/FreehandLineBuilder.ts +5 -5
- package/src/math/LineSegment2.ts +8 -0
- package/src/math/Path.test.ts +53 -0
- package/src/math/Path.toString.test.ts +4 -2
- package/src/math/Path.ts +109 -11
- package/src/math/Rect2.ts +1 -1
- package/src/math/Triangle.ts +29 -0
- package/src/rendering/Display.ts +2 -2
- package/src/rendering/renderers/AbstractRenderer.ts +1 -0
- package/src/rendering/renderers/SVGRenderer.ts +30 -84
- package/src/toolbar/HTMLToolbar.ts +1 -1
- package/src/toolbar/types.ts +1 -1
- package/src/toolbar/widgets/BaseWidget.ts +27 -1
- package/src/tools/BaseTool.ts +8 -0
- package/src/tools/Pen.ts +20 -1
- package/src/tools/ToolController.ts +10 -3
- package/src/tools/ToolSwitcherShortcut.ts +34 -0
- package/src/tools/lib.ts +1 -0
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +13 -1
package/dist/src/tools/Pen.d.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
2
|
import Editor from '../Editor';
|
3
3
|
import Pointer from '../Pointer';
|
4
|
-
import { PointerEvt, StrokeDataPoint } from '../types';
|
4
|
+
import { KeyPressEvent, PointerEvt, StrokeDataPoint } from '../types';
|
5
5
|
import BaseTool from './BaseTool';
|
6
6
|
import { ComponentBuilder, ComponentBuilderFactory } from '../components/builders/types';
|
7
7
|
export interface PenStyle {
|
@@ -30,4 +30,5 @@ export default class Pen extends BaseTool {
|
|
30
30
|
getThickness(): number;
|
31
31
|
getColor(): Color4;
|
32
32
|
getStrokeFactory(): ComponentBuilderFactory;
|
33
|
+
onKeyPress({ key }: KeyPressEvent): boolean;
|
33
34
|
}
|
package/dist/src/tools/Pen.js
CHANGED
@@ -121,4 +121,20 @@ export default class Pen extends BaseTool {
|
|
121
121
|
getThickness() { return this.style.thickness; }
|
122
122
|
getColor() { return this.style.color; }
|
123
123
|
getStrokeFactory() { return this.builderFactory; }
|
124
|
+
onKeyPress({ key }) {
|
125
|
+
key = key.toLowerCase();
|
126
|
+
let newThickness;
|
127
|
+
if (key === '-' || key === '_') {
|
128
|
+
newThickness = this.getThickness() * 2 / 3;
|
129
|
+
}
|
130
|
+
else if (key === '+' || key === '=') {
|
131
|
+
newThickness = this.getThickness() * 3 / 2;
|
132
|
+
}
|
133
|
+
if (newThickness !== undefined) {
|
134
|
+
newThickness = Math.min(Math.max(1, newThickness), 128);
|
135
|
+
this.setThickness(newThickness);
|
136
|
+
return true;
|
137
|
+
}
|
138
|
+
return false;
|
139
|
+
}
|
124
140
|
}
|
@@ -11,6 +11,7 @@ export default class ToolController {
|
|
11
11
|
constructor(editor: Editor, localization: ToolLocalization);
|
12
12
|
setTools(tools: BaseTool[], primaryToolGroup?: ToolEnabledGroup): void;
|
13
13
|
addPrimaryTool(tool: BaseTool): void;
|
14
|
+
getPrimaryTools(): BaseTool[];
|
14
15
|
addTool(tool: BaseTool): void;
|
15
16
|
dispatchInputEvent(event: InputEvt): boolean;
|
16
17
|
getMatchingTools<Type extends BaseTool>(type: new (...args: any[]) => Type): Type[];
|
@@ -8,6 +8,7 @@ import Color4 from '../Color4';
|
|
8
8
|
import UndoRedoShortcut from './UndoRedoShortcut';
|
9
9
|
import TextTool from './TextTool';
|
10
10
|
import PipetteTool from './PipetteTool';
|
11
|
+
import ToolSwitcherShortcut from './ToolSwitcherShortcut';
|
11
12
|
export default class ToolController {
|
12
13
|
/** @internal */
|
13
14
|
constructor(editor, localization) {
|
@@ -18,13 +19,13 @@ export default class ToolController {
|
|
18
19
|
const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom);
|
19
20
|
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
|
20
21
|
const primaryTools = [
|
21
|
-
new SelectionTool(editor, localization.selectionTool),
|
22
|
-
new Eraser(editor, localization.eraserTool),
|
23
22
|
// Three pens
|
24
23
|
primaryPenTool,
|
25
24
|
new Pen(editor, localization.penTool(2), { color: Color4.clay, thickness: 4 }),
|
26
25
|
// Highlighter-like pen with width=64
|
27
26
|
new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
|
27
|
+
new Eraser(editor, localization.eraserTool),
|
28
|
+
new SelectionTool(editor, localization.selectionTool),
|
28
29
|
new TextTool(editor, localization.textTool, localization),
|
29
30
|
];
|
30
31
|
this.tools = [
|
@@ -33,6 +34,7 @@ export default class ToolController {
|
|
33
34
|
...primaryTools,
|
34
35
|
keyboardPanZoomTool,
|
35
36
|
new UndoRedoShortcut(editor),
|
37
|
+
new ToolSwitcherShortcut(editor),
|
36
38
|
];
|
37
39
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
|
38
40
|
panZoomTool.setEnabled(true);
|
@@ -64,6 +66,11 @@ export default class ToolController {
|
|
64
66
|
}
|
65
67
|
this.addTool(tool);
|
66
68
|
}
|
69
|
+
getPrimaryTools() {
|
70
|
+
return this.tools.filter(tool => {
|
71
|
+
return tool.getToolGroup() === this.primaryToolGroup;
|
72
|
+
});
|
73
|
+
}
|
67
74
|
// Add a tool to the end of this' tool list (the added tool receives events after tools already added to this).
|
68
75
|
// This should be called before creating the app's toolbar.
|
69
76
|
addTool(tool) {
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { KeyPressEvent } from '../types';
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
export default class ToolSwitcherShortcut extends BaseTool {
|
5
|
+
private editor;
|
6
|
+
constructor(editor: Editor);
|
7
|
+
onKeyPress({ key }: KeyPressEvent): boolean;
|
8
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
// Handles ctrl+1, ctrl+2, ctrl+3, ..., shortcuts for switching tools.
|
2
|
+
// @packageDocumentation
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
// {@inheritDoc ToolSwitcherShortcut!}
|
5
|
+
export default class ToolSwitcherShortcut extends BaseTool {
|
6
|
+
constructor(editor) {
|
7
|
+
super(editor.notifier, editor.localization.changeTool);
|
8
|
+
this.editor = editor;
|
9
|
+
}
|
10
|
+
onKeyPress({ key }) {
|
11
|
+
const toolController = this.editor.toolController;
|
12
|
+
const primaryTools = toolController.getPrimaryTools();
|
13
|
+
// Map keys 0-9 to primary tools.
|
14
|
+
const keyMatch = /^[0-9]$/.exec(key);
|
15
|
+
let targetTool;
|
16
|
+
if (keyMatch) {
|
17
|
+
const targetIdx = parseInt(keyMatch[0], 10) - 1;
|
18
|
+
targetTool = primaryTools[targetIdx];
|
19
|
+
}
|
20
|
+
if (targetTool) {
|
21
|
+
targetTool.setEnabled(true);
|
22
|
+
return true;
|
23
|
+
}
|
24
|
+
return false;
|
25
|
+
}
|
26
|
+
}
|
package/dist/src/tools/lib.d.ts
CHANGED
@@ -5,6 +5,7 @@ export { default as BaseTool } from './BaseTool';
|
|
5
5
|
export { default as ToolController } from './ToolController';
|
6
6
|
export { default as ToolEnabledGroup } from './ToolEnabledGroup';
|
7
7
|
export { default as UndoRedoShortcut } from './UndoRedoShortcut';
|
8
|
+
export { default as ToolSwitcherShortcut } from './ToolSwitcherShortcut';
|
8
9
|
export { default as PanZoomTool, PanZoomMode } from './PanZoom';
|
9
10
|
export { default as PenTool, PenStyle } from './Pen';
|
10
11
|
export { default as TextTool } from './TextTool';
|
package/dist/src/tools/lib.js
CHANGED
@@ -5,6 +5,7 @@ export { default as BaseTool } from './BaseTool';
|
|
5
5
|
export { default as ToolController } from './ToolController';
|
6
6
|
export { default as ToolEnabledGroup } from './ToolEnabledGroup';
|
7
7
|
export { default as UndoRedoShortcut } from './UndoRedoShortcut';
|
8
|
+
export { default as ToolSwitcherShortcut } from './ToolSwitcherShortcut';
|
8
9
|
export { default as PanZoomTool, PanZoomMode } from './PanZoom';
|
9
10
|
export { default as PenTool } from './Pen';
|
10
11
|
export { default as TextTool } from './TextTool';
|
@@ -10,6 +10,7 @@ export interface ToolLocalization {
|
|
10
10
|
rightClickDragPanTool: string;
|
11
11
|
textTool: string;
|
12
12
|
enterTextToInsert: string;
|
13
|
+
changeTool: string;
|
13
14
|
toolEnabledAnnouncement: (toolName: string) => string;
|
14
15
|
toolDisabledAnnouncement: (toolName: string) => string;
|
15
16
|
}
|
@@ -10,6 +10,7 @@ export const defaultToolLocalization = {
|
|
10
10
|
keyboardPanZoom: 'Keyboard pan/zoom shortcuts',
|
11
11
|
textTool: 'Text',
|
12
12
|
enterTextToInsert: 'Text to insert',
|
13
|
+
changeTool: 'Change tool',
|
13
14
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
14
15
|
toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
|
15
16
|
};
|
package/dist/src/types.d.ts
CHANGED
@@ -8,6 +8,7 @@ import Rect2 from './math/Rect2';
|
|
8
8
|
import Pointer from './Pointer';
|
9
9
|
import Color4 from './Color4';
|
10
10
|
import Command from './commands/Command';
|
11
|
+
import { BaseWidget } from './lib';
|
11
12
|
export interface PointerEvtListener {
|
12
13
|
onPointerDown(event: PointerEvt): boolean;
|
13
14
|
onPointerMove(event: PointerEvt): void;
|
@@ -68,7 +69,8 @@ export declare enum EditorEventType {
|
|
68
69
|
ViewportChanged = 7,
|
69
70
|
DisplayResized = 8,
|
70
71
|
ColorPickerToggled = 9,
|
71
|
-
ColorPickerColorSelected = 10
|
72
|
+
ColorPickerColorSelected = 10,
|
73
|
+
ToolbarDropdownShown = 11
|
72
74
|
}
|
73
75
|
declare type EditorToolEventType = EditorEventType.ToolEnabled | EditorEventType.ToolDisabled | EditorEventType.ToolUpdated;
|
74
76
|
export interface EditorToolEvent {
|
@@ -109,7 +111,11 @@ export interface ColorPickerColorSelected {
|
|
109
111
|
readonly kind: EditorEventType.ColorPickerColorSelected;
|
110
112
|
readonly color: Color4;
|
111
113
|
}
|
112
|
-
export
|
114
|
+
export interface ToolbarDropdownShownEvent {
|
115
|
+
readonly kind: EditorEventType.ToolbarDropdownShown;
|
116
|
+
readonly parentWidget: BaseWidget;
|
117
|
+
}
|
118
|
+
export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent | ColorPickerToggled | ColorPickerColorSelected | ToolbarDropdownShownEvent;
|
113
119
|
export declare type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
|
114
120
|
export declare type ComponentAddedListener = (component: AbstractComponent) => void;
|
115
121
|
export declare type OnDetermineExportRectListener = (exportRect: Rect2) => void;
|
package/dist/src/types.js
CHANGED
@@ -22,4 +22,5 @@ export var EditorEventType;
|
|
22
22
|
EditorEventType[EditorEventType["DisplayResized"] = 8] = "DisplayResized";
|
23
23
|
EditorEventType[EditorEventType["ColorPickerToggled"] = 9] = "ColorPickerToggled";
|
24
24
|
EditorEventType[EditorEventType["ColorPickerColorSelected"] = 10] = "ColorPickerColorSelected";
|
25
|
+
EditorEventType[EditorEventType["ToolbarDropdownShown"] = 11] = "ToolbarDropdownShown";
|
25
26
|
})(EditorEventType || (EditorEventType = {}));
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.3.
|
3
|
+
"version": "0.3.1",
|
4
4
|
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
|
5
5
|
"main": "./dist/src/lib.d.ts",
|
6
6
|
"types": "./dist/src/lib.js",
|
@@ -72,7 +72,7 @@
|
|
72
72
|
"linter-precommit": "eslint --fix --ext .js --ext .ts",
|
73
73
|
"lint-staged": "lint-staged",
|
74
74
|
"prepare": "husky install && yarn build",
|
75
|
-
"prepack": "yarn build && pinst --disable",
|
75
|
+
"prepack": "yarn build && yarn test && pinst --disable",
|
76
76
|
"postpack": "pinst --enable"
|
77
77
|
},
|
78
78
|
"dependencies": {
|
@@ -64,6 +64,11 @@ describe('Stroke', () => {
|
|
64
64
|
"path": "m0,0 l10,10z"
|
65
65
|
}
|
66
66
|
]`);
|
67
|
+
const path = deserialized.getPath();
|
68
|
+
|
69
|
+
// Should cache the original string representation.
|
70
|
+
expect(deserialized.getPath().toString()).toBe('m0,0 l10,10z');
|
71
|
+
path['cachedStringVersion'] = null;
|
67
72
|
expect(deserialized.getPath().toString()).toBe('M0,0L10,10L0,0');
|
68
73
|
});
|
69
74
|
});
|
package/src/components/Stroke.ts
CHANGED
@@ -9,7 +9,6 @@ import { ImageComponentLocalization } from './localization';
|
|
9
9
|
|
10
10
|
interface StrokePart extends RenderablePathSpec {
|
11
11
|
path: Path;
|
12
|
-
bbox: Rect2;
|
13
12
|
}
|
14
13
|
|
15
14
|
export default class Stroke extends AbstractComponent {
|
@@ -19,7 +18,7 @@ export default class Stroke extends AbstractComponent {
|
|
19
18
|
public constructor(parts: RenderablePathSpec[]) {
|
20
19
|
super('stroke');
|
21
20
|
|
22
|
-
this.parts = parts.map(section => {
|
21
|
+
this.parts = parts.map((section): StrokePart => {
|
23
22
|
const path = Path.fromRenderable(section);
|
24
23
|
const pathBBox = this.bboxForPart(path.bbox, section.style);
|
25
24
|
|
@@ -31,7 +30,6 @@ export default class Stroke extends AbstractComponent {
|
|
31
30
|
|
32
31
|
return {
|
33
32
|
path,
|
34
|
-
bbox: pathBBox,
|
35
33
|
|
36
34
|
// To implement RenderablePathSpec
|
37
35
|
startPoint: path.startPoint,
|
@@ -54,10 +52,19 @@ export default class Stroke extends AbstractComponent {
|
|
54
52
|
public render(canvas: AbstractRenderer, visibleRect?: Rect2): void {
|
55
53
|
canvas.startObject(this.getBBox());
|
56
54
|
for (const part of this.parts) {
|
57
|
-
const bbox = part.bbox;
|
58
|
-
if (
|
59
|
-
|
55
|
+
const bbox = this.bboxForPart(part.path.bbox, part.style);
|
56
|
+
if (visibleRect) {
|
57
|
+
if (!bbox.intersects(visibleRect)) {
|
58
|
+
continue;
|
59
|
+
}
|
60
|
+
|
61
|
+
const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 2 || bbox.size.y > visibleRect.size.y * 2;
|
62
|
+
if (muchBiggerThanVisible && !part.path.closedRoughlyIntersects(visibleRect)) {
|
63
|
+
continue;
|
64
|
+
}
|
60
65
|
}
|
66
|
+
|
67
|
+
canvas.drawPath(part);
|
61
68
|
}
|
62
69
|
canvas.endObject(this.getLoadSaveData());
|
63
70
|
}
|
@@ -89,7 +96,6 @@ export default class Stroke extends AbstractComponent {
|
|
89
96
|
|
90
97
|
return {
|
91
98
|
path: newPath,
|
92
|
-
bbox: newBBox,
|
93
99
|
startPoint: newPath.startPoint,
|
94
100
|
commands: newPath.parts,
|
95
101
|
style: part.style,
|
@@ -12,9 +12,9 @@ import RenderingStyle from '../../rendering/RenderingStyle';
|
|
12
12
|
|
13
13
|
export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
|
14
14
|
// Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
|
15
|
-
// less than ±
|
15
|
+
// less than ±1 px from the curve.
|
16
16
|
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
|
17
|
-
const minSmoothingDist = viewport.getSizeOfPixelOnCanvas()
|
17
|
+
const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
|
18
18
|
|
19
19
|
return new FreehandLineBuilder(
|
20
20
|
initialPoint, minSmoothingDist, maxSmoothingDist
|
@@ -197,7 +197,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
197
197
|
return;
|
198
198
|
}
|
199
199
|
|
200
|
-
const width = Viewport.roundPoint(this.startPoint.width / 3.5, this.minFitAllowed);
|
200
|
+
const width = Viewport.roundPoint(this.startPoint.width / 3.5, Math.min(this.minFitAllowed, this.startPoint.width / 4));
|
201
201
|
const center = this.roundPoint(this.startPoint.pos);
|
202
202
|
|
203
203
|
// Start on the right, cycle clockwise:
|
@@ -492,7 +492,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
492
492
|
const dist = proj.minus(point).magnitude();
|
493
493
|
|
494
494
|
const minFit = Math.max(
|
495
|
-
Math.min(this.curveStartWidth, this.curveEndWidth) /
|
495
|
+
Math.min(this.curveStartWidth, this.curveEndWidth) / 3,
|
496
496
|
this.minFitAllowed
|
497
497
|
);
|
498
498
|
if (dist > minFit || dist > this.maxFitAllowed) {
|
@@ -503,7 +503,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
503
503
|
};
|
504
504
|
|
505
505
|
const approxCurveLen = controlPoint.minus(segmentStart).magnitude() + segmentEnd.minus(controlPoint).magnitude();
|
506
|
-
if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth /
|
506
|
+
if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
|
507
507
|
if (!curveMatchesPoints(this.currentCurve)) {
|
508
508
|
// Use a curve that better fits the points
|
509
509
|
this.currentCurve = prevCurve;
|
package/src/math/LineSegment2.ts
CHANGED
@@ -126,6 +126,10 @@ export default class LineSegment2 {
|
|
126
126
|
};
|
127
127
|
}
|
128
128
|
|
129
|
+
public intersects(other: LineSegment2) {
|
130
|
+
return this.intersection(other) !== null;
|
131
|
+
}
|
132
|
+
|
129
133
|
// Returns the closest point on this to [target]
|
130
134
|
public closestPointTo(target: Point2) {
|
131
135
|
// Distance from P1 along this' direction.
|
@@ -144,4 +148,8 @@ export default class LineSegment2 {
|
|
144
148
|
return this.p1;
|
145
149
|
}
|
146
150
|
}
|
151
|
+
|
152
|
+
public toString() {
|
153
|
+
return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
|
154
|
+
}
|
147
155
|
}
|
package/src/math/Path.test.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import { Bezier } from 'bezier-js';
|
2
2
|
import LineSegment2 from './LineSegment2';
|
3
3
|
import Path, { PathCommandType } from './Path';
|
4
|
+
import Rect2 from './Rect2';
|
4
5
|
import { Vec2 } from './Vec2';
|
5
6
|
|
6
7
|
describe('Path', () => {
|
@@ -93,4 +94,56 @@ describe('Path', () => {
|
|
93
94
|
y: 100,
|
94
95
|
});
|
95
96
|
});
|
97
|
+
|
98
|
+
describe('polylineApproximation', () => {
|
99
|
+
it('should approximate Bézier curves with polylines', () => {
|
100
|
+
const path = Path.fromString('m0,0 l4,4 Q 1,4 4,1z');
|
101
|
+
|
102
|
+
expect(path.polylineApproximation()).toMatchObject([
|
103
|
+
new LineSegment2(Vec2.of(0, 0), Vec2.of(4, 4)),
|
104
|
+
new LineSegment2(Vec2.of(4, 4), Vec2.of(1, 4)),
|
105
|
+
new LineSegment2(Vec2.of(1, 4), Vec2.of(4, 1)),
|
106
|
+
new LineSegment2(Vec2.of(4, 1), Vec2.of(0, 0)),
|
107
|
+
]);
|
108
|
+
});
|
109
|
+
});
|
110
|
+
|
111
|
+
describe('roughlyIntersectsClosed', () => {
|
112
|
+
it('small, line-only path', () => {
|
113
|
+
const path = Path.fromString('m0,0 l10,10 L0,10 z');
|
114
|
+
expect(
|
115
|
+
path.closedRoughlyIntersects(Rect2.fromCorners(Vec2.zero, Vec2.of(20, 20)))
|
116
|
+
).toBe(true);
|
117
|
+
expect(
|
118
|
+
path.closedRoughlyIntersects(Rect2.fromCorners(Vec2.zero, Vec2.of(2, 2)))
|
119
|
+
).toBe(true);
|
120
|
+
expect(
|
121
|
+
path.closedRoughlyIntersects(new Rect2(10, 1, 1, 1))
|
122
|
+
).toBe(false);
|
123
|
+
expect(
|
124
|
+
path.closedRoughlyIntersects(new Rect2(1, 5, 1, 1))
|
125
|
+
).toBe(true);
|
126
|
+
});
|
127
|
+
|
128
|
+
it('path with Bézier curves', () => {
|
129
|
+
const path = Path.fromString(`
|
130
|
+
M1090,2560
|
131
|
+
L1570,2620
|
132
|
+
Q1710,1300 1380,720
|
133
|
+
Q980,100 -460,-640
|
134
|
+
L-680,-200
|
135
|
+
Q670,470 960,980
|
136
|
+
Q1230,1370 1090,2560
|
137
|
+
`);
|
138
|
+
expect(
|
139
|
+
path.closedRoughlyIntersects(new Rect2(0, 0, 500, 500))
|
140
|
+
).toBe(true);
|
141
|
+
expect(
|
142
|
+
path.closedRoughlyIntersects(new Rect2(0, 0, 5, 5))
|
143
|
+
).toBe(true);
|
144
|
+
expect(
|
145
|
+
path.closedRoughlyIntersects(new Rect2(-10000, 0, 500, 500))
|
146
|
+
).toBe(false);
|
147
|
+
});
|
148
|
+
});
|
96
149
|
});
|
@@ -42,13 +42,15 @@ describe('Path.toString', () => {
|
|
42
42
|
},
|
43
43
|
]);
|
44
44
|
|
45
|
-
expect(path.toString()).toBe('M1000,
|
45
|
+
expect(path.toString()).toBe('M1000,2000000l-970-1999960');
|
46
46
|
});
|
47
47
|
|
48
48
|
it('deserialized path should serialize to the same/similar path, but with rounded components', () => {
|
49
49
|
const path1 = Path.fromString('M100,100 L101,101 Q102,102 90.000000001,89.99999999 Z');
|
50
|
+
path1['cachedStringVersion'] = null; // Clear the cache.
|
51
|
+
|
50
52
|
expect(path1.toString()).toBe([
|
51
|
-
'M100,100', '
|
53
|
+
'M100,100', 'l1,1', 'q1,1 -11-11', 'l10,10'
|
52
54
|
].join(''));
|
53
55
|
});
|
54
56
|
});
|
package/src/math/Path.ts
CHANGED
@@ -47,12 +47,9 @@ interface IntersectionResult {
|
|
47
47
|
|
48
48
|
type GeometryArrayType = Array<LineSegment2|Bezier>;
|
49
49
|
export default class Path {
|
50
|
-
private cachedGeometry: GeometryArrayType|null;
|
51
50
|
public readonly bbox: Rect2;
|
52
51
|
|
53
52
|
public constructor(public readonly startPoint: Point2, public readonly parts: PathCommand[]) {
|
54
|
-
this.cachedGeometry = null;
|
55
|
-
|
56
53
|
// Initial bounding box contains one point: the start point.
|
57
54
|
this.bbox = Rect2.bboxOf([startPoint]);
|
58
55
|
|
@@ -63,6 +60,8 @@ export default class Path {
|
|
63
60
|
}
|
64
61
|
}
|
65
62
|
|
63
|
+
private cachedGeometry: GeometryArrayType|null = null;
|
64
|
+
|
66
65
|
// Lazy-loads and returns this path's geometry
|
67
66
|
public get geometry(): Array<LineSegment2|Bezier> {
|
68
67
|
if (this.cachedGeometry) {
|
@@ -106,6 +105,41 @@ export default class Path {
|
|
106
105
|
return this.cachedGeometry;
|
107
106
|
}
|
108
107
|
|
108
|
+
private cachedPolylineApproximation: LineSegment2[]|null = null;
|
109
|
+
|
110
|
+
// Approximates this path with a group of line segments.
|
111
|
+
public polylineApproximation(): LineSegment2[] {
|
112
|
+
if (this.cachedPolylineApproximation) {
|
113
|
+
return this.cachedPolylineApproximation;
|
114
|
+
}
|
115
|
+
|
116
|
+
const points: Point2[] = [];
|
117
|
+
|
118
|
+
for (const part of this.parts) {
|
119
|
+
switch (part.kind) {
|
120
|
+
case PathCommandType.CubicBezierTo:
|
121
|
+
points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
|
122
|
+
break;
|
123
|
+
case PathCommandType.QuadraticBezierTo:
|
124
|
+
points.push(part.controlPoint, part.endPoint);
|
125
|
+
break;
|
126
|
+
case PathCommandType.MoveTo:
|
127
|
+
case PathCommandType.LineTo:
|
128
|
+
points.push(part.point);
|
129
|
+
break;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
const result: LineSegment2[] = [];
|
134
|
+
let prevPoint = this.startPoint;
|
135
|
+
for (const point of points) {
|
136
|
+
result.push(new LineSegment2(prevPoint, point));
|
137
|
+
prevPoint = point;
|
138
|
+
}
|
139
|
+
|
140
|
+
return result;
|
141
|
+
}
|
142
|
+
|
109
143
|
public static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2 {
|
110
144
|
const points = [startPoint];
|
111
145
|
let exhaustivenessCheck: never;
|
@@ -129,6 +163,10 @@ export default class Path {
|
|
129
163
|
}
|
130
164
|
|
131
165
|
public intersection(line: LineSegment2): IntersectionResult[] {
|
166
|
+
if (!line.bbox.intersects(this.bbox)) {
|
167
|
+
return [];
|
168
|
+
}
|
169
|
+
|
132
170
|
const result: IntersectionResult[] = [];
|
133
171
|
for (const part of this.geometry) {
|
134
172
|
if (part instanceof LineSegment2) {
|
@@ -229,6 +267,55 @@ export default class Path {
|
|
229
267
|
]);
|
230
268
|
}
|
231
269
|
|
270
|
+
// Treats this as a closed path and returns true if part of `rect` is roughly within
|
271
|
+
// this path's interior.
|
272
|
+
//
|
273
|
+
// Note: Assumes that this is a closed, non-self-intersecting path.
|
274
|
+
public closedRoughlyIntersects(rect: Rect2): boolean {
|
275
|
+
if (rect.containsRect(this.bbox)) {
|
276
|
+
return true;
|
277
|
+
}
|
278
|
+
|
279
|
+
// Choose a point outside of the path.
|
280
|
+
const startPt = this.bbox.topLeft.minus(Vec2.of(1, 1));
|
281
|
+
const testPts = rect.corners;
|
282
|
+
const polygon = this.polylineApproximation();
|
283
|
+
|
284
|
+
for (const point of testPts) {
|
285
|
+
const testLine = new LineSegment2(point, startPt);
|
286
|
+
|
287
|
+
let intersectionCount = 0;
|
288
|
+
for (const line of polygon) {
|
289
|
+
if (line.intersects(testLine)) {
|
290
|
+
intersectionCount ++;
|
291
|
+
}
|
292
|
+
}
|
293
|
+
|
294
|
+
// Odd? The point is within the polygon!
|
295
|
+
if (intersectionCount % 2 === 1) {
|
296
|
+
return true;
|
297
|
+
}
|
298
|
+
}
|
299
|
+
|
300
|
+
// Grow the rectangle for possible additional precision.
|
301
|
+
const grownRect = rect.grownBy(Math.min(rect.size.x, rect.size.y));
|
302
|
+
const edges = [];
|
303
|
+
for (const subrect of grownRect.divideIntoGrid(4, 4)) {
|
304
|
+
edges.push(...subrect.getEdges());
|
305
|
+
}
|
306
|
+
|
307
|
+
for (const edge of edges) {
|
308
|
+
for (const line of polygon) {
|
309
|
+
if (edge.intersects(line)) {
|
310
|
+
return true;
|
311
|
+
}
|
312
|
+
}
|
313
|
+
}
|
314
|
+
|
315
|
+
// Even? Probably no intersection.
|
316
|
+
return false;
|
317
|
+
}
|
318
|
+
|
232
319
|
// Returns a path that outlines [rect]. If [lineWidth] is not given, the resultant path is
|
233
320
|
// the outline of [rect]. Otherwise, the resultant path represents a line of width [lineWidth]
|
234
321
|
// that traces [rect].
|
@@ -273,6 +360,10 @@ export default class Path {
|
|
273
360
|
}
|
274
361
|
|
275
362
|
public static fromRenderable(renderable: RenderablePathSpec): Path {
|
363
|
+
if (renderable.path) {
|
364
|
+
return renderable.path;
|
365
|
+
}
|
366
|
+
|
276
367
|
return new Path(renderable.startPoint, renderable.commands);
|
277
368
|
}
|
278
369
|
|
@@ -281,18 +372,23 @@ export default class Path {
|
|
281
372
|
startPoint: this.startPoint,
|
282
373
|
style: fill,
|
283
374
|
commands: this.parts,
|
375
|
+
path: this,
|
284
376
|
};
|
285
377
|
}
|
286
378
|
|
379
|
+
private cachedStringVersion: string|null = null;
|
380
|
+
|
287
381
|
public toString(): string {
|
382
|
+
if (this.cachedStringVersion) {
|
383
|
+
return this.cachedStringVersion;
|
384
|
+
}
|
385
|
+
|
288
386
|
// Hueristic: Try to determine whether converting absolute to relative commands is worth it.
|
289
|
-
|
290
|
-
// it also probably isn't worth it.
|
291
|
-
const makeRelativeCommands =
|
292
|
-
Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.size.x) < 2
|
293
|
-
&& Math.abs(this.bbox.topLeft.y) > 10 && Math.abs(this.bbox.size.y) < 2;
|
387
|
+
const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
|
294
388
|
|
295
|
-
|
389
|
+
const result = Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
|
390
|
+
this.cachedStringVersion = result;
|
391
|
+
return result;
|
296
392
|
}
|
297
393
|
|
298
394
|
public serialize(): string {
|
@@ -301,7 +397,7 @@ export default class Path {
|
|
301
397
|
|
302
398
|
// @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
|
303
399
|
// conversions can lead to smaller output strings, but also take time.
|
304
|
-
public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands
|
400
|
+
public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands?: boolean): string {
|
305
401
|
const result: string[] = [];
|
306
402
|
|
307
403
|
let prevPoint: Point2|undefined;
|
@@ -561,7 +657,9 @@ export default class Path {
|
|
561
657
|
}
|
562
658
|
}
|
563
659
|
|
564
|
-
|
660
|
+
const result = new Path(startPos ?? Vec2.zero, commands);
|
661
|
+
result.cachedStringVersion = pathString;
|
662
|
+
return result;
|
565
663
|
}
|
566
664
|
|
567
665
|
public static empty: Path = new Path(Vec2.zero, []);
|
package/src/math/Rect2.ts
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
import Mat33 from './Mat33';
|
2
|
+
import Vec3 from './Vec3';
|
3
|
+
|
4
|
+
export default class Triangle {
|
5
|
+
public constructor(
|
6
|
+
public readonly vertex1: Vec3,
|
7
|
+
public readonly vertex2: Vec3,
|
8
|
+
public readonly vertex3: Vec3,
|
9
|
+
) {}
|
10
|
+
|
11
|
+
public map(mapping: (vertex: Vec3)=>Vec3): Triangle {
|
12
|
+
return new Triangle(
|
13
|
+
mapping(this.vertex1),
|
14
|
+
mapping(this.vertex2),
|
15
|
+
mapping(this.vertex3),
|
16
|
+
);
|
17
|
+
}
|
18
|
+
|
19
|
+
// Transform, treating this as composed of 2D points.
|
20
|
+
public transformed2DBy(affineTransform: Mat33) {
|
21
|
+
return this.map(affineTransform.transformVec2);
|
22
|
+
}
|
23
|
+
|
24
|
+
// Transforms this by a linear transform --- verticies are treated as
|
25
|
+
// 3D points.
|
26
|
+
public transformedBy(linearTransform: Mat33) {
|
27
|
+
return this.map(linearTransform.transformVec3);
|
28
|
+
}
|
29
|
+
}
|