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", "profile": "https://github.com/BenJamesBen",
"contributions": [ "contributions": [
"code", "code",
"doc" "doc",
"bug"
] ]
}, },
{ {

View file

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

View file

@ -1,7 +1,7 @@
--- ---
name: 🐛 Bug report name: 🐛 Bug report
description: Report a problem, or something that went wrong description: Report a problem, or something that went wrong
title: "[bug]: " title: '[bug]: '
labels: ["\U0001F41B bug"] labels: ["\U0001F41B bug"]
body: body:
- type: markdown - type: markdown
@ -10,7 +10,7 @@ body:
- type: textarea - type: textarea
id: desc id: desc
attributes: 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 description: Please provide a clear and concise description of the problem you encountered
placeholder: | placeholder: |
When I generate a Simone with my dimensions, the corners of the yoke appear malformed. \ When I generate a Simone with my dimensions, the corners of the yoke appear malformed. \
@ -18,7 +18,7 @@ body:
- type: dropdown - type: dropdown
id: pkg id: pkg
attributes: attributes:
label: "Design / Plugin / Package 🧐" label: 'Design / Plugin / Package 🧐'
description: Do you know what design/plugin/package the bug is in? description: Do you know what design/plugin/package the bug is in?
multiple: true multiple: true
options: options:
@ -51,6 +51,7 @@ body:
- designs/lucy - designs/lucy
- designs/lunetius - designs/lunetius
- designs/noble - designs/noble
- designs/octoplushy
- designs/paco - designs/paco
- designs/penelope - designs/penelope
- designs/plugintest - designs/plugintest
@ -62,7 +63,6 @@ body:
- designs/sven - designs/sven
- designs/tamiko - designs/tamiko
- designs/teagan - designs/teagan
- designs/theo
- designs/tiberius - designs/tiberius
- designs/titan - designs/titan
- designs/trayvon - designs/trayvon
@ -78,9 +78,9 @@ body:
- plugins/plugin-bundle - plugins/plugin-bundle
- plugins/plugin-bust - plugins/plugin-bust
- plugins/plugin-buttons - plugins/plugin-buttons
- plugins/plugin-cutlist
- plugins/plugin-cutonfold - plugins/plugin-cutonfold
- plugins/plugin-dimension - plugins/plugin-dimension
- plugins/plugin-export-dxf
- plugins/plugin-flip - plugins/plugin-flip
- plugins/plugin-gore - plugins/plugin-gore
- plugins/plugin-grainline - plugins/plugin-grainline
@ -94,33 +94,27 @@ body:
- plugins/plugin-sprinkle - plugins/plugin-sprinkle
- plugins/plugin-svgattr - plugins/plugin-svgattr
- plugins/plugin-theme - plugins/plugin-theme
- plugins/plugin-timing
- plugins/plugin-title - plugins/plugin-title
- plugins/plugin-validate
- plugins/plugin-versionfree-svg - plugins/plugin-versionfree-svg
- packages/components
- packages/config-helpers
- packages/core - packages/core
- packages/css-theme
- packages/gatsby-remark-jargon
- packages/i18n - packages/i18n
- packages/models - packages/models
- packages/mui-theme
- packages/new-design - packages/new-design
- packages/pattern-info
- packages/prettier-config - packages/prettier-config
- packages/rehype-highlight-lines
- packages/rehype-jargon - packages/rehype-jargon
- packages/remark-jargon - packages/snapseries
- packages/utils
- type: dropdown - type: dropdown
id: patron id: patron
attributes: attributes:
label: Are you a FreeSewing patron? 😃 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: options:
- "Yes, I am a tier-2 patron ❤️" - 'Yes, I am a tier-2 patron ❤️'
- "Yes, I am a tier-4 patron ❤️ 💙" - 'Yes, I am a tier-4 patron ❤️ 💙'
- "Yes, I am a tier-8 patron ❤️ 💙 💜" - 'Yes, I am a tier-8 patron ❤️ 💙 💜'
- "No, I am not 😞" - 'No, I am not 😞'
validations: validations:
required: true required: true
- type: textarea - type: textarea
@ -133,3 +127,4 @@ body:
value: | value: |
Please keep in mind that **FreeSewing is a community project** that depends on **[your support](https://freesewing.org/community/join/)**. 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 ##### ##### Labeler #####
labelPRBasedOnFilePath: labelPRBasedOnFilePath:
# "label": [ folder or subfolders ] # "label": [ folder or subfolders ]
":package: components": [ packages/components/* ]
":package: config-helpers": [ packages/config-helpers/* ]
":package: core": [ packages/core/* ] ":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: i18n": [ packages/i18n/* ]
":package: models": [ packages/models/* ] ":package: models": [ packages/models/* ]
":package: mui-theme": [ packages/mui-theme/* ]
":package: new-design": [ packages/new-design/* ] ":package: new-design": [ packages/new-design/* ]
":package: pattern-info": [ packages/pattern-info/* ]
":package: prettier-config": [ packages/prettier-config/* ] ":package: prettier-config": [ packages/prettier-config/* ]
":package: rehype-highlight-lines": [ packages/rehype-highlight-lines/* ]
":package: rehype-jargon": [ packages/rehype-jargon/* ] ":package: rehype-jargon": [ packages/rehype-jargon/* ]
":package: remark-jargon": [ packages/remark-jargon/* ] ":package: snapseries": [ packages/snapseries/* ]
":package: utils": [ packages/utils/* ]
":electric_plug: plugin-banner": [ plugins/plugin-banner/* ] ":electric_plug: plugin-banner": [ plugins/plugin-banner/* ]
":electric_plug: plugin-bartack": [ plugins/plugin-bartack/* ] ":electric_plug: plugin-bartack": [ plugins/plugin-bartack/* ]
":electric_plug: plugin-bundle": [ plugins/plugin-bundle/* ] ":electric_plug: plugin-bundle": [ plugins/plugin-bundle/* ]
":electric_plug: plugin-bust": [ plugins/plugin-bust/* ] ":electric_plug: plugin-bust": [ plugins/plugin-bust/* ]
":electric_plug: plugin-buttons": [ plugins/plugin-buttons/* ] ":electric_plug: plugin-buttons": [ plugins/plugin-buttons/* ]
":electric_plug: plugin-cutlist": [ plugins/plugin-cutlist/* ]
":electric_plug: plugin-cutonfold": [ plugins/plugin-cutonfold/* ] ":electric_plug: plugin-cutonfold": [ plugins/plugin-cutonfold/* ]
":electric_plug: plugin-dimension": [ plugins/plugin-dimension/* ] ":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-flip": [ plugins/plugin-flip/* ]
":electric_plug: plugin-gore": [ plugins/plugin-gore/* ] ":electric_plug: plugin-gore": [ plugins/plugin-gore/* ]
":electric_plug: plugin-grainline": [ plugins/plugin-grainline/* ] ":electric_plug: plugin-grainline": [ plugins/plugin-grainline/* ]
@ -37,18 +30,18 @@ labelPRBasedOnFilePath:
":electric_plug: plugin-sprinkle": [ plugins/plugin-sprinkle/* ] ":electric_plug: plugin-sprinkle": [ plugins/plugin-sprinkle/* ]
":electric_plug: plugin-svgattr": [ plugins/plugin-svgattr/* ] ":electric_plug: plugin-svgattr": [ plugins/plugin-svgattr/* ]
":electric_plug: plugin-theme": [ plugins/plugin-theme/* ] ":electric_plug: plugin-theme": [ plugins/plugin-theme/* ]
":electric_plug: plugin-timing": [ plugins/plugin-timing/* ]
":electric_plug: plugin-title": [ plugins/plugin-title/* ] ":electric_plug: plugin-title": [ plugins/plugin-title/* ]
":electric_plug: plugin-validate": [ plugins/plugin-validate/* ]
":electric_plug: plugin-versionfree-svg": [ plugins/plugin-versionfree-svg/* ] ":electric_plug: plugin-versionfree-svg": [ plugins/plugin-versionfree-svg/* ]
":book: documentation": [ markdown/* ] ":book: documentation": [ markdown/* ]
":scroll: scripts": [ scripts/* ] ":scroll: scripts": [ scripts/* ]
":computer: backend": [ sites/backend/* ] ":computer: backend": [ sites/backend/* ]
":computer: dev": [ sites/dev/* ] ":computer: dev": [ sites/dev/* ]
":computer: email": [ sites/email/* ]
":computer: lab": [ sites/lab/* ] ":computer: lab": [ sites/lab/* ]
":computer: org": [ sites/org/* ] ":computer: org": [ sites/org/* ]
":computer: sanity": [ sites/sanity/* ]
":computer: shared": [ sites/shared/* ] ":computer: shared": [ sites/shared/* ]
":computer: strapi": [ sites/strapi/* ]
":computer: svgtopdf": [ sites/svgtopdf/* ]
":test_tube: tests": [ tests/* ] ":test_tube: tests": [ tests/* ]
":gear: configuration": [ "config/*", ".github/*" ] ":gear: configuration": [ "config/*", ".github/*" ]
":shirt: aaron": [ designs/aaron/* ] ":shirt: aaron": [ designs/aaron/* ]
@ -93,7 +86,6 @@ labelPRBasedOnFilePath:
":shirt: sven": [ designs/sven/* ] ":shirt: sven": [ designs/sven/* ]
":shirt: tamiko": [ designs/tamiko/* ] ":shirt: tamiko": [ designs/tamiko/* ]
":shirt: teagan": [ designs/teagan/* ] ":shirt: teagan": [ designs/teagan/* ]
":shirt: theo": [ designs/theo/* ]
":shirt: tiberius": [ designs/tiberius/* ] ":shirt: tiberius": [ designs/tiberius/* ]
":shirt: titan": [ designs/titan/* ] ":shirt: titan": [ designs/titan/* ]
":shirt: trayvon": [ designs/trayvon/* ] ":shirt: trayvon": [ designs/trayvon/* ]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,136 +9,136 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<table> <table>
<tbody> <tbody>
<tr> <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"><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"><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"><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"><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"><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"><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="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>
<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"><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"><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"><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"><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" 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"><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"><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/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>
<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"><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"><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"><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"><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"><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"><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/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>
<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"><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"><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"><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"><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"><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"><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://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>
<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"><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"><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"><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"><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"><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"><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/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>
<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"><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"><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"><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"><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"><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"><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="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>
<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"><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"><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"><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"><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"><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"><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/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>
<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"><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"><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"><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"><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"><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"><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://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>
<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"><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"><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"><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"><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"><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"><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://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>
<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"><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"><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"><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"><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"><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"><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/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>
<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"><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"><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"><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"><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"><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"><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/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>
<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"><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"><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"><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"><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"><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"><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/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>
<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"><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"><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"><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"><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"><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"><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://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>
<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"><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"><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"><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"><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"><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"><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/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>
<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"><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"><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"><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/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> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -6,7 +6,7 @@ import { designs, plugins, packages } from './software/index.mjs'
* order. This file takes care of that * 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 blocks = ['brian', 'titan', 'bella', 'breanna']
const extended = ['bent', 'simon', 'carlton', 'ursula'] const extended = ['bent', 'simon', 'carlton', 'ursula']
const last = ['i18n'] const last = ['i18n']

View file

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

View file

@ -8,13 +8,6 @@ core:
- patterns - patterns
- sewing - sewing
- sewing patterns - sewing patterns
components:
- react
css-theme:
- css
- scss
- sass
- theme
design: design:
- design - design
- diy - diy
@ -40,10 +33,6 @@ models:
- fashion - fashion
- measurements - measurements
- sizes - sizes
mui-theme:
- material-ui
- react
- theme
other: other:
- design - design
- diy - 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", "core": "A library for creating made-to-measure sewing patterns",
"i18n": "Translations for the FreeSewing project", "i18n": "Translations for the FreeSewing project",
"models": "Body measurements data for a range of default sizes", "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 { __loadPatternDefaults } from './config.mjs'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
const DISTANCE_DEBUG = false
////////////////////////////////////////////// //////////////////////////////////////////////
// CONSTRUCTOR // // CONSTRUCTOR //
////////////////////////////////////////////// //////////////////////////////////////////////
@ -474,14 +476,25 @@ Pattern.prototype.__addPartOptions = function (part) {
// Keep design parts immutable in the pattern or risk subtle bugs // Keep design parts immutable in the pattern or risk subtle bugs
this.config.options[optionName] = Object.freeze(part.options[optionName]) this.config.options[optionName] = Object.freeze(part.options[optionName])
this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``) this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``)
} else if ( } else {
this.__mutated.optionDistance[optionName] < this.__mutated.partDistance[part.name] 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.config.options[optionName] = part.options[optionName]
this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``) this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
} }
} }
} }
}
if (part.from) this.__addPartOptions(part.from) if (part.from) this.__addPartOptions(part.from)
if (part.after) { if (part.after) {
if (Array.isArray(part.after)) { if (Array.isArray(part.after)) {
@ -1256,41 +1269,104 @@ Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
} }
} }
distance++ distance++
if (DISTANCE_DEBUG) this.store.log.debug('Distance incremented to ' + distance)
for (const part of this.designConfig.parts) { 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 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)) { 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 // Hide when hideAll is set
if (part.hideAll) this.__mutated.partHide[part.name] = true if (part.hideAll) this.__mutated.partHide[part.name] = true
// Inject (from) // Inject (from)
if (part.from) { if (part.from) {
if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "from:"')
this.__setFromHide(part, name, part.from.name) this.__setFromHide(part, name, part.from.name)
this.__designParts[part.from.name] = part.from this.__designParts[part.from.name] = part.from
this.__inject[name] = part.from.name 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) // Simple dependency (after)
if (part.after) { if (part.after) {
if (DISTANCE_DEBUG) this.store.log.debug('Processing ' + part.name + ' "after:"')
if (Array.isArray(part.after)) { if (Array.isArray(part.after)) {
for (const dep of part.after) { for (const dep of part.after) {
this.__setAfterHide(part, name, dep.name) this.__setAfterHide(part, name, dep.name)
this.__mutated.partDistance[dep.name] = distance
this.__designParts[dep.name] = dep this.__designParts[dep.name] = dep
this.__addDependency(name, part, 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 { } else {
this.__setAfterHide(part, name, part.after.name) this.__setAfterHide(part, name, part.after.name)
this.__mutated.partDistance[part.after.name] = distance
this.__designParts[part.after.name] = part.after this.__designParts[part.after.name] = part.after
this.__addDependency(name, part, 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? // Did we discover any new dependencies?
const len = Object.keys(this.__designParts).length const len = Object.keys(this.__designParts).length
// If so, resolve recursively // 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) 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) value = Number(value)
return value return value
} catch { } catch {
this.log.error( log.error(
`Called \`${method}(${param})\` but \`${param}\` is not a number nor can it be cast to one` `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 = { const part = {
name: 'test', name: 'test',
draft: ({ getId, part }) => { draft: ({ getId, part }) => {
console.log(getId)
id = getId() id = getId()
id = getId() id = getId()
id = getId() id = getId()

View file

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

View file

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

View file

@ -1,25 +1,37 @@
const fs = require('fs') const fs = require('fs')
const path = require('path'); const path = require('path')
const spawn = require('child_process').spawn const spawn = require('child_process').spawn
const projectRoot = path.normalize(path.join(__dirname, '..')); const projectRoot = path.normalize(path.join(__dirname, '..'))
const outputLog = path.join(projectRoot, '.test-failures.log'); const outputLog = path.join(projectRoot, '.test-failures.log')
const collectorScript = path.join(projectRoot, 'scripts', 'test-failure-collector.js'); const collectorScript = path.join(projectRoot, 'scripts', 'test-failure-collector.js')
// Start with a fresh output log on each run. // Start with a fresh output log on each run.
if (fs.existsSync(outputLog)) { if (fs.existsSync(outputLog)) {
fs.unlinkSync(outputLog); fs.unlinkSync(outputLog)
} }
// Run all tests, specifying the collector script. // Run all tests, specifying the collector script.
spawn('lerna', ['run', '--no-bail', 'testci', '--stream', '--parallel', '--loglevel', 'error', '--', '--file', `${collectorScript}`, '--no-warnings'], { stdio: 'inherit' }) spawn(
.on('exit', function(code) { '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 a failure occurred, the log file will have been created. Print it.
if (fs.existsSync(outputLog)) { if (fs.existsSync(outputLog)) {
console.error(fs.readFileSync(outputLog, 'utf8').trim()); console.error(fs.readFileSync(outputLog, 'utf8').trim())
} }
// Propagate the exit code. // 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)", "author": "Joost De Cock <joost@joost.at> (https://github.com/joostdecock)",
"homepage": "https://freesewing.org/", "homepage": "https://freesewing.org/",
"repository": "github:freesewing/freesewing", "repository": "github:freesewing/freesewing",
"license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/freesewing/freesewing/issues" "url": "https://github.com/freesewing/freesewing/issues"
}, },
@ -30,10 +29,13 @@
"crypto": "^1.0.1", "crypto": "^1.0.1",
"express": "4.18.2", "express": "4.18.2",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"passport": "^0.6.0",
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
"passport-jwt": "^4.0.0",
"pino": "^8.7.0" "pino": "^8.7.0"
}, },
"devDependencies": { "devDependencies": {
"chai-http": "^4.3.0",
"mocha": "^10.1.0", "mocha": "^10.1.0",
"mocha-steps": "^1.3.0", "mocha-steps": "^1.3.0",
"prisma": "4.5.0" "prisma": "4.5.0"

View file

@ -73,9 +73,12 @@ model Pattern {
data String data String
design String design String
img String? img String?
name String @default("")
notes String
person Person? @relation(fields: [personId], references: [id]) person Person? @relation(fields: [personId], references: [id])
personId Int? personId Int?
notes String public Boolean @default(false)
settings String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId Int userId Int
updatedAt DateTime @updatedAt 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 { ApikeyModel } from '../models/apikey.mjs'
import { UserModel } from '../models/user.mjs' import { UserModel } from '../models/user.mjs'
export function ApikeyController() {} export function ApikeysController() {}
/* /*
* Create API key * Create API key
@ -12,7 +12,7 @@ export function ApikeyController() {}
* This is the endpoint that handles creation of API keys/tokens * This is the endpoint that handles creation of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey * 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) const Apikey = new ApikeyModel(tools)
await Apikey.create(req) 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 * This is the endpoint that handles creation of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey * 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) const Apikey = new ApikeyModel(tools)
await Apikey.guardedRead(req) await Apikey.guardedRead(req)
@ -39,7 +39,7 @@ ApikeyController.prototype.read = async (req, res, tools) => {
* request * request
* See: https://freesewing.dev/reference/backend/api/apikey * 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 User = new UserModel(tools)
const Apikey = new ApikeyModel(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 * This is the endpoint that handles removal of API keys/tokens
* See: https://freesewing.dev/reference/backend/api/apikey * 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) const Apikey = new ApikeyModel(tools)
await Apikey.guardedDelete(req) 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' import { PersonModel } from '../models/person.mjs'
export function PersonController() {} export function PeopleController() {}
/* /*
* Create a person for the authenticated user * Create a person for the authenticated user
* See: https://freesewing.dev/reference/backend/api * 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) const Person = new PersonModel(tools)
await Person.guardedCreate(req) await Person.guardedCreate(req)
@ -17,7 +17,7 @@ PersonController.prototype.create = async (req, res, tools) => {
* Read a person * Read a person
* See: https://freesewing.dev/reference/backend/api * 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) const Person = new PersonModel(tools)
await Person.guardedRead(req) await Person.guardedRead(req)
@ -28,7 +28,7 @@ PersonController.prototype.read = async (req, res, tools) => {
* Update a person * Update a person
* See: https://freesewing.dev/reference/backend/api * 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) const Person = new PersonModel(tools)
await Person.guardedUpdate(req) await Person.guardedUpdate(req)
@ -39,7 +39,7 @@ PersonController.prototype.update = async (req, res, tools) => {
* Remove a person * Remove a person
* See: https://freesewing.dev/reference/backend/api * 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) const Person = new PersonModel(tools)
await Person.guardedDelete(req) await Person.guardedDelete(req)
@ -50,7 +50,7 @@ PersonController.prototype.delete = async (req, res, tools) => {
* Clone a person * Clone a person
* See: https://freesewing.dev/reference/backend/api * 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) const Person = new PersonModel(tools)
await Person.guardedClone(req) await Person.guardedClone(req)

View file

@ -1,6 +1,6 @@
import { UserModel } from '../models/user.mjs' import { UserModel } from '../models/user.mjs'
export function UserController() {} export function UsersController() {}
/* /*
* Signup * Signup
@ -8,7 +8,7 @@ export function UserController() {}
* This is the endpoint that handles account signups * This is the endpoint that handles account signups
* See: https://freesewing.dev/reference/backend/api * 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) const User = new UserModel(tools)
await User.guardedCreate(req) 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 * This is the endpoint that fully unlocks the account if the user gives their consent
* See: https://freesewing.dev/reference/backend/api * 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) const User = new UserModel(tools)
await User.confirm(req) 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 * This is the endpoint that provides traditional username/password login
* See: https://freesewing.dev/reference/backend/api * 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) const User = new UserModel(tools)
await User.passwordLogin(req) await User.passwordLogin(req)
@ -46,7 +46,7 @@ UserController.prototype.login = async function (req, res, tools) {
* *
* See: https://freesewing.dev/reference/backend/api * 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) const User = new UserModel(tools)
await User.guardedRead({ id: req.user.uid }, req) 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 * 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) const User = new UserModel(tools)
await User.guardedRead({ id: req.user.uid }, req) await User.guardedRead({ id: req.user.uid }, req)
await User.guardedUpdate(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 // Prepare data
const data = { name: body.name } 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 if (body.notes || typeof body.notes === 'string') data.notes = body.notes
else data.notes = '--'
if (body.public === true) data.public = true if (body.public === true) data.public = true
if (body.measies) data.measies = this.sanitizeMeasurements(body.measies) if (body.measies) data.measies = this.sanitizeMeasurements(body.measies)
data.imperial = body.imperial === true ? true : false data.imperial = body.imperial === true ? true : false

View file

@ -223,9 +223,9 @@ UserModel.prototype.guardedCreate = async function ({ body }) {
* Login based on username + password * Login based on username + password
*/ */
UserModel.prototype.passwordLogin = async function (req) { UserModel.prototype.passwordLogin = async function (req) {
if (Object.keys(req.body) < 1) return this.setReponse(400, 'postBodyMissing') if (Object.keys(req.body) < 1) return this.setResponse(400, 'postBodyMissing')
if (!req.body.username) return this.setReponse(400, 'usernameMissing') if (!req.body.username) return this.setResponse(400, 'usernameMissing')
if (!req.body.password) return this.setReponse(400, 'passwordMissing') if (!req.body.password) return this.setResponse(400, 'passwordMissing')
await this.find(req.body) await this.find(req.body)
if (!this.exists) { if (!this.exists) {
@ -255,7 +255,7 @@ UserModel.prototype.passwordLogin = async function (req) {
* Confirms a user account * Confirms a user account
*/ */
UserModel.prototype.confirm = async function ({ body, params }) { 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 (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
if (!body.consent || typeof body.consent !== 'number' || body.consent < 1) if (!body.consent || typeof body.consent !== 'number' || body.consent < 1)
return this.setResponse(400, 'consentRequired') 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 { apikeysRoutes } from './apikeys.mjs'
import { userRoutes } from './user.mjs' import { usersRoutes } from './users.mjs'
import { personRoutes } from './person.mjs' import { peopleRoutes } from './people.mjs'
import { patternsRoutes } from './patterns.mjs'
export const routes = { export const routes = {
apikeyRoutes, apikeysRoutes,
userRoutes, usersRoutes,
personRoutes, 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 jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }] const bsc = ['basic', { session: false }]
export function personRoutes(tools) { export function peopleRoutes(tools) {
const { app, passport } = tools const { app, passport } = tools
// Create person // Create person
app.post('/people/jwt', passport.authenticate(...jwt), (req, res) => 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) => app.post('/people/key', passport.authenticate(...bsc), (req, res) =>
Person.create(req, res, tools) People.create(req, res, tools)
) )
// Clone person // Clone person
app.post('/people/:id/clone/jwt', passport.authenticate(...jwt), (req, res) => 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) => app.post('/people/:id/clone/key', passport.authenticate(...bsc), (req, res) =>
Person.clone(req, res, tools) People.clone(req, res, tools)
) )
// Read person // Read person
app.get('/people/:id/jwt', passport.authenticate(...jwt), (req, res) => 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) => app.get('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
Person.read(req, res, tools) People.read(req, res, tools)
) )
// Update person // Update person
app.put('/people/:id/jwt', passport.authenticate(...jwt), (req, res) => 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) => app.put('/people/:id/key', passport.authenticate(...bsc), (req, res) =>
Person.update(req, res, tools) People.update(req, res, tools)
) )
// Delete person // Delete person
app.delete('/people/:id/jwt', passport.authenticate(...jwt), (req, res) => 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) => 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 jwt = ['jwt', { session: false }]
const bsc = ['basic', { session: false }] const bsc = ['basic', { session: false }]
export function userRoutes(tools) { export function usersRoutes(tools) {
const { app, passport } = tools const { app, passport } = tools
// Sign up // Sign up
app.post('/signup', (req, res) => User.signup(req, res, tools)) app.post('/signup', (req, res) => Users.signup(req, res, tools))
// Confirm account // 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 // Login
app.post('/login', (req, res) => User.login(req, res, tools)) app.post('/login', (req, res) => Users.login(req, res, tools))
// Read current jwt // Read current jwt
app.get('/whoami/jwt', passport.authenticate(...jwt), (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) => User.whoami(req, res, tools)) app.get('/account/jwt', passport.authenticate(...jwt), (req, res) =>
app.get('/account/key', passport.authenticate(...bsc), (req, res) => User.whoami(req, res, tools)) Users.whoami(req, res, tools)
)
app.get('/account/key', passport.authenticate(...bsc), (req, res) =>
Users.whoami(req, res, tools)
)
// Update account // Update account
app.put('/account/jwt', passport.authenticate(...jwt), (req, res) => User.update(req, res, tools)) app.put('/account/jwt', passport.authenticate(...jwt), (req, res) =>
app.put('/account/key', passport.authenticate(...bsc), (req, res) => User.update(req, res, tools)) 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 setUserAvatar = async (id, data) => setAvatar('user', id, data)
export const setPersonAvatar = async (id, data) => setAvatar('person', 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) { export async function setAvatar(type, id, data) {
// Step 1: Upload the image as asset // Step 1: Upload the image as asset
const [contentType, binary] = b64ToBinaryWithType(data) 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) => { step(`${store.icon('key', 'jwt')} Create API Key (jwt)`, (done) => {
chai chai
.request(config.api) .request(config.api)
.post('/apikey/jwt') .post('/apikeys/jwt')
.set('Authorization', 'Bearer ' + store.account.token) .set('Authorization', 'Bearer ' + store.account.token)
.send({ .send({
name: 'Test API key', 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) => { step(`${store.icon('key', 'key')} Create API Key (key)`, (done) => {
chai chai
.request(config.api) .request(config.api)
.post('/apikey/key') .post('/apikeys/key')
.auth(store.apikey1.key, store.apikey1.secret) .auth(store.apikey1.key, store.apikey1.secret)
.send({ .send({
name: 'Test API key with key', 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) => { step(`${store.icon('key', 'key')} Read API key (key)`, (done) => {
chai chai
.request(config.api) .request(config.api)
.get(`/apikey/${store.apikey1.key}/key`) .get(`/apikeys/${store.apikey1.key}/key`)
.auth(store.apikey2.key, store.apikey2.secret) .auth(store.apikey2.key, store.apikey2.secret)
.end((err, res) => { .end((err, res) => {
expect(res.status).to.equal(200) 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) => { step(`${store.icon('key', 'jwt')} Read API key (jwt)`, (done) => {
chai chai
.request(config.api) .request(config.api)
.get(`/apikey/${store.apikey2.key}/jwt`) .get(`/apikeys/${store.apikey2.key}/jwt`)
.set('Authorization', 'Bearer ' + store.account.token) .set('Authorization', 'Bearer ' + store.account.token)
.end((err, res) => { .end((err, res) => {
expect(res.status).to.equal(200) 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) => { step(`${store.icon('key', 'key')} Remove API key (key)`, (done) => {
chai chai
.request(config.api) .request(config.api)
.delete(`/apikey/${store.apikey2.key}/key`) .delete(`/apikeys/${store.apikey2.key}/key`)
.auth(store.apikey2.key, store.apikey2.secret) .auth(store.apikey2.key, store.apikey2.secret)
.end((err, res) => { .end((err, res) => {
expect(res.status).to.equal(204) 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) => { step(`${store.icon('key', 'jwt')} Remove API key (jwt)`, (done) => {
chai chai
.request(config.api) .request(config.api)
.delete(`/apikey/${store.apikey1.key}/jwt`) .delete(`/apikeys/${store.apikey1.key}/jwt`)
.set('Authorization', 'Bearer ' + store.account.token) .set('Authorization', 'Bearer ' + store.account.token)
.end((err, res) => { .end((err, res) => {
expect(res.status).to.equal(204) expect(res.status).to.equal(204)

View file

@ -2,6 +2,7 @@ import { userTests } from './user.mjs'
import { accountTests } from './account.mjs' import { accountTests } from './account.mjs'
import { apikeyTests } from './apikey.mjs' import { apikeyTests } from './apikey.mjs'
import { personTests } from './person.mjs' import { personTests } from './person.mjs'
import { patternTests } from './pattern.mjs'
import { setup } from './shared.mjs' import { setup } from './shared.mjs'
const runTests = async (...params) => { const runTests = async (...params) => {
@ -9,6 +10,7 @@ const runTests = async (...params) => {
await apikeyTests(...params) await apikeyTests(...params)
await accountTests(...params) await accountTests(...params)
await personTests(...params) await personTests(...params)
await patternTests(...params)
} }
// Load initial data required for tests // 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 http from 'chai-http'
import { verifyConfig } from '../src/config.mjs' import { verifyConfig } from '../src/config.mjs'
import { randomString } from '../src/utils/crypto.mjs' import { randomString } from '../src/utils/crypto.mjs'
import {
cisFemaleAdult34 as her,
cisMaleAdult42 as him,
} from '../../../packages/models/src/index.mjs'
dotenv.config() dotenv.config()
const config = verifyConfig(true) const config = verifyConfig(true)
const expect = chai.expect const expect = chai.expect
chai.use(http) chai.use(http)
const people = { her, him }
export const setup = async () => { export const setup = async () => {
// Initial store contents // Initial store contents
@ -21,17 +26,20 @@ export const setup = async () => {
email: `test_${randomString()}@${config.tests.domain}`, email: `test_${randomString()}@${config.tests.domain}`,
language: 'en', language: 'en',
password: randomString(), password: randomString(),
people: {},
}, },
altaccount: { altaccount: {
email: `test_${randomString()}@${config.tests.domain}`, email: `test_${randomString()}@${config.tests.domain}`,
language: 'en', language: 'en',
password: randomString(), password: randomString(),
people: {},
}, },
icons: { icons: {
user: '🧑 ', user: '🧑 ',
jwt: '🎫 ', jwt: '🎫 ',
key: '🎟️ ', key: '🎟️ ',
person: '🧕 ', person: '🧕 ',
pattern: '👕 ',
}, },
randomString, randomString,
} }
@ -63,12 +71,12 @@ export const setup = async () => {
} }
store[acc].token = result.data.token store[acc].token = result.data.token
store[acc].username = result.data.account.username store[acc].username = result.data.account.username
store[acc].userid = result.data.account.id store[acc].id = result.data.account.id
// Create API key // Create API key
try { try {
result = await axios.post( result = await axios.post(
`${store.config.api}/apikey/jwt`, `${store.config.api}/apikeys/jwt`,
{ {
name: 'Test API key', name: 'Test API key',
level: 4, level: 4,
@ -85,6 +93,29 @@ export const setup = async () => {
process.exit() process.exit()
} }
store[acc].apikey = result.data.apikey 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 } 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 chai
.request(config.api) .request(config.api)
.post('/login') .post('/login')
.send({ .send({
username: store.account.userid, username: store.account.id,
password: store.account.password, password: store.account.password,
}) })
.end((err, res) => { .end((err, res) => {

View file

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

View file

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

View file

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

2435
yarn.lock

File diff suppressed because it is too large Load diff