1
0
Fork 0

[plugin-path-utils] feat: Add path-utils plug-in (#236)

This plug-in helps with creating seam allowance and hem paths.

Rebased v4 version for #99, see the linked issue for screenshots/details.

Reviewed-on: https://codeberg.org/freesewing/freesewing/pulls/236
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:
Jonathan Haas 2025-04-13 08:58:45 +00:00 committed by Joost De Cock
parent 36da79afb6
commit 04a0b4b099
14 changed files with 1971 additions and 436 deletions

View file

@ -31,6 +31,19 @@
- Neck ties no longer shown to be cut on fold
- Band tie locks in duo colours when `options.crossBackTies` is true (fixes incorrect notch placements)
### plugin-annotations
#### Fixed
- Fixed incorrect anchor point of the logo due to share pathstring (#202)
### react
#### Fixed
- Fixed issues with the pattern export feature in the Editor (#218)
- Added Jane to the Linedrawing component (#211)
## 4.0.0 (2024-04-01)

1209
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
# Change log for: @freesewing/plugin-path-utils
## 3.0.0 (2023-09-30)
### Changed
- All FreeSewing packages are now ESM only.
- All FreeSewing packages now use named exports.
- Dropped support for NodeJS 14. NodeJS 18 (LTS/hydrogen) or more recent is now required.
### Removed
- This plugin no longer sets its version as an SVG attribute when rendering patterns
## 2.21.0 (2022-06-27)
### Changed
- Migrated from Rollup to Esbuild for all builds
## 2.19.6 (2021-12-29)
### Added
- Added (esm) unit tests
## 2.17.0 (2021-07-01)
### Changed
- Is now included in plugin-bundle
## 2.7.0 (2020-07-12)
### Added
- A FreeSewing plugin for path-utilsing points or paths
- Initial release
This is the **initial release**, and the start of this change log.
> Prior to version 2, FreeSewing was not a JavaScript project.
> As such, that history is out of scope for this change log.

View file

@ -0,0 +1,161 @@
<p align='center'><a
href="https://www.npmjs.com/package/@freesewing/plugin-path-utils"
title="@freesewing/plugin-path-utils on NPM"
><img src="https://img.shields.io/npm/v/@freesewing/plugin-path-utils.svg"
alt="@freesewing/plugin-path-utils on NPM"/>
</a><a
href="https://opensource.org/licenses/MIT"
title="License: MIT"
><img src="https://img.shields.io/npm/l/@freesewing/plugin-path-utils.svg?label=License"
alt="License: MIT"/>
</a><a
href="https://deepscan.io/dashboard#view=project&tid=2114&pid=2993&bid=23256"
title="Code quality on DeepScan"
><img src="https://deepscan.io/api/teams/2114/projects/2993/branches/23256/badge/grade.svg"
alt="Code quality on DeepScan"/>
</a><a
href="https://github.com/freesewing/freesewing/issues?q=is%3Aissue+is%3Aopen+label%3Apkg%3Aplugin-path-utils"
title="Open issues tagged pkg:plugin-path-utils"
><img src="https://img.shields.io/github/issues/freesewing/freesewing/pkg:plugin-path-utils.svg?label=Issues"
alt="Open issues tagged pkg:plugin-path-utils"/>
</a><a
href="#contributors-"
title="All Contributors"
><img src="https://img.shields.io/badge/all_contributors-129-pink.svg"
alt="All Contributors"/>
</a></p><p align='center'><a
href="https://twitter.com/freesewing_org"
title="Follow @freesewing_org on Twitter"
><img src="https://img.shields.io/badge/%F3%A0%80%A0-Follow%20us-blue.svg?logo=twitter&logoColor=white&logoWidth=15"
alt="Follow @freesewing_org on Twitter"/>
</a><a
href="https://chat.freesewing.org"
title="Chat with us on Discord"
><img src="https://img.shields.io/discord/698854858052075530?label=Chat%20on%20Discord"
alt="Chat with us on Discord"/>
</a><a
href="https://freesewing.org/patrons/join"
title="Become a FreeSewing Patron"
><img src="https://img.shields.io/badge/%F3%A0%80%A0-Support%20us-blueviolet.svg?logo=cash-app&logoColor=white&logoWidth=15"
alt="Become a FreeSewing Patron"/>
</a><a
href="https://instagram.com/freesewing_org"
title="Follow @freesewing_org on Twitter"
><img src="https://img.shields.io/badge/%F3%A0%80%A0-Follow%20us-E4405F.svg?logo=instagram&logoColor=white&logoWidth=15"
alt="Follow @freesewing_org on Twitter"/>
</a></p>
# @freesewing/plugin-path-utils
A FreeSewing plugin to create seam allowance paths
# FreeSewing
> [!TIP]
>#### Support FreeSewing: Become a patron, or make a one-time donation 🥰
>
> FreeSewing is an open source project maintained by Joost De Cock and financially supported by the FreeSewing patrons.
>
> If you feel FreeSewing is worthwhile, and you can spend a few coins without
hardship, then you should [join us and become a patron](https://freesewing.org/community/join).
## What am I looking at? 🤔
This repository is the FreeSewing *monorepo* holding all FreeSewing's websites, documentation, designs, plugins, and other NPM packages.
This folder holds: @freesewing/plugin-path-utils
If you're not entirely sure what to do or how to start, type this command:
```
npm run tips
```
> [!NOTE]
> If you don't want to set up a dev environment, you can run it in your browser:
>
> [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/freesewing/freesewing)
>
> We recommend that you fork our repository and then
> put `gitpod.io/#<entire-url-of-your-fork` into a browser
> to start up a browser-based dev environment of your own.
## About FreeSewing 💀
Where the world of makers and developers collide, that's where you'll find FreeSewing.
If you're a maker, checkout [freesewing.org](https://freesewing.org/) where you can generate
sewing patterns adapted to your measurements.
If you're a developer, the FreeSewing documentation lives at [freesewing.dev](https://freesewing.dev/).
The FreeSewing [core library](https://freesewing.dev/reference/api/) is a *batteries-included* toolbox
for parametric design of sewing patterns. But FreeSewing also provides a range
of [plugins](https://freesewing.dev/reference/plugins/) that further extend the
functionality of the platform.
If you have NodeJS installed, you can try it right now by running:
```bash
npx @freesewing/new-design
```
Getting started guides are available for:
- [Linux](https://freesewing.dev/tutorials/getting-started-linux/)
- [MacOS](https://freesewing.dev/tutorials/getting-started-mac/)
- [Windows](https://freesewing.dev/tutorials/getting-started-windows/)
The [pattern design tutorial](https://freesewing.dev/tutorials/pattern-design/) will
show you how to create your first parametric design.
## Getting started ⚡
To get started with FreeSewing, you can spin up our development environment with:
```bash
npx @freesewing/new-design
```
To work with FreeSewing's monorepo, you'll need [NodeJS v18](https://nodejs.org), [lerna](https://lerna.js.org/) and [yarn](https://yarnpkg.com/) on your system.
Once you have those, clone (or fork) this repo and run `yarn kickstart`:
```bash
git clone git@github.com:freesewing/freesewing.git
cd freesewing
yarn kickstart
```
## Links 👩‍💻
**Official channels**
- 💻 Makers website: [FreeSewing.org](https://freesewing.org)
- 💻 Developers website: [FreeSewing.dev](https://freesewing.dev)
- ✅ [Support](https://github.com/freesewing/freesewing/issues/new/choose),
[Issues](https://github.com/freesewing/freesewing/issues) &
[Discussions](https://github.com/freesewing/freesewing/discussions) on
[GitHub](https://github.com/freesewing/freesewing)
**Social media**
- 🐦 Twitter: [@freesewing_org](https://twitter.com/freesewing_org)
- 📷 Instagram: [@freesewing_org](https://instagram.com/freesewing_org)
**Places the FreeSewing community hangs out**
- 💬 [Discord](https://discord.freesewing.org/)
- 💬 [Facebook](https://www.facebook.com/groups/627769821272714/)
- 💬 [Reddit](https://www.reddit.com/r/freesewing/)
## License: MIT 🤓
© [Joost De Cock](https://github.com/joostdecock).
See [the license file](https://github.com/freesewing/freesewing/blob/develop/LICENSE) for details.
## Where to get help 🤯
For [Support](https://github.com/freesewing/freesewing/issues/new/choose),
please use the [Issues](https://github.com/freesewing/freesewing/issues) &
[Discussions](https://github.com/freesewing/freesewing/discussions) on
[GitHub](https://github.com/freesewing/freesewing).

View file

@ -0,0 +1,4 @@
{
"version": "4.0.0-rc.8",
"name": "plugin-path-utils"
}

View file

@ -0,0 +1,61 @@
{
"name": "@freesewing/plugin-path-utils",
"version": "4.0.0",
"description": "A FreeSewing plugin to modify paths",
"author": "Joost De Cock <joost@joost.at> (https://github.com/joostdecock)",
"homepage": "https://freesewing.org/",
"repository": "github:freesewing/freesewing",
"license": "MIT",
"bugs": {
"url": "https://github.com/freesewing/freesewing/issues"
},
"funding": {
"type": "individual",
"url": "https://freesewing.org/patrons/join"
},
"keywords": [
"freesewing",
"plugin",
"sewing pattern",
"sewing",
"design",
"parametric design",
"made to measure",
"diy",
"fashion"
],
"type": "module",
"module": "src/index.mjs",
"exports": {
".": "./src/index.mjs"
},
"scripts": {
"symlink": "mkdir -p ./node_modules/@freesewing && cd ./node_modules/@freesewing && ln -s -f ../../../* . && cd -",
"test": "npx mocha tests/*.test.mjs",
"tips": "node ../../scripts/help.mjs",
"lint": "npx eslint 'src/**' 'tests/*.mjs'",
"prettier": "npx prettier --write 'src/*.mjs' 'tests/*.mjs'",
"testci": "NODE_OPTIONS=\"--conditions=internal\" npx mocha tests/*.test.mjs --reporter ../../tests/reporters/terse.js"
},
"peerDependencies": {
"@freesewing/core": "4.0.0"
},
"dependencies": {},
"devDependencies": {
"mocha": "10.4.0",
"chai": "5.1.1"
},
"files": [
"src/",
"i18n/",
"about.json",
"README.md"
],
"publishConfig": {
"access": "public",
"tag": "latest"
},
"engines": {
"node": ">= 20"
}
}

View file

@ -0,0 +1,499 @@
import about from '../about.json' with { type: 'json' }
/**
* Fills in a corner between two paths with the given mode
*
* @param {Path} result
* @param {Path} prev
* @param {Path} next
* @param {string} mode currently supported are 'cut' and 'corner'
* @param {number} limit
* @param utils
*/
function insertJoin(result, prev, next, mode, limit, utils) {
if (prev.ops.length < 2) {
return
}
if (next.ops.length < 2) {
return
}
if (prev.start().sitsOn(prev.end())) {
// no start angle determinable
return
}
if (next.start().sitsOn(next.end())) {
// no end angle determinable
return
}
if (prev.end().sitsOn(next.start())) {
// No join necessary
return
}
if (mode === 'cut') {
return
}
const reversed = prev.reverse()
const prevPoint = reversed.ops[0].to
const nextPoint = next.ops[0].to
if (prevPoint.dist(nextPoint) < 1) {
// No join necessary
return
}
let prevCp = reversed.ops[1].cp1 ?? reversed.ops[1].to
let nextCp = next.ops[1].cp1 ?? next.ops[1].to
if (prevPoint.sitsOn(prevCp)) {
// We need to calculate a previous point if the control point already lies on the end of the line
prevCp = prevPoint.shift(prev.angleAt(prevPoint), -66)
}
if (nextPoint.sitsOn(nextCp)) {
// We need to calculate a previous point if the control point already lies on the end of the line
nextCp = nextPoint.shift(next.angleAt(nextPoint), 66)
}
const intersect = utils.beamsIntersect(prevCp, prevPoint, nextPoint, nextCp)
if (intersect) {
const d1 = intersect.dist(prevPoint)
const d2 = intersect.dist(nextPoint)
if (d1 > intersect.dist(prevCp)) {
// dont go backwards
return
}
if (d2 > intersect.dist(nextCp)) {
// dont go backwards
return
}
if (mode === 'corner') {
if (limit) {
const min = Math.min(d1, d2)
if (d1 > limit && d2 > limit) {
const p1 = intersect.shiftTowards(prevPoint, min - limit)
const p2 = intersect.shiftTowards(nextPoint, min - limit)
result.line(p1).line(p2)
return
}
}
result.line(intersect)
}
}
}
function loadPaths(configuredPaths, allPaths, log) {
let result = []
for (const configuredPath of configuredPaths) {
if (typeof configuredPath === 'string') {
const p = allPaths[configuredPath]
if (p && p.ops) {
// note: instanceof would fail due to different constructors
result.push(p)
} else {
log.error(`Path "${configuredPath}" is not a valid path`)
}
} else if (configuredPath && configuredPath.ops) {
result.push(configuredPath)
} else if (configuredPath !== null) {
log.error(`Path "${configuredPath}" is not a valid path`)
} else {
result.push(null)
}
}
return result
}
/**
* Joins paths together with angled corners.
*
* Node: elements in the `paths` array, that are hidden with [Path.hide()] will stay invisible in the output, but will
* still create corners
*
* @param conf {object}
* @property {object[]} paths
* @property {string} mode
* @property {number} limit
* @param {object} props
*
* @return {Path} the resulting Path object that consists of the joined paths
*/
const joinMacro = function (conf, props) {
let { log, Path } = props
const paths = loadPaths(conf.paths, props.paths, log)
let prevPathIndex = paths.length - 1
let result = new Path()
const mode = conf.mode ?? 'corner'
// remove intersecting sections of adjacent paths that e.g. occur when offsetting a concave corner
paths.forEach(function (path, index) {
if (path !== null) {
if (paths[prevPathIndex] !== null) {
// check intersection
const intersections = paths[prevPathIndex].intersects(path)
if (intersections.length > 0) {
const intersection = intersections.pop()
paths[prevPathIndex] = paths[prevPathIndex].split(intersection)[0] ?? paths[prevPathIndex]
paths[index] = path.split(intersection).pop() ?? path
}
}
}
prevPathIndex = index
})
paths.forEach(function (path, index) {
if (path !== null) {
const prevPath = paths[prevPathIndex]
if (prevPath !== null) {
if (path.hidden && prevPath.hidden) {
// nothing to do, both paths are hidden, so no join needed
prevPathIndex = index
return
}
if (result.ops.length === 0) {
// make sure the path starts with a move
result.move(paths[prevPathIndex].end())
}
insertJoin(result, paths[prevPathIndex], path, mode, conf.limit, props.utils)
if (!path.hidden) {
result.line(path.start())
result.ops.push(...path.ops.slice(1))
}
} else if (!path.hidden) {
result.ops.push(...path.ops)
}
}
prevPathIndex = index
})
return result
}
/**
* Calculates an offset path from a list of paths with given offset
*
* @param conf {object}
* @property {object[]} paths
* @property {string} mode
* @property {number} limit
* @param {object} props
*
* @return {Path} the resulting Path object that consists of the offset and joined paths
*/
const offsetMacro = function (conf, props) {
const segments = []
let prevPath = 'unset'
let firstPath = 'unset'
let { log, Path } = props
for (const entry of conf.paths) {
let paths = entry?.p ?? [null]
let offset = entry?.offset ?? 0
let hide = entry?.hidden ?? false
if (!Array.isArray(paths)) {
paths = [paths]
}
paths = loadPaths(paths, props.paths, log)
for (const path of paths) {
if (path == null) {
if (firstPath === 'unset') {
firstPath = null
}
if (prevPath.ops) {
// insert dummy node to make path go to the endpoint of the previous path (to return to the baseline)
segments.push(new Path().move(prevPath.end()).line(prevPath.end()))
segments.push(null)
prevPath = null
}
} else {
if (prevPath === null) {
// insert dummy node to make path go to the start point of the current path (before going sideways)
segments.push(new Path().move(path.start()).line(path.start()))
}
const division = path.divide()
for (const divisionElement of division) {
if (firstPath === 'unset') {
firstPath = divisionElement
}
if (
divisionElement.ops.length === 2 &&
divisionElement.ops[1].type === 'line' &&
divisionElement.ops[0].to.sitsOn(divisionElement.ops[1].to)
) {
continue
}
const offsetPath = divisionElement.offset(offset)
if (offsetPath.ops.length > 1) {
// skip degenerate paths
if (hide) offsetPath.hide()
segments.push(offsetPath)
prevPath = divisionElement
}
}
}
}
}
if (firstPath === null && prevPath !== null) {
// insert gap at end of path
segments.push(new Path().move(prevPath.start()).line(prevPath.start()))
}
if (firstPath !== null && prevPath === null) {
// insert gap at start of path
segments.unshift(new Path().move(firstPath.start()).line(firstPath.start()))
}
return joinMacro({ ...conf, paths: segments }, props)
}
/**
* Creates a seam allowance path from a lists of paths (optionally with assigned seam allowance override)
*
* @param conf {object}
* @property {object[]} paths
* @property {string|null} class
* @property {string} mode
* @property {number} limit
* @param {object} props
*
* @return {Path} the resulting seam allowance path
*/
const saMacro = function (conf, props) {
let { log } = props
let sa = conf.sa ?? props.sa
if (!sa) {
sa = 0
}
let offsetConf = { ...conf, paths: [] }
for (const entry of conf.paths) {
if (entry === null) {
offsetConf.paths.push(null)
} else if (entry.ops || typeof entry === 'string') {
offsetConf.paths.push({ p: entry, offset: sa })
} else if (entry.p && typeof entry.offset === 'number') {
offsetConf.paths.push(entry)
} else {
log.error(`Entry "${entry}" is not a valid seam allowance path specification`)
}
}
let result = offsetMacro(offsetConf, props)
result.setClass(conf.class ?? 'fabric')
result.addClass('sa')
return result
}
/**
* Creates a hem allowance path with trueing
*
* @param cssClass css class for resulting paths
* @param path1 path before the hem (e.g. the inseam)
* @param path2 path after the hem (e.g. the outseam)
* @param offset1 seam allowance (offset) used on `path1`
* @param offset2 seam allowance (offset) used on `path2`
* @param hemWidth width of hem
* @param lastFoldWidth width of last fold
* @param folds number of folds (you need two folds for an enclosed raw edge)
* @param prefix prefix for created paths
* @param props
*/
const hemMacro = function (
{
path1,
path2,
offset1 = null,
offset2 = null,
hemWidth,
lastFoldWidth = null,
folds = 2,
prefix: prefix = 'hemMacro',
cssClass = 'fabric',
},
props
) {
let { sa, utils, log, paths, Point, Path } = props
const lineValues = (start, end) => {
const { x: x1, y: y1 } = start
const { x: x2, y: y2 } = end
const [A, B] = [-(y2 - y1), x2 - x1]
const C = -(A * x1 + B * y1)
return [A, B, C]
}
const mirrorGen = (start, end) => {
const [A, B, C] = lineValues(start, end)
return (point) => {
const { x, y } = point
const uNom = (B * B - A * A) * x - 2 * A * B * y - 2 * A * C
const vNom = (A * A - B * B) * y - 2 * A * B * x - 2 * B * C
const denom = A * A + B * B
return new Point(uNom / denom, vNom / denom)
}
}
const mirrorPath = (mirrorPoint, path) => {
const p = path.clone()
for (const op of p.ops) {
switch (op.type) {
case 'move':
case 'line':
op.to = mirrorPoint(op.to)
break
case 'curve':
op.to = mirrorPoint(op.to)
op.cp1 = mirrorPoint(op.cp1)
op.cp2 = mirrorPoint(op.cp2)
break
default:
// Do nothing
}
}
return p.reverse()
}
if (!lastFoldWidth) {
lastFoldWidth = sa > 0 ? sa : 10
}
path1 = loadPaths([path1], paths, log).pop()
path2 = loadPaths([path2], paths, log).shift()
if (offset1 === null) {
offset1 = sa
}
if (offset2 === null) {
offset2 = sa
}
let hemStart = path1.end()
let hemEnd = path2.start()
let foldStart = hemStart
let foldEnd = hemEnd
let hemAngle = hemStart.angle(hemEnd)
// Beam that determines which part of the side seams to mirror
let innerBeamStart = hemStart.shift(hemAngle + 90, hemWidth)
let innerBeamEnd = hemEnd.shift(hemAngle + 90, hemWidth)
if (offset1) {
path1 = path1.offset(offset1)
}
if (offset2) {
path2 = path2.offset(offset2)
}
if (0 === path1.intersectsBeam(innerBeamStart, innerBeamEnd).length) {
// extend path1 to beam
let p1tmp = utils.beamsIntersect(
path1.end(),
path1.end().shift(hemAngle + 90, 10),
innerBeamStart,
innerBeamEnd
)
path1 = new Path().move(p1tmp).join(path1)
}
if (0 === path2.intersectsBeam(innerBeamStart, innerBeamEnd).length) {
// extend path2 to beam
let p2tmp = utils.beamsIntersect(
path2.start(),
path2.start().shift(hemAngle + 90, 10),
innerBeamStart,
innerBeamEnd
)
path2 = path2.clone().line(p2tmp)
}
function cutPathStart(path, beamStart, beamEnd) {
let path2Intersections = path.intersectsBeam(beamStart, beamEnd)
let path2Tmp =
path2Intersections.length === 0 ? path : path.split(path2Intersections.shift()).shift()
if (path2Tmp === null) {
path2Tmp = path
}
return path2Tmp
}
function cutPathEnd(path, beamStart, beamEnd) {
let path1Intersections = path.intersectsBeam(beamStart, beamEnd)
let path1Tmp =
path1Intersections.length === 0 ? path : path.split(path1Intersections.pop()).pop()
if (path1Tmp === null) {
path1Tmp = path
}
return path1Tmp
}
// Determine portion of side seam paths to mirror
let path1End = cutPathStart(cutPathEnd(path1, innerBeamStart, innerBeamEnd), hemStart, hemEnd)
let path2Start = cutPathEnd(cutPathStart(path2, innerBeamStart, innerBeamEnd), hemStart, hemEnd)
paths[prefix + 'Mirror1'] = path1End.clone().hide()
paths[prefix + 'Mirror2'] = path2Start.clone().hide()
let mirrorPoint = mirrorGen(foldStart, foldEnd)
let path1Mirrored = mirrorPath(mirrorPoint, path1End)
let path2Mirrored = mirrorPath(mirrorPoint, path2Start)
let startSidePaths = [path1Mirrored]
let endSidePaths = [path2Mirrored]
for (let i = 1; i < folds; i++) {
paths[prefix + 'Fold' + i] = new Path()
.move(path1Mirrored.end())
.line(path2Mirrored.start())
.setClass(cssClass)
.addClass('help')
// create additional folds around the new "hem" line
foldStart = foldStart.shift(hemAngle - 90, hemWidth)
foldEnd = foldEnd.shift(hemAngle - 90, hemWidth)
mirrorPoint = mirrorGen(foldStart, foldEnd)
path1Mirrored = mirrorPath(mirrorPoint, path1Mirrored)
path2Mirrored = mirrorPath(mirrorPoint, path2Mirrored)
startSidePaths.push(path1Mirrored)
endSidePaths.unshift(path2Mirrored)
}
foldStart = foldStart.shift(hemAngle - 90, lastFoldWidth)
foldEnd = foldEnd.shift(hemAngle - 90, lastFoldWidth)
startSidePaths[startSidePaths.length - 1] = cutPathStart(
startSidePaths[startSidePaths.length - 1],
foldStart,
foldEnd
)
endSidePaths[0] = cutPathEnd(endSidePaths[0], foldStart, foldEnd)
let joinPaths = [
...startSidePaths,
new Path().move(foldStart).line(foldEnd),
...endSidePaths,
null,
]
return joinMacro(
{
paths: joinPaths,
},
props
)
.setClass(cssClass)
.addClass('sa')
}
export const plugin = {
...about,
macros: {
join: joinMacro,
offset: offsetMacro,
sa: saMacro,
hem: hemMacro,
},
}
// More specifically named exports
export const pathUtilsPlugin = plugin
export const pluginPathUtils = plugin

View file

@ -0,0 +1 @@
describe('Path-Utils Plugin Tests', () => {})

View file

@ -0,0 +1,6 @@
// This file is auto-generated | Any changes you make will be overwritten.
import { plugin } from '../src/index.mjs'
import { sharedPluginTests } from '../../../tests/plugins/shared.mjs'
// Run shared tests
sharedPluginTests(plugin)

View file

@ -0,0 +1,131 @@
---
title: hem
---
The `hem` macro drafts a hem allowance with fold lines.
## Signature
```js
Path macro('hem', {
Path|string path1,
Path|string path2,
number offset1 = null,
number offset2 = null,
number hemAllowance,
number lastHemAllowance = null,
number folds = 2,
number prefix = 'hemMacro',
number cssClass = 'fabric'
})
```
## Example
<Example caption="An example of the hem macro">
```js
;({ Point, points, Path, paths, macro, part }) => {
paths.inseam = new Path().move(new Point(150, 0)).line(new Point(200, 200)).hide()
paths.outseam = new Path()
.move(new Point(300, 200))
.curve(new Point(350, 100), new Point(350, 50), new Point(350, 0))
.hide()
paths.fabric = paths.inseam.clone().join(paths.outseam)
paths.hem = macro('hem', {
class: 'fabric',
path1: 'inseam',
path2: 'outseam',
hemWidth: 30,
offset1: 10,
offset2: 10,
})
// show helper mirror paths
paths.hemMacroMirror1.unhide().addClass('various sa')
paths.hemMacroMirror2.unhide().addClass('various sa')
return part
}
```
</Example>
## Configuration
| Property | Default | Type | Description |
| --------------: | ---------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path1` | | `Path` or `string` | The path before the hem (when the part is seen anticlockwise), for example the inseam on a pants pattern when construction a leg hole. You can reference a Path object directly or give a name of an existing path in the `paths` array. |
| `path2` | | `Path` or `string` | The path after the hem, for example the outseam on a pants pattern when constructing a leg hole. |
| `offset1` | (seam allowance) | `number` | The seam allowance of `path1`, defaults to used seam allowance (`sa`), if not given. |
| `offset2` | (seam allowance) | `number` | The seam allowance of `path2`, defaults to used seam allowance (`sa`), if not given. |
| `hemWidth` | | `number` | The width of the hem in millimeters. |
| `lastFoldWidth` | (seam allowance) | `number` | The width of the last fold, defaults to used seam allowance (`sa`), if not given. |
| `folds` | 2 | `number` | The number of folds for the hem. `2` creates a normal double folded hem. `1` would create a single fold for hems that are overlocked or made from knit fabric. Values below 1 are invalid. |
| `prefix` | `'hemMacro'` | `number` | The name prefix used for paths created by this macro. |
| `cssClass` | `'fabric'` | `number` | The CSS class added to the fold lines and the hem outline. Should usually match the CSS class for the part outline |
## Detailed Description
This macro will create the following paths (assuming the default `'hemMacro'` prefix is used):
- `hemMacroMirror1`: The part of the offset `path1` that is used to construct the starting edge of the hem line (by repeatedly mirroring it)
- `hemMacroMirror2`: The part of the offset `path2` that is used to construct the closing edge of the hem line (by repeatedly mirroring it)
- `hemMacroFold1`, `hemMacroFold2`, ...: The dot-dashed lines that mark the fold lines (one less than the `fold` parameter)
The Paths `hemMacroMirror1` and `hemMacroMirror2` are hidden by default, but you could use them to e.g. mark where the folded part would end up.
The macro call will _return_ the outline path of the hem, so you need to do something like
```js
paths.hem = macro('hem', {
...
});
```
to get and use the actual hem path. You probably want to embed the hem path into the seam allowance path, the following example code shows how to do this using the `sa` macro.
:::note
The hem path already includes the seam allowance, so it doesn't need any additional offset.
:::
<Example caption="Embedding the hem macro in the seam allowance path">
```js
;({ Point, points, Path, paths, macro, part }) => {
paths.inseam = new Path().move(new Point(150, 0)).line(new Point(200, 200)).hide()
paths.outseam = new Path()
.move(new Point(300, 200))
.curve(new Point(350, 100), new Point(350, 50), new Point(350, 0))
.hide()
paths.fabric = paths.inseam.clone().join(paths.outseam)
paths.hem = macro('hem', {
class: 'fabric',
path1: 'inseam',
path2: 'outseam',
hemWidth: 30,
offset1: 10,
offset2: 10,
}).hide()
paths.sa = macro('sa', {
paths: ['inseam', { p: 'hem', offset: 0 }, 'outseam', null],
sa: 10,
}).setClass('fabric sa')
return part
}
```
</Example>
:::note
When copying the example code for your design, you can probably omit the `sa`, `offset1` and `offset2` parameters.
They're only needed here as there is no default seam allowance in this preview window.
:::

View file

@ -0,0 +1,76 @@
---
title: join
---
The `join` macro joins multiple paths together.
Unlike the core `path.join(...)` function, the `join` macro can extend line ends to meet in a sharp corner and will automatically
trim useless path ends when adjacent paths in the array are intersecting.
The join function accepts an array of `Path` objects
(either names in the `paths` array or direct references to `Path` objects).
This array can contain `null` values or hidden paths (created with `path.hide()`) to create gaps.
:::warning
Since hidden paths will create gaps in the resulting paths, make sure that all the paths you want to
include in the join are visible before calling the macro.
You can hide them afterwards again, if needed.
:::
By default, the `join` macro will join the paths in a circular fashion, joining the end
of the last path in the array to the start of the first path, creating a full outline.
If this is not desired, insert a `null` element between paths where you want the gap
(or at the end of the `paths` parameter).
Note that a `null` value will create a basic gap in the output,
if you instead include a hidden path, this method will still create sharp corners as if the path were present,
but the actual path will be skipped in the output.
## Signature
```js
Path macro('join', {
Array paths,
number limit,
string mode
})
```
## Example
<Example caption="An example of the join macro">
```js
;({ Point, points, Path, paths, macro, part }) => {
paths.a = new Path().move(new Point(10, 10)).line(new Point(10, 20))
paths.b = new Path().move(new Point(25, 30)).line(new Point(55, 30))
paths.c = new Path().move(new Point(40, 20)).line(new Point(40, 50))
paths.d = new Path().move(new Point(50, 60)).line(new Point(60, 60))
paths.e = new Path()
.move(new Point(70, 40))
.curve(new Point(55, 15), new Point(55, 15), new Point(30, 0))
paths.join = macro('join', {
paths: ['a', 'b', 'c', null, 'd', 'e'],
}).setClass('stroke-sm mark dotted')
return part
}
```
</Example>
## Configuration
| Property | Default | Type | Description |
| -------: | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paths` | | `array` | An array of pathnames, the names of Paths in the `paths` array to join (you can also reference `Path` objects directly, or insert `null` elements to create gaps) |
| `mode` | `'corner'` | `string` | Mode for joining paths. Either `'corner'` or `'cut'`. `'cut'` will join the paths directly without extending them (like `path.join(...)`). |
| `limit` | `null` | `number` | Allows limiting the length of corners in `'corner'` mode. Prevents overly long joins on very sharp angles. Ignored if `null` or `false`. |

View file

@ -0,0 +1,84 @@
---
title: offset
---
The `offset` macro will offset and join paths.
Unlike the core `path.join(...)` and `path.offset(...)` functions, the `offset` macro can extend line ends to meet in a sharp corner and will automatically
trim useless path ends when adjacent paths in the array are intersecting.
The offset macro accepts an array of `Path` objects with their offset, like `{p: 'somePath', offset: 30}` or `{p: ['path1', 'path2'], offset: sa * 3, hidden: true}`.
For the paths in the `p` attribute, you can reference either names in the `paths` array or insert direct references to `Path` objects.
The array can contain `null` values to create gaps (e.g., for cuts on the fold or for other sections that don't need seam allowance).
By default, the `offset` macro will offset the paths in a circular fashion, joining the end
of the last path in the array to the start of the first path, creating a full outline.
If this is not desired, insert `null` elements where you want to create gaps.
You can use `offset: 0` to include paths which don't need an additional offset.
You can use `hidden: true` to omit path segments from the output, but still build corner joins as if they were there.
This can be used for cut-on-fold lines, where no seam allowance is needed.
:::note
To create a seam allowance, prefer the `sa` macro instead of the `offset` macro. It does pretty much the same,
but the `sa` macro defaults to offsetting paths by the user defined seam allowance.
:::
## Signature
```js
Path macro('offset', {
Array paths,
number limit,
string mode,
string class
})
```
## Example
<Example caption="An example of the offset macro">
```js
;({ Point, points, Path, paths, macro, part }) => {
paths.outline = new Path()
.move(new Point(10, 30))
.line(new Point(30, 30))
.line(new Point(30, 50))
.line(new Point(50, 50))
.line(new Point(50, 70))
.line(new Point(70, 70))
.curve(new Point(55, 15), new Point(55, 15), new Point(30, 0))
paths.outline2 = new Path().move(new Point(30, 0)).line(new Point(10, 30))
paths.offset = macro('offset', {
paths: [
{ p: 'outline', offset: 10 },
{ p: 'outline2', offset: 30 },
],
}).setClass('stroke-sm mark dotted')
paths.halfCircle = new Path().move(new Point(100, 0)).circleSegment(37, new Point(200, 0)).hide()
paths.halfCircleClosed = paths.halfCircle.clone().line(new Point(200, 0)).unhide().close()
paths.halfCircleSa = macro('offset', {
paths: [{ p: 'halfCircle', offset: 10 }, null],
}).setClass('stroke-sm mark dotted')
return part
}
```
</Example>
## Configuration
| Property | Default | Type | Description |
| -------: | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paths` | | `array` | An array of paths with their offset and visibility. You can use path names in the `paths` array or reference `Path` objects directly, or insert `null` elements to create gaps. |
| `mode` | `'corner'` | `string` | Mode for joining paths. Either `'corner'` or `'cut'`. `'cut'` will join the paths directly (like `path.join(...)`) without extending the corners. |
| `limit` | `null` | `number` | Allows limiting the length of extended path corners in `'corner'` mode. Prevents overly long joins on very sharp angles. Ignored if `null` or `false`. |
| `class` | `'offset'` | `string` | CSS class that is automatically applied to the resulting path. |

View file

@ -0,0 +1,82 @@
---
title: sa
---
The `sa` macro will create seam allowance paths.
Unlike the core `path.join(...)` and `path.offset(...)` functions, the `sa` macro can extend line ends to meet in a sharp corner and will automatically
trim useless path ends when adjacent paths in the array are intersecting.
The sa macro accepts an array of `Path` objects
(either names in the `paths` array or direct references to `Path` objects).
This array can contain `null` values to create gaps (e.g. for cuts on the fold or for other sections that don't need seam allowance).
By default, the `sa` macro will sa the paths in a circular fashion, joining the end
of the last path in the array to the start of the first path, creating a full outline.
If this is not desired, insert `null` elements where you want to create gaps.
You can optionally override the offset and invisibility for individual paths.
To do so, insert an object literal like `{p: 'somePath', offset: 30}` or `{p: ['path1', 'path2'], offset: sa * 3, hidden: true}` into the `paths` array.
You can use `offset: 0` to include paths which have already build-in the seam allowance (e.g. the result of the `hem` macro).
You can use `hidden: true` to hide path segments from the output, but still build corner joins as if they were there.
This can be used for cut-on-fold lines, where no seam allowance is needed.
## Signature
```js
macro('sa', {
Array paths,
number limit,
string mode,
string class
})
```
## Example
<Example caption="An example of the sa macro">
```js
;({ Point, points, Path, paths, macro, part }) => {
paths.outline = new Path()
.move(new Point(10, 30))
.line(new Point(30, 30))
.line(new Point(30, 50))
.line(new Point(50, 50))
.line(new Point(50, 70))
.line(new Point(70, 70))
.curve(new Point(55, 15), new Point(55, 15), new Point(30, 0))
paths.outline2 = new Path().move(new Point(30, 0)).line(new Point(10, 30))
paths.sa = macro('sa', {
paths: ['outline', { p: 'outline2', offset: 30 }],
sa: 10,
}).setClass('stroke-sm mark dotted')
paths.halfCircle = new Path().move(new Point(100, 0)).circleSegment(37, new Point(200, 0)).hide()
paths.halfCircleClosed = paths.halfCircle.clone().line(new Point(200, 0)).unhide().close()
paths.halfCircleSa = macro('sa', {
paths: ['halfCircle', null],
sa: 10,
}).setClass('stroke-sm mark dotted')
return part
}
```
</Example>
## Configuration
| Property | Default | Type | Description |
| -------: | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paths` | | `array` | An array of paths. You can use path names in the `paths` array or reference `Path` objects directly, or insert `null` elements to create gaps. You can also override offset and visibility for individual paths in the same way as with the `offset` macro. |
| `mode` | `'corner'` | `string` | Mode for joining paths. Either `'corner'` or `'cut'`. `'cut'` will join the paths directly (like `path.join(...)`) without extending the corners. |
| `limit` | `null` | `number` | Allows limiting the length of extended path corners in `'corner'` mode. Prevents overly long joins on very sharp angles. Ignored if `null` or `false`. |
| `sa` | (seam allowance) | `number` | Allows you to override the seam allowance used for this macro. Defaults to the standard seam allowance (`sa` parameter in draft function). |
| `class` | `'sa'` | `string` | CSS class that is automatically applied to the resulting path. |

View file

@ -0,0 +1,34 @@
---
title: plugin-path-utils
---
Published as [@freesewing/plugin-path-utils][1], this plugin provides the [hem](/reference/macros/hem), [sa](/reference/macros/sa), [offset](/reference/macros/offset) and [sa](/reference/macros/join) macros,
whose main purpose is to make it easier to construct seam allowance paths.
## Installation
```sh
npm install @freesewing/plugin-path-utils
```
## Usage
Either [add it as a part plugins](/reference/api/part/config/plugins) in your
design, or [add it to a pattern instance with
Pattern.use()](/reference/api/pattern/use).
To import the plugin for use:
```js
import { pathUtilsPlugin } from '@freesewing/plugin-path-utils'
// or
import { pluginPathUtils } from '@freesewing/plugin-path-utils'
```
## Notes
This plugin is part of the [core-plugins bundle](/reference/plugins/core),
so there is no need to install or import it manually unless you wish to forego
loading of core plugins yet still want to load this plugin.
[1]: https://www.npmjs.com/package/@freesewing/plugin-path-utils