1
0
Fork 0

Merge branch 'develop' into eriese-v3-printing

This commit is contained in:
Enoch Riese 2022-11-16 10:05:37 -06:00
commit 271686b23b
47 changed files with 1539 additions and 3068 deletions

View file

@ -875,7 +875,8 @@
"profile": "https://github.com/BenJamesBen",
"contributions": [
"code",
"doc"
"doc",
"bug"
]
},
{

View file

@ -91,6 +91,7 @@ module.exports = {
],
env: {
mocha: true,
node: true,
},
},

View file

@ -1,8 +1,8 @@
---
name: 🐛 Bug report
description: Report a problem, or something that went wrong
title: "[bug]: "
labels: [ "\U0001F41B bug" ]
title: '[bug]: '
labels: ["\U0001F41B bug"]
body:
- type: markdown
attributes:
@ -10,7 +10,7 @@ body:
- type: textarea
id: desc
attributes:
label: "What seems to be the problem? 🤔"
label: 'What seems to be the problem? 🤔'
description: Please provide a clear and concise description of the problem you encountered
placeholder: |
When I generate a Simone with my dimensions, the corners of the yoke appear malformed. \
@ -18,7 +18,7 @@ body:
- type: dropdown
id: pkg
attributes:
label: "Design / Plugin / Package 🧐"
label: 'Design / Plugin / Package 🧐'
description: Do you know what design/plugin/package the bug is in?
multiple: true
options:
@ -51,6 +51,7 @@ body:
- designs/lucy
- designs/lunetius
- designs/noble
- designs/octoplushy
- designs/paco
- designs/penelope
- designs/plugintest
@ -62,7 +63,6 @@ body:
- designs/sven
- designs/tamiko
- designs/teagan
- designs/theo
- designs/tiberius
- designs/titan
- designs/trayvon
@ -78,9 +78,9 @@ body:
- plugins/plugin-bundle
- plugins/plugin-bust
- plugins/plugin-buttons
- plugins/plugin-cutlist
- plugins/plugin-cutonfold
- plugins/plugin-dimension
- plugins/plugin-export-dxf
- plugins/plugin-flip
- plugins/plugin-gore
- plugins/plugin-grainline
@ -94,33 +94,27 @@ body:
- plugins/plugin-sprinkle
- plugins/plugin-svgattr
- plugins/plugin-theme
- plugins/plugin-timing
- plugins/plugin-title
- plugins/plugin-validate
- plugins/plugin-versionfree-svg
- packages/components
- packages/config-helpers
- packages/core
- packages/css-theme
- packages/gatsby-remark-jargon
- packages/i18n
- packages/models
- packages/mui-theme
- packages/new-design
- packages/pattern-info
- packages/prettier-config
- packages/rehype-highlight-lines
- packages/rehype-jargon
- packages/remark-jargon
- packages/utils
- packages/snapseries
- type: dropdown
id: patron
attributes:
label: Are you a FreeSewing patron? 😃
description: "Patrons support us financially :pray: so they get priority"
description: 'Patrons support us financially :pray: so they get priority'
options:
- "Yes, I am a tier-2 patron ❤️"
- "Yes, I am a tier-4 patron ❤️ 💙"
- "Yes, I am a tier-8 patron ❤️ 💙 💜"
- "No, I am not 😞"
- 'Yes, I am a tier-2 patron ❤️'
- 'Yes, I am a tier-4 patron ❤️ 💙'
- 'Yes, I am a tier-8 patron ❤️ 💙 💜'
- 'No, I am not 😞'
validations:
required: true
- type: textarea
@ -133,3 +127,4 @@ body:
value: |
Please keep in mind that **FreeSewing is a community project** that depends on **[your support](https://freesewing.org/community/join/)**.
---

View file

@ -1,29 +1,22 @@
##### Labeler #####
labelPRBasedOnFilePath:
# "label": [ folder or subfolders ]
":package: components": [ packages/components/* ]
":package: config-helpers": [ packages/config-helpers/* ]
":package: core": [ packages/core/* ]
":package: create-freesewing-pattern": [ packages/create-freesewing-pattern/* ]
":package: css-theme": [ packages/css-theme/* ]
":package: gatsby-remark-jargon": [ packages/gatsby-remark-jargon/* ]
":package: i18n": [ packages/i18n/* ]
":package: models": [ packages/models/* ]
":package: mui-theme": [ packages/mui-theme/* ]
":package: new-design": [ packages/new-design/* ]
":package: pattern-info": [ packages/pattern-info/* ]
":package: prettier-config": [ packages/prettier-config/* ]
":package: rehype-highlight-lines": [ packages/rehype-highlight-lines/* ]
":package: rehype-jargon": [ packages/rehype-jargon/* ]
":package: remark-jargon": [ packages/remark-jargon/* ]
":package: utils": [ packages/utils/* ]
":package: snapseries": [ packages/snapseries/* ]
":electric_plug: plugin-banner": [ plugins/plugin-banner/* ]
":electric_plug: plugin-bartack": [ plugins/plugin-bartack/* ]
":electric_plug: plugin-bundle": [ plugins/plugin-bundle/* ]
":electric_plug: plugin-bust": [ plugins/plugin-bust/* ]
":electric_plug: plugin-buttons": [ plugins/plugin-buttons/* ]
":electric_plug: plugin-cutlist": [ plugins/plugin-cutlist/* ]
":electric_plug: plugin-cutonfold": [ plugins/plugin-cutonfold/* ]
":electric_plug: plugin-dimension": [ plugins/plugin-dimension/* ]
":electric_plug: plugin-export-dxf": [ plugins/plugin-export-dxf/* ]
":electric_plug: plugin-flip": [ plugins/plugin-flip/* ]
":electric_plug: plugin-gore": [ plugins/plugin-gore/* ]
":electric_plug: plugin-grainline": [ plugins/plugin-grainline/* ]
@ -37,18 +30,18 @@ labelPRBasedOnFilePath:
":electric_plug: plugin-sprinkle": [ plugins/plugin-sprinkle/* ]
":electric_plug: plugin-svgattr": [ plugins/plugin-svgattr/* ]
":electric_plug: plugin-theme": [ plugins/plugin-theme/* ]
":electric_plug: plugin-timing": [ plugins/plugin-timing/* ]
":electric_plug: plugin-title": [ plugins/plugin-title/* ]
":electric_plug: plugin-validate": [ plugins/plugin-validate/* ]
":electric_plug: plugin-versionfree-svg": [ plugins/plugin-versionfree-svg/* ]
":book: documentation": [ markdown/* ]
":scroll: scripts": [ scripts/* ]
":computer: backend": [ sites/backend/* ]
":computer: dev": [ sites/dev/* ]
":computer: email": [ sites/email/* ]
":computer: lab": [ sites/lab/* ]
":computer: org": [ sites/org/* ]
":computer: sanity": [ sites/sanity/* ]
":computer: shared": [ sites/shared/* ]
":computer: strapi": [ sites/strapi/* ]
":computer: svgtopdf": [ sites/svgtopdf/* ]
":test_tube: tests": [ tests/* ]
":gear: configuration": [ "config/*", ".github/*" ]
":shirt: aaron": [ designs/aaron/* ]
@ -93,7 +86,6 @@ labelPRBasedOnFilePath:
":shirt: sven": [ designs/sven/* ]
":shirt: tamiko": [ designs/tamiko/* ]
":shirt: teagan": [ designs/teagan/* ]
":shirt: theo": [ designs/theo/* ]
":shirt: tiberius": [ designs/tiberius/* ]
":shirt: titan": [ designs/titan/* ]
":shirt: trayvon": [ designs/trayvon/* ]

View file

@ -23,9 +23,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install Remark

View file

@ -48,11 +48,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -77,4 +77,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View file

@ -19,9 +19,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies

View file

@ -19,15 +19,15 @@ jobs:
steps:
- name: Fetch PR base ref
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: ${{ github.base_ref }}
- name: Checkout PR merge ref
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies

View file

@ -27,11 +27,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- name: Install dependencies
run: npx lerna bootstrap
env:

View file

@ -9,136 +9,136 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://adamrtomkins.github.io/"><img src="https://avatars.githubusercontent.com/u/5709603?v=4?s=100" width="100px;" alt="Adam Tomkins"/><br /><sub><b>Adam Tomkins</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=AdamRTomkins" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://polymerisation-des-concepts.fr/"><img src="https://avatars.githubusercontent.com/u/365999?v=4?s=100" width="100px;" alt="Alexandre Ignjatovic"/><br /><sub><b>Alexandre Ignjatovic</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=bankair" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AlfaLyr"><img src="https://avatars.githubusercontent.com/u/39273729?v=4?s=100" width="100px;" alt="AlfaLyr"/><br /><sub><b>AlfaLyr</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=AlfaLyr" title="Code">💻</a> <a href="#plugin-AlfaLyr" title="Plugin/utility libraries">🔌</a> <a href="#design-AlfaLyr" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://thelettereph.com"><img src="https://avatars.githubusercontent.com/u/357684?v=4?s=100" width="100px;" alt="Andrew James"/><br /><sub><b>Andrew James</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=ephphatha" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/annekecaramin"><img src="https://avatars.githubusercontent.com/u/38046191?v=4?s=100" width="100px;" alt="Anneke"/><br /><sub><b>Anneke</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=annekecaramin" title="Documentation">📖</a> <a href="#translation-annekecaramin" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anniekao"><img src="https://avatars.githubusercontent.com/u/1550506?v=4?s=100" width="100px;" alt="Annie Kao"/><br /><sub><b>Annie Kao</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=anniekao" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Anternative"><img src="https://avatars.githubusercontent.com/u/81079850?v=4?s=100" width="100px;" alt="Anternative"/><br /><sub><b>Anternative</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Anternative" title="Documentation">📖</a></td>
<td align="center"><a href="http://adamrtomkins.github.io/"><img src="https://avatars.githubusercontent.com/u/5709603?v=4?s=100" width="100px;" alt="Adam Tomkins"/><br /><sub><b>Adam Tomkins</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=AdamRTomkins" title="Documentation">📖</a></td>
<td align="center"><a href="http://polymerisation-des-concepts.fr/"><img src="https://avatars.githubusercontent.com/u/365999?v=4?s=100" width="100px;" alt="Alexandre Ignjatovic"/><br /><sub><b>Alexandre Ignjatovic</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=bankair" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/AlfaLyr"><img src="https://avatars.githubusercontent.com/u/39273729?v=4?s=100" width="100px;" alt="AlfaLyr"/><br /><sub><b>AlfaLyr</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=AlfaLyr" title="Code">💻</a> <a href="#plugin-AlfaLyr" title="Plugin/utility libraries">🔌</a> <a href="#design-AlfaLyr" title="Design">🎨</a></td>
<td align="center"><a href="http://thelettereph.com"><img src="https://avatars.githubusercontent.com/u/357684?v=4?s=100" width="100px;" alt="Andrew James"/><br /><sub><b>Andrew James</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=ephphatha" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/annekecaramin"><img src="https://avatars.githubusercontent.com/u/38046191?v=4?s=100" width="100px;" alt="Anneke"/><br /><sub><b>Anneke</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=annekecaramin" title="Documentation">📖</a> <a href="#translation-annekecaramin" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/anniekao"><img src="https://avatars.githubusercontent.com/u/1550506?v=4?s=100" width="100px;" alt="Annie Kao"/><br /><sub><b>Annie Kao</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=anniekao" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Anternative"><img src="https://avatars.githubusercontent.com/u/81079850?v=4?s=100" width="100px;" alt="Anternative"/><br /><sub><b>Anternative</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Anternative" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Quiltmaster"><img src="https://avatars.githubusercontent.com/u/71795777?v=4?s=100" width="100px;" alt="Anthony"/><br /><sub><b>Anthony</b></sub></a><br /><a href="#question-Quiltmaster" title="Answering Questions">💬</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/arigrayzel"><img src="https://avatars.githubusercontent.com/u/33040950?v=4?s=100" width="100px;" alt="Ari Grayzel-student"/><br /><sub><b>Ari Grayzel-student</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=arigrayzel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bart-PXL"><img src="https://avatars.githubusercontent.com/u/45118788?v=4?s=100" width="100px;" alt="Bart"/><br /><sub><b>Bart</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Bart-PXL" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BenJamesBen"><img src="https://avatars.githubusercontent.com/u/109869956?v=4?s=100" width="100px;" alt="BenJamesBen"/><br /><sub><b>BenJamesBen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=BenJamesBen" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=BenJamesBen" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/camerondubas"><img src="https://avatars.githubusercontent.com/u/6216460?v=4?s=100" width="100px;" alt="Cameron Dubas"/><br /><sub><b>Cameron Dubas</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=camerondubas" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cabi"><img src="https://avatars.githubusercontent.com/u/2596253?v=4?s=100" width="100px;" alt="Carsten Biebricher"/><br /><sub><b>Carsten Biebricher</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=cabi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cathyzoller"><img src="https://avatars.githubusercontent.com/u/2120275?v=4?s=100" width="100px;" alt="Cathy Zoller"/><br /><sub><b>Cathy Zoller</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=cathyzoller" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Quiltmaster"><img src="https://avatars.githubusercontent.com/u/71795777?v=4?s=100" width="100px;" alt="Anthony"/><br /><sub><b>Anthony</b></sub></a><br /><a href="#question-Quiltmaster" title="Answering Questions">💬</a></td>
<td align="center"><a href="https://github.com/arigrayzel"><img src="https://avatars.githubusercontent.com/u/33040950?v=4?s=100" width="100px;" alt="Ari Grayzel-student"/><br /><sub><b>Ari Grayzel-student</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=arigrayzel" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Bart-PXL"><img src="https://avatars.githubusercontent.com/u/45118788?v=4?s=100" width="100px;" alt="Bart"/><br /><sub><b>Bart</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Bart-PXL" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/BenJamesBen"><img src="https://avatars.githubusercontent.com/u/109869956?v=4?s=100" width="100px;" alt="BenJamesBen"/><br /><sub><b>BenJamesBen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=BenJamesBen" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=BenJamesBen" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/issues?q=author%3ABenJamesBen" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/camerondubas"><img src="https://avatars.githubusercontent.com/u/6216460?v=4?s=100" width="100px;" alt="Cameron Dubas"/><br /><sub><b>Cameron Dubas</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=camerondubas" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/cabi"><img src="https://avatars.githubusercontent.com/u/2596253?v=4?s=100" width="100px;" alt="Carsten Biebricher"/><br /><sub><b>Carsten Biebricher</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=cabi" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/cathyzoller"><img src="https://avatars.githubusercontent.com/u/2120275?v=4?s=100" width="100px;" alt="Cathy Zoller"/><br /><sub><b>Cathy Zoller</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=cathyzoller" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Chantalbijoux"><img src="https://avatars.githubusercontent.com/u/39673694?v=4?s=100" width="100px;" alt="Chantal Lapointe"/><br /><sub><b>Chantal Lapointe</b></sub></a><br /><a href="#translation-Chantalbijoux" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dpiquet"><img src="https://avatars.githubusercontent.com/u/4688628?v=4?s=100" width="100px;" alt="Damien PIQUET"/><br /><sub><b>Damien PIQUET</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=dpiquet" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.darigovresearch.com/"><img src="https://avatars.githubusercontent.com/u/30328618?v=4?s=100" width="100px;" alt="Darigov Research"/><br /><sub><b>Darigov Research</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=darigovresearch" title="Documentation">📖</a> <a href="#ideas-darigovresearch" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ElenaFdR"><img src="https://avatars.githubusercontent.com/u/5113815?v=4?s=100" width="100px;" alt="Elena FdR"/><br /><sub><b>Elena FdR</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=ElenaFdR" title="Documentation">📖</a> <a href="#blog-ElenaFdR" title="Blogposts">📝</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://emmanuelnyachoke.com/"><img src="https://avatars.githubusercontent.com/u/1908926?v=4?s=100" width="100px;" alt="Emmanuel Nyachoke"/><br /><sub><b>Emmanuel Nyachoke</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=enyachoke" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=enyachoke" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://enochriese.com"><img src="https://avatars.githubusercontent.com/u/5298929?v=4?s=100" width="100px;" alt="Enoch Riese"/><br /><sub><b>Enoch Riese</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=eriese" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EvEkSwed"><img src="https://avatars.githubusercontent.com/u/39723451?v=4?s=100" width="100px;" alt="EvEkSwed"/><br /><sub><b>EvEkSwed</b></sub></a><br /><a href="#translation-EvEkSwed" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Chantalbijoux"><img src="https://avatars.githubusercontent.com/u/39673694?v=4?s=100" width="100px;" alt="Chantal Lapointe"/><br /><sub><b>Chantal Lapointe</b></sub></a><br /><a href="#translation-Chantalbijoux" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/dpiquet"><img src="https://avatars.githubusercontent.com/u/4688628?v=4?s=100" width="100px;" alt="Damien PIQUET"/><br /><sub><b>Damien PIQUET</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=dpiquet" title="Code">💻</a></td>
<td align="center"><a href="https://www.darigovresearch.com/"><img src="https://avatars.githubusercontent.com/u/30328618?v=4?s=100" width="100px;" alt="Darigov Research"/><br /><sub><b>Darigov Research</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=darigovresearch" title="Documentation">📖</a> <a href="#ideas-darigovresearch" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/ElenaFdR"><img src="https://avatars.githubusercontent.com/u/5113815?v=4?s=100" width="100px;" alt="Elena FdR"/><br /><sub><b>Elena FdR</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=ElenaFdR" title="Documentation">📖</a> <a href="#blog-ElenaFdR" title="Blogposts">📝</a></td>
<td align="center"><a href="https://emmanuelnyachoke.com/"><img src="https://avatars.githubusercontent.com/u/1908926?v=4?s=100" width="100px;" alt="Emmanuel Nyachoke"/><br /><sub><b>Emmanuel Nyachoke</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=enyachoke" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=enyachoke" title="Documentation">📖</a></td>
<td align="center"><a href="http://enochriese.com"><img src="https://avatars.githubusercontent.com/u/5298929?v=4?s=100" width="100px;" alt="Enoch Riese"/><br /><sub><b>Enoch Riese</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=eriese" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/EvEkSwed"><img src="https://avatars.githubusercontent.com/u/39723451?v=4?s=100" width="100px;" alt="EvEkSwed"/><br /><sub><b>EvEkSwed</b></sub></a><br /><a href="#translation-EvEkSwed" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fantastik-Maman"><img src="https://avatars.githubusercontent.com/u/39785382?v=4?s=100" width="100px;" alt="Fantastik-Maman"/><br /><sub><b>Fantastik-Maman</b></sub></a><br /><a href="#translation-Fantastik-Maman" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.forresto.com/"><img src="https://avatars.githubusercontent.com/u/395307?v=4?s=100" width="100px;" alt="Forrest O."/><br /><sub><b>Forrest O.</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=forresto" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fmatray"><img src="https://avatars.githubusercontent.com/u/8267716?v=4?s=100" width="100px;" alt="Frédéric"/><br /><sub><b>Frédéric</b></sub></a><br /><a href="#translation-fmatray" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/glennfmatthews/"><img src="https://avatars.githubusercontent.com/u/5603551?v=4?s=100" width="100px;" alt="Glenn Matthews"/><br /><sub><b>Glenn Matthews</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=glennmatthews" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://greg.technology/"><img src="https://avatars.githubusercontent.com/u/1017304?v=4?s=100" width="100px;" alt="Greg Sadetsky"/><br /><sub><b>Greg Sadetsky</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=gregsadetsky" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://kirby.zone"><img src="https://avatars.githubusercontent.com/u/75245963?v=4?s=100" width="100px;" alt="Igor Couto"/><br /><sub><b>Igor Couto</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3Aiocouto" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=eltociear" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Fantastik-Maman"><img src="https://avatars.githubusercontent.com/u/39785382?v=4?s=100" width="100px;" alt="Fantastik-Maman"/><br /><sub><b>Fantastik-Maman</b></sub></a><br /><a href="#translation-Fantastik-Maman" title="Translation">🌍</a></td>
<td align="center"><a href="https://www.forresto.com/"><img src="https://avatars.githubusercontent.com/u/395307?v=4?s=100" width="100px;" alt="Forrest O."/><br /><sub><b>Forrest O.</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=forresto" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/fmatray"><img src="https://avatars.githubusercontent.com/u/8267716?v=4?s=100" width="100px;" alt="Frédéric"/><br /><sub><b>Frédéric</b></sub></a><br /><a href="#translation-fmatray" title="Translation">🌍</a></td>
<td align="center"><a href="https://www.linkedin.com/in/glennfmatthews/"><img src="https://avatars.githubusercontent.com/u/5603551?v=4?s=100" width="100px;" alt="Glenn Matthews"/><br /><sub><b>Glenn Matthews</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=glennmatthews" title="Documentation">📖</a></td>
<td align="center"><a href="https://greg.technology/"><img src="https://avatars.githubusercontent.com/u/1017304?v=4?s=100" width="100px;" alt="Greg Sadetsky"/><br /><sub><b>Greg Sadetsky</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=gregsadetsky" title="Documentation">📖</a></td>
<td align="center"><a href="https://kirby.zone"><img src="https://avatars.githubusercontent.com/u/75245963?v=4?s=100" width="100px;" alt="Igor Couto"/><br /><sub><b>Igor Couto</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3Aiocouto" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=eltociear" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Irapeke"><img src="https://avatars.githubusercontent.com/u/39604334?v=4?s=100" width="100px;" alt="Irapeke"/><br /><sub><b>Irapeke</b></sub></a><br /><a href="#translation-Irapeke" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jsawo"><img src="https://avatars.githubusercontent.com/u/1294706?v=4?s=100" width="100px;" alt="Jacek Sawoszczuk"/><br /><sub><b>Jacek Sawoszczuk</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jsawo" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jgfichte"><img src="https://avatars.githubusercontent.com/u/1787162?v=4?s=100" width="100px;" alt="Jason Williams"/><br /><sub><b>Jason Williams</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jgfichte" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jejacks0n"><img src="https://avatars.githubusercontent.com/u/13765?v=4?s=100" width="100px;" alt="Jeremy Jackson"/><br /><sub><b>Jeremy Jackson</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jejacks0n" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://jeroenhoek.nl"><img src="https://avatars.githubusercontent.com/u/683699?v=4?s=100" width="100px;" alt="Jeroen Hoek"/><br /><sub><b>Jeroen Hoek</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jdhoek" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joeschofield0"><img src="https://avatars.githubusercontent.com/u/47668691?v=4?s=100" width="100px;" alt="Joe Schofield"/><br /><sub><b>Joe Schofield</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=joeschofield0" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Joebidido"><img src="https://avatars.githubusercontent.com/u/39796210?v=4?s=100" width="100px;" alt="Joebidido"/><br /><sub><b>Joebidido</b></sub></a><br /><a href="#translation-Joebidido" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Irapeke"><img src="https://avatars.githubusercontent.com/u/39604334?v=4?s=100" width="100px;" alt="Irapeke"/><br /><sub><b>Irapeke</b></sub></a><br /><a href="#translation-Irapeke" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/jsawo"><img src="https://avatars.githubusercontent.com/u/1294706?v=4?s=100" width="100px;" alt="Jacek Sawoszczuk"/><br /><sub><b>Jacek Sawoszczuk</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jsawo" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jgfichte"><img src="https://avatars.githubusercontent.com/u/1787162?v=4?s=100" width="100px;" alt="Jason Williams"/><br /><sub><b>Jason Williams</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jgfichte" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jejacks0n"><img src="https://avatars.githubusercontent.com/u/13765?v=4?s=100" width="100px;" alt="Jeremy Jackson"/><br /><sub><b>Jeremy Jackson</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jejacks0n" title="Code">💻</a></td>
<td align="center"><a href="http://jeroenhoek.nl"><img src="https://avatars.githubusercontent.com/u/683699?v=4?s=100" width="100px;" alt="Jeroen Hoek"/><br /><sub><b>Jeroen Hoek</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jdhoek" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/joeschofield0"><img src="https://avatars.githubusercontent.com/u/47668691?v=4?s=100" width="100px;" alt="Joe Schofield"/><br /><sub><b>Joe Schofield</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=joeschofield0" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Joebidido"><img src="https://avatars.githubusercontent.com/u/39796210?v=4?s=100" width="100px;" alt="Joebidido"/><br /><sub><b>Joebidido</b></sub></a><br /><a href="#translation-Joebidido" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://joost.at/"><img src="https://avatars.githubusercontent.com/u/1708494?v=4?s=100" width="100px;" alt="Joost De Cock"/><br /><sub><b>Joost De Cock</b></sub></a><br /><a href="#maintenance-joostdecock" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joshessman"><img src="https://avatars.githubusercontent.com/u/9941074?v=4?s=100" width="100px;" alt="Josh Essman"/><br /><sub><b>Josh Essman</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=joshessman" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.earth.li/~kake/"><img src="https://avatars.githubusercontent.com/u/1956810?v=4?s=100" width="100px;" alt="Kake"/><br /><sub><b>Kake</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=KakeLP" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/kapunahele"><img src="https://avatars.githubusercontent.com/u/4116963?v=4?s=100" width="100px;" alt="Kapunahele Wong"/><br /><sub><b>Kapunahele Wong</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kapunahelewong" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tangerineshark"><img src="https://avatars.githubusercontent.com/u/70777269?v=4?s=100" width="100px;" alt="Karen"/><br /><sub><b>Karen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=tangerineshark" title="Documentation">📖</a> <a href="#eventOrganizing-tangerineshark" title="Event Organizing">📋</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mcgnly"><img src="https://avatars.githubusercontent.com/u/5653631?v=4?s=100" width="100px;" alt="Katie McGinley"/><br /><sub><b>Katie McGinley</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=mcgnly" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.kieranklaassen.com/"><img src="https://avatars.githubusercontent.com/u/209089?v=4?s=100" width="100px;" alt="Kieran Klaassen"/><br /><sub><b>Kieran Klaassen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kieranklaassen" title="Code">💻</a></td>
<td align="center"><a href="https://joost.at/"><img src="https://avatars.githubusercontent.com/u/1708494?v=4?s=100" width="100px;" alt="Joost De Cock"/><br /><sub><b>Joost De Cock</b></sub></a><br /><a href="#maintenance-joostdecock" title="Maintenance">🚧</a></td>
<td align="center"><a href="https://github.com/joshessman"><img src="https://avatars.githubusercontent.com/u/9941074?v=4?s=100" width="100px;" alt="Josh Essman"/><br /><sub><b>Josh Essman</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=joshessman" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.earth.li/~kake/"><img src="https://avatars.githubusercontent.com/u/1956810?v=4?s=100" width="100px;" alt="Kake"/><br /><sub><b>Kake</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=KakeLP" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/kapunahele"><img src="https://avatars.githubusercontent.com/u/4116963?v=4?s=100" width="100px;" alt="Kapunahele Wong"/><br /><sub><b>Kapunahele Wong</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kapunahelewong" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/tangerineshark"><img src="https://avatars.githubusercontent.com/u/70777269?v=4?s=100" width="100px;" alt="Karen"/><br /><sub><b>Karen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=tangerineshark" title="Documentation">📖</a> <a href="#eventOrganizing-tangerineshark" title="Event Organizing">📋</a></td>
<td align="center"><a href="https://github.com/mcgnly"><img src="https://avatars.githubusercontent.com/u/5653631?v=4?s=100" width="100px;" alt="Katie McGinley"/><br /><sub><b>Katie McGinley</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=mcgnly" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.kieranklaassen.com/"><img src="https://avatars.githubusercontent.com/u/209089?v=4?s=100" width="100px;" alt="Kieran Klaassen"/><br /><sub><b>Kieran Klaassen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kieranklaassen" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kittycatou"><img src="https://avatars.githubusercontent.com/u/48165583?v=4?s=100" width="100px;" alt="Kittycatou"/><br /><sub><b>Kittycatou</b></sub></a><br /><a href="#translation-Kittycatou" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.krishoward.org/"><img src="https://avatars.githubusercontent.com/u/5946286?v=4?s=100" width="100px;" alt="Kris"/><br /><sub><b>Kris</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=web-goddess" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kristinruben"><img src="https://avatars.githubusercontent.com/u/17237479?v=4?s=100" width="100px;" alt="Kristin Ruben"/><br /><sub><b>Kristin Ruben</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kristinruben" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loudepeuter"><img src="https://avatars.githubusercontent.com/u/38081954?v=4?s=100" width="100px;" alt="Loudepeuter"/><br /><sub><b>Loudepeuter</b></sub></a><br /><a href="#translation-Loudepeuter" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lucibytes"><img src="https://avatars.githubusercontent.com/u/77203781?v=4?s=100" width="100px;" alt="Lucian"/><br /><sub><b>Lucian</b></sub></a><br /><a href="#eventOrganizing-lucibytes" title="Event Organizing">📋</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/manufakturedelweiss"><img src="https://avatars.githubusercontent.com/u/38063391?v=4?s=100" width="100px;" alt="Marcus"/><br /><sub><b>Marcus</b></sub></a><br /><a href="#translation-manufakturedelweiss" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/martintribo"><img src="https://avatars.githubusercontent.com/u/1613442?v=4?s=100" width="100px;" alt="Martin Tribo"/><br /><sub><b>Martin Tribo</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=martintribo" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Kittycatou"><img src="https://avatars.githubusercontent.com/u/48165583?v=4?s=100" width="100px;" alt="Kittycatou"/><br /><sub><b>Kittycatou</b></sub></a><br /><a href="#translation-Kittycatou" title="Translation">🌍</a></td>
<td align="center"><a href="https://www.krishoward.org/"><img src="https://avatars.githubusercontent.com/u/5946286?v=4?s=100" width="100px;" alt="Kris"/><br /><sub><b>Kris</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=web-goddess" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/kristinruben"><img src="https://avatars.githubusercontent.com/u/17237479?v=4?s=100" width="100px;" alt="Kristin Ruben"/><br /><sub><b>Kristin Ruben</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=kristinruben" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Loudepeuter"><img src="https://avatars.githubusercontent.com/u/38081954?v=4?s=100" width="100px;" alt="Loudepeuter"/><br /><sub><b>Loudepeuter</b></sub></a><br /><a href="#translation-Loudepeuter" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/lucibytes"><img src="https://avatars.githubusercontent.com/u/77203781?v=4?s=100" width="100px;" alt="Lucian"/><br /><sub><b>Lucian</b></sub></a><br /><a href="#eventOrganizing-lucibytes" title="Event Organizing">📋</a></td>
<td align="center"><a href="https://github.com/manufakturedelweiss"><img src="https://avatars.githubusercontent.com/u/38063391?v=4?s=100" width="100px;" alt="Marcus"/><br /><sub><b>Marcus</b></sub></a><br /><a href="#translation-manufakturedelweiss" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/martintribo"><img src="https://avatars.githubusercontent.com/u/1613442?v=4?s=100" width="100px;" alt="Martin Tribo"/><br /><sub><b>Martin Tribo</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=martintribo" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nadege"><img src="https://avatars.githubusercontent.com/u/3792171?v=4?s=100" width="100px;" alt="Nadege Michel"/><br /><sub><b>Nadege Michel</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nadege" title="Tests">⚠️</a> <a href="https://github.com/freesewing/freesewing/commits?author=nadege" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nataliasayang"><img src="https://avatars.githubusercontent.com/u/48160791?v=4?s=100" width="100px;" alt="Natalia"/><br /><sub><b>Natalia</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nataliasayang" title="Code">💻</a> <a href="#design-nataliasayang" title="Design">🎨</a> <a href="#blog-nataliasayang" title="Blogposts">📝</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://yergler.net/"><img src="https://avatars.githubusercontent.com/u/510875?v=4?s=100" width="100px;" alt="Nathan Yergler"/><br /><sub><b>Nathan Yergler</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nyergler" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicholasdower"><img src="https://avatars.githubusercontent.com/u/9117775?v=4?s=100" width="100px;" alt="Nick Dower"/><br /><sub><b>Nick Dower</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nicholasdower" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=nicholasdower" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/issues?q=author%3Anicholasdower" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nchilada"><img src="https://avatars.githubusercontent.com/u/692925?v=4?s=100" width="100px;" alt="Nikhil Chelliah"/><br /><sub><b>Nikhil Chelliah</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nchilada" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/OysteinHoiby"><img src="https://avatars.githubusercontent.com/u/49735055?v=4?s=100" width="100px;" alt="OysteinHoiby"/><br /><sub><b>OysteinHoiby</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=OysteinHoiby" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://pat.forringer.com/"><img src="https://avatars.githubusercontent.com/u/136456?v=4?s=100" width="100px;" alt="Patrick Forringer"/><br /><sub><b>Patrick Forringer</b></sub></a><br /><a href="#plugin-destos" title="Plugin/utility libraries">🔌</a></td>
<td align="center"><a href="https://github.com/nadege"><img src="https://avatars.githubusercontent.com/u/3792171?v=4?s=100" width="100px;" alt="Nadege Michel"/><br /><sub><b>Nadege Michel</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nadege" title="Tests">⚠️</a> <a href="https://github.com/freesewing/freesewing/commits?author=nadege" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/nataliasayang"><img src="https://avatars.githubusercontent.com/u/48160791?v=4?s=100" width="100px;" alt="Natalia"/><br /><sub><b>Natalia</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nataliasayang" title="Code">💻</a> <a href="#design-nataliasayang" title="Design">🎨</a> <a href="#blog-nataliasayang" title="Blogposts">📝</a></td>
<td align="center"><a href="http://yergler.net/"><img src="https://avatars.githubusercontent.com/u/510875?v=4?s=100" width="100px;" alt="Nathan Yergler"/><br /><sub><b>Nathan Yergler</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nyergler" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/nicholasdower"><img src="https://avatars.githubusercontent.com/u/9117775?v=4?s=100" width="100px;" alt="Nick Dower"/><br /><sub><b>Nick Dower</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nicholasdower" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=nicholasdower" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/issues?q=author%3Anicholasdower" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/nchilada"><img src="https://avatars.githubusercontent.com/u/692925?v=4?s=100" width="100px;" alt="Nikhil Chelliah"/><br /><sub><b>Nikhil Chelliah</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=nchilada" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/OysteinHoiby"><img src="https://avatars.githubusercontent.com/u/49735055?v=4?s=100" width="100px;" alt="OysteinHoiby"/><br /><sub><b>OysteinHoiby</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=OysteinHoiby" title="Code">💻</a></td>
<td align="center"><a href="https://pat.forringer.com/"><img src="https://avatars.githubusercontent.com/u/136456?v=4?s=100" width="100px;" alt="Patrick Forringer"/><br /><sub><b>Patrick Forringer</b></sub></a><br /><a href="#plugin-destos" title="Plugin/utility libraries">🔌</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://pd75.github.io/"><img src="https://avatars.githubusercontent.com/u/10294795?v=4?s=100" width="100px;" alt="Paul"/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=PD75" title="Documentation">📖</a> <a href="#blog-PD75" title="Blogposts">📝</a> <a href="#translation-PD75" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/phillipthelen"><img src="https://avatars.githubusercontent.com/u/298062?v=4?s=100" width="100px;" alt="Phillip Thelen"/><br /><sub><b>Phillip Thelen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=phillipthelen" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pixieish"><img src="https://avatars.githubusercontent.com/u/32991415?v=4?s=100" width="100px;" alt="Pixieish"/><br /><sub><b>Pixieish</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Pixieish" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.uza.be/persoon/prof-dr-sorcha-ni-dhubhghaill"><img src="https://avatars.githubusercontent.com/u/30624634?v=4?s=100" width="100px;" alt="Prof. dr. Sorcha Ní Dhubhghaill"/><br /><sub><b>Prof. dr. Sorcha Ní Dhubhghaill</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=sorchanidhubhghaill" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QuentinFelix"><img src="https://avatars.githubusercontent.com/u/5288091?v=4?s=100" width="100px;" alt="Quentin FELIX"/><br /><sub><b>Quentin FELIX</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=QuentinFelix" title="Code">💻</a> <a href="#design-QuentinFelix" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RikHekker"><img src="https://avatars.githubusercontent.com/u/31843274?v=4?s=100" width="100px;" alt="Rik Hekker"/><br /><sub><b>Rik Hekker</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3ARikHekker" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://resume.livingston-gray.com/faq.html"><img src="https://avatars.githubusercontent.com/u/6462?v=4?s=100" width="100px;" alt="Sam Livingston-Gray"/><br /><sub><b>Sam Livingston-Gray</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=geeksam" title="Documentation">📖</a></td>
<td align="center"><a href="http://pd75.github.io/"><img src="https://avatars.githubusercontent.com/u/10294795?v=4?s=100" width="100px;" alt="Paul"/><br /><sub><b>Paul</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=PD75" title="Documentation">📖</a> <a href="#blog-PD75" title="Blogposts">📝</a> <a href="#translation-PD75" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/phillipthelen"><img src="https://avatars.githubusercontent.com/u/298062?v=4?s=100" width="100px;" alt="Phillip Thelen"/><br /><sub><b>Phillip Thelen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=phillipthelen" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Pixieish"><img src="https://avatars.githubusercontent.com/u/32991415?v=4?s=100" width="100px;" alt="Pixieish"/><br /><sub><b>Pixieish</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Pixieish" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.uza.be/persoon/prof-dr-sorcha-ni-dhubhghaill"><img src="https://avatars.githubusercontent.com/u/30624634?v=4?s=100" width="100px;" alt="Prof. dr. Sorcha Ní Dhubhghaill"/><br /><sub><b>Prof. dr. Sorcha Ní Dhubhghaill</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=sorchanidhubhghaill" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/QuentinFelix"><img src="https://avatars.githubusercontent.com/u/5288091?v=4?s=100" width="100px;" alt="Quentin FELIX"/><br /><sub><b>Quentin FELIX</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=QuentinFelix" title="Code">💻</a> <a href="#design-QuentinFelix" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/RikHekker"><img src="https://avatars.githubusercontent.com/u/31843274?v=4?s=100" width="100px;" alt="Rik Hekker"/><br /><sub><b>Rik Hekker</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3ARikHekker" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://resume.livingston-gray.com/faq.html"><img src="https://avatars.githubusercontent.com/u/6462?v=4?s=100" width="100px;" alt="Sam Livingston-Gray"/><br /><sub><b>Sam Livingston-Gray</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=geeksam" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sannek"><img src="https://avatars.githubusercontent.com/u/17491062?v=4?s=100" width="100px;" alt="Sanne"/><br /><sub><b>Sanne</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=sannek" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=sannek" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tyrannogina"><img src="https://avatars.githubusercontent.com/u/19556565?v=4?s=100" width="100px;" alt="Sara Latorre"/><br /><sub><b>Sara Latorre</b></sub></a><br /><a href="#translation-Tyrannogina" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SeaZeeZee"><img src="https://avatars.githubusercontent.com/u/86711383?v=4?s=100" width="100px;" alt="SeaZeeZee"/><br /><sub><b>SeaZeeZee</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=SeaZeeZee" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=SeaZeeZee" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SimonbJohnson"><img src="https://avatars.githubusercontent.com/u/2110742?v=4?s=100" width="100px;" alt="SimonbJohnson"/><br /><sub><b>SimonbJohnson</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3ASimonbJohnson" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SirCharlotte"><img src="https://avatars.githubusercontent.com/u/63847870?v=4?s=100" width="100px;" alt="SirCharlotte"/><br /><sub><b>SirCharlotte</b></sub></a><br /><a href="#translation-SirCharlotte" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.instagram.com/celine_mge/"><img src="https://avatars.githubusercontent.com/u/57619777?v=4?s=100" width="100px;" alt="Slylele"/><br /><sub><b>Slylele</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Slylele" title="Documentation">📖</a> <a href="#translation-Slylele" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Soazillon"><img src="https://avatars.githubusercontent.com/u/40845940?v=4?s=100" width="100px;" alt="Soazillon"/><br /><sub><b>Soazillon</b></sub></a><br /><a href="#translation-Soazillon" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/sannek"><img src="https://avatars.githubusercontent.com/u/17491062?v=4?s=100" width="100px;" alt="Sanne"/><br /><sub><b>Sanne</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=sannek" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=sannek" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Tyrannogina"><img src="https://avatars.githubusercontent.com/u/19556565?v=4?s=100" width="100px;" alt="Sara Latorre"/><br /><sub><b>Sara Latorre</b></sub></a><br /><a href="#translation-Tyrannogina" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/SeaZeeZee"><img src="https://avatars.githubusercontent.com/u/86711383?v=4?s=100" width="100px;" alt="SeaZeeZee"/><br /><sub><b>SeaZeeZee</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=SeaZeeZee" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=SeaZeeZee" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SimonbJohnson"><img src="https://avatars.githubusercontent.com/u/2110742?v=4?s=100" width="100px;" alt="SimonbJohnson"/><br /><sub><b>SimonbJohnson</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3ASimonbJohnson" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/SirCharlotte"><img src="https://avatars.githubusercontent.com/u/63847870?v=4?s=100" width="100px;" alt="SirCharlotte"/><br /><sub><b>SirCharlotte</b></sub></a><br /><a href="#translation-SirCharlotte" title="Translation">🌍</a></td>
<td align="center"><a href="https://www.instagram.com/celine_mge/"><img src="https://avatars.githubusercontent.com/u/57619777?v=4?s=100" width="100px;" alt="Slylele"/><br /><sub><b>Slylele</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=Slylele" title="Documentation">📖</a> <a href="#translation-Slylele" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Soazillon"><img src="https://avatars.githubusercontent.com/u/40845940?v=4?s=100" width="100px;" alt="Soazillon"/><br /><sub><b>Soazillon</b></sub></a><br /><a href="#translation-Soazillon" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SoneaTheBest"><img src="https://avatars.githubusercontent.com/u/64635425?v=4?s=100" width="100px;" alt="SoneaTheBest"/><br /><sub><b>SoneaTheBest</b></sub></a><br /><a href="#translation-SoneaTheBest" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://metafly.info/"><img src="https://avatars.githubusercontent.com/u/961256?v=4?s=100" width="100px;" alt="Stefan Sydow"/><br /><sub><b>Stefan Sydow</b></sub></a><br /><a href="#translation-stsydow" title="Translation">🌍</a> <a href="https://github.com/freesewing/freesewing/commits?author=stsydow" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=stsydow" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TriploidTree"><img src="https://avatars.githubusercontent.com/u/4170521?v=4?s=100" width="100px;" alt="Tríona"/><br /><sub><b>Tríona</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=TriploidTree" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/theUnmutual"><img src="https://avatars.githubusercontent.com/u/22374635?v=4?s=100" width="100px;" alt="Unmutual"/><br /><sub><b>Unmutual</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=theUnmutual" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woutervdub"><img src="https://avatars.githubusercontent.com/u/24414629?v=4?s=100" width="100px;" alt="Wouter van Wageningen"/><br /><sub><b>Wouter van Wageningen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=woutervdub" title="Code">💻</a> <a href="#design-woutervdub" title="Design">🎨</a> <a href="#tool-woutervdub" title="Tools">🔧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amysews"><img src="https://avatars.githubusercontent.com/u/25280778?v=4?s=100" width="100px;" alt="amysews"/><br /><sub><b>amysews</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=amysews" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anna-puk"><img src="https://avatars.githubusercontent.com/u/100537439?v=4?s=100" width="100px;" alt="anna-puk"/><br /><sub><b>anna-puk</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=anna-puk" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/SoneaTheBest"><img src="https://avatars.githubusercontent.com/u/64635425?v=4?s=100" width="100px;" alt="SoneaTheBest"/><br /><sub><b>SoneaTheBest</b></sub></a><br /><a href="#translation-SoneaTheBest" title="Translation">🌍</a></td>
<td align="center"><a href="http://metafly.info/"><img src="https://avatars.githubusercontent.com/u/961256?v=4?s=100" width="100px;" alt="Stefan Sydow"/><br /><sub><b>Stefan Sydow</b></sub></a><br /><a href="#translation-stsydow" title="Translation">🌍</a> <a href="https://github.com/freesewing/freesewing/commits?author=stsydow" title="Documentation">📖</a> <a href="https://github.com/freesewing/freesewing/commits?author=stsydow" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/TriploidTree"><img src="https://avatars.githubusercontent.com/u/4170521?v=4?s=100" width="100px;" alt="Tríona"/><br /><sub><b>Tríona</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=TriploidTree" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/theUnmutual"><img src="https://avatars.githubusercontent.com/u/22374635?v=4?s=100" width="100px;" alt="Unmutual"/><br /><sub><b>Unmutual</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=theUnmutual" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/woutervdub"><img src="https://avatars.githubusercontent.com/u/24414629?v=4?s=100" width="100px;" alt="Wouter van Wageningen"/><br /><sub><b>Wouter van Wageningen</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=woutervdub" title="Code">💻</a> <a href="#design-woutervdub" title="Design">🎨</a> <a href="#tool-woutervdub" title="Tools">🔧</a></td>
<td align="center"><a href="https://github.com/amysews"><img src="https://avatars.githubusercontent.com/u/25280778?v=4?s=100" width="100px;" alt="amysews"/><br /><sub><b>amysews</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=amysews" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/anna-puk"><img src="https://avatars.githubusercontent.com/u/100537439?v=4?s=100" width="100px;" alt="anna-puk"/><br /><sub><b>anna-puk</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=anna-puk" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/beautifulsummermoon"><img src="https://avatars.githubusercontent.com/u/40396388?v=4?s=100" width="100px;" alt="beautifulsummermoon"/><br /><sub><b>beautifulsummermoon</b></sub></a><br /><a href="#translation-beautifulsummermoon" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/berce"><img src="https://avatars.githubusercontent.com/u/10439709?v=4?s=100" width="100px;" alt="berce"/><br /><sub><b>berce</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=berce" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/biou"><img src="https://avatars.githubusercontent.com/u/1340376?v=4?s=100" width="100px;" alt="biou"/><br /><sub><b>biou</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=biou" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bobgeorgethe3rd"><img src="https://avatars.githubusercontent.com/u/16866285?v=4?s=100" width="100px;" alt="bobgeorgethe3rd"/><br /><sub><b>bobgeorgethe3rd</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=bobgeorgethe3rd" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=bobgeorgethe3rd" title="Documentation">📖</a> <a href="#design-bobgeorgethe3rd" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brmlyklr"><img src="https://avatars.githubusercontent.com/u/22308713?v=4?s=100" width="100px;" alt="brmlyklr"/><br /><sub><b>brmlyklr</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=brmlyklr" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.chrisbarrett.fr"><img src="https://avatars.githubusercontent.com/u/2373249?v=4?s=100" width="100px;" alt="chri5b"/><br /><sub><b>chri5b</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=chri5b" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=chri5b" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dingcycle"><img src="https://avatars.githubusercontent.com/u/1681985?v=4?s=100" width="100px;" alt="dingcycle"/><br /><sub><b>dingcycle</b></sub></a><br /><a href="#translation-dingcycle" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/beautifulsummermoon"><img src="https://avatars.githubusercontent.com/u/40396388?v=4?s=100" width="100px;" alt="beautifulsummermoon"/><br /><sub><b>beautifulsummermoon</b></sub></a><br /><a href="#translation-beautifulsummermoon" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/berce"><img src="https://avatars.githubusercontent.com/u/10439709?v=4?s=100" width="100px;" alt="berce"/><br /><sub><b>berce</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=berce" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/biou"><img src="https://avatars.githubusercontent.com/u/1340376?v=4?s=100" width="100px;" alt="biou"/><br /><sub><b>biou</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=biou" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bobgeorgethe3rd"><img src="https://avatars.githubusercontent.com/u/16866285?v=4?s=100" width="100px;" alt="bobgeorgethe3rd"/><br /><sub><b>bobgeorgethe3rd</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=bobgeorgethe3rd" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=bobgeorgethe3rd" title="Documentation">📖</a> <a href="#design-bobgeorgethe3rd" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/brmlyklr"><img src="https://avatars.githubusercontent.com/u/22308713?v=4?s=100" width="100px;" alt="brmlyklr"/><br /><sub><b>brmlyklr</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=brmlyklr" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.chrisbarrett.fr"><img src="https://avatars.githubusercontent.com/u/2373249?v=4?s=100" width="100px;" alt="chri5b"/><br /><sub><b>chri5b</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=chri5b" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=chri5b" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/dingcycle"><img src="https://avatars.githubusercontent.com/u/1681985?v=4?s=100" width="100px;" alt="dingcycle"/><br /><sub><b>dingcycle</b></sub></a><br /><a href="#translation-dingcycle" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drowned-in-books"><img src="https://avatars.githubusercontent.com/u/100040772?v=4?s=100" width="100px;" alt="drowned-in-books"/><br /><sub><b>drowned-in-books</b></sub></a><br /><a href="#question-drowned-in-books" title="Answering Questions">💬</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/econo202"><img src="https://avatars.githubusercontent.com/u/34138153?v=4?s=100" width="100px;" alt="econo202"/><br /><sub><b>econo202</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=econo202" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ericamattos"><img src="https://avatars.githubusercontent.com/u/4341417?v=4?s=100" width="100px;" alt="ericamattos"/><br /><sub><b>ericamattos</b></sub></a><br /><a href="#translation-ericamattos" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fightingrabbit"><img src="https://avatars.githubusercontent.com/u/25751445?v=4?s=100" width="100px;" alt="fightingrabbit"/><br /><sub><b>fightingrabbit</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=fightingrabbit" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DocSpencer77"><img src="https://avatars.githubusercontent.com/u/43393580?v=4?s=100" width="100px;" alt="gaylyndie"/><br /><sub><b>gaylyndie</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=DocSpencer77" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/grimlokason"><img src="https://avatars.githubusercontent.com/u/5112238?v=4?s=100" width="100px;" alt="grimlokason"/><br /><sub><b>grimlokason</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=grimlokason" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://weblog.redisdead.net"><img src="https://avatars.githubusercontent.com/u/6494414?v=4?s=100" width="100px;" alt="hellgy"/><br /><sub><b>hellgy</b></sub></a><br /><a href="#design-hellgy" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/drowned-in-books"><img src="https://avatars.githubusercontent.com/u/100040772?v=4?s=100" width="100px;" alt="drowned-in-books"/><br /><sub><b>drowned-in-books</b></sub></a><br /><a href="#question-drowned-in-books" title="Answering Questions">💬</a></td>
<td align="center"><a href="https://github.com/econo202"><img src="https://avatars.githubusercontent.com/u/34138153?v=4?s=100" width="100px;" alt="econo202"/><br /><sub><b>econo202</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=econo202" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/ericamattos"><img src="https://avatars.githubusercontent.com/u/4341417?v=4?s=100" width="100px;" alt="ericamattos"/><br /><sub><b>ericamattos</b></sub></a><br /><a href="#translation-ericamattos" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/fightingrabbit"><img src="https://avatars.githubusercontent.com/u/25751445?v=4?s=100" width="100px;" alt="fightingrabbit"/><br /><sub><b>fightingrabbit</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=fightingrabbit" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/DocSpencer77"><img src="https://avatars.githubusercontent.com/u/43393580?v=4?s=100" width="100px;" alt="gaylyndie"/><br /><sub><b>gaylyndie</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=DocSpencer77" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/grimlokason"><img src="https://avatars.githubusercontent.com/u/5112238?v=4?s=100" width="100px;" alt="grimlokason"/><br /><sub><b>grimlokason</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=grimlokason" title="Code">💻</a></td>
<td align="center"><a href="https://weblog.redisdead.net"><img src="https://avatars.githubusercontent.com/u/6494414?v=4?s=100" width="100px;" alt="hellgy"/><br /><sub><b>hellgy</b></sub></a><br /><a href="#design-hellgy" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jackseye"><img src="https://avatars.githubusercontent.com/u/27834526?v=4?s=100" width="100px;" alt="jackseye"/><br /><sub><b>jackseye</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jackseye" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marckiesel"><img src="https://avatars.githubusercontent.com/u/39653780?v=4?s=100" width="100px;" alt="marckiesel"/><br /><sub><b>marckiesel</b></sub></a><br /><a href="#translation-marckiesel" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mesil"><img src="https://avatars.githubusercontent.com/u/14284175?v=4?s=100" width="100px;" alt="mesil"/><br /><sub><b>mesil</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3Amesil" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starfetch"><img src="https://avatars.githubusercontent.com/u/80041179?v=4?s=100" width="100px;" alt="starfetch"/><br /><sub><b>starfetch</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=starfetch" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=starfetch" title="Documentation">📖</a> <a href="#translation-starfetch" title="Translation">🌍</a> <a href="#design-starfetch" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/timorl"><img src="https://avatars.githubusercontent.com/u/4363804?v=4?s=100" width="100px;" alt="timorl"/><br /><sub><b>timorl</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=timorl" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ttimearl"><img src="https://avatars.githubusercontent.com/u/77916590?v=4?s=100" width="100px;" alt="ttimearl"/><br /><sub><b>ttimearl</b></sub></a><br /><a href="#content-ttimearl" title="Content">🖋</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chrisgloom"><img src="https://avatars.githubusercontent.com/u/15905991?v=4?s=100" width="100px;" alt="tuesgloomsday"/><br /><sub><b>tuesgloomsday</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=chrisgloom" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jackseye"><img src="https://avatars.githubusercontent.com/u/27834526?v=4?s=100" width="100px;" alt="jackseye"/><br /><sub><b>jackseye</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=jackseye" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/marckiesel"><img src="https://avatars.githubusercontent.com/u/39653780?v=4?s=100" width="100px;" alt="marckiesel"/><br /><sub><b>marckiesel</b></sub></a><br /><a href="#translation-marckiesel" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Mesil"><img src="https://avatars.githubusercontent.com/u/14284175?v=4?s=100" width="100px;" alt="mesil"/><br /><sub><b>mesil</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/issues?q=author%3Amesil" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/starfetch"><img src="https://avatars.githubusercontent.com/u/80041179?v=4?s=100" width="100px;" alt="starfetch"/><br /><sub><b>starfetch</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=starfetch" title="Code">💻</a> <a href="https://github.com/freesewing/freesewing/commits?author=starfetch" title="Documentation">📖</a> <a href="#translation-starfetch" title="Translation">🌍</a> <a href="#design-starfetch" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/timorl"><img src="https://avatars.githubusercontent.com/u/4363804?v=4?s=100" width="100px;" alt="timorl"/><br /><sub><b>timorl</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=timorl" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ttimearl"><img src="https://avatars.githubusercontent.com/u/77916590?v=4?s=100" width="100px;" alt="ttimearl"/><br /><sub><b>ttimearl</b></sub></a><br /><a href="#content-ttimearl" title="Content">🖋</a></td>
<td align="center"><a href="https://github.com/chrisgloom"><img src="https://avatars.githubusercontent.com/u/15905991?v=4?s=100" width="100px;" alt="tuesgloomsday"/><br /><sub><b>tuesgloomsday</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=chrisgloom" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/valadaptive"><img src="https://avatars.githubusercontent.com/u/79560998?v=4?s=100" width="100px;" alt="valadaptive"/><br /><sub><b>valadaptive</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=valadaptive" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/viocky"><img src="https://avatars.githubusercontent.com/u/39279173?v=4?s=100" width="100px;" alt="viocky"/><br /><sub><b>viocky</b></sub></a><br /><a href="#translation-viocky" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woolishboy"><img src="https://avatars.githubusercontent.com/u/57816321?v=4?s=100" width="100px;" alt="woolishboy"/><br /><sub><b>woolishboy</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=woolishboy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cloutiy"><img src="https://avatars.githubusercontent.com/u/8433147?v=4?s=100" width="100px;" alt="yc"/><br /><sub><b>yc</b></sub></a><br /><a href="#translation-cloutiy" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/valadaptive"><img src="https://avatars.githubusercontent.com/u/79560998?v=4?s=100" width="100px;" alt="valadaptive"/><br /><sub><b>valadaptive</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=valadaptive" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/viocky"><img src="https://avatars.githubusercontent.com/u/39279173?v=4?s=100" width="100px;" alt="viocky"/><br /><sub><b>viocky</b></sub></a><br /><a href="#translation-viocky" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/woolishboy"><img src="https://avatars.githubusercontent.com/u/57816321?v=4?s=100" width="100px;" alt="woolishboy"/><br /><sub><b>woolishboy</b></sub></a><br /><a href="https://github.com/freesewing/freesewing/commits?author=woolishboy" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/cloutiy"><img src="https://avatars.githubusercontent.com/u/8433147?v=4?s=100" width="100px;" alt="yc"/><br /><sub><b>yc</b></sub></a><br /><a href="#translation-cloutiy" title="Translation">🌍</a></td>
</tr>
</tbody>
</table>

View file

@ -6,7 +6,7 @@ import { designs, plugins, packages } from './software/index.mjs'
* order. This file takes care of that
*/
const first = ['core', 'config-helpers', 'remark-jargon', 'snapseries']
const first = ['core', 'remark-jargon', 'snapseries']
const blocks = ['brian', 'titan', 'bella', 'breanna']
const extended = ['bent', 'simon', 'carlton', 'ursula']
const last = ['i18n']

View file

@ -3,7 +3,6 @@ _types:
peer:
'@freesewing/core': &freesewing '^{{version}}'
'@freesewing/plugin-bundle': *freesewing
'@freesewing/config-helpers': *freesewing
dev:
'mocha': &mocha '^10.0.0'
'chai': &chai '^4.2.0'
@ -50,6 +49,7 @@ charlie:
'@freesewing/plugin-bartack': *freesewing
'@freesewing/plugin-mirror': *freesewing
'@freesewing/titan': *freesewing
'@freesewing/snapseries': *freesewing
core:
_:
'bezier-js': '^6.1.0'
@ -111,6 +111,7 @@ legend:
paco:
peer:
'@freesewing/titan': *freesewing
'@freesewing/snapseries': *freesewing
plugin-bundle:
dev:
'@freesewing/plugin-banner': *freesewing
@ -154,6 +155,10 @@ rehype-jargon:
rehype-highlight-lines:
_:
'unist-util-remove': '^3.1.0'
sandy:
'@freesewing/snapseries': *freesewing
shin:
'@freesewing/snapseries': *freesewing
simon:
peer:
'@freesewing/brian': *freesewing
@ -176,6 +181,10 @@ teagan:
peer:
'@freesewing/brian': *freesewing
'@freesewing/plugin-bust': *freesewing
titan:
'@freesewing/snapseries': *freesewing
trayvon:
'@freesewing/snapseries': *freesewing
wahid:
peer:
'@freesewing/brian': *freesewing

View file

@ -8,13 +8,6 @@ core:
- patterns
- sewing
- sewing patterns
components:
- react
css-theme:
- css
- scss
- sass
- theme
design:
- design
- diy
@ -40,10 +33,6 @@ models:
- fashion
- measurements
- sizes
mui-theme:
- material-ui
- react
- theme
other:
- design
- diy

View file

@ -1,5 +1,4 @@
{
"snapseries": "A FreeSewing package to facilitate snapped percentage options in designs",
"core": "A library for creating made-to-measure sewing patterns",
"i18n": "Translations for the FreeSewing project",
"models": "Body measurements data for a range of default sizes",

View file

@ -13,6 +13,8 @@ import { version } from '../data.mjs'
import { __loadPatternDefaults } from './config.mjs'
import cloneDeep from 'lodash.clonedeep'
const DISTANCE_DEBUG = false
//////////////////////////////////////////////
// CONSTRUCTOR //
//////////////////////////////////////////////
@ -474,14 +476,25 @@ Pattern.prototype.__addPartOptions = function (part) {
// Keep design parts immutable in the pattern or risk subtle bugs
this.config.options[optionName] = Object.freeze(part.options[optionName])
this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``)
} else if (
this.__mutated.optionDistance[optionName] < this.__mutated.partDistance[part.name]
) {
} else {
if (DISTANCE_DEBUG)
this.store.log.debug(
'optionDistance for ' +
optionName +
' is ' +
this.__mutated.optionDistance[optionName] +
', and partDistance for ' +
part.name +
' is ' +
this.__mutated.partDistance[part.name]
)
if (this.__mutated.optionDistance[optionName] > this.__mutated.partDistance[part.name]) {
this.config.options[optionName] = part.options[optionName]
this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
}
}
}
}
if (part.from) this.__addPartOptions(part.from)
if (part.after) {
if (Array.isArray(part.after)) {
@ -1256,41 +1269,104 @@ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
}
}
distance++
if (DISTANCE_DEBUG) this.store.log.debug('Distance incremented to ' + distance)
for (const part of this.designConfig.parts) {
if (typeof this.__mutated.partDistance[part.name] === 'undefined')
if (typeof this.__mutated.partDistance[part.name] === 'undefined') {
this.__mutated.partDistance[part.name] = distance
if (DISTANCE_DEBUG)
this.store.log.debug(
'Base partDistance for ' + part.name + ' is ' + this.__mutated.partDistance[part.name]
)
}
}
for (const [name, part] of Object.entries(this.__designParts)) {
const current_part_distance = this.__mutated.partDistance[part.name]
const proposed_dependent_part_distance = current_part_distance + 1
// Hide when hideAll is set
if (part.hideAll) this.__mutated.partHide[part.name] = true
// Inject (from)
if (part.from) {
if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "from:"')
this.__setFromHide(part, name, part.from.name)
this.__designParts[part.from.name] = part.from
this.__inject[name] = part.from.name
this.__mutated.partDistance[part.from.name] = distance
if (
typeof this.__mutated.partDistance[part.from.name] === 'undefined' ||
this.__mutated.partDistance[part.from.name] < proposed_dependent_part_distance
) {
this.__mutated.partDistance[part.from.name] = proposed_dependent_part_distance
if (DISTANCE_DEBUG)
this.store.log.debug(
'"from:" partDistance for ' +
part.from.name +
' is ' +
this.__mutated.partDistance[part.from.name]
)
}
}
// Simple dependency (after)
if (part.after) {
if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "after:"')
if (Array.isArray(part.after)) {
for (const dep of part.after) {
this.__setAfterHide(part, name, dep.name)
this.__mutated.partDistance[dep.name] = distance
this.__designParts[dep.name] = dep
this.__addDependency(name, part, dep)
if (
typeof this.__mutated.partDistance[dep.name] === 'undefined' ||
this.__mutated.partDistance[dep.name] < proposed_dependent_part_distance
) {
this.__mutated.partDistance[dep.name] = proposed_dependent_part_distance
if (DISTANCE_DEBUG)
this.store.log.debug(
'"after:" partDistance for ' +
dep.name +
' is ' +
this.__mutated.partDistance[dep.name]
)
}
}
} else {
this.__setAfterHide(part, name, part.after.name)
this.__mutated.partDistance[part.after.name] = distance
this.__designParts[part.after.name] = part.after
this.__addDependency(name, part, part.after)
if (
typeof this.__mutated.partDistance[part.after.name] === 'undefined' ||
this.__mutated.partDistance[part.after.name] < proposed_dependent_part_distance
) {
this.__mutated.partDistance[part.after.name] = proposed_dependent_part_distance
if (DISTANCE_DEBUG)
this.store.log.debug(
'"after:" partDistance for ' +
part.after.name +
' is ' +
this.__mutated.partDistance[part.after.name]
)
}
}
}
}
// Did we discover any new dependencies?
const len = Object.keys(this.__designParts).length
// If so, resolve recursively
if (len > count) return this.__resolveParts(len, distance)
if (len > count) {
if (DISTANCE_DEBUG) this.store.log.debug('Recursing...')
return this.__resolveParts(len, distance)
}
// Print final part distances.
for (const part of this.designConfig.parts) {
let qualifier = ''
if (DISTANCE_DEBUG) qualifier = 'final '
this.store.log.debug(
'⚪️ `' +
part.name +
'` ' +
qualifier +
'options priority is __' +
this.__mutated.partDistance[part.name] +
'__'
)
}
for (const part of Object.values(this.__designParts)) this.__addPartConfig(part)

View file

@ -631,7 +631,7 @@ export function __asNumber(value, param, method, log) {
value = Number(value)
return value
} catch {
this.log.error(
log.error(
`Called \`${method}(${param})\` but \`${param}\` is not a number nor can it be cast to one`
)
}

View file

@ -63,7 +63,6 @@ describe('Part', () => {
const part = {
name: 'test',
draft: ({ getId, part }) => {
console.log(getId)
id = getId()
id = getId()
id = getId()

View file

@ -1,13 +1,13 @@
import chai from 'chai'
import { round, Path, Point, Design } from '../src/index.mjs'
import { round, Path, Point } from '../src/index.mjs'
import { pathsProxy } from '../src/path.mjs'
const expect = chai.expect
describe('Path', () => {
describe('smurve', () => {
it('Should draw a smurve', () => {
const part = {
name: 'test',
draft: ({ Point, points, Path, paths, part }) => {
const points = {}
points.from = new Point(10, 20)
points.cp1 = new Point(40, 10)
points.cp2 = new Point(60, 30)
@ -15,275 +15,157 @@ describe('Path', () => {
points.scp2 = new Point(140, 10)
points.sto = new Point(170, 20)
paths.test = new Path()
const test = new Path()
.move(points.from)
.curve(points.cp1, points.cp2, points.to)
.smurve(points.scp2, points.sto)
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(round(pattern.parts[0].test.paths.test.ops[2].cp1.x)).to.equal(120)
expect(round(pattern.parts[0].test.paths.test.ops[2].cp1.y)).to.equal(10)
expect(round(test.ops[2].cp1.x)).to.equal(120)
expect(round(test.ops[2].cp1.y)).to.equal(10)
})
it('Should draw a smurve_', () => {
const part = {
name: 'test',
draft: ({ Point, points, Path, paths, part }) => {
const points = {}
points.from = new Point(10, 20)
points.cp1 = new Point(40, 10)
points.cp2 = new Point(60, 30)
points.to = new Point(90, 20)
points.sto = new Point(170, 20)
paths.test = new Path()
const test = new Path()
.move(points.from)
.curve(points.cp1, points.cp2, points.to)
.smurve_(points.sto)
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(round(pattern.parts[0].test.paths.test.ops[2].cp1.x)).to.equal(120)
expect(round(pattern.parts[0].test.paths.test.ops[2].cp1.y)).to.equal(10)
expect(round(test.ops[2].cp1.x)).to.equal(120)
expect(round(test.ops[2].cp1.y)).to.equal(10)
})
it('Should log a warning when passing a non-Point to smurve()', () => {
const part = {
name: 'test',
draft: ({ Point, points, Path, paths, part }) => {
const points = {}
points.from = new Point(10, 20)
points.cp1 = new Point(40, 10)
points.cp2 = new Point(60, 30)
points.to = new Point(90, 20)
paths.test = new Path()
const messages = []
const log = { warning: (msg) => messages.push(msg) }
new Path()
.__withLog(log)
.move(points.from)
.curve(points.cp1, points.cp2, points.to)
.smurve('hi', 'there')
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft()
expect(pattern.setStores[0].logs.warning.length).to.equal(2)
expect(pattern.setStores[0].logs.warning[0]).to.equal(
'Called `Path.smurve(cp2, to)` but `to` is not a `Point` object'
)
expect(messages.length).to.equal(2)
expect(messages[0]).to.equal('Called `Path.smurve(cp2, to)` but `to` is not a `Point` object')
})
it('Should log a warning when passing a non-Point to smurve_()', () => {
const part = {
name: 'test',
draft: ({ Path, paths, part }) => {
paths.test = new Path().smurve_('hi')
return part
},
const messages = []
const log = { warning: (msg) => messages.push(msg) }
try {
new Path().__withLog(log).smurve_('hi')
} catch (e) {
expect('' + e).to.contain("TypeError: Cannot read properties of undefined (reading 'cp2')")
} finally {
expect(messages.length).to.equal(1)
expect(messages[0]).to.equal('Called `Path.smurve_(to)` but `to` is not a `Point` object')
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft()
expect(pattern.setStores[0].logs.warning.length).to.equal(1)
expect(pattern.setStores[0].logs.warning[0]).to.equal(
'Called `Path.smurve_(to)` but `to` is not a `Point` object'
)
})
})
it('Should log a warning when passing a non-Path to the paths proxy', () => {
const part = {
name: 'test',
draft: ({ paths, part }) => {
paths.test = 'Wriing code can get very lonely sometimes'
const messages = []
const log = { warning: (msg) => messages.push(msg) }
const pathsObj = {}
const paths = pathsProxy(pathsObj, log)
paths.set(pathsObj, 'test', 'Writing code can get very lonely sometimes')
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft()
expect(pattern.setStores[0].logs.warning.length).to.equal(2)
expect(pattern.setStores[0].logs.warning[0]).to.equal(
'`paths.test` was set with a value that is not a `Path` object'
)
expect(pattern.setStores[0].logs.warning[1]).to.equal(
'Could not set `name` property on `paths.test`'
)
expect(messages.length).to.equal(2)
expect(messages[0]).to.equal('`paths.test` was set with a value that is not a `Path` object')
expect(messages[1]).to.equal('Could not set `name` property on `paths.test`')
})
describe('offset', () => {
it('Should offset a line', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.line = new Path().move(new Point(0, 0)).line(new Point(0, 40))
paths.offset = paths.line.offset(10)
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(pattern.parts[0].test.paths.offset.bottomRight.x).to.equal(-10)
expect(pattern.parts[0].test.paths.offset.bottomRight.y).to.equal(40)
const line = new Path().move(new Point(0, 0)).line(new Point(0, 40))
const offLine = line.offset(10)
const bbox = offLine.bbox()
expect(bbox.bottomRight.x).to.equal(-10)
expect(bbox.bottomRight.y).to.equal(40)
})
it('Should offset a curve', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.curve = new Path()
const curve = new Path()
.move(new Point(0, 0))
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
paths.offset = paths.curve.offset(10)
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(72.18)
expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(38.26)
const offset = curve.offset(10)
const bbox = offset.bbox()
expect(round(bbox.bottomRight.x)).to.equal(72.18)
expect(round(bbox.bottomRight.y)).to.equal(38.26)
})
it('Should offset a curve where cp1 = start', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.curve = new Path().move(new Point(0, 0))._curve(new Point(123, 34), new Point(23, 4))
paths.offset = paths.curve.offset(10)
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(72.63)
expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(26.47)
const curve = new Path().move(new Point(0, 0))._curve(new Point(123, 34), new Point(23, 4))
const offset = curve.offset(10)
const bbox = offset.bbox()
expect(round(bbox.bottomRight.x)).to.equal(72.63)
expect(round(bbox.bottomRight.y)).to.equal(26.47)
})
it('Should offset a curve where cp2 = end', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.curve = new Path().move(new Point(0, 0)).curve_(new Point(40, 0), new Point(123, 34))
paths.offset = paths.curve.offset(10)
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(round(pattern.parts[0].test.paths.offset.bottomRight.x)).to.equal(119.86)
expect(round(pattern.parts[0].test.paths.offset.bottomRight.y)).to.equal(43.49)
const curve = new Path().move(new Point(0, 0)).curve_(new Point(40, 0), new Point(123, 34))
const offset = curve.offset(10)
const bbox = offset.bbox()
expect(round(bbox.bottomRight.x)).to.equal(119.86)
expect(round(bbox.bottomRight.y)).to.equal(43.49)
})
})
describe('length', () => {
it('Should return the length of a line', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.line = new Path().move(new Point(0, 0)).line(new Point(40, 0))
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(pattern.parts[0].test.paths.line.length()).to.equal(40)
const line = new Path().move(new Point(0, 0)).line(new Point(40, 0))
expect(line.length()).to.equal(40)
})
it('Should return the length of a curve', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.curve = new Path()
const curve = new Path()
.move(new Point(0, 0))
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
.close()
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(round(pattern.parts[0].test.paths.curve.length())).to.equal(145.11)
expect(round(curve.length())).to.equal(145.11)
})
})
it('Should return the rough length of a curve', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.curve = new Path()
const curve = new Path()
.move(new Point(0, 0))
.curve(new Point(0, 50), new Point(100, 50), new Point(100, 0))
.close()
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(round(pattern.parts[0].test.paths.curve.roughLength())).to.equal(300)
expect(round(curve.roughLength())).to.equal(300)
})
it('Should return the rough length of a line', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.line = new Path().move(new Point(0, 0)).line(new Point(0, 50))
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(round(pattern.parts[0].test.paths.line.roughLength())).to.equal(50)
const line = new Path().move(new Point(0, 0)).line(new Point(0, 50))
expect(round(line.roughLength())).to.equal(50)
})
it('Should return the path start point', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.curve = new Path()
const curve = new Path()
.move(new Point(123, 456))
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
.close()
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(pattern.parts[0].test.paths.curve.start().x).to.equal(123)
expect(pattern.parts[0].test.paths.curve.start().y).to.equal(456)
expect(curve.start().x).to.equal(123)
expect(curve.start().y).to.equal(456)
})
it('Should return the path end point', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.curve = new Path()
const curve = new Path()
.move(new Point(123, 456))
.curve(new Point(0, 40), new Point(123, 34), new Point(23, 4))
.close()
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
expect(pattern.parts[0].test.paths.curve.end().x).to.equal(123)
expect(pattern.parts[0].test.paths.curve.end().y).to.equal(456)
expect(curve.end().x).to.equal(123)
expect(curve.end().y).to.equal(456)
})
it('Should calculate that path boundary', () => {
@ -810,24 +692,17 @@ describe('Path', () => {
})
it('Should overwrite a path attribute', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, part }) => {
paths.line = new Path()
const line = new Path()
line.log = { debug: () => {} }
line
.move(new Point(0, 0))
.line(new Point(0, 40))
.attr('class', 'foo')
.attr('class', 'bar')
.attr('class', 'overwritten', true)
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft().render()
// Paths from shorthand have the log method
expect(pattern.parts[0].test.paths.line.attributes.get('class')).to.equal('overwritten')
expect(line.attributes.get('class')).to.equal('overwritten')
})
it('Should move along a path even if it lands just on a joint', () => {
@ -1054,7 +929,6 @@ describe('Path', () => {
it('Should log a warning when setting an attribute without a name', () => {
let invalid = false
const log = { warning: () => (invalid = true) }
expect(invalid).to.equal(false)
new Path().__withLog(log).attr()
expect(invalid).to.equal(true)
})
@ -1062,52 +936,42 @@ describe('Path', () => {
it('Should log a warning when setting an attribute without a value', () => {
let invalid = false
const log = { warning: () => (invalid = true) }
expect(invalid).to.equal(false)
new Path().__withLog(log).attr('test')
expect(invalid).to.equal(true)
})
it('Should log a warning when calling offset without a distance', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, points }) => {
paths.line = new Path().move(new Point(0, 0)).line(new Point(0, 40)).attr('class', 'foo')
paths.a = new Path().move(points.a).line(points.b)
paths.b = paths.a.offset()
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft()
expect(pattern.setStores[0].logs.error.length).to.equal(2)
expect(pattern.setStores[0].logs.error[0]).to.equal(
'Called `Path.offset(distance)` but `distance` is not a number'
)
it('Should log an error when calling offset without a distance', () => {
let invalid = true
const log = { warning: () => {}, error: () => (invalid = true) }
const pointLog = { error: () => {} }
const pointA = new Point(0, 0).__withLog(pointLog)
const pointB = new Point(0, 40).__withLog(pointLog)
const a = new Path().__withLog(log).move(pointA).line(pointB)
a.offset()
expect(invalid).to.equal(true)
})
it('Should log a warning when calling join without a path', () => {
const part = {
name: 'test',
draft: ({ paths, Path, Point, points }) => {
paths.line = new Path().move(new Point(0, 0)).line(new Point(0, 40)).attr('class', 'foo')
paths.a = new Path().move(points.a).line(points.b).join()
return part
},
it('Should log an error when calling join without a path', () => {
let invalid = false
const log = { error: () => (invalid = true) }
const line = new Path()
.move(new Point(0, 0))
.line(new Point(0, 40))
.attr('class', 'foo')
.__withLog(log)
try {
line.join()
} catch (e) {
expect('' + e).to.contain("TypeError: Cannot read properties of undefined (reading 'ops')")
} finally {
expect(invalid).to.equal(true)
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft()
expect(pattern.setStores[0].logs.error.length).to.equal(2)
expect(pattern.setStores[0].logs.error[0]).to.equal(
'Called `Path.join(that)` but `that` is not a `Path` object'
)
})
it('Should log a warning when calling start on a path without drawing operations', () => {
let invalid = false
const log = { error: () => (invalid = true) }
expect(invalid).to.equal(false)
try {
new Path().__withLog(log).start()
} catch (err) {
@ -1116,10 +980,9 @@ describe('Path', () => {
expect(invalid).to.equal(true)
})
it('Should log a warning when calling end on a path without drawing operations', () => {
it('Should log an error when calling end on a path without drawing operations', () => {
let invalid = false
const log = { error: () => (invalid = true) }
expect(invalid).to.equal(false)
try {
new Path().__withLog(log).end()
} catch (err) {
@ -1128,7 +991,7 @@ describe('Path', () => {
expect(invalid).to.equal(true)
})
it('Should log a warning when calling shiftAlong but distance is not a number', () => {
it('Should log an error when calling shiftAlong but distance is not a number', () => {
let invalid = false
const log = { error: () => (invalid = true) }
expect(invalid).to.equal(false)
@ -1136,53 +999,36 @@ describe('Path', () => {
expect(invalid).to.equal(true)
})
it('Should log a warning when calling shiftFractionalong but fraction is not a number', () => {
const part = {
name: 'test',
draft: ({ Path, Point, points }) => {
points.a = new Path().move(new Point(0, 0)).line(new Point(0, 40)).shiftFractionAlong()
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft()
expect(pattern.setStores[0].logs.error[0]).to.equal(
'Called `Path.shiftFractionAlong(fraction)` but `fraction` is not a number'
)
it('Should log an error when calling shiftFractionalong but fraction is not a number', () => {
let invalid = false
const log = { error: () => (invalid = true) }
new Path().__withLog(log).move(new Point(0, 0)).line(new Point(0, 40)).shiftFractionAlong()
expect(invalid).to.equal(true)
})
it('Should log a warning when splitting a path on a non-point', () => {
const part = {
name: 'test',
draft: ({ Path, Point, points, part }) => {
points.a = new Path().move(new Point(0, 0)).line(new Point(0, 40)).split()
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft()
expect(pattern.setStores[0].logs.error[0]).to.equal(
'Called `Path.split(point)` but `point` is not a `Point` object'
it('Should log an error when splitting a path on a non-point', () => {
let invalid = false
const log = { error: () => (invalid = true) }
const pointLog = { warning: () => {} }
try {
new Path()
.__withLog(log)
.move(new Point(0, 0).__withLog(pointLog))
.line(new Point(0, 40).__withLog(pointLog))
.split()
} catch (e) {
expect(e.toString()).to.include(
"TypeError: Cannot read properties of undefined (reading '__check')"
)
} finally {
expect(invalid).to.equal(true)
}
})
it('Should add a class', () => {
const part = {
name: 'test',
draft: ({ Path, paths, Point, part }) => {
paths.line = new Path()
.move(new Point(0, 0))
.line(new Point(10, 10))
.addClass('fabric banana')
return part
},
}
const design = new Design({ parts: [part] })
const pattern = new design()
pattern.draft()
expect(pattern.parts[0].test.paths.line.attributes.get('class')).to.equal('fabric banana')
const line = new Path().move(new Point(0, 0)).line(new Point(10, 10)).addClass('fabric banana')
expect(line.attributes.get('class')).to.equal('fabric banana')
})
it('Should (un)hide a path with hide()/unhide()', () => {

View file

@ -132,26 +132,30 @@ describe('Point', () => {
expect(round(ss.shiftTowards(se, 200).y)).to.equal(-18.42)
})
describe('shiftFractionTowards', () => {
it('Should shift a point a fraction towards another', () => {
let origin = new Point(0, 0)
let n = new Point(0, -10)
let e = new Point(10, 0)
let s = new Point(0, 10)
let w = new Point(-10, 0)
let sn = origin.shiftFractionTowards(n, 1.5)
let se = origin.shiftFractionTowards(e, 1.5)
let ss = origin.shiftFractionTowards(s, 0.5)
let sw = origin.shiftFractionTowards(w, 2.5)
expect(round(sn.x)).to.equal(0)
expect(round(sn.y)).to.equal(-15)
expect(round(se.x)).to.equal(15)
expect(round(se.y)).to.equal(0)
expect(round(ss.x)).to.equal(0)
expect(round(ss.y)).to.equal(5)
expect(round(sw.x)).to.equal(-25)
expect(round(sw.y)).to.equal(0)
expect(round(sw.shiftFractionTowards(sn, 100).x)).to.equal(2475)
expect(round(ss.shiftFractionTowards(se, 200).y)).to.equal(-995)
})
it('Should shift a point a fraction beyond another if the fraction is > 1', () => {
let origin = new Point(0, 0)
let n = new Point(0, -10)
let sn = origin.shiftFractionTowards(n, 1.5)
expect(round(sn.x)).to.equal(0)
expect(round(sn.y)).to.equal(-15)
})
it('Should shift a point a fraction away from another if the fraction is < 0', () => {
let origin = new Point(0, 0)
let n = new Point(0, -10)
let sn = origin.shiftFractionTowards(n, -0.5)
expect(round(sn.x)).to.equal(0)
expect(round(sn.y)).to.equal(5)
})
})
it('Should shift a point beyond another', () => {

View file

@ -154,6 +154,7 @@ describe('Utils', () => {
expect(hits.length).to.equal(3)
})
describe('curvesIntersect', function () {
it('Should find 9 intersections between two curves', () => {
let A = new Point(10, 10)
let Acp = new Point(310, 40)
@ -196,6 +197,7 @@ describe('Utils', () => {
let hit = curvesIntersect(A, Acp, Bcp, B, C, Ccp, Dcp, D)
expect(hit).to.equal(false)
})
})
it('Should correctly format units', () => {
expect(units(123.456)).to.equal('12.35cm')

View file

@ -1,25 +1,37 @@
const fs = require('fs')
const path = require('path');
const path = require('path')
const spawn = require('child_process').spawn
const projectRoot = path.normalize(path.join(__dirname, '..'));
const outputLog = path.join(projectRoot, '.test-failures.log');
const collectorScript = path.join(projectRoot, 'scripts', 'test-failure-collector.js');
const projectRoot = path.normalize(path.join(__dirname, '..'))
const outputLog = path.join(projectRoot, '.test-failures.log')
const collectorScript = path.join(projectRoot, 'scripts', 'test-failure-collector.js')
// Start with a fresh output log on each run.
if (fs.existsSync(outputLog)) {
fs.unlinkSync(outputLog);
fs.unlinkSync(outputLog)
}
// Run all tests, specifying the collector script.
spawn('lerna', ['run', '--no-bail', 'testci', '--stream', '--parallel', '--loglevel', 'error', '--', '--file', `${collectorScript}`, '--no-warnings'], { stdio: 'inherit' })
.on('exit', function(code) {
spawn(
'lerna',
[
'run',
'--no-bail',
'testci',
'--loglevel',
'error',
'--',
'--file',
`${collectorScript}`,
'--no-warnings',
],
{ stdio: 'inherit' }
).on('exit', function (code) {
// If a failure occurred, the log file will have been created. Print it.
if (fs.existsSync(outputLog)) {
console.error(fs.readFileSync(outputLog, 'utf8').trim());
console.error(fs.readFileSync(outputLog, 'utf8').trim())
}
// Propagate the exit code.
process.exit(code);
});
process.exit(code)
})

View file

@ -7,7 +7,6 @@
"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"
},
@ -30,10 +29,13 @@
"crypto": "^1.0.1",
"express": "4.18.2",
"mustache": "^4.2.0",
"passport": "^0.6.0",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",
"pino": "^8.7.0"
},
"devDependencies": {
"chai-http": "^4.3.0",
"mocha": "^10.1.0",
"mocha-steps": "^1.3.0",
"prisma": "4.5.0"

View file

@ -73,9 +73,12 @@ model Pattern {
data String
design String
img String?
name String @default("")
notes String
person Person? @relation(fields: [personId], references: [id])
personId Int?
notes String
public Boolean @default(false)
settings String
user User @relation(fields: [userId], references: [id])
userId Int
updatedAt DateTime @updatedAt

Binary file not shown.

View file

@ -4,7 +4,7 @@ import { log } from '../utils/log.mjs'
import { ApikeyModel } from '../models/apikey.mjs'
import { UserModel } from '../models/user.mjs'
export function ApikeyController() {}
export function ApikeysController() {}
/*
* Create API key
@ -12,7 +12,7 @@ export function ApikeyController() {}
* This is the endpoint that handles creation of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey
*/
ApikeyController.prototype.create = async (req, res, tools) => {
ApikeysController.prototype.create = async (req, res, tools) => {
const Apikey = new ApikeyModel(tools)
await Apikey.create(req)
@ -25,7 +25,7 @@ ApikeyController.prototype.create = async (req, res, tools) => {
* This is the endpoint that handles creation of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey
*/
ApikeyController.prototype.read = async (req, res, tools) => {
ApikeysController.prototype.read = async (req, res, tools) => {
const Apikey = new ApikeyModel(tools)
await Apikey.guardedRead(req)
@ -39,7 +39,7 @@ ApikeyController.prototype.read = async (req, res, tools) => {
* request
* See: https://freesewing.dev/reference/backend/api/apikey
*/
ApikeyController.prototype.whoami = async (req, res, tools) => {
ApikeysController.prototype.whoami = async (req, res, tools) => {
const User = new UserModel(tools)
const Apikey = new ApikeyModel(tools)
@ -69,7 +69,7 @@ ApikeyController.prototype.whoami = async (req, res, tools) => {
* This is the endpoint that handles removal of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey
*/
ApikeyController.prototype.delete = async (req, res, tools) => {
ApikeysController.prototype.delete = async (req, res, tools) => {
const Apikey = new ApikeyModel(tools)
await Apikey.guardedDelete(req)

View file

@ -0,0 +1,58 @@
import { PatternModel } from '../models/pattern.mjs'
export function PatternsController() {}
/*
* Create a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.create = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedCreate(req)
return Pattern.sendResponse(res)
}
/*
* Read a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.read = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedRead(req)
return Pattern.sendResponse(res)
}
/*
* Update a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.update = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedUpdate(req)
return Pattern.sendResponse(res)
}
/*
* Remove a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.delete = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedDelete(req)
return Pattern.sendResponse(res)
}
/*
* Clone a pattern
* See: https://freesewing.dev/reference/backend/api
*/
PatternsController.prototype.clone = async (req, res, tools) => {
const Pattern = new PatternModel(tools)
await Pattern.guardedClone(req)
return Pattern.sendResponse(res)
}

View file

@ -1,12 +1,12 @@
import { PersonModel } from '../models/person.mjs'
export function PersonController() {}
export function PeopleController() {}
/*
* Create a person for the authenticated user
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.create = async (req, res, tools) => {
PeopleController.prototype.create = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedCreate(req)
@ -17,7 +17,7 @@ PersonController.prototype.create = async (req, res, tools) => {
* Read a person
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.read = async (req, res, tools) => {
PeopleController.prototype.read = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedRead(req)
@ -28,7 +28,7 @@ PersonController.prototype.read = async (req, res, tools) => {
* Update a person
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.update = async (req, res, tools) => {
PeopleController.prototype.update = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedUpdate(req)
@ -39,7 +39,7 @@ PersonController.prototype.update = async (req, res, tools) => {
* Remove a person
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.delete = async (req, res, tools) => {
PeopleController.prototype.delete = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedDelete(req)
@ -50,7 +50,7 @@ PersonController.prototype.delete = async (req, res, tools) => {
* Clone a person
* See: https://freesewing.dev/reference/backend/api
*/
PersonController.prototype.clone = async (req, res, tools) => {
PeopleController.prototype.clone = async (req, res, tools) => {
const Person = new PersonModel(tools)
await Person.guardedClone(req)

View file

@ -1,6 +1,6 @@
import { UserModel } from '../models/user.mjs'
export function UserController() {}
export function UsersController() {}
/*
* Signup
@ -8,7 +8,7 @@ export function UserController() {}
* This is the endpoint that handles account signups
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.signup = async (req, res, tools) => {
UsersController.prototype.signup = async (req, res, tools) => {
const User = new UserModel(tools)
await User.guardedCreate(req)
@ -21,7 +21,7 @@ UserController.prototype.signup = async (req, res, tools) => {
* This is the endpoint that fully unlocks the account if the user gives their consent
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.confirm = async (req, res, tools) => {
UsersController.prototype.confirm = async (req, res, tools) => {
const User = new UserModel(tools)
await User.confirm(req)
@ -34,7 +34,7 @@ UserController.prototype.confirm = async (req, res, tools) => {
* This is the endpoint that provides traditional username/password login
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.login = async function (req, res, tools) {
UsersController.prototype.login = async function (req, res, tools) {
const User = new UserModel(tools)
await User.passwordLogin(req)
@ -46,7 +46,7 @@ UserController.prototype.login = async function (req, res, tools) {
*
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.whoami = async (req, res, tools) => {
UsersController.prototype.whoami = async (req, res, tools) => {
const User = new UserModel(tools)
await User.guardedRead({ id: req.user.uid }, req)
@ -58,7 +58,7 @@ UserController.prototype.whoami = async (req, res, tools) => {
*
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.update = async (req, res, tools) => {
UsersController.prototype.update = async (req, res, tools) => {
const User = new UserModel(tools)
await User.guardedRead({ id: req.user.uid }, req)
await User.guardedUpdate(req)

View file

@ -0,0 +1,317 @@
import { log } from '../utils/log.mjs'
import { setPatternAvatar } from '../utils/sanity.mjs'
export function PatternModel(tools) {
this.config = tools.config
this.prisma = tools.prisma
this.decrypt = tools.decrypt
this.encrypt = tools.encrypt
this.encryptedFields = ['data', 'img', 'name', 'notes', 'settings']
this.clear = {} // For holding decrypted data
return this
}
/*
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
data String
design String
img String?
person Person? @relation(fields: [personId], references: [id])
personId Int?
name String @default("")
notes String
public
settings String
user User @relation(fields: [userId], references: [id])
userId Int
updatedAt DateTime @updatedAt
*/
PatternModel.prototype.guardedCreate = async function ({ body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
if (Object.keys(body) < 2) return this.setResponse(400, 'postBodyMissing')
if (!body.person) return this.setResponse(400, 'personMissing')
if (typeof body.person !== 'number') return this.setResponse(400, 'personNotNumeric')
if (typeof body.settings !== 'object') return this.setResponse(400, 'settingsNotAnObject')
if (body.data && typeof body.data !== 'object') return this.setResponse(400, 'dataNotAnObject')
if (!body.design && !body.data?.design) return this.setResponse(400, 'designMissing')
if (typeof body.design !== 'string') return this.setResponse(400, 'designNotStringy')
// Prepare data
const data = {
design: body.design,
personId: body.person,
settings: body.settings,
}
// Data (will be encrypted, so always set _some_ value)
if (typeof body.data === 'object') data.data = body.data
else data.data = {}
// Name (will be encrypted, so always set _some_ value)
if (typeof body.name === 'string' && body.name.length > 0) data.name = body.name
else data.name = '--'
// Notes (will be encrypted, so always set _some_ value)
if (typeof body.notes === 'string' && body.notes.length > 0) data.notes = body.notes
else data.notes = '--'
// Public
if (body.public === true) data.public = true
data.userId = user.uid
// Set this one initially as we need the ID to create a custom img via Sanity
data.img = this.config.avatars.pattern
// Create record
await this.unguardedCreate(data)
// Update img? (now that we have the ID)
const img =
this.config.use.sanity &&
typeof body.img === 'string' &&
(!body.unittest || (body.unittest && this.config.use.tests?.sanity))
? await setPatternAvatar(this.record.id, body.img)
: false
if (img) await this.unguardedUpdate(this.cloak({ img: img.url }))
else await this.read({ id: this.record.id })
return this.setResponse(201, 'created', { pattern: this.asPattern() })
}
PatternModel.prototype.unguardedCreate = async function (data) {
try {
this.record = await this.prisma.pattern.create({ data: this.cloak(data) })
} catch (err) {
log.warn(err, 'Could not create pattern')
return this.setResponse(500, 'createPatternFailed')
}
return this
}
/*
* Loads a pattern from the database based on the where clause you pass it
*
* Stores result in this.record
*/
PatternModel.prototype.read = async function (where) {
try {
this.record = await this.prisma.pattern.findUnique({ where })
} catch (err) {
log.warn({ err, where }, 'Could not read pattern')
}
this.reveal()
return this.setExists()
}
/*
* Loads a pattern from the database based on the where clause you pass it
* In addition prepares it for returning the pattern data
*
* Stores result in this.record
*/
PatternModel.prototype.guardedRead = async function ({ params, user }) {
if (user.level < 1) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 5) {
return this.setResponse(403, 'insufficientAccessLevel')
}
return this.setResponse(200, false, {
result: 'success',
pattern: this.asPattern(),
})
}
/*
* Clones a pattern
* In addition prepares it for returning the pattern data
*
* Stores result in this.record
*/
PatternModel.prototype.guardedClone = async function ({ params, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && !this.record.public && user.level < 5) {
return this.setResponse(403, 'insufficientAccessLevel')
}
// Clone pattern
const data = this.asPattern()
delete data.id
data.name += ` (cloned from #${this.record.id})`
data.notes += ` (Note: This pattern was cloned from pattern #${this.record.id})`
await this.unguardedCreate(data)
// Update unencrypted data
this.reveal()
return this.setResponse(200, false, {
result: 'success',
pattern: this.asPattern(),
})
}
/*
* Helper method to decrypt at-rest data
*/
PatternModel.prototype.reveal = async function () {
this.clear = {}
if (this.record) {
for (const field of this.encryptedFields) {
this.clear[field] = this.decrypt(this.record[field])
}
}
return this
}
/*
* Helper method to encrypt at-rest data
*/
PatternModel.prototype.cloak = function (data) {
for (const field of this.encryptedFields) {
if (typeof data[field] !== 'undefined') {
data[field] = this.encrypt(data[field])
}
}
return data
}
/*
* Checks this.record and sets a boolean to indicate whether
* the pattern exists or not
*
* Stores result in this.exists
*/
PatternModel.prototype.setExists = function () {
this.exists = this.record ? true : false
return this
}
/*
* Updates the pattern data - Used when we create the data ourselves
* so we know it's safe
*/
PatternModel.prototype.unguardedUpdate = async function (data) {
try {
this.record = await this.prisma.pattern.update({
where: { id: this.record.id },
data,
})
} catch (err) {
log.warn(err, 'Could not update pattern record')
process.exit()
return this.setResponse(500, 'updatePatternFailed')
}
await this.reveal()
return this.setResponse(200)
}
/*
* Updates the pattern data - Used when we pass through user-provided data
* so we can't be certain it's safe
*/
PatternModel.prototype.guardedUpdate = async function ({ params, body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 8) {
return this.setResponse(403, 'insufficientAccessLevel')
}
const data = {}
// Name
if (typeof body.name === 'string') data.name = body.name
// Notes
if (typeof body.notes === 'string') data.notes = body.notes
// Public
if (body.public === true || body.public === false) data.public = body.public
// Data
if (typeof body.data === 'object') data.data = body.data
// Settings
if (typeof body.settings === 'object') data.settings = body.settings
// Image (img)
if (typeof body.img === 'string') {
const img = await setPatternAvatar(params.id, body.img)
data.img = img.url
}
// Now update the record
await this.unguardedUpdate(this.cloak(data))
return this.setResponse(200, false, { pattern: this.asPattern() })
}
/*
* Removes the pattern - No questions asked
*/
PatternModel.prototype.unguardedDelete = async function () {
await this.prisma.pattern.delete({ here: { id: this.record.id } })
this.record = null
this.clear = null
return this.setExists()
}
/*
* Removes the pattern - Checks permissions
*/
PatternModel.prototype.guardedDelete = async function ({ params, body, user }) {
if (user.level < 3) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && user.level < 8) {
return this.setResponse(403, 'insufficientAccessLevel')
}
await this.unguardedDelete()
return this.setResponse(204, false)
}
/*
* Returns record data
*/
PatternModel.prototype.asPattern = function () {
return {
...this.record,
...this.clear,
}
}
/*
* Helper method to set the response code, result, and body
*
* Will be used by this.sendResponse()
*/
PatternModel.prototype.setResponse = function (status = 200, error = false, data = {}) {
this.response = {
status,
body: {
result: 'success',
...data,
},
}
if (status > 201) {
this.response.body.error = error
this.response.body.result = 'error'
this.error = true
} else this.error = false
return this.setExists()
}
/*
* Helper method to send response
*/
PatternModel.prototype.sendResponse = async function (res) {
return res.status(this.response.status).send(this.response.body)
}

View file

@ -19,7 +19,12 @@ PersonModel.prototype.guardedCreate = async function ({ body, user }) {
// Prepare data
const data = { name: body.name }
// Name (will be encrypted, so always set _some_ value)
if (typeof body.name === 'string') data.name = body.name
else data.name = '--'
// Notes (will be encrypted, so always set _some_ value)
if (body.notes || typeof body.notes === 'string') data.notes = body.notes
else data.notes = '--'
if (body.public === true) data.public = true
if (body.measies) data.measies = this.sanitizeMeasurements(body.measies)
data.imperial = body.imperial === true ? true : false

View file

@ -223,9 +223,9 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
* Login based on username + password
*/
UserModel.prototype.passwordLogin = async function (req) {
if (Object.keys(req.body) < 1) return this.setReponse(400, 'postBodyMissing')
if (!req.body.username) return this.setReponse(400, 'usernameMissing')
if (!req.body.password) return this.setReponse(400, 'passwordMissing')
if (Object.keys(req.body) < 1) return this.setResponse(400, 'postBodyMissing')
if (!req.body.username) return this.setResponse(400, 'usernameMissing')
if (!req.body.password) return this.setResponse(400, 'passwordMissing')
await this.find(req.body)
if (!this.exists) {
@ -255,7 +255,7 @@ UserModel.prototype.passwordLogin = async function (req) {
* Confirms a user account
*/
UserModel.prototype.confirm = async function ({ body, params }) {
if (!params.id) return this.setReponse(404, 'missingConfirmationId')
if (!params.id) return this.setResponse(404, 'missingConfirmationId')
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
if (!body.consent || typeof body.consent !== 'number' || body.consent < 1)
return this.setResponse(400, 'consentRequired')

View file

@ -1,38 +0,0 @@
import { ApikeyController } from '../controllers/apikey.mjs'
const Apikey = new ApikeyController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function apikeyRoutes(tools) {
const { app, passport } = tools
// Create Apikey
app.post('/apikey/jwt', passport.authenticate(...jwt), (req, res) =>
Apikey.create(req, res, tools)
)
app.post('/apikey/key', passport.authenticate(...bsc), (req, res) =>
Apikey.create(req, res, tools)
)
// Read Apikey
app.get('/apikey/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Apikey.read(req, res, tools)
)
app.get('/apikey/:id/key', passport.authenticate(...bsc), (req, res) =>
Apikey.read(req, res, tools)
)
// Read current Apikey
app.get('/whoami/key', passport.authenticate(...bsc), (req, res) =>
Apikey.whoami(req, res, tools)
)
// Remove Apikey
app.delete('/apikey/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Apikey.delete(req, res, tools)
)
app.delete('/apikey/:id/key', passport.authenticate(...bsc), (req, res) =>
Apikey.delete(req, res, tools)
)
}

View file

@ -0,0 +1,38 @@
import { ApikeysController } from '../controllers/apikeys.mjs'
const Apikeys = new ApikeysController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function apikeysRoutes(tools) {
const { app, passport } = tools
// Create Apikey
app.post('/apikeys/jwt', passport.authenticate(...jwt), (req, res) =>
Apikeys.create(req, res, tools)
)
app.post('/apikeys/key', passport.authenticate(...bsc), (req, res) =>
Apikeys.create(req, res, tools)
)
// Read Apikey
app.get('/apikeys/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Apikeys.read(req, res, tools)
)
app.get('/apikeys/:id/key', passport.authenticate(...bsc), (req, res) =>
Apikeys.read(req, res, tools)
)
// Read current Apikey
app.get('/whoami/key', passport.authenticate(...bsc), (req, res) =>
Apikeys.whoami(req, res, tools)
)
// Remove Apikey
app.delete('/apikeys/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Apikeys.delete(req, res, tools)
)
app.delete('/apikeys/:id/key', passport.authenticate(...bsc), (req, res) =>
Apikeys.delete(req, res, tools)
)
}

View file

@ -1,9 +1,11 @@
import { apikeyRoutes } from './apikey.mjs'
import { userRoutes } from './user.mjs'
import { personRoutes } from './person.mjs'
import { apikeysRoutes } from './apikeys.mjs'
import { usersRoutes } from './users.mjs'
import { peopleRoutes } from './people.mjs'
import { patternsRoutes } from './patterns.mjs'
export const routes = {
apikeyRoutes,
userRoutes,
personRoutes,
apikeysRoutes,
usersRoutes,
peopleRoutes,
patternsRoutes,
}

View file

@ -0,0 +1,49 @@
import { PatternsController } from '../controllers/patterns.mjs'
const Patterns = new PatternsController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function patternsRoutes(tools) {
const { app, passport } = tools
// Create pattern
app.post('/patterns/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.create(req, res, tools)
)
app.post('/patterns/key', passport.authenticate(...bsc), (req, res) =>
Patterns.create(req, res, tools)
)
// Clone pattern
app.post('/patterns/:id/clone/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.clone(req, res, tools)
)
app.post('/patterns/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
Patterns.clone(req, res, tools)
)
// Read pattern
app.get('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.read(req, res, tools)
)
app.get('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
Patterns.read(req, res, tools)
)
// Update pattern
app.put('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.update(req, res, tools)
)
app.put('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
Patterns.update(req, res, tools)
)
// Delete pattern
app.delete('/patterns/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Patterns.delete(req, res, tools)
)
app.delete('/patterns/:id/key', passport.authenticate(...bsc), (req, res) =>
Patterns.delete(req, res, tools)
)
}

View file

@ -1,49 +1,49 @@
import { PersonController } from '../controllers/person.mjs'
import { PeopleController } from '../controllers/people.mjs'
const Person = new PersonController()
const People = new PeopleController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function personRoutes(tools) {
export function peopleRoutes(tools) {
const { app, passport } = tools
// Create person
app.post('/people/jwt', passport.authenticate(...jwt), (req, res) =>
Person.create(req, res, tools)
People.create(req, res, tools)
)
app.post('/people/key', passport.authenticate(...bsc), (req, res) =>
Person.create(req, res, tools)
People.create(req, res, tools)
)
// Clone person
app.post('/people/:id/clone/jwt', passport.authenticate(...jwt), (req, res) =>
Person.clone(req, res, tools)
People.clone(req, res, tools)
)
app.post('/people/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
Person.clone(req, res, tools)
People.clone(req, res, tools)
)
// Read person
app.get('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Person.read(req, res, tools)
People.read(req, res, tools)
)
app.get('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
Person.read(req, res, tools)
People.read(req, res, tools)
)
// Update person
app.put('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Person.update(req, res, tools)
People.update(req, res, tools)
)
app.put('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
Person.update(req, res, tools)
People.update(req, res, tools)
)
// Delete person
app.delete('/people/:id/jwt', passport.authenticate(...jwt), (req, res) =>
Person.delete(req, res, tools)
People.delete(req, res, tools)
)
app.delete('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
Person.delete(req, res, tools)
People.delete(req, res, tools)
)
}

View file

@ -1,30 +1,38 @@
import { UserController } from '../controllers/user.mjs'
import { UsersController } from '../controllers/users.mjs'
const User = new UserController()
const Users = new UsersController()
const jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }]
export function userRoutes(tools) {
export function usersRoutes(tools) {
const { app, passport } = tools
// Sign up
app.post('/signup', (req, res) => User.signup(req, res, tools))
app.post('/signup', (req, res) => Users.signup(req, res, tools))
// Confirm account
app.post('/confirm/signup/:id', (req, res) => User.confirm(req, res, tools))
app.post('/confirm/signup/:id', (req, res) => Users.confirm(req, res, tools))
// Login
app.post('/login', (req, res) => User.login(req, res, tools))
app.post('/login', (req, res) => Users.login(req, res, tools))
// Read current jwt
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => User.whoami(req, res, tools))
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) => User.whoami(req, res, tools))
app.get('/account/key', passport.authenticate(...bsc), (req, res) => User.whoami(req, res, tools))
app.get('/whoami/jwt', passport.authenticate(...jwt), (req, res) => Users.whoami(req, res, tools))
app.get('/account/jwt', passport.authenticate(...jwt), (req, res) =>
Users.whoami(req, res, tools)
)
app.get('/account/key', passport.authenticate(...bsc), (req, res) =>
Users.whoami(req, res, tools)
)
// Update account
app.put('/account/jwt', passport.authenticate(...jwt), (req, res) => User.update(req, res, tools))
app.put('/account/key', passport.authenticate(...bsc), (req, res) => User.update(req, res, tools))
app.put('/account/jwt', passport.authenticate(...jwt), (req, res) =>
Users.update(req, res, tools)
)
app.put('/account/key', passport.authenticate(...bsc), (req, res) =>
Users.update(req, res, tools)
)
/*

View file

@ -33,6 +33,7 @@ async function getAvatar(type, id) {
*/
export const setUserAvatar = async (id, data) => setAvatar('user', id, data)
export const setPersonAvatar = async (id, data) => setAvatar('person', id, data)
export const setPatternAvatar = async (id, data) => setAvatar('pattern', id, data)
export async function setAvatar(type, id, data) {
// Step 1: Upload the image as asset
const [contentType, binary] = b64ToBinaryWithType(data)

View file

@ -3,7 +3,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'jwt')} Create API Key (jwt)`, (done) => {
chai
.request(config.api)
.post('/apikey/jwt')
.post('/apikeys/jwt')
.set('Authorization', 'Bearer ' + store.account.token)
.send({
name: 'Test API key',
@ -27,7 +27,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'key')} Create API Key (key)`, (done) => {
chai
.request(config.api)
.post('/apikey/key')
.post('/apikeys/key')
.auth(store.apikey1.key, store.apikey1.secret)
.send({
name: 'Test API key with key',
@ -67,7 +67,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'key')} Read API key (key)`, (done) => {
chai
.request(config.api)
.get(`/apikey/${store.apikey1.key}/key`)
.get(`/apikeys/${store.apikey1.key}/key`)
.auth(store.apikey2.key, store.apikey2.secret)
.end((err, res) => {
expect(res.status).to.equal(200)
@ -83,7 +83,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'jwt')} Read API key (jwt)`, (done) => {
chai
.request(config.api)
.get(`/apikey/${store.apikey2.key}/jwt`)
.get(`/apikeys/${store.apikey2.key}/jwt`)
.set('Authorization', 'Bearer ' + store.account.token)
.end((err, res) => {
expect(res.status).to.equal(200)
@ -99,7 +99,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'key')} Remove API key (key)`, (done) => {
chai
.request(config.api)
.delete(`/apikey/${store.apikey2.key}/key`)
.delete(`/apikeys/${store.apikey2.key}/key`)
.auth(store.apikey2.key, store.apikey2.secret)
.end((err, res) => {
expect(res.status).to.equal(204)
@ -110,7 +110,7 @@ export const apikeyTests = async (chai, config, expect, store) => {
step(`${store.icon('key', 'jwt')} Remove API key (jwt)`, (done) => {
chai
.request(config.api)
.delete(`/apikey/${store.apikey1.key}/jwt`)
.delete(`/apikeys/${store.apikey1.key}/jwt`)
.set('Authorization', 'Bearer ' + store.account.token)
.end((err, res) => {
expect(res.status).to.equal(204)

View file

@ -2,6 +2,7 @@ import { userTests } from './user.mjs'
import { accountTests } from './account.mjs'
import { apikeyTests } from './apikey.mjs'
import { personTests } from './person.mjs'
import { patternTests } from './pattern.mjs'
import { setup } from './shared.mjs'
const runTests = async (...params) => {
@ -9,6 +10,7 @@ const runTests = async (...params) => {
await apikeyTests(...params)
await accountTests(...params)
await personTests(...params)
await patternTests(...params)
}
// Load initial data required for tests

View file

@ -0,0 +1,326 @@
import { cat } from './cat.mjs'
export const patternTests = async (chai, config, expect, store) => {
store.account.patterns = {}
for (const auth of ['jwt', 'key']) {
describe(`${store.icon('pattern', auth)} Pattern tests (${auth})`, () => {
it(`${store.icon('pattern', auth)} Should create a new pattern (${auth})`, (done) => {
chai
.request(config.api)
.post(`/patterns/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({
design: 'aaron',
settings: {},
person: store.account.people.her.id,
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(201)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.pattern?.id).to.equal('number')
expect(res.body.pattern.userId).to.equal(store.account.id)
expect(res.body.pattern.personId).to.equal(store.account.people.her.id)
expect(res.body.pattern.design).to.equal('aaron')
expect(res.body.pattern.public).to.equal(false)
store.account.patterns[auth] = res.body.pattern
done()
})
}).timeout(5000)
for (const field of ['name', 'notes']) {
it(`${store.icon('pattern', auth)} Should update the ${field} field (${auth})`, (done) => {
const data = {}
const val = store.account.patterns[auth][field] + '_updated'
data[field] = val
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(
`${store.account.apikey.key}:${store.account.apikey.secret}`
).toString('base64')
)
.send(data)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.pattern[field]).to.equal('--_updated')
done()
})
})
}
it(`${store.icon('person', auth)} Should update the public field (${auth})`, (done) => {
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ public: true })
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.pattern.public).to.equal(true)
done()
})
})
it(`${store.icon('person', auth)} Should not update the design field (${auth})`, (done) => {
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ design: 'updated' })
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.pattern.design).to.equal('aaron')
done()
})
})
it(`${store.icon('person', auth)} Should not update the person field (${auth})`, (done) => {
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.send({ person: 1 })
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.pattern.personId).to.equal(store.account.people.her.id)
done()
})
})
for (const field of ['data', 'settings']) {
it(`${store.icon('person', auth)} Should update the ${field} field (${auth})`, (done) => {
const data = {}
data[field] = { test: { value: 'hello' } }
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(
`${store.account.apikey.key}:${store.account.apikey.secret}`
).toString('base64')
)
.send(data)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.pattern[field].test.value).to.equal('hello')
done()
})
})
}
it(`${store.icon('pattern', auth)} Should read a pattern (${auth})`, (done) => {
chai
.request(config.api)
.get(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(res.body.pattern.data.test.value).to.equal('hello')
done()
})
})
it(`${store.icon(
'person',
auth
)} Should not allow reading another user's pattern (${auth})`, (done) => {
chai
.request(config.api)
.get(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.altaccount.token
: 'Basic ' +
new Buffer(
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
).toString('base64')
)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(403)
expect(res.body.result).to.equal(`error`)
expect(res.body.error).to.equal(`insufficientAccessLevel`)
done()
})
})
it(`${store.icon(
'person',
auth
)} Should not allow updating another user's pattern (${auth})`, (done) => {
chai
.request(config.api)
.put(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.altaccount.token
: 'Basic ' +
new Buffer(
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
).toString('base64')
)
.send({
name: 'I have been taken over',
})
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(403)
expect(res.body.result).to.equal(`error`)
expect(res.body.error).to.equal(`insufficientAccessLevel`)
done()
})
})
it(`${store.icon(
'person',
auth
)} Should not allow removing another user's pattern (${auth})`, (done) => {
chai
.request(config.api)
.delete(`/patterns/${store.account.patterns[auth].id}/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.altaccount.token
: 'Basic ' +
new Buffer(
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
).toString('base64')
)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(403)
expect(res.body.result).to.equal(`error`)
expect(res.body.error).to.equal(`insufficientAccessLevel`)
done()
})
})
/*
it(`${store.icon('person', auth)} Should clone a person (${auth})`, (done) => {
chai
.request(config.api)
.post(`/people/${store.person[auth].id}/clone/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.account.token
: 'Basic ' +
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
'base64'
)
)
.end((err, res) => {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.error).to.equal(`undefined`)
expect(typeof res.body.person.id).to.equal(`number`)
done()
})
})
it(`${store.icon(
'person',
auth
)} Should (not) clone a public person across accounts (${auth})`, (done) => {
chai
.request(config.api)
.post(`/people/${store.person[auth].id}/clone/${auth}`)
.set(
'Authorization',
auth === 'jwt'
? 'Bearer ' + store.altaccount.token
: 'Basic ' +
new Buffer(
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
).toString('base64')
)
.end((err, res) => {
if (store.person[auth].public) {
expect(err === null).to.equal(true)
expect(res.status).to.equal(200)
expect(res.body.result).to.equal(`success`)
expect(typeof res.body.error).to.equal(`undefined`)
expect(typeof res.body.person.id).to.equal(`number`)
} else {
expect(err === null).to.equal(true)
expect(res.status).to.equal(403)
expect(res.body.result).to.equal(`error`)
expect(res.body.error).to.equal(`insufficientAccessLevel`)
}
done()
})
})
// TODO:
// - Clone person
// - Clone person accross accounts of they are public
*/
})
}
}

View file

@ -4,12 +4,17 @@ import chai from 'chai'
import http from 'chai-http'
import { verifyConfig } from '../src/config.mjs'
import { randomString } from '../src/utils/crypto.mjs'
import {
cisFemaleAdult34 as her,
cisMaleAdult42 as him,
} from '../../../packages/models/src/index.mjs'
dotenv.config()
const config = verifyConfig(true)
const expect = chai.expect
chai.use(http)
const people = { her, him }
export const setup = async () => {
// Initial store contents
@ -21,17 +26,20 @@ export const setup = async () => {
email: `test_${randomString()}@${config.tests.domain}`,
language: 'en',
password: randomString(),
people: {},
},
altaccount: {
email: `test_${randomString()}@${config.tests.domain}`,
language: 'en',
password: randomString(),
people: {},
},
icons: {
user: '🧑 ',
jwt: '🎫 ',
key: '🎟️ ',
person: '🧕 ',
pattern: '👕 ',
},
randomString,
}
@ -63,12 +71,12 @@ export const setup = async () => {
}
store[acc].token = result.data.token
store[acc].username = result.data.account.username
store[acc].userid = result.data.account.id
store[acc].id = result.data.account.id
// Create API key
try {
result = await axios.post(
`${store.config.api}/apikey/jwt`,
`${store.config.api}/apikeys/jwt`,
{
name: 'Test API key',
level: 4,
@ -85,6 +93,29 @@ export const setup = async () => {
process.exit()
}
store[acc].apikey = result.data.apikey
// Create people key
for (const name in people) {
try {
result = await axios.post(
`${store.config.api}/people/jwt`,
{
name: `This is ${name} name`,
name: `These are ${name} notes`,
measies: people[name],
},
{
headers: {
authorization: `Bearer ${store[acc].token}`,
},
}
)
} catch (err) {
console.log('Failed at API key creation request', err)
process.exit()
}
store[acc].people[name] = result.data.person
}
}
return { chai, config, expect, store }

View file

@ -184,12 +184,12 @@ export const userTests = async (chai, config, expect, store) => {
})
})
step(`${store.icon('user')} Should login with userid and password`, (done) => {
step(`${store.icon('user')} Should login with id and password`, (done) => {
chai
.request(config.api)
.post('/login')
.send({
username: store.account.userid,
username: store.account.id,
password: store.account.password,
})
.end((err, res) => {

View file

@ -11,13 +11,12 @@ import { designs, plugins } from '../../../config/software/index.mjs'
* srcPkgs: Array of folders in the monorepo/packages that should be aliased
* so they are loaded from source, rather than from a compiled bundle
*/
const config = (site, remarkPlugins=[]) => ({
const config = (site, remarkPlugins = []) => ({
experimental: {
externalDir: true,
},
pageExtensions: [ 'js', 'md', 'mjs' ],
pageExtensions: ['js', 'md', 'mjs'],
webpack: (config, options) => {
// Fixes npm packages that depend on node modules
if (!options.isServer) {
config.resolve.fallback.fs = false
@ -34,35 +33,30 @@ const config = (site, remarkPlugins=[]) => ({
loader: '@mdx-js/loader',
//providerImportSource: '@mdx-js/react',
options: {
remarkPlugins: [
remarkGfm,
...remarkPlugins,
]
}
}
]
remarkPlugins: [remarkGfm, ...remarkPlugins],
},
},
],
})
// YAML support
config.module.rules.push({
test: /\.ya?ml$/,
use: 'yaml-loader'
use: 'yaml-loader',
})
// Fix for nextjs bug #17806
config.module.rules.push({
test: /index.mjs$/,
type: "javascript/auto",
type: 'javascript/auto',
resolve: {
fullySpecified: false
}
fullySpecified: false,
},
})
// Suppress warnings about importing version from package.json
// We'll deal with it in v3 of FreeSewing
config.ignoreWarnings = [
/only default export is available soon/
]
config.ignoreWarnings = [/only default export is available soon/]
// Aliases
config.resolve.alias.shared = path.resolve('../shared/')
@ -75,19 +69,25 @@ const config = (site, remarkPlugins=[]) => ({
// Load designs from source, rather than compiled package
for (const design in designs) {
config.resolve.alias[`@freesewing/${design}$`] = path.resolve(`../../designs/${design}/src/index.mjs`)
config.resolve.alias[`@freesewing/${design}$`] = path.resolve(
`../../designs/${design}/src/index.mjs`
)
}
// Load plugins from source, rather than compiled package
for (const plugin in plugins) {
config.resolve.alias[`@freesewing/${plugin}$`] = path.resolve(`../../plugins/${plugin}/src/index.mjs`)
config.resolve.alias[`@freesewing/${plugin}$`] = path.resolve(
`../../plugins/${plugin}/src/index.mjs`
)
}
// Load these from source, rather than compiled package
for (const pkg of ['core', 'config-helpers', 'i18n', 'models']) {
config.resolve.alias[`@freesewing/${pkg}$`] = path.resolve(`../../packages/${pkg}/src/index.mjs`)
for (const pkg of ['core', 'i18n', 'models', 'snapseries']) {
config.resolve.alias[`@freesewing/${pkg}$`] = path.resolve(
`../../packages/${pkg}/src/index.mjs`
)
}
return config
}
},
})
export default config

View file

@ -4,6 +4,7 @@ import chai from 'chai'
import { timingPlugin } from '@freesewing/plugin-timing'
const expect = chai.expect
const ciTimeout = 10000
/*
* This runs unit tests for pattern drafting
@ -39,7 +40,7 @@ export const testPatternDrafting = (Pattern, log = false) => {
*/
if (family !== 'utilities') {
describe('Draft for humans:', function () {
this.timeout(5000)
this.timeout(ciTimeout)
for (const type of ['cisFemale', 'cisMale']) {
describe(type, () => {
for (const size in adult[type]) {
@ -62,7 +63,7 @@ export const testPatternDrafting = (Pattern, log = false) => {
const fams = { doll, giant }
for (const family of ['doll', 'giant']) {
describe(`Draft for ${family}:`, function () {
this.timeout(5000)
this.timeout(ciTimeout)
for (const type of ['cisFemale', 'cisMale']) {
describe(type, () => {
for (const size in fams[family][type]) {

View file

@ -6,6 +6,7 @@ class TerseReporter {
constructor(runner) {
runner.on(EVENT_TEST_FAIL, (test, err) => {
console.log(` FAIL: ${test.fullTitle()}`)
console.log(err)
})
}
}

2435
yarn.lock

File diff suppressed because it is too large Load diff