diff --git a/packages/freesewing.shared/components/workbench/draft/circle/index.js b/packages/freesewing.shared/components/workbench/draft/circle/index.js
new file mode 100644
index 00000000000..72dedf1bf14
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/circle/index.js
@@ -0,0 +1,10 @@
+const Circle = (props) => (
+
+)
+
+export default Circle
diff --git a/packages/freesewing.shared/components/workbench/draft/defs/index.js b/packages/freesewing.shared/components/workbench/draft/defs/index.js
new file mode 100644
index 00000000000..4f494dee70a
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/defs/index.js
@@ -0,0 +1,69 @@
+const style = ` style="fill: none; stroke: currentColor;" `
+const grids = {
+ imperial: `
+
+
+
+
+
+
+ `,
+ metric: `
+
+
+
+
+
+
+
+
+ `
+}
+
+const Defs = (props) => {
+ let defs = props.svg.defs
+ if (props.settings.paperless) {
+ defs += grids[props.settings.units || 'metric']
+ for (let p in props.parts) {
+ let anchor = { x: 0, y: 0 }
+ if (typeof props.parts[p].points.gridAnchor !== 'undefined')
+ anchor = props.parts[p].points.gridAnchor
+ else if (typeof props.parts[p].points.anchor !== 'undefined')
+ anchor = props.parts[p].points.anchor
+
+ defs += ``
+ }
+ }
+
+ return
+}
+
+export default Defs
diff --git a/packages/freesewing.shared/components/workbench/draft/index.js b/packages/freesewing.shared/components/workbench/draft/index.js
new file mode 100644
index 00000000000..7d41c1bbe27
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/index.js
@@ -0,0 +1,35 @@
+import React, { useState } from 'react'
+import Svg from './svg'
+import Defs from './defs'
+import Part from './part'
+
+const LabDraft = ({ app, pattern, gist, updateGist }) => {
+
+ const patternInstance = new pattern(gist)
+ patternInstance.draft()
+ const patternProps = patternInstance.getRenderProps()
+
+
+
+ return (
+
+ )
+}
+
+export default LabDraft
diff --git a/packages/freesewing.shared/components/workbench/draft/part/index.js b/packages/freesewing.shared/components/workbench/draft/part/index.js
new file mode 100644
index 00000000000..daa51335935
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/part/index.js
@@ -0,0 +1,139 @@
+import Path from '../path'
+import Point from '../point'
+import Snippet from '../snippet'
+import { getProps } from '../utils'
+
+const Part = (props) => {
+ const focusPoint = (point, i) => {
+ const p = props.part.points[point]
+ const pathString = `M ${p.x} ${props.part.topLeft.y} `
+ + `L ${p.x} ${props.part.bottomRight.y} `
+ + `M ${props.part.topLeft.x} ${p.y} `
+ + `L ${props.part.bottomRight.x} ${p.y} `
+ const classes = 'focus point c' + (i % 8) // Cycle through 8 colors
+ return (
+
+
+
+ props.raiseEvent('clearFocus', {
+ part: props.name,
+ type: 'points',
+ name: point
+ })
+ }
+ />
+
+ )
+ }
+
+ const focusCoords = (p, i) => {
+ let pathString = `M ${p.x} ${props.part.topLeft.y} `
+ pathString += `L ${p.x} ${props.part.bottomRight.y} `
+ pathString += `M ${props.part.topLeft.x} ${p.y} `
+ pathString += `L ${props.part.bottomRight.x} ${p.y} `
+ let classes = 'focus coords c' + (i % 4) // Cycle through 4 CSS classes
+ return (
+
+
+
+ props.raiseEvent('clearFocus', {
+ part: props.name,
+ type: 'coords',
+ data: p
+ })
+ }
+ />
+
+ )
+ }
+
+ let grid = props.paperless ? (
+
+ ) : null
+
+ let focus = []
+ if (props.develop) {
+ if (props.focus && typeof props.focus[props.name] !== 'undefined') {
+ for (let i in props.focus[props.name].points)
+ focus.push(focusPoint(props.focus[props.name].points[i], i))
+ for (let i in props.focus[props.name].paths) {
+ let name = props.focus[props.name].paths[i]
+ focus.push(
+
+ props.raiseEvent('clearFocus', {
+ part: props.name,
+ type: 'paths',
+ name
+ })
+ }
+ />
+ )
+ }
+ for (let i in props.focus[props.name].coords)
+ focus.push(focusCoords(props.focus[props.name].coords[i], i))
+ }
+ }
+
+ return (
+
+ {grid}
+ {Object.keys(props.part.paths).map((name) => (
+
+ ))}
+ {Object.keys(props.part.points).map((name) => (
+
+ ))}
+ {Object.keys(props.part.snippets).map((name) => (
+
+ ))}
+ {focus}
+
+ )
+}
+
+export default Part
diff --git a/packages/freesewing.shared/components/workbench/draft/path/index.js b/packages/freesewing.shared/components/workbench/draft/path/index.js
new file mode 100644
index 00000000000..2a1f6f33258
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/path/index.js
@@ -0,0 +1,17 @@
+import TextOnPath from '../text-on-path'
+import { getProps } from '../utils'
+
+const Path = (props) => {
+ if (!props.path.render) return null
+ const output = []
+ const pathId = 'path-' + props.part + '-' + props.name
+ output.push(
+
+ )
+ if (props.path.attributes.get('data-text'))
+ output.push()
+
+ return output
+}
+
+export default Path
diff --git a/packages/freesewing.shared/components/workbench/draft/point/index.js b/packages/freesewing.shared/components/workbench/draft/point/index.js
new file mode 100644
index 00000000000..88e5a290460
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/point/index.js
@@ -0,0 +1,14 @@
+import Text from '../text'
+import Circle from '../circle'
+
+const Point = (props) => {
+ const output = []
+ if (props.point.attributes && props.point.attributes.get('data-text'))
+ output.push()
+ if (props.point.attributes && props.point.attributes.get('data-circle'))
+ output.push()
+
+ return output.length < 1 ? null : output
+}
+
+export default Point
diff --git a/packages/freesewing.shared/components/workbench/draft/snippet/index.js b/packages/freesewing.shared/components/workbench/draft/snippet/index.js
new file mode 100644
index 00000000000..1d1fd0b1627
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/snippet/index.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { getProps } from '../utils'
+
+const Snippet = (props) => {
+ const snippetProps = {
+ xlinkHref: '#' + props.snippet.def,
+ x: props.snippet.anchor.x,
+ y: props.snippet.anchor.y
+ }
+ let scale = props.snippet.attributes.get('data-scale')
+ let rotate = props.snippet.attributes.get('data-rotate')
+ if (scale || rotate) {
+ snippetProps.transform = ''
+ if (scale) {
+ snippetProps.transform += `translate(${snippetProps.x}, ${snippetProps.y}) `
+ snippetProps.transform += `scale(${scale}) `
+ snippetProps.transform += `translate(${snippetProps.x * -1}, ${snippetProps.y * -1}) `
+ }
+ if (rotate) {
+ snippetProps.transform += `rotate(${rotate}, ${snippetProps.x}, ${snippetProps.y}) `
+ }
+ }
+
+ return
+}
+
+export default Snippet
diff --git a/packages/freesewing.shared/components/workbench/draft/svg/index.js b/packages/freesewing.shared/components/workbench/draft/svg/index.js
new file mode 100644
index 00000000000..d9c629ea42a
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/svg/index.js
@@ -0,0 +1,33 @@
+import React from 'react'
+
+const Svg = ({
+ embed = true,
+ develop = false,
+ language = 'en',
+ className = 'freesewing pattern',
+ style = {},
+ viewBox = false,
+ width,
+ height,
+ children
+}) => {
+ let attributes = {
+ xmlns: 'http://www.w3.org/2000/svg',
+ 'xmlns:svg': 'http://www.w3.org/2000/svg',
+ xmlnsXlink: 'http://www.w3.org/1999/xlink',
+ xmlLang: language,
+ viewBox: viewBox || `0 0 ${width} ${height}`,
+ className,
+ style
+ }
+
+ if (!embed) {
+ attributes.width = width + 'mm'
+ attributes.height = height + 'mm'
+ }
+ if (develop) attributes.className += ' develop'
+
+ return
+}
+
+export default Svg
diff --git a/packages/freesewing.shared/components/workbench/draft/text-on-path/index.js b/packages/freesewing.shared/components/workbench/draft/text-on-path/index.js
new file mode 100644
index 00000000000..1228d6fca51
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/text-on-path/index.js
@@ -0,0 +1,25 @@
+const TextOnPath = (props) => {
+ const text = []
+ // Handle translation
+ let translated = ''
+ for (let string of props.path.attributes.getAsArray('data-text')) {
+ translated += props.app.t(string).replace(/"/g, '"') + ' '
+ }
+ const textPathProps = {
+ xlinkHref: '#' + props.pathId,
+ startOffset: '0%'
+ }
+ const align = props.path.attributes.get('data-text-class')
+ if (align && align.indexOf('center') > -1) textPathProps.startOffset = '50%'
+ else if (align && align.indexOf('right') > -1) textPathProps.startOffset = '100%'
+
+ return (
+
+
+ {translated}
+
+
+ )
+}
+
+export default TextOnPath
diff --git a/packages/freesewing.shared/components/workbench/draft/text/index.js b/packages/freesewing.shared/components/workbench/draft/text/index.js
new file mode 100644
index 00000000000..05655c43057
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/text/index.js
@@ -0,0 +1,38 @@
+const Text = (props) => {
+ let text = []
+ // Handle translation
+ let translated = ''
+ for (let string of props.point.attributes.getAsArray('data-text')) {
+ translated += props.app.t(string.toString()).replace(/"/g, '"') + ' '
+ }
+ // Handle muti-line text
+ if (translated.indexOf('\n') !== -1) {
+ let key = 0
+ let lines = translated.split('\n')
+ text.push({lines.shift()})
+ for (let line of lines) {
+ key++
+ text.push(
+
+ {line.toString().replace(/"/g, '"')}
+
+ )
+ }
+ } else text.push({translated})
+
+ return (
+
+ {text}
+
+ )
+}
+
+export default Text
diff --git a/packages/freesewing.shared/components/workbench/draft/utils.js b/packages/freesewing.shared/components/workbench/draft/utils.js
new file mode 100644
index 00000000000..f023e26b58b
--- /dev/null
+++ b/packages/freesewing.shared/components/workbench/draft/utils.js
@@ -0,0 +1,36 @@
+export const getProps = (obj) => {
+ /** I can't believe it but there seems to be no method on NPM todo this */
+ const cssKey = (key) => {
+ let chunks = key.split('-')
+ if (chunks.length > 1) {
+ key = chunks.shift()
+ for (let s of chunks) key += s.charAt(0).toUpperCase() + s.slice(1)
+ }
+
+ return key
+ }
+
+ const convert = (css) => {
+ let style = {}
+ let rules = css.split(';')
+ for (let rule of rules) {
+ let chunks = rule.split(':')
+ if (chunks.length === 2) style[cssKey(chunks[0].trim())] = chunks[1].trim()
+ }
+ return style
+ }
+
+ let rename = {
+ class: 'className',
+ 'marker-start': 'markerStart',
+ 'marker-end': 'markerEnd'
+ }
+ let props = {}
+ for (let key in obj.attributes.list) {
+ if (key === 'style') props[key] = convert(obj.attributes.get(key))
+ if (Object.keys(rename).indexOf(key) !== -1) props[rename[key]] = obj.attributes.get(key)
+ else if (key !== 'style') props[key] = obj.attributes.get(key)
+ }
+
+ return props
+}