[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:
parent
bd44ca1cd2
commit
5672638bf9
6 changed files with 172 additions and 4 deletions
135
packages/core/src/bezier.mjs
Normal file
135
packages/core/src/bezier.mjs
Normal 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 }
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Bezier } from 'bezier-js'
|
||||
import { Bezier } from './bezier.mjs'
|
||||
import { Path } from './path.mjs'
|
||||
import { Point } from './point.mjs'
|
||||
|
||||
|
|
33
packages/core/tests/bezierjs.test.mjs
Normal file
33
packages/core/tests/bezierjs.test.mjs
Normal 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)
|
||||
})
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue