From 5672638bf979512713b307f5dcc98548aa77096c Mon Sep 17 00:00:00 2001 From: Jonathan Haas Date: Sat, 12 Apr 2025 07:48:39 +0000 Subject: [PATCH] [core] feat: Hotfix the reduce function in the bezier library (#224) Fixes #117 Co-authored-by: joostdecock Reviewed-on: https://codeberg.org/freesewing/freesewing/pulls/224 Reviewed-by: Joost De Cock Co-authored-by: Jonathan Haas Co-committed-by: Jonathan Haas --- packages/core/src/bezier.mjs | 135 ++++++++++++++++++++++++++ packages/core/src/index.mjs | 2 +- packages/core/src/part.mjs | 2 +- packages/core/src/path.mjs | 2 +- packages/core/src/utils.mjs | 2 +- packages/core/tests/bezierjs.test.mjs | 33 +++++++ 6 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/bezier.mjs create mode 100644 packages/core/tests/bezierjs.test.mjs diff --git a/packages/core/src/bezier.mjs b/packages/core/src/bezier.mjs new file mode 100644 index 00000000000..146a44f42ab --- /dev/null +++ b/packages/core/src/bezier.mjs @@ -0,0 +1,135 @@ +import { Bezier as UpstreamBezier } from 'bezier-js' + +/* + * The BezierJS library has an issue where it does not find + * the intersection of two paths under some circumstances. + * See: https://github.com/Pomax/bezierjs/issues/203 + * + * A PR was submitted to address this by Jonathan Haas + * See: https://github.com/Pomax/bezierjs/pull/219 + * + * However, that PR does not get any attention, and in general + * the library seems to be rather unmaintained. The maintainer + * (Pomax) says as much in the README. + * + * That being said, BezierJS is a great library and this stuff + * is just hard because there is no closed-form integral solution + * for this, so a lot of this is trial an error. + * + * Rather than maintain a fork, we extend the Bezier class and + * implement our own reduce method. + * + * The changes to the reduce method where written by Jonathan <3 + */ + +/* + * Extend the upstream Bezier class with a custom reduce method + */ +class Bezier extends UpstreamBezier { + reduce() { + const utils = this.getUtils() + const EPSILON = 0.001 + + function reduceStep(bezier) { + const splitTs = [] + let t1 = 0 + + if (bezier._t2 - bezier._t1 < EPSILON || bezier.simple()) { + return [bezier] + } + + while (t1 < 1) { + // Check if the rest of the curve is already simple + const remaining = bezier.split(t1, 1) + if (remaining.simple()) { + break + } + + // Binary search to find the furthest simple segment + let low = t1 + EPSILON + let high = 1 + let best = t1 + EPSILON + + if (low > best) { + break + } + + for (let i = 0; i < 100; i++) { + // limit to 20 iterations max + const mid = (low + high) / 2 + const segment = bezier.split(t1, mid) + + if (segment.simple()) { + best = mid + low = mid + } else { + high = mid + } + + if (t1 !== best && i >= 5) { + // we have found a good split location, don't need to be super exact + break + } + } + + splitTs.push(best) + + t1 = best + } + + // endpoint + splitTs.push(1) + + // Split the curve using the collected t values + const parts = [] + let prevT = 0 + for (const t of splitTs) { + const segment = bezier.split(prevT, t) + segment._t1 = utils.map(prevT, 0, 1, bezier._t1, bezier._t2) + segment._t2 = utils.map(t, 0, 1, bezier._t1, bezier._t2) + parts.push(segment) + prevT = t + } + + return parts + } + + // Vars we'll use + let i, + t1, + t2 = 0, + segment, + pass1 = [], + pass2 = [] + + // First pass: split on extrema + let extrema = this.extrema().values + // remove extrema very close to 1 or 0 + while (extrema[0] < EPSILON) { + extrema.shift() + } + while (extrema[extrema.length - 1] > 1 - EPSILON) { + extrema.shift() + } + // add 1 and 0 + extrema.unshift(0) + extrema.push(1) + + for (t1 = extrema[0], i = 1; i < extrema.length; i++) { + t2 = extrema[i] + segment = this.split(t1, t2) + segment._t1 = t1 + segment._t2 = t2 + pass1.push(segment) + t1 = t2 + } + + // second pass: further reduce these segments to simple segments + pass1.forEach(function (p1) { + pass2.push(...reduceStep(p1)) + }) + return pass2 + } +} + +export { Bezier } diff --git a/packages/core/src/index.mjs b/packages/core/src/index.mjs index 32ec22b01fb..e32a7081c07 100644 --- a/packages/core/src/index.mjs +++ b/packages/core/src/index.mjs @@ -1,4 +1,4 @@ -import { Bezier } from 'bezier-js' +import { Bezier } from './bezier.mjs' import { Attributes } from './attributes.mjs' import { Design } from './design.mjs' import { Pattern } from './pattern/index.mjs' diff --git a/packages/core/src/part.mjs b/packages/core/src/part.mjs index 2a86e8e9fde..7f404d77afe 100644 --- a/packages/core/src/part.mjs +++ b/packages/core/src/part.mjs @@ -1,4 +1,4 @@ -import { Bezier } from 'bezier-js' +import { Bezier } from './bezier.mjs' import { Attributes } from './attributes.mjs' import * as utils from './utils.mjs' import { Point, pointsProxy } from './point.mjs' diff --git a/packages/core/src/path.mjs b/packages/core/src/path.mjs index 8ea86918a02..f60dedc8349 100644 --- a/packages/core/src/path.mjs +++ b/packages/core/src/path.mjs @@ -1,6 +1,6 @@ import { Attributes } from './attributes.mjs' import { Point } from './point.mjs' -import { Bezier } from 'bezier-js' +import { Bezier } from './bezier.mjs' import { deg2rad, linesIntersect, diff --git a/packages/core/src/utils.mjs b/packages/core/src/utils.mjs index d1c6706b5a4..3d36428bf5c 100644 --- a/packages/core/src/utils.mjs +++ b/packages/core/src/utils.mjs @@ -1,4 +1,4 @@ -import { Bezier } from 'bezier-js' +import { Bezier } from './bezier.mjs' import { Path } from './path.mjs' import { Point } from './point.mjs' diff --git a/packages/core/tests/bezierjs.test.mjs b/packages/core/tests/bezierjs.test.mjs new file mode 100644 index 00000000000..83349289cbb --- /dev/null +++ b/packages/core/tests/bezierjs.test.mjs @@ -0,0 +1,33 @@ +import { expect } from 'chai' +import { Bezier } from '../src/bezier.mjs' + +describe('BezierJS', () => { + it('Should find intersections for paths that fail in upstream BezierJS', () => { + const tt1 = new Bezier( + 20.294698715209961, + 20.116849899291992, + 26.718513488769531, + 28.516490936279297, + 33.345268249511719, + 37.4105110168457, + 36.240531921386719, + 37.736736297607422 + ) + const tt2 = new Bezier( + 43.967803955078125, + 30.767040252685547, + 43.967803955078125, + 31.771089553833008, + 35.013500213623047, + 32.585041046142578, + 23.967803955078125, + 32.585041046142578 + ) + + const intersections = tt1.intersects(tt2) + expect(intersections.length).to.equal(1) + + const ttReduced = tt1.reduce() + expect(ttReduced.length).to.equal(3) + }) +})