[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:
parent
36da79afb6
commit
04a0b4b099
14 changed files with 1971 additions and 436 deletions
46
plugins/plugin-path-utils/CHANGELOG.md
Normal file
46
plugins/plugin-path-utils/CHANGELOG.md
Normal 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.
|
||||
|
161
plugins/plugin-path-utils/README.md
Normal file
161
plugins/plugin-path-utils/README.md
Normal 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:
|
||||
>
|
||||
> [](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).
|
||||
|
4
plugins/plugin-path-utils/about.json
Normal file
4
plugins/plugin-path-utils/about.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": "4.0.0-rc.8",
|
||||
"name": "plugin-path-utils"
|
||||
}
|
61
plugins/plugin-path-utils/package.json
Normal file
61
plugins/plugin-path-utils/package.json
Normal 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"
|
||||
}
|
||||
}
|
499
plugins/plugin-path-utils/src/index.mjs
Normal file
499
plugins/plugin-path-utils/src/index.mjs
Normal 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
|
1
plugins/plugin-path-utils/tests/plugin.test.mjs
Normal file
1
plugins/plugin-path-utils/tests/plugin.test.mjs
Normal file
|
@ -0,0 +1 @@
|
|||
describe('Path-Utils Plugin Tests', () => {})
|
6
plugins/plugin-path-utils/tests/shared.test.mjs
Normal file
6
plugins/plugin-path-utils/tests/shared.test.mjs
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue