diff --git a/src/path.js b/src/path.js index e0b35cb..960efde 100644 --- a/src/path.js +++ b/src/path.js @@ -23,12 +23,29 @@ function appendRound(digits) { }; } +function equal(x0, y0, x1, y1) { + return x0 === x1 && y0 === y1; +} + +function equalRound(digits) { + const d = Math.floor(digits); + if (!(d <= 15)) return equal; + const k = 10 ** d; + const r = function(value) { + return Math.round(value * k); + }; + return function(x0, y0, x1, y1) { + return r(x0) === r(x1) && r(y0) === r(y1); + }; +} + export class Path { constructor(digits) { this._x0 = this._y0 = // start of current subpath this._x1 = this._y1 = null; // end of current subpath this._ = ""; this._append = digits == null ? append : appendRound(digits); + this._equal = digits == null ? equal : equalRound(digits); } moveTo(x, y) { this._append`M${this._x0 = this._x1 = +x},${this._y0 = this._y1 = +y}`; @@ -133,7 +150,15 @@ export class Path { // Is this arc non-empty? Draw an arc! else if (da > epsilon) { - this._append`A${r},${r},0,${+(da >= pi)},${cw},${this._x1 = x + r * Math.cos(a1)},${this._y1 = y + r * Math.sin(a1)}`; + // If the start and end points are coincident after rounding, we need to draw two consecutive arcs. + const x1 = x + r * Math.cos(a1); + const y1 = y + r * Math.sin(a1); + if (da >= pi && this._equal(x0, y0, x1, y1)) { + da /= 2; + let a00 = a0 + da; + this._append`A${r},${r},0,${+(da >= pi)},${cw},${x + r * Math.cos(a00)},${y + r * Math.sin(a00)}`; + } + this._append`A${r},${r},0,${+(da >= pi)},${cw},${this._x1 = x1},${this._y1 = y1}`; } } rect(x, y, w, h) { diff --git a/test/pathRound-test.js b/test/pathRound-test.js index 6c82827..d714c69 100644 --- a/test/pathRound-test.js +++ b/test/pathRound-test.js @@ -50,6 +50,29 @@ it("pathRound.arc(x, y, r, a0, a1, ccw) limits the precision", () => { assert.strictEqual(p + "", precision(p0 + "", 1)); }); +it("pathRound.arc(x, y, r, a0, a1, false) draws two arcs for near-circular arcs with rounding", () => { + const p0 = path(), p = pathRound(1); + const a0 = -1.5707963267948966; + const a1 = 4.712383653719071; + const a00 = a0 + (a1 - a0) / 2; + p0.arc(0, 0, 75, a0, a00); + p0.arc(0, 0, 75, a00, a1); + p.arc(0, 0, 75, a0, a1); + assert.strictEqual(p + "", precision(p0 + "", 1)); +}); + +it("pathRound.arc(x, y, r, a0, a1, true) draws two arcs for near-circular arcs with rounding", () => { + const p0 = path(), p = pathRound(1); + const a0 = 0; + const a1 = a0 + 1e-5; + const da = 2 * Math.PI - 1e-5; + const a00 = a0 - da / 2; + p0.arc(0, 0, 75, a0, a00, true); + p0.arc(0, 0, 75, a00, a1, true); + p.arc(0, 0, 75, a0, a1, true); + assert.strictEqual(p + "", precision(p0 + "", 1)); +}); + it("pathRound.arcTo(x1, y1, x2, y2, r) limits the precision", () => { const p0 = path(), p = pathRound(1); p0.arcTo(10.0001, 10.0001, 123.456, 456.789, 12345.6789); @@ -79,5 +102,5 @@ it("pathRound.rect(x, y, w, h) limits the precision", () => { }); function precision(str, precision) { - return str.replace(/\d+\.\d+/g, s => +parseFloat(s).toFixed(precision)); + return str.replace(/-?\d+\.\d+(e-?\d+)?/g, s => +parseFloat(s).toFixed(precision)); }