diff --git a/markdown/dev/guides/patterns/config/en.md b/markdown/dev/guides/patterns/config/en.md
deleted file mode 100644
index 92d843770e1..00000000000
--- a/markdown/dev/guides/patterns/config/en.md
+++ /dev/null
@@ -1,22 +0,0 @@
----
-title: Configuration
-order: 10
----
-
-
-
-The pattern configuration holds important information about the pattern
-
-
-
-A pattern's [configuration](/reference/api/config/) is created by the pattern designer
-and details a number of important things about the pattern, like:
-
-- The **measurements** that are required to draft the pattern
-- The different **parts** in the pattern and how they depend on each other
-- The different **options** that are available to tweak the pattern
-
-The configuration is part of the pattern's code. It is created by the designer and
-it is the same for everybody using the pattern.
-In other words, you cannot change the configuration. Instead, the configuration
-specifies what kind of settings the pattern accepts.
diff --git a/markdown/dev/guides/patterns/en.md b/markdown/dev/guides/patterns/en.md
index 0882327ec5d..68c8baf2361 100644
--- a/markdown/dev/guides/patterns/en.md
+++ b/markdown/dev/guides/patterns/en.md
@@ -1,5 +1,5 @@
---
-title: How patterns work
+title: Pattern guide
---
This short guide will illustrate and explain how patterns work in FreeSewing.
@@ -11,19 +11,188 @@ pattern is generated by FreeSewing.
This illustration is a good starting point to gain a better
understanding of the structure of a FreeSewing pattern:
-
-A schematic overview of FreeSewing
+
+```mjs
+({ Point, points, Path, paths, options, part }) => {
+
+ // Draws a w*h box, returns a Path object
+ const box = (name, origin, width, height, classes='fabric') => {
+ let base = height
+ if (width < height) base = width
+ let t = base
+ points[name + 'TopLeft'] = new Point(origin.x, origin.y)
+ points[name + 'BottomLeft'] = new Point(origin.x, origin.y + height)
+ points[name + 'BottomRight'] = new Point(
+ origin.x + width,
+ origin.y + height
+ )
+ points[name + 'TopRight'] = new Point(origin.x + width, origin.y)
+ points[name + 'Mid'] = points[name + 'TopLeft'].shiftFractionTowards(
+ points[name + 'BottomRight'],
+ 0.5
+ )
+ points[name + 'Mid'].y += 3
+
+ return new Path()
+ .move(points[name + 'TopLeft'])
+ .line(points[name + 'BottomLeft'])
+ .line(points[name + 'BottomRight'])
+ .line(points[name + 'TopRight'])
+ .line(points[name + 'TopLeft'])
+ .close()
+ .addClass(classes)
+ }
+
+ // Draws and arrow from to and places text
+ const arrow = (name, text = '') => {
+ let from = points[name + 'From']
+ let to = points[name + 'To']
+ from = from.shiftTowards(to, 2)
+ to = to.shiftTowards(from, 2)
+ const base = from.dist(to)
+ const r = 3
+ points[name + 'Tip1'] = to.shiftTowards(from, 3.5).rotate(r, from)
+ points[name + 'Tip2'] = to.shiftTowards(from, 3.5).rotate(r * -1, from)
+ const path = new Path()
+ .move(from)
+ .line(to)
+ .move(points[name + 'Tip1'])
+ .line(to)
+ .line(points[name + 'Tip2'])
+ .addClass('lining stroke-lg')
+ if (options.focus === name) path.addClass('note')
+
+ return text
+ ? path.addText(' ' + text, options.focus === name ? 'fill-note center' : 'center')
+ : path
+ }
+
+ // Draws a box and handled text placement
+ const drawBox = (name, x, y, width, height, text=true, classes, textClasses='') => {
+ points[name + 'Origin'] = new Point(x, y)
+ paths[name] = box(name, points[name + 'Origin'], width, height, classes)
+ if (text === 'b') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomLeft'])
+ .line(points[name+'BottomRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', -1)
+ }
+ else if (text === 't') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'TopLeft'])
+ .line(points[name+'TopRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', 11)
+ }
+ else if (text === 'r') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomRight'])
+ .line(points[name+'TopRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dx', -5)
+ }
+ else if (text) points[name + 'Mid'].addText(name, 'center')
+ }
+
+ // Settings
+ drawBox('Settings Set 0', -140, -18, 50, 20, 'Settings 0', 'fabric fill-bg')
+ drawBox('Settings Set 1', -140, 6, 50, 20, 'Settings 0', 'fabric fill-bg')
+ const sname = 'Settings'
+ drawBox(sname, -145, -24, 61, 65, 'b', 'fabric', 'text-lg bold')
+ points.arrowDraftFrom = new Point(-84,15)
+ points.arrowDraftTo = new Point(-45, 15)
+ paths.arrowDraft = arrow('arrowDraft', 'draft()')
+
+ // Stacks
+ drawBox('Stack 0', -30, -33, 50, 169, 'b', 'mark fill-mark', 'fill-mark')
+ paths['Stack 0'].attr('fill-opacity', 0.2)
+ drawBox('Stack 1', 23, -33, 50, 169, 'b', 'mark fill-mark', 'fill-mark')
+ paths['Stack 1'].attr('fill-opacity', 0.2)
+ drawBox('Stack 2', 76, -33, 50, 169, 'b', 'mark fill-mark', 'fill-mark')
+ paths['Stack 2'].attr('fill-opacity', 0.2)
+
+ // Sets
+ drawBox('Set 0', -33, -30, 174, 76, 'r', 'contrast fill-contrast', 'fill-contrast')
+ paths['Set 0'].attr('fill-opacity', 0.2)
+ drawBox('Set 1', -33, 50, 174, 76, 'r', 'contrast fill-contrast', 'fill-contrast')
+ paths['Set 1'].attr('fill-opacity', 0.2)
+
+ // Parts set 0
+ drawBox('Part A (set 0)', -27, -27, 44, 70, 'b', 'note fill-bg')
+ drawBox('points', -24, -24, 38, 12, true, 'note')
+ drawBox('paths', -24, -9, 38, 12, true, 'note')
+ drawBox('snippets', -24, 6, 38, 12, true, 'note')
+
+ drawBox('Part B (set 0)', 26, -27, 44, 70, 'b', 'note fill-bg')
+ drawBox(' points ', 29, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 29, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 29, 6, 38, 12, true, 'note')
+
+ drawBox('Part C (set 0)', 79, -27, 44, 70, 'b', 'note fill-bg')
+ drawBox(' points ', 82, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 82, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 82, 6, 38, 12, true, 'note')
+
+ drawBox('setStore 0', -24, 21, 144, 12, true, 'lining fill-lining dashed', 'fill-various')
+ paths['setStore 0'].attr('fill-opacity', 0.2)
+
+ // Parts set 1
+ drawBox('Part A (set 1)', -27, 53, 44, 70, 'b', 'note fill-bg')
+ drawBox(' points', -24, 56, 38, 12, true, 'note')
+ drawBox(' paths', -24, 71, 38, 12, true, 'note')
+ drawBox(' snippets', -24, 86, 38, 12, true, 'note')
+
+ drawBox('Part B (set 1)', 26, 53, 44, 70, 'b', 'note fill-bg')
+ drawBox(' points ', 29, 56, 38, 12, true, 'note')
+ drawBox(' paths ', 29, 71, 38, 12, true, 'note')
+ drawBox(' snippets ', 29, 86, 38, 12, true, 'note')
+
+ drawBox('Part C (set 1)', 79, 53, 44, 70, 'b', 'note fill-bg')
+ drawBox(' points ', 82, 56, 38, 12, true, 'note')
+ drawBox(' paths ', 82, 71, 38, 12, true, 'note')
+ drawBox(' snippets ', 82, 86, 38, 12, true, 'note')
+
+ drawBox('setStore 1', -24, 101, 147, 12, true, 'lining fill-lining dashed', 'fill-various')
+ paths['setStore 1'].attr('fill-opacity', 0.2)
+
+ // Pattern
+ drawBox('Pattern Store', -30, -52, 155, 15, true, 'lining fill-lining')
+ paths['Pattern Store'].attr('fill-opacity', 0.2)
+ drawBox('Pattern', -43, -59, 195, 216, 'b', 'fabric stroke-lg', 'text-lg bold')
+
+ // Render
+ points.arrowSvgFrom = new Point(154,15)
+ points.arrowSvgTo = new Point(225, -15)
+ paths.arrowSvg = arrow('arrowSvg', 'render()')
+ points.arrowReactFrom = new Point(154,15)
+ points.arrowReactTo = new Point(225, 45)
+ paths.arrowReact = arrow('arrowReact', 'getRenderProps()')
+ drawBox('Render stage', 190, -52, 105, 175, 'b', 'interfacing lashed', 'text-lg bold')
+
+ // Render logos
+ points.svg = new Point(225, -15).addText('SVG', 'text-lg bold')
+ points.js = new Point(225, 45).addText("React\nSvelte\nVue\nJS\n...", 'text-lg')
+
+ // Prevent clipping
+ paths.unclip = new Path().move(new Point(260, -80))
+
+ return part
+}
+```
+If it looks like _a lot_ don't despair. There's a lot of repetition, and we'll
+work throug the building blocks step by step.
+
If we look at our image, it can be divided into three areas:
-- The left area with the **settings** box
-- The middle area with the **Pattern** box and everything in it
-- The right area with the **draft** box and the _SVG_ and _React_ logos
-
-Let's take a closer look at everything that is contained within our central **Pattern** box:
-
-
+- [The **Settings**](#the-settings) on the left.
+- [The **Render stage**](#rendering-your-pattern) on the right
+- [The **Pattern**](#anatomy-of-a-pattern)
@@ -31,6 +200,74 @@ The left and right parts are all about how to integrate FreeSewing in your _fron
In other words, how you'll plug it into your website, or online store, or a mobile
application.
-That part is outside the scope of this guide.
+We'll briefly cover those areas in this page. But a deep-dive in those topics
+is outside the scope of this guide.
+
+## The settings
+
+On the left, we have the **settings** box. It represents the
+[settings](/reference/settings) that are provided by the user.
+
+Most of the settings are the same for all designs generated with FreeSewing as they are provided by the core library.
+
+Noteworthy exceptions are the [measurements](/reference/settings/measurements) and [options](/reference/settings/options) keys as they are defined by the pattern design.
+
+In other words, different patterns will required different measurements, and provide different options.
+But all patterns will allow users to set the [units](reference/settings/units) (for example).
+
+
+Settings are provided by passing them to the Pattern contructor
+
+
+
+##### Multiset support
+
+Since version 3 of FreeSewing, you can pass multiple sets of settings to `Pattern.draft()`.
+We call this _multset support_.
+
+In 99% of the cases, you will only have a single set of settings, and you can mostly forget about this feature.
+It is typically used to compare different drafts, or to draft for different sets of measurements (for example).
+
+
+
+## Rendering your pattern
+
+By rendering our pattern, we mean to generate the output to show to the user.
+
+This can be done in two different ways:
+
+### Render to SVG
+
+The core library ships with a renderer that will render your pattern to SVG.
+
+To use it, call [`Pattern.render()`](/reference/api/pattern/render) which will return an SVG string.
+
+### BYOR (bring your own renderer)
+
+If you'd like more fine-grained control over the output, you can use your own renderer.
+
+To do so, call [`Pattern.getRenderProps()`](/reference/api/pattern/getrenderprops) which will return all the data you need to render your pattern in the way you prefer.
+
+
+This is what we use on these pages to render with React
+
+
+## The pattern
+
+Last but not least, we've arrived at the central item: the pattern itself.
+The pattern is a container that holds all your parts, along with a
+(pattern-wide) store.
+
+In reality, your pattern is a _constructor_ that takes the user's settings as
+input and will return an instantiated pattern.
+
+That pattern instance will have a `draft()` method which will do the actual work of
+drafting the pattern. Once drafted, the pattern can be rendered.
+
+While the pattern does a lot of heavy lifting behind the scenes, we can content
+ourselves by understanding its different building blocks:
+
+
+
diff --git a/markdown/dev/guides/patterns/parts/en.md b/markdown/dev/guides/patterns/parts/en.md
index 0de62b5c381..7961c24fedf 100644
--- a/markdown/dev/guides/patterns/parts/en.md
+++ b/markdown/dev/guides/patterns/parts/en.md
@@ -1,18 +1,97 @@
---
title: Parts
-order: 20
+order: 120
---
-
-Parts divide your pattern into re-usable components
-
+A pattern is a container for a bunch of parts. And parts are in turn a
+container for the the points, paths, and snippets of (a part of) your pattern.
+Parts can be re-used and mixed and matched to create other patterns, a powerful
+concept to build a pattern library.
-Parts are a container for the points, paths, and snippets of (a part of) your pattern.
-They are also re-usable by other patterns, which makes them a powerful tool to build
-a pattern library.
-
-If you design a T-shirt pattern with a `front`, `back`, and `sleeve`, each of those would be a part.
-If you then wanted to make a long-sleeved version of your T-shirt pattern, you only need to design
-a new sleeve part. You can re-use the `front` and `back` parts of your short-sleeved T-shirt pattern, as they did not change.
+If you design a T-shirt pattern with a `front`, `back`, and `sleeve`, each of
+those would be a part. If you then wanted to make a long-sleeved version of
+your T-shirt pattern, you only need to design a new sleeve part. You can re-use
+the `front` and `back` parts of your short-sleeved T-shirt pattern, as they did
+not change.
When developing a FreeSewing pattern, you will spend most of your time designing the individual parts.
+
+
+```mjs
+({ Point, points, Path, paths, options, part }) => {
+
+ // Draws a w*h box, returns a Path object
+ const box = (name, origin, width, height, classes='fabric') => {
+ let base = height
+ if (width < height) base = width
+ let t = base
+ points[name + 'TopLeft'] = new Point(origin.x, origin.y)
+ points[name + 'BottomLeft'] = new Point(origin.x, origin.y + height)
+ points[name + 'BottomRight'] = new Point(
+ origin.x + width,
+ origin.y + height
+ )
+ points[name + 'TopRight'] = new Point(origin.x + width, origin.y)
+ points[name + 'Mid'] = points[name + 'TopLeft'].shiftFractionTowards(
+ points[name + 'BottomRight'],
+ 0.5
+ )
+ points[name + 'Mid'].y += 3
+
+ return new Path()
+ .move(points[name + 'TopLeft'])
+ .line(points[name + 'BottomLeft'])
+ .line(points[name + 'BottomRight'])
+ .line(points[name + 'TopRight'])
+ .line(points[name + 'TopLeft'])
+ .close()
+ .addClass(classes)
+ }
+
+ // Draws a box and handled text placement
+ const drawBox = (name, x, y, width, height, text=true, classes, textClasses='') => {
+ points[name + 'Origin'] = new Point(x, y)
+ paths[name] = box(name, points[name + 'Origin'], width, height, classes)
+ if (text === 'b') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomLeft'])
+ .line(points[name+'BottomRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', -1)
+ }
+ else if (text) points[name + 'Mid'].addText(name, 'center')
+ }
+
+ // Parts set 0
+ drawBox('Part A (set 0)', -27, -27, 44, 70, 'b', 'note fill-note', 'bold')
+ paths['Part A (set 0)'].attr('fill-opacity', 0.2)
+ drawBox('points', -24, -24, 38, 12, true, 'note')
+ drawBox('paths', -24, -9, 38, 12, true, 'note')
+ drawBox('snippets', -24, 6, 38, 12, true, 'note')
+
+ drawBox('Part B (set 0)', 26, -27, 44, 70, 'b', 'note fill-note', 'bold')
+ paths['Part B (set 0)'].attr('fill-opacity', 0.2)
+ drawBox(' points ', 29, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 29, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 29, 6, 38, 12, true, 'note')
+
+ drawBox('Part C (set 0)', 79, -27, 44, 70, 'b', 'note fill-note', 'bold')
+ paths['Part C (set 0)'].attr('fill-opacity', 0.2)
+ drawBox(' points ', 82, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 82, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 82, 6, 38, 12, true, 'note')
+
+ drawBox('setStore 0', -24, 21, 144, 12, true, 'lining dashed')
+ paths['setStore 0'].attr('fill-opacity', 0.2)
+
+ // Pattern
+ drawBox('Pattern Store', -30, -45, 155, 15, true, 'lining')
+ paths['Pattern Store'].attr('fill-opacity', 0.2)
+ drawBox('Pattern', -34, -49, 163, 106, 'b', 'fabric stroke-lg', 'text-lg bold')
+
+ return part
+}
+```
+
+
diff --git a/markdown/dev/guides/patterns/paths/en.md b/markdown/dev/guides/patterns/paths/en.md
index 71ddcf6e7d6..501b368fd13 100644
--- a/markdown/dev/guides/patterns/paths/en.md
+++ b/markdown/dev/guides/patterns/paths/en.md
@@ -3,13 +3,9 @@ title: Paths
order: 40
---
-
-Paths are the lines and curves of your pattern
-
-
Paths are the lines and curves that make up your pattern.
+They are made up of individual drawing operations that together make up the path.
-They are made up of a set of drawing operations that together make up the path.
FreeSewing supports the following types of drawing operations:
- The **move** operation moves our virtual pen but does not draw anything.
@@ -33,9 +29,92 @@ Understanding that each drawing operation builds upon the next one is an importa
+
+```mjs
+({ Point, points, Path, paths, options, part }) => {
+
+ // Draws a w*h box, returns a Path object
+ const box = (name, origin, width, height, classes='fabric') => {
+ let base = height
+ if (width < height) base = width
+ let t = base
+ points[name + 'TopLeft'] = new Point(origin.x, origin.y)
+ points[name + 'BottomLeft'] = new Point(origin.x, origin.y + height)
+ points[name + 'BottomRight'] = new Point(
+ origin.x + width,
+ origin.y + height
+ )
+ points[name + 'TopRight'] = new Point(origin.x + width, origin.y)
+ points[name + 'Mid'] = points[name + 'TopLeft'].shiftFractionTowards(
+ points[name + 'BottomRight'],
+ 0.5
+ )
+ points[name + 'Mid'].y += 3
+
+ return new Path()
+ .move(points[name + 'TopLeft'])
+ .line(points[name + 'BottomLeft'])
+ .line(points[name + 'BottomRight'])
+ .line(points[name + 'TopRight'])
+ .line(points[name + 'TopLeft'])
+ .close()
+ .addClass(classes)
+ }
+
+ // Draws a box and handled text placement
+ const drawBox = (name, x, y, width, height, text=true, classes, textClasses='') => {
+ points[name + 'Origin'] = new Point(x, y)
+ paths[name] = box(name, points[name + 'Origin'], width, height, classes)
+ if (text === 'b') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomLeft'])
+ .line(points[name+'BottomRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', -1)
+ }
+ else if (text) points[name + 'Mid'].addText(name, 'center')
+ }
+
+ // Parts set 0
+ drawBox('Part A (set 0)', -27, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox('points', -24, -24, 38, 12, true, 'note')
+ drawBox('paths', -24, -9, 38, 12, true, 'note fill-note')
+ paths['paths'].attr('fill-opacity', 0.2)
+ drawBox('snippets', -24, 6, 38, 12, true, 'note')
+
+ drawBox('Part B (set 0)', 26, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox(' points ', 29, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 29, -9, 38, 12, true, 'note fill-note')
+ paths[' paths '].attr('fill-opacity', 0.2)
+ drawBox(' snippets ', 29, 6, 38, 12, true, 'note')
+
+ drawBox('Part C (set 0)', 79, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox(' points ', 82, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 82, -9, 38, 12, true, 'note fill-note')
+ paths[' paths '].attr('fill-opacity', 0.2)
+ drawBox(' snippets ', 82, 6, 38, 12, true, 'note')
+
+ drawBox('setStore 0', -24, 21, 144, 12, true, 'lining dashed')
+ paths['setStore 0'].attr('fill-opacity', 0.2)
+
+ // Pattern
+ drawBox('Pattern Store', -30, -45, 155, 15, true, 'lining')
+ paths['Pattern Store'].attr('fill-opacity', 0.2)
+ drawBox('Pattern', -34, -49, 163, 106, 'b', 'fabric stroke-lg', 'text-lg bold')
+
+ return part
+}
+```
+
+
Our example image (which, if you hadn't realized was created with FreeSewing) has a lot of
paths in it. Each box, the arrows, the lines in the React logo, and so on.
+Click the **X-Ray** tab to reveal them.
+
+
+
diff --git a/markdown/dev/guides/patterns/points/en.md b/markdown/dev/guides/patterns/points/en.md
index 4b87f4d3761..767d33fba68 100644
--- a/markdown/dev/guides/patterns/points/en.md
+++ b/markdown/dev/guides/patterns/points/en.md
@@ -3,10 +3,6 @@ title: Points
order: 30
---
-
-Points store coordinates
-
-
Developing a pattern with FreeSewing is similar to doing it on paper.
But instead of using a pencil and paper, you'll be writing code.
@@ -29,9 +25,92 @@ may intuitively expect.
+
+```mjs
+({ Point, points, Path, paths, options, part }) => {
+
+ // Draws a w*h box, returns a Path object
+ const box = (name, origin, width, height, classes='fabric') => {
+ let base = height
+ if (width < height) base = width
+ let t = base
+ points[name + 'TopLeft'] = new Point(origin.x, origin.y)
+ points[name + 'BottomLeft'] = new Point(origin.x, origin.y + height)
+ points[name + 'BottomRight'] = new Point(
+ origin.x + width,
+ origin.y + height
+ )
+ points[name + 'TopRight'] = new Point(origin.x + width, origin.y)
+ points[name + 'Mid'] = points[name + 'TopLeft'].shiftFractionTowards(
+ points[name + 'BottomRight'],
+ 0.5
+ )
+ points[name + 'Mid'].y += 3
+
+ return new Path()
+ .move(points[name + 'TopLeft'])
+ .line(points[name + 'BottomLeft'])
+ .line(points[name + 'BottomRight'])
+ .line(points[name + 'TopRight'])
+ .line(points[name + 'TopLeft'])
+ .close()
+ .addClass(classes)
+ }
+
+ // Draws a box and handled text placement
+ const drawBox = (name, x, y, width, height, text=true, classes, textClasses='') => {
+ points[name + 'Origin'] = new Point(x, y)
+ paths[name] = box(name, points[name + 'Origin'], width, height, classes)
+ if (text === 'b') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomLeft'])
+ .line(points[name+'BottomRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', -1)
+ }
+ else if (text) points[name + 'Mid'].addText(name, 'center')
+ }
+
+ // Parts set 0
+ drawBox('Part A (set 0)', -27, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox('points', -24, -24, 38, 12, true, 'note fill-note')
+ paths['points'].attr('fill-opacity', 0.2)
+ drawBox('paths', -24, -9, 38, 12, true, 'note')
+ drawBox('snippets', -24, 6, 38, 12, true, 'note')
+
+ drawBox('Part B (set 0)', 26, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox(' points ', 29, -24, 38, 12, true, 'fill-note note')
+ paths[' points '].attr('fill-opacity', 0.2)
+ drawBox(' paths ', 29, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 29, 6, 38, 12, true, 'note')
+
+ drawBox('Part C (set 0)', 79, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox(' points ', 82, -24, 38, 12, true, 'note fill-note')
+ paths[' points '].attr('fill-opacity', 0.2)
+ drawBox(' paths ', 82, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 82, 6, 38, 12, true, 'note')
+
+ drawBox('setStore 0', -24, 21, 144, 12, true, 'lining dashed')
+ paths['setStore 0'].attr('fill-opacity', 0.2)
+
+ // Pattern
+ drawBox('Pattern Store', -30, -45, 155, 15, true, 'lining')
+ paths['Pattern Store'].attr('fill-opacity', 0.2)
+ drawBox('Pattern', -34, -49, 163, 106, 'b', 'fabric stroke-lg', 'text-lg bold')
+
+ return part
+}
+```
+
+
Our example image (which, if you hadn't realized was created with FreeSewing) has a lot of
points in it. The corners of the boxes, the location where the text goes, and so on.
+Click the **X-Ray** tab to reveal them.
+
+
+
diff --git a/markdown/dev/guides/patterns/sets/en.md b/markdown/dev/guides/patterns/sets/en.md
new file mode 100644
index 00000000000..3261e0306d3
--- /dev/null
+++ b/markdown/dev/guides/patterns/sets/en.md
@@ -0,0 +1,340 @@
+---
+title: Sets
+order: 100
+---
+
+When we refer to a _set_ we mean a _set of settings_.
+In the vast majority of cases, there is only one set of settings. So we just
+refer to them as _the settings_. But FreeSewing supports instantiating a
+pattern with multiple sets of settings. We refer to this as **multiset
+support**.
+
+## Multiset support
+
+Multiset support underpins features such as
+[sampling](/reference/api/pattern/sample) and in general can be used to
+_combine_ multiple drafted variants into a single pattern container.
+
+Here's a simple example:
+
+
+```js
+({
+ Point,
+ points,
+ Path,
+ paths,
+ Snippet,
+ snippets,
+ measurements,
+ part
+}) => {
+
+ const size = measurements.head
+ paths.box = new Path()
+ .move(new Point(0,0))
+ .line(new Point(0, size/3))
+ .line(new Point(size, size/3))
+ .line(new Point(size, 0))
+ .addClass('fabric')
+ .close()
+ points.logo = new Point(size/2, size/5)
+ snippets.logo = new Snippet('logo', points.logo)
+
+ return part
+}
+```
+
+
+When drafting for multiple sets of settings, it's important to keep the different draft variants from cross-contaminating each other.
+To ensure this, the core library will:
+
+- Set up each pattern part per set
+- Provide a per-set store that is shared between parts in the set
+
+This is illustrated below:
+
+
+
+
+```mjs
+({ Point, points, Path, paths, options, part }) => {
+
+ // Draws a w*h box, returns a Path object
+ const box = (name, origin, width, height, classes='fabric') => {
+ let base = height
+ if (width < height) base = width
+ let t = base
+ points[name + 'TopLeft'] = new Point(origin.x, origin.y)
+ points[name + 'BottomLeft'] = new Point(origin.x, origin.y + height)
+ points[name + 'BottomRight'] = new Point(
+ origin.x + width,
+ origin.y + height
+ )
+ points[name + 'TopRight'] = new Point(origin.x + width, origin.y)
+ points[name + 'Mid'] = points[name + 'TopLeft'].shiftFractionTowards(
+ points[name + 'BottomRight'],
+ 0.5
+ )
+ points[name + 'Mid'].y += 3
+
+ return new Path()
+ .move(points[name + 'TopLeft'])
+ .line(points[name + 'BottomLeft'])
+ .line(points[name + 'BottomRight'])
+ .line(points[name + 'TopRight'])
+ .line(points[name + 'TopLeft'])
+ .close()
+ .addClass(classes)
+ }
+
+ // Draws and arrow from to and places text
+ const arrow = (name, text = '') => {
+ let from = points[name + 'From']
+ let to = points[name + 'To']
+ from = from.shiftTowards(to, 2)
+ to = to.shiftTowards(from, 2)
+ const base = from.dist(to)
+ const r = 3
+ points[name + 'Tip1'] = to.shiftTowards(from, 3.5).rotate(r, from)
+ points[name + 'Tip2'] = to.shiftTowards(from, 3.5).rotate(r * -1, from)
+ const path = new Path()
+ .move(from)
+ .line(to)
+ .move(points[name + 'Tip1'])
+ .line(to)
+ .line(points[name + 'Tip2'])
+ .addClass('lining stroke-lg')
+ if (options.focus === name) path.addClass('note')
+
+ return text
+ ? path.addText(' ' + text, options.focus === name ? 'fill-note center' : 'center')
+ : path
+ }
+
+ // Draws a box and handled text placement
+ const drawBox = (name, x, y, width, height, text=true, classes, textClasses='') => {
+ points[name + 'Origin'] = new Point(x, y)
+ paths[name] = box(name, points[name + 'Origin'], width, height, classes)
+ if (text === 'b') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomLeft'])
+ .line(points[name+'BottomRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', -1)
+ }
+ else if (text === 't') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'TopLeft'])
+ .line(points[name+'TopRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', 11)
+ }
+ else if (text === 'r') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomRight'])
+ .line(points[name+'TopRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dx', -5)
+ }
+ else if (text) points[name + 'Mid'].addText(name, 'center')
+ }
+
+ // Stacks
+ drawBox('Stack 0', -30, -33, 50, 169, 'b', 'mark', 'fill-mark')
+ paths['Stack 0'].attr('fill-opacity', 0.2)
+ drawBox('Stack 1', 23, -33, 50, 169, 'b', 'mark', 'fill-mark')
+ paths['Stack 1'].attr('fill-opacity', 0.2)
+ drawBox('Stack 2', 76, -33, 50, 169, 'b', 'mark', 'fill-mark')
+ paths['Stack 2'].attr('fill-opacity', 0.2)
+
+ // Sets
+ drawBox('Set 0', -33, -30, 174, 76, 'r', 'contrast fill-contrast', 'bold text-lg fill-contrast')
+ paths['Set 0'].attr('fill-opacity', 0.2)
+ drawBox('Set 1', -33, 50, 174, 76, 'r', 'contrast fill-contrast', 'bold text-lg fill-contrast')
+ paths['Set 1'].attr('fill-opacity', 0.2)
+
+ // Parts set 0
+ drawBox('Part A (set 0)', -27, -27, 44, 70, 'b', 'note')
+ drawBox('points', -24, -24, 38, 12, true, 'note')
+ drawBox('paths', -24, -9, 38, 12, true, 'note')
+ drawBox('snippets', -24, 6, 38, 12, true, 'note')
+
+ drawBox('Part B (set 0)', 26, -27, 44, 70, 'b', 'note')
+ drawBox(' points ', 29, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 29, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 29, 6, 38, 12, true, 'note')
+
+ drawBox('Part C (set 0)', 79, -27, 44, 70, 'b', 'note')
+ drawBox(' points ', 82, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 82, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 82, 6, 38, 12, true, 'note')
+
+ drawBox('setStore 0', -24, 21, 144, 12, true, 'lining', 'fill-various')
+ paths['setStore 0'].attr('fill-opacity', 0.2)
+
+ // Parts set 1
+ drawBox('Part A (set 1)', -27, 53, 44, 70, 'b', 'note')
+ drawBox(' points', -24, 56, 38, 12, true, 'note')
+ drawBox(' paths', -24, 71, 38, 12, true, 'note')
+ drawBox(' snippets', -24, 86, 38, 12, true, 'note')
+
+ drawBox('Part B (set 1)', 26, 53, 44, 70, 'b', 'note')
+ drawBox(' points ', 29, 56, 38, 12, true, 'note')
+ drawBox(' paths ', 29, 71, 38, 12, true, 'note')
+ drawBox(' snippets ', 29, 86, 38, 12, true, 'note')
+
+ drawBox('Part C (set 1)', 79, 53, 44, 70, 'b', 'note')
+ drawBox(' points ', 82, 56, 38, 12, true, 'note')
+ drawBox(' paths ', 82, 71, 38, 12, true, 'note')
+ drawBox(' snippets ', 82, 86, 38, 12, true, 'note')
+
+ drawBox('setStore 1', -24, 101, 147, 12, true, 'lining', 'fill-various')
+ paths['setStore 1'].attr('fill-opacity', 0.2)
+
+ // Pattern
+ drawBox('Pattern Store', -30, -52, 155, 15, true, 'lining', 'fill-lining')
+ paths['Pattern Store'].attr('fill-opacity', 0.2)
+ drawBox('Pattern', -43, -59, 195, 216, 'b', 'fabric stroke-lg', 'text-lg bold')
+
+ return part
+}
+```
+
+
+## One set is plenty
+
+In the vast majority of cases, a pattern will only have one set of settings. As such, multiset support is not something you need to be intimately familiar with.
+But it is good to know it exists, and explain certain things that might seem _odd_ if you are unaware of multiset support, such as the fact that there's a pattern-wide store, and a different store per set, the so-called setStore(s).
+
+Below is an illustration of a pattern with a single set of settings which, once again, is the vast majority of use cases:
+
+
+```mjs
+({ Point, points, Path, paths, options, part }) => {
+
+ // Draws a w*h box, returns a Path object
+ const box = (name, origin, width, height, classes='fabric') => {
+ let base = height
+ if (width < height) base = width
+ let t = base
+ points[name + 'TopLeft'] = new Point(origin.x, origin.y)
+ points[name + 'BottomLeft'] = new Point(origin.x, origin.y + height)
+ points[name + 'BottomRight'] = new Point(
+ origin.x + width,
+ origin.y + height
+ )
+ points[name + 'TopRight'] = new Point(origin.x + width, origin.y)
+ points[name + 'Mid'] = points[name + 'TopLeft'].shiftFractionTowards(
+ points[name + 'BottomRight'],
+ 0.5
+ )
+ points[name + 'Mid'].y += 3
+
+ return new Path()
+ .move(points[name + 'TopLeft'])
+ .line(points[name + 'BottomLeft'])
+ .line(points[name + 'BottomRight'])
+ .line(points[name + 'TopRight'])
+ .line(points[name + 'TopLeft'])
+ .close()
+ .addClass(classes)
+ }
+
+ // Draws and arrow from to and places text
+ const arrow = (name, text = '') => {
+ let from = points[name + 'From']
+ let to = points[name + 'To']
+ from = from.shiftTowards(to, 2)
+ to = to.shiftTowards(from, 2)
+ const base = from.dist(to)
+ const r = 3
+ points[name + 'Tip1'] = to.shiftTowards(from, 3.5).rotate(r, from)
+ points[name + 'Tip2'] = to.shiftTowards(from, 3.5).rotate(r * -1, from)
+ const path = new Path()
+ .move(from)
+ .line(to)
+ .move(points[name + 'Tip1'])
+ .line(to)
+ .line(points[name + 'Tip2'])
+ .addClass('lining stroke-lg')
+ if (options.focus === name) path.addClass('note')
+
+ return text
+ ? path.addText(' ' + text, options.focus === name ? 'fill-note center' : 'center')
+ : path
+ }
+
+ // Draws a box and handled text placement
+ const drawBox = (name, x, y, width, height, text=true, classes, textClasses='') => {
+ points[name + 'Origin'] = new Point(x, y)
+ paths[name] = box(name, points[name + 'Origin'], width, height, classes)
+ if (text === 'b') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomLeft'])
+ .line(points[name+'BottomRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', -1)
+ }
+ else if (text === 't') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'TopLeft'])
+ .line(points[name+'TopRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', 11)
+ }
+ else if (text === 'r') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomRight'])
+ .line(points[name+'TopRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dx', -5)
+ }
+ else if (text) points[name + 'Mid'].addText(name, 'center')
+ }
+
+ // Stacks
+ drawBox(' Stack 0', -30, -33, 50, 89, 'b', 'mark', 'fill-mark')
+ drawBox(' Stack 1', 23, -33, 50, 89, 'b', 'mark', 'fill-mark')
+ drawBox(' Stack 2', 76, -33, 50, 89, 'b', 'mark', 'fill-mark')
+
+ // Sets
+ drawBox('Set 0', -33, -30, 174, 76, 'r', 'contrast fill-contrast', 'bold text-lg fill-contrast')
+ paths['Set 0'].attr('fill-opacity', 0.2)
+
+ // Parts set 0
+ drawBox('Part A (set 0)', -27, -27, 44, 70, 'b', 'note')
+ drawBox('points', -24, -24, 38, 12, true, 'note')
+ drawBox('paths', -24, -9, 38, 12, true, 'note')
+ drawBox('snippets', -24, 6, 38, 12, true, 'note')
+
+ drawBox('Part B (set 0)', 26, -27, 44, 70, 'b', 'note')
+ drawBox(' points ', 29, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 29, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 29, 6, 38, 12, true, 'note')
+
+ drawBox('Part C (set 0)', 79, -27, 44, 70, 'b', 'note')
+ drawBox(' points ', 82, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 82, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 82, 6, 38, 12, true, 'note')
+
+ drawBox('setStore 0', -24, 21, 144, 12, true, 'lining', 'fill-various')
+ paths['setStore 0'].attr('fill-opacity', 0.2)
+
+ // Pattern
+ drawBox('Pattern Store', -30, -52, 155, 15, true, 'lining', 'fill-lining')
+ paths['Pattern Store'].attr('fill-opacity', 0.2)
+ drawBox(' Pattern', -43, -59, 195, 128, 'b', 'fabric stroke-lg', 'text-lg bold')
+
+ return part
+}
+```
+
+
+
diff --git a/markdown/dev/guides/patterns/snippets/en.md b/markdown/dev/guides/patterns/snippets/en.md
index eb79ec409dc..1891490a306 100644
--- a/markdown/dev/guides/patterns/snippets/en.md
+++ b/markdown/dev/guides/patterns/snippets/en.md
@@ -3,10 +3,6 @@ title: Snippets
order: 50
---
-
-Snippets are little embelishments that go on your pattern
-
-
Snippets are little embellishments you can use and re-use on your pattern.
They are typically used for things like logos or buttons.
@@ -15,9 +11,110 @@ Each snippet must have:
- An anchor point that determine where the snippet will be located
- The name of the snippet to insert
-Since our example image does not have any snippets in it, here's another example
-of a `button`, `buttonhole`, and `logo` snippet added to a FreeSewing pattern:
+
+```mjs
+({ Point, points, Path, paths, options, part }) => {
-
-An example of the use of snippets
+ // Draws a w*h box, returns a Path object
+ const box = (name, origin, width, height, classes='fabric') => {
+ let base = height
+ if (width < height) base = width
+ let t = base
+ points[name + 'TopLeft'] = new Point(origin.x, origin.y)
+ points[name + 'BottomLeft'] = new Point(origin.x, origin.y + height)
+ points[name + 'BottomRight'] = new Point(
+ origin.x + width,
+ origin.y + height
+ )
+ points[name + 'TopRight'] = new Point(origin.x + width, origin.y)
+ points[name + 'Mid'] = points[name + 'TopLeft'].shiftFractionTowards(
+ points[name + 'BottomRight'],
+ 0.5
+ )
+ points[name + 'Mid'].y += 3
+
+ return new Path()
+ .move(points[name + 'TopLeft'])
+ .line(points[name + 'BottomLeft'])
+ .line(points[name + 'BottomRight'])
+ .line(points[name + 'TopRight'])
+ .line(points[name + 'TopLeft'])
+ .close()
+ .addClass(classes)
+ }
+
+ // Draws a box and handled text placement
+ const drawBox = (name, x, y, width, height, text=true, classes, textClasses='') => {
+ points[name + 'Origin'] = new Point(x, y)
+ paths[name] = box(name, points[name + 'Origin'], width, height, classes)
+ if (text === 'b') {
+ paths[name+'_label'] = new Path()
+ .move(points[name+'BottomLeft'])
+ .line(points[name+'BottomRight'])
+ .addClass('hidden')
+ .addText(name, 'center ' + textClasses)
+ .attr('data-text-dy', -1)
+ }
+ else if (text) points[name + 'Mid'].addText(name, 'center')
+ }
+
+ // Parts set 0
+ drawBox('Part A (set 0)', -27, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox('points', -24, -24, 38, 12, true, 'note')
+ drawBox('paths', -24, -9, 38, 12, true, 'note')
+ drawBox('snippets', -24, 6, 38, 12, true, 'note fill-note')
+ paths['snippets'].attr('fill-opacity', 0.2)
+
+ drawBox('Part B (set 0)', 26, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox(' points ', 29, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 29, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 29, 6, 38, 12, true, 'note fill-note')
+ paths[' snippets '].attr('fill-opacity', 0.2)
+
+ drawBox('Part C (set 0)', 79, -27, 44, 70, 'b', 'note', 'bold')
+ drawBox(' points ', 82, -24, 38, 12, true, 'note')
+ drawBox(' paths ', 82, -9, 38, 12, true, 'note')
+ drawBox(' snippets ', 82, 6, 38, 12, true, 'note fill-note')
+ paths[' snippets '].attr('fill-opacity', 0.2)
+
+ drawBox('setStore 0', -24, 21, 144, 12, true, 'lining dashed')
+ paths['setStore 0'].attr('fill-opacity', 0.2)
+
+ // Pattern
+ drawBox('Pattern Store', -30, -45, 155, 15, true, 'lining')
+ paths['Pattern Store'].attr('fill-opacity', 0.2)
+ drawBox('Pattern', -34, -49, 163, 106, 'b', 'fabric stroke-lg', 'text-lg bold')
+
+ return part
+}
+```
+
+
+
+Our example image (which, if you hadn't realized was created with FreeSewing) has a lot of
+paths in it. Each box, the arrows, the lines in the React logo, and so on.
+
+Click the **X-Ray** tab to reveal them.
+
+
+
+Since our example image does not have any snippets in it, here's an example
+of a `button` snippet:
+
+
+```js
+({ Point, Path, paths, Snippet, snippets, part }) => {
+
+ snippets.demo = new Snippet('button', new Point(0,0))
+
+ // Prevent clipping
+ paths.diag = new Path()
+ .move(new Point(-50,-4))
+ .move(new Point(50,4))
+
+ return part
+}
+```
+
+
diff --git a/markdown/dev/guides/patterns/stacks/en.md b/markdown/dev/guides/patterns/stacks/en.md
new file mode 100644
index 00000000000..36c34d026f6
--- /dev/null
+++ b/markdown/dev/guides/patterns/stacks/en.md
@@ -0,0 +1,57 @@
+---
+title: Stacks
+order: 110
+---
+
+Stacks come into play when layouting a pattern.
+The FreeSewing core library, by default, will handle the layout of a pattern
+for you by placing all parts next to each other in as small a space as
+possible.
+
+That is _typically_ what you want, but not always. For example, when sampling,
+you want parts to be stacked on top of each other:
+
+
+```js
+({
+ Point,
+ points,
+ Path,
+ paths,
+ Snippet,
+ snippets,
+ measurements,
+ part
+}) => {
+
+ const size = measurements.head
+ paths.box = new Path()
+ .move(new Point(0,0))
+ .line(new Point(0, size/3))
+ .line(new Point(size, size/3))
+ .line(new Point(size, 0))
+ .addClass('fabric')
+ .close()
+ points.logo = new Point(size/2, size/5)
+ snippets.logo = new Snippet('logo', points.logo)
+
+ return part
+}
+```
+
+
+Under the hood, sampling uses multiple sets of settings, and then uses stacks
+to place them on top of each other. But this functionality is also available
+to patterns designers who want to use it.
+
+Essentially, stacks behave as layers. Parts that are on the same _stack_ will be stacked on top of each other in the layout.
+
+You can stack parts from the same set, or from different sets.
+
+Include code example
+
+
+In the vast majority of cases you won't be using any stacks, or the stacking
+will be handled for you by the core library (like in the sampling example
+above).
+
diff --git a/markdown/dev/guides/patterns/store/en.md b/markdown/dev/guides/patterns/store/en.md
index d8fa98ede6c..0234f1e2185 100644
--- a/markdown/dev/guides/patterns/store/en.md
+++ b/markdown/dev/guides/patterns/store/en.md
@@ -3,25 +3,21 @@ title: Store
order: 60
---
-
-The store provides pattern-wide key/value storage
-
-
-The store provides key-value storage that is shared across your pattern.
-
+The store in a FreeSewing pattern provides shared key-value storage.
If you have some information in one part that you want to make available
outside that part (in another part) you can save it to the store.
-A **Store** object holds a simple key/value store with methods for storing and retrieving information.
+There are two types of stores:
+- A pattern-wide store that
+- A store per set that is shared across parts in that set
-## Note
+When you interact with a store in your part code, it is almost certainly the
+so-called setStore, the store that is shared accross parts in the set.
+The pattern-wide store is used for pattern initialization and storing logs and
+other data in the early stages of the pattern lifecycle.
-A store is typically used to share information between parts. For example
-the length of the neck opening in one part can be used to calculate the
-length for the collar in another part.
-All patterns have access to
```js