diff --git a/CHANGELOG.md b/CHANGELOG.md index af3db810a15..4f7953442e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ #### Changed - Rephrased flag message when expand is off to avoid confusion about included seam allowance. Fixes + - The skirt and curved waistband are now constructed with the ringsector macro ### shin @@ -124,6 +125,12 @@ - First release of the plugin providing the default packing implementation +### plugin-ringsector + +#### Added + + - First release of the plugin facilitating drafting a ring sector + ### core #### Added diff --git a/config/changelog.yaml b/config/changelog.yaml index 71fc4067c02..af9c4c82c67 100644 --- a/config/changelog.yaml +++ b/config/changelog.yaml @@ -4,6 +4,8 @@ Unreleased: - Allow plugins to provide their own packing implementation plugin-bin-pack: - First release of the plugin providing the default packing implementation + plugin-ringsector: + - First release of the plugin facilitating drafting a ring sector Changed: aaron: @@ -33,6 +35,7 @@ Unreleased: - Rephrased flag message when expand is off to avoid confusion about included seam allowance. Fixes #5057 sandy: - Rephrased flag message when expand is off to avoid confusion about included seam allowance. Fixes #5057 + - The skirt and curved waistband are now constructed with the ringsector macro shin: - Rephrased flag message when expand is off to avoid confusion about included seam allowance. Fixes #5057 sven: diff --git a/config/software/plugins.json b/config/software/plugins.json index a785f1c13c4..91eb1942069 100644 --- a/config/software/plugins.json +++ b/config/software/plugins.json @@ -8,6 +8,7 @@ "plugin-i18n": "A FreeSewing plugin for pattern translation", "plugin-measurements": "A FreeSewing plugin that adds additional measurements that can be calculated from existing ones", "plugin-mirror": "A FreeSewing plugin to mirror points or paths", + "plugin-ringsector": "A FreeSewing plugin to draft a ring sector", "plugin-round": "A FreeSewing plugin to round corners", "plugin-sprinkle": "A FreeSewing plugin to bulk-add snippets to your pattern", "plugin-svgattr": "A FreeSewing plugin to set SVG attributes", diff --git a/designs/sandy/src/curved-waistband.mjs b/designs/sandy/src/curved-waistband.mjs index 45ab5bee9e0..60c500163ff 100644 --- a/designs/sandy/src/curved-waistband.mjs +++ b/designs/sandy/src/curved-waistband.mjs @@ -1,5 +1,3 @@ -import { draftRingSector } from './shared.mjs' - export function draftCurvedWaistband({ utils, store, @@ -32,18 +30,35 @@ export function draftCurvedWaistband({ store.get('waistbandOverlap') / (rad + absoluteOptions.waistbandWidth) ) - // The curved waistband is shown with no rotation - const rot = 0 - // Call draftRingSector to draft the part - paths.seam = draftRingSector( - part, - rot, - an + anExtra, - rad, - rad + absoluteOptions.waistbandWidth - ).attr('class', 'fabric') + // Call the RingSector macro to draft the waistband + macro('ringsector', { + angle: an + anExtra, + insideRadius: rad, + outsideRadius: rad + absoluteOptions.waistbandWidth, + }) + const storeRoot = [ + 'parts', + part.name, + 'macros', + '@freesewing/plugin-ringsector', + 'ids', + 'ringsector', + ] + const pathId = store.get([...storeRoot, 'paths', 'path']) + paths.seam = paths[pathId].clone().addClass('fabric') + paths[pathId].hide() - if (sa) paths.sa = paths.seam.offset(sa * -1).attr('class', 'fabric sa') + /* + * Macros ensure they can be used more than once in a part, and will generate unique (and complex) + * point names. Since we're only calling the macro once here, we will simplify these names + */ + for (const [shortId, uid] of Object.entries(store.get([...storeRoot, 'points']))) { + points[shortId] = points[uid].copy() + // Some points are rotated, we need those too + if (points[uid + 'Rotated']) points[shortId + 'Rotated'] = points[uid + 'Rotated'].copy() + } + + if (sa) paths.sa = paths.seam.offset(sa * -1).addClass('fabric sa') /* * Annotations @@ -55,7 +70,7 @@ export function draftCurvedWaistband({ points.gridAnchor = points.in1.clone() // Title - points.title = points.in1Rotated.shiftFractionTowards(points.ex1Rotated, 0.5).shift(0, 25) + points.title = points.ex2Flipped.shiftFractionTowards(points.ex2, 0.5) macro('title', { at: points.title, nr: 2, @@ -64,42 +79,33 @@ export function draftCurvedWaistband({ }) // Grainline - points.grainlineFrom = utils.curveIntersectsY( - points.ex2FlippedRotated, - points.ex2CFlippedRotated, - points.ex1CFlippedRotated, - points.ex1Rotated, - points.title.y - ) - points.grainlineTo = points.grainlineFrom.flipX() macro('grainline', { - from: points.grainlineFrom, - to: points.grainlineTo, + from: points.ex2Flipped, + to: points.ex2, }) // Buttons / Notches if (store.get('waistbandOverlap') >= options.minimumOverlap) { - points.pivot = points.in2Rotated.shiftFractionTowards(points.ex2Rotated, 0.5) + points.pivot = points.in2.shiftFractionTowards(points.ex2, 0.5) points.button = points.pivot - .shiftTowards(points.ex2Rotated, store.get('waistbandOverlap') / 2) + .shiftTowards(points.ex2, store.get('waistbandOverlap') / 2) .rotate(-90, points.pivot) points.buttonhole = points.button.flipX() snippets.button = new Snippet('button', points.button) - snippets.buttonhole = new Snippet('buttonhole', points.buttonhole).attr( - 'data-rotate', - -1 * points.ex2FlippedRotated.angle(points.in2FlippedRotated) + snippets.buttonhole = new Snippet('buttonhole', points.buttonhole).rotate( + points.in2.angle(points.ex2) ) points.centerNotch = new Path() .move(points.ex1Rotated) - .curve(points.ex1CFlippedRotated, points.ex2CFlippedRotated, points.ex2FlippedRotated) + .curve(points.ex1cFlippedRotated, points.ex2cFlippedRotated, points.ex2FlippedRotated) .shiftAlong(store.get('waistbandOverlap') / 2) points.buttonNotch = new Path() .move(points.ex2Rotated) - .curve(points.ex2CRotated, points.ex1CRotated, points.ex1Rotated) + .curve(points.ex2cRotated, points.ex1cRotated, points.ex1Rotated) .shiftAlong(store.get('waistbandOverlap')) macro('sprinkle', { snippet: 'notch', - on: ['centerNotch', 'buttonNotch', 'ex2FlippedRotated'], + on: ['centerNotch', 'buttonNotch', 'ex2Flipped'], }) } @@ -107,25 +113,25 @@ export function draftCurvedWaistband({ macro('hd', { id: 'wTop', from: points.in2FlippedRotated, - to: points.in2Rotated, - y: points.in2Rotated.y - sa - 15, + to: points.in2, + y: points.in2.y - sa - 15, }) macro('hd', { from: points.ex2FlippedRotated, id: 'wFull', - to: points.ex2Rotated, - y: points.in2Rotated.y - sa - 30, + to: points.ex2, + y: points.in2.y - sa - 30, }) macro('vd', { id: 'hFull', - from: points.ex1Rotated, - to: points.in2Rotated, - x: points.in2Rotated.x + sa + 30, + from: points.ex1, + to: points.in2, + x: points.in2.x + sa + 40, }) macro('ld', { id: 'lWidth', - from: points.ex2Rotated, - to: points.in2Rotated, + from: points.ex2, + to: points.in2, d: -1 * sa - 15, }) diff --git a/designs/sandy/src/shared.mjs b/designs/sandy/src/shared.mjs deleted file mode 100644 index ee13eb7f5b2..00000000000 --- a/designs/sandy/src/shared.mjs +++ /dev/null @@ -1,105 +0,0 @@ -export const draftRingSector = (part, rot, an, radIn, radEx, rotate = false) => { - const { utils, Point, points, Path } = part.shorthand() - - const roundExtended = (radius, angle = 90) => { - const arg = utils.deg2rad(angle / 2) - - return (radius * 4 * (1 - Math.cos(arg))) / Math.sin(arg) / 3 - } - - /** - * Calculates the distance of the control point for the internal - * and external arcs using bezierCircleExtended - */ - const distIn = roundExtended(radIn, an / 2) - const distEx = roundExtended(radEx, an / 2) - // The centre of the circles - points.center = new Point(0, 0) - - /** - * This function is expected to draft ring sectors for - * angles up to 180%. Since roundExtended works - * best for angles until 90º, we generate the ring - * sector using the half angle and then duplicate it - */ - - /** - * The first point of the internal arc, situated at - * a radIn distance below the centre - */ - points.in1 = points.center.shift(-90, radIn) - - /** - * The control point for 'in1'. It's situated at a - * distance $distIn calculated with bezierCircleExtended - * and the line between it and 'in1' is perpendicular to - * the line between 'in1' and the centre, so it's - * shifted in the direction 0º - */ - points.in1C = points.in1.shift(0, distIn) - - /** - * The second point of the internal arc, situated at - * a $radIn distance of the centre in the direction - * $an/2 - 90º - */ - points.in2 = points.center.shift(an / 2 - 90, radIn) - - /** - * The control point for 'in2'. It's situated at a - * distance $distIn calculated with bezierCircleExtended - * and the line between it and 'in2' is perpendicular to - * the line between 'in2' and the centre, so it's - * shifted in the direction $an/2 + 180º - */ - points.in2C = points.in2.shift(an / 2 + 180, distIn) - - /** - * The points for the external arc are generated in the - * same way, using $radEx and $distEx instead - */ - points.ex1 = points.center.shift(-90, radEx) - points.ex1C = points.ex1.shift(0, distEx) - points.ex2 = points.center.shift(an / 2 - 90, radEx) - points.ex2C = points.ex2.shift(an / 2 + 180, distEx) - - // Flip all the points to generate the full ring sector - for (const id of ['in2', 'in2C', 'in1C', 'ex1C', 'ex2C', 'ex2']) - points[id + 'Flipped'] = points[id].flipX() - - // Rotate all the points an angle rot - for (const id of [ - 'in1', - 'in1C', - 'in2', - 'in2C', - 'ex1', - 'ex1C', - 'ex2', - 'ex2C', - 'in2Flipped', - 'in2CFlipped', - 'in1CFlipped', - 'ex1CFlipped', - 'ex2CFlipped', - 'ex2Flipped', - ]) - points[id + 'Rotated'] = points[id].rotate(rot, points.center) - - if (rotate) { - // Rotate all points so the line from in1Rotated to ex1Rotated is vertical - const deg = 270 - points.in2Flipped.angle(points.ex2Flipped) - for (const id in points) { - points[id] = points[id].rotate(deg, points.in2Flipped) - } - } - // Return the path of the full ring sector - return new Path() - .move(points.in2Flipped) - .curve(points.in2CFlipped, points.in1CFlipped, points.in1) - .curve(points.in1C, points.in2C, points.in2) - .line(points.ex2) - .curve(points.ex2C, points.ex1C, points.ex1) - .curve(points.ex1CFlipped, points.ex2CFlipped, points.ex2Flipped) - .close() -} diff --git a/designs/sandy/src/skirt.mjs b/designs/sandy/src/skirt.mjs index 52d3c95095b..5d12d36e3ec 100644 --- a/designs/sandy/src/skirt.mjs +++ b/designs/sandy/src/skirt.mjs @@ -1,6 +1,6 @@ -import { draftRingSector } from './shared.mjs' import { pctBasedOn } from '@freesewing/core' import { elastics } from '@freesewing/snapseries' +import { ringsectorPlugin } from '@freesewing/plugin-ringsector' function sandySkirt({ utils, @@ -85,29 +85,50 @@ function sandySkirt({ const radiusHem = radiusWaist + store.get('fullLength') * options.lengthBonus - absoluteOptions.waistbandWidth - /** - * The ring sector will be rotated an angle an/2 so we - * display the part with one edge of the skirt vertical - */ - const rot = an / 2 + // Call the RingSector macro to draft the part + macro('ringsector', { + angle: an, + insideRadius: radiusWaist, + outsideRadius: radiusHem, + rotate: true, + }) + const storeRoot = [ + 'parts', + part.name, + 'macros', + '@freesewing/plugin-ringsector', + 'ids', + 'ringsector', + ] + const pathId = store.get([...storeRoot, 'paths', 'path']) + paths.seam = paths[pathId].clone().addClass('fabric') + paths[pathId].hide() - // Call draftRingSector to draft the part - paths.seam = draftRingSector(part, rot, an, radiusWaist, radiusHem, true).attr('class', 'fabric') + /* + * Macros ensure they can be used more than once in a part, and will generate unique (and complex) + * point names. Since we're only calling the macro once here, we will simplify these names + */ + for (const [shortId, uid] of Object.entries(store.get([...storeRoot, 'points']))) { + points[shortId] = points[uid].copy() + // Some points are rotated, we need those too + if (points[uid + 'Rotated']) points[shortId + 'Rotated'] = points[uid + 'Rotated'].copy() + } // Anchor samples to the centre of the waist points.gridAnchor = points.in2Flipped.clone() if (sa) { paths.hemBase = new Path() - .move(points.ex1Rotated) - .curve(points.ex1CFlippedRotated, points.ex2CFlippedRotated, points.ex2FlippedRotated) - .curve(points.ex1CFlipped, points.ex2CFlipped, points.ex2Flipped) + .move(points.ex2) + .curve(points.ex2c, points.ex1c, points.ex1) + .curve(points.ex1cFlipped, points.ex2cFlipped, points.ex2Flipped) .offset(store.get('fullLength') * options.lengthBonus * options.hemWidth * -1) paths.saBase = new Path() .move(points.in2Flipped) - .curve(points.in2CFlipped, points.in1CFlipped, points.in2FlippedRotated) - .curve(points.in2CFlippedRotated, points.in1CFlippedRotated, points.in1Rotated) - if (!options.seamlessFullCircle) paths.saBase = paths.saBase.line(points.ex1Rotated) + .curve(points.in2cFlipped, points.in1cFlipped, points.in1) + .curve(points.in1c, points.in2c, points.in2) + + if (!options.seamlessFullCircle) paths.saBase = paths.saBase.line(points.ex2) paths.saBase = paths.saBase.offset(sa * -1) paths.hemBase.hide() @@ -118,8 +139,8 @@ function sandySkirt({ .move(points.in2Flipped) .line(paths.saBase.start()) .join(paths.saBase) - .line(points.in1Rotated) - .move(points.ex1Rotated) + .line(points.in2) + .move(points.ex2) .line(paths.hemBase.start()) .join(paths.hemBase) .line(points.ex2Flipped) @@ -150,9 +171,10 @@ function sandySkirt({ }) if (options.seamlessFullCircle) { macro('cutonfold', { - from: points.ex1Rotated, - to: points.in1Rotated, + from: points.ex2, + to: points.in2, id: 'double', + reverse: true, }) } @@ -171,7 +193,7 @@ function sandySkirt({ // Notches macro('sprinkle', { snippet: 'notch', - on: ['in1Rotated', 'gridAnchor'], + on: ['in2', 'gridAnchor'], }) snippets.center = new Snippet('bnotch', points.center) @@ -244,5 +266,6 @@ export const skirt = { menu: 'fit', }, }, + plugins: ringsectorPlugin, draft: sandySkirt, } diff --git a/markdown/dev/reference/macros/ringsector/en.md b/markdown/dev/reference/macros/ringsector/en.md new file mode 100644 index 00000000000..a637abb3f46 --- /dev/null +++ b/markdown/dev/reference/macros/ringsector/en.md @@ -0,0 +1,92 @@ +--- +title: ringsector +--- + +The `ringsector` macro drafts a ring sector, which is like a part of a donut +with an inside and outside radius. It is particularly useful for drafting +curved waistbands, circle skirts, and so on. + +It is provided by the [ringsector plugin](/reference/plugins/ringsector). + + +##### Not a core-plugins macro + +The `ringsector` macro is not provided by the [core-plugins](/reference/plugins/core), +so you need to load the [ringsector plugin](/reference/plugins/ringsector) explicitly +if you want to use it. + + +## Signature + +```js +macro('ringsector', { + Point center = new Point(0,0), + Number angle, + Number insideRadius, + Number outsideRadius, + Boolean rotate = false, + String id='ringsector', +}) +``` + +## Example + + +```js +({ Point, macro, Path, paths, part }) => { + + macro('ringsector', { + angle: 60, + insideRadius: 30, + outsideRadius: 45, + }) + + return part +} +``` + + +## Configuration + +| Property | Default | Type | Description | +|---------------:|-------------------|------------|-------------| +| `center` | `new Point(0,0)` | [Point][1] | The center point of the ring sector | +| `angle` | | Number | The angle the ring sector should cover | +| `insideRadius` | | Number | The inside radius of the ring sector | +| `outsideRadius` | | Number | The outside radius of the ring sector | +| `rotate` | `false` | Boolean | Whether or not to rotate the ringsector so one of its sides is vertical (see [example below](#example-when-rotatetrue)) | +| `id` | `ringsector` | String | The id to use in auto-generate macro points and paths | + +[1]: /reference/api/point + +## Notes + +### Nodes generated by this macro + +This macro will add points and a single path to your part. +Their IDs will be saved in store under: +`parts.{part.name}.macros.@freesewing/plugin-ringsector.ids.{id}` + +### Removing a ring sector + +If you inherit a part with a ring sector drafted by this macro and you'd like to remove it, + +you can do so with [the rmringsector macro](/reference/macros/rmringsector). +### Example when rotate=true + + +```js +({ Point, macro, Path, paths, part }) => { + + macro('ringsector', { + angle: 60, + insideRadius: 30, + outsideRadius: 45, + rotate: true, + }) + + return part +} +``` + + diff --git a/markdown/dev/reference/macros/rmringsector/en.md b/markdown/dev/reference/macros/rmringsector/en.md new file mode 100644 index 00000000000..b4f428e6db2 --- /dev/null +++ b/markdown/dev/reference/macros/rmringsector/en.md @@ -0,0 +1,56 @@ +--- +title: rmringsector +--- + +The `rmringsector` macro removes the nodes added by [the ringsector macro](/reference/macros/ringsector). +It is the recommended way to remove (the effects of) a `ringsector` macro. + +It is provided by the [ringsector plugin](/reference/plugins/ringsector). + + +##### Not a core-plugins macro + +The `rmringsector` macro is not provided by the [core-plugins](/reference/plugins/core), +so you need to load the [ringsector plugin](/reference/plugins/ringsector) explicitly +if you want to use it. + + +## Signature + +```js +macro('rmringsector', String id = 'ringsector') +``` + +## Example + + +```js +({ Point, macro, Path, paths, part }) => { + + macro('ringsector', { + angle: 60, + insideRadius: 30, + outsideRadius: 45, + }) + macro('rmringsector') + + return part +} +``` + + +## Configuration + +| Property | Default | Type | Description | +|---------:|--------------|--------|-------------| +| `id` | `ringsector` | String | The id of the ringsector macro to remove | + +## Notes + +### Nodes removed by this macro + +This macro will remove points and a single path from your part. +Their IDs have been saved in store under: +`parts.{part.name}.macros.@freesewing/plugin-ringsector.ids.{id}` +by the [the ringsector macro](/reference/macros/ringsector). + diff --git a/markdown/dev/reference/plugins/ringsector/en.md b/markdown/dev/reference/plugins/ringsector/en.md new file mode 100644 index 00000000000..e6cbeaaa907 --- /dev/null +++ b/markdown/dev/reference/plugins/ringsector/en.md @@ -0,0 +1,30 @@ +--- +title: plugin-ringsector +--- + +Published as [@freesewing/plugin-ringsector][1], this plugin facilitates +drafting a ring sector (like a part of a donut). +It is particularly usefor for drafting curved waistband, circle skirts, and so on. + +## Provides + +- [The ringsector macro](/reference/macros/ringsector) +- [The rmringsector macro](/reference/macros/rmringsector) + +## Installation + +```sh +npm install @freesewing/plugin-ringsector +``` + +## Usage + +You should [add it as a part plugin](/reference/api/part/config/plugins). +Refer to the documentation of [the provided macros](#provides) for details on how to use them. + +To import the plugin for use: +```js +import { plugin as ringsectorPlugin } from '@freesewing/plugin-ringsector' +``` + +[1]: https://www.npmjs.com/package/@freesewing/plugin-ringsector diff --git a/plugins/plugin-ringsector/CHANGELOG.md b/plugins/plugin-ringsector/CHANGELOG.md new file mode 100644 index 00000000000..a5e10ff0f7a --- /dev/null +++ b/plugins/plugin-ringsector/CHANGELOG.md @@ -0,0 +1,17 @@ +# Change log for: @freesewing/plugin-ringsector + + +## 3.0.0 (2022-09-30) + +### Changed + + - All FreeSewing pacakges are now ESM only. + - All FreeSewing pacakges now use named exports. + - Dropped support for NodeJS 14. NodeJS 18 (LTS/hydrogen) or more recent is now required. + + +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. + diff --git a/plugins/plugin-ringsector/README.md b/plugins/plugin-ringsector/README.md new file mode 100644 index 00000000000..fa0e078df04 --- /dev/null +++ b/plugins/plugin-ringsector/README.md @@ -0,0 +1,143 @@ +![FreeSewing](https://static.freesewing.org/banner.png) +

@freesewing/plugin-ringsector on NPM + License: MIT + Code quality on DeepScan + Open issues tagged pkg:plugin-ringsector + All Contributors +

Follow @freesewing_org on Twitter + Chat with us on Discord + Become a FreeSewing Patron + Follow @freesewing_org on Twitter +

+ +# @freesewing/plugin-ringsector + +A FreeSewing plugin to draft a ring sector + + + + +## 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-ringsector + +If you're not entirely sure what to do or how to start, type this command: + +``` +npm run tips +``` + +> 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/# 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 create-freesewing-pattern +``` + +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. + +## Support FreeSewing: Become a patron 🥰 + +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 coind without +hardship, then you should [join us and become a patron](https://freesewing.org/community/join). + +## 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). + diff --git a/plugins/plugin-ringsector/build.mjs b/plugins/plugin-ringsector/build.mjs new file mode 100644 index 00000000000..99ace216bc8 --- /dev/null +++ b/plugins/plugin-ringsector/build.mjs @@ -0,0 +1,35 @@ +/* This script will build the package with esbuild */ +import esbuild from 'esbuild' +import pkg from './package.json' assert { type: 'json' } + +// Create banner based on package info +const banner = `/** + * ${pkg.name} | v${pkg.version} + * ${pkg.description} + * (c) ${new Date().getFullYear()} ${pkg.author} + * @license ${pkg.license} + */` + +// Shared esbuild options +const options = { + banner: { js: banner }, + bundle: true, + entryPoints: ['src/index.mjs'], + format: 'esm', + outfile: 'dist/index.mjs', + external: ['@freesewing'], + metafile: process.env.VERBOSE ? true : false, + minify: process.env.NO_MINIFY ? false : true, + sourcemap: true, +} + +// Let esbuild generate the build +const build = async () => { + const result = await esbuild.build(options).catch(() => process.exit(1)) + + if (process.env.VERBOSE) { + const info = await esbuild.analyzeMetafile(result.metafile) + console.log(info) + } +} +build() diff --git a/plugins/plugin-ringsector/data.mjs b/plugins/plugin-ringsector/data.mjs new file mode 100644 index 00000000000..f376317ed3d --- /dev/null +++ b/plugins/plugin-ringsector/data.mjs @@ -0,0 +1,4 @@ +// This file is auto-generated | All changes you make will be overwritten. +export const name = '@freesewing/plugin-ringsector' +export const version = '3.0.0' +export const data = { name, version } diff --git a/plugins/plugin-ringsector/package.json b/plugins/plugin-ringsector/package.json new file mode 100644 index 00000000000..3b08465f7ed --- /dev/null +++ b/plugins/plugin-ringsector/package.json @@ -0,0 +1,71 @@ +{ + "name": "@freesewing/plugin-ringsector", + "version": "3.0.0", + "description": "A FreeSewing plugin to draft a ring sector", + "author": "Joost De Cock (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": "dist/index.mjs", + "exports": { + ".": { + "internal": "./src/index.mjs", + "default": "./dist/index.mjs" + } + }, + "scripts": { + "build": "node build.mjs", + "build:all": "yarn build", + "clean": "rimraf dist", + "mbuild": "NO_MINIFY=1 node build.mjs", + "symlink": "mkdir -p ./node_modules/@freesewing && cd ./node_modules/@freesewing && ln -s -f ../../../* . && cd -", + "test": "npx mocha tests/*.test.mjs", + "vbuild": "VERBOSE=1 node build.mjs", + "lab": "cd ../../sites/lab && yarn start", + "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", + "wbuild": "node build.mjs", + "wbuild:all": "yarn wbuild" + }, + "peerDependencies": { + "@freesewing/core": "3.0.0" + }, + "dependencies": {}, + "devDependencies": { + "mocha": "10.2.0", + "chai": "4.3.10" + }, + "files": [ + "dist/*", + "README.md" + ], + "publishConfig": { + "access": "public", + "tag": "latest" + }, + "engines": { + "node": "18", + "npm": "9" + } +} diff --git a/plugins/plugin-ringsector/src/index.mjs b/plugins/plugin-ringsector/src/index.mjs new file mode 100644 index 00000000000..03e13214a20 --- /dev/null +++ b/plugins/plugin-ringsector/src/index.mjs @@ -0,0 +1,171 @@ +import { name, version } from '../data.mjs' + +/* + * Helper method to get the various IDs for a macro + */ +export const getIds = (keys, id) => { + const ids = {} + for (const key of keys) ids[key] = `__macro_ringsector_${id}_${key}` + + return ids +} + +/* + * Helper method to calculate the arc + */ +const roundExtended = (radius, angle = 90, utils) => { + const arg = utils.deg2rad(angle / 2) + + return (radius * 4 * (1 - Math.cos(arg))) / Math.sin(arg) / 3 +} + +/* + * Short IDs + */ +const keys = [ + 'center', + 'in1', + 'in1c', + 'in2', + 'in2c', + 'ex1', + 'ex1c', + 'ex2', + 'ex2c', + 'in2Flipped', + 'in2cFlipped', + 'in1cFlipped', + 'ex1cFlipped', + 'ex2cFlipped', + 'ex2Flipped', +] + +/* + * The plugin object itself + */ +export const plugin = { + name, + version, + macros: { + rmringsector: function (id = 'ringsector', { points, paths, store, part }) { + const storeRoot = ['parts', part.name, 'macros', '@freesewing/plugin-ringsector', 'ids', id] + for (const id of Object.values(store.get([...storeRoot, 'paths']))) delete paths[id] + for (const id of Object.values(store.get([...storeRoot, 'points']))) delete points[id] + }, + ringsector: function (mc, { utils, Point, points, Path, paths, store, part }) { + const { + angle, + insideRadius, + outsideRadius, + rotate = false, + center = new Point(0, 0), + id = 'ringsector', + } = mc + + /* + * Get the list of IDs + */ + const ids = getIds(keys, id) + const pathId = getIds(['path'], id).path + + /** + * Calculates the distance of the control point for the internal + * and external arcs using bezierCircleExtended + */ + const distIn = roundExtended(insideRadius, angle / 2, utils) + const distEx = roundExtended(outsideRadius, angle / 2, utils) + // The centre of the circles + points[ids.center] = center.copy() + + /** + * This function is expected to draft ring sectors for + * angles up to 180%. Since roundExtended works + * best for angles until 90º, we generate the ring + * sector using the half angle and then duplicate it + */ + + /** + * The first point of the internal arc, situated at + * a insideRadius distance below the centre + */ + points[ids.in1] = points[ids.center].shift(-90, insideRadius) + + /** + * The control point for 'in1'. It's situated at a + * distance $distIn calculated with bezierCircleExtended + * and the line between it and 'in1' is perpendicular to + * the line between 'in1' and the centre, so it's + * shifted in the direction 0º + */ + points[ids.in1c] = points[ids.in1].shift(0, distIn) + + /** + * The second point of the internal arc, situated at + * a $insideRadius distance of the centre in the direction + * $angle/2 - 90º + */ + points[ids.in2] = points[ids.center].shift(angle / 2 - 90, insideRadius) + + /** + * The control point for 'in2'. It's situated at a + * distance $distIn calculated with bezierCircleExtended + * and the line between it and 'in2' is perpendicular to + * the line between 'in2' and the centre, so it's + * shifted in the direction $angle/2 + 180º + */ + points[ids.in2c] = points[ids.in2].shift(angle / 2 + 180, distIn) + + /** + * The points for the external arc are generated in the + * same way, using $outsideRadius and $distEx instead + */ + points[ids.ex1] = points[ids.center].shift(-90, outsideRadius) + points[ids.ex1c] = points[ids.ex1].shift(0, distEx) + points[ids.ex2] = points[ids.center].shift(angle / 2 - 90, outsideRadius) + points[ids.ex2c] = points[ids.ex2].shift(angle / 2 + 180, distEx) + + // Flip all the points to generate the full ring sector + for (const id of ['in2', 'in2c', 'in1c', 'ex1c', 'ex2c', 'ex2']) { + points[ids[id + 'Flipped']] = points[ids[id]].flipX() + } + + // Rotate all the points angle/2 + for (const id of keys) { + points[ids[id] + 'Rotated'] = points[ids[id]].rotate(angle / 2, points[ids.center]) + // Also add this to the ids so we can save them later + ids[id + 'Rotated'] = ids[id] + 'Rotated' + } + + // (optionally) Rotate all points so the line from in1Rotated to ex1Rotated is vertical + if (rotate) { + const deg = 270 - points[ids.in2Flipped].angle(points[ids.ex2Flipped]) + for (const id of keys) { + points[ids[id]] = points[ids[id]].rotate(deg, points[ids.in2Flipped]) + // We need the rotated points too + points[ids[id] + 'Rotated'].rotate(deg, points[ids.in2Flipped]) + } + } + // Return the path of the full ring sector + paths[pathId] = new Path() + .move(points[ids.in2Flipped]) + .curve(points[ids.in2cFlipped], points[ids.in1cFlipped], points[ids.in1]) + .curve(points[ids.in1c], points[ids.in2c], points[ids.in2]) + .line(points[ids.ex2]) + .curve(points[ids.ex2c], points[ids.ex1c], points[ids.ex1]) + .curve(points[ids.ex1cFlipped], points[ids.ex2cFlipped], points[ids.ex2Flipped]) + .close() + + /* + * Store all IDs in the store so we can remove this macro with rmringsector + */ + store.set(['parts', part.name, 'macros', name, 'ids', id, 'paths'], { path: pathId }) + store.set(['parts', part.name, 'macros', name, 'ids', id, 'points'], ids) + }, + }, +} + +// More specifically named exports +export const ringsectorPlugin = plugin +export const ringSectorPlugin = plugin +export const pluginRingSector = plugin +export const pluginRingsector = plugin diff --git a/plugins/plugin-ringsector/tests/shared.test.mjs b/plugins/plugin-ringsector/tests/shared.test.mjs new file mode 100644 index 00000000000..7bf4f668398 --- /dev/null +++ b/plugins/plugin-ringsector/tests/shared.test.mjs @@ -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) diff --git a/sites/shared/components/mdx/tabbed-example.mjs b/sites/shared/components/mdx/tabbed-example.mjs index a7500845877..38d4db42482 100644 --- a/sites/shared/components/mdx/tabbed-example.mjs +++ b/sites/shared/components/mdx/tabbed-example.mjs @@ -2,6 +2,7 @@ import { Tab, Tabs } from '../tabs.mjs' import { Mdx } from 'shared/components/mdx/dynamic.mjs' import { pluginFlip } from '@freesewing/plugin-flip' import { pluginGore } from '@freesewing/plugin-gore' +import { pluginRingsector } from '@freesewing/plugin-ringsector' import { Design } from '@freesewing/core' import yaml from 'js-yaml' import { Pattern, PatternXray } from 'pkgs/react-components/src/index.mjs' @@ -48,7 +49,7 @@ const buildPattern = (children, settings = { margin: 5 }, tutorial = false, pape lengthRatio: { pct: 75, min: 55, max: 85, menu: 'style' }, } : {}, - plugins: [pluginFlip, pluginGore], + plugins: [pluginFlip, pluginGore, pluginRingsector], } const design = new Design({ parts: [part],