diff --git a/markdown/dev/reference/api/path/angleat/en.md b/markdown/dev/reference/api/path/angleat/en.md new file mode 100644 index 00000000000..7068802c39c --- /dev/null +++ b/markdown/dev/reference/api/path/angleat/en.md @@ -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 + + +```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 +} +``` + + +## Notes + +Keep in mind that calculations with Bézier curves are often approximations. diff --git a/markdown/dev/reference/api/utils/curveparameterfrompoint/en.md b/markdown/dev/reference/api/utils/curveparameterfrompoint/en.md new file mode 100644 index 00000000000..b1346a3d342 --- /dev/null +++ b/markdown/dev/reference/api/utils/curveparameterfrompoint/en.md @@ -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 + + +```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 +} +``` + + + +## 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. diff --git a/packages/core/src/path.mjs b/packages/core/src/path.mjs index 8e9287cd4e9..08a4972fce1 100644 --- a/packages/core/src/path.mjs +++ b/packages/core/src/path.mjs @@ -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 diff --git a/packages/core/src/utils.mjs b/packages/core/src/utils.mjs index 22bc34fb6d5..6d8c8b559c6 100644 --- a/packages/core/src/utils.mjs +++ b/packages/core/src/utils.mjs @@ -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 }, diff --git a/packages/core/tests/path.test.mjs b/packages/core/tests/path.test.mjs index 4c57a7d7bd1..50562bd6f47 100644 --- a/packages/core/tests/path.test.mjs +++ b/packages/core/tests/path.test.mjs @@ -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)