js-draw 0.20.0 → 0.21.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/CHANGELOG.md +8 -0
- package/README.md +4 -4
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/src/Editor.d.ts +4 -1
- package/dist/cjs/src/Editor.js +25 -7
- package/dist/cjs/src/components/AbstractComponent.d.ts +13 -1
- package/dist/cjs/src/components/AbstractComponent.js +19 -9
- package/dist/cjs/src/components/Stroke.d.ts +1 -0
- package/dist/cjs/src/components/Stroke.js +14 -1
- package/dist/cjs/src/components/util/StrokeSmoother.js +12 -14
- package/dist/cjs/src/math/LineSegment2.d.ts +2 -0
- package/dist/cjs/src/math/LineSegment2.js +4 -0
- package/dist/cjs/src/math/Path.d.ts +24 -3
- package/dist/cjs/src/math/Path.js +224 -3
- package/dist/cjs/src/math/Rect2.js +4 -3
- package/dist/cjs/src/math/polynomial/QuadraticBezier.d.ts +1 -1
- package/dist/cjs/src/math/polynomial/QuadraticBezier.js +3 -4
- package/dist/cjs/src/toolbar/HTMLToolbar.js +7 -0
- package/dist/mjs/src/Editor.d.ts +4 -1
- package/dist/mjs/src/Editor.mjs +25 -7
- package/dist/mjs/src/components/AbstractComponent.d.ts +13 -1
- package/dist/mjs/src/components/AbstractComponent.mjs +19 -9
- package/dist/mjs/src/components/Stroke.d.ts +1 -0
- package/dist/mjs/src/components/Stroke.mjs +14 -1
- package/dist/mjs/src/components/util/StrokeSmoother.mjs +12 -14
- package/dist/mjs/src/math/LineSegment2.d.ts +2 -0
- package/dist/mjs/src/math/LineSegment2.mjs +4 -0
- package/dist/mjs/src/math/Path.d.ts +24 -3
- package/dist/mjs/src/math/Path.mjs +224 -3
- package/dist/mjs/src/math/Rect2.mjs +4 -3
- package/dist/mjs/src/math/polynomial/QuadraticBezier.d.ts +1 -1
- package/dist/mjs/src/math/polynomial/QuadraticBezier.mjs +3 -4
- package/dist/mjs/src/toolbar/HTMLToolbar.mjs +8 -1
- package/package.json +1 -1
- package/src/Coloris.css +52 -0
- package/src/Editor.css +12 -0
- package/src/toolbar/toolbar.css +9 -0
@@ -10,6 +10,7 @@ const LineSegment2_1 = __importDefault(require("./LineSegment2"));
|
|
10
10
|
const Mat33_1 = __importDefault(require("./Mat33"));
|
11
11
|
const Rect2_1 = __importDefault(require("./Rect2"));
|
12
12
|
const Vec2_1 = require("./Vec2");
|
13
|
+
const Vec3_1 = __importDefault(require("./Vec3"));
|
13
14
|
var PathCommandType;
|
14
15
|
(function (PathCommandType) {
|
15
16
|
PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
|
@@ -17,6 +18,23 @@ var PathCommandType;
|
|
17
18
|
PathCommandType[PathCommandType["CubicBezierTo"] = 2] = "CubicBezierTo";
|
18
19
|
PathCommandType[PathCommandType["QuadraticBezierTo"] = 3] = "QuadraticBezierTo";
|
19
20
|
})(PathCommandType = exports.PathCommandType || (exports.PathCommandType = {}));
|
21
|
+
// Returns the bounding box of one path segment.
|
22
|
+
const getPartBBox = (part) => {
|
23
|
+
let partBBox;
|
24
|
+
if (part instanceof LineSegment2_1.default) {
|
25
|
+
partBBox = part.bbox;
|
26
|
+
}
|
27
|
+
else if (part instanceof bezier_js_1.Bezier) {
|
28
|
+
const bbox = part.bbox();
|
29
|
+
const width = bbox.x.max - bbox.x.min;
|
30
|
+
const height = bbox.y.max - bbox.y.min;
|
31
|
+
partBBox = new Rect2_1.default(bbox.x.min, bbox.y.min, width, height);
|
32
|
+
}
|
33
|
+
else {
|
34
|
+
partBBox = new Rect2_1.default(part.x, part.y, 0, 0);
|
35
|
+
}
|
36
|
+
return partBBox;
|
37
|
+
};
|
20
38
|
class Path {
|
21
39
|
constructor(startPoint, parts) {
|
22
40
|
this.startPoint = startPoint;
|
@@ -32,6 +50,13 @@ class Path {
|
|
32
50
|
this.bbox = this.bbox.union(Path.computeBBoxForSegment(startPoint, part));
|
33
51
|
}
|
34
52
|
}
|
53
|
+
getExactBBox() {
|
54
|
+
const bboxes = [];
|
55
|
+
for (const part of this.geometry) {
|
56
|
+
bboxes.push(getPartBBox(part));
|
57
|
+
}
|
58
|
+
return Rect2_1.default.union(...bboxes);
|
59
|
+
}
|
35
60
|
// Lazy-loads and returns this path's geometry
|
36
61
|
get geometry() {
|
37
62
|
if (this.cachedGeometry) {
|
@@ -54,6 +79,7 @@ class Path {
|
|
54
79
|
startPoint = part.point;
|
55
80
|
break;
|
56
81
|
case PathCommandType.MoveTo:
|
82
|
+
geometry.push(part.point);
|
57
83
|
startPoint = part.point;
|
58
84
|
break;
|
59
85
|
}
|
@@ -109,11 +135,197 @@ class Path {
|
|
109
135
|
}
|
110
136
|
return Rect2_1.default.bboxOf(points);
|
111
137
|
}
|
112
|
-
|
113
|
-
|
138
|
+
/**
|
139
|
+
* Let `S` be a closed path a distance `strokeRadius` from this path.
|
140
|
+
*
|
141
|
+
* @returns Approximate intersections of `line` with `S` using ray marching, starting from
|
142
|
+
* both end points of `line` and each point in `additionalRaymarchStartPoints`.
|
143
|
+
*/
|
144
|
+
raymarchIntersectionWith(line, strokeRadius, additionalRaymarchStartPoints = []) {
|
145
|
+
var _a, _b;
|
146
|
+
// No intersection between bounding boxes: No possible intersection
|
147
|
+
// of the interior.
|
148
|
+
if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius))) {
|
149
|
+
return [];
|
150
|
+
}
|
151
|
+
const lineLength = line.length;
|
152
|
+
const partDistFunctionRecords = [];
|
153
|
+
// Determine distance functions for all parts that the given line could possibly intersect with
|
154
|
+
for (const part of this.geometry) {
|
155
|
+
const bbox = getPartBBox(part).grownBy(strokeRadius);
|
156
|
+
if (!bbox.intersects(line.bbox)) {
|
157
|
+
continue;
|
158
|
+
}
|
159
|
+
// Signed distance function
|
160
|
+
let partDist;
|
161
|
+
if (part instanceof LineSegment2_1.default) {
|
162
|
+
partDist = (point) => part.distance(point);
|
163
|
+
}
|
164
|
+
else if (part instanceof Vec3_1.default) {
|
165
|
+
partDist = (point) => part.minus(point).magnitude();
|
166
|
+
}
|
167
|
+
else {
|
168
|
+
partDist = (point) => {
|
169
|
+
return part.project(point).d;
|
170
|
+
};
|
171
|
+
}
|
172
|
+
// Part signed distance function (negative result implies `point` is
|
173
|
+
// inside the shape).
|
174
|
+
const partSdf = (point) => partDist(point) - strokeRadius;
|
175
|
+
// If the line can't possibly intersect the part,
|
176
|
+
if (partSdf(line.p1) > lineLength && partSdf(line.p2) > lineLength) {
|
177
|
+
continue;
|
178
|
+
}
|
179
|
+
partDistFunctionRecords.push({
|
180
|
+
part,
|
181
|
+
distFn: partDist,
|
182
|
+
bbox,
|
183
|
+
});
|
184
|
+
}
|
185
|
+
// If no distance functions, there are no intersections.
|
186
|
+
if (partDistFunctionRecords.length === 0) {
|
114
187
|
return [];
|
115
188
|
}
|
189
|
+
// Returns the minimum distance to a part in this stroke, where only parts that the given
|
190
|
+
// line could intersect are considered.
|
191
|
+
const sdf = (point) => {
|
192
|
+
let minDist = Infinity;
|
193
|
+
let minDistPart = null;
|
194
|
+
const uncheckedDistFunctions = [];
|
195
|
+
// First pass: only curves for which the current point is inside
|
196
|
+
// the bounding box.
|
197
|
+
for (const distFnRecord of partDistFunctionRecords) {
|
198
|
+
const { part, distFn, bbox } = distFnRecord;
|
199
|
+
// Check later if the current point isn't in the bounding box.
|
200
|
+
if (!bbox.containsPoint(point)) {
|
201
|
+
uncheckedDistFunctions.push(distFnRecord);
|
202
|
+
continue;
|
203
|
+
}
|
204
|
+
const currentDist = distFn(point);
|
205
|
+
if (currentDist <= minDist) {
|
206
|
+
minDist = currentDist;
|
207
|
+
minDistPart = part;
|
208
|
+
}
|
209
|
+
}
|
210
|
+
// Second pass: Everything else
|
211
|
+
for (const { part, distFn, bbox } of uncheckedDistFunctions) {
|
212
|
+
// Skip if impossible for the distance to the target to be lesser than
|
213
|
+
// the current minimum.
|
214
|
+
if (!bbox.grownBy(minDist).containsPoint(point)) {
|
215
|
+
continue;
|
216
|
+
}
|
217
|
+
const currentDist = distFn(point);
|
218
|
+
if (currentDist <= minDist) {
|
219
|
+
minDist = currentDist;
|
220
|
+
minDistPart = part;
|
221
|
+
}
|
222
|
+
}
|
223
|
+
return [minDistPart, minDist - strokeRadius];
|
224
|
+
};
|
225
|
+
// Raymarch:
|
226
|
+
const maxRaymarchSteps = 7;
|
227
|
+
// Start raymarching from each of these points. This allows detection of multiple
|
228
|
+
// intersections.
|
229
|
+
const startPoints = [
|
230
|
+
line.p1, ...additionalRaymarchStartPoints, line.p2
|
231
|
+
];
|
232
|
+
// Converts a point ON THE LINE to a parameter
|
233
|
+
const pointToParameter = (point) => {
|
234
|
+
// Because line.direction is a unit vector, this computes the length
|
235
|
+
// of the projection of the vector(line.p1->point) onto line.direction.
|
236
|
+
//
|
237
|
+
// Note that this can be negative if the given point is outside of the given
|
238
|
+
// line segment.
|
239
|
+
return point.minus(line.p1).dot(line.direction);
|
240
|
+
};
|
241
|
+
// Sort start points by parameter on the line.
|
242
|
+
// This allows us to determine whether the current value of a parameter
|
243
|
+
// drops down to a value already tested.
|
244
|
+
startPoints.sort((a, b) => {
|
245
|
+
const t_a = pointToParameter(a);
|
246
|
+
const t_b = pointToParameter(b);
|
247
|
+
// Sort in increasing order
|
248
|
+
return t_a - t_b;
|
249
|
+
});
|
116
250
|
const result = [];
|
251
|
+
const stoppingThreshold = strokeRadius / 1000;
|
252
|
+
// Returns the maximum x value explored
|
253
|
+
const raymarchFrom = (startPoint,
|
254
|
+
// Direction to march in (multiplies line.direction)
|
255
|
+
directionMultiplier,
|
256
|
+
// Terminate if the current point corresponds to a parameter
|
257
|
+
// below this.
|
258
|
+
minimumLineParameter) => {
|
259
|
+
let currentPoint = startPoint;
|
260
|
+
let [lastPart, lastDist] = sdf(currentPoint);
|
261
|
+
let lastParameter = pointToParameter(currentPoint);
|
262
|
+
if (lastDist > lineLength) {
|
263
|
+
return lastParameter;
|
264
|
+
}
|
265
|
+
const direction = line.direction.times(directionMultiplier);
|
266
|
+
for (let i = 0; i < maxRaymarchSteps; i++) {
|
267
|
+
// Step in the direction of the edge of the shape.
|
268
|
+
const step = lastDist;
|
269
|
+
currentPoint = currentPoint.plus(direction.times(step));
|
270
|
+
lastParameter = pointToParameter(currentPoint);
|
271
|
+
// If we're below the minimum parameter, stop. We've already tried
|
272
|
+
// this.
|
273
|
+
if (lastParameter <= minimumLineParameter) {
|
274
|
+
return lastParameter;
|
275
|
+
}
|
276
|
+
const [currentPart, signedDist] = sdf(currentPoint);
|
277
|
+
// Ensure we're stepping in the correct direction.
|
278
|
+
// Note that because we could start with a negative distance and work towards a
|
279
|
+
// positive distance, we need absolute values here.
|
280
|
+
if (Math.abs(signedDist) > Math.abs(lastDist)) {
|
281
|
+
// If not, stop.
|
282
|
+
return null;
|
283
|
+
}
|
284
|
+
lastDist = signedDist;
|
285
|
+
lastPart = currentPart;
|
286
|
+
// Is the distance close enough that we can stop early?
|
287
|
+
if (Math.abs(lastDist) < stoppingThreshold) {
|
288
|
+
break;
|
289
|
+
}
|
290
|
+
}
|
291
|
+
// Ensure that the point we ended with is on the line.
|
292
|
+
const isOnLineSegment = lastParameter >= 0 && lastParameter <= lineLength;
|
293
|
+
if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
|
294
|
+
result.push({
|
295
|
+
point: currentPoint,
|
296
|
+
parameterValue: NaN,
|
297
|
+
curve: lastPart,
|
298
|
+
});
|
299
|
+
}
|
300
|
+
return lastParameter;
|
301
|
+
};
|
302
|
+
// The maximum value of the line's parameter explored so far (0 corresponds to
|
303
|
+
// line.p1)
|
304
|
+
let maxLineT = 0;
|
305
|
+
// Raymarch for each start point.
|
306
|
+
//
|
307
|
+
// Use a for (i from 0 to length) loop because startPoints may be added
|
308
|
+
// during iteration.
|
309
|
+
for (let i = 0; i < startPoints.length; i++) {
|
310
|
+
const startPoint = startPoints[i];
|
311
|
+
// Try raymarching in both directions.
|
312
|
+
maxLineT = Math.max(maxLineT, (_a = raymarchFrom(startPoint, 1, maxLineT)) !== null && _a !== void 0 ? _a : maxLineT);
|
313
|
+
maxLineT = Math.max(maxLineT, (_b = raymarchFrom(startPoint, -1, maxLineT)) !== null && _b !== void 0 ? _b : maxLineT);
|
314
|
+
}
|
315
|
+
return result;
|
316
|
+
}
|
317
|
+
/**
|
318
|
+
* Returns a list of intersections with this path. If `strokeRadius` is given,
|
319
|
+
* intersections are approximated with the surface `strokeRadius` away from this.
|
320
|
+
*
|
321
|
+
* If `strokeRadius > 0`, the resultant `parameterValue` has no defined value.
|
322
|
+
*/
|
323
|
+
intersection(line, strokeRadius) {
|
324
|
+
let result = [];
|
325
|
+
// Is any intersection between shapes within the bounding boxes impossible?
|
326
|
+
if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius !== null && strokeRadius !== void 0 ? strokeRadius : 0))) {
|
327
|
+
return [];
|
328
|
+
}
|
117
329
|
for (const part of this.geometry) {
|
118
330
|
if (part instanceof LineSegment2_1.default) {
|
119
331
|
const intersection = part.intersection(line);
|
@@ -125,7 +337,7 @@ class Path {
|
|
125
337
|
});
|
126
338
|
}
|
127
339
|
}
|
128
|
-
else {
|
340
|
+
else if (part instanceof bezier_js_1.Bezier) {
|
129
341
|
const intersectionPoints = part.intersects(line).map(t => {
|
130
342
|
// We're using the .intersects(line) function, which is documented
|
131
343
|
// to always return numbers. However, to satisfy the type checker (and
|
@@ -148,6 +360,15 @@ class Path {
|
|
148
360
|
result.push(...intersectionPoints);
|
149
361
|
}
|
150
362
|
}
|
363
|
+
// If given a non-zero strokeWidth, attempt to raymarch.
|
364
|
+
// Even if raymarching, we need to collect starting points.
|
365
|
+
// We use the above-calculated intersections for this.
|
366
|
+
const doRaymarching = strokeRadius && strokeRadius > 1e-8;
|
367
|
+
if (doRaymarching) {
|
368
|
+
// Starting points for raymarching (in addition to the end points of the line).
|
369
|
+
const startPoints = result.map(intersection => intersection.point);
|
370
|
+
result = this.raymarchIntersectionWith(line, strokeRadius, startPoints);
|
371
|
+
}
|
151
372
|
return result;
|
152
373
|
}
|
153
374
|
static mapPathCommand(part, mapping) {
|
@@ -73,9 +73,7 @@ class Rect2 {
|
|
73
73
|
}
|
74
74
|
// Returns a new rectangle containing both [this] and [other].
|
75
75
|
union(other) {
|
76
|
-
|
77
|
-
const bottomRight = this.bottomRight.zip(other.bottomRight, Math.max);
|
78
|
-
return Rect2.fromCorners(topLeft, bottomRight);
|
76
|
+
return Rect2.union(this, other);
|
79
77
|
}
|
80
78
|
// Returns a the subdivision of this into [columns] columns
|
81
79
|
// and [rows] rows. For example,
|
@@ -113,6 +111,9 @@ class Rect2 {
|
|
113
111
|
}
|
114
112
|
// Returns this grown by [margin] in both the x and y directions.
|
115
113
|
grownBy(margin) {
|
114
|
+
if (margin === 0) {
|
115
|
+
return this;
|
116
|
+
}
|
116
117
|
return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
|
117
118
|
}
|
118
119
|
getClosestPointOnBoundaryTo(target) {
|
@@ -21,7 +21,7 @@ export default class QuadraticBezier {
|
|
21
21
|
*/
|
22
22
|
approximateDistance(point: Point2): number;
|
23
23
|
/**
|
24
|
-
* @returns the exact distance from `point` to this.
|
24
|
+
* @returns the (more) exact distance from `point` to this.
|
25
25
|
*/
|
26
26
|
distance(point: Point2): number;
|
27
27
|
normal(t: number): Vec2;
|
@@ -97,15 +97,14 @@ class QuadraticBezier {
|
|
97
97
|
return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
|
98
98
|
}
|
99
99
|
/**
|
100
|
-
* @returns the exact distance from `point` to this.
|
100
|
+
* @returns the (more) exact distance from `point` to this.
|
101
101
|
*/
|
102
102
|
distance(point) {
|
103
103
|
if (!this.bezierJs) {
|
104
104
|
this.bezierJs = new bezier_js_1.Bezier([this.p0.xy, this.p1.xy, this.p2.xy]);
|
105
105
|
}
|
106
|
-
|
107
|
-
|
108
|
-
return dist;
|
106
|
+
// .d: Distance
|
107
|
+
return this.bezierJs.project(point.xy).d;
|
109
108
|
}
|
110
109
|
normal(t) {
|
111
110
|
const tangent = this.derivativeAt(t);
|
@@ -66,6 +66,13 @@ class HTMLToolbar {
|
|
66
66
|
const closePickerOverlay = document.createElement('div');
|
67
67
|
closePickerOverlay.className = `${exports.toolbarCSSPrefix}closeColorPickerOverlay`;
|
68
68
|
this.editor.createHTMLOverlay(closePickerOverlay);
|
69
|
+
// Hide the color picker when attempting to draw on the overlay.
|
70
|
+
this.listeners.push(this.editor.handlePointerEventsFrom(closePickerOverlay, (eventName) => {
|
71
|
+
if (eventName === 'pointerdown') {
|
72
|
+
(0, coloris_1.close)();
|
73
|
+
}
|
74
|
+
return true;
|
75
|
+
}));
|
69
76
|
const maxSwatchLen = 12;
|
70
77
|
const swatches = [
|
71
78
|
Color4_1.default.red.toHexString(),
|
package/dist/mjs/src/Editor.d.ts
CHANGED
@@ -191,7 +191,10 @@ export declare class Editor {
|
|
191
191
|
* });
|
192
192
|
* ```
|
193
193
|
*/
|
194
|
-
handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter):
|
194
|
+
handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter): {
|
195
|
+
/** Remove all event listeners registered by this function. */
|
196
|
+
remove: () => void;
|
197
|
+
};
|
195
198
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
196
199
|
handleKeyEventsFrom(elem: HTMLElement): void;
|
197
200
|
/** `apply` a command. `command` will be announced for accessibility. */
|
package/dist/mjs/src/Editor.mjs
CHANGED
@@ -455,20 +455,38 @@ export class Editor {
|
|
455
455
|
handlePointerEventsFrom(elem, filter) {
|
456
456
|
// May be required to prevent text selection on iOS/Safari:
|
457
457
|
// See https://stackoverflow.com/a/70992717/17055750
|
458
|
-
|
459
|
-
|
458
|
+
const touchstartListener = (evt) => evt.preventDefault();
|
459
|
+
const contextmenuListener = (evt) => {
|
460
460
|
// Don't show a context menu
|
461
461
|
evt.preventDefault();
|
462
|
-
}
|
462
|
+
};
|
463
|
+
const listeners = {
|
464
|
+
'touchstart': touchstartListener,
|
465
|
+
'contextmenu': contextmenuListener,
|
466
|
+
};
|
463
467
|
const eventNames = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
|
464
468
|
for (const eventName of eventNames) {
|
465
|
-
|
466
|
-
|
469
|
+
listeners[eventName] = (evt) => {
|
470
|
+
// This listener will only be called in the context of PointerEvents.
|
471
|
+
const event = evt;
|
472
|
+
if (filter && !filter(eventName, event)) {
|
467
473
|
return true;
|
468
474
|
}
|
469
|
-
return this.handleHTMLPointerEvent(eventName,
|
470
|
-
}
|
475
|
+
return this.handleHTMLPointerEvent(eventName, event);
|
476
|
+
};
|
471
477
|
}
|
478
|
+
// Add all listeners.
|
479
|
+
for (const eventName in listeners) {
|
480
|
+
elem.addEventListener(eventName, listeners[eventName]);
|
481
|
+
}
|
482
|
+
return {
|
483
|
+
/** Remove all event listeners registered by this function. */
|
484
|
+
remove: () => {
|
485
|
+
for (const eventName in listeners) {
|
486
|
+
elem.removeEventListener(eventName, listeners[eventName]);
|
487
|
+
}
|
488
|
+
},
|
489
|
+
};
|
472
490
|
}
|
473
491
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
474
492
|
handleKeyEventsFrom(elem) {
|
@@ -32,8 +32,15 @@ export default abstract class AbstractComponent {
|
|
32
32
|
/** See {@link attachLoadSaveData} */
|
33
33
|
getLoadSaveData(): LoadSaveDataTable;
|
34
34
|
getZIndex(): number;
|
35
|
-
/**
|
35
|
+
/**
|
36
|
+
* @returns the bounding box of this. This can be a slight overestimate if doing so
|
37
|
+
* significantly improves performance.
|
38
|
+
*/
|
36
39
|
getBBox(): Rect2;
|
40
|
+
/**
|
41
|
+
* @returns the bounding box of this. Unlike `getBBox`, this should **not** be a rough estimate.
|
42
|
+
*/
|
43
|
+
getExactBBox(): Rect2;
|
37
44
|
/** Called when this component is added to the given image. */
|
38
45
|
onAddToImage(_image: EditorImage): void;
|
39
46
|
onRemoveFromImage(): void;
|
@@ -43,6 +50,11 @@ export default abstract class AbstractComponent {
|
|
43
50
|
/**
|
44
51
|
* @returns true if this component intersects `rect` -- it is entirely contained
|
45
52
|
* within the rectangle or one of the rectangle's edges intersects this component.
|
53
|
+
*
|
54
|
+
* The default implementation assumes that `this.getExactBBox()` returns a tight bounding box
|
55
|
+
* -- that any horiziontal/vertical line that intersects this' boounding box also
|
56
|
+
* intersects a point in this component. If this is not the case, components must override
|
57
|
+
* this function.
|
46
58
|
*/
|
47
59
|
intersectsRect(rect: Rect2): boolean;
|
48
60
|
protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
|
@@ -55,30 +55,40 @@ export default class AbstractComponent {
|
|
55
55
|
getZIndex() {
|
56
56
|
return this.zIndex;
|
57
57
|
}
|
58
|
-
/**
|
58
|
+
/**
|
59
|
+
* @returns the bounding box of this. This can be a slight overestimate if doing so
|
60
|
+
* significantly improves performance.
|
61
|
+
*/
|
59
62
|
getBBox() {
|
60
63
|
return this.contentBBox;
|
61
64
|
}
|
65
|
+
/**
|
66
|
+
* @returns the bounding box of this. Unlike `getBBox`, this should **not** be a rough estimate.
|
67
|
+
*/
|
68
|
+
getExactBBox() {
|
69
|
+
return this.getBBox();
|
70
|
+
}
|
62
71
|
/** Called when this component is added to the given image. */
|
63
72
|
onAddToImage(_image) { }
|
64
73
|
onRemoveFromImage() { }
|
65
74
|
/**
|
66
75
|
* @returns true if this component intersects `rect` -- it is entirely contained
|
67
76
|
* within the rectangle or one of the rectangle's edges intersects this component.
|
77
|
+
*
|
78
|
+
* The default implementation assumes that `this.getExactBBox()` returns a tight bounding box
|
79
|
+
* -- that any horiziontal/vertical line that intersects this' boounding box also
|
80
|
+
* intersects a point in this component. If this is not the case, components must override
|
81
|
+
* this function.
|
68
82
|
*/
|
69
83
|
intersectsRect(rect) {
|
70
|
-
// If this component intersects
|
84
|
+
// If this component intersects the given rectangle,
|
71
85
|
// it is either contained entirely within rect or intersects one of rect's edges.
|
72
86
|
// If contained within,
|
73
|
-
if (rect.containsRect(this.
|
87
|
+
if (rect.containsRect(this.getExactBBox())) {
|
74
88
|
return true;
|
75
89
|
}
|
76
|
-
//
|
77
|
-
|
78
|
-
const testLines = [];
|
79
|
-
for (const subregion of rect.divideIntoGrid(2, 2)) {
|
80
|
-
testLines.push(...subregion.getEdges());
|
81
|
-
}
|
90
|
+
// Otherwise check if it intersects one of the rectangle's edges.
|
91
|
+
const testLines = rect.getEdges();
|
82
92
|
return testLines.some(edge => this.intersects(edge));
|
83
93
|
}
|
84
94
|
// Returns a command that, when applied, transforms this by [affineTransfm] and
|
@@ -53,6 +53,7 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
|
|
53
53
|
render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
54
54
|
getProportionalRenderingTime(): number;
|
55
55
|
private bboxForPart;
|
56
|
+
getExactBBox(): Rect2;
|
56
57
|
protected applyTransformation(affineTransfm: Mat33): void;
|
57
58
|
/**
|
58
59
|
* @returns the {@link Path.union} of all paths that make up this stroke.
|
@@ -109,8 +109,11 @@ export default class Stroke extends AbstractComponent {
|
|
109
109
|
}
|
110
110
|
}
|
111
111
|
intersects(line) {
|
112
|
+
var _a;
|
112
113
|
for (const part of this.parts) {
|
113
|
-
|
114
|
+
const strokeWidth = (_a = part.style.stroke) === null || _a === void 0 ? void 0 : _a.width;
|
115
|
+
const strokeRadius = strokeWidth ? strokeWidth / 2 : undefined;
|
116
|
+
if (part.path.intersection(line, strokeRadius).length > 0) {
|
114
117
|
return true;
|
115
118
|
}
|
116
119
|
}
|
@@ -144,6 +147,16 @@ export default class Stroke extends AbstractComponent {
|
|
144
147
|
}
|
145
148
|
return origBBox.grownBy(style.stroke.width / 2);
|
146
149
|
}
|
150
|
+
getExactBBox() {
|
151
|
+
let bbox = null;
|
152
|
+
for (const { path, style } of this.parts) {
|
153
|
+
// Paths' default .bbox can be
|
154
|
+
const partBBox = this.bboxForPart(path.getExactBBox(), style);
|
155
|
+
bbox !== null && bbox !== void 0 ? bbox : (bbox = partBBox);
|
156
|
+
bbox = bbox.union(partBBox);
|
157
|
+
}
|
158
|
+
return bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
|
159
|
+
}
|
147
160
|
applyTransformation(affineTransfm) {
|
148
161
|
this.contentBBox = Rect2.empty;
|
149
162
|
let isFirstPart = true;
|
@@ -146,7 +146,7 @@ export class StrokeSmoother {
|
|
146
146
|
}
|
147
147
|
let exitingVec = this.computeExitingVec();
|
148
148
|
// Find the intersection between the entering vector and the exiting vector
|
149
|
-
const maxRelativeLength = 2
|
149
|
+
const maxRelativeLength = 2;
|
150
150
|
const segmentStart = this.buffer[0];
|
151
151
|
const segmentEnd = newPoint.pos;
|
152
152
|
const startEndDist = segmentEnd.minus(segmentStart).magnitude();
|
@@ -184,22 +184,20 @@ export class StrokeSmoother {
|
|
184
184
|
// Should we start making a new curve? Check whether all buffer points are within
|
185
185
|
// ±strokeWidth of the curve.
|
186
186
|
const curveMatchesPoints = (curve) => {
|
187
|
-
|
188
|
-
|
189
|
-
const
|
187
|
+
const minFit = Math.min(Math.max(Math.min(this.curveStartWidth, this.curveEndWidth) / 4, this.minFitAllowed), this.maxFitAllowed);
|
188
|
+
// The sum of distances greater than minFit must not exceed this:
|
189
|
+
const maxNonMatchingDistSum = minFit;
|
190
|
+
// Sum of distances greater than minFit.
|
191
|
+
let nonMatchingDistSum = 0;
|
190
192
|
for (const point of this.buffer) {
|
191
193
|
let dist = curve.approximateDistance(point);
|
192
|
-
if (dist > minFit
|
193
|
-
//
|
194
|
-
|
195
|
-
|
194
|
+
if (dist > minFit) {
|
195
|
+
// Use the more accurate distance function
|
196
|
+
dist = curve.distance(point);
|
197
|
+
nonMatchingDistSum += Math.max(0, dist - minFit);
|
198
|
+
if (nonMatchingDistSum > maxNonMatchingDistSum) {
|
199
|
+
return false; // false: Curve doesn't match points well enough.
|
196
200
|
}
|
197
|
-
if (dist > minFit || dist > this.maxFitAllowed) {
|
198
|
-
nonMatching++;
|
199
|
-
}
|
200
|
-
}
|
201
|
-
if (nonMatching >= maxNonMatching) {
|
202
|
-
return false;
|
203
201
|
}
|
204
202
|
}
|
205
203
|
return true;
|
@@ -18,6 +18,8 @@ export default class LineSegment2 {
|
|
18
18
|
intersection(other: LineSegment2): IntersectionResult | null;
|
19
19
|
intersects(other: LineSegment2): boolean;
|
20
20
|
closestPointTo(target: Point2): import("./Vec3").default;
|
21
|
+
/** Returns the distance from this line segment to `target`. */
|
22
|
+
distance(target: Point2): number;
|
21
23
|
transformedBy(affineTransfm: Mat33): LineSegment2;
|
22
24
|
toString(): string;
|
23
25
|
}
|
@@ -116,6 +116,10 @@ export default class LineSegment2 {
|
|
116
116
|
return this.p1;
|
117
117
|
}
|
118
118
|
}
|
119
|
+
/** Returns the distance from this line segment to `target`. */
|
120
|
+
distance(target) {
|
121
|
+
return this.closestPointTo(target).minus(target).magnitude();
|
122
|
+
}
|
119
123
|
transformedBy(affineTransfm) {
|
120
124
|
return new LineSegment2(affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2));
|
121
125
|
}
|
@@ -32,21 +32,42 @@ export interface MoveToPathCommand {
|
|
32
32
|
}
|
33
33
|
export type PathCommand = CubicBezierPathCommand | LinePathCommand | QuadraticBezierPathCommand | MoveToPathCommand;
|
34
34
|
interface IntersectionResult {
|
35
|
-
curve: LineSegment2 | Bezier;
|
35
|
+
curve: LineSegment2 | Bezier | Point2;
|
36
36
|
parameterValue: number;
|
37
37
|
point: Point2;
|
38
38
|
}
|
39
|
+
type GeometryType = LineSegment2 | Bezier | Point2;
|
40
|
+
type GeometryArrayType = Array<GeometryType>;
|
39
41
|
export default class Path {
|
40
42
|
readonly startPoint: Point2;
|
41
43
|
readonly parts: PathCommand[];
|
44
|
+
/**
|
45
|
+
* A rough estimate of the bounding box of the path.
|
46
|
+
* A slight overestimate.
|
47
|
+
* See {@link getExactBBox}
|
48
|
+
*/
|
42
49
|
readonly bbox: Rect2;
|
43
50
|
constructor(startPoint: Point2, parts: PathCommand[]);
|
51
|
+
getExactBBox(): Rect2;
|
44
52
|
private cachedGeometry;
|
45
|
-
get geometry():
|
53
|
+
get geometry(): GeometryArrayType;
|
46
54
|
private cachedPolylineApproximation;
|
47
55
|
polylineApproximation(): LineSegment2[];
|
48
56
|
static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2;
|
49
|
-
|
57
|
+
/**
|
58
|
+
* Let `S` be a closed path a distance `strokeRadius` from this path.
|
59
|
+
*
|
60
|
+
* @returns Approximate intersections of `line` with `S` using ray marching, starting from
|
61
|
+
* both end points of `line` and each point in `additionalRaymarchStartPoints`.
|
62
|
+
*/
|
63
|
+
private raymarchIntersectionWith;
|
64
|
+
/**
|
65
|
+
* Returns a list of intersections with this path. If `strokeRadius` is given,
|
66
|
+
* intersections are approximated with the surface `strokeRadius` away from this.
|
67
|
+
*
|
68
|
+
* If `strokeRadius > 0`, the resultant `parameterValue` has no defined value.
|
69
|
+
*/
|
70
|
+
intersection(line: LineSegment2, strokeRadius?: number): IntersectionResult[];
|
50
71
|
private static mapPathCommand;
|
51
72
|
mapPoints(mapping: (point: Point2) => Point2): Path;
|
52
73
|
transformedBy(affineTransfm: Mat33): Path;
|