diff --git a/config/software/designs.json b/config/software/designs.json
index 75abc902887..03ad76be50b 100644
--- a/config/software/designs.json
+++ b/config/software/designs.json
@@ -284,6 +284,13 @@
"difficulty": 1,
"tags": [ "tops", "historic" ]
},
+ "unice": {
+ "description": "A FreeSewing pattern for a basic, highly-customizable underwear pattern",
+ "code": [ "Anna Puk", "Natalia Sayang" ],
+ "design": [ "Anna Puk", "Natalia Sayang" ],
+ "difficulty": 1,
+ "tags": [ "bottoms", "underwear" ]
+ },
"ursula": {
"description": "A FreeSewing pattern for a basic, highly-customizable underwear pattern",
"code": "Natalia Sayang",
diff --git a/designs/unice/CHANGELOG.md b/designs/unice/CHANGELOG.md
new file mode 100644
index 00000000000..6b458471bea
--- /dev/null
+++ b/designs/unice/CHANGELOG.md
@@ -0,0 +1,15 @@
+# Change log for: @freesewing/unice
+
+
+## 2.21.0 (2022-06-27)
+
+### Added
+
+ - Unice is an underwear pattern
+
+
+This is the **initial release**, and the start of this change log.
+
+> Prior to version 2, FreeSewing was not a JavaScript project.
+> As such, that history is out of scope for this change log.
+
diff --git a/designs/unice/README.md b/designs/unice/README.md
new file mode 100644
index 00000000000..3c02b12a2d2
--- /dev/null
+++ b/designs/unice/README.md
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+
+
+
+
+
+
+# @freesewing/unice
+
+A FreeSewing pattern for a basic, highly-customizable underwear pattern
+
+
+
+
+> #### Note: Version 3 is a work in progress
+>
+> We are working on a new major version (v3) but it is not ready for prime-time.
+> For production use, please refer to our v2 packages (the `latest` on NPM)
+> or [the `v2` branch in our monorepo](https://github.com/freesewing/freesewing/tree/v2).
+
+## What am I looking at? π€
+
+This repository is our *monorepo* holding all our NPM designs, plugins, other NPM packages, and (web)sites.
+
+This folder holds: @freesewing/unice
+
+If you're not entirely sure what to do or how to start, type this command:
+
+```
+npm run tips
+```
+
+> If you don't want to set up a dev environment, you can run it in your browser:
+>
+> [](https://gitpod.io/#https://github.com/freesewing/freesewing)
+>
+> We recommend that you fork our repository and then
+> put `gitpod.io/# to start up a browser-based dev environment of your own.
+
+## About FreeSewing π
+
+Where the world of makers and developers collide, that's where you'll find FreeSewing.
+
+If you're a maker, checkout [freesewing.org](https://freesewing.org/) where you can generate
+our sewing patterns adapted to your measurements.
+
+If you're a developer, our documentation is on [freesewing.dev](https://freesewing.dev/).
+Our [core library](https://freesewing.dev/reference/api/) is a *batteries-included* toolbox
+for parametric design of sewing patterns. But we also provide a range
+of [plugins](https://freesewing.dev/reference/plugins/) that further extend the
+functionality of the platform.
+
+If you have NodeJS installed, you can try it right now by running:
+
+```bash
+npx create-freesewing-pattern
+```
+
+Or, consult our getting started guides
+for [Linux](https://freesewing.dev/tutorials/getting-started-linux/),
+[MacOS](https://freesewing.dev/tutorials/getting-started-mac/),
+or [Windows](https://freesewing.dev/tutorials/getting-started-windows/).
+
+We also have a [pattern design tutorial](https://freesewing.dev/tutorials/pattern-design/) that
+walks you through your first parametric design,
+and [a friendly community](https://freesewing.org/community/where/) with
+people who can help you when you get stuck.
+
+## Support FreeSewing: Become a patron π₯°
+
+FreeSewing is an open source project run by a community,
+and financially supported by our patrons.
+
+If you feel what we do is worthwhile, and you can spend a few coind without
+hardship, then you should [join us and become a patron](https://freesewing.org/community/join).
+
+## Links π©βπ»
+
+ - π» Makers website: [freesewing.org](https://freesewing.org)
+ - π» Developers website: [freesewing.dev](https://freesewing.dev)
+ - π¬ Chat: On Discord via [discord.freesewing.org](https://discord.freesewing.org/)
+ - β
Todo list/Kanban board: On Github via [todo.freesewing.org](https://todo.freesewing.org/)
+ - π¦ Twitter: [@freesewing_org](https://twitter.com/freesewing_org)
+ - π· Instagram: [@freesewing_org](https://instagram.com/freesewing_org)
+
+## License: MIT π€
+
+Β© [Joost De Cock](https://github.com/joostdecock).
+See [the license file](https://github.com/freesewing/freesewing/blob/develop/LICENSE) for details.
+
+## Where to get help π€―
+
+Our [chatrooms on Discord](https://chat.freesewing.org/) are the best place to ask questions,
+share your feedback, or just hang out.
+
+If you want to report a problem, please [create an issue](https://github.com/freesewing/freesewing/issues/new).
+
+
+
+## Contributors β¨
+
+Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+
+
+
+
+
+
+
+
+
+
+This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
+
diff --git a/designs/unice/build.mjs b/designs/unice/build.mjs
new file mode 100644
index 00000000000..c5581c1e4a2
--- /dev/null
+++ b/designs/unice/build.mjs
@@ -0,0 +1,50 @@
+/* This script will build the package with esbuild */
+import esbuild from 'esbuild'
+import pkg from './package.json' assert { type: 'json' }
+
+// Create banner based on package info
+const banner = `/**
+ * ${pkg.name} | v${pkg.version}
+ * ${pkg.description}
+ * (c) ${new Date().getFullYear()} ${pkg.author}
+ * @license ${pkg.license}
+ */`
+
+// Shared esbuild options
+const options = {
+ banner: { js: banner },
+ bundle: true,
+ entryPoints: ['src/index.mjs'],
+ format: 'esm',
+ outfile: 'dist/index.mjs',
+ external: ["@freesewing"],
+ metafile: process.env.VERBOSE ? true : false,
+ minify: process.env.NO_MINIFY ? false : true,
+ sourcemap: true,
+}
+
+// Let esbuild generate the build
+let result
+(async () => {
+ result = await esbuild.build(options).catch(() => process.exit(1))
+
+ if (process.env.VERBOSE) {
+ const info = await esbuild.analyzeMetafile(result.metafile)
+ console.log(info)
+ }
+
+ // Also build a version that has all dependencies bundled
+ // This makes it easy to run tests
+ await esbuild
+ .build({
+ ...options,
+ minify: false,
+ keepNames: true,
+ sourcemap: false,
+ outfile: 'tests/dist/index.mjs',
+ format: 'esm',
+ external: [],
+ })
+ .catch(() => process.exit(1))
+
+})()
diff --git a/designs/unice/config/index.js b/designs/unice/config/index.js
new file mode 100644
index 00000000000..25bc838e8cf
--- /dev/null
+++ b/designs/unice/config/index.js
@@ -0,0 +1,62 @@
+import { config as ursulaConfig } from '@freesewing/ursula'
+import pkg from '../package.json' assert { type: 'json' }
+
+const { version } = pkg
+const design = ['Anna Puk', 'Natalia Sayang']
+
+const config = {
+ ...ursulaConfig,
+ version,
+ design,
+ code: design,
+ name: 'unice',
+ inject: {
+ front: 'ursulaFront',
+ back: 'ursulaBack',
+ gusset: 'ursulaGusset',
+ front2: 'front',
+ back2: 'back',
+ gusset2: 'gusset',
+ },
+ hide: ['ursulaBack', 'ursulaFront', 'ursulaGusset','front', 'back', 'gusset'],
+ parts: ['front','back','gusset','elastic','front2','back2','gusset2'],
+ optionalMeasurements: ['crossSeam','crossSeamFront'],
+ measurements: ['waist', 'seat', 'waistToSeat', 'waistToUpperLeg','hips','waistToHips'],
+ optionGroups: {
+ ...ursulaConfig.optionGroups,
+ fit: [
+ 'fabricStretchX',
+ 'fabricStretchY',
+ 'adjustStretch',
+ 'elasticStretch',
+ 'useCrossSeam',
+ 'gussetWidth',
+ 'gussetLength'
+ ],
+ },
+ dependencies: {
+ back: 'front',
+ gusset: 'back',
+ elastic: 'gusset',
+ front2: 'elastic',
+ back2: 'elastic',
+ gusset2: 'elastic',
+ },
+ options: {
+ ...ursulaConfig.options,
+ gussetShift: 0.015, // fraction of seat circumference - could be an advanced option?
+ gussetWidth: { pct: 7.2, min: 2, max: 12 }, // Gusset width in relation to waist-to-upperleg
+ fabricStretchX: { pct: 15, min: 0, max: 100 }, // horizontal stretch (range set wide for beta testing)
+ fabricStretchY: {pct: 0, min: 0, max: 100 }, // vertical stretch (range set wide for beta testing)
+ rise: { pct: 60, min: 30, max: 100 }, // extending rise beyond 100% would require adapting paths.sideLeft!
+ legOpening: { pct: 45, min: 5, max: 85 },
+ // booleans
+ useCrossSeam: { bool: true },
+ adjustStretch: {bool: true}, // to not stretch fabric to the limits
+ }
+}
+
+//delete config.options.fabricStretch
+
+export default config
+
diff --git a/designs/unice/data.mjs b/designs/unice/data.mjs
new file mode 100644
index 00000000000..510883477b0
--- /dev/null
+++ b/designs/unice/data.mjs
@@ -0,0 +1,4 @@
+// This file is auto-generated | All changes you make will be overwritten.
+export const name = "@freesewing/unice"
+export const version = "3.0.0-alpha.0"
+export const data = { name, version }
diff --git a/designs/unice/example/.babelrc b/designs/unice/example/.babelrc
new file mode 100644
index 00000000000..6e3090a4956
--- /dev/null
+++ b/designs/unice/example/.babelrc
@@ -0,0 +1,10 @@
+{
+ "plugins": [
+ ["prismjs", {
+ "languages": ["javascript", "css", "markup"],
+ "plugins": ["line-numbers"],
+ "theme": "twilight",
+ "css": true
+ }]
+ ]
+}
diff --git a/designs/unice/example/README.md b/designs/unice/example/README.md
new file mode 100644
index 00000000000..73d3153ee6f
--- /dev/null
+++ b/designs/unice/example/README.md
@@ -0,0 +1,96 @@
+
+
+
+FreeSewing v2
+
+A JavaScript library for made-to-measure sewing patterns
+
+
+
+
+
+
+
+# unice example
+
+This project was bootstrapped with [Create Freesewing Pattern](https://en.freesewing.dev/create-freesewing-pattern):
+
+```js
+npm init freesewing-pattern
+```
+
+This example folder is part of the local development environment.
+It is **not** part of the pattern's source code.
+
+To run this example, follow these steps:
+
+ - In the folder above this one, run: `yarn start` (or `npm start`)
+ - Then, in new terminal, run the same command in this folder: `yarn start` (or `npm start`)
+
+This will spin up the development environment, similar to [our online demo](https://unice.freesewing.dev/).
+
+## About FreeSewing π€
+
+Where the world of makers and developers collide, that's where you'll find FreeSewing.
+
+Our [core library](https://freesewing.dev/en/freesewing) is a *batteries-included* toolbox
+for parametric design of sewing patterns. It's a modular system (check our list
+of [plugins](https://freesewing.dev/en/plugins) and getting started is as simple as:
+
+```bash
+npm init freesewing-pattern
+```
+
+The [getting started] section on [freesewing.dev](https://freesewing.dev/) is a good
+entrypoint to our documentation, but you'll find a lot more there, including
+our [API documentation](https://freesewing.dev/en/freesewing/api),
+as well as [examples](https://freesewing.dev/en/freesewing/examples),
+and [best practices](https://freesewing.dev/en/do).
+
+If you're a maker, checkout [freesewing.org](https://freesewing/) where you can generate
+our sewing patterns adapted to your measurements.
+
+## Support FreeSewing: Become a patron π₯°
+
+FreeSewing is an open source project run by a community,
+and financially supported by our patrons.
+
+If you feel what we do is worthwhile, you too
+should [become a patron](https://freesewing.org/patrons/join).
+
+## Links π©βπ»
+
+ - π» Makers website: [freesewing.org](https://freesewing.org)
+ - π» Developers website: [freesewing.dev](https://freesewing.org)
+ - π¬ Chat: [gitter.im/freesewing](https://gitter.im/freesewing/freesewing)
+ - π¦ Twitter: [@freesewing_org](https://twitter.com/freesewing_org)
+ - π· Instagram: [@freesewing_org](https://instagram.com/freesewing_org)
+
+## License: MIT π€
+
+Β© [Joost De Cock](https://github.com/joostdecock).
+See [the license file](https://github.com/freesewing/freesewing/blob/develop/LICENSE) for details.
+
+## Where to get help π€―
+
+Our [chatroom on Gitter](https://gitter.im) is the best place to ask questions,
+share your feedback, or just hang out.
+
+If you want to report a problem, please [create an issue](https://github.com/freesewing/freesewing/issues/new).
+
diff --git a/designs/unice/example/netlify.toml b/designs/unice/example/netlify.toml
new file mode 100644
index 00000000000..9890e6c62f2
--- /dev/null
+++ b/designs/unice/example/netlify.toml
@@ -0,0 +1,9 @@
+[build]
+ base = "designs/unice/example"
+ publish = "build"
+ command = "npm run build"
+
+[[redirects]]
+ from = "/*"
+ to = "/index.html"
+ status = 200
diff --git a/designs/unice/example/package.json b/designs/unice/example/package.json
new file mode 100644
index 00000000000..77774ed36a3
--- /dev/null
+++ b/designs/unice/example/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "unice",
+ "homepage": "https://unice.freesewing.dev/",
+ "version": "",
+ "private": true,
+ "dependencies": {
+ "@fontsource/permanent-marker": "latest",
+ "@fontsource/roboto-mono": "latest",
+ "@fontsource/ubuntu": "latest",
+ "@freesewing/components": "latest",
+ "@freesewing/core": "latest",
+ "@freesewing/css-theme": "latest",
+ "@freesewing/i18n": "latest",
+ "@freesewing/models": "latest",
+ "@freesewing/mui-theme": "latest",
+ "@freesewing/pattern-info": "latest",
+ "@freesewing/plugin-bundle": "latest",
+ "@freesewing/plugin-theme": "latest",
+ "@freesewing/plugin-i18n": "latest",
+ "@freesewing/plugin-svgattr": "latest",
+ "@freesewing/utils": "latest",
+ "@material-ui/core": "^4.11.4",
+ "@material-ui/icons": "^4.11.2",
+ "@material-ui/lab": "^v4.0.0-alpha.57",
+ "prismjs": "1.25.0",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
+ "react-intl": "^5.18.0",
+ "react-scripts": "^4.0.3",
+ "react-error-overlay": "6.0.9",
+ "file-saver": "^2.0.5",
+ "react-markdown": "6.0.2",
+ "source-map-explorer": "^2.5.2"
+ },
+ "scripts": {
+ "analyze": "source-map-explorer 'build/static/js/*.js'",
+ "size": "source-map-explorer 'build/static/js/*.js' --tsv --no-root",
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": "react-app"
+ },
+ "browserslist": [
+ "defaults"
+ ],
+ "devDependencies": {
+ "babel-plugin-prismjs": "2.0.1",
+ "react-error-overlay": "6.0.9"
+ },
+ "resolutions": {
+ "react-error-overlay": "6.0.9"
+ }
+}
diff --git a/designs/unice/example/public/favicon.ico b/designs/unice/example/public/favicon.ico
new file mode 100644
index 00000000000..95061a260f1
Binary files /dev/null and b/designs/unice/example/public/favicon.ico differ
diff --git a/designs/unice/example/public/index.html b/designs/unice/example/public/index.html
new file mode 100644
index 00000000000..af182fd3cc0
--- /dev/null
+++ b/designs/unice/example/public/index.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+ unice
+
+
+
+
+
+
+
diff --git a/designs/unice/example/public/layout.css b/designs/unice/example/public/layout.css
new file mode 100644
index 00000000000..c62502f9791
--- /dev/null
+++ b/designs/unice/example/public/layout.css
@@ -0,0 +1 @@
+div.layout-wrapper{width:100%;margin:0;padding:0;background-color:red;background:#f8f9fa;background:linear-gradient(90deg, #f1f3f5 0%, #f1f3f5 25%, #f8f9fa 26%, #f8f9fa 100%)}div.layout-wrapper div.layout{display:flex;max-width:1600px;margin:auto;padding:0;flex-direction:row;flex-wrap:nowrap;justify-content:space-between;background-color:#f8f9fa;min-height:calc(100vh - 64px)}div.layout-wrapper div.layout>aside{width:33%;background:#f1f3f5;border-right:2px solid #dee2e6}div.layout-wrapper div.layout>section{margin:0;padding:1rem}div.layout-wrapper div.layout>section>div.content{max-width:66ch;min-width:340px}div.layout-wrapper div.layout>section>div.content.wide{max-width:100%;margin:auto}.theme-wrapper.dark header{background-color:#1a1d21}.theme-wrapper.dark div.layout-wrapper{background:#f8f9fa;background:linear-gradient(90deg, #1a1d21 0%, #1a1d21 25%, #212529 26%, #212529 100%)}.theme-wrapper.dark div.layout-wrapper div.layout{background-color:#212529}.theme-wrapper.dark div.layout-wrapper div.layout>aside{background-color:#1a1d21;border-right:2px solid #343a40}header a svg{color:#ced4da}header a:first-of-type svg{color:#f8f9fa}header a:hover svg{color:#b197fc}header a span,header button span{color:#ced4da}header a span svg,header button span svg{color:#dee2e6}header a:hover span,header button:hover span{color:#f8f9fa}header a:hover span svg,header button:hover span svg{color:#b197fc}header a,header button{padding:0 1vw !important}@media (min-width: 1200px){div.layout>section{width:63%}}@media (max-width: 1199px) and (min-width: 960px){div.layout>aside{width:298px}div.layout>section{width:calc(100% - 300px - 4rem);max-width:none;margin:0 1rem 0 3rem}}@media (max-width: 959px){div.layout>aside{width:218px}div.layout>section{width:calc(100% - 220px - 4rem);max-width:none;margin:0;padding:0 2rem}div.layout>section div.content{min-width:inherit}}@media (max-width: 599px){div.layout>aside{display:none}div.layout>section{width:calc(100%);margin:0 auto;padding:0 1.5rem;max-width:none}}div.gatsby-highlight{margin-bottom:1rem}@media (max-width: 599px){#mobile-menu{position:fixed;top:0;left:0;width:100%;height:100vh;padding:0 0 1rem;max-width:600px;z-index:-10;transition:opacity 0.25s ease 0s;opacity:0;overflow:scroll}#mobile-menu>ul,#mobile-menu>div{transform:translate(0px, 10px);transition:transform 0.25s ease 0s}.theme-wrapper.show-menu #mobile-menu{opacity:1;z-index:10}.theme-wrapper.show-menu #mobile-menu>div{transform:translate(0px, 0px)}}.theme-wrapper.light div.draft-ui-menu,.theme-wrapper.light div.menu{background:#f1f3f5}.theme-wrapper.dark div.draft-ui-menu,.theme-wrapper.dark div.menu{background:#343a40}.theme-wrapper.show-menu div.menu{opacity:1;z-index:10}.theme-wrapper.show-menu div.menu>div{transform:translate(0px, 0px)}div.spaced-buttons>button{margin:0 0.5rem 0.5rem 0}div.spaced>*{margin:0 0.5rem 0.5rem 0}ul#pre-main-menu{margin:0;padding:0}.boldish{font-weight:500}.freesewing.draft{padding:1rem}li.action{clear:both}li.action span.MuiSwitch-root{float:right}.theme-wrapper.light ul#draft-config li.action.toggle.off,.theme-wrapper.dark ul#draft-config li.action.toggle.off{color:#868e96}.theme-wrapper.light ul#draft-config li.action.toggle.off>span svg,.theme-wrapper.dark ul#draft-config li.action.toggle.off>span svg{color:#868e96}footer{background-color:#1a1d21;color:#adb5bd;padding:3rem 0 6rem}footer a{color:#dee2e6 !important;font-weight:400}footer a:hover{color:#d0bfff !important}footer div.cols{display:flex;flex-direction:row;justify-content:space-between;max-width:1600px;margin:auto;padding:0 1.5rem}footer div.cols>div{min-width:150px;max-width:calc(20% - 4rem);padding:0 2rem 0 0;width:100%}footer ul{text-align:left;font-size:1.1rem;margin:0;padding:0;width:100%}footer ul li:first-of-type{padding:0.35rem 0.75rem}footer ul li{display:block}footer ul li a:hover{text-decoration:none !important}footer ul li.heading{font-weight:bold;border-bottom:3px solid #adb5bd;margin-bottom:0.5rem}@media (min-width: 1200px){footer div.cols>div:last-of-type{min-width:350px}}@media (min-width: 600px) and (max-width: 959px){footer div.cols{flex-wrap:wrap}footer div.cols>div{width:calc(30% - 4rem);padding:0 1rem}}@media (max-width: 599px){footer div.cols{display:block}footer div.cols>div{margin:2rem auto 0;max-width:calc(100% - 4rem)}footer div.cols>div:first-of-type{margin-top:0}}
diff --git a/designs/unice/example/public/manifest.json b/designs/unice/example/public/manifest.json
new file mode 100644
index 00000000000..578f27ed307
--- /dev/null
+++ b/designs/unice/example/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "unice",
+ "name": "unice",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/designs/unice/example/src/App.js b/designs/unice/example/src/App.js
new file mode 100644
index 00000000000..7bb0f0e7112
--- /dev/null
+++ b/designs/unice/example/src/App.js
@@ -0,0 +1,45 @@
+import React from 'react'
+import freesewing from '@freesewing/core'
+import Workbench from '@freesewing/components/Workbench'
+import '@freesewing/css-theme'
+import Pattern from './pattern/src/index.js'
+/*
+ * The following symlink is required to make this import work:
+ * `root_folder/example/src/pattern => `../../`
+ *
+ * Without it, we can't import the pattern as a local file
+ * since create-react-app does not allow imports outside ./src
+ * If it's imported as a dependency, webpack will cache the
+ * build and there will be no hot-reloading of changes
+ */
+
+const App = (props) => {
+ // You can use this to add translations
+ /*
+ let translations = {
+ JSON: 'JSON',
+ someOtherString: 'Some other string that needs translation'
+ }
+ */
+
+ // Adds support for loading an external pattern configuration
+ let recreate = false
+ if (window) recreate = window.location.pathname.substr(1).split('/')
+ if (recreate.length === 3 && recreate[0] === 'recreate') {
+ recreate = { from: recreate[1], id: recreate[2] }
+ } else {
+ recreate = false
+ }
+
+ return (
+
+ )
+}
+
+export default App
diff --git a/designs/unice/example/src/index.js b/designs/unice/example/src/index.js
new file mode 100644
index 00000000000..24aefad45a1
--- /dev/null
+++ b/designs/unice/example/src/index.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import App from './App'
+import * as serviceWorker from './serviceWorker'
+import './layout.css'
+
+ReactDOM.render(, document.getElementById('root'))
+
+// If you want your app to work offline and load faster, you can change
+// unregister() to register() below. Note this comes with some pitfalls.
+// Learn more about service workers: http://bit.ly/CRA-PWA
+serviceWorker.unregister()
diff --git a/designs/unice/example/src/layout.css b/designs/unice/example/src/layout.css
new file mode 100644
index 00000000000..a4963e16e55
--- /dev/null
+++ b/designs/unice/example/src/layout.css
@@ -0,0 +1,273 @@
+* {
+ box-sizing: border-box;
+}
+.MuiToolbar-root {
+ overflow-y: auto;
+}
+div.layout-wrapper {
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ background: #f8f9fa;
+ background: linear-gradient(90deg, #f1f3f5 0%, #f1f3f5 25%, #f8f9fa 26%, #f8f9fa 100%);
+}
+div.layout-wrapper div.layout {
+ display: flex;
+ max-width: 1600px;
+ margin: auto;
+ padding: 0;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ background-color: #f8f9fa;
+ min-height: calc(100vh - 64px);
+}
+div.layout-wrapper div.layout > aside {
+ width: 33%;
+ background: #f1f3f5;
+ border-right: 2px solid #dee2e6;
+}
+div.layout-wrapper div.layout > section {
+ margin: 0;
+ padding: 1rem;
+}
+div.layout-wrapper div.layout > section > div.content {
+ max-width: 66ch;
+ min-width: 340px;
+}
+div.layout-wrapper div.layout > section > div.content.wide {
+ max-width: 100%;
+ margin: auto;
+}
+
+.theme-wrapper.dark header {
+ background-color: #1a1d21;
+}
+
+.theme-wrapper.dark div.layout-wrapper {
+ background: #f8f9fa;
+ background: linear-gradient(90deg, #1a1d21 0%, #1a1d21 25%, #212529 26%, #212529 100%);
+}
+.theme-wrapper.dark div.layout-wrapper div.layout {
+ background-color: #212529;
+}
+.theme-wrapper.dark div.layout-wrapper div.layout > aside {
+ background-color: #1a1d21;
+ border-right: 2px solid #343a40;
+}
+
+/* monitor */
+@media (min-width: 1200px) {
+ div.layout > section {
+ width: 63%;
+ }
+}
+
+/* slate */
+@media (max-width: 1199px) and (min-width: 960px) {
+ div.layout > aside {
+ width: 298px;
+ }
+ div.layout > section {
+ width: calc(100% - 300px - 4rem);
+ max-width: none;
+ margin: 0 1rem 0 3rem;
+ }
+}
+
+/* tablet */
+@media (max-width: 959px) {
+ div.layout > aside {
+ width: 218px;
+ }
+ div.layout > section {
+ width: calc(100% - 220px - 4rem);
+ max-width: none;
+ margin: 0;
+ padding: 0 2rem;
+ }
+ div.layout > section div.content {
+ min-width: inherit;
+ }
+}
+
+/* mobile */
+@media (max-width: 599px) {
+ div.layout > aside {
+ display: none;
+ }
+ div.layout > section {
+ width: calc(100%);
+ margin: 0 auto;
+ padding: 0 1.5rem;
+ max-width: none;
+ }
+}
+
+div.gatsby-highlight {
+ margin-bottom: 1rem;
+}
+
+@media (max-width: 599px) {
+ #mobile-menu {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100vh;
+ padding: 0 0 1rem;
+ max-width: 600px;
+ z-index: -10;
+ transition: opacity 0.25s ease 0s;
+ opacity: 0;
+ overflow: scroll;
+ }
+ #mobile-menu > ul,
+ #mobile-menu > div {
+ transform: translate(0px, 10px);
+ transition: transform 0.25s ease 0s;
+ }
+ .theme-wrapper.show-menu #mobile-menu {
+ opacity: 1;
+ z-index: 10;
+ }
+ .theme-wrapper.show-menu #mobile-menu > div {
+ transform: translate(0px, 0px);
+ }
+}
+
+.theme-wrapper.light div.draft-ui-menu,
+.theme-wrapper.light div.menu {
+ background: #f1f3f5;
+}
+
+.theme-wrapper.dark div.draft-ui-menu,
+.theme-wrapper.dark div.menu {
+ background: #343a40;
+}
+
+.theme-wrapper.show-menu div.menu {
+ opacity: 1;
+ z-index: 10;
+}
+.theme-wrapper.show-menu div.menu > div {
+ transform: translate(0px, 0px);
+}
+
+div.spaced-buttons > button {
+ margin: 0 0.5rem 0.5rem 0;
+}
+
+div.spaced > * {
+ margin: 0 0.5rem 0.5rem 0;
+}
+
+ul#pre-main-menu {
+ margin: 0;
+ padding: 0;
+}
+
+.boldish {
+ font-weight: 500;
+}
+
+.freesewing.draft {
+ padding: 1rem;
+}
+
+li.action {
+ clear: both;
+}
+
+li.action span.MuiSwitch-root {
+ float: right;
+}
+
+.theme-wrapper.light ul#draft-config li.action.toggle.off,
+.theme-wrapper.dark ul#draft-config li.action.toggle.off {
+ color: #868e96;
+}
+.theme-wrapper.light ul#draft-config li.action.toggle.off > span svg,
+.theme-wrapper.dark ul#draft-config li.action.toggle.off > span svg {
+ color: #868e96;
+}
+
+footer {
+ background-color: #1a1d21;
+ color: #adb5bd;
+ padding: 3rem 0 6rem;
+}
+footer a {
+ color: #dee2e6 !important;
+ font-weight: 400;
+}
+footer a:hover {
+ color: #d0bfff !important;
+}
+footer div.cols {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ max-width: 1600px;
+ margin: auto;
+ padding: 0 1.5rem;
+}
+footer div.cols > div {
+ min-width: 150px;
+ max-width: calc(20% - 4rem);
+ padding: 0 2rem 0 0;
+ width: 100%;
+}
+footer ul {
+ text-align: left;
+ font-size: 1.1rem;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+}
+footer ul li:first-of-type {
+ padding: 0.35rem 0.75rem;
+}
+footer ul li {
+ display: block;
+}
+footer ul li a:hover {
+ text-decoration: none !important;
+}
+footer ul li.heading {
+ font-weight: bold;
+ border-bottom: 3px solid #adb5bd;
+ margin-bottom: 0.5rem;
+}
+
+/* XL screens */
+@media (min-width: 1200px) {
+ footer div.cols > div:last-of-type {
+ min-width: 350px;
+ }
+}
+
+/* SM screens */
+@media (min-width: 600px) and (max-width: 959px) {
+ footer div.cols {
+ flex-wrap: wrap;
+ }
+ footer div.cols > div {
+ width: calc(30% - 4rem);
+ padding: 0 1rem;
+ }
+}
+
+/* XS screens */
+@media (max-width: 599px) {
+ footer div.cols {
+ display: block;
+ }
+ footer div.cols > div {
+ margin: 2rem auto 0;
+ max-width: calc(100% - 4rem);
+ }
+ footer div.cols > div:first-of-type {
+ margin-top: 0;
+ }
+}
diff --git a/designs/unice/example/src/pattern b/designs/unice/example/src/pattern
new file mode 100644
index 00000000000..6581736d623
--- /dev/null
+++ b/designs/unice/example/src/pattern
@@ -0,0 +1 @@
+../../
\ No newline at end of file
diff --git a/designs/unice/example/src/serviceWorker.js b/designs/unice/example/src/serviceWorker.js
new file mode 100644
index 00000000000..4fe923e7795
--- /dev/null
+++ b/designs/unice/example/src/serviceWorker.js
@@ -0,0 +1,123 @@
+// In production, we register a service worker to serve assets from local cache.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on the "N+1" visit to a page, since previously
+// cached resources are updated in the background.
+
+// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
+// This link also includes instructions on opting out of this behavior.
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.1/8 is considered localhost for IPv4.
+ window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
+)
+
+export function register(config) {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config)
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://goo.gl/SC7cgQ'
+ )
+ })
+ } else {
+ // Is not local host. Just register service worker
+ registerValidSW(swUrl, config)
+ }
+ })
+ }
+}
+
+function registerValidSW(swUrl, config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then((registration) => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the old content will have been purged and
+ // the fresh content will have been added to the cache.
+ // It's the perfect time to display a "New content is
+ // available; please refresh." message in your web app.
+ console.log('New content is available; please refresh.')
+
+ // Execute callback
+ if (config.onUpdate) {
+ config.onUpdate(registration)
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.')
+
+ // Execute callback
+ if (config.onSuccess) {
+ config.onSuccess(registration)
+ }
+ }
+ }
+ }
+ }
+ })
+ .catch((error) => {
+ console.error('Error during service worker registration:', error)
+ })
+}
+
+function checkValidServiceWorker(swUrl, config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl)
+ .then((response) => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ if (
+ response.status === 404 ||
+ response.headers.get('content-type').indexOf('javascript') === -1
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then((registration) => {
+ registration.unregister().then(() => {
+ window.location.reload()
+ })
+ })
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config)
+ }
+ })
+ .catch(() => {
+ console.log('No internet connection found. App is running in offline mode.')
+ })
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then((registration) => {
+ registration.unregister()
+ })
+ }
+}
diff --git a/designs/unice/package.json b/designs/unice/package.json
new file mode 100644
index 00000000000..4c3f21efd5f
--- /dev/null
+++ b/designs/unice/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@freesewing/unice",
+ "version": "3.0.0-alpha.0",
+ "description": "A FreeSewing pattern for a basic, highly-customizable underwear pattern",
+ "author": "Anna Puk (https://github.com/anna-puk)",
+ "homepage": "https://freesewing.org/",
+ "repository": "github:freesewing/freesewing",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/freesewing/freesewing/issues"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://freesewing.org/patrons/join"
+ },
+ "keywords": [
+ "freesewing",
+ "design",
+ "diy",
+ "fashion",
+ "made to measure",
+ "parametric design",
+ "pattern",
+ "sewing",
+ "sewing pattern"
+ ],
+ "type": "module",
+ "module": "dist/index.mjs",
+ "exports": {
+ ".": "./dist/index.mjs"
+ },
+ "scripts": {
+ "build": "node build.mjs",
+ "clean": "rimraf dist",
+ "mbuild": "NO_MINIFY=1 node build.mjs",
+ "symlink": "mkdir -p ./node_modules/@freesewing && cd ./node_modules/@freesewing && ln -s -f ../../../* . && cd -",
+ "test": "npx mocha tests/*.test.mjs",
+ "vbuild": "VERBOSE=1 node build.mjs",
+ "lab": "cd ../../sites/lab && yarn start",
+ "tips": "node ../../scripts/help.mjs",
+ "prettier": "npx prettier --write 'src/*.mjs' 'tests/*.mjs'",
+ "testci": "npx mocha tests/*.test.mjs --reporter ../../tests/reporters/terse.js",
+ "cibuild_step5": "node build.mjs"
+ },
+ "peerDependencies": {
+ "@freesewing/core": "^3.0.0-alpha.0",
+ "@freesewing/plugin-bundle": "^3.0.0-alpha.0",
+ "@freesewing/config-helpers": "^3.0.0-alpha.0"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "mocha": "^10.0.0",
+ "chai": "^4.2.0"
+ },
+ "files": [
+ "dist/*",
+ "README.md"
+ ],
+ "publishConfig": {
+ "access": "public",
+ "tag": "next"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8"
+ }
+}
diff --git a/designs/unice/src/back.mjs b/designs/unice/src/back.mjs
new file mode 100644
index 00000000000..f2da67d4093
--- /dev/null
+++ b/designs/unice/src/back.mjs
@@ -0,0 +1,42 @@
+import { pctBasedOn } from '@freesewing/core'
+import {back as ursulaBack } from '@freesewing/ursula'
+
+export const back = {
+ name: 'unice.back',
+ measurements: ['waist', 'seat', 'waistToSeat', 'waistToUpperLeg','hips','waistToHips'],
+ optionalMeasurements: ['crossSeam','crossSeamFront'],
+ options: {
+ gussetShift: 0.015, // fraction of seat circumference - could be an advanced option?
+ gussetWidth: { pct: 7.2, min: 2, max: 12, menu: 'fit' }, // Gusset width in relation to waist-to-upperleg
+ gussetLength: { pct: 12.7, min: 10, max: 16, menu: 'fit' }, // Gusset length in relation to seat
+ fabricStretch: { pct: 15, min: 0, max: 100, menu: 'fit' }, // used in Ursula
+ fabricStretchX: { pct: 15, min: 0, max: 100, menu: 'fit' }, // horizontal stretch (range set wide for beta testing)
+ fabricStretchY: {pct: 0, min: 0, max: 100, menu: 'fit' }, // vertical stretch (range set wide for beta testing)
+ rise: { pct: 60, min: 30, max: 100, menu: 'style' }, // extending rise beyond 100% would require adapting paths.sideLeft!
+ legOpening: { pct: 45, min: 5, max: 85, menu: 'style' },
+ frontDip: { pct: 5.0, min: -5, max: 15, menu: 'style' },
+ taperToGusset: { pct: 70, min: 5, max: 100, menu: 'style' },
+ // booleans
+ useCrossSeam: { bool: true, menu: 'fit' },
+ adjustStretch: {bool: true, menu: 'fit' }, // to not stretch fabric to the limits
+ },
+ draft: ({
+ utils,
+ store,
+ sa,
+ Point,
+ points,
+ Path,
+ paths,
+ Snippet,
+ snippets,
+ options,
+ measurements,
+ complete,
+ paperless,
+ macro,
+ part,
+ }) => {
+ return part
+ },
+}
diff --git a/designs/unice/src/front.mjs b/designs/unice/src/front.mjs
new file mode 100644
index 00000000000..c9a160ac812
--- /dev/null
+++ b/designs/unice/src/front.mjs
@@ -0,0 +1,309 @@
+import { pctBasedOn } from '@freesewing/core'
+
+export const front = {
+ name: 'unice.front',
+ measurements: ['waist', 'seat', 'waistToSeat', 'waistToUpperLeg','hips','waistToHips'],
+ optionalMeasurements: ['crossSeam','crossSeamFront'],
+ options: {
+ gussetShift: 0.015, // fraction of seat circumference - could be an advanced option?
+ gussetWidth: { pct: 7.2, min: 2, max: 12, menu: 'fit' }, // Gusset width in relation to waist-to-upperleg
+ gussetLength: { pct: 12.7, min: 10, max: 16, menu: 'fit' }, // Gusset length in relation to seat
+ fabricStretchX: { pct: 15, min: 0, max: 100, menu: 'fit' }, // horizontal stretch (range set wide for beta testing)
+ fabricStretchY: {pct: 0, min: 0, max: 100, menu: 'fit' }, // vertical stretch (range set wide for beta testing)
+ rise: { pct: 60, min: 30, max: 100, menu: 'style' }, // extending rise beyond 100% would require adapting paths.sideLeft!
+ legOpening: { pct: 45, min: 5, max: 85, menu: 'style' },
+ frontDip: { pct: 5.0, min: -5, max: 15, menu: 'style' },
+ taperToGusset: { pct: 70, min: 5, max: 100, menu: 'style' },
+ // booleans
+ useCrossSeam: { bool: true, menu: 'fit' },
+ adjustStretch: {bool: true, menu: 'fit' }, // to not stretch fabric to the limits
+ },
+ draft: ({
+ utils,
+ store,
+ sa,
+ Point,
+ points,
+ Path,
+ paths,
+ Snippet,
+ snippets,
+ options,
+ measurements,
+ complete,
+ paperless,
+ macro,
+ part,
+ }) => {
+ // Stretch utility method
+
+ // Use stretch inputs to calculate four different scale factors: horizontal/vertical and 'regular'/'reduced', depending on direction of the tension
+ // xScale: for parts that go across the body (= stretched horizontally)
+ // xScaleReduced: parts that are not under (horizontal) tension, e.g. the gusset
+ // yScale: for parts which are stretched vertically but not horizontally (anything below leg opening)
+ // yScaleReduced: parts which are already under horizontal stretch, which limits vertical stretch
+
+ if (options.adjustStretch) { // roughly 15% of stretch is reserved for comfort
+ // horizontal: first, 'regular' stretch (for parts that go across the body)
+ if (options.fabricStretchX < 0.30) {
+ // subtract 15, but never go below 0
+ store.set('xScale', utils.stretchToScale(Math.max(0 , options.fabricStretchX - 0.15)))
+ } else {
+ store.set('xScale', utils.stretchToScale(options.fabricStretchX / 2))
+ // rough approximation of rule of thumb quoted in Sanne's July 29, 2021 showcase
+ }
+ // use half of whatever the regular stretch is (no util available, convert from stretch to fraction manually
+ store.set('xScaleReduced',(1 + store.get('xScale'))/2)
+
+ // vertical:
+ if (options.fabricStretchY < 0.30) {
+ // subtract 15, but never go below 0
+ store.set('yScale', utils.stretchToScale(Math.max(0 , options.fabricStretchY - 0.15)))
+ } else {
+ store.set('yScale', utils.stretchToScale(options.fabricStretchY / 2))
+ // rough approximation of rule of thumb quoted in Sanne's July 29, 2021 showcase
+ }
+ // reduced vertical stretch calculated below, same as for non-adjusted case
+ } else {
+ // in order: regular, then reduced horizontal stretch, followed by regular vertical stretch
+ store.set('xScale', utils.stretchToScale(options.fabricStretchX))
+ store.set('xScaleReduced', utils.stretchToScale(options.fabricStretchX / 2))
+ store.set('yScale', utils.stretchToScale(options.fabricStretchY))
+ }
+ if (options.fabricStretchY < 0.20) {
+ store.set('yScaleReduced',1)
+ } else {
+ // reduced yScale gradually increases from equivalent of stretch 0 to 5%, then cuts off (uses third-order polynomial)
+ // function to approximate Sanne's guidelines given in Discord (roughly 2.5% for stretch 30-40%, 5% above that)
+ store.set('yScaleReduced', utils.stretchToScale(Math.min( 0.05, 6.25 * Math.pow(options.fabricStretchY - 0.20, 3))))
+ }
+
+ // // temporarily overrule yScale and yScaleReduced
+ // store.set('yScale',1)
+ // store.set('yScaleReduced',1)
+
+
+ // // Part definition starts here
+
+ // determine height of front part: use cross seam (and cross seam front) if selected and available
+ // NOTE: neither crossSeam not frontHeight are adjusted for (vertical) stretch
+ if (options.useCrossSeam && measurements.crossSeam) {
+ store.set('crossSeam',measurements.crossSeam)
+ } else { // use original approximation: front and back are roughly waistToUpperLeg high, plus gusset length
+ store.set('crossSeam',measurements.waistToUpperLeg * (1 + options.backToFrontLength) + options.gussetLength * measurements.seat)
+ }
+ // optionally use crossSeamFront to determine relative length of front and back
+ // this does not account for vertical stretch yet
+ if (options.useCrossSeam && measurements.crossSeamFront) { // subtract half the gusset length from cross seam front, and an additional 3.5% of the seat circumference to move the gusset upward (to match commercial panties)
+ store.set('frontHeight',measurements.crossSeamFront - measurements.seat*(0.5*options.gussetLength + options.gussetShift))
+ } else { // subtract gusset length, divide by roughly 2
+ store.set('frontHeight',(store.get('crossSeam') - options.gussetLength * measurements.seat)/(1 + options.backToFrontLength))
+ }
+
+
+ // Create points
+
+ // side seam is on a line from upper leg to seat to hips (optional?) to waist
+ points.frontWaistMid = new Point(measurements.seat / 4, 0)
+ points.frontWaistLeft = new Point(
+ measurements.seat / 4 - (measurements.waist / 4) * store.get('xScale'),
+ 0
+ )
+ points.frontSeatLeft = new Point(
+ measurements.seat / 4 - (measurements.seat / 4) * store.get('xScale'),
+ measurements.waistToSeat * store.get('yScaleReduced')
+ )
+ points.frontUpperLegLeft = new Point(
+ measurements.seat / 4 - (measurements.seat / 4) * store.get('xScale'), // assume same circ. as seat
+ measurements.waistToUpperLeg * store.get('yScaleReduced')
+ )
+ points.frontHipLeft = new Point(
+ measurements.seat / 4 - (measurements.hips / 4) * store.get('xScale'),
+ measurements.waistToHips * store.get('yScaleReduced')
+ )
+
+ // use these points to define an invisible path
+ paths.sideLeft = new Path()
+ .move(points.frontUpperLegLeft)
+ .line(points.frontSeatLeft)
+ .line(points.frontHipLeft)
+ .line(points.frontWaistLeft)
+ .setRender(false) // only show when debugging
+
+ /* Waist band is somewhere on the sideLeft path */
+ points.frontWaistBandLeft = paths.sideLeft.shiftFractionAlong(options.rise)
+ points.frontWaistBandRight = points.frontWaistBandLeft.flipX(points.frontWaistMid)
+ points.frontWaistBandMid = points.frontWaistBandLeft
+ .shiftFractionTowards(points.frontWaistBandRight, 0.5)
+ .shift(270, measurements.waistToUpperLeg * options.frontDip) /* Waist band dip */
+
+ /* Leg opening is also on the sideLeft path, and cannot be higher than rise */
+ /* Minimum side seam length is defined as 3.5% of the sideLeft path (which is at least waistToUpperLeg long) */
+ store.set('adjustedLegOpening',Math.min(options.legOpening,options.rise - 0.035)) // TODO: account for rise having a different domain
+
+ points.frontLegOpeningLeft = paths.sideLeft.shiftFractionAlong(store.get('adjustedLegOpening'))
+ points.frontLegOpeningRight = points.frontLegOpeningLeft.flipX(points.frontWaistMid) // Waist band low point
+
+ // calculate the actual front height, using yScale above and yScaleReduced below leg opening
+ store.set('frontHeightAbove',points.frontWaistLeft.dy(points.frontLegOpeningLeft))
+
+ var frontHeightBelow
+ frontHeightBelow = store.get('yScale')*(store.get('frontHeight') - store.get('frontHeightAbove')/store.get('yScaleReduced'))
+
+ var frontHeightReduced
+ frontHeightReduced = frontHeightBelow + store.get('frontHeightAbove')
+
+ // gusset width uses modified xScale (barely stretches) and depends on waistToUpperLeg - least sensitive to girth
+ points.frontGussetLeft = new Point(
+ measurements.seat / 4 - (measurements.waistToSeat * options.gussetWidth * store.get('xScaleReduced')) * 2.2,
+ frontHeightReduced
+ )
+ points.frontGussetMid = new Point(measurements.seat / 4, frontHeightReduced)
+
+ /* Flip points to right side */
+ points.frontGussetRight = points.frontGussetLeft.flipX(points.frontWaistMid)
+ points.frontHipRight = points.frontSeatLeft.flipX(points.frontWaistMid)
+ points.frontWaistRight = points.frontWaistLeft.flipX(points.frontWaistMid)
+
+ /* Middle point for label */
+ points.frontMidMid = points.frontLegOpeningLeft.shiftFractionTowards(
+ points.frontLegOpeningRight,
+ 0.5
+ )
+
+ // Create control points
+
+ /* Control points for leg opening curves */
+ points.frontLegOpeningLeftCp1 = points.frontLegOpeningLeft.shift(
+ 180,
+ points.frontGussetLeft.dy(points.frontLegOpeningLeft) / 3
+ )
+
+ /* Control point above gusset moves higher as taperToGusset (= front exposure) increases, but is limited by both the leg opening (allow minimal arching only) and the rise (leg opening must not intersect the waist band) */
+ points.frontGussetLeftCp1 = points.frontGussetLeft
+ .shift(270, Math.max(Math.max(points.frontGussetLeft.dy(points.frontWaistMid) * options.taperToGusset / 2,points.frontGussetLeft.dy(points.frontLegOpeningLeft) * 2),points.frontGussetLeft.dy(points.frontWaistBandMid)))
+
+ /* Control point for waistband dip */
+ points.frontWaistBandLeftCp1 = points.frontWaistBandMid.shift(0,points.frontWaistBandMid.dx(points.frontWaistBandLeft) / 3 )
+
+
+ /* Flip control points to right side */
+ points.frontGussetRightCp1 = points.frontGussetLeftCp1.flipX(points.frontWaistMid)
+ points.frontLegOpeningRightCp1 = points.frontLegOpeningLeftCp1.flipX(points.frontWaistMid)
+ points.frontWaistBandRightCp1 = points.frontWaistBandLeftCp1.flipX(points.frontWaistMid)
+
+ // Draw paths
+
+ paths.seam = new Path()
+ .move(points.frontWaistBandMid)
+ .curve(points.frontWaistBandLeftCp1, points.frontWaistBandLeft, points.frontWaistBandLeft) // Waist band dip
+ .line(points.frontLegOpeningLeft)
+ .curve(points.frontLegOpeningLeftCp1, points.frontGussetLeftCp1, points.frontGussetLeft)
+ .line(points.frontGussetMid)
+ .line(points.frontGussetRight)
+ .curve(points.frontGussetRightCp1, points.frontLegOpeningRightCp1, points.frontLegOpeningRight)
+ .line(points.frontWaistBandRight)
+ .curve(points.frontWaistBandRight, points.frontWaistBandRightCp1, points.frontWaistBandMid) // Waist band dip
+ .close()
+ .attr('class', 'fabric')
+
+ // Store points for use in other parts
+
+ /* Store side seam points for use in back */
+
+ store.set('sideSeamWaist', points.frontWaistBandLeft)
+ store.set('sideSeamHip', points.frontLegOpeningLeft)
+
+ /* Store gusset points for use in gusset */
+
+ store.set('frontGussetLeft', points.frontGussetLeft)
+ store.set('frontGussetRight', points.frontGussetRight)
+ store.set('frontGussetMid', points.frontGussetMid)
+
+ /* Store lengths for use in elastic */
+
+ paths.frontLegOpening = new Path()
+ .move(points.frontGussetRight)
+ .curve(
+ points.frontGussetRightCp1,
+ points.frontLegOpeningRightCp1,
+ points.frontLegOpeningRight
+ )
+ .setRender(false)
+ store.set('frontLegOpeningLength',paths.frontLegOpening.length())
+
+ paths.frontWaistBand = new Path()
+ .move(points.frontWaistBandRight)
+ .curve(
+ points.frontWaistBandRightCp1,
+ points.frontWaistBandLeftCp1,
+ points.frontWaistBandLeft
+ )
+ .setRender(false)
+ store.set('frontWaistBandLength',paths.frontWaistBand.length())
+
+ // Complete?
+ if (complete) {
+ if (sa) {
+ paths.sa = paths.seam.offset(sa).attr('class', 'fabric sa')
+ }
+ }
+
+ macro('title', {
+ at: points.frontMidMid,
+ nr: 1,
+ title: 'front',
+ })
+
+ macro('grainline', {
+ from: points.frontGussetMid,
+ to: points.frontGussetMid.shiftFractionTowards(points.frontWaistBandMid, 0.5),
+ })
+
+ // Paperless?
+ if (paperless) {
+ macro('hd', {
+ from: points.frontWaistBandRight,
+ to: points.frontWaistBandLeft,
+ y: points.frontWaistBandRight.y + sa - 15,
+ })
+ macro('hd', {
+ from: points.frontLegOpeningRight,
+ to: points.frontLegOpeningLeft,
+ y: points.frontLegOpeningRight.y + sa - 15,
+ })
+ macro('hd', {
+ from: points.frontGussetLeft,
+ to: points.frontGussetRight,
+ y: points.frontGussetLeft.y + sa + 15,
+ })
+ macro('vd', {
+ from: points.frontWaistBandMid,
+ to: points.frontGussetMid,
+ x: points.frontWaistBandMid.x + sa + 15,
+ })
+ macro('ld', {
+ from: points.frontWaistBandLeft,
+ to: points.frontLegOpeningLeft,
+ d: points.frontWaistBandLeft.y + sa - 15,
+ })
+ macro('pd', {
+ path: new Path()
+ .move(points.frontGussetRight)
+ .curve(
+ points.frontGussetRightCp1,
+ points.frontLegOpeningRightCp1,
+ points.frontLegOpeningRight
+ ),
+ d: 15,
+ })
+ /* macro('vd', {
+ from: points.frontWaistBandLeft,
+ to: points.frontWaistBandMid,
+ x: points.frontWaistBandMid.x + sa + 15,
+ }) */
+ }
+
+ return part
+ },
+}
diff --git a/designs/unice/src/gusset.mjs b/designs/unice/src/gusset.mjs
new file mode 100644
index 00000000000..39b86acb13d
--- /dev/null
+++ b/designs/unice/src/gusset.mjs
@@ -0,0 +1,4 @@
+const method = part => part
+
+export default method
+
diff --git a/designs/unice/src/index.mjs b/designs/unice/src/index.mjs
new file mode 100644
index 00000000000..b8c55238e26
--- /dev/null
+++ b/designs/unice/src/index.mjs
@@ -0,0 +1,14 @@
+import { Design } from '@freesewing/core'
+import { front } from './front.mjs'
+// import { back } from './back.mjs'
+// import { gusset } from './gusset.mjs'
+import { data } from '../data.mjs'
+
+// Setup our new design
+const Unice = new Design({
+ data,
+ parts: [front], // parts: [back, front],
+})
+
+// Named exports
+export {front, Unice} // export { back, front, gusset, Unice }
\ No newline at end of file
diff --git a/designs/unice/tests/shared.test.mjs b/designs/unice/tests/shared.test.mjs
new file mode 100644
index 00000000000..13c82f4ee71
--- /dev/null
+++ b/designs/unice/tests/shared.test.mjs
@@ -0,0 +1,16 @@
+// This file is auto-generated | Any changes you make will be overwritten.
+import { Unice } from './dist/index.mjs'
+
+// Shared tests
+import { testPatternConfig } from '../../../tests/designs/config.mjs'
+import { testPatternDrafting } from '../../../tests/designs/drafting.mjs'
+import { testPatternSampling } from '../../../tests/designs/sampling.mjs'
+
+// Test config
+testPatternConfig(Unice)
+
+// Test drafting - Change the second parameter to `true` to log errors
+testPatternDrafting(Unice, false)
+
+// Test sampling - Change the second parameter to `true` to log errors
+testPatternSampling(Unice, false)