js-draw 1.17.0 → 1.18.0
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/README.md +70 -10
- package/dist/bundle.js +2 -2
- package/dist/cjs/Editor.d.ts +18 -20
- package/dist/cjs/Editor.js +5 -2
- package/dist/cjs/components/AbstractComponent.d.ts +17 -5
- package/dist/cjs/components/AbstractComponent.js +15 -15
- package/dist/cjs/components/Stroke.d.ts +4 -1
- package/dist/cjs/components/Stroke.js +158 -2
- package/dist/cjs/components/builders/PolylineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PolylineBuilder.js +9 -2
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
- package/dist/cjs/image/EditorImage.js +1 -1
- package/dist/cjs/localizations/de.js +1 -1
- package/dist/cjs/localizations/es.js +1 -1
- package/dist/cjs/testing/createEditor.d.ts +2 -2
- package/dist/cjs/testing/createEditor.js +2 -2
- package/dist/cjs/toolbar/IconProvider.d.ts +3 -1
- package/dist/cjs/toolbar/IconProvider.js +15 -3
- package/dist/cjs/toolbar/localization.d.ts +6 -1
- package/dist/cjs/toolbar/localization.js +7 -2
- package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +10 -3
- package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
- package/dist/cjs/tools/Eraser.d.ts +24 -4
- package/dist/cjs/tools/Eraser.js +107 -20
- package/dist/cjs/tools/PasteHandler.js +0 -1
- package/dist/cjs/tools/lib.d.ts +1 -4
- package/dist/cjs/tools/lib.js +2 -4
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +18 -20
- package/dist/mjs/Editor.mjs +5 -2
- package/dist/mjs/components/AbstractComponent.d.ts +17 -5
- package/dist/mjs/components/AbstractComponent.mjs +15 -15
- package/dist/mjs/components/Stroke.d.ts +4 -1
- package/dist/mjs/components/Stroke.mjs +159 -3
- package/dist/mjs/components/builders/PolylineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PolylineBuilder.mjs +10 -3
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
- package/dist/mjs/image/EditorImage.mjs +1 -1
- package/dist/mjs/localizations/de.mjs +1 -1
- package/dist/mjs/localizations/es.mjs +1 -1
- package/dist/mjs/testing/createEditor.d.ts +2 -2
- package/dist/mjs/testing/createEditor.mjs +2 -2
- package/dist/mjs/toolbar/IconProvider.d.ts +3 -1
- package/dist/mjs/toolbar/IconProvider.mjs +15 -3
- package/dist/mjs/toolbar/localization.d.ts +6 -1
- package/dist/mjs/toolbar/localization.mjs +7 -2
- package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +10 -3
- package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
- package/dist/mjs/tools/Eraser.d.ts +24 -4
- package/dist/mjs/tools/Eraser.mjs +107 -21
- package/dist/mjs/tools/PasteHandler.mjs +0 -1
- package/dist/mjs/tools/lib.d.ts +1 -4
- package/dist/mjs/tools/lib.mjs +1 -4
- package/dist/mjs/version.mjs +1 -1
- package/package.json +3 -3
package/dist/cjs/Editor.d.ts
CHANGED
@@ -79,30 +79,28 @@ export interface EditorSettings {
|
|
79
79
|
* Configures the default pen tools.
|
80
80
|
*
|
81
81
|
* **Example**:
|
82
|
-
*
|
83
|
-
* import { Editor, makePolylineBuilder } from 'js-draw';
|
84
|
-
*
|
85
|
-
* const editor = new Editor(document.body, {
|
86
|
-
* pens: {
|
87
|
-
* additionalPenTypes: [{
|
88
|
-
* name: 'Polyline (For debugging)',
|
89
|
-
* id: 'custom-polyline',
|
90
|
-
* factory: makePolylineBuilder,
|
91
|
-
*
|
92
|
-
* // The pen doesn't create fixed shapes (e.g. squares, rectangles, etc)
|
93
|
-
* // and so should go under the "pens" section.
|
94
|
-
* isShapeBuilder: false,
|
95
|
-
* }],
|
96
|
-
* },
|
97
|
-
* });
|
98
|
-
* editor.addToolbar();
|
99
|
-
* ```
|
82
|
+
* [[include:doc-pages/inline-examples/editor-settings-polyline-pen.md]]
|
100
83
|
*/
|
101
84
|
pens: {
|
102
85
|
/**
|
103
86
|
* Additional pen types that can be selected in a toolbar.
|
104
87
|
*/
|
105
|
-
additionalPenTypes
|
88
|
+
additionalPenTypes?: readonly Readonly<PenTypeRecord>[];
|
89
|
+
/**
|
90
|
+
* Should return `true` if a pen type should be shown in the toolbar.
|
91
|
+
*
|
92
|
+
* @example
|
93
|
+
* ```ts,runnable
|
94
|
+
* import {Editor} from 'js-draw';
|
95
|
+
* const editor = new Editor(document.body, {
|
96
|
+
* // Only allow selecting the polyline pen from the toolbar.
|
97
|
+
* pens: { filterPenTypes: p => p.id === 'polyline-pen' },
|
98
|
+
* });
|
99
|
+
* editor.addToolbar();
|
100
|
+
* ```
|
101
|
+
* Notice that this setting only affects the toolbar GUI.
|
102
|
+
*/
|
103
|
+
filterPenTypes?: (penType: PenTypeRecord) => boolean;
|
106
104
|
} | null;
|
107
105
|
}
|
108
106
|
/**
|
@@ -123,7 +121,7 @@ export interface EditorSettings {
|
|
123
121
|
* ```
|
124
122
|
*
|
125
123
|
* See also
|
126
|
-
* [`
|
124
|
+
* * [`examples.md`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/examples.md).
|
127
125
|
*/
|
128
126
|
export declare class Editor {
|
129
127
|
private container;
|
package/dist/cjs/Editor.js
CHANGED
@@ -76,7 +76,7 @@ const ClipboardHandler_1 = __importDefault(require("./util/ClipboardHandler"));
|
|
76
76
|
* ```
|
77
77
|
*
|
78
78
|
* See also
|
79
|
-
* [`
|
79
|
+
* * [`examples.md`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/examples.md).
|
80
80
|
*/
|
81
81
|
class Editor {
|
82
82
|
/**
|
@@ -138,7 +138,10 @@ class Editor {
|
|
138
138
|
iconProvider: settings.iconProvider ?? new IconProvider_1.default(),
|
139
139
|
notices: [],
|
140
140
|
appInfo: settings.appInfo ? { ...settings.appInfo } : null,
|
141
|
-
pens: {
|
141
|
+
pens: {
|
142
|
+
additionalPenTypes: settings.pens?.additionalPenTypes ?? [],
|
143
|
+
filterPenTypes: settings.pens?.filterPenTypes ?? (() => true)
|
144
|
+
},
|
142
145
|
};
|
143
146
|
// Validate settings
|
144
147
|
if (this.settings.minZoom > this.settings.maxZoom) {
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import SerializableCommand from '../commands/SerializableCommand';
|
2
2
|
import EditorImage from '../image/EditorImage';
|
3
|
-
import { LineSegment2, Mat33, Rect2 } from '@js-draw/math';
|
3
|
+
import { LineSegment2, Mat33, Path, Rect2 } from '@js-draw/math';
|
4
4
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
5
|
import { ImageComponentLocalization } from './localization';
|
6
|
+
import Viewport from '../Viewport';
|
6
7
|
export type LoadSaveData = (string[] | Record<symbol, string | number>);
|
7
8
|
export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
|
8
9
|
export type DeserializeCallback = (data: string) => AbstractComponent;
|
@@ -112,7 +113,9 @@ export default abstract class AbstractComponent {
|
|
112
113
|
* this function.
|
113
114
|
*/
|
114
115
|
intersectsRect(rect: Rect2): boolean;
|
115
|
-
|
116
|
+
isSelectable(): boolean;
|
117
|
+
isBackground(): boolean;
|
118
|
+
getProportionalRenderingTime(): number;
|
116
119
|
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
117
120
|
/**
|
118
121
|
* Returns a command that, when applied, transforms this by [affineTransfm] and
|
@@ -131,9 +134,6 @@ export default abstract class AbstractComponent {
|
|
131
134
|
* this command.
|
132
135
|
*/
|
133
136
|
setZIndexAndTransformBy(affineTransfm: Mat33, newZIndex: number, originalZIndex?: number): SerializableCommand;
|
134
|
-
isSelectable(): boolean;
|
135
|
-
isBackground(): boolean;
|
136
|
-
getProportionalRenderingTime(): number;
|
137
137
|
private static transformElementCommandId;
|
138
138
|
private static TransformElementCommand;
|
139
139
|
/**
|
@@ -143,6 +143,18 @@ export default abstract class AbstractComponent {
|
|
143
143
|
abstract description(localizationTable: ImageComponentLocalization): string;
|
144
144
|
protected abstract createClone(): AbstractComponent;
|
145
145
|
clone(): AbstractComponent;
|
146
|
+
/**
|
147
|
+
* **Optional method**: Divides this component into sections roughly along the given path,
|
148
|
+
* removing parts that are roughly within `shape`.
|
149
|
+
*
|
150
|
+
* **Notes**:
|
151
|
+
* - A default implementation may be provided for this method in the future. Until then,
|
152
|
+
* this method is `undefined` if unsupported.
|
153
|
+
*
|
154
|
+
* `viewport` should be provided to determine how newly-added points should be rounded.
|
155
|
+
*/
|
156
|
+
withRegionErased?(shape: Path, viewport: Viewport): AbstractComponent[];
|
157
|
+
protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
|
146
158
|
serialize(): {
|
147
159
|
name: string;
|
148
160
|
zIndex: number;
|
@@ -142,6 +142,21 @@ class AbstractComponent {
|
|
142
142
|
const testLines = rect.getEdges();
|
143
143
|
return testLines.some(edge => this.intersects(edge));
|
144
144
|
}
|
145
|
+
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
146
|
+
isSelectable() {
|
147
|
+
return true;
|
148
|
+
}
|
149
|
+
// @returns true iff this component should be added to the background, rather than the
|
150
|
+
// foreground of the image.
|
151
|
+
isBackground() {
|
152
|
+
return false;
|
153
|
+
}
|
154
|
+
// @returns an approximation of the proportional time it takes to render this component.
|
155
|
+
// This is intended to be a rough estimate, but, for example, a stroke with two points sould have
|
156
|
+
// a renderingWeight approximately twice that of a stroke with one point.
|
157
|
+
getProportionalRenderingTime() {
|
158
|
+
return 1;
|
159
|
+
}
|
145
160
|
/**
|
146
161
|
* Returns a command that, when applied, transforms this by [affineTransfm] and
|
147
162
|
* updates the editor.
|
@@ -166,21 +181,6 @@ class AbstractComponent {
|
|
166
181
|
setZIndexAndTransformBy(affineTransfm, newZIndex, originalZIndex) {
|
167
182
|
return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this, newZIndex, originalZIndex);
|
168
183
|
}
|
169
|
-
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
170
|
-
isSelectable() {
|
171
|
-
return true;
|
172
|
-
}
|
173
|
-
// @returns true iff this component should be added to the background, rather than the
|
174
|
-
// foreground of the image.
|
175
|
-
isBackground() {
|
176
|
-
return false;
|
177
|
-
}
|
178
|
-
// @returns an approximation of the proportional time it takes to render this component.
|
179
|
-
// This is intended to be a rough estimate, but, for example, a stroke with two points sould have
|
180
|
-
// a renderingWeight approximately twice that of a stroke with one point.
|
181
|
-
getProportionalRenderingTime() {
|
182
|
-
return 1;
|
183
|
-
}
|
184
184
|
// Returns a copy of this component.
|
185
185
|
clone() {
|
186
186
|
const clone = this.createClone();
|
@@ -6,6 +6,7 @@ import AbstractComponent from './AbstractComponent';
|
|
6
6
|
import { ImageComponentLocalization } from './localization';
|
7
7
|
import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
|
8
8
|
import RenderablePathSpec, { RenderablePathSpecWithPath } from '../rendering/RenderablePathSpec';
|
9
|
+
import Viewport from '../Viewport';
|
9
10
|
/**
|
10
11
|
* Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
|
11
12
|
*
|
@@ -46,10 +47,12 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
|
|
46
47
|
* ]);
|
47
48
|
* ```
|
48
49
|
*/
|
49
|
-
constructor(parts: RenderablePathSpec[]);
|
50
|
+
constructor(parts: RenderablePathSpec[], initialZIndex?: number);
|
50
51
|
getStyle(): ComponentStyle;
|
51
52
|
updateStyle(style: ComponentStyle): SerializableCommand;
|
52
53
|
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
54
|
+
/** @beta -- May fail for concave `path`s */
|
55
|
+
withRegionErased(eraserPath: Path, viewport: Viewport): Stroke[];
|
53
56
|
intersects(line: LineSegment2): boolean;
|
54
57
|
intersectsRect(rect: Rect2): boolean;
|
55
58
|
private simplifiedPath;
|
@@ -44,8 +44,8 @@ class Stroke extends AbstractComponent_1.default {
|
|
44
44
|
* ]);
|
45
45
|
* ```
|
46
46
|
*/
|
47
|
-
constructor(parts) {
|
48
|
-
super('stroke');
|
47
|
+
constructor(parts, initialZIndex) {
|
48
|
+
super('stroke', initialZIndex);
|
49
49
|
// @internal
|
50
50
|
// eslint-disable-next-line @typescript-eslint/prefer-as-const
|
51
51
|
this.isRestylableComponent = true;
|
@@ -123,6 +123,162 @@ class Stroke extends AbstractComponent_1.default {
|
|
123
123
|
editor.queueRerender();
|
124
124
|
}
|
125
125
|
}
|
126
|
+
/** @beta -- May fail for concave `path`s */
|
127
|
+
withRegionErased(eraserPath, viewport) {
|
128
|
+
const polyline = eraserPath.polylineApproximation();
|
129
|
+
const isPointInsideEraser = (point) => {
|
130
|
+
return eraserPath.closedContainsPoint(point);
|
131
|
+
};
|
132
|
+
const newStrokes = [];
|
133
|
+
let failedAssertions = false;
|
134
|
+
for (const part of this.parts) {
|
135
|
+
const path = part.path;
|
136
|
+
const makeStroke = (path) => {
|
137
|
+
if (part.style.fill.a > 0) {
|
138
|
+
// Remove visually empty paths.
|
139
|
+
if (path.parts.length < 1 || (path.parts.length === 1 && path.parts[0].kind === math_1.PathCommandType.LineTo)) {
|
140
|
+
// TODO: If this isn't present, a very large number of strokes are created while erasing.
|
141
|
+
return null;
|
142
|
+
}
|
143
|
+
else {
|
144
|
+
// Filled paths must be closed (allows for optimizations elsewhere)
|
145
|
+
path = path.asClosed();
|
146
|
+
}
|
147
|
+
}
|
148
|
+
if (isNaN(path.getExactBBox().area)) {
|
149
|
+
console.warn('Prevented creating a stroke with NaN area');
|
150
|
+
failedAssertions = true;
|
151
|
+
return null;
|
152
|
+
}
|
153
|
+
return new Stroke([(0, RenderablePathSpec_1.pathToRenderable)(path, part.style)], this.getZIndex());
|
154
|
+
};
|
155
|
+
const intersectionPoints = [];
|
156
|
+
// If stroked, finds intersections with the middle of the stroke.
|
157
|
+
// If filled, finds intersections with the edge of the stroke.
|
158
|
+
for (const segment of polyline) {
|
159
|
+
intersectionPoints.push(...path.intersection(segment));
|
160
|
+
}
|
161
|
+
// When stroked, if the stroke width is significantly larger than the eraser,
|
162
|
+
// it can't intersect both the edge of the stroke and its middle at the same time
|
163
|
+
// (generally, erasing is triggered by the eraser touching the edge of this stroke).
|
164
|
+
//
|
165
|
+
// As such, we also look for intersections along the edge of this, if none with the
|
166
|
+
// center were found, but only within a certain range of sizes because:
|
167
|
+
// 1. Intersection testing with stroked paths is generally much slower than with
|
168
|
+
// non-stroked paths.
|
169
|
+
// 2. If zoomed in significantly, it's unlikely that the user wants to erase a large
|
170
|
+
// part of the stroke.
|
171
|
+
let isErasingFromEdge = false;
|
172
|
+
if (intersectionPoints.length === 0
|
173
|
+
&& part.style.stroke
|
174
|
+
&& part.style.stroke.width > eraserPath.bbox.minDimension * 0.3
|
175
|
+
&& part.style.stroke.width < eraserPath.bbox.maxDimension * 30) {
|
176
|
+
for (const segment of polyline) {
|
177
|
+
intersectionPoints.push(...path.intersection(segment, part.style.stroke.width / 2));
|
178
|
+
}
|
179
|
+
isErasingFromEdge = true;
|
180
|
+
}
|
181
|
+
// Sort first by curve index, then by parameter value
|
182
|
+
intersectionPoints.sort(math_1.comparePathIndices);
|
183
|
+
const isInsideJustBeforeFirst = (() => {
|
184
|
+
if (intersectionPoints.length === 0) {
|
185
|
+
return false;
|
186
|
+
}
|
187
|
+
// The eraser may not be near the center of the curve -- approximate.
|
188
|
+
if (isErasingFromEdge) {
|
189
|
+
return intersectionPoints[0].curveIndex === 0 && intersectionPoints[0].parameterValue <= 0;
|
190
|
+
}
|
191
|
+
const justBeforeFirstIntersection = (0, math_1.stepPathIndexBy)(intersectionPoints[0], -1e-10);
|
192
|
+
return isPointInsideEraser(path.at(justBeforeFirstIntersection));
|
193
|
+
})();
|
194
|
+
let intersectionCount = isInsideJustBeforeFirst ? 1 : 0;
|
195
|
+
const addNewPath = (path, knownToBeInside) => {
|
196
|
+
const component = makeStroke(path);
|
197
|
+
let isInside = intersectionCount % 2 === 1;
|
198
|
+
intersectionCount++;
|
199
|
+
if (knownToBeInside !== undefined) {
|
200
|
+
isInside = knownToBeInside;
|
201
|
+
}
|
202
|
+
// Here, we work around bugs in the underlying Bezier curve library
|
203
|
+
// (including https://github.com/Pomax/bezierjs/issues/179).
|
204
|
+
// Even if not all intersections are returned correctly, we still want
|
205
|
+
// isInside to be roughly correct.
|
206
|
+
if (knownToBeInside === undefined && !isInside && eraserPath.closedContainsPoint(path.getExactBBox().center)) {
|
207
|
+
isInside = !isInside;
|
208
|
+
}
|
209
|
+
if (!component) {
|
210
|
+
return;
|
211
|
+
}
|
212
|
+
// Assertion: Avoid deleting sections that are much larger than the eraser.
|
213
|
+
failedAssertions ||= isInside && path.getExactBBox().maxDimension > eraserPath.getExactBBox().maxDimension * 2;
|
214
|
+
if (!isInside) {
|
215
|
+
newStrokes.push(component);
|
216
|
+
}
|
217
|
+
};
|
218
|
+
if (part.style.fill.a === 0) { // Not filled?
|
219
|
+
// An additional case where we erase completely -- without the padding of the stroke,
|
220
|
+
// the path is smaller than the eraser (allows us to erase dots completely).
|
221
|
+
const shouldEraseCompletely = eraserPath.getExactBBox().maxDimension / 10 > path.getExactBBox().maxDimension;
|
222
|
+
if (!shouldEraseCompletely) {
|
223
|
+
const split = path.splitAt(intersectionPoints, { mapNewPoint: p => viewport.roundPoint(p) });
|
224
|
+
for (const splitPart of split) {
|
225
|
+
addNewPath(splitPart);
|
226
|
+
}
|
227
|
+
}
|
228
|
+
}
|
229
|
+
else if (intersectionPoints.length >= 2 && intersectionPoints.length % 2 === 0) {
|
230
|
+
// TODO: Support subtractive erasing on small scales -- see https://github.com/personalizedrefrigerator/js-draw/pull/63/commits/568686e2384219ad0bb07617ea4efff1540aed00
|
231
|
+
// for a broken implementation.
|
232
|
+
//
|
233
|
+
// We currently assume that a 4-point intersection means that the intersection
|
234
|
+
// looks similar to this:
|
235
|
+
// -----------
|
236
|
+
// | STROKE |
|
237
|
+
// | |
|
238
|
+
//%%x-----------x%%%%%%%
|
239
|
+
//% %
|
240
|
+
//% ERASER %
|
241
|
+
//% %
|
242
|
+
//%%x-----------x%%%%%%%
|
243
|
+
// | STROKE |
|
244
|
+
// -----------
|
245
|
+
//
|
246
|
+
// Our goal is to separate STROKE into the contiguous parts outside
|
247
|
+
// of the eraser (as shown above).
|
248
|
+
//
|
249
|
+
// To do this, we split STROKE at each intersection:
|
250
|
+
// 3 3 3 3 3 3
|
251
|
+
// 3 STROKE 3
|
252
|
+
// 3 3
|
253
|
+
// x x
|
254
|
+
// 2 4
|
255
|
+
// 2 STROKE 4
|
256
|
+
// 2 4
|
257
|
+
// x x
|
258
|
+
// 1 STROKE 5
|
259
|
+
// . 5 5 5 5 5
|
260
|
+
// ^
|
261
|
+
// Start
|
262
|
+
//
|
263
|
+
// The difficulty here is correctly pairing edges to create the the output
|
264
|
+
// strokes, particularly because we don't know the order of intersection points.
|
265
|
+
const parts = path.splitAt(intersectionPoints, { mapNewPoint: p => viewport.roundPoint(p) });
|
266
|
+
for (let i = 0; i < Math.floor(parts.length / 2); i++) {
|
267
|
+
addNewPath(parts[i].union(parts[parts.length - i - 1]).asClosed());
|
268
|
+
}
|
269
|
+
if (parts.length % 2 !== 0) {
|
270
|
+
addNewPath(parts[Math.floor(parts.length / 2)].asClosed());
|
271
|
+
}
|
272
|
+
}
|
273
|
+
else {
|
274
|
+
addNewPath(path, false);
|
275
|
+
}
|
276
|
+
}
|
277
|
+
if (failedAssertions) {
|
278
|
+
return [this];
|
279
|
+
}
|
280
|
+
return newStrokes;
|
281
|
+
}
|
126
282
|
intersects(line) {
|
127
283
|
for (const part of this.parts) {
|
128
284
|
const strokeWidth = part.style.stroke?.width;
|
@@ -9,7 +9,6 @@ import RenderingStyle from '../../rendering/RenderingStyle';
|
|
9
9
|
/**
|
10
10
|
* Creates strokes from line segments rather than Bézier curves.
|
11
11
|
*
|
12
|
-
* @beta Output behavior may change significantly between versions. For now, intended for debugging.
|
13
12
|
*/
|
14
13
|
export declare const makePolylineBuilder: ComponentBuilderFactory;
|
15
14
|
export default class PolylineBuilder implements ComponentBuilder {
|
@@ -21,6 +20,7 @@ export default class PolylineBuilder implements ComponentBuilder {
|
|
21
20
|
private widthAverageNumSamples;
|
22
21
|
private lastPoint;
|
23
22
|
private startPoint;
|
23
|
+
private lastLineSegment;
|
24
24
|
constructor(startPoint: StrokeDataPoint, minFitAllowed: number, viewport: Viewport);
|
25
25
|
getBBox(): Rect2;
|
26
26
|
protected getRenderingStyle(): RenderingStyle;
|
@@ -11,7 +11,6 @@ const makeShapeFitAutocorrect_1 = __importDefault(require("./autocorrect/makeSha
|
|
11
11
|
/**
|
12
12
|
* Creates strokes from line segments rather than Bézier curves.
|
13
13
|
*
|
14
|
-
* @beta Output behavior may change significantly between versions. For now, intended for debugging.
|
15
14
|
*/
|
16
15
|
exports.makePolylineBuilder = (0, makeShapeFitAutocorrect_1.default)((initialPoint, viewport) => {
|
17
16
|
const minFit = viewport.getSizeOfPixelOnCanvas();
|
@@ -23,6 +22,7 @@ class PolylineBuilder {
|
|
23
22
|
this.viewport = viewport;
|
24
23
|
this.parts = [];
|
25
24
|
this.widthAverageNumSamples = 1;
|
25
|
+
this.lastLineSegment = null;
|
26
26
|
this.averageWidth = startPoint.width;
|
27
27
|
this.startPoint = {
|
28
28
|
...startPoint,
|
@@ -56,7 +56,7 @@ class PolylineBuilder {
|
|
56
56
|
if (commands.length <= 1) {
|
57
57
|
commands.push({
|
58
58
|
kind: math_1.PathCommandType.LineTo,
|
59
|
-
point: startPoint,
|
59
|
+
point: startPoint.plus(math_1.Vec2.of(this.averageWidth / 4, 0)),
|
60
60
|
});
|
61
61
|
}
|
62
62
|
return {
|
@@ -104,11 +104,18 @@ class PolylineBuilder {
|
|
104
104
|
+ newPoint.width / this.widthAverageNumSamples;
|
105
105
|
const roundedPoint = this.roundPoint(newPoint.pos);
|
106
106
|
if (!roundedPoint.eq(this.lastPoint)) {
|
107
|
+
// If almost exactly in the same line as the previous
|
108
|
+
if (this.lastLineSegment && this.lastLineSegment.direction.dot(roundedPoint.minus(this.lastPoint).normalized()) > 0.997) {
|
109
|
+
this.parts.pop();
|
110
|
+
this.lastPoint = this.lastLineSegment.p1;
|
111
|
+
}
|
107
112
|
this.parts.push({
|
108
113
|
kind: math_1.PathCommandType.LineTo,
|
109
114
|
point: this.roundPoint(newPoint.pos),
|
110
115
|
});
|
111
116
|
this.bbox = this.bbox.grownToPoint(roundedPoint);
|
117
|
+
this.lastLineSegment = new math_1.LineSegment2(this.lastPoint, roundedPoint);
|
118
|
+
this.lastPoint = roundedPoint;
|
112
119
|
}
|
113
120
|
}
|
114
121
|
}
|
@@ -12,6 +12,7 @@ export default class PressureSensitiveFreehandLineBuilder implements ComponentBu
|
|
12
12
|
private isFirstSegment;
|
13
13
|
private pathStartConnector;
|
14
14
|
private mostRecentConnector;
|
15
|
+
private nextCurveStartConnector;
|
15
16
|
private upperSegments;
|
16
17
|
private lowerSegments;
|
17
18
|
private lastUpperBezier;
|
@@ -25,7 +26,6 @@ export default class PressureSensitiveFreehandLineBuilder implements ComponentBu
|
|
25
26
|
private getRenderingStyle;
|
26
27
|
private previewCurrentPath;
|
27
28
|
private previewFullPath;
|
28
|
-
private previewStroke;
|
29
29
|
preview(renderer: AbstractRenderer): void;
|
30
30
|
build(): Stroke;
|
31
31
|
private roundPoint;
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
exports.makePressureSensitiveFreehandLineBuilder = void 0;
|
7
|
-
const bezier_js_1 = require("bezier-js");
|
8
7
|
const math_1 = require("@js-draw/math");
|
9
8
|
const Stroke_1 = __importDefault(require("../Stroke"));
|
10
9
|
const Viewport_1 = __importDefault(require("../../Viewport"));
|
@@ -31,6 +30,7 @@ class PressureSensitiveFreehandLineBuilder {
|
|
31
30
|
this.isFirstSegment = true;
|
32
31
|
this.pathStartConnector = null;
|
33
32
|
this.mostRecentConnector = null;
|
33
|
+
this.nextCurveStartConnector = null;
|
34
34
|
this.lastUpperBezier = null;
|
35
35
|
this.lastLowerBezier = null;
|
36
36
|
this.parts = [];
|
@@ -48,18 +48,18 @@ class PressureSensitiveFreehandLineBuilder {
|
|
48
48
|
fill: this.startPoint.color ?? null,
|
49
49
|
};
|
50
50
|
}
|
51
|
-
previewCurrentPath() {
|
51
|
+
previewCurrentPath(extendWithLatest = true) {
|
52
52
|
const upperPath = this.upperSegments.slice();
|
53
53
|
const lowerPath = this.lowerSegments.slice();
|
54
54
|
let lowerToUpperCap;
|
55
55
|
let pathStartConnector;
|
56
56
|
const currentCurve = this.curveFitter.preview();
|
57
|
-
if (currentCurve) {
|
57
|
+
if (currentCurve && extendWithLatest) {
|
58
58
|
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand } = this.segmentToPath(currentCurve);
|
59
59
|
upperPath.push(upperCurveCommand);
|
60
60
|
lowerPath.push(lowerCurveCommand);
|
61
61
|
lowerToUpperCap = lowerToUpperConnector;
|
62
|
-
pathStartConnector = this.pathStartConnector ?? upperToLowerConnector;
|
62
|
+
pathStartConnector = this.pathStartConnector ?? [upperToLowerConnector];
|
63
63
|
}
|
64
64
|
else {
|
65
65
|
if (this.mostRecentConnector === null || this.pathStartConnector === null) {
|
@@ -100,7 +100,7 @@ class PressureSensitiveFreehandLineBuilder {
|
|
100
100
|
// __/ __/
|
101
101
|
// /___ /
|
102
102
|
// •
|
103
|
-
pathStartConnector,
|
103
|
+
...pathStartConnector,
|
104
104
|
// Move back to the start point:
|
105
105
|
// •
|
106
106
|
// __/ __/
|
@@ -117,13 +117,6 @@ class PressureSensitiveFreehandLineBuilder {
|
|
117
117
|
}
|
118
118
|
return null;
|
119
119
|
}
|
120
|
-
previewStroke() {
|
121
|
-
const pathPreview = this.previewFullPath();
|
122
|
-
if (pathPreview) {
|
123
|
-
return new Stroke_1.default(pathPreview);
|
124
|
-
}
|
125
|
-
return null;
|
126
|
-
}
|
127
120
|
preview(renderer) {
|
128
121
|
const paths = this.previewFullPath();
|
129
122
|
if (paths) {
|
@@ -141,7 +134,7 @@ class PressureSensitiveFreehandLineBuilder {
|
|
141
134
|
// Ensure we have something.
|
142
135
|
this.addCurve(null);
|
143
136
|
}
|
144
|
-
return this.
|
137
|
+
return new Stroke_1.default(this.previewFullPath());
|
145
138
|
}
|
146
139
|
roundPoint(point) {
|
147
140
|
let minFit = Math.min(this.minFitAllowed, this.curveStartWidth / 3);
|
@@ -156,25 +149,16 @@ class PressureSensitiveFreehandLineBuilder {
|
|
156
149
|
return false;
|
157
150
|
}
|
158
151
|
const getIntersection = (curve1, curve2) => {
|
159
|
-
const
|
160
|
-
if (!
|
152
|
+
const intersections = curve1.intersectsBezier(curve2);
|
153
|
+
if (!intersections.length)
|
161
154
|
return null;
|
162
|
-
|
163
|
-
// From http://pomax.github.io/bezierjs/#intersect-curve,
|
164
|
-
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
|
165
|
-
const firstTPair = intersection[0];
|
166
|
-
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
|
167
|
-
if (!match) {
|
168
|
-
throw new Error(`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`);
|
169
|
-
}
|
170
|
-
const t = parseFloat(match[1]);
|
171
|
-
return math_1.Vec2.ofXY(curve1.get(t));
|
155
|
+
return intersections[0].point;
|
172
156
|
};
|
173
157
|
const getExitDirection = (curve) => {
|
174
|
-
return
|
158
|
+
return curve.p2.minus(curve.p1).normalized();
|
175
159
|
};
|
176
160
|
const getEnterDirection = (curve) => {
|
177
|
-
return
|
161
|
+
return curve.p1.minus(curve.p0).normalized();
|
178
162
|
};
|
179
163
|
// Prevent
|
180
164
|
// /
|
@@ -185,8 +169,8 @@ class PressureSensitiveFreehandLineBuilder {
|
|
185
169
|
// where the next stroke and the previous stroke are in different directions.
|
186
170
|
//
|
187
171
|
// Are the exit/enter directions of the previous and current curves in different enough directions?
|
188
|
-
if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.
|
189
|
-
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.
|
172
|
+
if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.35
|
173
|
+
|| getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.35
|
190
174
|
// Also handle if the curves exit/enter directions differ
|
191
175
|
|| getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
|
192
176
|
|| getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
|
@@ -242,32 +226,37 @@ class PressureSensitiveFreehandLineBuilder {
|
|
242
226
|
controlPoint: center.plus(math_1.Vec2.of(width, -width)),
|
243
227
|
endPoint: center.plus(math_1.Vec2.of(width, 0)),
|
244
228
|
});
|
245
|
-
|
229
|
+
const connector = {
|
246
230
|
kind: math_1.PathCommandType.LineTo,
|
247
231
|
point: startPoint,
|
248
232
|
};
|
249
|
-
this.
|
233
|
+
this.pathStartConnector = [connector];
|
234
|
+
this.mostRecentConnector = connector;
|
250
235
|
return;
|
251
236
|
}
|
252
|
-
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, } = this.segmentToPath(curve);
|
253
|
-
|
237
|
+
const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, nextCurveStartConnector, } = this.segmentToPath(curve);
|
238
|
+
let shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
|
254
239
|
if (shouldStartNew) {
|
255
|
-
const part = this.previewCurrentPath();
|
240
|
+
const part = this.previewCurrentPath(false);
|
256
241
|
if (part) {
|
257
242
|
this.parts.push(part);
|
258
243
|
this.upperSegments = [];
|
259
244
|
this.lowerSegments = [];
|
260
245
|
}
|
246
|
+
else {
|
247
|
+
shouldStartNew = false;
|
248
|
+
}
|
261
249
|
}
|
262
250
|
if (this.isFirstSegment || shouldStartNew) {
|
263
251
|
// We draw the upper path (reversed), then the lower path, so we need the
|
264
252
|
// upperToLowerConnector to join the two paths.
|
265
|
-
this.pathStartConnector = upperToLowerConnector;
|
253
|
+
this.pathStartConnector = this.nextCurveStartConnector ?? [upperToLowerConnector];
|
266
254
|
this.isFirstSegment = false;
|
267
255
|
}
|
268
256
|
// With the most recent connector, we're joining the end of the lowerPath to the most recent
|
269
257
|
// upperPath:
|
270
258
|
this.mostRecentConnector = lowerToUpperConnector;
|
259
|
+
this.nextCurveStartConnector = nextCurveStartConnector;
|
271
260
|
this.lowerSegments.push(lowerCurveCommand);
|
272
261
|
this.upperSegments.push(upperCurveCommand);
|
273
262
|
this.lastLowerBezier = lowerCurve;
|
@@ -276,9 +265,9 @@ class PressureSensitiveFreehandLineBuilder {
|
|
276
265
|
}
|
277
266
|
// Returns [upper curve, connector, lower curve]
|
278
267
|
segmentToPath(curve) {
|
279
|
-
const bezier = new
|
280
|
-
let startVec =
|
281
|
-
let endVec =
|
268
|
+
const bezier = new math_1.QuadraticBezier(curve.startPoint, curve.controlPoint, curve.endPoint);
|
269
|
+
let startVec = bezier.normal(0);
|
270
|
+
let endVec = bezier.normal(1);
|
282
271
|
startVec = startVec.times(curve.startWidth / 2);
|
283
272
|
endVec = endVec.times(curve.endWidth / 2);
|
284
273
|
if (!isFinite(startVec.magnitude())) {
|
@@ -289,18 +278,9 @@ class PressureSensitiveFreehandLineBuilder {
|
|
289
278
|
const endPt = curve.endPoint;
|
290
279
|
const controlPoint = curve.controlPoint;
|
291
280
|
// Approximate the normal at the location of the control point
|
292
|
-
|
293
|
-
if (!projectionT) {
|
294
|
-
if (startPt.squareDistanceTo(controlPoint) < endPt.squareDistanceTo(controlPoint)) {
|
295
|
-
projectionT = 0.1;
|
296
|
-
}
|
297
|
-
else {
|
298
|
-
projectionT = 0.9;
|
299
|
-
}
|
300
|
-
}
|
281
|
+
const projectionT = bezier.nearestPointTo(controlPoint).parameterValue;
|
301
282
|
const halfVecT = projectionT;
|
302
|
-
const halfVec =
|
303
|
-
.normalized().times(curve.startWidth / 2 * halfVecT
|
283
|
+
const halfVec = bezier.normal(halfVecT).times(curve.startWidth / 2 * halfVecT
|
304
284
|
+ curve.endWidth / 2 * (1 - halfVecT));
|
305
285
|
// Each starts at startPt ± startVec
|
306
286
|
const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
|
@@ -324,16 +304,29 @@ class PressureSensitiveFreehandLineBuilder {
|
|
324
304
|
kind: math_1.PathCommandType.LineTo,
|
325
305
|
point: upperCurveStartPoint,
|
326
306
|
};
|
307
|
+
// The segment to be used to start the next path (to insert to connect the start of its
|
308
|
+
// lower and the end of its upper).
|
309
|
+
const nextCurveStartConnector = [
|
310
|
+
{
|
311
|
+
kind: math_1.PathCommandType.LineTo,
|
312
|
+
point: upperCurveStartPoint,
|
313
|
+
},
|
314
|
+
{
|
315
|
+
kind: math_1.PathCommandType.LineTo,
|
316
|
+
point: lowerCurveEndPoint,
|
317
|
+
},
|
318
|
+
];
|
327
319
|
const upperCurveCommand = {
|
328
320
|
kind: math_1.PathCommandType.QuadraticBezierTo,
|
329
321
|
controlPoint: upperCurveControlPoint,
|
330
322
|
endPoint: upperCurveEndPoint,
|
331
323
|
};
|
332
|
-
const upperCurve = new
|
333
|
-
const lowerCurve = new
|
324
|
+
const upperCurve = new math_1.QuadraticBezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
|
325
|
+
const lowerCurve = new math_1.QuadraticBezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
|
334
326
|
return {
|
335
327
|
upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
|
336
328
|
upperCurve, lowerCurve,
|
329
|
+
nextCurveStartConnector,
|
337
330
|
};
|
338
331
|
}
|
339
332
|
addPoint(newPoint) {
|