1
0
Fork 0

feat(core): Added Path.combine and related changes, closes #5976

The discussion in #5976 is whether `Path.join()` should use a line
segment to close any gaps in the path caused by move operations, or by
differences in the end and start points of paths being joined.

The answer is yes, that is the intended behaviour, but people who read
_join_ might expect differently.

So I have made a few changes to clarify this:

- The new `Path.combine()` method combines multiple path instances into
  a single instance without making any changes to the drawing operations
- Since `Path.combine()` is variadic, I have also updated `Path.join()`
  to be variadic too, since that is more consistent.
- The old way of calling `Path.join(path, bool)` is deprecated and will
  log a warning. Calling `Path.join()` this way will be removed in v4.
- Related to this change is how `Path.length()` should behave when there
  are gaps in the path. Currently, it skips those. So I've added a
  parameter that when set to `true` will include them.
- Added documentation for `Path.combine()`
- Updated documentation for `Path.join()`
- Updated documentation for `Path.length()`
This commit is contained in:
joostdecock 2024-02-10 15:40:41 +01:00
parent 323d8d5bd7
commit a30b08371c
8 changed files with 200 additions and 61 deletions

View file

@ -1,6 +1,10 @@
Unreleased:
Added:
core:
- Added the `Path.combine()` method
- `Path.join()` is now variadico
- `Path.length()` now takes an parameter to include move operations in the length calculation
lumina:
- Initial release
lumira:
@ -17,6 +21,10 @@ Unreleased:
tristan:
- Inital release
Deprecated:
core:
- Calling `Path.join` with a second parameter to indicate that the resulting paths most be closed is now deprecated and will be removed in FreeSewing v4.
Fixed:
carlton:
- Fixed a stray seam allowance path on the collar

View file

@ -0,0 +1,58 @@
---
title: Path.combine()
---
The `Path.combines()` method combines this path with one or more other paths
into a single Path instance.
Any gaps in the path (caused by move operations) will be left as-is, rather
than joined with a line. If that's not what you want, you should use
[`Path.join()`](/reference/api/path/join) instead.
## Signature
```js
Path path.combine(path other)
```
## Examples
<Example caption="Example of the Path.combine() method">
```js
({ Point, points, Path, paths, part }) => {
points.A1 = new Point(0, 0)
points.A2 = new Point(60, 0)
points.B1 = new Point(0, 10)
points.B2 = new Point(60, 10)
points.C1 = new Point(0, 20)
points.C2 = new Point(60, 20)
paths.path1 = new Path()
.move(points.A1)
.line(points.A2)
.setClass("various")
paths.path2 = new Path()
.move(points.B1)
.line(points.B2)
.setClass("note")
paths.path3 = new Path()
.move(points.C1)
.line(points.C2)
.setClass("canvas")
paths.combo = paths.path1
.combine(paths.path2, paths.path3)
.setClass("lining dotted")
return part
}
```
</Example>
## Notes
`Path.combine()` method is _variadic_, so you can pass multiple paths to join

View file

@ -2,7 +2,11 @@
title: Path.join()
---
The `Path.join()` method joins this path with another path.
The `Path.join()` method joins this path with one or more other paths.
Any gaps in the path (caused by move operations) will be filled-in with a line.
If that's not what you want, you should use
[`Path.combine()`](/reference/api/path/combine) instead.
## Signature
@ -17,24 +21,30 @@ Path path.join(path other)
```js
({ Point, points, Path, paths, 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.A1 = new Point(0, 0)
points.A2 = new Point(60, 0)
points.B1 = new Point(0, 10)
points.B2 = new Point(60, 10)
points.C1 = new Point(0, 20)
points.C2 = new Point(60, 20)
paths.path1 = new Path()
.move(points.A)
.line(points.B)
.move(points.A1)
.line(points.A2)
.setClass("various")
paths.path2 = new Path()
.move(points.B)
.curve(points.BCp2, points.CCp1, points.C)
.move(points.B1)
.line(points.B2)
.setClass("note")
paths.path3 = new Path()
.move(points.C1)
.line(points.C2)
.setClass("canvas")
paths.joint = paths.path1
.join(paths.path2)
.join(paths.path2, paths.path3)
.setClass("lining dotted")
return part
@ -45,4 +55,5 @@ Path path.join(path other)
## Notes
You cannot join a closed path to another path
- `Path.join()` is _variadic_, so you can pass multiple paths to join
- You cannot join a closed path to another path

View file

@ -7,50 +7,43 @@ The `Path.length()` method returns the length of the path.
## Signature
```js
float path.length()
float path.length(bool withMoves = false)
```
## Example
<Example caption="Example of the Path.length() method">
```js
({ Point, points, Path, paths, macro, utils, units, part }) => {
({ Point, points, Path, paths, units, 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.A1 = new Point(0, 0)
points.A2 = new Point(160, 0)
points.B1 = new Point(0, 10)
points.B2 = new Point(160, 10)
points.C1 = new Point(0, 20)
points.C2 = new Point(160, 20)
paths.AB = new Path()
.move(points.A)
.line(points.B)
paths.path1 = new Path()
.move(points.A1)
.line(points.A2)
.move(points.B1)
.line(points.B2)
.move(points.C1)
.line(points.C2)
.setClass("various")
paths.BC = new Path()
.move(points.B)
.curve(points.BCp2, points.CCp1, points.C)
points.label1 = new Point(25, 8).addText('Total length = ' + units(paths.path1.length()))
points.label2 = new Point(25, 18).addText('Total length with moves = ' + units(paths.path1.length(true)))
const lengthABC = paths.AB.length() + paths.BC.length()
macro("pd", {
path: new Path().move(points.B).line(points.A),
d: 10
})
macro("pd", {
path: new Path().move(points.B).curve(points.BCp2, points.CCp1, points.C),
d: -10
})
points.label = new Point(25, 40)
.addText('Total length = ' + units(lengthABC))
// Set a path to prevent clipping
paths.noclip = new Path()
.move(new Point(10, -15))
.move(new Point(90, 60))
return part
}
```
</Example>
## Notes
By default, `Path.length()` will measure the combined length of all drawing operations in the Path, but skip
over gaps in the path (caused by move operations).
If you want the full length of the Path, including move operations, pass `true` to `Path.length()`.

View file

@ -266,6 +266,16 @@ Path.prototype.close = function () {
return this
}
/**
* Combines one or more Paths into a single Path instance
*
* @param {array} paths - The paths to combine
* @return {Path} combo - The combined Path instance
*/
Path.prototype.combine = function (...paths) {
return __combinePaths(this, ...paths)
}
/**
* Adds a curve operation via cp1 & cp2 to Point to
*
@ -512,28 +522,63 @@ Path.prototype.intersectsY = function (y) {
/**
* Joins this Path with that Path, and closes them if wanted
*
* @param {Path} that - The Path to join this Path with
* @param {bool} closed - Whether or not to close the joint Path
* The legacy (prior to v3.2) form of this method too two parameters:
* - The Path to join this path with
* - A boolean expressing whether the joined path should be closed
* In retrospect, that was kind of a dumb idea, because if the path
* needs tobe closed, you can juse chain the .join() with a .close()
*
* So now, this method is variadic, and it will join as many paths as you want.
* However, we keep it backwards compatible, and raise a deprecation warning when used that way.
*
* @param {array} paths - The Paths to join this Path with
* @return {Path} joint - The joint Path instance
*/
Path.prototype.join = function (that, closed = false) {
if (that instanceof Path !== true)
Path.prototype.join = function (...paths) {
if (paths.length < 1) {
this.log.error('Called `Path.join(that)` but `that` is not a `Path` object')
return __joinPaths([this, that], closed)
return this
}
/*
* Check for legacy signature
*/
if (paths.length === 2 && [true, false].includes(paths[1])) {
this.log.warn(
'`Path.join()` was called with the legacy signature passing a bool as second parameter. This is deprecated and will be removed in FreeSewing v4'
)
return paths[1] ? __joinPaths([this, paths[0]]).close() : __joinPaths([this, paths[0]])
}
/*
* New variadic approach
*/
let i = 0
for (const path of paths) {
if (path instanceof Path !== true)
this.log.error(
`Called \`Path.join(paths)\` but the path with index \`${i}\` is not a \`Path\` object`
)
i++
}
return __joinPaths([this, ...paths])
}
/**
* Return the length of this Path
*
* @param {bool} withMoves - Include length of move operations inside the path
* @return {float} length - The length of this path
*/
Path.prototype.length = function () {
Path.prototype.length = function (withMoves = false) {
let current, start
let length = 0
for (let i in this.ops) {
let op = this.ops[i]
if (op.type === 'move') {
start = op.to
if (typeof start === 'undefined') start = op.to
else if (withMoves) length += current.dist(op.to)
} else if (op.type === 'line') {
length += current.dist(op.to)
} else if (op.type === 'curve') {
@ -861,8 +906,8 @@ Path.prototype.split = function (point) {
}
}
if (firstHalf.length > 0) firstHalf = __joinPaths(firstHalf, false)
if (secondHalf.length > 0) secondHalf = __joinPaths(secondHalf, false)
if (firstHalf.length > 0) firstHalf = __joinPaths(firstHalf)
if (secondHalf.length > 0) secondHalf = __joinPaths(secondHalf)
return [firstHalf, secondHalf]
}
@ -946,9 +991,9 @@ Path.prototype.trim = function () {
first = false
}
let joint
if (trimmedStart.length > 0) joint = __joinPaths(trimmedStart, false).join(glue)
if (trimmedStart.length > 0) joint = __joinPaths(trimmedStart).join(glue)
else joint = glue
if (trimmedEnd.length > 0) joint = joint.join(__joinPaths(trimmedEnd, false))
if (trimmedEnd.length > 0) joint = joint.join(__joinPaths(trimmedEnd))
return joint.trim()
}
@ -1187,6 +1232,21 @@ function __bbbbox(boxes) {
return { topLeft: new Point(minX, minY), bottomRight: new Point(maxX, maxY) }
}
/**
* Combines path segments into a single path instance
*
* @private
* @param {Array} paths - An Array of Path objects
* @return {object} path - A Path instance
*/
function __combinePaths(...paths) {
const joint = new Path().__withLog(paths[0].log)
const ops = []
for (const path of paths) joint.ops.push(...path.ops)
return joint
}
/**
* Returns an object holding topLeft and bottomRight Points of the bounding box of a curve
*
@ -1208,10 +1268,9 @@ function __curveBoundingBox(curve) {
*
* @private
* @param {Array} paths - An Array of Path objects
* @param {bool} closed - Whether or not to close the joined paths
* @return {object} path - A Path instance
*/
function __joinPaths(paths, closed = false) {
function __joinPaths(paths) {
let joint = new Path().__withLog(paths[0].log).move(paths[0].ops[0].to)
let current
for (let p of paths) {
@ -1231,7 +1290,6 @@ function __joinPaths(paths, closed = false) {
if (op.to) current = op.to
}
}
if (closed) joint.close()
return joint
}
@ -1335,7 +1393,7 @@ function __pathOffset(path, distance) {
if (!start) start = current
}
return __joinPaths(offset, closed)
return closed ? __joinPaths(offset).close() : __joinPaths(offset)
}
/**

View file

@ -197,6 +197,7 @@ describe('Path', () => {
.curve(new Point(0, 40), new Point(123, 34), new Point(230, 4))
const joint = curve.join(line)
expect(joint.ops.length).to.equal(4)
expect(joint.ops[2].type).to.equal('line')
})
it('Should join paths that have noop operations', () => {
@ -209,7 +210,7 @@ describe('Path', () => {
expect(joint.ops.length).to.equal(6)
})
it('Should throw error when joining a closed paths', () => {
it('Should throw error when joining a closed path', () => {
const line = new Path().move(new Point(0, 0)).line(new Point(0, 40))
const curve = new Path()
.move(new Point(123, 456))
@ -218,6 +219,16 @@ describe('Path', () => {
expect(() => curve.join(line)).to.throw()
})
it('Should combine paths', () => {
const line = new Path().move(new Point(0, 0)).line(new Point(0, 40))
const curve = new Path()
.move(new Point(123, 456))
.curve(new Point(0, 40), new Point(123, 34), new Point(230, 4))
const combo = curve.combine(line)
expect(combo.ops.length).to.equal(4)
expect(combo.ops[2].type).to.equal('move')
})
it('Should shift along a line', () => {
const line = new Path().move(new Point(0, 0)).line(new Point(0, 40))
expect(line.shiftAlong(20).y).to.equal(20)

View file

@ -155,7 +155,6 @@ describe('Pattern', () => {
const pattern = new Test()
pattern.draft()
console.log(pattern.setStores[pattern.activeSet].logs.error[0])
expect(pattern.setStores[pattern.activeSet].logs.error[0]).to.include(
'Could not inject part `otherPart` into part `front`'
)

View file

@ -1,4 +1,5 @@
export const jargon = {
cjs: '<b>CJS</b> stands for CommonJS, it is the JavaScript module format popularized by NodeJS, but now increasingly phased out in favor of <b>ESM</b>',
esm: '<b>ESM</b> stands for EcmaScript Module, it is the standardized module syntax in JavaScript',
variadic: 'A variadic function is a function that accepts a variable number of arguments',
}