1
0
Fork 0

chore: Port FreeSewing.dev to docusaurus

The replaces the NextJS site powering FreeSewing.dev with a Docusaurus
setup. It's part of my efforts to simplify FreeSewing's setup so we can
focus on our core value proposition.
This commit is contained in:
Joost De Cock 2024-09-28 13:13:48 +02:00
parent 497633d1d3
commit ab3204f9f1
692 changed files with 11037 additions and 20674 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View file

@ -0,0 +1,75 @@
---
title: Setting up the development environment
order: 20
---
FreeSewing provides a development environment that visualizes your design for
you. This tutorial is for the stand-alone development environment, not the
monorepo development environment (which you may have set up if you followed a
getting started tutorial).
To set it up, I will open a terminal and enter the following command:
```sh
npx @freesewing/new-design
```
It will ask if it is ok to install the development environment in a new folder
named `freesewing`. You can accept the default, or pick a different folder name
if you prefer.
It will also ask what package manager you would like to use.
Here too the default (`npm`) is fine., unless you are certain you have **yarn** installed.
After answering these questions, files will be downloaded, dependencies installed,
and it will also initialize a git repository for you (if you have git on your system).
:::note
This will take a few minutes because the development environment has a number
of dependencies that need to be downloaded.
:::
When it's ready, you can enter the `freesewing` directory that was just created and run `npm run dev`:
```sh
cd freesewing
npm run dev
```
Or if you want to use yarn as package manager:
```sh
cd freesewing
yarn dev
```
Now open a browser and go to http://localhost:8000
If all goes well, we'll should see this landing page:
![The FreeSewing development environment](./nd.png)
:::tip
##### More detailed setup tutorials are available
This pattern design tutorial contains only an abbreviated overview
of the setup process.
For more detailed instructions, please refer to one of our setup tutorials:
- [Getting started on Linux](/tutorials/getting-started-linux)
- [Getting started on Mac](/tutorials/getting-started-mac)
- [Getting started on Windows](/tutorials/getting-started-windows)
:::
:::tip
##### Need help?
If you run into any issues, head over to [FreeSewing.org/support](https://next.freesewing.org/support)
which lists the various ways in which you can get help.
:::

View file

@ -0,0 +1,41 @@
---
title: Installing NodeJS
order: 10
---
FreeSewing is a JavaScript project, so you need JavaScript to work with it.
You certainly already have JavaScript on your system. In your browser to be
precise. You can switch this website theme from light to dark mode, and
that would not work without JavaScript.
As a **user** of FreeSewing, this is all you need. To develop with FreeSewing
you are going to need to be able to run JavaScript *outside* the browser using
a JavaScript *runtime*. Which just means a thing that can *run* JavaScript.
We are going to be using [NodeJS](https://nodejs.org/) in this tutorial. It is
the most established of the different JavaScript runtimes. But there's also
other runtimes like [Deno](https://deno.com/) or [Bun](https://bun.sh/).
## Install
If you don't have NodeJS on your system, you can go to
[NodeJS.org](https://nodejs.org/) and follow the install instructions.
:::tip
##### NodeJS versions
You need Node.js 18 (lts/hydrogen) or higher to use FreeSewing
If you're looking to use different versions, I can recommend using `nvm` which makes this very easy: https://github.com/nvm-sh/nvm
:::
## Test
To test whether NodeJS is installed, and see it's version, you can run this command:
```sh
node -v
```
If you get the Node.js version number, that means NodeJs is installed. Yay!

View file

@ -0,0 +1,33 @@
---
title: "Part 1: Prerequisites"
---
In this first part, I will get your up and running with the FreeSewing
development environment.
If you are familiar with JavaScript and its ecosystem, you can probably skip
this section. If not, I have good news and bad news (and then some more good
news) for you.
The good news is that JavaScript is an easy language to pick up. It is also a
very popular and versatile language and the skills you learn here will serve
you well.
The bad news is that the JavaScript ecosystem is vast, and unfortunately
somewhat fractured. Most of the problems people need help with are not so much
in the code itself, but rather getting everything to work together.
This is true not just for FreeSewing, but pretty much all modern JavaScript.
But, no need to despair, FreeSewing provides a development environment that
will take care of all of this for you. So you can focus on designing patterns.
If you have NodeJS on your system, getting that development environment up
and running takes only a single command:
```sh
npx @freesewing/new-design
```
If you don't have NodeJS on your system --- or if you're not sure what
NodeJS is to begin with --- read on to learn how to install it.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,47 @@
---
title: The FreeSewing development environment
order: 30
---
If you have been to FreeSewing.org the FreeSewing development environment will look familiar.
That's because under the hood, it re-uses the same building blocks.
At the top of the page is the header with a row of icons that lay out what is available to you.
![The icons in the header of the FreeSewing development environment](./header.png)
From left to right you can see:
- **Home** will take you to the home page / welcome page
- **Design** will offer you a list of templates to start a design from (more on this below)
- **Documentation** will show a page with links to our documentation
- **Code** will show a page with links to our source code
- **Support** will show a page with the various ways you can get help
- **Theme** allows you to change the theme (in other works the color scheme)
- **Language** allows you to change the language
- **Sign In** allows you to sign in to your FreeSewing account so you can use
your (and our) measurements sets while designing
## Design templates
If you click the **Design** icon it will show this menu:
![Design templates provided by the FreeSewing development environment](./templates.png)
It allows you to choose a design template to start from. The following templates are included:
- **From scratch**: Start with an (almost) empty design
- **Tutorial**: Start with the end result of this very tutorial
- **From Brian**: Start with a design that extends [Brian](https://freesewing.org/designs/brian)
- **From Bent**: Start with a design that extends [Bent](https://freesewing.org/designs/bent)
- **From Titan**: Start with a design that extends [Titan](https://freesewing.org/designs/titan)
- **From Bella**: Start with a design that extends [Bella](https://freesewing.org/designs/bella)
- **From Breanna**: Start with a design that extends [Breanna](https://freesewing.org/designs/breanna)
For the following along this tutorial, you have two options:
- Pick **Tutorial** if you prefer to read along, make small changes, and see how they affect the design.
- Pick **From scratch** if you prefer to actively participate by recreating the design in this tutorial.
I recommend the latter. You will learn (and remember) a lot more if you are actively engaging.

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View file

@ -0,0 +1,32 @@
---
title: Folder structure
order: 40
---
Inside the `freesewing` folder -- which might have a different name if that is
the choice you made -- you will find a bunch of files and folders.
The one that matter is the `design` folder. In it, you will find the following
subfolders:
- `from-bella`
- `from-bent`
- `from-breanna`
- `from-brian`
- `from-scratch`
- `from-titan`
- `tutorial`
Remember when you click the **Design** icon in the header it would bring up
this menu:
![Design templates provided by the FreeSewing development environment](./templates.png)
As you might have guessed by now, each of these options is contained in its
own subfolder under `designs`.
You can edit the files under `designs/[template]/src/` and the changes you make
will be reflected in the development environment.
Don't take my word for it though. Let's start doing exactly that
in [Part 2](/tutorials/pattern-design/part2).

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View file

@ -0,0 +1,68 @@
---
title: Adding measurements
order: 30
---
FreeSewing is all about _bespoke_ sewing patterns -- or *parametric
design* to use a more generic term.
That means that when drafting our pattern, I will take the measurements provided
by the user into account.
Which begs the question, which measurements?
As the pattern designers, you get to decide which measurements are used
to draft the pattern. For this bib, I am going to use the
_head circumference_.
So let's add it as a required measurement.
## Adding required measurements
In our `src/bib.mjs` file, we will add a `measurements` property to the `bib` object.
This property will be an Array (a list) holding all required measurements for this part.
I am using [*the official name* of the measurement](/reference/measurements) here. For head
circumference, that name is `head`.
:::note [FIXME]
The `design/src/bib.mjs` "language" title on the code snippets is out of date. It is used in the tutorial from this point forward to maintain syntax-highlight not yet available for the `src/bib.mjs` title, but should be replaced with `src/bib.mjs`.
:::
```design/src/bib.mjs
function draftBib({ part }) {
return part
}
export const bib = {
name: 'fromscratch.bib',
draft: draftBib,
// highlight-start
measurements: [ 'head' ],
// highlight-end
}
```
From now on, this part requires the `head` measurement.
This change will also get picked up by the development environment, which will now complain that it is missing some measurements.
Since it's just one measurement, I will simply enter a value by hand.
For example `38` as 38 cm is a realistic head circumference measurement for a baby.
:::tip
##### Why using standard measurements names matters
In principle, I can use any name I want for our measurements.
The FreeSewing core library does not care.
However, if everybody uses their own (names for) measurements, then people
aren't able to re-use their measurements across designs.
So if you have any intention at all to play nice with the FreeSewing ecosystem,
please make sure to re-use the names of existing measurements, rather than
invent your own.
See the [best practices](/guides/best-practices#reuse-measurements) on this
topic for details.
:::

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

View file

@ -0,0 +1,119 @@
---
title: Adding options
order: 40
---
I have shown what our bib should look like, and added the _head_ measurement
to work with. But there's still a number of choices I have to make:
- How large should the neck opening be?
- How wide should the bib be?
- How long should the bib be?
I could make all of these choices for the user and set them in stone, so to speak.
But since the pattern I am designing is code, it is trivial (and _IMHO_ very satisfying)
to make a pattern flexible and let the user choose.
All I need to do to give control to the user is add _options_ to the part.
## Add the neckRatio option
The first option I will add controls the ratio between the neck opening
and the head circumference. Let's call it `neckRatio`.
For this, I will add the `options` property to our `bib` object:
```design/src/bib.mjs
function draftBib({ part }) {
return part
}
export const bib = {
name: 'fromscratch.bib',
draft: draftBib,
measurements: [ 'head' ],
// highlight-start
options: {
neckRatio: {
pct: 80,
min: 70,
max: 90,
menu: 'fit'
},
},
// highlight-end
}
```
Can you guess what it means?
- We've added the `options` property to our `bib` object
- On the `options` property, we have added `neckRatio` which holds the configuration for our option
- It is a `pct` option -- which means it's a percentage
- Its default value is 80%
- Its minimum value is 70%
- Its maximum value is 90%
There are different types of options, but percentages are by far the most common ones.
They are all documented [in the part reference docs](/reference/api/part/config/options).
:::note
##### What is `menu` and why should you care?
The `menu` property on our option is *extra*.
It will be ignored by FreeSewing's core library and if we leave it out, our design will produce the same result.
Instead, this `menu` property is there for the benefit of FreeSewing's development environment which will use this to build a menu structure for the various
options.
This is covered in more detail in [Part 3](/tutorials/pattern-design/part3) of this tutorial.
:::
## Add the widthRatio and lengthRatio options
Let's do something similar for the width and length of our bib:
```design/src/bib.mjs
function draftBib({ part }) => {
return part
}
export const bib = {
name: 'fromScratch.bib',
draft: draftBib,
measurements: [ 'head' ],
options: {
neckRatio: {
pct: 80,
min: 70,
max: 90,
menu: 'fit'
},
// highlight-start
widthRatio: {
pct: 45,
min: 35,
max: 55,
menu: 'style'
},
lengthRatio: {
pct: 75,
min: 55,
max: 85,
menu: 'style'
},
// highlight-end
},
}
```
This is pretty much the exact same thing, except that are placing these in the `style` menu.
Later, I will test-drive our pattern to see how it behaves when we adapt the options
between their minimum and maximum values. At that time, I may need to tweak these values.
With that out of the way, I will start drawing the bib.

View file

@ -0,0 +1,426 @@
---
title: Avoiding overlap
order: 92
---
While we've only drawn the end of one strap, it's pretty obvious they overlap,
which makes it impossible to cut out, so we're going to have to address
that.
Specifically, we're going to rotate our strap out of the way until it no longer overlaps.
The rest of our bib should stay as it is, so let's start by making a list of points we need
to rotate.
However, there is a catch.
## Macros and auto-generated IDs
We have used the `round` macro to help us round the corners
of our strap, and it added a bunch of auto-generated points to our pattern. We need to
rotate these points too, but what are their names?
A macro will return the names of the things it created. So far, we have not captured
that return value, but if we did, it would look like this:
```mjs
{
"tipRightTop": {
"points": {
"start": "__macro_@freesewing/plugin-round_tipRightTop_start",
"cp1": "__macro_@freesewing/plugin-round_tipRightTop_cp1",
"cp2": "__macro_@freesewing/plugin-round_tipRightTop_cp2",
"end": "__macro_@freesewing/plugin-round_tipRightTop_end"
},
"paths": {
"path": "__macro_@freesewing/plugin-round_tipRightTop_path"
}
},
"tipRightBottom": {
"points": {
"start": "__macro_@freesewing/plugin-round_tipRightBottom_start",
"cp1": "__macro_@freesewing/plugin-round_tipRightBottom_cp1",
"cp2": "__macro_@freesewing/plugin-round_tipRightBottom_cp2",
"end": "__macro_@freesewing/plugin-round_tipRightBottom_end"
},
"paths": {
"path": "__macro_@freesewing/plugin-round_tipRightBottom_path"
}
}
}
```
Those names aren't very handy to remember. So I will rewrite this code a bit to
we'll capture these return values from the `round` macros and create
easy-to-remember points from them:
<Example tutorial caption="It looks the same as before, but now those macro points are accessible to us">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
// highlight-start
utils,
// highlight-end
macro,
part,
}) {
/*
* Construct the quarter neck opening
*/
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.hide()
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
/*
* Construct the complete neck opening
*/
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
paths.neck = new Path()
.move(points.top)
.curve(points.topCp2, points.leftCp1, points.left)
.curve(points.leftCp2, points.bottomCp1, points.bottom)
.curve(points.bottomCp2, points.rightCp1, points.right)
.curve(points.rightCp2, points.topCp1, points.top)
.close()
.addClass('fabric')
/*
* Drawing the bib outline
*/
const width = measurements.head * options.widthRatio
const length = measurements.head * options.lengthRatio
points.topLeft = new Point(
width / -2,
points.top.y - (width / 2 - points.right.x)
)
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
/*
* Shape the straps
*/
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(
points.topLeft,
0.5
)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
// Round the straps
const strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
// highlight-start
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro("round", {
id: "tipRightTop",
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
hide: false
}),
tipRightBottom: macro("round", {
id: "tipRightBottom",
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
hide: false
})
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
// highlight-end
/*
* Now, adapt our `rect` path so it's no longer a rectangle:
*/
paths.rect = new Path()
.move(points.edgeTop)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.edgeTop)
.close()
return part
}
```
</Example>
Once we have our list of points to rotate, we can rotate them. How far? Until the strap no longer overlaps.
<Example tutorial caption="It is looking pretty wonky now, but we'll deal with that next">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
utils,
macro,
part,
}) {
/*
* Construct the quarter neck opening
*/
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.hide()
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
/*
* Construct the complete neck opening
*/
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
paths.neck = new Path()
.move(points.top)
.curve(points.topCp2, points.leftCp1, points.left)
.curve(points.leftCp2, points.bottomCp1, points.bottom)
.curve(points.bottomCp2, points.rightCp1, points.right)
.curve(points.rightCp2, points.topCp1, points.top)
.close()
.addClass('fabric')
// Drawing the bib outline
const width = measurements.head * options.widthRatio
const length = measurements.head * options.lengthRatio
points.topLeft = new Point(
width / -2,
points.top.y - (width / 2 - points.right.x)
)
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
/*
* Shape the straps
*/
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(
points.topLeft,
0.5
)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
// Round the straps
const strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro("round", {
id: "tipRightTop",
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
hide: false
}),
tipRightBottom: macro("round", {
id: "tipRightBottom",
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
hide: false
})
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
// highlight-start
/*
* This is the list of points we need to rotate
* to move our strap out of the way
*/
const rotateThese = [
"edgeTopLeftCp",
"edgeTop",
"tipRight",
"tipRightTop",
"tipRightTopStart",
"tipRightTopCp1",
"tipRightTopCp2",
"tipRightTopEnd",
"tipRightBottomStart",
"tipRightBottomCp1",
"tipRightBottomCp2",
"tipRightBottomEnd",
"tipRightBottom",
"top",
"topCp2"
]
/*
* We're rotating all the points in
* the `rotateThese` array around
* the `edgeLeft` point.
*
* We're using increments of 1 degree
* until the `tipRightBottomStart` point
* is 1 mm beyond the center of our bib.
*/
while (points.tipRightBottomStart.x > -1) {
for (const p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
/*
* This is not needed
* we are merely adding it to show
* what the rotated path looks like
*/
macro("round", {
id: "showRightTop",
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
hide: false,
classes: 'contrast dotted',
})
macro("round", {
id: "showRightBottom",
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
hide: false,
classes: 'contrast dotted',
})
// highlight-end
/*
* Now, adapt our `rect` path so it's no longer a rectangle:
*/
paths.rect = new Path()
.move(points.edgeTop)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.edgeTop)
.close()
return part
}
```
</Example>

View file

@ -0,0 +1,172 @@
---
title: Completing the neck opening
order: 80
---
We've constructed the perfectly sized quarter neck, and we're going to use this
to create our complete neck path by flipping and mirroring it.
## Hiding our quarter neck opening
To make our code easier to understand, we're going to leave the `quarterNeck` path
as it is, and simply chose to not show it.
To accomplish this, we'll call the `hide()` method on our path:
<Example tutorial caption="A hidden path is not shown">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
part,
}) {
/*
* Construct the quarter neck opening
*/
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
// highlight-start
.hide()
// highlight-end
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
return part
}
```
</Example>
We're saying: _hide this path_. In other words, don't show it.
The path is still known, and we can still use it to calculate the length of the neck opening.
But it won't show up on screen or on the page.
## Create the complete neck opening
Now that we've hidden our homework, let's create the complete neck path.
As the neck opening is symmetrical, there's no need to re-calculate the points
on the other side. We can just flip them over, so to speak. And that's exactly
what we'll do.
Let's add some more points, and then construct the complete path for the neck
opening.
<Example tutorial caption="Our completed neck opening">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
part,
}) {
/*
* Construct the quarter neck opening
*/
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.hide()
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
// highlight-start
/*
* Construct the complete neck opening
*/
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
paths.neck = new Path()
.move(points.top)
.curve(points.topCp2, points.leftCp1, points.left)
.curve(points.leftCp2, points.bottomCp1, points.bottom)
.curve(points.bottomCp2, points.rightCp1, points.right)
.curve(points.rightCp2, points.topCp1, points.top)
.close()
.addClass('fabric')
// highlight-end
return part
}
```
</Example>
To add the points, we're using the `Point.flipX()` and `Point.flipY()` methods
here. There's a few new Path methods too, like `close()` and `addClass()`.
Perhaps you can figure out what they do? If not, both [the Point
documentation](/reference/api/point/) and [the Path
documentation](/reference/api/path) have detailed info on all the methods
available, including these.

View file

@ -0,0 +1,22 @@
---
title: Conclusion (of part 2)
order: 99
---
You made it to the end of part 2, in which we got to do some real parametric design.
Wile our bib is a rather simple design, it did allow us to gain familiarity with FreeSewing's core API.
You have learned how to create points. And seen various way to manipulate them,
including some more advanced things like rotating a bunch of them out of the
way.
You've also learned how to draw paths, which are the lines and curves that make up our pattern.
And we've used macros which can help us with repetitive tasks.
What we've gotten so far is a perfectly suitable sewing pattern. You can print this,
and make a nice bib out of it.
But when we stick to these basics, FreeSewing doesn't really get a chance to shine.
For that, I recommend [Part 3 of this tutorial](/tutorials/pattern-design/part3) where
we'll go beyond the basics.

View file

@ -0,0 +1,176 @@
---
title: Constructing the neck opening
order: 60
---
Our goal is to construct an oval neck opening that has a circumference
that is the `head` measurements multiplied by the `neckRatio` option.
That might involve some trial and error. But since the neck opening will be symmetric
both horizontal and vertical, we only need to construct one quadrant.
## Destructuring measurements and options
We'll be adding some points to our pattern to do just that. But we want to have
access to our measurements and options to do so. For this, we first destructure
`measurements` and `options` so we can access them:
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
// highlight-start
measurements,
options,
// highlight-end
part,
}) {
return part
}
```
Great. Now let's get to work.
## Drawing our first path
Let's add some points, and use them to draw our first curve:
<Example tutorial caption="Our very first path forms a quarter of our neck opening">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
part,
}) {
// highlight-start
/*
* Construct the quarter neck opening
*/
points.right = new Point(
measurements.head / 10,
0
)
points.bottom = new Point(
0,
measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
// highlight-end
return part
}
```
</Example>
We've added some points to our part, and drawn our first path.
Let's look at each line in detail.
## Adding points
```js
points.right = new Point(
measurements.head / 10,
0
)
```
- We're adding a point named `right` to the `points` object which holds our
part's points
- We're using the Point constructor, which takes two arguments: The point's X
and Y coordinates in the 2-dimensional space
- The X value is `measurements.head / 10`
- The Y value is `0`
The creation of `points.bottom` is very similar, so let's skip to the next line:
```js
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
```
- We're adding a point named `rightCp1`, which will become the _control point_
of the right part
- Instead of using the Point constructor, we're calling the `Point.shift()`
method on an existing point
- It takes two arguments: The angle to shift towards, and the distance
- We can see that we're shifting 90 degrees (that means up) but the distance
uses another method
- The `Point.dy()` method returns the delta along the Y axis between the point
we call it on and the point we pass it
- We shift half of the Y-delta
The next point is very similar again, except that this time we're shifting to
the right (0 degrees) for half of the X-delta between points `bottom` and
`right`.
:::tip
##### Further reading
The `Point.shift()` and `Point.dy()` are just the tip of the iceberg.
Points come with a bunch of these methods.
You can find them all in [the Point API docs](/reference/api/point/).
:::
## Adding paths
Adding points is typically merely a means to an end. And that end gets
introduced on the next line: Paths.
```js
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
```
- We're adding a path named `quarterNeck` to the `paths` object which holds our
part's paths
- We're using the Path constructor, which takes no arguments
- We're following up with a `Path.move()` call that takes one Point as argument
- Then, there's a `Path.curve()` call that takes 3 points as arguments
If you've read through the high-level [Design guide](/guides/designs) you
will have learned that paths always start with a `move()` operation. In this
case, we moved to our `right` points.
From there, we drew a cubic Bézier curve to our `bottom` point by using
`rightCp1` and `bottomCp2` as control points.
:::tip
Many of the methods in the FreeSewing API are *chainable* allowing you
to string them together like in this example.
:::
When all is said and done, we now have a quarter of our neck opening.
The only problem is, we have no guarantee whatsoever that this opening is the correct size.
Rather than hope it is the correct size, we'll make sure it is next.

View file

@ -0,0 +1,174 @@
---
title: Creating the closure
order: 91
---
Things are starting to look good, but we can't fit the bib over the baby's head like this.
So we must create a closure. We'll let the straps overlap at the end, and put in a snap
later.
## Using macros
To round the straps, we'll use something new: **a macro**. To use macros, we
need the `macro` method, which we can destructure to get access to it.
Macros are little helpers that automate things that would otherwise get rather
tedious. There are macros to add titles to our pattern, or grainline
indicators, a scalebox, and there's a macro to round corners. The `round`
macro.
:::note You can find more information on the `round` macro in [the macros docs](/reference/macros/round/).:::
We need a half circle here, but the `round` macro works on 90° angles, so
we'll use it twice. As such, we'll add some points to guide the macro, and
then put it to work.
Like our neck opening, we've only drawn half since we can simply copy the
points to the other side.
<Example tutorial caption="Now the straps overlap. Which doesn't work for a pattern as it would make it impossible to cut it out of a single piece of fabric. So let's deal with the overlap next.">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
// highlight-start
macro,
// highlight-end
part,
}) {
/*
* Construct the quarter neck opening
*/
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.hide()
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
/*
* Construct the complete neck opening
*/
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
paths.neck = new Path()
.move(points.top)
.curve(points.topCp2, points.leftCp1, points.left)
.curve(points.leftCp2, points.bottomCp1, points.bottom)
.curve(points.bottomCp2, points.rightCp1, points.right)
.curve(points.rightCp2, points.topCp1, points.top)
.close()
.addClass('fabric')
/*
* Drawing the bib outline
*/
const width = measurements.head * options.widthRatio
const length = measurements.head * options.lengthRatio
points.topLeft = new Point(
width / -2,
points.top.y - (width / 2 - points.right.x)
)
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
/*
* Shape the straps
*/
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(
points.topLeft,
0.5
)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
// highlight-start
// Round the straps
const strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
macro("round", {
id: "tipRightTop",
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
hide: false
})
macro("round", {
id: "tipRightBottom",
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
hide: false
})
// highlight-end
/*
* Now, adapt our `rect` path so it's no longer a rectangle:
*/
paths.rect = new Path()
.move(points.edgeTop)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.edgeTop)
.close()
return part
}
```
</Example>
Notice that we always draw our path at the end after we've manipulated our points.

View file

@ -0,0 +1,99 @@
---
title: A part's draft method
order: 50
---
Time to turn our attention to the draft method of our part.
Inside our `src/bib.mjs` file, this is what it currently looks like:
```design/src/bib.mjs
function draftBib({ part }) {
return part
}
```
This is an empty skeleton for a draft method. A draft method should always
return the part object, and that's effectively the only thing it currently
does.
## Destructuring the function parameter
If you're not familiar with the `({ part })` syntax you see above, this is a
technique called *parameter destructuring* or more generally, [object
destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment).
The draft method receives only 1 parameter: An object that holds everything we
need to draft our method. Destructuring is a way to *pull things out of the
object into their own variable*. It saves us a bunch of typing as these two are
equivalent:
<Tabs>
<TabItem value='without' label="Without descructuring">
```design/src/bib.mjs
function draftBib(props) {
return props.part
}
```
</TabItem>
<TabItem value='with' label="With descructuring">
```design/src/bib.mjs
function draftBib({ part }) {
return part
}
```
</TabItem>
</Tabs>
As we'll make our way through this tutorial, we'll need more and more stuff, so
we'll be pulling it out of the object passed to the draft method via
*destructuring*.
:::note
If you're new to JavaScript, and don't intuitively _get this_, stick with it. It will become second nature soon enough.
:::
## Destructuring what we need to start drawing our bib
Change the function to look like this:
```design/src/bib.mjs
function draftBib({
// highlight-start
Path,
Point,
paths,
points,
// highlight-end
part,
}) {
return part
}
```
That's a bunch of new lines, but each of one gives us something we'll use in this
tutorial.
For a complete list of what you can access via destructuring like this, refer
to [the draft method reference documentation](/reference/api/part/draft).
Here's a brief summary of the things we've added above:
- `Path`: The Path constructor, allows us to create new Paths
- `Point`: The Point constructor, allows us to create new Points
- `points`: A container object to hold the part's points
- `paths`: A container object to hold the part's paths
:::tipRemember: Constructors start with a **C**apital letter:::
Long story short: These will make it possible for us to draw points and paths easily.
So let's go ahead and do that.

View file

@ -0,0 +1,145 @@
---
title: Drawing the bib outline
order: 88
---
With our neck opening in place, let us draw the basic outline of our bib.
<Example tutorial caption="Note how the neck opening is the same distance from the left, right, and top edge">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
part,
}) {
// Construct the quarter neck opening
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.hide()
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
/*
* Construct the complete neck opening
*/
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
paths.neck = new Path()
.move(points.top)
.curve(points.topCp2, points.leftCp1, points.left)
.curve(points.leftCp2, points.bottomCp1, points.bottom)
.curve(points.bottomCp2, points.rightCp1, points.right)
.curve(points.rightCp2, points.topCp1, points.top)
.close()
.addClass('fabric')
// highlight-start
/*
* Drawing the bib outline
*/
const width = measurements.head * options.widthRatio
const length = measurements.head * options.lengthRatio
points.topLeft = new Point(
width / -2,
points.top.y - (width / 2 - points.right.x)
)
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
paths.rect = new Path()
.move(points.topLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.topRight)
.line(points.topLeft)
.close()
.addClass('fabric')
// highlight-end
return part
}
```
</Example>
First thing we did was create the `width` and `length` variables to
save ourselves some typing:
```js
const width = measurements.head * options.widthRatio
const length = measurements.head * options.lengthRatio
```
Both the length and width of our bib are a factor of the head circumference.
This way, our bib size will adapt to the size of the baby, and the user can tweak
the length and width by playing with the options we added to the pattern.
Once we have our variables, we're adding some new points, and a second path called `rect`.
```js
points.topLeft = new Point(
width / -2,
points.top.y - (width / 2 - points.right.x)
)
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
paths.rect = new Path()
.move(points.topLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.topRight)
.line(points.topLeft)
.close()
.addClass('fabric')
```
We're calculating the `topLeft` point so that the top edge of our bib
and the sides are equidistant from the neck opening.
We didn't have to do that. But it looks nicely balanced this way.

View file

@ -0,0 +1,311 @@
---
title: Drawing the straps
order: 93
---
All we have to do now is flip a bunch of points on the other side,
and create one single path that follows our bib outline.
And as we now have one path to draw the bib, we can (and should)
remove the earlier paths we drew to see what we are doing.
The `round` macro we added earlier is still required to calculate the points we
need to construct the half-circle. But we don't want it to draw the half-circle
path. As it happens, that is the default behaviour, so we merely have to remove
its `hidden: false` property.
<Example tutorial caption="It is starting to look good. But this sharp corners at the bottom don't exactly say baby, do they?">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
utils,
macro,
part,
}) {
/*
* Construct the neck opening
*/
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.hide()
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
/*
* Construct the complete neck opening
*/
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
// strikeout-start
/* Remove this path
paths.neck = new Path()
.move(points.top)
.curve(points.topCp2, points.leftCp1, points.left)
.curve(points.leftCp2, points.bottomCp1, points.bottom)
.curve(points.bottomCp2, points.rightCp1, points.right)
.curve(points.rightCp2, points.topCp1, points.top)
.close()
.addClass('fabric')
*/
// strikeout-end
// Drawing the bib outline
const width = measurements.head * options.widthRatio
const length = measurements.head * options.lengthRatio
points.topLeft = new Point(
width / -2,
points.top.y - (width / 2 - points.right.x)
)
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
/*
* Shape the straps
*/
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(
points.topLeft,
0.5
)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
// Round the straps
const strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro("round", {
id: "tipRightTop",
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
// strikeout-start
/* Remove this to have the macro
* only create the points we need
* and not draw a path
hide: false
*/
// strikeout-end
}),
tipRightBottom: macro("round", {
id: "tipRightBottom",
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
// strikeout-start
/* Remove this to have the macro
* only create the points we need
* and not draw a path
hide: false
*/
// strikeout-end
})
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
/*
* This is the list of points we need to rotate
* to move our strap out of the way
*/
const rotateThese = [
"edgeTopLeftCp",
"edgeTop",
"tipRight",
"tipRightTop",
"tipRightTopStart",
"tipRightTopCp1",
"tipRightTopCp2",
"tipRightTopEnd",
"tipRightBottomStart",
"tipRightBottomCp1",
"tipRightBottomCp2",
"tipRightBottomEnd",
"tipRightBottom",
"top",
"topCp2"
]
/*
* We're rotating all the points in
* the `rotateThese` array around
* the `edgeLeft` point.
*
* We're using increments of 1 degree
* until the `tipRightBottomStart` point
* is 1 mm beyond the center of our bib.
*/
while (points.tipRightBottomStart.x > -1) {
for (const p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
// strikeout-start
/* Remove this repetition
macro("round", {
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
prefix: "tipRightTop",
hide: false,
class: 'contrast dotted',
})
macro("round", {
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
prefix: "tipRightBottom",
hide: false,
class: 'contrast dotted',
})
paths.rect = new Path()
.move(points.edgeTop)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.edgeTop)
.close()
*/
// strikeout-end
// highlight-start
// Add points for second strap
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
points.topCp1 = points.topCp2.flipX()
points.tipLeftTopStart = points.tipRightTopStart.flipX()
points.tipLeftTopCp1 = points.tipRightTopCp1.flipX()
points.tipLeftTopCp2 = points.tipRightTopCp2.flipX()
points.tipLeftTopEnd = points.tipRightTopEnd.flipX()
points.tipLeftBottomStart = points.tipRightBottomStart.flipX()
points.tipLeftBottomCp1 = points.tipRightBottomCp1.flipX()
points.tipLeftBottomCp2 = points.tipRightBottomCp2.flipX()
points.tipLeftBottomEnd = points.tipRightBottomEnd.flipX()
// Create one path for the bib outline
paths.seam = new Path()
.move(points.edgeLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.edgeRight)
.curve(
points.edgeRightCp,
points.edgeTopRightCp,
points.tipLeftTopStart
)
.curve(
points.tipLeftTopCp1,
points.tipLeftTopCp2,
points.tipLeftTopEnd
)
.curve(
points.tipLeftBottomCp1,
points.tipLeftBottomCp2,
points.tipLeftBottomEnd
)
.curve(
points.topCp1,
points.rightCp2,
points.right
)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.curve(
points.bottomCp1,
points.leftCp2,
points.left
)
.curve(
points.leftCp1,
points.topCp2,
points.tipRightBottomEnd
)
.curve(
points.tipRightBottomCp2,
points.tipRightBottomCp1,
points.tipRightBottomStart
)
.curve(
points.tipRightTopCp2,
points.tipRightTopCp1,
points.tipRightTopStart
)
.curve(
points.edgeTopLeftCp,
points.edgeLeftCp,
points.edgeLeft
)
.close()
.addClass("fabric")
// highlight-end
return part
}
```
</Example>

View file

@ -0,0 +1,96 @@
---
title: Fitting the neck opening
order: 70
---
We are not going to create some opening that we _hope_ is the right size, we're
going to make sure it is. Here's how we'll make sure the neck opening is _just
right_:
<Example tutorial caption="It might look the same as before, but now it's just right">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
part,
}) {
/*
* Construct the quarter neck opening
*/
// highlight-start
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
// highlight-end
points.right = new Point(
// highlight-start
tweak * measurements.head / 10,
// highlight-end
0
)
points.bottom = new Point(
0,
// highlight-start
tweak * measurements.head / 12
// highlight-end
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
// highlight-start
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
// highlight-end
return part
}
```
</Example>
We've added a few new variables:
- `tweak`: A _tweak factor_ that we'll use to increase or decrease the neck
opening by making it more or less than 1
- `target`: How long our (quarter) neck opening should be
- `delta`: How far we're off. Positive numbers mean it's too long, negative
means too short
Now that we know what `target` is, we construct our path as we did before. But
this time around, we multiply our point coordinates with our `tweak` variable
(1 at the start).
Then, we compare our `target` to the result of `paths.neck.length()` which —
you guessed it — returns the length of our neck path.
If the delta is positive, our path is too long and we reduce the tweak factor.
If the delta is negative, our path is too short and we increase the tweak
factor.
We keep on doing this until `Math.abs(delta)` is less than 1. Meaning that we
are within 1 mm of our target value.
Now that we're happy with the length of our quarter neck opening, let's
complete the entire neck opening.

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View file

@ -0,0 +1,132 @@
---
title: Creating a new design
order: 10
---
The development environment has already setup various designs for us.
Since I am using the **From scratch** template, the files I want to edit live
in `design/from-scratch`.
The design's main file is `design/from-scratch/src/index.mjs`, and our bib part
will live in `design/from-scratch/src/bib.mjs`.
This `bib.mjs` file is where will be doing most of the work here in part 2 of this
tutorial. But let's start with the `index.mjs` file as an appetizer, because this
is where a new FreeSewing design is brought to life. It looks like this:
```src/index.mjs
import { Design } from '@freesewing/core'
import { i18n } from '../i18n/index.mjs'
import { bib } from './bib.mjs'
/*
* Create the design
*/
const FromScratch = new Design({
data: {
name: 'fromScratch',
version: '0.0.1',
},
parts: [bib],
})
export { bib, FromScratch, i18n }
```
Not too intimidating, is it?
## Imports
At the top of the file, we have a bunch of *imports*:
```src/index.mjs
import { Design } from '@freesewing/core'
import { i18n } from '../i18n/index.mjs'
import { bib } from './bib.mjs'
```
An `import` is how JavaScript loads code from a different file. \
It essentially says: _import **something** from **somewhere**_:
| Something | Somewhere | Description |
| ---------:|:--------- |:----------- |
| `Design` | `@freesewing/core` | Loads the `Design` constructor from FreeSewing's core library |
| `i18n` | `../i18n/index.mjs` | Loads `i18n` from the `index.mjs` file in the `i18n` one level higher (these are the translations) |
| `bib` | `./bib.mjs` | Loads `bib` from the `bib.mjs` file in the same folder (this is our part) |
As you can see, the *somewhere* can be different things. A local file like in
lines 2 and 3, or a package published on
[NPM](https://www.npmjs.com/package/@freesewing/core), in line 1.
It's nothing for you to worry about too much at this point, but it does help us
understand what's going on at the bottom of our file.
## Exports
The bottom holds a single `export` line:
```src/index.mjs
export { bib, FromScratch, i18n }
```
When we `export` things from our code, we allow others to `import` those things
for re-use in their code. That's how it works.
These are named exports. We are exporting three things:
- `bib` is our part. We are exporting it so people can re-use just this part.
- `FromScratch` is our complete design. Exporting it means people can use it.
- `i18n` are the translations. We are exporting it so people can load them to use with their own translation solution.
If you are not familiar with this syntax, you'll get the hang of it soon enough.
## Design constructor
Finally, the most interesting part of this file is the middle part where we are
creating a new design:
```src/index.mjs
const FromScratch = new Design({
data: {
name: 'fromScratch',
version: '0.0.1',
},
parts: [bib],
})
```
The `Design` that we imported on line 1 is a so-called **constructor**.
A constructor is a function that can create things out of nothing. Or,
to be more accurate, that you can use with the `new` keyword.
:::tip
It's a convention that constructor names start with an **C**apital letter.
:::
We are passing some info to this `Design` function, but the `data` we are
passing is optional. If we strip that away for a moment, and don't bother
assigning the result to a variable, we reveal the essence of what it takes to
create a new FreeSewing design:
```src/index.mjs
new Design({
parts: [bib],
})
```
In several places in this documentation, I will mention that *a design is not
much more than a thin wrapper around parts*. But I feel nothing drives
that point home like seeing it in code like this.
To create a new design, we call the `Design` constructor and give it a list
(an array) or parts.
:::note RELATED
Refer to [the design reference documentation](/reference/api/design) for
all details about what you can pass to the Design constructor.
:::
That's it. So let's look at where the real action is next: Our first part.

View file

@ -0,0 +1,89 @@
---
title: Creating a part
order: 20
---
Much like garments themselves, patterns are made up of _parts_.
Most patterns will have multiple parts. A sleeve, a back part, the collar, and
so on. The pattern you create today is very simple, and only has one part: the bib.
:::tip
It's a good idea to keep each part in its own file. You don't *have to* do
this, but it's a good habit to get into.
:::
## bib.mjs
I am going to use the **From scratch** template. So the files I want to edit live
in `design/from-scratch`.
Our part lives in `design/from-scratch/src/bib.mjs`, and it currently looks like this:
```src/bib.mjs
function draftBib({ part }) {
return part
}
export const bib = {
name: 'fromscratch.bib',
draft: draftBib,
}
```
## The part object
Each part in FreeSewing is an object that describes the part, and has a `draft`
method to do the actual work of drafting the part.
The only mandatory keys on a part object are `name` and `draft`.
:::note RELATED
Refer to [the part reference documentation](/reference/api/part) for
all details about configuring the part object
:::
:::note
A design in FreeSewing is a thin wrapper around a collection of parts.
Each parts stands on its own, and parts from various designs can be combined
to create other designs.
:::
### The part name
```src/bib.mjs
name: 'fromscratch.bib',
```
A part's `name` should be unique in a design. I used `fromscratch.bib` as the
name, because that makes sense.
:::warning
I **strongly** recommend that you apply the same `designName.partName` naming scheme in all your parts.
This avoids naming conflicts when mixing parts from various designs to create a new design.
:::
### The part's draft method
```src/bib.mjs
draft: draftBib,
```
The second mandatory key on the part object is `draft` which should hold the method that drafts the part.
In the example above, it refers to the `draftBib` function that was defined at the top of the same `bib.mjs` file.
For now this function doesn't do much, but that will change soon enough.
:::note
This structure of putting the draft method at the top of the file and
the part object at the bottom is a bit of a convention in FreeSewing.
:::
We'll take a deeper look at our part's draft method soon. For now, let's look at adding measurements to our part.

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

View file

@ -0,0 +1,47 @@
---
title: "Part 2: Parametric design"
---
Welcome to part 2 of this FreeSewing pattern design tutorial.
In this part I will show you how to design a bespoke
sewing pattern, start to finish.
:::tip
##### Before you start
This tutorial assumes you are familiar with the following:
- Scalable Vector Graphics
- The coordinate system
- Units in FreeSewing
- Cubic Bézier curves
Which is a lot to assume. So if you'd like you can take a quick detour
via our [Before you start guide](/guides/prerequisites). \
It's very short, but covers some basic
terminology and concepts that we'll use throughout this guide.
:::
## Pick a template
The FreeSewing development environment ships with several templates you
can start from. I recommend you start **From scratch** as you'll learn the most
that way. But you can also start from the **Tutorial** template in which case
you will already have the end result we are aiming for today:
<div class="grid grid-cols-2 gap-2">
!['From scratch' template](./fromscratch.png)
!['Tutorial' template](./tutorial.png)
</div>
Depending on the choice you made, you will need to edit files in a different folder.
- Edit files in `design/from-scratch` if you are using the **From scratch** template
- Edit files in `design/tutorial` if you are using the **Tutorial** template
You can choose either, or even switch back and forth between both.
To follow along step-by-step with the tutorial, go to the `design/from-scratch` folder.

View file

@ -0,0 +1,288 @@
---
title: Rounding the corners
order: 94
---
We already know how to round corners, we'll have the `round` macro take care of that for us.
With our corners rounded, we should also update our path.
Fortunately, we merely have to update the start of it.
<Example tutorial caption="The shape of our bib is now completed">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
utils,
macro,
part,
}) {
/*
* Construct the quarter neck opening
*/
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.hide()
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
/*
* Construct the complete neck opening
*/
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
// Drawing the bib outline
const width = measurements.head * options.widthRatio
const length = measurements.head * options.lengthRatio
points.topLeft = new Point(
width / -2,
points.top.y - (width / 2 - points.right.x)
)
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
/*
* Shape the straps
*/
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(
points.topLeft,
0.5
)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
// Round the straps
const strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro("round", {
id: "tipRightTop",
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
}),
tipRightBottom: macro("round", {
id: "tipRightBottom",
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
})
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
/*
* This is the list of points we need to rotate
* to move our strap out of the way
*/
const rotateThese = [
"edgeTopLeftCp",
"edgeTop",
"tipRight",
"tipRightTop",
"tipRightTopStart",
"tipRightTopCp1",
"tipRightTopCp2",
"tipRightTopEnd",
"tipRightBottomStart",
"tipRightBottomCp1",
"tipRightBottomCp2",
"tipRightBottomEnd",
"tipRightBottom",
"top",
"topCp2"
]
/*
* We're rotating all the points in
* the `rotateThese` array around
* the `edgeLeft` point.
*
* We're using increments of 1 degree
* until the `tipRightBottomStart` point
* is 1 mm beyond the center of our bib.
*/
while (points.tipRightBottomStart.x > -1) {
for (const p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
// Add points for second strap
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
points.topCp1 = points.topCp2.flipX()
points.tipLeftTopStart = points.tipRightTopStart.flipX()
points.tipLeftTopCp1 = points.tipRightTopCp1.flipX()
points.tipLeftTopCp2 = points.tipRightTopCp2.flipX()
points.tipLeftTopEnd = points.tipRightTopEnd.flipX()
points.tipLeftBottomStart = points.tipRightBottomStart.flipX()
points.tipLeftBottomCp1 = points.tipRightBottomCp1.flipX()
points.tipLeftBottomCp2 = points.tipRightBottomCp2.flipX()
points.tipLeftBottomEnd = points.tipRightBottomEnd.flipX()
// highlight-start
/*
* Round the bottom corners
* Macros will return the auto-generated IDs
*/
const ids2 = {
bottomLeft: macro("round", {
id: "bottomLeft",
from: points.topLeft,
to: points.bottomRight,
via: points.bottomLeft,
radius: points.bottomRight.x / 4,
}),
bottomRight: macro("round", {
id: "bottomRight",
from: points.bottomLeft,
to: points.topRight,
via: points.bottomRight,
radius: points.bottomRight.x / 4,
})
}
/*
* Create points from them with easy names
*/
for (const side in ids2) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids2[side].points[id]].copy()
}
}
// highlight-end
// Create one path for the bib outline
paths.seam = new Path()
.move(points.edgeLeft)
// strikeout-start
/* We only need to replace the start
* with the new lines below
.line(points.bottomLeft)
.line(points.bottomRight)
*/
// strikeout-end
// highlight-start
.line(points.bottomLeftStart)
.curve(points.bottomLeftCp1, points.bottomLeftCp2, points.bottomLeftEnd)
.line(points.bottomRightStart)
.curve(points.bottomRightCp1, points.bottomRightCp2, points.bottomRightEnd)
// highlight-end
.line(points.edgeRight)
.curve(
points.edgeRightCp,
points.edgeTopRightCp,
points.tipLeftTopStart
)
.curve(
points.tipLeftTopCp1,
points.tipLeftTopCp2,
points.tipLeftTopEnd
)
.curve(
points.tipLeftBottomCp1,
points.tipLeftBottomCp2,
points.tipLeftBottomEnd
)
.curve(
points.topCp1,
points.rightCp2,
points.right
)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.curve(
points.bottomCp1,
points.leftCp2,
points.left
)
.curve(
points.leftCp1,
points.topCp2,
points.tipRightBottomEnd
)
.curve(
points.tipRightBottomCp2,
points.tipRightBottomCp1,
points.tipRightBottomStart
)
.curve(
points.tipRightTopCp2,
points.tipRightTopCp1,
points.tipRightTopStart
)
.curve(
points.edgeTopLeftCp,
points.edgeLeftCp,
points.edgeLeft
)
.close()
.addClass("fabric")
return part
}
```
</Example>

View file

@ -0,0 +1,153 @@
---
title: Shaping the straps
order: 90
---
Our straps should follow the neck opening, which isn't that hard to do.
We just need to keep the control points of our curves at similar proportions.
Which means, halfway between the start of the curve, and the corner of our rectangle.
:::note
For this, we'll be using a new method: `Point.shiftFractionTowards()`. We've already
used `Point.shift()` and there's also `Point.shiftTowards()` and `Point.shiftOutwards()`.
As always, [the API docs](/reference/api/point/) have all the details.
:::
<Example tutorial caption="All of a sudden, things are starting to look like a bib">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
measurements,
options,
part,
}) {
/*
* Construct the quarter neck opening
*/
let tweak = 1
let target = (measurements.head * options.neckRatio) /4
let delta
do {
points.right = new Point(
tweak * measurements.head / 10,
0
)
points.bottom = new Point(
0,
tweak * measurements.head / 12
)
points.rightCp1 = points.right.shift(
90,
points.bottom.dy(points.right) / 2
)
points.bottomCp2 = points.bottom.shift(
0,
points.bottom.dx(points.right) / 2
)
paths.quarterNeck = new Path()
.move(points.right)
.curve(
points.rightCp1,
points.bottomCp2,
points.bottom
)
.hide()
delta = paths.quarterNeck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
/*
* Construct the complete neck opening
*/
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
paths.neck = new Path()
.move(points.top)
.curve(points.topCp2, points.leftCp1, points.left)
.curve(points.leftCp2, points.bottomCp1, points.bottom)
.curve(points.bottomCp2, points.rightCp1, points.right)
.curve(points.rightCp2, points.topCp1, points.top)
.close()
.addClass('fabric')
/*
* Drawing the bib outline
*/
const width = measurements.head * options.widthRatio
const length = measurements.head * options.lengthRatio
points.topLeft = new Point(
width / -2,
points.top.y - (width / 2 - points.right.x)
)
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
// strikeout-start
/*
* Remove this path
paths.rect = new Path()
.move(points.topLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.topRight)
.line(points.topLeft)
.close()
.addClass('fabric')
*/
// strikeout-end
// highlight-start
/*
* Shape the straps
*/
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(
points.topLeft,
0.5
)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
/*
* Now, adapt our `rect` path so it's no longer a rectangle:
*/
paths.rect = new Path()
.move(points.edgeTop)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.line(points.bottomLeft)
.line(points.bottomRight)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.edgeTop)
.close()
// highlight-end
return part
}
```
</Example>

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

View file

@ -0,0 +1,527 @@
---
title: Adding annotations
order: 20
---
Our pattern is still a little bit *bare*. It would be nice to add some *annotations* to it.
When I say *annotations* it's an umbrella term for things like text or other
bits of information that help the user understand the pattern.
## Adding snippets
Snippets are little re-useable things to embellish our pattern with.
Things like buttons or buttonholes, a logo, or snaps.
To use them, much like points and paths, we need to destructure both
the `Snippet` constructor as well as the `snippets` object to hold
our snippets.
We'll be using them below to add a `snap-stud`, `snap-socket`, and a `logo`
snippet.
:::note
You can find all possible snippets in [our documentation](/reference/api/snippet/).
:::
<Example previewFirst tutorial caption="This looks better">
```design/src/bib.mjs
function draftBib({
Point,
points,
Path,
paths,
utils,
measurements,
options,
macro,
// highlight-start
Snippet,
snippets,
// highlight-end
part,
}) {
/*
* Construct the neck opening
*/
const target = (measurements.head * options.neckRatio) / 4
let tweak = 1
let delta
do {
points.right = new Point((tweak * measurements.head) / 10, 0)
points.bottom = new Point(0, (tweak * measurements.head) / 12)
points.rightCp1 = points.right.shift(90, points.bottom.dy(points.right) / 2)
points.bottomCp2 = points.bottom.shift(0, points.bottom.dx(points.right) / 2)
paths.neck = new Path()
.move(points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
delta = paths.neck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
/*
* Construct the outline
*/
let width = measurements.head * options.widthRatio
let length = measurements.head * options.lengthRatio
points.topLeft = new Point(width / -2, points.top.y - (width / 2 - points.right.x))
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(points.topLeft, 0.5)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
/*
* Round the end of the straps
*/
let strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro('round', {
id: 'tipRightTop',
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
}),
tipRightBottom: macro('round', {
id: 'tipRightBottom',
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
/*
* Rotate straps so they don't overlap
*/
let rotateThese = [
'edgeTopLeftCp',
'edgeTop',
'tipRight',
'tipRightTop',
'tipRightTopStart',
'tipRightTopCp1',
'tipRightTopCp2',
'tipRightTopEnd',
'tipRightBottomStart',
'tipRightBottomCp1',
'tipRightBottomCp2',
'tipRightBottomEnd',
'tipRightBottom',
'top',
'topCp2',
]
while (points.tipRightBottomStart.x > -1) {
for (let p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
/*
* Add points to anchor snaps on
*/
points.snapLeft = points.top.shiftFractionTowards(points.edgeTop, 0.5)
/*
* Mirror points to the other side
*/
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
points.topCp1 = points.topCp2.flipX()
points.tipLeftTopStart = points.tipRightTopStart.flipX()
points.tipLeftTopCp1 = points.tipRightTopCp1.flipX()
points.tipLeftTopCp2 = points.tipRightTopCp2.flipX()
points.tipLeftTopEnd = points.tipRightTopEnd.flipX()
points.tipLeftBottomStart = points.tipRightBottomStart.flipX()
points.tipLeftBottomCp1 = points.tipRightBottomCp1.flipX()
points.tipLeftBottomCp2 = points.tipRightBottomCp2.flipX()
points.tipLeftBottomEnd = points.tipRightBottomEnd.flipX()
points.snapRight = points.snapLeft.flipX()
/*
* Round the bottom of the bib
* Radius is fixed, but you could use an option for it)
*
* Macros will return the auto-generated IDs
*/
const ids2 = {
bottomLeft: macro('round', {
id: 'bottomLeft',
from: points.topLeft,
to: points.bottomRight,
via: points.bottomLeft,
radius: points.bottomRight.x / 4,
}),
bottomRight: macro('round', {
id: 'bottomRight',
from: points.bottomLeft,
to: points.topRight,
via: points.bottomRight,
radius: points.bottomRight.x / 4,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids2) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids2[side].points[id]].copy()
}
}
/*
* Construct the path
*/
paths.seam = new Path()
.move(points.edgeLeft)
.line(points.bottomLeftStart)
.curve(points.bottomLeftCp1, points.bottomLeftCp2, points.bottomLeftEnd)
.line(points.bottomRightStart)
.curve(points.bottomRightCp1, points.bottomRightCp2, points.bottomRightEnd)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.tipLeftTopStart)
.curve(points.tipLeftTopCp1, points.tipLeftTopCp2, points.tipLeftTopEnd)
.curve(points.tipLeftBottomCp1, points.tipLeftBottomCp2, points.tipLeftBottomEnd)
.curve(points.topCp1, points.rightCp2, points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
.curve(points.bottomCp1, points.leftCp2, points.left)
.curve(points.leftCp1, points.topCp2, points.tipRightBottomEnd)
.curve(points.tipRightBottomCp2, points.tipRightBottomCp1, points.tipRightBottomStart)
.curve(points.tipRightTopCp2, points.tipRightTopCp1, points.tipRightTopStart)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.close()
.attr('class', 'fabric')
// highlight-start
/*
*
* Annotations
*
*/
/*
* Add the snaps
*/
snippets.snapStud = new Snippet('snap-stud', points.snapLeft)
snippets.snapSocket = new Snippet('snap-socket', points.snapRight).attr('opacity', 0.5)
/*
* Add the logo
*/
points.logo = new Point(0, 0)
snippets.logo = new Snippet('logo', points.logo)
// highlight-end
return part
}
```
</Example>
## Setting the cutlist, adding a title and scalebox
We are also going to add a title and scalebox. For both of these we will use a macro.
The `title` and `scalebox` macros to be precise.
Before we add the title, we will also set the cutlist via a method that was added to the
store by one of the core plugins.
As a matter of fact, all of these snippets, macros, and store methods are provided by plugins.
For more details, [refer to the plugin guide](/guides/plugins).
<Example previewFirst tutorial caption="This looks way better">
```design/src/bib.mjs
function draftBib({
Point,
points,
Path,
paths,
utils,
measurements,
options,
macro,
complete,
snippets,
Snippet,
// highlight-start
store,
// highlight-end
part,
}) {
/*
* Construct the neck opening
*/
const target = (measurements.head * options.neckRatio) / 4
let tweak = 1
let delta
do {
points.right = new Point((tweak * measurements.head) / 10, 0)
points.bottom = new Point(0, (tweak * measurements.head) / 12)
points.rightCp1 = points.right.shift(90, points.bottom.dy(points.right) / 2)
points.bottomCp2 = points.bottom.shift(0, points.bottom.dx(points.right) / 2)
paths.neck = new Path()
.move(points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
delta = paths.neck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
/*
* Construct the outline
*/
let width = measurements.head * options.widthRatio
let length = measurements.head * options.lengthRatio
points.topLeft = new Point(width / -2, points.top.y - (width / 2 - points.right.x))
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(points.topLeft, 0.5)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
/*
* Round the end of the straps
*/
let strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro('round', {
id: 'tipRightTop',
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
}),
tipRightBottom: macro('round', {
id: 'tipRightBottom',
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
/*
* Rotate straps so they don't overlap
*/
let rotateThese = [
'edgeTopLeftCp',
'edgeTop',
'tipRight',
'tipRightTop',
'tipRightTopStart',
'tipRightTopCp1',
'tipRightTopCp2',
'tipRightTopEnd',
'tipRightBottomStart',
'tipRightBottomCp1',
'tipRightBottomCp2',
'tipRightBottomEnd',
'tipRightBottom',
'top',
'topCp2',
]
while (points.tipRightBottomStart.x > -1) {
for (let p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
/*
* Add points to anchor snaps on
*/
points.snapLeft = points.top.shiftFractionTowards(points.edgeTop, 0.5)
/*
* Mirror points to the other side
*/
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
points.topCp1 = points.topCp2.flipX()
points.tipLeftTopStart = points.tipRightTopStart.flipX()
points.tipLeftTopCp1 = points.tipRightTopCp1.flipX()
points.tipLeftTopCp2 = points.tipRightTopCp2.flipX()
points.tipLeftTopEnd = points.tipRightTopEnd.flipX()
points.tipLeftBottomStart = points.tipRightBottomStart.flipX()
points.tipLeftBottomCp1 = points.tipRightBottomCp1.flipX()
points.tipLeftBottomCp2 = points.tipRightBottomCp2.flipX()
points.tipLeftBottomEnd = points.tipRightBottomEnd.flipX()
points.snapRight = points.snapLeft.flipX()
/*
* Round the bottom of the bib
* Radius is fixed, but you could use an option for it)
*
* Macros will return the auto-generated IDs
*/
const ids2 = {
bottomLeft: macro('round', {
id: 'bottomLeft',
from: points.topLeft,
to: points.bottomRight,
via: points.bottomLeft,
radius: points.bottomRight.x / 4,
}),
bottomRight: macro('round', {
id: 'bottomRight',
from: points.bottomLeft,
to: points.topRight,
via: points.bottomRight,
radius: points.bottomRight.x / 4,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids2) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids2[side].points[id]].copy()
}
}
/*
* Construct the path
*/
paths.seam = new Path()
.move(points.edgeLeft)
.line(points.bottomLeftStart)
.curve(points.bottomLeftCp1, points.bottomLeftCp2, points.bottomLeftEnd)
.line(points.bottomRightStart)
.curve(points.bottomRightCp1, points.bottomRightCp2, points.bottomRightEnd)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.tipLeftTopStart)
.curve(points.tipLeftTopCp1, points.tipLeftTopCp2, points.tipLeftTopEnd)
.curve(points.tipLeftBottomCp1, points.tipLeftBottomCp2, points.tipLeftBottomEnd)
.curve(points.topCp1, points.rightCp2, points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
.curve(points.bottomCp1, points.leftCp2, points.left)
.curve(points.leftCp1, points.topCp2, points.tipRightBottomEnd)
.curve(points.tipRightBottomCp2, points.tipRightBottomCp1, points.tipRightBottomStart)
.curve(points.tipRightTopCp2, points.tipRightTopCp1, points.tipRightTopStart)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.close()
.attr('class', 'fabric')
/*
*
* Annotations
*
*/
// highlight-start
/*
* Cut list
*/
store.cutlist.addCut({ cut: 1, from: 'fabric' })
// highlight-end
/*
* Add the snaps
*/
snippets.snapStud = new Snippet('snap-stud', points.snapLeft)
snippets.snapSocket = new Snippet('snap-socket', points.snapRight).attr('opacity', 0.5)
// highlight-start
/*
* Add the title
*/
points.title = points.bottom.shift(-90, 45)
macro('title', {
at: points.title,
nr: 1,
title: 'bib',
align: 'center',
scale: 0.8,
})
/*
* Add the scalebox
*/
points.scalebox = points.title.shift(-90, 65)
macro('scalebox', { at: points.scalebox })
// highlight-end
/*
* Add the logo
*/
points.logo = new Point(0, 0)
snippets.logo = new Snippet('logo', points.logo)
return part
}
```
</Example>

View file

@ -0,0 +1,561 @@
---
title: Dealing with laser cutters
order: 25
---
Laser cutters is merely an example of a situation where your user wants not the
complete detailed pattern with all annotations, but just the outlines.
Essentially what we had at the end of part 2 of this tutorial.
Since then, we've added a bunch of embellishments, and perhaps the user does
not want those.
Well, good news: there is a setting for that too. That setting is `complete`,
and more annotations will automatically take it into account.
For example, if you put a logo or title on the pattern, it will check the
`complete` setting and if it is `false` it will do nothing.
When we say we're going to *complete* our pattern, we mean we're going to add
things like a title and a scalebox and so on.
So while in most scenarios you don't have to worry about `complete`, you
should keep it in mind when you are adding text or paths to the design
that should not be shown on a non-complete pattern.
An example will make this more clear:
## Adding an indicator for bias tape
<Example tutorial previewFirst caption="A nice note about bias tape">
```design/src/bib.mjs
function draftBib({
Point,
points,
Path,
paths,
utils,
measurements,
options,
macro,
Snippet,
snippets,
store,
// highlight-start
complete,
// highlight-end
part,
}) {
/*
* Construct the neck opening
*/
const target = (measurements.head * options.neckRatio) / 4
let tweak = 1
let delta
do {
points.right = new Point((tweak * measurements.head) / 10, 0)
points.bottom = new Point(0, (tweak * measurements.head) / 12)
points.rightCp1 = points.right.shift(90, points.bottom.dy(points.right) / 2)
points.bottomCp2 = points.bottom.shift(0, points.bottom.dx(points.right) / 2)
paths.neck = new Path()
.move(points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
delta = paths.neck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
/*
* Construct the outline
*/
let width = measurements.head * options.widthRatio
let length = measurements.head * options.lengthRatio
points.topLeft = new Point(width / -2, points.top.y - (width / 2 - points.right.x))
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(points.topLeft, 0.5)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
/*
* Round the end of the straps
*/
let strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro('round', {
id: 'tipRightTop',
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
}),
tipRightBottom: macro('round', {
id: 'tipRightBottom',
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
/*
* Rotate straps so they don't overlap
*/
let rotateThese = [
'edgeTopLeftCp',
'edgeTop',
'tipRight',
'tipRightTop',
'tipRightTopStart',
'tipRightTopCp1',
'tipRightTopCp2',
'tipRightTopEnd',
'tipRightBottomStart',
'tipRightBottomCp1',
'tipRightBottomCp2',
'tipRightBottomEnd',
'tipRightBottom',
'top',
'topCp2',
]
while (points.tipRightBottomStart.x > -1) {
for (let p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
/*
* Add points to anchor snaps on
*/
points.snapLeft = points.top.shiftFractionTowards(points.edgeTop, 0.5)
/*
* Mirror points to the other side
*/
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
points.topCp1 = points.topCp2.flipX()
points.tipLeftTopStart = points.tipRightTopStart.flipX()
points.tipLeftTopCp1 = points.tipRightTopCp1.flipX()
points.tipLeftTopCp2 = points.tipRightTopCp2.flipX()
points.tipLeftTopEnd = points.tipRightTopEnd.flipX()
points.tipLeftBottomStart = points.tipRightBottomStart.flipX()
points.tipLeftBottomCp1 = points.tipRightBottomCp1.flipX()
points.tipLeftBottomCp2 = points.tipRightBottomCp2.flipX()
points.tipLeftBottomEnd = points.tipRightBottomEnd.flipX()
points.snapRight = points.snapLeft.flipX()
/*
* Round the bottom of the bib
* Radius is fixed, but you could use an option for it)
*
* Macros will return the auto-generated IDs
*/
const ids2 = {
bottomLeft: macro('round', {
id: 'bottomLeft',
from: points.topLeft,
to: points.bottomRight,
via: points.bottomLeft,
radius: points.bottomRight.x / 4,
}),
bottomRight: macro('round', {
id: 'bottomRight',
from: points.bottomLeft,
to: points.topRight,
via: points.bottomRight,
radius: points.bottomRight.x / 4,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids2) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids2[side].points[id]].copy()
}
}
/*
* Construct the path
*/
paths.seam = new Path()
.move(points.edgeLeft)
.line(points.bottomLeftStart)
.curve(points.bottomLeftCp1, points.bottomLeftCp2, points.bottomLeftEnd)
.line(points.bottomRightStart)
.curve(points.bottomRightCp1, points.bottomRightCp2, points.bottomRightEnd)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.tipLeftTopStart)
.curve(points.tipLeftTopCp1, points.tipLeftTopCp2, points.tipLeftTopEnd)
.curve(points.tipLeftBottomCp1, points.tipLeftBottomCp2, points.tipLeftBottomEnd)
.curve(points.topCp1, points.rightCp2, points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
.curve(points.bottomCp1, points.leftCp2, points.left)
.curve(points.leftCp1, points.topCp2, points.tipRightBottomEnd)
.curve(points.tipRightBottomCp2, points.tipRightBottomCp1, points.tipRightBottomStart)
.curve(points.tipRightTopCp2, points.tipRightTopCp1, points.tipRightTopStart)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.close()
.attr('class', 'fabric')
/*
*
* Annotations
*
*/
/*
* Cut list
*/
store.cutlist.addCut({ cut: 1, from: 'fabric' })
/*
* Add the snaps
*/
snippets.snapStud = new Snippet('snap-stud', points.snapLeft)
snippets.snapSocket = new Snippet('snap-socket', points.snapRight).attr('opacity', 0.5)
// highlight-start
/*
* Add the bias tape
*/
if (complete)
paths.bias = paths.seam
.offset(-5)
.addClass('note dashed')
.addText('tutorial:finishWithBiasTape', 'center fill-note')
// highlight-end
/*
* Add the title
*/
points.title = points.bottom.shift(-90, 45)
macro('title', {
at: points.title,
nr: 1,
title: 'bib',
align: 'center',
scale: 0.8,
})
/*
* Add the scalebox
*/
points.scalebox = points.title.shift(-90, 65)
macro('scalebox', { at: points.scalebox })
/*
* Add the logo
*/
points.logo = new Point(0, 0)
snippets.logo = new Snippet('logo', points.logo)
return part
}
```
</Example>
And here is the exact same thing, but withe `complete` setting set to `false`:
<Example tutorial previewFirst settings="complete: false" caption="Same code, but with complete disabled">
```design/src/bib.mjs
function draftBib({
Point,
points,
Path,
paths,
utils,
measurements,
options,
macro,
Snippet,
snippets,
store,
// highlight-start
complete,
// highlight-end
part,
}) {
/*
* Construct the neck opening
*/
const target = (measurements.head * options.neckRatio) / 4
let tweak = 1
let delta
do {
points.right = new Point((tweak * measurements.head) / 10, 0)
points.bottom = new Point(0, (tweak * measurements.head) / 12)
points.rightCp1 = points.right.shift(90, points.bottom.dy(points.right) / 2)
points.bottomCp2 = points.bottom.shift(0, points.bottom.dx(points.right) / 2)
paths.neck = new Path()
.move(points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
delta = paths.neck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
/*
* Construct the outline
*/
let width = measurements.head * options.widthRatio
let length = measurements.head * options.lengthRatio
points.topLeft = new Point(width / -2, points.top.y - (width / 2 - points.right.x))
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(points.topLeft, 0.5)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
/*
* Round the end of the straps
*/
let strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro('round', {
id: 'tipRightTop',
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
}),
tipRightBottom: macro('round', {
id: 'tipRightBottom',
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
/*
* Rotate straps so they don't overlap
*/
let rotateThese = [
'edgeTopLeftCp',
'edgeTop',
'tipRight',
'tipRightTop',
'tipRightTopStart',
'tipRightTopCp1',
'tipRightTopCp2',
'tipRightTopEnd',
'tipRightBottomStart',
'tipRightBottomCp1',
'tipRightBottomCp2',
'tipRightBottomEnd',
'tipRightBottom',
'top',
'topCp2',
]
while (points.tipRightBottomStart.x > -1) {
for (let p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
/*
* Add points to anchor snaps on
*/
points.snapLeft = points.top.shiftFractionTowards(points.edgeTop, 0.5)
/*
* Mirror points to the other side
*/
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
points.topCp1 = points.topCp2.flipX()
points.tipLeftTopStart = points.tipRightTopStart.flipX()
points.tipLeftTopCp1 = points.tipRightTopCp1.flipX()
points.tipLeftTopCp2 = points.tipRightTopCp2.flipX()
points.tipLeftTopEnd = points.tipRightTopEnd.flipX()
points.tipLeftBottomStart = points.tipRightBottomStart.flipX()
points.tipLeftBottomCp1 = points.tipRightBottomCp1.flipX()
points.tipLeftBottomCp2 = points.tipRightBottomCp2.flipX()
points.tipLeftBottomEnd = points.tipRightBottomEnd.flipX()
points.snapRight = points.snapLeft.flipX()
/*
* Round the bottom of the bib
* Radius is fixed, but you could use an option for it)
*
* Macros will return the auto-generated IDs
*/
const ids2 = {
bottomLeft: macro('round', {
id: 'bottomLeft',
from: points.topLeft,
to: points.bottomRight,
via: points.bottomLeft,
radius: points.bottomRight.x / 4,
}),
bottomRight: macro('round', {
id: 'bottomRight',
from: points.bottomLeft,
to: points.topRight,
via: points.bottomRight,
radius: points.bottomRight.x / 4,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids2) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids2[side].points[id]].copy()
}
}
/*
* Construct the path
*/
paths.seam = new Path()
.move(points.edgeLeft)
.line(points.bottomLeftStart)
.curve(points.bottomLeftCp1, points.bottomLeftCp2, points.bottomLeftEnd)
.line(points.bottomRightStart)
.curve(points.bottomRightCp1, points.bottomRightCp2, points.bottomRightEnd)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.tipLeftTopStart)
.curve(points.tipLeftTopCp1, points.tipLeftTopCp2, points.tipLeftTopEnd)
.curve(points.tipLeftBottomCp1, points.tipLeftBottomCp2, points.tipLeftBottomEnd)
.curve(points.topCp1, points.rightCp2, points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
.curve(points.bottomCp1, points.leftCp2, points.left)
.curve(points.leftCp1, points.topCp2, points.tipRightBottomEnd)
.curve(points.tipRightBottomCp2, points.tipRightBottomCp1, points.tipRightBottomStart)
.curve(points.tipRightTopCp2, points.tipRightTopCp1, points.tipRightTopStart)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.close()
.attr('class', 'fabric')
/*
*
* Annotations
*
*/
/*
* Cut list
*/
store.cutlist.addCut({ cut: 1, from: 'fabric' })
/*
* Add the snaps
*/
snippets.snapStud = new Snippet('snap-stud', points.snapLeft)
snippets.snapSocket = new Snippet('snap-socket', points.snapRight).attr('opacity', 0.5)
// highlight-start
/*
* Add the bias tape
*/
if (complete)
paths.bias = paths.seam
.offset(-5)
.addClass('note dashed')
.addText('tutorial:finishWithBiasTape', 'center fill-note')
// highlight-end
/*
* Add the title
*/
points.title = points.bottom.shift(-90, 45)
macro('title', {
at: points.title,
nr: 1,
title: 'bib',
align: 'center',
scale: 0.8,
})
/*
* Add the scalebox
*/
points.scalebox = points.title.shift(-90, 65)
macro('scalebox', { at: points.scalebox })
/*
* Add the logo
*/
points.logo = new Point(0, 0)
snippets.logo = new Snippet('logo', points.logo)
return part
}
```
</Example>

View file

@ -0,0 +1,39 @@
---
title: Conclusion
order: 90
---
Congratulations, we have created our first pattern. And while it's arguably
rather simple, we have learned a bunch of things along the way.
## More reading material
- If you haven't done so already, read through [the design
guide](/guides/designs/) which provides a good overview of how designs work
under the hood
- Bookmark [the FreeSewing API docs](/reference/api/), they are your reference
every time you're not entirely certain how something works
- Have a look at [the pattern design best practices](/guides/best-practices/) for best practices
that will help you make the best possible patterns
## What to do next
Now that you have learned how to create a pattern, why don't you make one?
Think about what it is you would like to create, and just do it. If you get
stuck at any moment, or need some help or advice, you can [join our chat
room](https://discord.freesewing.org/) and we'll help you out.
:::note COMMENT (by joost)
##### How did I do?
You could do me a real favor by letting me know what you loved or hated about
this tutorial.
Were there areas that were not clear? Did I dwell too long on one topic, or
rushed through another one too quickly? Your feedback helps improve things,
so don't be shy and tell me what you think.
You can reach me at joost@freesewing.org
:::

View file

@ -0,0 +1,53 @@
---
title: Saving space (and trees) with exand
order: 40
---
There is one more way we like to save space (and trees): The `expand` setting.
The `expand` setting (which is true by default) indicates that user wants pattern
parts to be fully expanded.
It's common to reduce the amount of space required by reducing simple shapes like
rectangles to a description or cut things on the fold.
The `exand` setting gives the user control over this, at least insofar the designer
supports it. It's not relevant to our bib, but it's good to know it's there.
Here is an example snippets from [Aaron's arm binding](https://github.com/freesewing/freesewing/blob/develop/designs/aaron/src/arm-binding.mjs#L24):
```mjs
const w = store.get('bindingWidth')
const l = store.get('armBindingLength')
if (expand) {
store.flag.preset('expandIsOn')
} else {
// Expand is off, do not draw the part but flag this to the user
store.flag.note({
msg: `aaron:cutArmBinding`,
replace: {
width: units(w),
length: units(l),
},
suggest: {
text: 'flag:show',
icon: 'expand',
update: {
settings: ['expand', 1],
},
},
})
// Also hint about expand
store.flag.preset('expand')
return part.hide()
}
```
There's some stuff going on here that we'll learn next, but essentially if
`expand` is falsy, the part will hide itself with `part.hide()` and so it won't
be included on the pattern.
However, to avoid misunderstandings we want to inform the user about this.
That's what all those `store.flag` lines are about. We'll learn about then next.

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -0,0 +1,343 @@
---
title: How to communicate to the user
order: 50
---
As a designer, there are times you want to bring something to the attention of
the user. I am not talking about generic information that can go in the
documentation, but rather a message that is tailored specifically to this
pattern, much like this pattern is specifically tailored to the user.
Doing so is possible with the various `store.flag` methods, and below is
our updated bib making use of this. It's important to realize that things
will look the same here. But if you load this pattern in the development
environment (or on FreeSewing.org for that matter) the user will see this:
![A message for the user](./flag.png)
It's a simple example, but I hope it gets the point across.
Finally, keep in mind that we are now straddling the world of the core library
and frontend integration. These messages won't do anything unless you have a
frontend the shows them.
In other words **core does not care**. We are merely storing data in the store
and relying on the frontend to show this data to the user. We merely offer
standard methods to do so, but you can choose to ignore this info, or show it
in a different way in your own frontend implementation.
<Example previewFirst tutorial caption="We flagged something for the user">
```design/src/bib.mjs
function draftBib({
Point,
points,
Path,
paths,
utils,
store,
measurements,
options,
macro,
Snippet,
snippets,
complete,
// highlight-start
units,
// highlight-end
part,
}) {
/*
* Construct the neck opening
*/
const target = (measurements.head * options.neckRatio) / 4
let tweak = 1
let delta
do {
points.right = new Point((tweak * measurements.head) / 10, 0)
points.bottom = new Point(0, (tweak * measurements.head) / 12)
points.rightCp1 = points.right.shift(90, points.bottom.dy(points.right) / 2)
points.bottomCp2 = points.bottom.shift(0, points.bottom.dx(points.right) / 2)
paths.neck = new Path()
.move(points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
delta = paths.neck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
/*
* Construct the outline
*/
let width = measurements.head * options.widthRatio
let length = measurements.head * options.lengthRatio
points.topLeft = new Point(width / -2, points.top.y - (width / 2 - points.right.x))
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(points.topLeft, 0.5)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
/*
* Round the end of the straps
*/
let strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro('round', {
id: 'tipRightTop',
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
}),
tipRightBottom: macro('round', {
id: 'tipRightBottom',
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
/*
* Rotate straps so they don't overlap
*/
let rotateThese = [
'edgeTopLeftCp',
'edgeTop',
'tipRight',
'tipRightTop',
'tipRightTopStart',
'tipRightTopCp1',
'tipRightTopCp2',
'tipRightTopEnd',
'tipRightBottomStart',
'tipRightBottomCp1',
'tipRightBottomCp2',
'tipRightBottomEnd',
'tipRightBottom',
'top',
'topCp2',
]
while (points.tipRightBottomStart.x > -1) {
for (let p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
/*
* Add points to anchor snaps on
*/
points.snapLeft = points.top.shiftFractionTowards(points.edgeTop, 0.5)
/*
* Mirror points to the other side
*/
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
points.topCp1 = points.topCp2.flipX()
points.tipLeftTopStart = points.tipRightTopStart.flipX()
points.tipLeftTopCp1 = points.tipRightTopCp1.flipX()
points.tipLeftTopCp2 = points.tipRightTopCp2.flipX()
points.tipLeftTopEnd = points.tipRightTopEnd.flipX()
points.tipLeftBottomStart = points.tipRightBottomStart.flipX()
points.tipLeftBottomCp1 = points.tipRightBottomCp1.flipX()
points.tipLeftBottomCp2 = points.tipRightBottomCp2.flipX()
points.tipLeftBottomEnd = points.tipRightBottomEnd.flipX()
points.snapRight = points.snapLeft.flipX()
/*
* Round the bottom of the bib
* Radius is fixed, but you could use an option for it)
*
* Macros will return the auto-generated IDs
*/
const ids2 = {
bottomLeft: macro('round', {
id: 'bottomLeft',
from: points.topLeft,
to: points.bottomRight,
via: points.bottomLeft,
radius: points.bottomRight.x / 4,
}),
bottomRight: macro('round', {
id: 'bottomRight',
from: points.bottomLeft,
to: points.topRight,
via: points.bottomRight,
radius: points.bottomRight.x / 4,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids2) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids2[side].points[id]].copy()
}
}
/*
* Construct the path
*/
paths.seam = new Path()
.move(points.edgeLeft)
.line(points.bottomLeftStart)
.curve(points.bottomLeftCp1, points.bottomLeftCp2, points.bottomLeftEnd)
.line(points.bottomRightStart)
.curve(points.bottomRightCp1, points.bottomRightCp2, points.bottomRightEnd)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.tipLeftTopStart)
.curve(points.tipLeftTopCp1, points.tipLeftTopCp2, points.tipLeftTopEnd)
.curve(points.tipLeftBottomCp1, points.tipLeftBottomCp2, points.tipLeftBottomEnd)
.curve(points.topCp1, points.rightCp2, points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
.curve(points.bottomCp1, points.leftCp2, points.left)
.curve(points.leftCp1, points.topCp2, points.tipRightBottomEnd)
.curve(points.tipRightBottomCp2, points.tipRightBottomCp1, points.tipRightBottomStart)
.curve(points.tipRightTopCp2, points.tipRightTopCp1, points.tipRightTopStart)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.close()
.attr('class', 'fabric')
/*
*
* Annotations
*
*/
// highlight-start
/*
* Let the user know about the bias tape and fabric requirements
*/
store.flag.note({
msg: 'tutorial:biasTapeLength',
replace: {
l: units(paths.seam.length()),
},
})
// highlight-end
/*
* Cut list
*/
store.cutlist.addCut({ cut: 1, from: 'fabric' })
/*
* Add the snaps
*/
snippets.snapStud = new Snippet('snap-stud', points.snapLeft)
snippets.snapSocket = new Snippet('snap-socket', points.snapRight).attr('opacity', 0.5)
/*
* Add the bias tape
*/
if (complete)
paths.bias = paths.seam
.offset(-5)
.addClass('note dashed')
.addText('finishWithBiasTape', 'center fill-note')
/*
* Add the title
*/
points.title = points.bottom.shift(-90, 45)
macro('title', {
at: points.title,
nr: 1,
title: 'bib',
align: 'center',
scale: 0.8,
})
/*
* Add the scalebox
*/
points.scalebox = points.title.shift(-90, 65)
macro('scalebox', { at: points.scalebox })
/*
* Add the logo
*/
points.logo = new Point(0, 0)
snippets.logo = new Snippet('logo', points.logo)
/*
* Add dimensions
*/
macro('hd', {
id: 'wFull',
from: points.bottomLeftStart,
to: points.bottomRightEnd,
y: points.bottomLeft.y + 15,
})
macro('vd', {
id: 'hBottomToOpeningBottom',
from: points.bottomRightStart,
to: points.bottom,
x: points.bottomRight.x + 15,
})
macro('vd', {
id: 'hBottomToOpeningCenter',
from: points.bottomRightStart,
to: points.right,
x: points.bottomRight.x + 30,
})
macro('vd', {
id: 'hTotal',
from: points.bottomRightStart,
to: points.tipLeftTopStart,
x: points.bottomRight.x + 45,
})
macro('hd', {
id: 'wOpening',
from: points.left,
to: points.right,
y: points.left.y + 25,
})
macro('ld', {
id: 'wStrap',
from: points.tipLeftBottomEnd,
to: points.tipLeftTopStart,
d: -15,
})
return part
}
```
</Example>

View file

@ -0,0 +1,7 @@
---
title: Supporting translation
order: 80
---
:::note [FIXME]Write this section for v3:::

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -0,0 +1,68 @@
---
title: Facilitating frontend integration
order: 60
---
Strictly speaking, this tutorial is about learning to use FreeSewing's core
library to do parametric design, and we made great strides in that regard.
But FreeSewing is a lot more than its core library, and you might be wondering
how your pattern options magically end up in the development environment under
**Design options**:
![Design options menu](./options.png)
To make this happen, we add extra information to the options configuration.
You can add anything you want, here is a made-up example:
```mjs
options: {
shipWith: {
dflt: 'pickUp',
list: ['pickUp', 'post', 'courier'],
menu: 'shipping',
extraNote: 'Pick-up Monday to Friday 10:00, to 19:00'
},
}
```
It's just a silly example, but there's two important take-aways here:
- You don't have to use options in your design. You can add options for things
that are not about the design, but that you still want to capture the user's
input for (like shipping preferences in this case).
- You can add extra properties to an option. Each option type has it's required
properties. But you can add more and use them as you see fit.
Which is exactly what we do at FreeSewing, so I'd like to mention the `menu` one:
## Setting `menu` on your option
If you set a `menu` property on your option, the FreeSewing frontend will use
this to organize your various options in a menu structure.
### Sub menus
You can a nested menu structure with dot-notation. So `style.pockets` will
create a `pockets` submenu under the `style` menu and put your option there.
### Hiding options
If your `menu` property holds a falsy value, the option will be hidden from the
menu.
### Hiding options conditionally
If the `menu` property of your option holds a function, that function will be called with the following signature:
```mjs
function(
settings, // The settings provided by the user
mergedOptions, // The user-provided options merged with the defaults
) {
// return menu value here
}
```
This is typically used to hide options conditionally.
:::note [FIXME]Include example:::

View file

@ -0,0 +1,344 @@
---
title: Supporting paperless patterns
order: 30
---
The goal of paperless patterns is to create a pattern that we don't need to
print to be able to use it. Saving paper is always a good thing, but it's
also a way to democratize access to patterns.
While more and more of humanity is on the internet, access to printers and
printing paper is often harder to come by, especially in developing countries.
So let's make the extra effort to make our bib design support paperless.
## The paperless setting
Users can request paperless patterns by setting [the `paperless`
setting](/reference/settings/paperless) to a *truthy* value.
With paperless enabled, FreeSewing will automatically render a grid for each
pattern part with metric or imperial markings, depending on the units requested
by the user.
Such a grid is already a good starting point. In addition, we'll be using
different macros to add *dimensions* to the pattern.
While the grid gets added automatically, the dimensions we have to add ourselves.
Thankfully, there's macros that can help us with that, specifically:
- The `hd` macro adds a horizontal dimension
- The `vd` macro adds a vertical dimension
- The `ld` macro adds a linear dimension
- The `pd` macro adds a path dimension that follows a given path
These macros will also adapt to the units chosen by the user (metric or imperial).
:::note
Refer to [the list of macros](/reference/macros/) for more details.
:::
<Example previewFirst tutorial paperless caption="Suddenly, a printer is very much optional">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
utils,
measurements,
options,
macro,
complete,
snippets,
Snippet,
store,
// highlight-start
paperless,
// highlight-end
part,
}) {
/*
* Construct the neck opening
*/
const target = (measurements.head * options.neckRatio) / 4
let tweak = 1
let delta
do {
points.right = new Point((tweak * measurements.head) / 10, 0)
points.bottom = new Point(0, (tweak * measurements.head) / 12)
points.rightCp1 = points.right.shift(90, points.bottom.dy(points.right) / 2)
points.bottomCp2 = points.bottom.shift(0, points.bottom.dx(points.right) / 2)
paths.neck = new Path()
.move(points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
delta = paths.neck.length() - target
if (delta > 0) tweak = tweak * 0.99
else tweak = tweak * 1.02
} while (Math.abs(delta) > 1)
points.rightCp2 = points.rightCp1.flipY()
points.bottomCp1 = points.bottomCp2.flipX()
points.left = points.right.flipX()
points.leftCp1 = points.rightCp2.flipX()
points.leftCp2 = points.rightCp1.flipX()
points.top = points.bottom.flipY()
points.topCp1 = points.bottomCp2.flipY()
points.topCp2 = points.bottomCp1.flipY()
/*
* Construct the outline
*/
let width = measurements.head * options.widthRatio
let length = measurements.head * options.lengthRatio
points.topLeft = new Point(width / -2, points.top.y - (width / 2 - points.right.x))
points.topRight = points.topLeft.shift(0, width)
points.bottomLeft = points.topLeft.shift(-90, length)
points.bottomRight = points.topRight.shift(-90, length)
points.edgeLeft = new Point(points.topLeft.x, points.left.y)
points.edgeRight = new Point(points.topRight.x, points.right.y)
points.edgeTop = new Point(0, points.topLeft.y)
points.edgeLeftCp = points.edgeLeft.shiftFractionTowards(points.topLeft, 0.5)
points.edgeRightCp = points.edgeLeftCp.flipX()
points.edgeTopLeftCp = points.edgeTop.shiftFractionTowards(points.topLeft, 0.5)
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
/*
* Round the end of the straps
*/
let strap = points.edgeTop.dy(points.top)
points.tipRight = points.edgeTop.translate(strap / 2, strap / 2)
points.tipRightTop = new Point(points.tipRight.x, points.edgeTop.y)
points.tipRightBottom = new Point(points.tipRight.x, points.top.y)
/*
* Macros will return the auto-generated IDs
*/
const ids1 = {
tipRightTop: macro('round', {
id: 'tipRightTop',
from: points.edgeTop,
to: points.tipRight,
via: points.tipRightTop,
}),
tipRightBottom: macro('round', {
id: 'tipRightBottom',
from: points.tipRight,
to: points.top,
via: points.tipRightBottom,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids1) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids1[side].points[id]].copy()
}
}
/*
* Rotate straps so they don't overlap
*/
let rotateThese = [
'edgeTopLeftCp',
'edgeTop',
'tipRight',
'tipRightTop',
'tipRightTopStart',
'tipRightTopCp1',
'tipRightTopCp2',
'tipRightTopEnd',
'tipRightBottomStart',
'tipRightBottomCp1',
'tipRightBottomCp2',
'tipRightBottomEnd',
'tipRightBottom',
'top',
'topCp2',
]
while (points.tipRightBottomStart.x > -1) {
for (let p of rotateThese) points[p] = points[p].rotate(1, points.edgeLeft)
}
/*
* Add points to anchor snaps on
*/
points.snapLeft = points.top.shiftFractionTowards(points.edgeTop, 0.5)
/*
* Mirror points to the other side
*/
points.edgeTopRightCp = points.edgeTopLeftCp.flipX()
points.topCp1 = points.topCp2.flipX()
points.tipLeftTopStart = points.tipRightTopStart.flipX()
points.tipLeftTopCp1 = points.tipRightTopCp1.flipX()
points.tipLeftTopCp2 = points.tipRightTopCp2.flipX()
points.tipLeftTopEnd = points.tipRightTopEnd.flipX()
points.tipLeftBottomStart = points.tipRightBottomStart.flipX()
points.tipLeftBottomCp1 = points.tipRightBottomCp1.flipX()
points.tipLeftBottomCp2 = points.tipRightBottomCp2.flipX()
points.tipLeftBottomEnd = points.tipRightBottomEnd.flipX()
points.snapRight = points.snapLeft.flipX()
/*
* Round the bottom of the bib
* Radius is fixed, but you could use an option for it)
*
* Macros will return the auto-generated IDs
*/
const ids2 = {
bottomLeft: macro('round', {
id: 'bottomLeft',
from: points.topLeft,
to: points.bottomRight,
via: points.bottomLeft,
radius: points.bottomRight.x / 4,
}),
bottomRight: macro('round', {
id: 'bottomRight',
from: points.bottomLeft,
to: points.topRight,
via: points.bottomRight,
radius: points.bottomRight.x / 4,
}),
}
/*
* Create points from them with easy names
*/
for (const side in ids2) {
for (const id of ['start', 'cp1', 'cp2', 'end']) {
points[`${side}${utils.capitalize(id)}`] = points[ids2[side].points[id]].copy()
}
}
/*
* Construct the path
*/
paths.seam = new Path()
.move(points.edgeLeft)
.line(points.bottomLeftStart)
.curve(points.bottomLeftCp1, points.bottomLeftCp2, points.bottomLeftEnd)
.line(points.bottomRightStart)
.curve(points.bottomRightCp1, points.bottomRightCp2, points.bottomRightEnd)
.line(points.edgeRight)
.curve(points.edgeRightCp, points.edgeTopRightCp, points.tipLeftTopStart)
.curve(points.tipLeftTopCp1, points.tipLeftTopCp2, points.tipLeftTopEnd)
.curve(points.tipLeftBottomCp1, points.tipLeftBottomCp2, points.tipLeftBottomEnd)
.curve(points.topCp1, points.rightCp2, points.right)
.curve(points.rightCp1, points.bottomCp2, points.bottom)
.curve(points.bottomCp1, points.leftCp2, points.left)
.curve(points.leftCp1, points.topCp2, points.tipRightBottomEnd)
.curve(points.tipRightBottomCp2, points.tipRightBottomCp1, points.tipRightBottomStart)
.curve(points.tipRightTopCp2, points.tipRightTopCp1, points.tipRightTopStart)
.curve(points.edgeTopLeftCp, points.edgeLeftCp, points.edgeLeft)
.close()
.attr('class', 'fabric')
/*
*
* Annotations
*
*/
/*
* Cut list
*/
store.cutlist.addCut({ cut: 1, from: 'fabric' })
/*
* Add the snaps
*/
snippets.snapStud = new Snippet('snap-stud', points.snapLeft)
snippets.snapSocket = new Snippet('snap-socket', points.snapRight).attr('opacity', 0.5)
/*
* Add the bias tape
*/
if (complete)
paths.bias = paths.seam
.offset(-5)
.addClass('note dashed')
.addText('fronscratch:finishWithBiasTape', 'center fill-note')
/*
* Add the title
*/
points.title = points.bottom.shift(-90, 45)
macro('title', {
at: points.title,
nr: 1,
title: 'bib',
align: 'center',
scale: 0.8,
})
/*
* Add the scalebox
*/
points.scalebox = points.title.shift(-90, 65)
macro('scalebox', { at: points.scalebox })
/*
* Add the logo
*/
points.logo = new Point(0, 0)
snippets.logo = new Snippet('logo', points.logo)
// highlight-start
/*
* Add dimensions
*/
macro('hd', {
id: 'wFull',
from: points.bottomLeftStart,
to: points.bottomRightEnd,
y: points.bottomLeft.y + 15,
})
macro('vd', {
id: 'hBottomToOpeningBottom',
from: points.bottomRightStart,
to: points.bottom,
x: points.bottomRight.x + 15,
})
macro('vd', {
id: 'hBottomToOpeningCenter',
from: points.bottomRightStart,
to: points.right,
x: points.bottomRight.x + 30,
})
macro('vd', {
id: 'hTotal',
from: points.bottomRightStart,
to: points.tipLeftTopStart,
x: points.bottomRight.x + 45,
})
macro('hd', {
id: 'wOpening',
from: points.left,
to: points.right,
y: points.left.y + 25,
})
macro('ld', {
id: 'wStrap',
from: points.tipLeftBottomEnd,
to: points.tipLeftTopStart,
d: -15,
})
// highlight-end
return part
}
```
</Example>

View file

@ -0,0 +1,18 @@
---
title: "Part 3: Beyond the basics"
---
I'm excited about diving into part 3 with you, and I hope that a brief
overview of what we'll cover will get you excited too:
- Adding seam allowance
- Dealing with laser cutters
- Providing paperless patterns
- Limiting output through expand
- Facilitating frontend integration
- How to communicate to the user
- Testing your designs
- Supporting translation
If that sounds like something you'd like to learn more about, then let's dive
right in.

View file

@ -0,0 +1,77 @@
---
title: Adding seam allowance
order: 10
---
When adding seam allowance to a pattern, there are 3 things that come into play:
- Does the user want seam allowance?
- How much seam allowance does the user want?
- How can we add seam allowance?
Let's whip up a quick example for a moment to clarify how we will handle this:
<Example settings="sa: 10"caption="Some straight lines and some curves">
```design/src/bib.mjs
function draftBib({
Path,
Point,
paths,
points,
// highlight-start
sa,
// highlight-end
part,
}) {
points.topLeft = new Point(0,0)
points.bottomRight = new Point(100,40)
points.topRight = new Point(points.bottomRight.x, points.topLeft.y)
points.bottomLeft = new Point(points.topLeft.x, points.bottomRight.y)
points.cp1 = new Point(50, 20)
points.cp2 = new Point(70, 60)
paths.shape = new Path()
.move(points.topLeft)
.line(points.bottomLeft)
.curve(points.cp1, points.cp2, points.bottomRight)
.line(points.topRight)
.line(points.topLeft)
.close()
.addClass('fabric')
// highlight-start
if (sa) paths.sa = paths.shape.offset(sa).addClass('fabric sa')
// highlight-end
return part
}
```
</Example>
As you can see from the source, we can destructure an `sa` variable (short for
seam allowance) that will hold either:
- `false` if the user does not want seam allowance
- A value in `mm` indicating how much seam allowance the user wants
To add seam allowance to our path, we just `offset` it by `sa` and add
some classes to it to style it. But, crucially, only if the user wants
seam allowance:
```mjs
if (sa) paths.sa = paths.shape
.offset(sa)
.addClass('fabric sa')
```
To refer back to our three question: Whether the user wants seam allowance, and
if so how much seam allowance is answered by the `sa` value passed to our draft
method.
And we add it with the `path.offset()` method which will offset the path.
Our bib pattern does not require seam allowance as it will be finished with
bias tape. But we can use this same technique to indicate that.
Before we do so though, we should talk about `complete`.

View file

@ -0,0 +1,147 @@
---
title: Testing your designs
order: 70
---
:::note [FIXME] Update this for v3 :::
With the basic outline of our pattern ready, now would be a good time
to test it to see how well it adapts to different measurements,
and the range of options we provided.
:::note [FIXME]
This page needs to be updated with screenshots from the v3 development
environment
:::
:::tip
###### No more grading
FreeSewing patterns are _bespoke_, which means that we don't need to
grade our pattern to provide a range of sizes. We should test our pattern
for different measurements and options to see how well it adapts.
:::
If testing our pattern sounds like a lot of work, we're in luck. FreeSewing can do it
for us. Click the **Test Design** link in the sidebar under the **View** title.
We have a number of ways to test our pattern:
- Test design options
- Test measurements
- Test measurements sets
The [API docs on sampling](/reference/api/pattern/sample) have all the details
on how this works, but for now we'll just look at the end result of each of
these.
## Testing pattern options
We used percentage options, which can vary between their minimum and maximum
value. For these tests, FreeSewing will divide that range into 10 steps and
draft our pattern for each step.
Click on any of the options we've added to our pattern, and our bib will be
drawn with that option sampled.
### lengthRatio
The `lengthRatio` option controls the length of our bib. Testing it confirms
that it only influences the length:
![This is what it should look like when we test the `lengthRatio`
option](test-option-lengthratio.png)
:::note [FIXME]Update screenshot for v3:::
### neckRatio
The `neckRatio` option will determine the size of the neck opening. For the
same `head` measurement, varying this option should result in bibs with
increasingly larger neck opening.
Testing it confirms this. We can also see that as the neck opening gets
smaller, we will rotate the straps further out of the way to avoid overlap:
![This is what it should look like when we test the `neckRatio`
option](test-option-neckratio.png)
:::note [FIXME]Update screenshot for v3:::
### widthRatio
The `widthRatio` option will determine the width of our bib. For the same
`head` measurement, varying this option should result in increasingly wider
bibs.
If we test it, we can see that it works as intended. But there's one thing that
perhaps requires our attention. Making the bib wider shortens the length from
the bottom of the neck opening to the bottom of the bib. Thereby making the
bib shorter when it's worn.
![This is what it should look like when we test the `widthRatio`
option](test-option-widthratio.png)
:::note [FIXME]Update screenshot for v3:::
:::note
Even if the _total length_ of the bib stays the same, the _useable length_
shortens when the bib is made wider. Users will not expect this, so it's
something that we should fix in our pattern.
Adjusting the pattern to make the `widthRatio` not influence the _useable
length_ of the bib is not covered in this tutorial. It is left _as an exercise
to the reader_.
:::
## Testing measurements
Testing a measurement will vary that measurement 10% up or down while leaving
everything else the same. This gives us the option to determine how any given
measurement is influencing the pattern.
For our bib, we only use one measurement, so it influences the entire pattern.
:::note [FIXME]Add screenshot:::
## Testing measurments sets
Whereas testing a measurement will only vary one individual measurement,
testing measurements sets will draft our pattern for different sets of
measurements.
On the surface, the result below is the same as our measurement test. But that
is because our bib only uses one measurement. So testing that one measurement
ends up being the same as testing a complete set of measurements.
But most patterns use multiple measurements, and we'll find this test gives
us insight into how our pattern will adapt to differently sized bodies.
:::note [FIXME]Add screenshot:::
## The antperson test
A special case of model testing is the so-called _antperson test_. It drafts
our pattern with a set of _typical_ measurements , and then drafts it again
with measurements that are 1/10th of those _typical_ measurements.
It is named after [the cartoon
character](https://en.wikipedia.org/wiki/Ant-Man_\(film\)) who can shrink, yet
somehow his suit still fits.
The purpose of the antperson test is to bring out areas in our pattern where
we made assumptions that will not properly scale. Many drafting books will
tell us to _add 3 cm there_ or _measure 2 inch to the right_. Those
instructions don't scale, and we should avoid them.
The best patterns will pass the antperson test with 2 patterns exactly the
same, where one will simply be 1/10th the scale of the other.
:::note [FIXME]Add screenshot:::
When we're happy with how our pattern passes these tests, it's time to
complete our design.

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 KiB

View file

@ -0,0 +1,48 @@
---
title: Pattern design tutorial
---
Hello there, and welcome to this FreeSewing pattern design tutorial.
My name is Joost, and in this tutorial I will show you
how to design a bespoke sewing pattern, start to finish.
This tutorial is divided into three parts, allowing you to speedrun or entirely
skip certain parts depending on your interests or prior experience:
## Part 1: Prerequisites
The first sections of this tutorial, [Part 1](/tutorials/pattern-design/part1),
deals with the prerequisites. Installing node, setting up the FreeSewing
development environment on your system, and so on. If you are familiar with
the JavaScript ecosystem, I can summarize that entire section in this one-liner
that sets up the FreeSewing development environment on your system:
```sh
npx @freesewing/new-design
```
## Part 2: Parametric design
In [Part 2](/tutorials/pattern-design/part2) I will show you how to design a
parametric sewing pattern with FreeSewing. We'll create a part, add a bunch of
points, draw lines and curves, and so on.
All the basic skills required to create a sewing pattern in code.
This is probably a part you do not want to skip, unless you have prior
experience with FreeSewing and are looking for a refresh on some of the more
advanced features covered in the next section.
## Part 3: Beyond the basics
There is more to FreeSewing patterns than meets the eye, and in [Part
3](/tutorials/pattern-design/part3) I will cover some of the ways you can add
further value to your designs.
This includes things like translation, supporting laser cutters, avoiding the
need to printing with so-called *paperless patterns*, as well as how you can
configure your pattern to integrate with FreeSewing.org, or your own
frontend.
You can follow this tutorial start to finish, or skip ahead and back, the
choice is yours.