@altpsyche/maths 0.8.0 → 0.9.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/README.md +41 -0
- package/dist/figure/area.d.ts +22 -0
- package/dist/figure/area.js +45 -0
- package/dist/figure/boolean.d.ts +13 -0
- package/dist/figure/boolean.js +223 -0
- package/dist/figure/cut.d.ts +36 -0
- package/dist/figure/cut.js +83 -0
- package/dist/figure/inside.d.ts +53 -0
- package/dist/figure/inside.js +142 -0
- package/dist/figure/intersect.d.ts +36 -0
- package/dist/figure/intersect.js +417 -0
- package/dist/figure/morph.js +2 -16
- package/dist/figure/path.d.ts +11 -0
- package/dist/figure/path.js +28 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,47 @@ matters. The slope in the demo is a value of the walk, so it is worked out by th
|
|
|
163
163
|
track and cannot drift from the dot. The rise is counted by the clock, which is honest only because
|
|
164
164
|
the dot has stopped by the time it counts: two clocks running at once would be free to disagree.
|
|
165
165
|
|
|
166
|
+
## Two shapes combined
|
|
167
|
+
|
|
168
|
+
<img src="docs/boolean.svg" width="720" alt="Two discs drawn three times side by side: everything either one covers, only what both cover, and the first with the second taken out of it.">
|
|
169
|
+
|
|
170
|
+
`unionOf` is everything either path covers, `intersectionOf` is only what both cover, and
|
|
171
|
+
`differenceOf` is the first with the second taken out of it. Each input is closed loops that do not
|
|
172
|
+
cross themselves, and a loop left open is closed by a straight run back to where it started before
|
|
173
|
+
anything else happens.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { circle, differenceOf, intersectionOf, unionOf, vec2 } from '@altpsyche/maths';
|
|
177
|
+
|
|
178
|
+
const first = circle(vec2(0, 0), 0.9);
|
|
179
|
+
const second = circle(vec2(-0.6, 0), 0.36);
|
|
180
|
+
|
|
181
|
+
unionOf(first, second);
|
|
182
|
+
intersectionOf(first, second);
|
|
183
|
+
differenceOf(first, second);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
A result may have a hole even though an input may not. A disc with a smaller disc taken out of it is
|
|
187
|
+
a ring, which is an outer loop and an inner loop wound the opposite way, and the nonzero rule the
|
|
188
|
+
mark already carries leaves the middle empty.
|
|
189
|
+
|
|
190
|
+
The work happens in four steps, and each of them is a call of its own. `curveCrossings` says where
|
|
191
|
+
two cubics cross, by halving both curves and following only the halves whose boxes still overlap,
|
|
192
|
+
then sharpening what it finds by Newton's method. `cutPath` puts a cut wherever something crosses,
|
|
193
|
+
so that afterwards every piece is wholly inside the other path or wholly outside it. `containsPoint`
|
|
194
|
+
decides which of those a piece is, by counting how many times the other path winds round its middle.
|
|
195
|
+
`areaOf` says how much a path encloses, in closed form rather than by sampling, which is what every
|
|
196
|
+
claim above is checked against.
|
|
197
|
+
|
|
198
|
+
<img src="docs/boolean-strip.svg" width="960" alt="Four frames side by side, each showing the three panels, as the small disc walks from clear of the large one, through touching it at one point, through overlapping it, to sitting wholly inside it.">
|
|
199
|
+
|
|
200
|
+
Four times of one figure. A small disc walks across a larger one: clear of it, touching it at one
|
|
201
|
+
point, crossing it at two, and wholly inside it. Those are the four cases this kind of code gets
|
|
202
|
+
silently wrong, which is why the demo walks through all of them rather than drawing one.
|
|
203
|
+
|
|
204
|
+
Two edges that lie on top of each other for a stretch have no one answer, and what comes back for
|
|
205
|
+
them is decided by the tolerance rather than by the geometry.
|
|
206
|
+
|
|
166
207
|
## The animations
|
|
167
208
|
|
|
168
209
|
`fadeIn`, `fadeOut`, `fadeTo`, `draw`, `morph`, `morphEquation`, `countTo`, `moveBy`, `rotate`,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much a path encloses.
|
|
3
|
+
*
|
|
4
|
+
* This is Green's theorem, which turns the area inside a closed loop into an
|
|
5
|
+
* integral round its edge, and for a cubic that integral has a closed form. So
|
|
6
|
+
* the answer is worked out rather than sampled, and it is exact for the shape
|
|
7
|
+
* the cubics actually draw.
|
|
8
|
+
*
|
|
9
|
+
* The sign is the direction the loop is wound in, positive anticlockwise. That
|
|
10
|
+
* is what makes a hole subtract: a loop wound the other way inside another one
|
|
11
|
+
* encloses a negative amount, and the two added together are the ring.
|
|
12
|
+
*/
|
|
13
|
+
import type { Path } from './path.js';
|
|
14
|
+
/**
|
|
15
|
+
* How much a path encloses, positive where it is wound anticlockwise.
|
|
16
|
+
*
|
|
17
|
+
* A subpath left open is closed by the straight run back to where it started,
|
|
18
|
+
* since an open loop encloses nothing on its own. Every subpath is added, so a
|
|
19
|
+
* ring written as an outer loop and an inner loop wound the other way comes
|
|
20
|
+
* back as the difference between the two discs.
|
|
21
|
+
*/
|
|
22
|
+
export declare function areaOf(path: Path): number;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Twice the area of the triangle two points make with the origin, positive
|
|
2
|
+
* anticlockwise. */
|
|
3
|
+
function wedge(a, b) {
|
|
4
|
+
return a.x * b.y - b.x * a.y;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* What one piece contributes to the integral round the loop.
|
|
8
|
+
*
|
|
9
|
+
* Each pair of the piece's four points contributes the triangle it makes with
|
|
10
|
+
* the origin, and the weights are what integrating a cubic against its own
|
|
11
|
+
* derivative leaves: six, three, one, three, three and six twentieths, in the
|
|
12
|
+
* order the pairs are taken.
|
|
13
|
+
*/
|
|
14
|
+
function pieceArea(from, curve) {
|
|
15
|
+
const { control1, control2, to } = curve;
|
|
16
|
+
return ((6 * wedge(from, control1) +
|
|
17
|
+
3 * wedge(from, control2) +
|
|
18
|
+
wedge(from, to) +
|
|
19
|
+
3 * wedge(control1, control2) +
|
|
20
|
+
3 * wedge(control1, to) +
|
|
21
|
+
6 * wedge(control2, to)) /
|
|
22
|
+
20);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* How much a path encloses, positive where it is wound anticlockwise.
|
|
26
|
+
*
|
|
27
|
+
* A subpath left open is closed by the straight run back to where it started,
|
|
28
|
+
* since an open loop encloses nothing on its own. Every subpath is added, so a
|
|
29
|
+
* ring written as an outer loop and an inner loop wound the other way comes
|
|
30
|
+
* back as the difference between the two discs.
|
|
31
|
+
*/
|
|
32
|
+
export function areaOf(path) {
|
|
33
|
+
let total = 0;
|
|
34
|
+
for (const subpath of path) {
|
|
35
|
+
if (subpath.curves.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
let from = subpath.start;
|
|
38
|
+
for (const curve of subpath.curves) {
|
|
39
|
+
total += pieceArea(from, curve);
|
|
40
|
+
from = curve.to;
|
|
41
|
+
}
|
|
42
|
+
total += wedge(from, subpath.start) / 2;
|
|
43
|
+
}
|
|
44
|
+
return total;
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Path } from './path.js';
|
|
2
|
+
export interface BooleanOptions {
|
|
3
|
+
/** How close two things come before they count as the same place, in the
|
|
4
|
+
* picture's own units. It decides where two paths are read as crossing and
|
|
5
|
+
* which ends are read as meeting. */
|
|
6
|
+
readonly tolerance?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Everything either path covers. */
|
|
9
|
+
export declare function unionOf(first: Path, second: Path, options?: BooleanOptions): Path;
|
|
10
|
+
/** Only what both paths cover. */
|
|
11
|
+
export declare function intersectionOf(first: Path, second: Path, options?: BooleanOptions): Path;
|
|
12
|
+
/** The first path with the second taken out of it. */
|
|
13
|
+
export declare function differenceOf(first: Path, second: Path, options?: BooleanOptions): Path;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Union, intersection and difference on paths.
|
|
3
|
+
*
|
|
4
|
+
* Both paths are cut at every place they cross, so that after the cutting every
|
|
5
|
+
* piece is wholly inside the other path or wholly outside it. Each piece is
|
|
6
|
+
* then decided one way or the other, the operation says which pieces it wants,
|
|
7
|
+
* and what is kept is stitched back into loops by joining ends that meet.
|
|
8
|
+
*
|
|
9
|
+
* The difference takes the second path's kept pieces the other way round, which
|
|
10
|
+
* is what turns a disc taken out of the middle of another disc into a ring: the
|
|
11
|
+
* inner loop is wound against the outer one and the nonzero rule the mark
|
|
12
|
+
* carries leaves it empty.
|
|
13
|
+
*
|
|
14
|
+
* A piece lying along the other path's own edge is decided by which way the two
|
|
15
|
+
* run rather than by which side it is on, because a point on an edge is the one
|
|
16
|
+
* place the winding count has no answer for. Two paths walking a shared stretch
|
|
17
|
+
* the same way have their solid on the same side of it, so the stretch is on the
|
|
18
|
+
* edge of a union and of an overlap and is kept once. Walking it opposite ways
|
|
19
|
+
* puts their solids on opposite sides, so the stretch is inside a union and
|
|
20
|
+
* outside an overlap, and a difference keeps the first path's copy of it.
|
|
21
|
+
*/
|
|
22
|
+
import { vec2 } from '../values/vec2.js';
|
|
23
|
+
import { pointOn, slopeOn } from './path.js';
|
|
24
|
+
import { cutPath } from './cut.js';
|
|
25
|
+
import { curveCrossings } from './intersect.js';
|
|
26
|
+
import { flattenPath, nearestEdge, windingAt } from './inside.js';
|
|
27
|
+
const TOLERANCE = 1e-6;
|
|
28
|
+
/** A subpath left open closed by the straight run back to where it started,
|
|
29
|
+
* since a path with an open loop has no inside for any of this to ask about. */
|
|
30
|
+
function closedLoops(path) {
|
|
31
|
+
return path.map((subpath) => {
|
|
32
|
+
if (subpath.curves.length === 0)
|
|
33
|
+
return subpath;
|
|
34
|
+
const end = subpath.curves[subpath.curves.length - 1].to;
|
|
35
|
+
if (subpath.closed && end.x === subpath.start.x && end.y === subpath.start.y) {
|
|
36
|
+
return subpath;
|
|
37
|
+
}
|
|
38
|
+
const curves = end.x === subpath.start.x && end.y === subpath.start.y
|
|
39
|
+
? subpath.curves
|
|
40
|
+
: [
|
|
41
|
+
...subpath.curves,
|
|
42
|
+
{
|
|
43
|
+
control1: vec2.lerp(end, subpath.start, 1 / 3),
|
|
44
|
+
control2: vec2.lerp(end, subpath.start, 2 / 3),
|
|
45
|
+
to: subpath.start,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
return { start: subpath.start, curves, closed: true };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function piecesOf(path) {
|
|
52
|
+
const pieces = [];
|
|
53
|
+
for (const subpath of path) {
|
|
54
|
+
let from = subpath.start;
|
|
55
|
+
for (const curve of subpath.curves) {
|
|
56
|
+
pieces.push({ from, curve });
|
|
57
|
+
from = curve.to;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return pieces;
|
|
61
|
+
}
|
|
62
|
+
/** A piece walked the other way, which is what puts a hole the opposite way
|
|
63
|
+
* round from the loop it sits in. */
|
|
64
|
+
function reversed(piece) {
|
|
65
|
+
return {
|
|
66
|
+
from: piece.curve.to,
|
|
67
|
+
curve: { control1: piece.curve.control2, control2: piece.curve.control1, to: piece.from },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Where the two paths cross, as the cuts each of them needs. */
|
|
71
|
+
function cutsBetween(first, second, tolerance) {
|
|
72
|
+
const forFirst = [];
|
|
73
|
+
const forSecond = [];
|
|
74
|
+
for (let left = 0; left < first.length; left++) {
|
|
75
|
+
let leftFrom = first[left].start;
|
|
76
|
+
for (let leftPiece = 0; leftPiece < first[left].curves.length; leftPiece++) {
|
|
77
|
+
const leftCurve = first[left].curves[leftPiece];
|
|
78
|
+
for (let right = 0; right < second.length; right++) {
|
|
79
|
+
let rightFrom = second[right].start;
|
|
80
|
+
for (let rightPiece = 0; rightPiece < second[right].curves.length; rightPiece++) {
|
|
81
|
+
const rightCurve = second[right].curves[rightPiece];
|
|
82
|
+
for (const crossing of curveCrossings(leftFrom, leftCurve, rightFrom, rightCurve, { tolerance })) {
|
|
83
|
+
forFirst.push({ subpath: left, curve: leftPiece, along: crossing.alongFirst });
|
|
84
|
+
forSecond.push({ subpath: right, curve: rightPiece, along: crossing.alongSecond });
|
|
85
|
+
}
|
|
86
|
+
rightFrom = rightCurve.to;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
leftFrom = leftCurve.to;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [forFirst, forSecond];
|
|
93
|
+
}
|
|
94
|
+
function place(point) {
|
|
95
|
+
return `(${point.x.toFixed(6)}, ${point.y.toFixed(6)})`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* What a run of pieces that will not close stops with.
|
|
99
|
+
*
|
|
100
|
+
* Handing it back as a loop anyway is the one failure a caller cannot see: the
|
|
101
|
+
* shape drawn is wrong and nothing about it says so. Every input is closed
|
|
102
|
+
* loops, so a run that will not close is this code being wrong rather than the
|
|
103
|
+
* caller, and stopping is what makes that visible on the frame it happens.
|
|
104
|
+
*/
|
|
105
|
+
function refuse(pieces, start, end, tolerance) {
|
|
106
|
+
throw new Error(`the pieces kept do not close into a loop: ${pieces} of them run from ${place(start)} ` +
|
|
107
|
+
`to ${place(end)}, which is ${vec2.distance(end, start).toFixed(6)} apart against a ` +
|
|
108
|
+
`tolerance of ${tolerance}`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The kept pieces joined into loops, by taking each end to the piece that
|
|
112
|
+
* starts where it finishes.
|
|
113
|
+
*
|
|
114
|
+
* A piece carries its two controls and where it ends, so the loop takes where
|
|
115
|
+
* each piece begins from where the piece before it ended. The two paths put
|
|
116
|
+
* their cut at a place they each worked out on their own, so the two ends of a
|
|
117
|
+
* join differ by whatever the crossing was out by, and that difference is
|
|
118
|
+
* absorbed here rather than left as a gap.
|
|
119
|
+
*/
|
|
120
|
+
function stitch(pieces, tolerance) {
|
|
121
|
+
const used = pieces.map(() => false);
|
|
122
|
+
const loops = [];
|
|
123
|
+
for (let seed = 0; seed < pieces.length; seed++) {
|
|
124
|
+
if (used[seed])
|
|
125
|
+
continue;
|
|
126
|
+
used[seed] = true;
|
|
127
|
+
const start = pieces[seed].from;
|
|
128
|
+
const curves = [pieces[seed].curve];
|
|
129
|
+
let end = pieces[seed].curve.to;
|
|
130
|
+
while (vec2.distance(end, start) > tolerance) {
|
|
131
|
+
let next = -1;
|
|
132
|
+
let nearest = tolerance;
|
|
133
|
+
for (let at = 0; at < pieces.length; at++) {
|
|
134
|
+
if (used[at])
|
|
135
|
+
continue;
|
|
136
|
+
const gap = vec2.distance(pieces[at].from, end);
|
|
137
|
+
if (gap <= nearest) {
|
|
138
|
+
nearest = gap;
|
|
139
|
+
next = at;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (next < 0)
|
|
143
|
+
refuse(curves.length, start, end, tolerance);
|
|
144
|
+
used[next] = true;
|
|
145
|
+
curves.push(pieces[next].curve);
|
|
146
|
+
end = pieces[next].curve.to;
|
|
147
|
+
}
|
|
148
|
+
loops.push({ start, curves, closed: true });
|
|
149
|
+
}
|
|
150
|
+
return loops;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Where each piece stands against the other path, read at its middle.
|
|
154
|
+
*
|
|
155
|
+
* The middle stands for the whole piece because the cutting has already put a
|
|
156
|
+
* break wherever the two paths meet, so a piece after it is wholly one thing.
|
|
157
|
+
*/
|
|
158
|
+
function sidesAgainst(pieces, other, tolerance) {
|
|
159
|
+
return pieces.map((piece) => {
|
|
160
|
+
const middle = pointOn(piece.from, piece.curve, 0.5);
|
|
161
|
+
const edge = nearestEdge(other, middle);
|
|
162
|
+
if (edge && edge.gap <= tolerance) {
|
|
163
|
+
return vec2.dot(slopeOn(piece.from, piece.curve, 0.5), edge.heading) >= 0 ? 'along' : 'against';
|
|
164
|
+
}
|
|
165
|
+
return windingAt(other, middle) !== 0 ? 'inside' : 'outside';
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Whether the first path's piece belongs to the answer. */
|
|
169
|
+
function keepsFirst(keep, side) {
|
|
170
|
+
if (side === 'along')
|
|
171
|
+
return keep !== 'difference';
|
|
172
|
+
if (side === 'against')
|
|
173
|
+
return keep === 'difference';
|
|
174
|
+
if (keep === 'intersection')
|
|
175
|
+
return side === 'inside';
|
|
176
|
+
return side === 'outside';
|
|
177
|
+
}
|
|
178
|
+
/** Whether the second path's piece belongs to the answer. A shared stretch is
|
|
179
|
+
* never taken from here, since the first path's copy of it is already in. */
|
|
180
|
+
function keepsSecond(keep, side) {
|
|
181
|
+
if (side === 'along' || side === 'against')
|
|
182
|
+
return false;
|
|
183
|
+
if (keep === 'union')
|
|
184
|
+
return side === 'outside';
|
|
185
|
+
return side === 'inside';
|
|
186
|
+
}
|
|
187
|
+
function combine(first, second, keep, options) {
|
|
188
|
+
const tolerance = options.tolerance ?? TOLERANCE;
|
|
189
|
+
const left = closedLoops(first);
|
|
190
|
+
const right = closedLoops(second);
|
|
191
|
+
if (left.length === 0)
|
|
192
|
+
return keep === 'union' ? right : [];
|
|
193
|
+
if (right.length === 0)
|
|
194
|
+
return keep === 'intersection' ? [] : left;
|
|
195
|
+
const [leftCuts, rightCuts] = cutsBetween(left, right, tolerance);
|
|
196
|
+
const leftPieces = piecesOf(cutPath(left, leftCuts, { tolerance }));
|
|
197
|
+
const rightPieces = piecesOf(cutPath(right, rightCuts, { tolerance }));
|
|
198
|
+
const leftSide = sidesAgainst(leftPieces, flattenPath(right, { tolerance }), tolerance);
|
|
199
|
+
const rightSide = sidesAgainst(rightPieces, flattenPath(left, { tolerance }), tolerance);
|
|
200
|
+
const kept = [];
|
|
201
|
+
for (let at = 0; at < leftPieces.length; at++) {
|
|
202
|
+
if (keepsFirst(keep, leftSide[at]))
|
|
203
|
+
kept.push(leftPieces[at]);
|
|
204
|
+
}
|
|
205
|
+
for (let at = 0; at < rightPieces.length; at++) {
|
|
206
|
+
if (!keepsSecond(keep, rightSide[at]))
|
|
207
|
+
continue;
|
|
208
|
+
kept.push(keep === 'difference' ? reversed(rightPieces[at]) : rightPieces[at]);
|
|
209
|
+
}
|
|
210
|
+
return stitch(kept, tolerance);
|
|
211
|
+
}
|
|
212
|
+
/** Everything either path covers. */
|
|
213
|
+
export function unionOf(first, second, options = {}) {
|
|
214
|
+
return combine(first, second, 'union', options);
|
|
215
|
+
}
|
|
216
|
+
/** Only what both paths cover. */
|
|
217
|
+
export function intersectionOf(first, second, options = {}) {
|
|
218
|
+
return combine(first, second, 'intersection', options);
|
|
219
|
+
}
|
|
220
|
+
/** The first path with the second taken out of it. */
|
|
221
|
+
export function differenceOf(first, second, options = {}) {
|
|
222
|
+
return combine(first, second, 'difference', options);
|
|
223
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A path cut where something crosses it.
|
|
3
|
+
*
|
|
4
|
+
* Each cut names a piece and how far along it the cut falls, and the pieces
|
|
5
|
+
* that come back draw exactly what the pieces they replace drew, because de
|
|
6
|
+
* Casteljau's construction gives both halves of a cubic as cubics. Several cuts
|
|
7
|
+
* in one piece are taken in order, and each one after the first is measured
|
|
8
|
+
* against what is left rather than against the piece it started as.
|
|
9
|
+
*/
|
|
10
|
+
import { type Path } from './path.js';
|
|
11
|
+
/** Where one cut falls: which subpath, which piece of it, and how far along
|
|
12
|
+
* that piece. */
|
|
13
|
+
export interface Cut {
|
|
14
|
+
readonly subpath: number;
|
|
15
|
+
readonly curve: number;
|
|
16
|
+
readonly along: number;
|
|
17
|
+
}
|
|
18
|
+
export interface CutOptions {
|
|
19
|
+
/** How close two cuts, or a cut and the end of a piece, are before they count
|
|
20
|
+
* as the same place, in the picture's own units. */
|
|
21
|
+
readonly tolerance?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A path with every cut put in, drawing what it drew and holding one more piece
|
|
25
|
+
* for each cut.
|
|
26
|
+
*
|
|
27
|
+
* The tolerance is a distance rather than a fraction, so it is read against
|
|
28
|
+
* each piece's own length: a cut is worth making only where the piece it would
|
|
29
|
+
* leave behind is long enough to see, and a piece of nothing is one the stitch
|
|
30
|
+
* that follows would have to know to skip.
|
|
31
|
+
*
|
|
32
|
+
* A cut naming a piece the path does not have is ignored, since a caller that
|
|
33
|
+
* has already thrown one subpath away should not have to renumber the cuts it
|
|
34
|
+
* gathered before it did.
|
|
35
|
+
*/
|
|
36
|
+
export declare function cutPath(path: Path, cuts: readonly Cut[], options?: CutOptions): Path;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A path cut where something crosses it.
|
|
3
|
+
*
|
|
4
|
+
* Each cut names a piece and how far along it the cut falls, and the pieces
|
|
5
|
+
* that come back draw exactly what the pieces they replace drew, because de
|
|
6
|
+
* Casteljau's construction gives both halves of a cubic as cubics. Several cuts
|
|
7
|
+
* in one piece are taken in order, and each one after the first is measured
|
|
8
|
+
* against what is left rather than against the piece it started as.
|
|
9
|
+
*/
|
|
10
|
+
import { splitCurve } from './path.js';
|
|
11
|
+
import { measurePath } from './length.js';
|
|
12
|
+
const TOLERANCE = 1e-9;
|
|
13
|
+
/** The fractions for one piece, in order, with the ones that would leave a
|
|
14
|
+
* piece of nothing dropped. */
|
|
15
|
+
function wanted(fractions, tolerance) {
|
|
16
|
+
const kept = [];
|
|
17
|
+
for (const fraction of [...fractions].sort((a, b) => a - b)) {
|
|
18
|
+
if (fraction <= tolerance || fraction >= 1 - tolerance)
|
|
19
|
+
continue;
|
|
20
|
+
if (kept.length > 0 && fraction - kept[kept.length - 1] <= tolerance)
|
|
21
|
+
continue;
|
|
22
|
+
kept.push(fraction);
|
|
23
|
+
}
|
|
24
|
+
return kept;
|
|
25
|
+
}
|
|
26
|
+
/** One piece as the run of pieces the cuts leave, each cut measured against
|
|
27
|
+
* what is left of the piece rather than against the whole of it. */
|
|
28
|
+
function cutCurve(from, curve, fractions) {
|
|
29
|
+
const pieces = [];
|
|
30
|
+
let rest = curve;
|
|
31
|
+
let restFrom = from;
|
|
32
|
+
let taken = 0;
|
|
33
|
+
for (const fraction of fractions) {
|
|
34
|
+
const [head, tail] = splitCurve(restFrom, rest, (fraction - taken) / (1 - taken));
|
|
35
|
+
pieces.push(head);
|
|
36
|
+
restFrom = head.to;
|
|
37
|
+
rest = tail;
|
|
38
|
+
taken = fraction;
|
|
39
|
+
}
|
|
40
|
+
pieces.push(rest);
|
|
41
|
+
return pieces;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A path with every cut put in, drawing what it drew and holding one more piece
|
|
45
|
+
* for each cut.
|
|
46
|
+
*
|
|
47
|
+
* The tolerance is a distance rather than a fraction, so it is read against
|
|
48
|
+
* each piece's own length: a cut is worth making only where the piece it would
|
|
49
|
+
* leave behind is long enough to see, and a piece of nothing is one the stitch
|
|
50
|
+
* that follows would have to know to skip.
|
|
51
|
+
*
|
|
52
|
+
* A cut naming a piece the path does not have is ignored, since a caller that
|
|
53
|
+
* has already thrown one subpath away should not have to renumber the cuts it
|
|
54
|
+
* gathered before it did.
|
|
55
|
+
*/
|
|
56
|
+
export function cutPath(path, cuts, options = {}) {
|
|
57
|
+
const tolerance = options.tolerance ?? TOLERANCE;
|
|
58
|
+
if (cuts.length === 0)
|
|
59
|
+
return path;
|
|
60
|
+
const gathered = new Map();
|
|
61
|
+
for (const cut of cuts) {
|
|
62
|
+
const key = `${cut.subpath}:${cut.curve}`;
|
|
63
|
+
const already = gathered.get(key);
|
|
64
|
+
if (already)
|
|
65
|
+
already.push(cut.along);
|
|
66
|
+
else
|
|
67
|
+
gathered.set(key, [cut.along]);
|
|
68
|
+
}
|
|
69
|
+
const measured = measurePath(path);
|
|
70
|
+
return path.map((subpath, at) => {
|
|
71
|
+
let from = subpath.start;
|
|
72
|
+
const curves = [];
|
|
73
|
+
for (let piece = 0; piece < subpath.curves.length; piece++) {
|
|
74
|
+
const curve = subpath.curves[piece];
|
|
75
|
+
const span = measured.per[at][piece].total;
|
|
76
|
+
const asFraction = span > 0 ? Math.min(tolerance / span, 0.5) : 1;
|
|
77
|
+
const fractions = wanted(gathered.get(`${at}:${piece}`) ?? [], asFraction);
|
|
78
|
+
curves.push(...cutCurve(from, curve, fractions));
|
|
79
|
+
from = curve.to;
|
|
80
|
+
}
|
|
81
|
+
return { start: subpath.start, curves, closed: subpath.closed };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a point is inside a path.
|
|
3
|
+
*
|
|
4
|
+
* The count is taken on a flattening rather than on the cubics, because a ray
|
|
5
|
+
* against a cubic is a cubic to solve and what is wanted here is a yes or a no
|
|
6
|
+
* rather than a place. The geometry a boolean operation keeps stays the exact
|
|
7
|
+
* cubics; only this decision is taken on the flattening.
|
|
8
|
+
*
|
|
9
|
+
* The rule is the nonzero winding rule, which is the rule a mark is already
|
|
10
|
+
* drawn under, so a loop wound the other way inside another loop is a hole.
|
|
11
|
+
* Every edge crossing the ray is counted with the sign of the direction it
|
|
12
|
+
* crosses in, and an edge is counted at its lower end and not at its upper one,
|
|
13
|
+
* which is what makes a ray leaving through a corner answer what every other
|
|
14
|
+
* ray answers.
|
|
15
|
+
*/
|
|
16
|
+
import { type Vec2 } from '../values/vec2.js';
|
|
17
|
+
import type { Path } from './path.js';
|
|
18
|
+
export interface FlattenOptions {
|
|
19
|
+
/** How far a straight run may sit from the curve it stands for, in the
|
|
20
|
+
* picture's own units. */
|
|
21
|
+
readonly tolerance?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Every subpath as a run of points, each loop closed.
|
|
25
|
+
*
|
|
26
|
+
* A subpath that was left open is closed by the straight run back to where it
|
|
27
|
+
* started, since a path with an open loop has no inside until it has one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function flattenPath(path: Path, options?: FlattenOptions): Vec2[][];
|
|
30
|
+
/** How many times the loops wind round a point, counted along the ray heading
|
|
31
|
+
* in the positive x direction. */
|
|
32
|
+
export declare function windingAt(loops: readonly (readonly Vec2[])[], point: Vec2): number;
|
|
33
|
+
/** The nearest straight run of a flattening to a point: how far off it is, and
|
|
34
|
+
* which way that run goes. */
|
|
35
|
+
export interface Edge {
|
|
36
|
+
readonly gap: number;
|
|
37
|
+
readonly heading: Vec2;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Which edge of a flattening a point sits nearest, and which way it runs.
|
|
41
|
+
*
|
|
42
|
+
* This is what tells a piece lying along another path's edge from one merely
|
|
43
|
+
* near it, which the winding count cannot answer because a point on the edge
|
|
44
|
+
* itself is the one place the count has no answer for.
|
|
45
|
+
*/
|
|
46
|
+
export declare function nearestEdge(loops: readonly (readonly Vec2[])[], point: Vec2): Edge | null;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a path holds a point, under the nonzero rule.
|
|
49
|
+
*
|
|
50
|
+
* A point sitting on the edge itself has no answer this can be right about, and
|
|
51
|
+
* what comes back for one is whichever side the tolerance put it on.
|
|
52
|
+
*/
|
|
53
|
+
export declare function containsPoint(path: Path, point: Vec2, options?: FlattenOptions): boolean;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a point is inside a path.
|
|
3
|
+
*
|
|
4
|
+
* The count is taken on a flattening rather than on the cubics, because a ray
|
|
5
|
+
* against a cubic is a cubic to solve and what is wanted here is a yes or a no
|
|
6
|
+
* rather than a place. The geometry a boolean operation keeps stays the exact
|
|
7
|
+
* cubics; only this decision is taken on the flattening.
|
|
8
|
+
*
|
|
9
|
+
* The rule is the nonzero winding rule, which is the rule a mark is already
|
|
10
|
+
* drawn under, so a loop wound the other way inside another loop is a hole.
|
|
11
|
+
* Every edge crossing the ray is counted with the sign of the direction it
|
|
12
|
+
* crosses in, and an edge is counted at its lower end and not at its upper one,
|
|
13
|
+
* which is what makes a ray leaving through a corner answer what every other
|
|
14
|
+
* ray answers.
|
|
15
|
+
*/
|
|
16
|
+
import { vec2 } from '../values/vec2.js';
|
|
17
|
+
const TOLERANCE = 1e-6;
|
|
18
|
+
/** How many times a piece may be halved, which a tolerance of zero would
|
|
19
|
+
* otherwise leave unbounded. */
|
|
20
|
+
const DEPTH = 24;
|
|
21
|
+
/**
|
|
22
|
+
* How many points a whole flattening may hold before it is refused.
|
|
23
|
+
*
|
|
24
|
+
* Halving is the only bound the depth gives, and sixteen million points for one
|
|
25
|
+
* piece is a machine out of memory rather than a fine flattening. A circle of
|
|
26
|
+
* radius 1 wants 4096 of these at a tolerance of a millionth, so the room here
|
|
27
|
+
* is a thousandfold.
|
|
28
|
+
*/
|
|
29
|
+
const POINTS = 1_000_000;
|
|
30
|
+
/** How far the two controls sit from the straight run between the ends.
|
|
31
|
+
*
|
|
32
|
+
* The curve itself stays within three quarters of this, so measuring the
|
|
33
|
+
* controls asks for a little more than the tolerance rather than a little
|
|
34
|
+
* less. */
|
|
35
|
+
function offChord(from, curve) {
|
|
36
|
+
const run = vec2.sub(curve.to, from);
|
|
37
|
+
const span = Math.hypot(run.x, run.y);
|
|
38
|
+
if (span === 0) {
|
|
39
|
+
return Math.max(vec2.distance(curve.control1, from), vec2.distance(curve.control2, from));
|
|
40
|
+
}
|
|
41
|
+
const away = (point) => Math.abs(run.x * (point.y - from.y) - run.y * (point.x - from.x)) / span;
|
|
42
|
+
return Math.max(away(curve.control1), away(curve.control2));
|
|
43
|
+
}
|
|
44
|
+
function walk(from, curve, tolerance, depth, into) {
|
|
45
|
+
if (depth >= DEPTH || offChord(from, curve) <= tolerance) {
|
|
46
|
+
into.push(curve.to);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (into.length >= POINTS) {
|
|
50
|
+
throw new Error(`a tolerance of ${tolerance} asks for more than ${POINTS} straight runs to stand for one ` +
|
|
51
|
+
`curve, which is finer than this flattens`);
|
|
52
|
+
}
|
|
53
|
+
const a = vec2.lerp(from, curve.control1, 0.5);
|
|
54
|
+
const b = vec2.lerp(curve.control1, curve.control2, 0.5);
|
|
55
|
+
const c = vec2.lerp(curve.control2, curve.to, 0.5);
|
|
56
|
+
const d = vec2.lerp(a, b, 0.5);
|
|
57
|
+
const e = vec2.lerp(b, c, 0.5);
|
|
58
|
+
const middle = vec2.lerp(d, e, 0.5);
|
|
59
|
+
walk(from, { control1: a, control2: d, to: middle }, tolerance, depth + 1, into);
|
|
60
|
+
walk(middle, { control1: e, control2: c, to: curve.to }, tolerance, depth + 1, into);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Every subpath as a run of points, each loop closed.
|
|
64
|
+
*
|
|
65
|
+
* A subpath that was left open is closed by the straight run back to where it
|
|
66
|
+
* started, since a path with an open loop has no inside until it has one.
|
|
67
|
+
*/
|
|
68
|
+
export function flattenPath(path, options = {}) {
|
|
69
|
+
const tolerance = options.tolerance ?? TOLERANCE;
|
|
70
|
+
const loops = [];
|
|
71
|
+
for (const subpath of path) {
|
|
72
|
+
if (subpath.curves.length === 0)
|
|
73
|
+
continue;
|
|
74
|
+
const points = [subpath.start];
|
|
75
|
+
let from = subpath.start;
|
|
76
|
+
for (const curve of subpath.curves) {
|
|
77
|
+
walk(from, curve, tolerance, 0, points);
|
|
78
|
+
from = curve.to;
|
|
79
|
+
}
|
|
80
|
+
const last = points[points.length - 1];
|
|
81
|
+
if (last.x !== points[0].x || last.y !== points[0].y)
|
|
82
|
+
points.push(points[0]);
|
|
83
|
+
loops.push(points);
|
|
84
|
+
}
|
|
85
|
+
return loops;
|
|
86
|
+
}
|
|
87
|
+
/** Which side of the run from one point to another a third point falls on,
|
|
88
|
+
* positive to the left of it. */
|
|
89
|
+
function leftOf(from, to, point) {
|
|
90
|
+
return (to.x - from.x) * (point.y - from.y) - (point.x - from.x) * (to.y - from.y);
|
|
91
|
+
}
|
|
92
|
+
/** How many times the loops wind round a point, counted along the ray heading
|
|
93
|
+
* in the positive x direction. */
|
|
94
|
+
export function windingAt(loops, point) {
|
|
95
|
+
let winding = 0;
|
|
96
|
+
for (const loop of loops) {
|
|
97
|
+
for (let at = 1; at < loop.length; at++) {
|
|
98
|
+
const from = loop[at - 1];
|
|
99
|
+
const to = loop[at];
|
|
100
|
+
if (from.y <= point.y) {
|
|
101
|
+
if (to.y > point.y && leftOf(from, to, point) > 0)
|
|
102
|
+
winding += 1;
|
|
103
|
+
}
|
|
104
|
+
else if (to.y <= point.y && leftOf(from, to, point) < 0) {
|
|
105
|
+
winding -= 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return winding;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Which edge of a flattening a point sits nearest, and which way it runs.
|
|
113
|
+
*
|
|
114
|
+
* This is what tells a piece lying along another path's edge from one merely
|
|
115
|
+
* near it, which the winding count cannot answer because a point on the edge
|
|
116
|
+
* itself is the one place the count has no answer for.
|
|
117
|
+
*/
|
|
118
|
+
export function nearestEdge(loops, point) {
|
|
119
|
+
let nearest = null;
|
|
120
|
+
for (const loop of loops) {
|
|
121
|
+
for (let at = 1; at < loop.length; at++) {
|
|
122
|
+
const from = loop[at - 1];
|
|
123
|
+
const to = loop[at];
|
|
124
|
+
const run = vec2.sub(to, from);
|
|
125
|
+
const square = vec2.dot(run, run);
|
|
126
|
+
const along = square === 0 ? 0 : Math.min(1, Math.max(0, vec2.dot(vec2.sub(point, from), run) / square));
|
|
127
|
+
const gap = vec2.distance(point, vec2.add(from, vec2.scale(run, along)));
|
|
128
|
+
if (!nearest || gap < nearest.gap)
|
|
129
|
+
nearest = { gap, heading: run };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return nearest;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether a path holds a point, under the nonzero rule.
|
|
136
|
+
*
|
|
137
|
+
* A point sitting on the edge itself has no answer this can be right about, and
|
|
138
|
+
* what comes back for one is whichever side the tolerance put it on.
|
|
139
|
+
*/
|
|
140
|
+
export function containsPoint(path, point, options = {}) {
|
|
141
|
+
return windingAt(flattenPath(path, options), point) !== 0;
|
|
142
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where two cubics cross.
|
|
3
|
+
*
|
|
4
|
+
* Two curves whose boxes miss each other cannot cross, so a pair is halved by
|
|
5
|
+
* de Casteljau's construction and only the halves whose boxes still overlap are
|
|
6
|
+
* followed down, until both are smaller than the tolerance. A box is bigger
|
|
7
|
+
* than the curve inside it, so a pair that small is kept only when the straight
|
|
8
|
+
* runs across the two pieces come within the tolerance of each other. What is
|
|
9
|
+
* left is a run of touching hits per meeting rather than one hit, so the run is
|
|
10
|
+
* joined up and answered once, and then sharpened by Newton's method on the
|
|
11
|
+
* pair, which is what takes a crossing of two straight pieces from the
|
|
12
|
+
* tolerance to the last few bits.
|
|
13
|
+
*/
|
|
14
|
+
import { type Vec2 } from '../values/vec2.js';
|
|
15
|
+
import type { Cubic } from './path.js';
|
|
16
|
+
/** One place two curves meet, as a fraction along each of them and the point
|
|
17
|
+
* itself. */
|
|
18
|
+
export interface Crossing {
|
|
19
|
+
readonly alongFirst: number;
|
|
20
|
+
readonly alongSecond: number;
|
|
21
|
+
readonly point: Vec2;
|
|
22
|
+
}
|
|
23
|
+
export interface CrossingOptions {
|
|
24
|
+
/** How close two pieces come before they count as meeting, in the picture's
|
|
25
|
+
* own units. */
|
|
26
|
+
readonly tolerance?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Every place two cubics cross, as a fraction along each and the point.
|
|
30
|
+
*
|
|
31
|
+
* Each segment is given the point it starts from, since a segment carries where
|
|
32
|
+
* it ends and not where it began. Two curves covering the same stretch answer
|
|
33
|
+
* with the two ends of that stretch, so a caller reading the answer as places to
|
|
34
|
+
* cut at gets the stretch marked off rather than chopped into slivers.
|
|
35
|
+
*/
|
|
36
|
+
export declare function curveCrossings(fromFirst: Vec2, first: Cubic, fromSecond: Vec2, second: Cubic, options?: CrossingOptions): Crossing[];
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where two cubics cross.
|
|
3
|
+
*
|
|
4
|
+
* Two curves whose boxes miss each other cannot cross, so a pair is halved by
|
|
5
|
+
* de Casteljau's construction and only the halves whose boxes still overlap are
|
|
6
|
+
* followed down, until both are smaller than the tolerance. A box is bigger
|
|
7
|
+
* than the curve inside it, so a pair that small is kept only when the straight
|
|
8
|
+
* runs across the two pieces come within the tolerance of each other. What is
|
|
9
|
+
* left is a run of touching hits per meeting rather than one hit, so the run is
|
|
10
|
+
* joined up and answered once, and then sharpened by Newton's method on the
|
|
11
|
+
* pair, which is what takes a crossing of two straight pieces from the
|
|
12
|
+
* tolerance to the last few bits.
|
|
13
|
+
*/
|
|
14
|
+
import { vec2 } from '../values/vec2.js';
|
|
15
|
+
/** How close two pieces come before they count as meeting. It decides which
|
|
16
|
+
* meetings are told apart rather than how sharp one is, since Newton's method
|
|
17
|
+
* supplies the sharpness afterwards. */
|
|
18
|
+
const TOLERANCE = 1e-6;
|
|
19
|
+
/**
|
|
20
|
+
* How many pairs the search may make before it answers with where it had got to.
|
|
21
|
+
*
|
|
22
|
+
* It counts pairs made rather than pairs looked at, because each one looked at
|
|
23
|
+
* makes up to four more: counting the ones looked at leaves the pile of pairs
|
|
24
|
+
* waiting growing four times faster than it drains, and a tolerance small enough
|
|
25
|
+
* ran the machine out of memory before the count was ever reached.
|
|
26
|
+
*/
|
|
27
|
+
const BUDGET = 200_000;
|
|
28
|
+
/** How deep the halving goes, which a tolerance of zero would otherwise leave
|
|
29
|
+
* unbounded. */
|
|
30
|
+
const DEPTH = 60;
|
|
31
|
+
/** Below this the two tangents are parallel, Newton's step divides by nothing,
|
|
32
|
+
* and the answer stays the one the halving gave. */
|
|
33
|
+
const PARALLEL = 1e-12;
|
|
34
|
+
/** How far Newton is allowed to move a cluster before its step is read as a
|
|
35
|
+
* jump to a different crossing and thrown away. */
|
|
36
|
+
const REACH = 1e-3;
|
|
37
|
+
/** A segment written as its four points, since a segment carries where it ends
|
|
38
|
+
* and not where it began. */
|
|
39
|
+
function hullOf(from, curve) {
|
|
40
|
+
return [from, curve.control1, curve.control2, curve.to];
|
|
41
|
+
}
|
|
42
|
+
/** The box round the four points, which holds the curve because a cubic never
|
|
43
|
+
* leaves the hull of the points it is written from. */
|
|
44
|
+
function boxOf(hull) {
|
|
45
|
+
let lowX = hull[0].x;
|
|
46
|
+
let highX = hull[0].x;
|
|
47
|
+
let lowY = hull[0].y;
|
|
48
|
+
let highY = hull[0].y;
|
|
49
|
+
for (let at = 1; at < 4; at++) {
|
|
50
|
+
const point = hull[at];
|
|
51
|
+
if (point.x < lowX)
|
|
52
|
+
lowX = point.x;
|
|
53
|
+
if (point.x > highX)
|
|
54
|
+
highX = point.x;
|
|
55
|
+
if (point.y < lowY)
|
|
56
|
+
lowY = point.y;
|
|
57
|
+
if (point.y > highY)
|
|
58
|
+
highY = point.y;
|
|
59
|
+
}
|
|
60
|
+
return { lowX, highX, lowY, highY };
|
|
61
|
+
}
|
|
62
|
+
function spread(box) {
|
|
63
|
+
return Math.max(box.highX - box.lowX, box.highY - box.lowY);
|
|
64
|
+
}
|
|
65
|
+
/** Whether two boxes miss, with the tolerance added on both sides so two curves
|
|
66
|
+
* touching at exactly one point are not lost to rounding. */
|
|
67
|
+
function apart(a, b, slack) {
|
|
68
|
+
return (a.lowX - slack > b.highX ||
|
|
69
|
+
b.lowX - slack > a.highX ||
|
|
70
|
+
a.lowY - slack > b.highY ||
|
|
71
|
+
b.lowY - slack > a.highY);
|
|
72
|
+
}
|
|
73
|
+
/** How far a point sits from a straight run between two points. */
|
|
74
|
+
function offSegment(point, from, to) {
|
|
75
|
+
const run = vec2.sub(to, from);
|
|
76
|
+
const square = vec2.dot(run, run);
|
|
77
|
+
const along = square === 0 ? 0 : held(vec2.dot(vec2.sub(point, from), run) / square);
|
|
78
|
+
return vec2.distance(point, vec2.add(from, vec2.scale(run, along)));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* How far apart two pieces are, taken between the straight runs across them.
|
|
82
|
+
*
|
|
83
|
+
* A piece no bigger than the tolerance leaves its own chord by a fraction of
|
|
84
|
+
* that, so the chords answer for the curves here, and this is what tells a
|
|
85
|
+
* meeting from two boxes that merely overlap without their curves coming close.
|
|
86
|
+
*/
|
|
87
|
+
function chordGap(first, second) {
|
|
88
|
+
const a = first[0];
|
|
89
|
+
const b = first[3];
|
|
90
|
+
const c = second[0];
|
|
91
|
+
const d = second[3];
|
|
92
|
+
const ab = vec2.sub(b, a);
|
|
93
|
+
const cd = vec2.sub(d, c);
|
|
94
|
+
const under = ab.x * cd.y - ab.y * cd.x;
|
|
95
|
+
if (under !== 0) {
|
|
96
|
+
const ac = vec2.sub(c, a);
|
|
97
|
+
const alongFirst = (ac.x * cd.y - ac.y * cd.x) / under;
|
|
98
|
+
const alongSecond = (ac.x * ab.y - ac.y * ab.x) / under;
|
|
99
|
+
if (alongFirst >= 0 && alongFirst <= 1 && alongSecond >= 0 && alongSecond <= 1)
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
return Math.min(offSegment(a, c, d), offSegment(b, c, d), offSegment(c, a, b), offSegment(d, a, b));
|
|
103
|
+
}
|
|
104
|
+
/** A segment cut in half, both pieces drawing what the whole drew. */
|
|
105
|
+
function halves(hull) {
|
|
106
|
+
const a = vec2.lerp(hull[0], hull[1], 0.5);
|
|
107
|
+
const b = vec2.lerp(hull[1], hull[2], 0.5);
|
|
108
|
+
const c = vec2.lerp(hull[2], hull[3], 0.5);
|
|
109
|
+
const d = vec2.lerp(a, b, 0.5);
|
|
110
|
+
const e = vec2.lerp(b, c, 0.5);
|
|
111
|
+
const middle = vec2.lerp(d, e, 0.5);
|
|
112
|
+
return [
|
|
113
|
+
[hull[0], a, d, middle],
|
|
114
|
+
[middle, e, c, hull[3]],
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
function pointAt(hull, along) {
|
|
118
|
+
const u = 1 - along;
|
|
119
|
+
const a = u * u * u;
|
|
120
|
+
const b = 3 * u * u * along;
|
|
121
|
+
const c = 3 * u * along * along;
|
|
122
|
+
const d = along * along * along;
|
|
123
|
+
return vec2(a * hull[0].x + b * hull[1].x + c * hull[2].x + d * hull[3].x, a * hull[0].y + b * hull[1].y + c * hull[2].y + d * hull[3].y);
|
|
124
|
+
}
|
|
125
|
+
/** Which way the curve is heading, read off the four points the search already
|
|
126
|
+
* holds rather than off a piece, so following a pair down allocates nothing. */
|
|
127
|
+
function slopeAt(hull, along) {
|
|
128
|
+
const u = 1 - along;
|
|
129
|
+
const a = 3 * u * u;
|
|
130
|
+
const b = 6 * u * along;
|
|
131
|
+
const c = 3 * along * along;
|
|
132
|
+
return vec2(a * (hull[1].x - hull[0].x) + b * (hull[2].x - hull[1].x) + c * (hull[3].x - hull[2].x), a * (hull[1].y - hull[0].y) + b * (hull[2].y - hull[1].y) + c * (hull[3].y - hull[2].y));
|
|
133
|
+
}
|
|
134
|
+
/** A fraction pulled back onto the segment, so a crossing at an end lands on
|
|
135
|
+
* the end exactly rather than a hair outside it. */
|
|
136
|
+
function held(along) {
|
|
137
|
+
return along < 0 ? 0 : along > 1 ? 1 : along;
|
|
138
|
+
}
|
|
139
|
+
function gapBetween(first, second, alongFirst, alongSecond) {
|
|
140
|
+
return vec2.distance(pointAt(first, alongFirst), pointAt(second, alongSecond));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* One cluster moved onto the crossing itself, by Newton's method on the pair of
|
|
144
|
+
* curves: the step that takes the difference between the two points to zero is
|
|
145
|
+
* the two tangents solved as a two by two system.
|
|
146
|
+
*
|
|
147
|
+
* A tangency has no such step, since the tangents are parallel there and the
|
|
148
|
+
* system has no answer, so the halving's own reading is what stands.
|
|
149
|
+
*/
|
|
150
|
+
function sharpened(first, second, start) {
|
|
151
|
+
let alongFirst = start.alongFirst;
|
|
152
|
+
let alongSecond = start.alongSecond;
|
|
153
|
+
let gap = gapBetween(first, second, alongFirst, alongSecond);
|
|
154
|
+
for (let step = 0; step < 12; step++) {
|
|
155
|
+
const apartBy = vec2.sub(pointAt(first, alongFirst), pointAt(second, alongSecond));
|
|
156
|
+
const heading = slopeAt(first, alongFirst);
|
|
157
|
+
const other = slopeAt(second, alongSecond);
|
|
158
|
+
const under = other.x * heading.y - heading.x * other.y;
|
|
159
|
+
if (Math.abs(under) < PARALLEL)
|
|
160
|
+
break;
|
|
161
|
+
const moveFirst = (apartBy.x * other.y - other.x * apartBy.y) / under;
|
|
162
|
+
const moveSecond = (apartBy.x * heading.y - heading.x * apartBy.y) / under;
|
|
163
|
+
if (Math.abs(moveFirst) > REACH || Math.abs(moveSecond) > REACH)
|
|
164
|
+
break;
|
|
165
|
+
const nextFirst = held(alongFirst + moveFirst);
|
|
166
|
+
const nextSecond = held(alongSecond + moveSecond);
|
|
167
|
+
const nextGap = gapBetween(first, second, nextFirst, nextSecond);
|
|
168
|
+
if (nextGap > gap)
|
|
169
|
+
break;
|
|
170
|
+
alongFirst = nextFirst;
|
|
171
|
+
alongSecond = nextSecond;
|
|
172
|
+
gap = nextGap;
|
|
173
|
+
if (gap === 0)
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
return { alongFirst, alongSecond, point: pointAt(first, alongFirst) };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A run of hits reported as the places they gather at.
|
|
180
|
+
*
|
|
181
|
+
* Every hit carries the stretch of each curve it was found in, and two hits
|
|
182
|
+
* whose stretches touch on both curves are the same meeting: the stretch
|
|
183
|
+
* between them was never thrown away, so the curves stayed within the tolerance
|
|
184
|
+
* across it. A gap in the run is the curves moving apart by more than the
|
|
185
|
+
* tolerance, which is two meetings rather than one.
|
|
186
|
+
*/
|
|
187
|
+
function clustered(hits) {
|
|
188
|
+
const inOrder = [...hits].sort((a, b) => a.firstLow - b.firstLow);
|
|
189
|
+
const groups = [];
|
|
190
|
+
for (const hit of inOrder) {
|
|
191
|
+
const joined = groups.find((group) => hit.firstLow <= group.firstHigh &&
|
|
192
|
+
hit.firstHigh >= group.firstLow &&
|
|
193
|
+
hit.secondLow <= group.secondHigh &&
|
|
194
|
+
hit.secondHigh >= group.secondLow);
|
|
195
|
+
if (joined) {
|
|
196
|
+
joined.firstHigh = Math.max(joined.firstHigh, hit.firstHigh);
|
|
197
|
+
joined.firstLow = Math.min(joined.firstLow, hit.firstLow);
|
|
198
|
+
joined.secondHigh = Math.max(joined.secondHigh, hit.secondHigh);
|
|
199
|
+
joined.secondLow = Math.min(joined.secondLow, hit.secondLow);
|
|
200
|
+
joined.sumFirst += (hit.firstLow + hit.firstHigh) / 2;
|
|
201
|
+
joined.sumSecond += (hit.secondLow + hit.secondHigh) / 2;
|
|
202
|
+
joined.sum = vec2.add(joined.sum, hit.point);
|
|
203
|
+
joined.count += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
groups.push({
|
|
207
|
+
firstLow: hit.firstLow,
|
|
208
|
+
firstHigh: hit.firstHigh,
|
|
209
|
+
secondLow: hit.secondLow,
|
|
210
|
+
secondHigh: hit.secondHigh,
|
|
211
|
+
sumFirst: (hit.firstLow + hit.firstHigh) / 2,
|
|
212
|
+
sumSecond: (hit.secondLow + hit.secondHigh) / 2,
|
|
213
|
+
sum: hit.point,
|
|
214
|
+
count: 1,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return groups.map((group) => ({
|
|
218
|
+
alongFirst: group.sumFirst / group.count,
|
|
219
|
+
alongSecond: group.sumSecond / group.count,
|
|
220
|
+
point: vec2.scale(group.sum, 1 / group.count),
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
/** How many places the coarse sweep looks at before it decides which part of a
|
|
224
|
+
* curve a point is nearest. */
|
|
225
|
+
const SWEEP = 32;
|
|
226
|
+
/** How hard the curve is turning at a place, which Newton needs because the
|
|
227
|
+
* nearest point moves as the curve bends away from it. */
|
|
228
|
+
function bendAt(hull, along) {
|
|
229
|
+
const u = 1 - along;
|
|
230
|
+
return vec2(6 * (u * (hull[2].x - 2 * hull[1].x + hull[0].x) + along * (hull[3].x - 2 * hull[2].x + hull[1].x)), 6 * (u * (hull[2].y - 2 * hull[1].y + hull[0].y) + along * (hull[3].y - 2 * hull[2].y + hull[1].y)));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Where on a curve a point sits nearest, as a fraction along it, and how far
|
|
234
|
+
* away it is there.
|
|
235
|
+
*
|
|
236
|
+
* A coarse sweep picks which part of the curve to believe, and Newton's method
|
|
237
|
+
* on the distance finishes it, since the nearest point on a cubic is a fifth
|
|
238
|
+
* degree root and solving one is more than this needs. The sweep gives up before
|
|
239
|
+
* Newton when its best is further off than the tolerance plus the most one step
|
|
240
|
+
* of the sweep can be hiding, since no refining brings it under from there.
|
|
241
|
+
*/
|
|
242
|
+
function nearestPlace(hull, point, tolerance) {
|
|
243
|
+
let along = 0;
|
|
244
|
+
let gap = Infinity;
|
|
245
|
+
for (let step = 0; step <= SWEEP; step++) {
|
|
246
|
+
const at = step / SWEEP;
|
|
247
|
+
const away = vec2.distance(pointAt(hull, at), point);
|
|
248
|
+
if (away < gap) {
|
|
249
|
+
gap = away;
|
|
250
|
+
along = at;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const reach = spread(boxOf(hull)) * 2;
|
|
254
|
+
if (gap > tolerance + reach / SWEEP)
|
|
255
|
+
return { along, gap };
|
|
256
|
+
for (let step = 0; step < 12; step++) {
|
|
257
|
+
const away = vec2.sub(pointAt(hull, along), point);
|
|
258
|
+
const heading = slopeAt(hull, along);
|
|
259
|
+
const turning = vec2.dot(heading, heading) + vec2.dot(away, bendAt(hull, along));
|
|
260
|
+
if (Math.abs(turning) < PARALLEL)
|
|
261
|
+
break;
|
|
262
|
+
const next = held(along - vec2.dot(away, heading) / turning);
|
|
263
|
+
const closer = vec2.distance(pointAt(hull, next), point);
|
|
264
|
+
if (!(closer < gap))
|
|
265
|
+
break;
|
|
266
|
+
along = next;
|
|
267
|
+
gap = closer;
|
|
268
|
+
}
|
|
269
|
+
return { along, gap };
|
|
270
|
+
}
|
|
271
|
+
/** How many places along a shared stretch are checked to still be on the other
|
|
272
|
+
* curve before the stretch is believed. */
|
|
273
|
+
const ALONG_SHARED = 12;
|
|
274
|
+
/**
|
|
275
|
+
* The two ends of the stretch two curves cover together, when they cover one.
|
|
276
|
+
*
|
|
277
|
+
* Halving into a stretch like that answers it as a spray of meetings, because
|
|
278
|
+
* every pair of small pieces along it overlaps and the budget runs out before
|
|
279
|
+
* the run is walked. So the stretch is found first, from the ends of each curve
|
|
280
|
+
* that lie on the other, and answered by where it starts and where it ends.
|
|
281
|
+
*
|
|
282
|
+
* Two curves meeting at a single point are not a stretch and are left to the
|
|
283
|
+
* halving, which places one meeting more sharply than a sweep can.
|
|
284
|
+
*/
|
|
285
|
+
function sharedStretch(first, second, tolerance) {
|
|
286
|
+
const ends = [];
|
|
287
|
+
for (const along of [0, 1]) {
|
|
288
|
+
const place = nearestPlace(second, pointAt(first, along), tolerance);
|
|
289
|
+
if (place.gap <= tolerance)
|
|
290
|
+
ends.push({ first: along, second: place.along });
|
|
291
|
+
}
|
|
292
|
+
for (const along of [0, 1]) {
|
|
293
|
+
const place = nearestPlace(first, pointAt(second, along), tolerance);
|
|
294
|
+
if (place.gap <= tolerance)
|
|
295
|
+
ends.push({ first: place.along, second: along });
|
|
296
|
+
}
|
|
297
|
+
if (ends.length < 2)
|
|
298
|
+
return null;
|
|
299
|
+
let low = ends[0];
|
|
300
|
+
let high = ends[0];
|
|
301
|
+
for (const end of ends) {
|
|
302
|
+
if (end.first < low.first)
|
|
303
|
+
low = end;
|
|
304
|
+
if (end.first > high.first)
|
|
305
|
+
high = end;
|
|
306
|
+
}
|
|
307
|
+
const from = pointAt(first, low.first);
|
|
308
|
+
const to = pointAt(first, high.first);
|
|
309
|
+
if (vec2.distance(from, to) <= tolerance)
|
|
310
|
+
return null;
|
|
311
|
+
for (let step = 1; step < ALONG_SHARED; step++) {
|
|
312
|
+
const along = low.first + ((high.first - low.first) * step) / ALONG_SHARED;
|
|
313
|
+
if (nearestPlace(second, pointAt(first, along), tolerance).gap > tolerance)
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return [
|
|
317
|
+
{ alongFirst: low.first, alongSecond: low.second, point: from },
|
|
318
|
+
{ alongFirst: high.first, alongSecond: high.second, point: to },
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Every place two cubics cross, as a fraction along each and the point.
|
|
323
|
+
*
|
|
324
|
+
* Each segment is given the point it starts from, since a segment carries where
|
|
325
|
+
* it ends and not where it began. Two curves covering the same stretch answer
|
|
326
|
+
* with the two ends of that stretch, so a caller reading the answer as places to
|
|
327
|
+
* cut at gets the stretch marked off rather than chopped into slivers.
|
|
328
|
+
*/
|
|
329
|
+
export function curveCrossings(fromFirst, first, fromSecond, second, options = {}) {
|
|
330
|
+
const tolerance = options.tolerance ?? TOLERANCE;
|
|
331
|
+
const firstHull = hullOf(fromFirst, first);
|
|
332
|
+
const secondHull = hullOf(fromSecond, second);
|
|
333
|
+
if (apart(boxOf(firstHull), boxOf(secondHull), tolerance))
|
|
334
|
+
return [];
|
|
335
|
+
const shared = sharedStretch(firstHull, secondHull, tolerance);
|
|
336
|
+
if (shared)
|
|
337
|
+
return shared;
|
|
338
|
+
const stack = [
|
|
339
|
+
{
|
|
340
|
+
first: firstHull,
|
|
341
|
+
firstLow: 0,
|
|
342
|
+
firstHigh: 1,
|
|
343
|
+
second: secondHull,
|
|
344
|
+
secondLow: 0,
|
|
345
|
+
secondHigh: 1,
|
|
346
|
+
depth: 0,
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
const hits = [];
|
|
350
|
+
let made = 1;
|
|
351
|
+
const record = (pair) => {
|
|
352
|
+
const alongFirst = (pair.firstLow + pair.firstHigh) / 2;
|
|
353
|
+
const alongSecond = (pair.secondLow + pair.secondHigh) / 2;
|
|
354
|
+
hits.push({
|
|
355
|
+
alongFirst,
|
|
356
|
+
alongSecond,
|
|
357
|
+
point: pointAt(firstHull, alongFirst),
|
|
358
|
+
firstLow: pair.firstLow,
|
|
359
|
+
firstHigh: pair.firstHigh,
|
|
360
|
+
secondLow: pair.secondLow,
|
|
361
|
+
secondHigh: pair.secondHigh,
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
while (stack.length > 0) {
|
|
365
|
+
const pair = stack.pop();
|
|
366
|
+
const firstBox = boxOf(pair.first);
|
|
367
|
+
const secondBox = boxOf(pair.second);
|
|
368
|
+
if (apart(firstBox, secondBox, tolerance))
|
|
369
|
+
continue;
|
|
370
|
+
if (made >= BUDGET) {
|
|
371
|
+
record(pair);
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
const cutFirst = spread(firstBox) > tolerance;
|
|
375
|
+
const cutSecond = spread(secondBox) > tolerance;
|
|
376
|
+
if (!cutFirst && !cutSecond) {
|
|
377
|
+
if (chordGap(pair.first, pair.second) <= tolerance)
|
|
378
|
+
record(pair);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (pair.depth >= DEPTH) {
|
|
382
|
+
record(pair);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const firstMid = (pair.firstLow + pair.firstHigh) / 2;
|
|
386
|
+
const secondMid = (pair.secondLow + pair.secondHigh) / 2;
|
|
387
|
+
const firstParts = cutFirst
|
|
388
|
+
? halves(pair.first).map((hull, side) => ({
|
|
389
|
+
hull,
|
|
390
|
+
low: side === 0 ? pair.firstLow : firstMid,
|
|
391
|
+
high: side === 0 ? firstMid : pair.firstHigh,
|
|
392
|
+
}))
|
|
393
|
+
: [{ hull: pair.first, low: pair.firstLow, high: pair.firstHigh }];
|
|
394
|
+
const secondParts = cutSecond
|
|
395
|
+
? halves(pair.second).map((hull, side) => ({
|
|
396
|
+
hull,
|
|
397
|
+
low: side === 0 ? pair.secondLow : secondMid,
|
|
398
|
+
high: side === 0 ? secondMid : pair.secondHigh,
|
|
399
|
+
}))
|
|
400
|
+
: [{ hull: pair.second, low: pair.secondLow, high: pair.secondHigh }];
|
|
401
|
+
made += firstParts.length * secondParts.length;
|
|
402
|
+
for (const left of firstParts) {
|
|
403
|
+
for (const right of secondParts) {
|
|
404
|
+
stack.push({
|
|
405
|
+
first: left.hull,
|
|
406
|
+
firstLow: left.low,
|
|
407
|
+
firstHigh: left.high,
|
|
408
|
+
second: right.hull,
|
|
409
|
+
secondLow: right.low,
|
|
410
|
+
secondHigh: right.high,
|
|
411
|
+
depth: pair.depth + 1,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return clustered(hits).map((crossing) => sharpened(firstHull, secondHull, crossing));
|
|
417
|
+
}
|
package/dist/figure/morph.js
CHANGED
|
@@ -7,21 +7,7 @@
|
|
|
7
7
|
* line has to become an arc, because a line is an arc whose controls sit on it.
|
|
8
8
|
*/
|
|
9
9
|
import { vec2 } from '../values/vec2.js';
|
|
10
|
-
import { pointOn } from './path.js';
|
|
11
|
-
/** One segment cut into two at a fraction, both pieces kept, which is how a
|
|
12
|
-
* subpath gains a point without changing shape. */
|
|
13
|
-
function halves(from, curve, along) {
|
|
14
|
-
const a = vec2.lerp(from, curve.control1, along);
|
|
15
|
-
const b = vec2.lerp(curve.control1, curve.control2, along);
|
|
16
|
-
const c = vec2.lerp(curve.control2, curve.to, along);
|
|
17
|
-
const d = vec2.lerp(a, b, along);
|
|
18
|
-
const e = vec2.lerp(b, c, along);
|
|
19
|
-
const middle = vec2.lerp(d, e, along);
|
|
20
|
-
return [
|
|
21
|
-
{ control1: a, control2: d, to: middle },
|
|
22
|
-
{ control1: e, control2: c, to: curve.to },
|
|
23
|
-
];
|
|
24
|
-
}
|
|
10
|
+
import { pointOn, splitCurve } from './path.js';
|
|
25
11
|
/**
|
|
26
12
|
* A subpath rewritten to hold exactly this many segments, drawing the same shape.
|
|
27
13
|
*
|
|
@@ -49,7 +35,7 @@ function withCurves(subpath, wanted) {
|
|
|
49
35
|
let start = subpath.start;
|
|
50
36
|
for (let at = 0; at < cutAt; at++)
|
|
51
37
|
start = curves[at].to;
|
|
52
|
-
const [first, second] =
|
|
38
|
+
const [first, second] = splitCurve(start, curves[cutAt], 0.5);
|
|
53
39
|
curves = [...curves.slice(0, cutAt), first, second, ...curves.slice(cutAt + 1)];
|
|
54
40
|
}
|
|
55
41
|
return { start: subpath.start, curves, closed: subpath.closed };
|
package/dist/figure/path.d.ts
CHANGED
|
@@ -48,6 +48,17 @@ export declare function arc(centre: Vec2, radius: number, fromAngle: number, toA
|
|
|
48
48
|
/** A point on a cubic, with the segment's own start passed in because a segment
|
|
49
49
|
* carries where it ends and not where it began. */
|
|
50
50
|
export declare function pointOn(from: Vec2, curve: Cubic, along: number): Vec2;
|
|
51
|
+
/** Which way a piece is heading at a fraction along it, which is the derivative
|
|
52
|
+
* of a cubic and so a quadratic over the gaps between neighbouring points. */
|
|
53
|
+
export declare function slopeOn(from: Vec2, curve: Cubic, along: number): Vec2;
|
|
54
|
+
/**
|
|
55
|
+
* One piece cut into two at a fraction, both pieces drawing what the whole
|
|
56
|
+
* drew, by de Casteljau's construction.
|
|
57
|
+
*
|
|
58
|
+
* The piece's own start is passed in because a piece carries where it ends and
|
|
59
|
+
* not where it began.
|
|
60
|
+
*/
|
|
61
|
+
export declare function splitCurve(from: Vec2, curve: Cubic, along: number): [Cubic, Cubic];
|
|
51
62
|
/** Every point of a path moved by a transform, which is how a group's transform
|
|
52
63
|
* reaches the geometry rather than being carried alongside it. */
|
|
53
64
|
export declare function transformPath(path: Path, m: Mat3): Path;
|
package/dist/figure/path.js
CHANGED
|
@@ -121,6 +121,34 @@ export function pointOn(from, curve, along) {
|
|
|
121
121
|
const d = along * along * along;
|
|
122
122
|
return vec2(a * from.x + b * curve.control1.x + c * curve.control2.x + d * curve.to.x, a * from.y + b * curve.control1.y + c * curve.control2.y + d * curve.to.y);
|
|
123
123
|
}
|
|
124
|
+
/** Which way a piece is heading at a fraction along it, which is the derivative
|
|
125
|
+
* of a cubic and so a quadratic over the gaps between neighbouring points. */
|
|
126
|
+
export function slopeOn(from, curve, along) {
|
|
127
|
+
const u = 1 - along;
|
|
128
|
+
const a = 3 * u * u;
|
|
129
|
+
const b = 6 * u * along;
|
|
130
|
+
const c = 3 * along * along;
|
|
131
|
+
return vec2(a * (curve.control1.x - from.x) + b * (curve.control2.x - curve.control1.x) + c * (curve.to.x - curve.control2.x), a * (curve.control1.y - from.y) + b * (curve.control2.y - curve.control1.y) + c * (curve.to.y - curve.control2.y));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* One piece cut into two at a fraction, both pieces drawing what the whole
|
|
135
|
+
* drew, by de Casteljau's construction.
|
|
136
|
+
*
|
|
137
|
+
* The piece's own start is passed in because a piece carries where it ends and
|
|
138
|
+
* not where it began.
|
|
139
|
+
*/
|
|
140
|
+
export function splitCurve(from, curve, along) {
|
|
141
|
+
const a = vec2.lerp(from, curve.control1, along);
|
|
142
|
+
const b = vec2.lerp(curve.control1, curve.control2, along);
|
|
143
|
+
const c = vec2.lerp(curve.control2, curve.to, along);
|
|
144
|
+
const d = vec2.lerp(a, b, along);
|
|
145
|
+
const e = vec2.lerp(b, c, along);
|
|
146
|
+
const middle = vec2.lerp(d, e, along);
|
|
147
|
+
return [
|
|
148
|
+
{ control1: a, control2: d, to: middle },
|
|
149
|
+
{ control1: e, control2: c, to: curve.to },
|
|
150
|
+
];
|
|
151
|
+
}
|
|
124
152
|
/** Every point of a path moved by a transform, which is how a group's transform
|
|
125
153
|
* reaches the geometry rather than being carried alongside it. */
|
|
126
154
|
export function transformPath(path, m) {
|
package/dist/index.d.ts
CHANGED
|
@@ -20,9 +20,18 @@ export { mat3 } from './values/mat3.js';
|
|
|
20
20
|
export type { Mat3 } from './values/mat3.js';
|
|
21
21
|
export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
|
|
22
22
|
export type { Key, Track, TrackValue, Tracks } from './timing/track.js';
|
|
23
|
-
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
|
|
23
|
+
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, splitCurve, straight, transformPath } from './figure/path.js';
|
|
24
24
|
export type { Cubic, Path, Subpath } from './figure/path.js';
|
|
25
25
|
export { pathFromData } from './figure/path-data.js';
|
|
26
|
+
export { areaOf } from './figure/area.js';
|
|
27
|
+
export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
|
|
28
|
+
export type { BooleanOptions } from './figure/boolean.js';
|
|
29
|
+
export { containsPoint, flattenPath, nearestEdge, windingAt } from './figure/inside.js';
|
|
30
|
+
export type { Edge, FlattenOptions } from './figure/inside.js';
|
|
31
|
+
export { cutPath } from './figure/cut.js';
|
|
32
|
+
export type { Cut, CutOptions } from './figure/cut.js';
|
|
33
|
+
export { curveCrossings } from './figure/intersect.js';
|
|
34
|
+
export type { Crossing, CrossingOptions } from './figure/intersect.js';
|
|
26
35
|
export type { Colour, Fill, Mark, PathMark, Stroke, TextMark } from './figure/mark.js';
|
|
27
36
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
28
37
|
export type { Extent, ExtentChoice, Fit } from './figure/extent.js';
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,13 @@ export { vec3 } from './values/vec3.js';
|
|
|
14
14
|
export { interval } from './values/interval.js';
|
|
15
15
|
export { mat3 } from './values/mat3.js';
|
|
16
16
|
export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
|
|
17
|
-
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
|
|
17
|
+
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, splitCurve, straight, transformPath } from './figure/path.js';
|
|
18
18
|
export { pathFromData } from './figure/path-data.js';
|
|
19
|
+
export { areaOf } from './figure/area.js';
|
|
20
|
+
export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
|
|
21
|
+
export { containsPoint, flattenPath, nearestEdge, windingAt } from './figure/inside.js';
|
|
22
|
+
export { cutPath } from './figure/cut.js';
|
|
23
|
+
export { curveCrossings } from './figure/intersect.js';
|
|
19
24
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
20
25
|
export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
|
|
21
26
|
export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
|
package/package.json
CHANGED