1
0
Fork 0

[core] feat: Hotfix the reduce function in the bezier library (#224)

Fixes #117

Co-authored-by: joostdecock <joost@joost.at>
Reviewed-on: https://codeberg.org/freesewing/freesewing/pulls/224
Reviewed-by: Joost De Cock <joostdecock@noreply.codeberg.org>
Co-authored-by: Jonathan Haas <haasjona@gmail.com>
Co-committed-by: Jonathan Haas <haasjona@gmail.com>
This commit is contained in:
Jonathan Haas 2025-04-12 07:48:39 +00:00 committed by Joost De Cock
parent bd44ca1cd2
commit 5672638bf9
6 changed files with 172 additions and 4 deletions

View file

@ -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 }

View file

@ -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'

View file

@ -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'

View file

@ -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,

View file

@ -1,4 +1,4 @@
import { Bezier } from 'bezier-js'
import { Bezier } from './bezier.mjs'
import { Path } from './path.mjs'
import { Point } from './point.mjs'

View file

@ -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)
})
})