Merge pull request #6558 from HaasJona/add/path-angle
add(core): Add Path.angleAt(point) method
This commit is contained in:
commit
f82ebd97d8
5 changed files with 244 additions and 9 deletions
54
markdown/dev/reference/api/path/angleat/en.md
Normal file
54
markdown/dev/reference/api/path/angleat/en.md
Normal 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.
|
|
@ -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.
|
|
@ -6,7 +6,7 @@ import {
|
|||
lineIntersectsCurve,
|
||||
curvesIntersect,
|
||||
pointOnLine,
|
||||
pointOnCurve,
|
||||
curveParameterFromPoint,
|
||||
curveEdge,
|
||||
round,
|
||||
__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
|
||||
* @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
|
||||
}
|
||||
} 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) {
|
||||
let curve = new Bezier(
|
||||
{ x: path.ops[0].to.x, y: path.ops[0].to.y },
|
||||
|
@ -953,6 +959,51 @@ Path.prototype.split = function (point) {
|
|||
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
|
||||
*
|
||||
|
@ -1018,7 +1069,13 @@ Path.prototype.trim = function () {
|
|||
{ x: ops[1].cp2.x, y: ops[1].cp2.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 side
|
||||
if (first) side = split.left
|
||||
|
|
|
@ -541,16 +541,36 @@ export function pointOnBeam(from, to, check, precision = 1e6) {
|
|||
/**
|
||||
* 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 end 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 {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) {
|
||||
if (start.sitsOn(check)) return true
|
||||
if (end.sitsOn(check)) return true
|
||||
return curveParameterFromPoint(start, cp1, cp2, end, check) !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
{ x: start.x, y: start.y },
|
||||
{ x: cp1.x, y: cp1.y },
|
||||
|
|
|
@ -680,6 +680,33 @@ describe('Path', () => {
|
|||
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', () => {
|
||||
const A = new Point(0, 0)
|
||||
const B = new Point(100, 100)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue