1
0
Fork 0

Merge pull request #6558 from HaasJona/add/path-angle

add(core): Add Path.angleAt(point) method
This commit is contained in:
Joost De Cock 2024-04-18 19:02:26 +02:00 committed by GitHub
commit f82ebd97d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 244 additions and 9 deletions

View file

@ -0,0 +1,54 @@
---
title: Path.angleAt()
---
The `Path.angleAt()` method returns the (tangent) angle of a path at a specific point.
If the given point is a sharp corner, this method prefers returning the angle directly before the corner.
If the given point does not lie (approximately) on the path, this method returns `false`.
## Signature
```js
number|false path.angleAt(Point point)
```
## Example
<Example caption="Example of the Path.angleAt() method">
```js
({ Point, points, Path, paths, snippets, Snippet, part }) => {
points.A = new Point(45, 60)
points.B = new Point(10, 30)
points.BCp2 = new Point(40, 20)
points.C = new Point(90, 30)
points.CCp1 = new Point(50, -30)
points.D = new Point(50, 80)
points.DCp1 = new Point(70, 30)
paths.demo = new Path()
.move(points.D)
.curve(points.DCp1, points.DCp1, points.C)
.curve(points.CCp1, points.BCp2, points.B)
.line(points.A)
points.testPoint = paths.demo.shiftFractionAlong(0.55)
snippets.point = new Snippet("notch", points.testPoint)
let angle = paths.demo.angleAt(points.testPoint)
//draw a tangent path
paths.tangent = new Path()
.move(points.testPoint.shift(angle, -30))
.line(points.testPoint.shift(angle, 30))
.attr("class", "lining dashed")
return part
}
```
</Example>
## Notes
Keep in mind that calculations with Bézier curves are often approximations.

View file

@ -0,0 +1,77 @@
---
title: utils.curveParameterFromPoint()
---
The `utils.curveParameterFromPoint()` function calculates where the point `check` lies on a
curve described by points `start`, `cp1`, `cp2`, and `end`.
For example a return value of 0 indicates that the given point is the start of the curve, a return value
of 1 indicated that the given point is identical to the end of the curve.
A return value of 0.5 indicates that the start point and the first control point had the same influence
as the end point and the second control point, to create the checked point, but this doesn't necessarily mean
that the point lies exactly half-way on the curve.
This method returns `false` if the point isn't (approximately) located on the curve.
## Signature
```js
number|false utils.curveParameterFromPoint(
Point start,
Point cp1,
Point cp2,
Point end,
Point check
)
```
## Example
<Example caption="A Utils.curveParameterFromPoint() example">
```js
({ Point, points, Path, paths, Snippet, snippets, getId, utils, part }) => {
points.start = new Point(10, 10)
points.cp1 = new Point(90, 30)
points.cp2 = new Point(10, 40)
points.end = new Point(90, 60)
const scatter = []
for (let i = 1; i < 19; i++) {
for (let j = 1; j < 14; j++) {
scatter.push(new Point(i * 10, j * 10))
}
}
let snippet
for (let point of scatter) {
let t = utils.curveParameterFromPoint(
points.start,
points.cp1,
points.cp2,
points.end,
point
)
if(t !== false) {
points[getId()] = point.addText(` ${Math.round(t * 100) / 100}`, 'text-sm')
snippets[getId()] = new Snippet('notch', point)
}
}
paths.curve = new Path()
.move(points.start)
.curve(points.cp1, points.cp2, points.end)
.addClass("fabric stroke-lg")
return part
}
```
</Example>
## Notes
Keep in mind that calculations with Bézier curves are often approximations.
This method is mostly used as internal building block for methods like
`utils.pointOnCurve()`, `Path.split()` or `Path.angleAt()` and probably is not very relevant
for direct usage from pattern code.

View file

@ -6,7 +6,7 @@ import {
lineIntersectsCurve, lineIntersectsCurve,
curvesIntersect, curvesIntersect,
pointOnLine, pointOnLine,
pointOnCurve, curveParameterFromPoint,
curveEdge, curveEdge,
round, round,
__addNonEnumProp, __addNonEnumProp,
@ -875,7 +875,7 @@ Path.prototype.smurve_ = function (to) {
} }
/** /**
* Splits path on point, and retuns both halves as Path instances * Splits path on point, and returns both halves as Path instances
* *
* @param {Point} point - The Point to split this Path on * @param {Point} point - The Point to split this Path on
* @return {Array} halves - An array holding the two Path instances that make the split halves * @return {Array} halves - An array holding the two Path instances that make the split halves
@ -907,7 +907,13 @@ Path.prototype.split = function (point) {
break break
} }
} else if (path.ops[1].type === 'curve') { } else if (path.ops[1].type === 'curve') {
let t = pointOnCurve(path.ops[0].to, path.ops[1].cp1, path.ops[1].cp2, path.ops[1].to, point) let t = curveParameterFromPoint(
path.ops[0].to,
path.ops[1].cp1,
path.ops[1].cp2,
path.ops[1].to,
point
)
if (t !== false) { if (t !== false) {
let curve = new Bezier( let curve = new Bezier(
{ x: path.ops[0].to.x, y: path.ops[0].to.y }, { x: path.ops[0].to.x, y: path.ops[0].to.y },
@ -953,6 +959,51 @@ Path.prototype.split = function (point) {
return [firstHalf, secondHalf] return [firstHalf, secondHalf]
} }
/**
* Determines the angle (tangent) of this path at the given point. If the given point is a sharp corner of this path,
* this method returns the angle directly before the point.
*
* @param {Point} point - The Point to determine the angle of relative to this Path
* @return {number|false} the angle of degrees at that point or false if the given Point doesn't lie on this Path
*/
Path.prototype.angleAt = function (point) {
if (!(point instanceof Point))
this.log.error('Called `Path.angleAt(point)` but `point` is not a `Point` object')
let divided = this.divide()
for (let pi = 0; pi < divided.length; pi++) {
let path = divided[pi]
if (path.ops[1].type === 'line') {
if (pointOnLine(path.ops[0].to, path.ops[1].to, point)) {
return path.ops[0].to.angle(path.ops[1].to)
}
} else if (path.ops[1].type === 'curve') {
let t = curveParameterFromPoint(
path.ops[0].to,
path.ops[1].cp1,
path.ops[1].cp2,
path.ops[1].to,
point
)
if (t !== false) {
const curve = new Bezier(
{ x: path.ops[0].to.x, y: path.ops[0].to.y },
{ x: path.ops[1].cp1.x, y: path.ops[1].cp1.y },
{ x: path.ops[1].cp2.x, y: path.ops[1].cp2.y },
{ x: path.ops[1].to.x, y: path.ops[1].to.y }
)
let normal = curve.normal(t)
// atan2's first parameter is y, but we're swapping them because
// we're interested in the tangent angle, not normal
return (Math.atan2(normal.x, normal.y) / Math.PI) * 180
}
}
}
return false
}
/** /**
* Returns the startpoint of this path * Returns the startpoint of this path
* *
@ -1018,7 +1069,13 @@ Path.prototype.trim = function () {
{ x: ops[1].cp2.x, y: ops[1].cp2.y }, { x: ops[1].cp2.x, y: ops[1].cp2.y },
{ x: ops[1].to.x, y: ops[1].to.y } { x: ops[1].to.x, y: ops[1].to.y }
) )
let t = pointOnCurve(ops[0].to, ops[1].cp1, ops[1].cp2, ops[1].to, intersection) let t = curveParameterFromPoint(
ops[0].to,
ops[1].cp1,
ops[1].cp2,
ops[1].to,
intersection
)
let split = curve.split(t) let split = curve.split(t)
let side let side
if (first) side = split.left if (first) side = split.left

View file

@ -541,16 +541,36 @@ export function pointOnBeam(from, to, check, precision = 1e6) {
/** /**
* Finds out whether a Point lies on a (cubic) Bezier curve * Finds out whether a Point lies on a (cubic) Bezier curve
* *
* @param {Point} from - Start of the curve * @param {Point} start - Start of the curve
* @param {Point} cp1 - Control point at the start of the curve * @param {Point} cp1 - Control point at the start of the curve
* @param {Point} cp1 - Control point at the end of the curve * @param {Point} cp2 - Control point at the end of the curve
* @param {Point} end - End of the curve * @param {Point} end - End of the curve
* @param {Point} check - Point to check * @param {Point} check - Point to check
* @return {bool} result - True of the Point is on the curve, false when not * @return {boolean} result - True of the Point is on the curve, false when not
*/ */
export function pointOnCurve(start, cp1, cp2, end, check) { export function pointOnCurve(start, cp1, cp2, end, check) {
if (start.sitsOn(check)) return true return curveParameterFromPoint(start, cp1, cp2, end, check) !== false
if (end.sitsOn(check)) return true }
/**
* Finds where a Point lies on a (cubic) Bezier curve and returns the curve parameter t of this position.
* For example a return value of 0 indicates that the given point is the start of the curve, a return value
* of 1 indicated that the given point is identical to the end of the curve.
*
* A return value of 0.5 indicates that the start point and the first control point had the same influence
* as the end point and the second control point, to create the point, but this doesn't necessarily mean
* that the point lies exactly half-way on the curve.
*
* @param {Point} start - Start of the curve
* @param {Point} cp1 - Control point at the start of the curve
* @param {Point} cp2 - Control point at the end of the curve
* @param {Point} end - End of the curve
* @param {Point} check - Point to check
* @return {false|number} result - relative position on the curve (value between 0 and 1), false when not on curve
*/
export function curveParameterFromPoint(start, cp1, cp2, end, check) {
if (start.sitsOn(check)) return 0
if (end.sitsOn(check)) return 1
let curve = new Bezier( let curve = new Bezier(
{ x: start.x, y: start.y }, { x: start.x, y: start.y },
{ x: cp1.x, y: cp1.y }, { x: cp1.x, y: cp1.y },

View file

@ -680,6 +680,33 @@ describe('Path', () => {
expect(halves[1].ops[0].to.y).to.equal(30) expect(halves[1].ops[0].to.y).to.equal(30)
}) })
it('Should determine the angle on a path', () => {
const a = new Point(0, 0)
const b = new Point(0, 40)
const c = new Point(40, 40)
const d = new Point(100, 40)
const e = new Point(100, 0)
const linePoint = new Point(80, 40)
const curvePoint = new Point(5, 35)
const path = new Path().move(a).curve(b, b, c).line(d).line(e)
let angleAtStart = path.angleAt(a)
let angleOnCurve = path.angleAt(curvePoint)
let angleOnJoint = path.angleAt(c)
let angleOnLine = path.angleAt(linePoint)
let angleOnCorner = path.angleAt(d)
let angleOnEnd = path.angleAt(e)
expect(angleAtStart).to.equal(-90)
expect(angleOnCurve).to.equal(-45)
expect(angleOnJoint).to.equal(0)
expect(angleOnLine).to.equal(0)
expect(angleOnCorner).to.equal(0)
expect(angleOnEnd).to.equal(90)
})
it('Should trim a path when lines overlap', () => { it('Should trim a path when lines overlap', () => {
const A = new Point(0, 0) const A = new Point(0, 0)
const B = new Point(100, 100) const B = new Point(100, 100)