Skip to content

Commit

Permalink
Detect coincident points when drawing arcs with rounding.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jasondavies committed Oct 10, 2024
1 parent 103ce94 commit 97f6a50
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 2 deletions.
31 changes: 30 additions & 1 deletion src/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,33 @@ function appendRound(digits) {
};
}

function round(digits) {
const k = 10 ** digits;
return function(value) {
return Math.round(value * k) / k;
};
}

function equal(x0, y0, x1, y1) {
return x0 === x1 && y0 === y1;
}

function equalRound(digits) {
let d = Math.floor(digits);
if (d > 15) return equal;
const r = round(digits);
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}`;
Expand Down Expand Up @@ -133,7 +154,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) {
Expand Down
25 changes: 24 additions & 1 deletion test/pathRound-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}

0 comments on commit 97f6a50

Please sign in to comment.