Skip to content

Commit 7f4ed0d

Browse files
committed
Detect coincident points when drawing arcs with rounding.
When rounding is used, it's possible for `arc()` to generate empty arcs in the case where the start and end points are almost coincident, and become coincident after rounding is applied. This adds a check for coincident points after rounding is applied, and splits the arc into two if coincident points are detected. Fixes #38.
1 parent 103ce94 commit 7f4ed0d

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

src/path.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,33 @@ function appendRound(digits) {
2323
};
2424
}
2525

26+
function round(digits) {
27+
const k = 10 ** digits;
28+
return function(value) {
29+
return Math.round(value * k) / k;
30+
};
31+
}
32+
33+
function equal(x0, y0, x1, y1) {
34+
return x0 === x1 && y0 === y1;
35+
}
36+
37+
function equalRound(digits) {
38+
let d = Math.floor(digits);
39+
if (d > 15) return equal;
40+
const r = round(digits);
41+
return function(x0, y0, x1, y1) {
42+
return r(x0) === r(x1) && r(y0) === r(y1);
43+
};
44+
}
45+
2646
export class Path {
2747
constructor(digits) {
2848
this._x0 = this._y0 = // start of current subpath
2949
this._x1 = this._y1 = null; // end of current subpath
3050
this._ = "";
3151
this._append = digits == null ? append : appendRound(digits);
52+
this._equal = digits === null ? equal : equalRound(digits);
3253
}
3354
moveTo(x, y) {
3455
this._append`M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}`;
@@ -133,7 +154,15 @@ export class Path {
133154

134155
// Is this arc non-empty? Draw an arc!
135156
else if (da > epsilon) {
136-
this._append`A${r},${r},0,${+(da >= pi)},${cw},${this._x1 = x + r * Math.cos(a1)},${this._y1 = y + r * Math.sin(a1)}`;
157+
// If the start and end points are coincident after rounding, we need to draw two arcs.
158+
const x1 = x + r * Math.cos(a1);
159+
const y1 = y + r * Math.sin(a1);
160+
if (this._equal(x0, y0, x1, y1)) {
161+
da /= 2;
162+
let a00 = a0 + da;
163+
this._append`A${r},${r},0,${+(da >= pi)},${cw},${x + r * Math.cos(a00)},${y + r * Math.sin(a00)}`;
164+
}
165+
this._append`A${r},${r},0,${+(da >= pi)},${cw},${this._x1 = x1},${this._y1 = y1}`;
137166
}
138167
}
139168
rect(x, y, w, h) {

test/pathRound-test.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ it("pathRound.arc(x, y, r, a0, a1, ccw) limits the precision", () => {
5050
assert.strictEqual(p + "", precision(p0 + "", 1));
5151
});
5252

53+
it("pathRound.arc(x, y, r, a0, a1, ccw) draws two arcs for near-circular arcs with rounding", () => {
54+
const p0 = path(), p = pathRound(1);
55+
const a0 = -1.5707963267948966;
56+
const a1 = 4.712383653719071;
57+
const a00 = a0 + (a1 - a0) / 2;
58+
p0.arc(0, 0, 75, a0, a00, false);
59+
p0.arc(0, 0, 75, a00, a1, false);
60+
p.arc(0, 0, 75, a0, a1, false);
61+
assert.strictEqual(p + "", precision(p0 + "", 1));
62+
});
63+
5364
it("pathRound.arcTo(x1, y1, x2, y2, r) limits the precision", () => {
5465
const p0 = path(), p = pathRound(1);
5566
p0.arcTo(10.0001, 10.0001, 123.456, 456.789, 12345.6789);
@@ -79,5 +90,5 @@ it("pathRound.rect(x, y, w, h) limits the precision", () => {
7990
});
8091

8192
function precision(str, precision) {
82-
return str.replace(/\d+\.\d+/g, s => +parseFloat(s).toFixed(precision));
93+
return str.replace(/-?\d+\.\d+(e-?\d+)?/g, s => +parseFloat(s).toFixed(precision));
8394
}

0 commit comments

Comments
 (0)