1
0
Fork 0

Merge branch 'develop' into eriese-imperial

This commit is contained in:
Enoch Riese 2022-06-17 22:31:11 -05:00
commit 7476d45f54
575 changed files with 2464 additions and 1196 deletions

View file

@ -0,0 +1,16 @@
const defaultSettings = {
sa: 0,
saBool: false,
saMm: 10,
scale: 1,
complete: true,
paperless: false,
units: 'metric',
locale: 'en',
margin: 2,
renderer: 'react',
embed: true,
debug: true,
}
export default defaultSettings

View file

@ -0,0 +1,10 @@
const Circle = (props) => (
<circle
cx={props.point.x}
cy={props.point.y}
r={props.point.attributes.get('data-circle')}
{...props.point.attributes.asPropsIfPrefixIs('data-circle-')}
/>
)
export default Circle

View file

@ -0,0 +1,69 @@
const style = ` style="fill: none; stroke: currentColor;" `
const grids = {
imperial: `
<pattern id="grid" height="25.4" width="25.4" patternUnits="userSpaceOnUse" key="grid">
<path class="gridline lg imperial" d="M 0 0 L 0 25.4 L 25.4 25.4" ${style} />
<path
class="gridline lg imperial"
d="M 12.7 0 L 12.7 25.4 M 0 12.7 L 25.4 12.7"
${style}
/>
<path
class="gridline sm imperial"
d="M 3.175 0 L 3.175 25.4 M 6.32 0 L 6.35 25.4 M 9.525 0 L 9.525 25.4 M 15.875 0 L 15.875 25.4 M 19.05 0 L 19.05 25.4 M 22.225 0 L 22.225 25.4"
${style}
/>
<path
class="gridline sm imperial"
d="M 0 3.175 L 25.4 3.175 M 0 6.32 L 25.4 6.35 M 0 9.525 L 25.4 9.525 M 0 15.875 L 25.4 15.875 M 0 19.05 L 25.4 19.05 M 0 22.225 L 25.4 22.225"
${style}
/>
</pattern>
`,
metric: `
<pattern id="grid" height="100" width="100" patternUnits="userSpaceOnUse" key="grid">
<path class="gridline lg metric" d="M 0 0 L 0 100 L 100 100" ${style} />
<path class="gridline metric" d="M 50 0 L 50 100 M 0 50 L 100 50" ${style} />
<path
class="gridline sm metric"
d="M 10 0 L 10 100 M 20 0 L 20 100 M 30 0 L 30 100 M 40 0 L 40 100 M 60 0 L 60 100 M 70 0 L 70 100 M 80 0 L 80 100 M 90 0 L 90 100"
${style}
/>
<path
class="gridline sm metric"
d="M 0 10 L 100 10 M 0 20 L 100 20 M 0 30 L 100 30 M 0 40 L 100 40 M 0 60 L 100 60 M 0 70 L 100 70 M 0 80 L 100 80 M 0 90 L 100 90"
${style}
/>
<path
class="gridline xs metric"
d="M 5 0 L 5 100 M 15 0 L 15 100 M 25 0 L 25 100 M 35 0 L 35 100 M 45 0 L 45 100 M 55 0 L 55 100 M 65 0 L 65 100 M 75 0 L 75 100 M 85 0 L 85 100 M 95 0 L 95 100"
${style}
/>
<path
class="gridline xs metric"
d="M 0 5 L 100 5 M 0 15 L 100 15 M 0 25 L 100 25 M 0 35 L 100 35 M 0 45 L 100 45 M 0 55 L 100 55 M 0 65 L 100 65 M 0 75 L 100 75 M 0 85 L 100 85 M 0 95 L 100 95"
${style}
/>
</pattern>
`
}
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 += `<pattern id="grid-${p}" key="grid-${p}" xlink:href="#grid" x="${anchor.x}" y="${anchor.y}" />`
}
}
return <defs dangerouslySetInnerHTML={{ __html: defs }} />
}
export default Defs

View file

@ -0,0 +1,69 @@
import { useState } from 'react'
import Robot from 'shared/components/robot/index.js'
import Popout from 'shared/components/popout.js'
import { useTranslation } from 'next-i18next'
const Error = ({ draft, patternProps, error, updateGist }) => {
const { t } = useTranslation(['errors'])
const [share, setShare] = useState(false)
return (
<div className="max-w-4xl m-auto">
<Popout warning>
<div className="flex flex-row justify-between">
<div>
<h3>{t('errors:something')}</h3>
<p>Don't be alarmed, but we ran into some trouble while drafting this pattern.</p>
</div>
<Robot pose='fail' />
</div>
</Popout>
<Popout tip>
<h3>Would you like to report this problem?</h3>
<p>
You can help us <strong>make FreeSewing better by reporting this problem</strong>.
</p>
<p>If you choose to report this:</p>
<ul className="list-disc list-inside ml-4 text-xl">
<li>
We will compile a <strong>crash report</strong> that contains everything needed <strong>to recreate this problem</strong>
</li>
<li>
We will include <strong>personal data</strong> such as your <strong>username</strong>, <strong>
email address</strong> and <strong>measurements</strong>
</li>
<li>
We will share this report and the data in it with <a className="text-primary font-bold"
href="https://github.com/orgs/freesewing/teams/bughunters">FreeSewing's bughunters team</a> who will investigate the problem on your behalf
</li>
<li>Your personal data will <strong>not be shared publicly</strong></li>
</ul>
<div className="form-control">
<label className="cursor-pointer flex flex-row gap-4 my-4">
<input type="checkbox" checked={share} className="checkbox checkbox-primary" onChange={() => setShare(!share)}/>
<span className="label-text text-xl">I agree to the use of my personal data for the purposes outlined above</span>
</label>
</div>
<p>
<button disabled={!share} className="btn btn-primary">Report this</button>
</p>
<p>
If you prefer not to share any info, or want to investigate the problem yourself, you can do so:
</p>
<ul className="list-disc list-inside ml-4 text-xl">
<li>
Check the <button className="btn-link" onClick={() => updateGist(['_state', 'view'], 'events')}>
<strong>{patternProps.events.error.length} errors</strong> and <strong>
{patternProps.events.warning.length} warnings</strong></button>
</li>
<li>Check the partially rendered pattern below to see which areas are problematic</li>
</ul>
</Popout>
</div>
)
}
export default Error

View file

@ -0,0 +1,40 @@
import SvgWrapper from './svg-wrapper'
import Error from './error.js'
import Robot from 'shared/components/robot/index.js'
const LabDraft = props => {
const { app, draft, design, gist, updateGist, unsetGist, feedback } = props
if (!draft) return null
// Render as SVG
if (gist?.renderer === 'svg') {
let svg
try { svg = draft.render() }
catch(error) {
console.log('Failed to render design', error)
return <Error error={error} {...props} />
}
return <div dangerouslySetInnerHTML={{ __html: svg }} />
}
// Render as React
let patternProps = {}
try { patternProps = draft.getRenderProps() }
catch(error) {
console.log('Failed to get render props for design', error)
return <Error error={error} {...props} />
}
return (
<>
{(!patternProps || patternProps.events.error.length > 0)
? <Error {...{ draft, patternProps, updateGist }} />
: null
}
<SvgWrapper {...{ draft, patternProps, gist, updateGist, unsetGist, app, feedback }} />
</>
)
}
export default LabDraft

View file

@ -0,0 +1,114 @@
import React from 'react'
import Path from '../path'
import Point from '../point'
import Snippet from '../snippet'
import { getProps } from '../utils'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
console.log(error)
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
console.log(error, errorInfo)
}
render() {
if (this.state.hasError) {
console.log('in error boundary', props)
return <text>Something went wrong.</text>
}
return this.props.children
}
}
const XrayPart = props => {
// Don't bother if this is the only part on display
if (props.gist.only && props.gist.only.length === 1) return null
const i = props.gist._state?.xray?.reveal
? Object.keys(props.gist._state.xray.reveal).indexOf(props.partName)%10
: 0
const { topLeft, bottomRight } = props.part
return (
<g>
<path d={`
M ${topLeft.x} ${topLeft.y}
L ${topLeft.x} ${bottomRight.y}
L ${bottomRight.x} ${bottomRight.y}
L ${bottomRight.x} ${topLeft.y}
z`} className={`fill-color-${i} opacity-10`} />
</g>
)
}
const Part = props => {
const { partName, part, app, gist, updateGist } = props
const grid = gist.paperless ? (
<rect
x={part.topLeft.x}
y={part.topLeft.y}
width={part.width}
height={part.height}
className="grid"
fill={'url(#grid-' + partName + ')'}
/>
) : null
return (
<g {...getProps(part)} id={`part-${partName}`}>
{grid}
{
props.gist?._state?.xray?.enabled &&
props.gist?._state?.xray?.reveal?.[partName]
&& <XrayPart {...props} />
}
{Object.keys(part.paths).map((pathName) => (
<Path
key={pathName}
pathName={pathName}
path={part.paths[pathName]}
topLeft={props.part.topLeft}
bottomRight={props.part.bottomRight}
{...props}
/>
))}
{Object.keys(props.part.points).map((pointName) => (
<Point
key={pointName}
pointName={pointName}
point={props.part.points[pointName]}
topLeft={props.part.topLeft}
bottomRight={props.part.bottomRight}
{...props}
/>
))}
{Object.keys(props.part.snippets).map((snippetName) => (
<Snippet
key={snippetName}
snippetName={snippetName}
snippet={props.part.snippets[snippetName]}
{...props}
/>
))}
{focus}
</g>
)
}
/*
<ErrorBoundary
x={part.topLeft.x}
y={part.topLeft.y}
width={part.width}
height={part.height}
>
</ErrorBoundary>
*/
export default Part

View file

@ -0,0 +1,41 @@
import React from 'react'
import TextOnPath from '../text-on-path'
import { getProps } from '../utils'
const XrayPath = props => (
<g>
<path
d={props.path.asPathstring()}
{...getProps(props.path)}
className="opacity-0 stroke-3xl stroke-contrast hover:opacity-25 hover:cursor-pointer"
onClick={() => props.updateGist(
['_state', 'xray', 'parts', props.partName, 'paths', props.pathName],
1
)}
/>
</g>
)
const Path = props => {
const { path, partName, pathName } = props
if (!path.render) return null
const output = []
const pathId = 'path-' + partName + '-' + pathName
let d = ''
try { d = path.asPathstring() }
catch (err) {
// Bail out
console.log(`Failed to generate pathstring for path ${pathId} in part ${partName}`, err)
return null
}
output.push(<path id={pathId} key={pathId} d={d} {...getProps(path)} />)
if (path.attributes.get('data-text'))
output.push(<TextOnPath key={'text-on-path-' + name} pathId={pathId} {...props} />)
if (props.gist._state?.xray?.enabled) output.push(<XrayPath {...props} key={'xpath'+pathId} />)
return output
}
export default Path

View file

@ -0,0 +1,157 @@
import Text from '../text'
import Circle from '../circle'
import { round, formatMm } from 'shared/utils'
const RevealPoint = props => {
const r = 15 * props.gist.scale
const { x, y } = props.point
const { topLeft, bottomRight } = props.part
const i = Object.keys(props.gist._state.xray.reveal[props.partName].points).indexOf(props.pointName)%10
const classes = `stroke-sm stroke-color-${i} stroke-dashed`
return (
<g>
<circle
cx={x}
cy={y}
r={r}
className={classes}
/>
<path d={`
M ${x} ${topLeft.y} L ${x} ${y - r}
m 0 ${2*r} L ${x} ${bottomRight.y}
M ${topLeft.x} ${y} L ${x - r} ${y}
m ${2*r} 0 L ${bottomRight.x} ${y}`} className={classes} />
</g>
)
}
// Length for the indicators
const lead = 20
// Length for the text on indicators
// this is longer to prevent text from being cropped
const longLead = 40
const Coord = ({id, val, pointName}) => (
<text>
<textPath xlinkHref={`#${id}`} startOffset="50%">
<tspan className="center fill-note text-sm" dy={0}>
{round(val)}
</tspan>
</textPath>
<textPath xlinkHref={`#${id}`} startOffset="50%">
<tspan className="center fill-note text-xs" dy={5}>
{pointName}
</tspan>
</textPath>
</text>
)
const PosX = ({ id, point, scale, pointName }) => (
<g>
<path id={id+'_x'} d={`
M ${point.x - (point.x < 0 ? 0 : lead*scale)} ${point.y}
l ${lead * scale} 0
`}
className="stroke-note stroke-sm"
markerStart={point.x < 0 ? "url(#grainlineFrom)" : ''}
markerEnd={point.x < 0 ? '' : "url(#grainlineTo)"}
/>
<path id={id+'_xlong'} d={`
M ${point.x - (point.x < 0 ? longLead/2.4*scale : longLead*scale)} ${point.y}
l ${longLead * scale * 1.4} 0
`}
className="hidden"
/>
<Coord id={`${id}_xlong`} val={point.x} pointName={pointName}/>
</g>
)
const PosY = ({ id, point, scale, pointName }) => (
<g>
<path id={id+'_y'} d={`
M ${point.x} ${point.y + (point.y < 0 ? lead*scale : 0)}
l 0 ${lead * scale * -1}
`}
className="stroke-note stroke-sm"
markerStart={point.y < 0 ? '' : "url(#grainlineFrom)"}
markerEnd={point.y < 0 ? "url(#grainlineTo)" : ''}
/>
<path id={id+'_ylong'} d={`
M ${point.x} ${point.y + (point.y < 0 ? longLead/1.25*scale : longLead*scale/5)}
l 0 ${longLead * scale * -1}
`}
className="hidden"
/>
<Coord id={`${id}_ylong`} val={point.y} pointName={pointName} />
</g>
)
const ActiveXrayPoint = props => {
const id = `${props.partName}_${props.pointName}_xray_point`
const r = 15 * props.gist.scale
const { x, y } = props.point
const { topLeft, bottomRight } = props.part
const i = Object.keys(props.gist._state.xray.parts[props.partName].points).indexOf(props.pointName)%10
const classes = `stroke-sm stroke-color-${i} stroke-dashed`
const posProps = {
id,
point: props.point,
pointName: props.pointName,
scale: props.gist.scale,
}
return <g><PosX {...posProps} /><PosY {...posProps} /></g>
}
const PassiveXrayPoint = props => (
<g>
<circle
cx={props.point.x}
cy={props.point.y}
r={2 * props.gist.scale}
className="stroke-sm stroke-lining fill-lining fill-opacity-25" />
<circle
cx={props.point.x}
cy={props.point.y}
r={7.5 * props.gist.scale}
className="opacity-0 stroke-lining fill-lining hover:opacity-25 hover:cursor-pointer"
onClick={props.gist._state.xray?.parts?.[props.partName]?.points?.[props.pointName]
? () => props.unsetGist(
['_state', 'xray', 'parts', props.partName, 'points', props.pointName]
)
: () => props.updateGist(
['_state', 'xray', 'parts', props.partName, 'points', props.pointName],
1
)
}
/>
</g>
)
const Point = props => {
const { point, pointName, partName, gist } = props
const output = []
if (gist._state?.xray?.enabled) {
// Passive indication for points
output.push(<PassiveXrayPoint {...props} key={'xp-' + pointName} />)
// Active indication for points (point that have been clicked on)
if (gist._state?.xray?.parts?.[partName]?.points?.[pointName])
output.push(<ActiveXrayPoint {...props} key={'rp-' + pointName} />)
// Reveal (based on clicking the seach icon in sidebar
if (gist._state?.xray?.reveal?.[partName]?.points?.[pointName])
output.push(<RevealPoint {...props} key={'rp-' + pointName} />)
}
// Render text
if (point.attributes && point.attributes.get('data-text'))
output.push(<Text {...props} key={'point-' + pointName} />)
// Render circle
if (point.attributes && point.attributes.get('data-circle'))
output.push(<Circle point={point} key={'circle-' + pointName} />)
return output.length < 1 ? null : output
}
export default Point

View file

@ -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 <use {...snippetProps} {...getProps(props.snippet)} />
}
export default Snippet

View file

@ -0,0 +1,59 @@
import { SizeMe } from 'react-sizeme'
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"
import Svg from './svg'
import Defs from './defs'
import Part from './part'
/* What's with all the wrapping?
*
* Glad you asked. The problem lies within the pan & zoom functionality
* For this to work, we need to reliably determine the container widht
* However, our SVG is optimized for embedding (no width or height)
* which means it will simply adapt to the parent container.
* So now we have a parent container adapting to the content and the
* content adapting to the parent. This creates a sort of stalemate
* where the browser will render this as zero width and height.
*
* To avoid that, we use the SizeMe which will report the size of the
* grandparent element, and then we wrap our SVG in a div that we
* set to this size. This will cause the SVG to fill in that entire
* space, and the pan and zoom to adapt to this size.
*
* Note that this also means that changing the browser window size
* will cause things to get clipped until the next render.
*
* Also still to see how this will work with SSR
*/
const SvgWrapper = props => {
const { patternProps, gist, app, updateGist, unsetGist } = props
return <SizeMe>{({ size }) => (
<TransformWrapper
minScale={0.1}
centerZoomedOut={true}
wheel={{ activationKeys: ['Control'] }}
>
<TransformComponent>
<div style={{ width: size.width+'px', }}>
<Svg {...patternProps} embed={gist.embed}>
<Defs {...patternProps} />
<style>{`:root { --pattern-scale: ${gist.scale || 1}}`}</style>
<g>
{Object.keys(patternProps.parts).map((name) => (
<Part {...{ app, gist, updateGist, unsetGist }}
key={name}
partName={name}
part={patternProps.parts[name]}
/>
))}
</g>
</Svg>
</div>
</TransformComponent>
</TransformWrapper>
)}</SizeMe>
}
export default SvgWrapper

View file

@ -0,0 +1,35 @@
import React from 'react'
const Svg = ({
embed = true,
develop = false,
locale = 'en',
className = 'freesewing pattern',
style = {},
viewBox = false,
width,
height,
children
}) => {
if (width < 1) width = 1000
if (height < 1) height = 1000
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: locale,
viewBox: viewBox || `0 0 ${width} ${height}`,
className,
style
}
if (!embed) {
attributes.width = width + 'mm'
attributes.height = height + 'mm'
}
if (develop) attributes.className += ' develop'
return <svg {...attributes}>{children}</svg>
}
export default Svg

View file

@ -0,0 +1,31 @@
import { useTranslation } from 'next-i18next'
const TextOnPath = (props) => {
const { t } = useTranslation(['app'])
const text = []
// Handle translation (and spaces)
let translated = ''
for (let string of props.path.attributes.getAsArray('data-text')) {
translated += t(string).replace(/&quot;/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 (
<text>
<textPath {...textPathProps}>
<tspan
{...props.path.attributes.asPropsIfPrefixIs('data-text-')}
dangerouslySetInnerHTML={{__html: translated}}
/>
</textPath>
</text>
)
}
export default TextOnPath

View file

@ -0,0 +1,41 @@
import { useTranslation } from 'next-i18next'
const Text = (props) => {
const { t } = useTranslation(['app'])
let text = []
// Handle translation
let translated = ''
for (let string of props.point.attributes.getAsArray('data-text')) {
translated += t(string.toString()).replace(/&quot;/g, '"') + ' '
}
// Handle muti-line text
if (translated.indexOf('\n') !== -1) {
let key = 0
let lines = translated.split('\n')
text.push(<tspan key={'tspan-' + key}>{lines.shift()}</tspan>)
for (let line of lines) {
key++
text.push(
<tspan
key={'tspan-' + key}
x={props.point.x}
dy={props.point.attributes.get('data-text-lineheight') || 12}
>
{line.toString().replace(/&quot;/g, '"')}
</tspan>
)
}
} else text.push(<tspan key="tspan-1">{translated}</tspan>)
return (
<text
x={props.point.x}
y={props.point.y}
{...props.point.attributes.asPropsIfPrefixIs('data-text-')}
>
{text}
</text>
)
}
export default Text

View file

@ -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
}

View file

@ -0,0 +1,64 @@
import Markdown from 'react-markdown'
// Markdown wrapper to suppress creation of P tags
const Md = ({ children }) => <Markdown components={{ p: props => props.children }}>{children}</Markdown>
const renderEvent = evt => Array.isArray(evt)
? <Md>{evt[0]}</Md>
: typeof evt === 'object'
? JSON.stringify(evt, null, 2)
: <Md>{evt}</Md>
const EventGroup = ({ type='info', events=[] }) => events.length > 0 ? (
<div className="">
<h3 className="capitalize" id={`events-${type}`}>{type}</h3>
<table className="table w-full mdx">
<thead>
<tr>
<th className="text-right w-16">#</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{events.map((evt, i) => (
<tr key={i} className="leading-1 hover:bg-base-200 hover:bg-opacity-40">
<td className="text-right p-1 pr-4 font-bold opacity-80 text-accent">{i}</td>
<td className="p-1 pl-4">{renderEvent(evt)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : null
const order = [
'error',
'warning',
'info',
'debug'
]
const Events = props => props?.draft?.events
? (
<div className="max-w-screen-xl m-auto">
<div className="flex flex-col">
<ul className="flex flex-row row-wrap">
{order.map(type => (props.draft.events[type].length > 0)
? (
<li key={type} className="">
<a href={`#events-${type}`} className={`text-secondary font-bold capitalize text-xl`}>{type}</a>
{type === 'debug' ? '' : <span className="px-2 font-bold">|</span>}
</li>
) : (
<li key={type} className="text-base-content font-bold capitalize text-xl">
<span className="opacity-50">{type}</span>
{type === 'debug' ? '' : <span className="px-2 font-bold">|</span>}
</li>
)
)}
</ul>
{order.map(type => <EventGroup type={type} events={props.draft.events[type]} />)}
</div>
</div>
) : null
export default Events

View file

@ -0,0 +1,116 @@
import { useState } from 'react'
import { SizeMe } from 'react-sizeme'
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"
import svgattrPlugin from '@freesewing/plugin-svgattr'
import { useTranslation } from 'next-i18next'
import fileSaver from 'file-saver'
import yaml from 'js-yaml'
import axios from 'axios'
import Popout from 'shared/components/popout'
import WebLink from 'shared/components/web-link'
import theme from '@freesewing/plugin-theme'
export const exports = {
exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'],
exportForEditing: ['svg', 'pdf'],
exportAsData: ['json', 'yaml', 'github gist'],
}
const handleExport = (format, gist, design, app, setLink, setFormat) => {
setLink(false)
setFormat(format)
if (exports.exportAsData.indexOf(format) !== -1) {
if (format === 'json') exportJson(gist)
else if (format === 'yaml') exportYaml(gist)
else if (format === 'github gist') exportGithubGist(gist, app, setLink)
}
else {
gist.embed=false
let svg = ''
try {
svg = new design(gist).use(theme).draft().render()
} catch(err) {
console.log(err)
}
if (format === 'svg') return exportSvg(gist, svg)
app.startLoading()
axios.post('https://tiler.freesewing.org/api', {
svg,
format: 'pdf',
size: format === 'pdf' ? 'full' : format,
url: 'https://freesewing.org',
design: gist.design
})
.then(res => setLink(res.data.link))
.catch(err => console.log(err))
.finally(() => app.stopLoading())
}
}
const exportJson = gist => {
const blob = new Blob([JSON.stringify(gist, null, 2)], {
type: 'application/json;charset=utf-8'
})
fileSaver.saveAs(blob, `freesewing-${gist.design || 'gist'}.json`)
}
const exportYaml = gist => {
const blob = new Blob([yaml.dump(gist)], {
type: 'application/x-yaml;charset=utf-8'
})
fileSaver.saveAs(blob, `freesewing-${gist.design || 'gist'}.yaml`)
}
const exportSvg = (gist, svg) => {
const blob = new Blob([svg], {
type: 'image/svg+xml;charset=utf-8'
})
fileSaver.saveAs(blob, `freesewing-${gist.design || 'pattern'}.svg`)
}
const exportGithubGist = (data, app, setLink) => {
app.setLoading(true)
axios.post('https://backend.freesewing.org/github/gist', {
design: data.design,
data: yaml.dump(data)
})
.then(res => setLink('https://gist.github.com/' + res.data.id))
.catch(err => console.log(err))
.finally(() => app.stopLoading())
}
const ExportDraft = ({ gist, design, app }) => {
const [link, setLink] = useState(false)
const [format, setFormat] = useState(false)
const { t } = useTranslation(['app'])
return (
<div className="max-w-screen-xl m-auto">
<h2>{t('export')}</h2>
<p className="text-lg sm:text-xl">{t('exportPattern-txt')}</p>
{link && (
<Popout link compact>
<span className="font-bold mr-4 uppercase text-sm">
{format}:
</span>
<WebLink href={link} txt={link} />
</Popout>
)}
<div className="flex flex-row flex-wrap gap-8">
{Object.keys(exports).map(type => (
<div key={type} className="flex flex-col gap-2 w-full sm:w-auto">
<h4>{t(type)}</h4>
{exports[type].map(format => (
<button key={format}
className="btn btn-primary"
onClick={() => handleExport(format, gist, design, app, setLink, setFormat)}
>
{type === 'exportForPrinting' ? `${format} pdf` : format }
</button>
))}
</div>
))}
</div>
</div>
)
}
export default ExportDraft

View file

@ -0,0 +1,115 @@
import { useState } from 'react'
import ClearIcon from 'shared/components/icons/clear.js'
import EditIcon from 'shared/components/icons/edit.js'
import { useTranslation } from 'next-i18next'
const EditCount = props => (
<div className="form-control mb-2 w-full">
<label className="label">
<span className="label-text text-base-content">{props.min}</span>
<span className="label-text font-bold text-base-content">{props.value}</span>
<span className="label-text text-base-content">{props.max}</span>
</label>
<label className="input-group input-group-sm">
<input
type="number"
className={`
input input-sm input-bordered grow text-base-content
`}
value={props.value}
onChange={props.handleChange}
/>
<span className="text-base-content font-bold">#</span>
</label>
</div>
)
const DesignOptionCount = props => {
const { t } = useTranslation(['app'])
const { count, max, min } = props.design.config.options[props.option]
const val = (typeof props.gist?.options?.[props.option] === 'undefined')
? count
: props.gist.options[props.option]
const [value, setValue] = useState(val)
const [editCount, setEditCount] = useState(false)
const handleChange = (evt) => {
const newVal = evt.target.value
setValue(newVal)
props.updateGist(['options', props.option], newVal)
}
const reset = () => {
setValue(count)
props.unsetGist(['options', props.option])
}
return (
<div className="py-4 mx-6 border-l-2 pl-2">
<div className="flex flex-row justify-between">
{editCount
? <EditCount
value={value}
handleChange={handleChange}
min={min}
max={max}
setEditCount={setEditCount}
t={t}
/>
: (
<>
<span className="opacity-50">{min}</span>
<span className={
`font-bold ${val===count ? 'text-secondary' : 'text-accent'}`}
>
{val}
</span>
<span className="opacity-50">{max}</span>
</>
)
}
</div>
<input
type="range"
max={max}
min={min}
step={1}
value={value}
onChange={handleChange}
className={`
range range-sm mt-1
${val === count ? 'range-secondary' : 'range-accent'}
`}
/>
<div className="flex flex-row justify-between">
<span></span>
<div>
<button
title={t('reset')}
className="btn btn-ghost btn-xs text-accent"
disabled={val === count}
onClick={reset}
>
<ClearIcon />
</button>
<button
title={t('editThing', { thing: '#' })}
className={`
btn btn-ghost btn-xs hover:text-secondary-focus
${editCount
? 'text-accent'
: 'text-secondary'
}
`}
onClick={() => setEditCount(!editCount)}
>
<EditIcon />
</button>
</div>
</div>
</div>
)
}
export default DesignOptionCount

View file

@ -0,0 +1,66 @@
import { useState } from 'react'
import ClearIcon from 'shared/components/icons/clear.js'
import { useTranslation } from 'next-i18next'
const DesignOptionList = props => {
const { t } = useTranslation(['app'])
const { dflt, list } = props.design.config.options[props.option]
const val = (typeof props.gist?.options?.[props.option] === 'undefined')
? dflt
: props.gist.options[props.option]
const [value, setValue] = useState(val)
const handleChange = (newVal) => {
if (newVal === dflt) reset()
else {
setValue(newVal)
props.updateGist(['options', props.option], newVal)
}
}
const reset = () => {
setValue(dflt)
props.unsetGist(['options', props.option])
}
return (
<div className="py-4 mx-6 border-l-2 pl-2">
<div className="flex flex-row">
<div className="grow">
{list.map(choice => (
<button key={choice}
onClick={() => handleChange(choice)}
className={`
mr-1 mb-1 text-left text-lg w-full
${choice === value
? choice === dflt
? 'text-secondary'
: 'text-accent'
: 'text-base-content'
}
`}
>
<span className={`
text-3xl mr-2 inline-block p-0 leading-3
translate-y-3
`}>
<>&deg;</>
</span>
{props.ot(`${props.option}.o.${choice}`)}
</button>
))}
</div>
<button
title={t('reset')}
className=""
disabled={val === dflt}
onClick={reset}
>
<span className={val===dflt ? 'text-base' : 'text-accent'}><ClearIcon /></span>
</button>
</div>
</div>
)
}
export default DesignOptionList

View file

@ -0,0 +1,128 @@
import { useState } from 'react'
import ClearIcon from 'shared/components/icons/clear'
import EditIcon from 'shared/components/icons/edit'
import { formatMm, round } from 'shared/utils'
import { useTranslation } from 'next-i18next'
const EditOption = props => (
<div className="form-control mb-2 w-full">
<label className="label">
<span className="label-text text-base-content">{props.min}{props.suffix}</span>
<span className="label-text font-bold text-base-content">{props.value}{props.suffix}</span>
<span className="label-text text-base-content">{props.max}{props.suffix}</span>
</label>
<label className="input-group input-group-sm">
<input
type="number"
className={`
input input-sm input-bordered grow text-base-content
`}
value={props.value}
onChange={props.handleChange}
/>
<span className="text-base-content font-bold">{props.suffix}</span>
</label>
</div>
)
const DesignOptionPctDeg = props => {
const { t } = useTranslation(['app'])
const suffix = props.type === 'deg' ? '°' : '%'
const factor = props.type === 'deg' ? 1 : 100
const { max, min } = props.design.config.options[props.option]
const dflt = props.design.config.options[props.option][props.type || 'pct']
const val = (typeof props.gist?.options?.[props.option] === 'undefined')
? dflt
: props.gist.options[props.option] * factor
const [value, setValue] = useState(val)
const [editOption, setEditOption] = useState(false)
const handleChange = (evt) => {
const newVal = evt.target.value
setValue(newVal)
props.updateGist(['options', props.option], newVal/factor)
}
const reset = () => {
setValue(dflt)
props.unsetGist(['options', props.option])
}
return (
<div className="py-4 mx-6 border-l-2 pl-2">
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
{props.ot(`${props.option}.d`)}
</p>
<div className="flex flex-row justify-between">
{editOption
? <EditOption
value={value}
handleChange={handleChange}
min={min}
max={max}
setEditOption={setEditOption}
t={t}
suffix={suffix}
/>
: (
<>
<span className="opacity-50">{round(min)}{suffix}</span>
<span className={
`font-bold ${val===dflt ? 'text-secondary' : 'text-accent'}`}
>
{round(val)}{suffix}
</span>
<span className="opacity-50">{round(max)}{suffix}</span>
</>
)
}
</div>
<input
type="range"
max={max}
min={min}
step={0.1}
value={value}
onChange={handleChange}
className={`
range range-sm mt-1
${val === dflt ? 'range-secondary' : 'range-accent'}
`}
/>
<div className="flex flex-row justify-between">
<span className={val===dflt ? 'text-secondary' : 'text-accent'}>
{props.design.config.options[props.option]?.toAbs
? formatMm(props.design.config.options[props.option].toAbs(value/100, props.gist))
: ' '
}
</span>
<div>
<button
title={t('reset')}
className="btn btn-ghost btn-xs text-accent"
disabled={val === dflt}
onClick={reset}
>
<ClearIcon />
</button>
<button
title={t('editThing', { thing: suffix })}
className={`
btn btn-ghost btn-xs hover:text-secondary-focus
${editOption
? 'text-accent'
: 'text-secondary'
}
`}
onClick={() => setEditOption(!editOption)}
>
<EditIcon />
</button>
</div>
</div>
</div>
)
}
export default DesignOptionPctDeg

View file

@ -0,0 +1,122 @@
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'
import { useTranslation } from 'next-i18next'
import { isDegreeMeasurement } from '../../../config/measurements'
import measurementAsMm from 'pkgs/utils/src/measurementAsMm'
import formatMm from 'pkgs/utils/src/formatMm'
/*
* This is a single input for a measurements
* Note that it keeps local state with whatever the user types
* but will only trigger a gist update if the input is valid.
*
* m holds the measurement name. It's just so long to type
* measurement and I always have some typo in it because dyslexia.
*/
const MeasurementInput = ({ m, gist, app, updateMeasurements }) => {
const { t } = useTranslation(['app', 'measurements'])
const prefix = (app.site === 'org') ? '' : 'https://freesewing.org'
const title = t(`measurements:${m}`)
const isDegree = isDegreeMeasurement(m);
const factor = useMemo(() => (isDegree ? 1 : (gist.units == 'imperial' ? 25.4 : 10)), [gist.units])
const isValValid = val => (typeof val === 'undefined' || val === '')
? null
: val !== false && !isNaN(val)
const isValid = (newVal) => (typeof newVal === 'undefined')
? isValValid(val)
: isValValid(newVal)
const [val, setVal] = useState(gist?.measurements?.[m] / factor || '')
// keep a single reference to a debounce timer
const debounceTimeout = useRef(null);
// onChange
const update = useCallback((evt) => {
evt.stopPropagation();
let evtVal = evt.target.value;
// set Val immediately so that the input reflects it
setVal(evtVal)
let useVal = isDegree ? evtVal : measurementAsMm(evtVal, gist.units);
const ok = isValid(useVal)
// only set to the gist if it's valid
if (ok) {
// debounce in case it's still changing
if (debounceTimeout.current !== null) { clearTimeout(debounceTimeout.current); }
debounceTimeout.current = setTimeout(() => {
// clear the timeout reference
debounceTimeout.current = null;
updateMeasurements(useVal, m)
}, 500);
}
}, [gist.units])
// use this for better update efficiency
const memoVal = useMemo(() => gist?.measurements[m], [gist])
// track validity against the value and the units
const valid = useMemo(() => isValid(isDegree ? val : measurementAsMm(val, gist.units)), [val, gist.units])
// hook to update the value or format when the gist changes
useEffect(() => {
// set the value to the proper value and format
if (memoVal) {
let gistVal = +(memoVal / factor).toFixed(2);
setVal(gistVal)
}
}, [memoVal, factor])
// cleanup
useEffect(() => clearTimeout(debounceTimeout.current), [])
if (!m) return null
return (
<div className="form-control mb-2" key={`wrap-${m}`}>
<label className="label">
<span className="label-text font-bold text-xl">{title}</span>
<a
href={`${prefix}/docs/measurements/${m.toLowerCase()}`}
className="label-text-alt text-secondary hover:text-secondary-focus hover:underline"
title={`${t('docs')}: ${t(m)}`}
tabIndex="-1"
>
{t('docs')}
</a>
</label>
<label className="input-group input-group-lg">
<input
key={`input-${m}`}
type="text"
placeholder={title}
className={`
input input-lg input-bordered grow text-base-content border-r-0
${valid === false && 'input-error'}
${valid === true && 'input-success'}
`}
value={val}
onChange={update}
/>
<span role="img" className={`bg-transparent border-y
${valid === false && 'border-error text-neutral-content'}
${valid === true && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'}
`}>
{(valid === true) && '👍'}
{(valid === false) && '🤔'}
</span>
<span className={`
${valid === false && 'bg-error text-neutral-content'}
${valid === true && 'bg-success text-neutral-content'}
${valid === null && 'bg-base-200 text-base-content'}
`}>
{isDegree ? '° ' : gist.units == 'metric' ? 'cm' : 'in'}
</span>
</label>
</div>
)
}
export default MeasurementInput

View file

@ -0,0 +1,9 @@
import Json from 'shared/components/json.js'
const GistAsJson = props => (
<div className="max-w-screen-xl m-auto">
<Json>{JSON.stringify(props.gist, null, 2)}</Json>
</div>
)
export default GistAsJson

View file

@ -0,0 +1,21 @@
import { useTranslation } from 'next-i18next'
import Settings from './settings'
const CutLayout = props => {
const { t } = useTranslation(['workbench'])
return (
<div>
<h2 className="capitalize">
{
t('layoutThing', { thing: props.design.config.name })
+ ': '
+ t('forCutting')
}
</h2>
<Settings {...props} />
</div>
)
}
export default CutLayout

View file

@ -0,0 +1,10 @@
const CutLayoutSettings = props => {
return (
<div>
<p>Fixme: Cut layout settings here</p>
</div>
)
}
export default CutLayoutSettings

View file

@ -0,0 +1,415 @@
/*
* This React component is a long way from perfect, but it's a start for
* handling custom layouts.
*
* There are two reasons that (at least in my opinion) implementing this is non-trivial:
*
* 1) React re-render vs DOM updates
*
* For performance reasons, we can't re-render with React when the user drags a
* pattern part (or rotates it). It would kill performance.
* So, we don't re-render with React upon dragging/rotating, but instead manipulate
* the DOM directly.
*
* So far so good, but of course we don't want a pattern that's only correctly laid
* out in the DOM. We want to updat the pattern gist so that the new layout is stored.
* For this, we re-render with React on the end of the drag (or rotate).
*
* Handling this balance between DOM updates and React re-renders is a first contributing
* factor to why this component is non-trivial
*
* 2) SVG vs DOM coordinates
*
* When we drag or rotate with the mouse, all the events are giving us coordinates of
* where the mouse is in the DOM.
*
* The layout uses coordinates from the embedded SVG which are completely different.
* So we need to make this translation and that adds complexity.
*
* 3) Part-level transforms
*
* It's not just that the DOM coordinates and SVG coordinate system is different, each
* part also has it's own transforms applied and because of this behaves as if they have
* their own coordinate system.
*
* In other words, a point (0,0) in the part is not the top-left corner of the page.
* In the best-case scenario, it's the top-left corner of the part. But even this is
* often not the case as parts will have transforms applied to them.
*
* 4) Flip along X or Y axis
*
* Parts can be flipped along the X or Y axis to facilitate a custom layout.
* This is handled in a transform, so the part's coordinate's don't actually change. They
* are flipped late into the rendering process (by the browser displaying the SVG).
*
* Handling this adds yet more mental overhead
*
* 5) Bounding box
*
* While moving and rotating parts around is one thing. Recalculating the bounding box
* (think auto-cropping the pattern) gets kinda complicated because of the reasons
* outlined above.
*
* We are currently handling a lot in the frontend code. It might be more elegant to move
* some of this to core. For example, core expects the custom layout to set the widht and height
* rather than figuring it out on its own as it does for auto-generated layouts.
*
*
*
* Known issues
*
* - Rotating gets a little weird sometimes as the part rotates around it's center in the
* SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
* used to calculate the angle of the rotation
*
* - Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
*
* - Rotation gets weird when a part is mirrored
*
* - The bounding box update when a part is rotated is not entirely accurate
*
*
* I've sort of left it at this because I'm starting to wonder if we should perhaps re-think
* how custom layouts are supported in the core. And I would like to discuss this with the core team.
*/
import { useEffect, useRef, useState } from 'react'
import Svg from '../draft/svg'
import Defs from '../draft/defs'
import Path from '../draft/path'
import Point from '../draft/point'
import Snippet from '../draft/snippet'
import { getProps } from '../draft/utils'
import { drag } from 'd3-drag'
import { select } from 'd3-selection'
const Buttons = ({ transform, flip, rotate, setRotate, resetPart }) => {
const letter = 'F'
const style = { style: {fill: 'white', fontSize: 18, fontWeight: 'bold', textAnchor: 'middle'} }
return (
<g transform={transform}>
{rotate
? <circle cx="0" cy="0" r="50" className='stroke-2xl muted' />
: <path d="M -50, 0 l 100,0 M 0,-50 l 0,100" className="stroke-2xl muted" />
}
<g className="svg-layout-button" onClick={resetPart}>
<rect x="-10" y="-10" width="20" height="20" />
<text x="0" y="10" {...style}>{letter}</text>
</g>
<g className="svg-layout-button" onClick={() => flip('y')}>
<rect x="10" y="-10" width="20" height="20" className="button" />
<text x="20" y="10" {...style} transform="scale(1,-1)">{letter}</text>
</g>
<g className="svg-layout-button" onClick={() => flip('x')}>
<rect x="-30" y="-10" width="20" height="20" className="button" />
<text x="20" y="10" {...style} transform="scale(-1,1)">{letter}</text>
</g>
</g>
)
}
const dx = (pointA, pointB) => pointB.x - pointA.x
const dy = (pointA, pointB) => pointB.y - pointA.y
const rad2deg = radians => radians * 57.29577951308232
const angle = (pointA, pointB) => {
let rad = Math.atan2(-1 * dy(pointA, pointB), dx(pointA, pointB))
while (rad < 0) rad += 2 * Math.PI
return rad2deg(rad)
}
const generateTransform = (x, y, rot, flipX, flipY, part) => {
const anchor = {
x: 0,
y: 0
}
const center = {
x: part.topLeft.x + (part.bottomRight.x - part.topLeft.x)/2,
y: part.topLeft.y + (part.bottomRight.y - part.topLeft.y)/2,
}
const dx = part.topLeft.x - center.x
const dy = part.topLeft.y - center.y
const transforms = [`translate(${x},${y})`]
if (flipX) transforms.push(
`translate(${center.x * -1}, ${center.y * -1})`,
'scale(-1, 1)',
`translate(${center.x * -1 + 2 * dx}, ${center.y})`
)
if (flipY) transforms.push(
`translate(${center.x * -1}, ${center.y * -1})`,
'scale(1, -1)',
`translate(${center.x}, ${center.y * -1 + 2 * dy})`,
)
if (rot) transforms.push(
`rotate(${rot}, ${center.x - anchor.x}, ${center.y - anchor.y})`
)
return transforms.join(' ')
}
const Part = props => {
const { layout, gist, name, part } = props
const partLayout= layout.parts[name]
// Don't just assume this makes sense
if (typeof layout.parts?.[name]?.move?.x === 'undefined') return null
// Use a ref for direct DOM manipulation
const partRef = useRef(null)
const centerRef = useRef(null)
// State variable to switch between moving or rotating the part
const [rotate, setRotate] = useState(false)
// Initialize drag handler
useEffect(() => {
handleDrag(select(partRef.current))
}, [rotate])
// These are kept as vars because re-rendering on drag would kill performance
// Managing the difference between re-render and direct DOM updates makes this
// whole thing a bit tricky to wrap your head around
let translateX = partLayout.move.x
let translateY = partLayout.move.y
let rotation = partLayout.rotate || 0
let flipX = partLayout.flipX ? true : false
let flipY = partLayout.flipY ? true : false
let partRect
const center = {
x: part.topLeft.x + (part.bottomRight.x - part.topLeft.x)/2,
y: part.topLeft.y + (part.bottomRight.y - part.topLeft.y)/2,
}
const handleDrag = drag()
.subject(function() {
return { x: translateX, y: translateY }
})
.on('start', function(event) {
partRect = partRef.current.getBoundingClientRect()
})
.on('drag', function(event) {
if (rotate) rotation = angle(partRect, { x:event.x, y: event.y }) * -1
else {
translateX = event.x
translateY = event.y
}
const me = select(this);
me.attr('transform', generateTransform(translateX, translateY, rotation, flipX, flipY, part))
})
.on('end', function(event) {
updateLayout()
})
const resetPart = () => {
rotation = 0
flipX = 0
flipY = 0
updateLayout()
}
const toggleDragRotate = () => {
updateLayout()
setRotate(!rotate)
}
const updateLayout = () => {
props.updateLayout(name, {
move: {
x: translateX,
y: translateY,
},
rotate: rotation,
flipX,
flipY
})
}
// Method to flip (mirror) the part along the X or Y axis
const flip = axis => {
if (axis === 'x') flipX = !flipX
else flipY = !flipY
updateLayout()
}
const grid = gist.paperless ? (
<rect
x={part.topLeft.x}
y={part.topLeft.y}
width={part.width}
height={part.height}
className="grid"
fill={'url(#grid-' + eame + ')'}
/>
) : null
return (
<g
{...getProps(part)}
id={`part-${name}`}
ref={props.name === 'pages' ? null : partRef}
onClick={toggleDragRotate}
>
{grid}
{
props.gist?._state?.xray?.enabled &&
props.gist?._state?.xray?.reveal?.[name]
&& <XrayPart {...props} />
}
{Object.keys(part.paths).map((pathName) => (
<Path
key={pathName}
pathName={pathName}
path={part.paths[pathName]}
topLeft={props.part.topLeft}
bottomRight={props.part.bottomRight}
{...props}
/>
))}
{Object.keys(props.part.points).map((pointName) => (
<Point
key={pointName}
pointName={pointName}
point={props.part.points[pointName]}
topLeft={props.part.topLeft}
bottomRight={props.part.bottomRight}
{...props}
/>
))}
{Object.keys(props.part.snippets).map((snippetName) => (
<Snippet
key={snippetName}
snippetName={snippetName}
snippet={props.part.snippets[snippetName]}
{...props}
/>
))}
<text x={center.x} y={center.y} ref={centerRef} />
<rect
x={part.topLeft.x}
y={part.topLeft.y}
width={part.width}
height={part.height}
className={`layout-rect ${rotate ? 'rotate' : 'move'}`}
/>
{props.name !== 'pages' && <Buttons
transform={`translate(${part.topLeft.x + part.width/2}, ${part.topLeft.y + part.height/2})`}
flip={flip}
rotate={rotate}
setRotate={setRotate}
resetPart={resetPart}
/>}
</g>
)
}
const Draft = props => {
const { patternProps, gist, updateGist ,app, bgProps={} } = props
const { layout=false } = gist
useEffect(() => {
if (!layout) {
// On the initial draft, core does the layout, so we set the layout to the auto-layout
// After this, core won't handle layout anymore. It's up to the user from this point onwards
// FIXME: Allow use the option to clear the layout again
updateGist(['layout'], {
...patternProps.autoLayout,
width: patternProps.width,
height: patternProps.height
})
}
}, [layout])
if (!patternProps || !layout) return null
// Helper method to update part layout and re-calculate width * height
const updateLayout = (name, config) => {
// Start creating new layout
const newLayout = {...layout}
newLayout.parts[name] = config
newLayout.width = layout.width
newLayout.height = layout.height
// Pattern topLeft and bottomRight
let topLeft = { x: 0, y: 0 }
let bottomRight = { x: 0, y: 0 }
for (const [pname, part] of Object.entries(patternProps.parts)) {
// Pages part does not have its topLeft and bottomRight set by core since it's added post-draft
if (part.topLeft) {
// Find topLeft (tl) and bottomRight (br) of this part
const tl = {
x: part.topLeft.x + newLayout.parts[pname].move.x,
y: part.topLeft.y + newLayout.parts[pname].move.y
}
const br = {
x: part.bottomRight.x + newLayout.parts[pname].move.x,
y: part.bottomRight.y + newLayout.parts[pname].move.y
}
// Handle rotate
if (newLayout.parts[pname].rotate) {
// Angle to the corners
const center = {
x: part.topLeft.x + part.width/2,
y: part.topLeft.x + part.height/2,
}
const corners = {
tl: part.topLeft,
br: part.bottomRight,
}
const angles = {}
for (const corner in corners) angles[corner] = angle(center, corners[corner])
const delta = {}
const rotation = newLayout.parts[pname].rotate
for (const corner in corners) {
delta[corner] = {
x: part.width/2 * (Math.cos(angles[corner]) - Math.cos(angles[corner] + rotation)),
y: part.height/2 * (Math.sin(angles[corner]) - Math.sin(angles[corner] + rotation))
}
}
if (delta.br.x > 0) br.x += delta.br.x
if (delta.br.y > 0) br.y += delta.br.y
if (delta.tl.x < 0) tl.x -= delta.tl.x
if (delta.tl.y < 0) tl.y -= delta.tl.y
}
if (tl.x < topLeft.x) topLeft.x = tl.x
if (tl.y < topLeft.y) topLeft.y = tl.y
if (br.x > bottomRight.x) bottomRight.x = br.x
if (br.y > bottomRight.y) bottomRight.y = br.y
}
}
newLayout.width = bottomRight.x - topLeft.x
newLayout.height = bottomRight.y - topLeft.y
updateGist(['layout'], newLayout)
}
// We need to make sure the `pages` part is at the bottom of the pile
// so we can drag-drop all parts on top of it.
// Bottom in SVG means we need to draw it first
const partList = Object.keys(patternProps.parts)
return (
<div className="my-8 w-11/12 m-auto border-2 border-dotted border-base-content shadow">
<Svg {...patternProps} embed={gist.embed}>
<Defs {...patternProps} />
<style>{`:root { --pattern-scale: ${gist.scale || 1}}`}</style>
<g>
<rect x="0" y="0" width={patternProps.width} height={patternProps.height} {...bgProps} />
{[
partList.filter(name => name === 'pages'),
partList.filter(name => name !== 'pages'),
].map(list => list.map(name => (
<Part {...{
key:name,
name,
part: patternProps.parts[name],
layout,
app,
gist,
updateLayout,
}}/>
)))}
</g>
</Svg>
</div>
)
}
export default Draft

View file

@ -0,0 +1,54 @@
import { useEffect } from 'react'
import { useTranslation } from 'next-i18next'
import Settings from './settings'
import Draft from '../draft'
import pluginBuilder from './plugin'
const PrintLayout = props => {
useEffect(() => {
if (props.gist?._state?.xray?.enabled) props.updateGist(
['_state', 'xray', 'enabled'],
false
)
}, [])
const { t } = useTranslation(['workbench'])
const draft = new props.design(props.gist).use(pluginBuilder(
props.gist?._state?.layout?.forPrinting?.page?.size,
props.gist?._state?.layout?.forPrinting?.page?.orientation,
))
let patternProps
try {
draft.draft()
patternProps = draft.getRenderProps()
} catch(err) {
console.log(err, props.gist)
}
const bgProps = { fill: "url(#page)" }
return (
<div>
<h2 className="capitalize">
{
t('layoutThing', { thing: props.design.config.name })
+ ': '
+ t('forPrinting')
}
</h2>
<div className="m-4">
<Settings {...props} draft={draft}/>
</div>
<Draft
draft={draft}
gist={props.gist}
updateGist={props.updateGist}
patternProps={patternProps}
bgProps={bgProps}
/>
</div>
)
}
export default PrintLayout

View file

@ -0,0 +1,29 @@
import PageIcon from 'shared/components/icons/page'
import { useTranslation } from 'next-i18next'
const PageOrientationPicker = ({ gist, updateGist }) => {
const { t } = useTranslation(['workbench'])
return (
<button className={`
btn btn-primary flex flex-row gap-2 items-center
hover:text-primary-content
`}
onClick={() => updateGist(
['_state', 'layout', 'forPrinting', 'page', 'orientation'],
gist._state?.layout?.forPrinting?.page?.orientation === 'portrait'
? 'landscape'
: 'portrait'
)}
>
<span className={
gist._state?.layout?.forPrinting?.page?.orientation === 'landscape'
? 'rotate-90'
: ''
}><PageIcon /></span>
<span>{t(`pageOrientation`)}</span>
</button>
)
}
export default PageOrientationPicker

View file

@ -0,0 +1,76 @@
import PageSizeIcon from 'shared/components/icons/page-size'
import { useTranslation } from 'next-i18next'
import Popout from 'shared/components/popout'
const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid']
const PageSizePicker = ({ gist, updateGist }) => {
const { t } = useTranslation(['workbench'])
const setSize = size => {
updateGist(
['_state', 'layout', 'forPrinting', 'page', 'size'],
size
)
if (!gist._state?.layout?.forPrinting?.page?.orientation) {
updateGist(
['_state', 'layout', 'forPrinting', 'page', 'orientation'],
'portrait'
)
}
}
if (
!gist._state?.layout?.forPrinting?.page?.size ||
sizes.indexOf(gist._state.layout.forPrinting.page.size) === -1
) return (
<Popout tip>
<h3>{t('startBySelectingAThing', { thing: t('pageSize')})}</h3>
<div className="flex flex-row gap-4">
{sizes.map(size => (
<button
key={size}
onClick={() => setSize(size)}
className="btn btn-primary"
>
<span className="capitalize">
{size}
</span>
</button>
))}
</div>
</Popout>
)
return (
<div className={`dropdown`}>
<div tabIndex="0" className={`
m-0 btn btn-primary flex flex-row gap-2
hover:text-primary-content
`}>
<PageSizeIcon />
<span>{t(`pageSize`)}:</span>
<span className="ml-2 font-bold">{gist._state.layout.forPrinting.page.size}</span>
</div>
<ul
tabIndex="0"
className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52"
>
{sizes.map(size => (
<li key={size}>
<button
onClick={() => setSize(size)}
className="btn btn-ghost hover:bg-base-200"
>
<span className="text-base-content capitalize">
{size}
</span>
</button>
</li>
))}
</ul>
</div>
)
}
export default PageSizePicker

View file

@ -0,0 +1,77 @@
const name = 'Pages Plugin'
const version = '1.0.0'
const sizes = {
a4: [ 210, 297 ],
a3: [ 297, 420 ],
a2: [ 420, 594 ],
a1: [ 594, 841 ],
a0: [ 841, 1188 ],
letter: [ 215.9, 279.4 ],
tabloid: [ 279.4, 431.8 ],
}
const pagesPlugin = (size='a4', orientation='portrait') => ({
name,
version,
hooks: {
postLayout: function(pattern) {
// Add part
pattern.parts.pages = pattern.Part('pages')
// Keep part out of layout
pattern.parts.pages.layout = false
// But add the part to the autoLayout property
pattern.autoLayout.parts.pages = {
move: { x: 0, y: 0 }
}
// Add pages
const { macro } = pattern.parts.pages.shorthand()
const { height, width } = pattern
macro('addPages', { size, orientation, height, width })
}
},
macros: {
addPages: function(so) {
const ls = so.orientation === 'landscape'
const w = sizes[so.size][ls ? 1 : 0]
const h = sizes[so.size][ls ? 0 : 1]
const cols = Math.ceil(so.width / w)
const rows = Math.ceil(so.height / h)
const { points, Point, paths, Path } = this.shorthand()
let x = 0
let y = 0
let count = 0
for (let row=0;row<rows;row++) {
x=0
for (let col=0;col<cols;col++) {
count++
points[`_pages__row${row}-col${col}-tl`] = new Point(x,y)
points[`_pages__row${row}-col${col}-tr`] = new Point(x+w,y)
points[`_pages__row${row}-col${col}-br`] = new Point(x+w,y+h)
points[`_pages__row${row}-col${col}-bl`] = new Point(x,y+h)
points[`_pages__row${row}-col${col}-circle`] = new Point(x+w/2,y+h/2-24)
.attr('data-circle', 42)
.attr('data-circle-class', 'stroke-4xl muted fabric')
points[`_pages__row${row}-col${col}-text`] = new Point(x+w/2,y+h/2)
.attr('data-text', `${count}`)
.attr('data-text-class', 'text-4xl center bold muted fill-fabric')
paths[`_pages__row${row}-col${col}`] = new Path()
.move(points[`_pages__row${row}-col${col}-tl`])
.line(points[`_pages__row${row}-col${col}-bl`])
.line(points[`_pages__row${row}-col${col}-br`])
.line(points[`_pages__row${row}-col${col}-tr`])
.close()
.attr('class', 'fill-fabric')
.attr('style', `stroke-opacity: 0; fill-opacity: ${(col+row)%2===0 ? 0.03 : 0.09};`)
x += w
}
y += h
}
// Store page count in part
this.pages = { cols, rows, count: cols*rows }
}
}
})
export default pagesPlugin

View file

@ -0,0 +1,28 @@
import PageSizePicker from './pagesize-picker'
import OrientationPicker from './orientation-picker'
import PrintIcon from 'shared/components/icons/print'
import RightIcon from 'shared/components/icons/right'
const PrintLayoutSettings = props => {
if (!props.draft?.parts?.pages?.pages) return null
const { cols, rows, count } = props.draft.parts.pages.pages
return (
<div className="flex flex-row gap-8 justify-center">
<PageSizePicker {...props} />
<OrientationPicker {...props} />
<div className="flex flex-row font-bold items-center px-0 text-xl">
<PrintIcon />
<span className="ml-2">{count}</span>
<span className="mx-6 opacity-50">|</span>
<RightIcon />
<span className="ml-2">{cols}</span>
<span className="mx-6 opacity-50">|</span>
<div className="rotate-90"><RightIcon /></div>
<span className="text-xl ml-2">{rows}</span>
</div>
</div>
)
}
export default PrintLayoutSettings

View file

@ -0,0 +1,118 @@
import React from 'react'
import MeasurementInput from '../inputs/measurement.js'
import { withBreasts, withoutBreasts } from 'pkgs/models/src/index.js'
import nonHuman from './non-human.js'
import WithBreastsIcon from 'shared/components/icons/with-breasts.js'
import WithoutBreastsIcon from 'shared/components/icons/without-breasts.js'
import { useTranslation } from 'next-i18next'
import Setting from '../menu/core-settings/setting';
import {settings} from '../menu/core-settings/index';
const groups = {
people: {
with: withBreasts,
without: withoutBreasts,
},
dolls: {
with: nonHuman.withBreasts.dolls,
without: nonHuman.withoutBreasts.dolls,
},
giants: {
with: nonHuman.withBreasts.giants,
without: nonHuman.withoutBreasts.giants,
}
}
const icons = {
with: <WithBreastsIcon />,
without: <WithoutBreastsIcon />,
}
const WorkbenchMeasurements = ({ app, design, gist, updateGist }) => {
const { t } = useTranslation(['app', 'cfp'])
// Method to handle measurement updates
const updateMeasurements = (value, m=false) => {
if (m === false) {
// Set all measurements
updateGist('measurements', value)
} else {
// Set one measurement
const newValues = {...gist.measurements}
newValues[m] = value
updateGist('measurements', newValues)
}
}
// Save us some typing
const inputProps = { app, updateMeasurements, gist }
return (
<div className="m-auto max-w-2xl">
<h1>
<span className='capitalize mr-4 opacity-70'>
{design.config.name}:
</span> {t('measurements')}
</h1>
<details open className="my-2">
<summary><h2 className="inline pl-1">{t('cfp:preloadMeasurements')}</h2></summary>
<div className="ml-2 pl-4 border-l-2">
{Object.keys(groups).map(group => (
<details key={group}>
<summary><h3 className="inline pl-1">{t(group)}</h3></summary>
<div className="ml-2 pl-4 border-l-2">
{Object.keys(icons).map(type => (
<React.Fragment key={type}>
<h4>{t(`${type}Breasts`)}</h4>
<ul className="flex flex-row flex-wrap gap-2">
{Object.keys(groups[group][type]).map((m) => (
<li key={`${m}-${type}-${group}`} className="">
<button
className="flex flex-row btn btn-outline"
onClick={() => updateMeasurements(groups[group][type][m], false)}
>
{icons[type]}
{t('size')}&nbsp;
{ group === 'people'
? m.replace('size', '')
: m
}
</button>
</li>
))}
</ul>
</React.Fragment>
))}
</div>
</details>
))}
</div>
</details>
<details className="my-2">
<summary><h2 className="inline pl-2">{t('cfp:enterMeasurements')}</h2></summary>
<Setting key={'units'} setting={'units'} config={settings.units} updateGist={updateGist} {...inputProps} />
<div className="ml-2 pl-4 border-l-2">
{design.config.measurements && (
<>
<h3>{t('requiredMeasurements')}</h3>
{design.config.measurements.map(m => (
<MeasurementInput key={m} m={m} {...inputProps} />
))}
</>
)}
{design.config.optionalMeasurements && (
<>
<h3>{t('optionalMeasurements')}</h3>
{design.config.optionalMeasurements.map(m => (
<MeasurementInput key={m} m={m} {...inputProps} />
))}
</>
)}
</div>
</details>
</div>
)
}
export default WorkbenchMeasurements

View file

@ -0,0 +1,51 @@
import { withBreasts, withoutBreasts } from '@freesewing/models'
const nonHuman = {
withoutBreasts: {
dolls: {},
giants: {}
},
withBreasts: {
dolls: {},
giants: {}
}
}
const round = val => Math.round(val*10)/10
for (let i=0.1;i<0.7;i+=0.1) {
const name = `${Math.round(i*10)}/10`
nonHuman.withBreasts.dolls[name] = {}
// withBreasts: Based on Anneke (size 34)
for (const [m, val] of Object.entries(withBreasts.size34)) {
nonHuman.withBreasts.dolls[name][m] = (m === 'shoulderSlope')
? val
: round(val * i)
}
// withoutBreasts: Based on Ronan (size 42)
nonHuman.withoutBreasts.dolls[name] = {}
for (const [m, val] of Object.entries(withoutBreasts.size42)) {
nonHuman.withoutBreasts.dolls[name][m] = (m === 'shoulderSlope')
? val
: round(val * i)
}
}
for (let i=1.5;i<=3;i+=0.5) {
const name = `${i}/1`
nonHuman.withBreasts.giants[name] = {}
// withBreasts: Based on Anneke (size 34)
for (const [m, val] of Object.entries(withBreasts.size34)) {
nonHuman.withBreasts.giants[name][m] = (m === 'shoulderSlope')
? val
: round(val * i)
}
nonHuman.withoutBreasts.giants[name] = {}
// withoutBreasts: Based on Ronan (size 42)
for (const [m, val] of Object.entries(withoutBreasts.size42)) {
nonHuman.withoutBreasts.giants[name][m] = (m === 'shoulderSlope')
? val
: round(val * i)
}
}
export default nonHuman

View file

@ -0,0 +1,28 @@
import { useState } from 'react'
import { SecText, SumButton, Li, SumDiv, Deg } from 'shared/components/workbench/menu/index.js'
import { useTranslation } from 'next-i18next'
const CoreSettingBool = props => {
const { t } = useTranslation(['app'])
const [value, setValue] = useState(props.gist[props.setting])
const toggle = (evt) => {
props.updateGist([props.setting], !value)
setValue(!value)
}
return (
<Li>
<SumButton onClick={toggle}>
<SumDiv>
<Deg />
<span>{ t(`settings:${props.setting}.t`) }</span>
</SumDiv>
<SecText>{ t(value ? 'yes' : 'no')}</SecText>
</SumButton>
</Li>
)
}
export default CoreSettingBool

View file

@ -0,0 +1,51 @@
import { useState } from 'react'
import { Deg } from 'shared/components/workbench/menu/index.js'
import { useTranslation } from 'next-i18next'
const CoreSettingList = props => {
const { t } = useTranslation(['settings'])
const { dflt, list } = props
const val = props.gist?.[props.setting]
const [value, setValue] = useState(val)
const handleChange = (newVal) => {
if (newVal === dflt) reset()
else {
setValue(newVal)
props.updateGist([props.setting], newVal)
}
}
const reset = () => {
setValue(props.dflt)
props.updateGist([props.setting], props.dflt)
}
return (
<div className="py-4 mx-6 border-l-2 pl-2">
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
{t(`settings:${props.setting}.d`)}
</p>
<div className="flex flex-row">
<div className="grow">
{props.list.map(entry => (
<button
key={entry.key}
onClick={() => handleChange(entry.key)}
className={`
mr-1 mb-1 text-left text-lg w-full hover:text-secondary-focus px-2
${entry.key === value && 'font-bold text-secondary'}
`}
>
<Deg />
{entry.title}
</button>
))}
</div>
</div>
</div>
)
}
export default CoreSettingList

View file

@ -0,0 +1,74 @@
import { useState } from 'react'
import { formatMm } from 'shared/utils'
import ClearIcon from 'shared/components/icons/clear'
import { useTranslation } from 'next-i18next'
const CoreSettingMm = props => {
const { t } = useTranslation(['app', 'settings'])
const { dflt, min, max } = props
const val = props.gist?.[props.setting]
const [value, setValue] = useState(val)
const handleChange = evt => {
const newVal = parseFloat(evt.target.value)
if (newVal === dflt) reset()
else {
setValue(newVal)
props.updateGist([props.setting], newVal)
}
}
const reset = () => {
setValue(props.dflt)
props.updateGist([props.setting], props.dflt)
}
return (
<div className="py-4 mx-6 border-l-2 pl-2">
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
{t(`settings:${props.setting}.d`)}
</p>
<div className="flex flex-row justify-between">
<span
className="opacity-50"
dangerouslySetInnerHTML={{__html: formatMm(min, props.gist.units)}}
/>
<span
className={`font-bold ${val===dflt ? 'text-secondary-focus' : 'text-accent'}`}
dangerouslySetInnerHTML={{__html: formatMm(val, props.gist.units)}}
/>
<span
className="opacity-50"
dangerouslySetInnerHTML={{__html: formatMm(max, props.gist.units)}}
/>
</div>
<input
type="range"
max={max}
min={min}
step={0.1}
value={value}
onChange={handleChange}
className={`
range range-sm mt-1
${val === dflt ? 'range-secondary' : 'range-accent'}
`}
/>
<div className="flex flex-row justify-between">
<span />
<button
title={t('reset')}
className="btn btn-ghost btn-xs text-accent"
disabled={val === dflt}
onClick={reset}
>
<ClearIcon />
</button>
</div>
</div>
)
}
export default CoreSettingMm

View file

@ -0,0 +1,71 @@
import { useState } from 'react'
import ClearIcon from 'shared/components/icons/clear.js'
import EditIcon from 'shared/components/icons/edit.js'
import { useTranslation } from 'next-i18next'
const CoreSettingNr = props => {
const { t } = useTranslation(['app', 'settings'])
const { dflt, min, max } = props
const val = props.gist?.[props.setting]
const [value, setValue] = useState(val)
const handleChange = evt => {
const newVal = parseFloat(evt.target.value)
if (newVal === dflt) reset()
else {
setValue(newVal)
props.updateGist([props.setting], newVal)
}
}
const reset = () => {
setValue(props.dflt)
props.updateGist([props.setting], props.dflt)
}
return (
<div className="py-4 mx-6 border-l-2 pl-2">
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
{t(`settings:${props.setting}.d`)}
</p>
<div className="flex flex-row justify-between">
<span className="opacity-50">
{min}
</span>
<span className={`font-bold ${val===dflt ? 'text-secondary-focus' : 'text-accent'}`}>
{val}
</span>
<span className="opacity-50">
{max}
</span>
</div>
<input
type="range"
max={max}
min={min}
step={0.1}
value={value}
onChange={handleChange}
className={`
range range-sm mt-1
${val === dflt ? 'range-secondary' : 'range-accent'}
`}
/>
<div className="flex flex-row justify-between">
<span />
<button
title={t('reset')}
className="btn btn-ghost btn-xs text-accent"
disabled={val === dflt}
onClick={reset}
>
<ClearIcon />
</button>
</div>
</div>
)
}
export default CoreSettingNr

View file

@ -0,0 +1,66 @@
import ClearIcon from 'shared/components/icons/clear.js'
import orderBy from 'lodash.orderby'
import { useTranslation } from 'next-i18next'
const CoreSettingOnly = props => {
const { t } = useTranslation(['app', 'parts', 'settings'])
const list = props.design.config.draftOrder
const partNames = list.map(part => ({ id: part, name: t(`parts:${part}`) }))
const togglePart = part => {
const parts = props.gist.only || []
const newParts = new Set(parts)
if (newParts.has(part)) newParts.delete(part)
else newParts.add(part)
if (newParts.size < 1) reset()
else props.updateGist(['only'], [...newParts])
}
const reset = () => {
props.unsetGist(['only'])
}
return (
<div className="py-4 mx-6 border-l-2 pl-2">
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
{t(`settings:only.d`)}
</p>
<div className="flex flex-row">
<div className="grow">
{orderBy(partNames, ['name'], ['asc']).map(part => (
<button
key={part.id}
onClick={() => togglePart(part.id)}
className={`
mr-1 mb-1 text-left text-lg w-full hover:text-secondary-focus px-2
${props.gist?.only && props.gist.only.indexOf(part.id) !== -1 && 'font-bold text-secondary-focus'}
`}
>
<span className={`
text-3xl mr-2 inline-block p-0 leading-3
translate-y-3
`}>
<>&deg;</>
</span>
{part.name}
</button>
))}
</div>
</div>
<div className="flex flex-row-reverse">
<button
title={t('reset')}
className="btn btn-ghost btn-xs text-accent"
disabled={!props.gist.only || props.gist.only.length < 1}
onClick={reset}
>
<ClearIcon />
</button>
</div>
</div>
)
}
export default CoreSettingOnly

View file

@ -0,0 +1,36 @@
import { useState } from 'react'
import { SecText, SumButton, Li, SumDiv, Deg } from 'shared/components/workbench/menu/index.js'
import { useTranslation } from 'next-i18next'
const CoreSettingSaBool = props => {
const { t } = useTranslation(['app', 'settings'])
const [value, setValue] = useState(props.gist.saBool || false)
const toggle = () => {
props.setGist({
...props.gist,
saBool: !value,
sa: value ? 0 : props.gist.saMm
})
setValue(!value)
}
return (
<Li>
<SumButton onClick={toggle}>
<SumDiv>
<Deg />
<span>{ t('settings:sa.t') }</span>
<span className="ml-4 opacity-50">
[ { t(`yes`) }/
{ t(`no`) } ]
</span>
</SumDiv>
<SecText>{t(value ? 'yes' : 'no')}</SecText>
</SumButton>
</Li>
)
}
export default CoreSettingSaBool

View file

@ -0,0 +1,76 @@
import { useState } from 'react'
import { formatMm } from 'shared/utils'
import ClearIcon from 'shared/components/icons/clear'
import { useTranslation } from 'next-i18next'
const CoreSettingMm = props => {
const { t } = useTranslation(['app', 'settings'])
const { dflt, min, max } = props
const val = props.gist?.[props.setting]
const [value, setValue] = useState(val)
const handleChange = evt => {
const newVal = parseFloat(evt.target.value)
setValue(newVal)
if (props.gist.sa) props.setGist({
...props.gist,
saMm: newVal,
sa: newVal,
})
else props.updateGist(['saMm'], newVal)
}
const reset = () => {
setValue(dflt)
props.updateGist(['saMm'], dflt)
}
return (
<div className="py-4 mx-6 border-l-2 pl-2">
<p className="m-0 p-0 px-2 mb-2 text-base-content opacity-60 italic">
{t(`settings:sa.d`)}
</p>
<div className="flex flex-row justify-between">
<span
className="opacity-50"
dangerouslySetInnerHTML={{__html: formatMm(min, props.gist.units)}}
/>
<span
className={`font-bold ${val===dflt ? 'text-secondary-focus' : 'text-accent'}`}
dangerouslySetInnerHTML={{__html: formatMm(val, props.gist.units)}}
/>
<span
className="opacity-50"
dangerouslySetInnerHTML={{__html: formatMm(max, props.gist.units)}}
/>
</div>
<input
type="range"
max={max}
min={min}
step={0.1}
value={value}
onChange={handleChange}
className={`
range range-sm mt-1
${val === dflt ? 'range-secondary' : 'range-accent'}
`}
/>
<div className="flex flex-row justify-between">
<span />
<button
title={t('reset')}
className="btn btn-ghost btn-xs text-accent"
disabled={val === dflt}
onClick={reset}
>
<ClearIcon />
</button>
</div>
</div>
)
}
export default CoreSettingMm

View file

@ -0,0 +1,69 @@
import SettingsIcon from 'shared/components/icons/settings.js'
import { linkClasses, Chevron } from 'shared/components/navigation/primary.js'
import Setting from './setting.js'
import { Ul, Details, TopSummary, TopSumTitle } from '../index.js'
import { useTranslation } from 'next-i18next'
export const settings = {
paperless: {
dflt: false,
},
saBool: {
dflt: false,
},
saMm: {
min: 0,
max: 25,
dflt: 10,
},
complete: {
dflt: false,
},
only: { },
locale: {
list: ['de', 'en', 'es', 'fr', 'nl'],
},
units: {
list: ['metric', 'imperial'],
},
margin: {
min: 0,
max: 25,
dflt: 2,
},
scale: {
min: 0.1,
max: 5,
dflt: 1,
},
renderer: {
list: ['react', 'svg'],
titles: {
react: '<Draft /> (React)',
svg: '@freesewing/core (SVG)'
}
},
debug: {
dflt: false,
},
}
const CoreSettings = props => {
const { t } = useTranslation(['app'])
return (
<Details open>
<TopSummary icon={<SettingsIcon />}>
<TopSumTitle>{t('settings')}</TopSumTitle>
<Chevron />
</TopSummary>
<Ul>
{Object.keys(settings).map(setting => (
<Setting key={setting} setting={setting} config={settings[setting]} {...props} />
))}
</Ul>
</Details>
)
}
export default CoreSettings

View file

@ -0,0 +1,118 @@
import { Chevron } from 'shared/components/navigation/primary'
import PctDegOption from 'shared/components/workbench/inputs/design-option-pct-deg'
import ListSetting from './core-setting-list'
import OnlySetting from './core-setting-only'
import MmSetting from './core-setting-mm'
import NrSetting from './core-setting-nr'
import BoolSetting from './core-setting-bool'
import SaBoolSetting from './core-setting-sa-bool'
import SaMmSetting from './core-setting-sa-mm'
import { formatMm } from 'shared/utils'
import { SecText, Li, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu/index'
import { useTranslation } from 'next-i18next'
const settings = {
paperless: props => (
<SecText>
{props.t(props.gist.paperless ? 'yes' : 'no')}
</SecText>
),
complete: props => (
<SecText>
{props.t(props.gist.complete ? 'yes' : 'no')}
</SecText>
),
debug: props => (
<SecText>
{props.t(props.gist.debug ? 'yes' : 'no')}
</SecText>
),
locale: props => (
<SecText>
{props.t(`i18n:${props.gist.locale}`)}
</SecText>
),
units: props => (
<SecText>
{props.t(`${props.gist.units}Units`)}
</SecText>
),
margin: props => <SecText raw={formatMm(props.gist.margin, props.gist.units)} />,
scale: props => props.gist.scale === 1
? <SecText>{props.gist.scale}</SecText>
: <span className="text-accent">{props.gist.scale}</span>,
saMm: props => <SecText raw={formatMm(props.gist.saMm, props.gist.units)} />,
renderer: props => (
<SecText>
{props.config.titles[props.gist.renderer]}
</SecText>
),
only: props => (props.gist?.only && props.gist.only.length > 0)
? <SecText>{props.gist.only.length}</SecText>
: <span className="text-secondary-focus">{props.t('default')}</span>
}
const inputs = {
locale: props => <ListSetting
{...props}
list={props.config.list.map(key => ({
key,
title: props.t(`i18n:${key}`)
}))}
/>,
units: props => <ListSetting
{...props}
list={props.config.list.map(key => ({
key,
title: props.t(`${key}Units`)
}))}
/>,
margin: props => <MmSetting {...props} {...props.config} />,
scale: props => <NrSetting {...props} {...props.config} />,
saMm: props => <SaMmSetting {...props} {...props.config} />,
renderer: props => <ListSetting
{...props}
list={props.config.list.map(key => ({
key,
title: props.config.titles[key]
}))}
/>,
only: props => <OnlySetting {...props} />
}
const Setting = props => {
const { t } = useTranslation(['app', 'i18n', 'settings'])
if (props.setting === 'saBool')
return <SaBoolSetting {...props} {...props.config} />
if (['paperless', 'complete', 'debug', 'xray'].indexOf(props.setting) !== -1)
return <BoolSetting {...props} {...props.config} />
const Input = inputs[props.setting]
const Value = settings[props.setting]
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
{props.setting === 'saMm'
? (
<>
<span>{t(`settings:sa.t`)}</span>
<span className="ml-4 opacity-50">[ {t(`size`)} ]</span>
</>
)
: <span>{t(`settings:${props.setting}.t`)}</span>
}
</SumDiv>
<Value setting={props.setting} {...props} t={t}/>
<Chevron />
</Summary>
<Input {...props} t={t} />
</Details>
</Li>
)
}
export default Setting

View file

@ -0,0 +1,25 @@
import OptionsIcon from 'shared/components/icons/options.js'
import { Chevron } from 'shared/components/navigation/primary.js'
import OptionGroup from './option-group'
import { Ul, Details, TopSummary, TopSumTitle } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const DesignOptions = props => {
const { t } = useTranslation(['app'])
return (
<Details open>
<TopSummary icon={<OptionsIcon />}>
<TopSumTitle>{t('designOptions')}</TopSumTitle>
<Chevron />
</TopSummary>
<Ul className="pl-5 list-inside">
{Object.keys(props.design.config.optionGroups).map(group => (
<OptionGroup {...props} group={group} key={group} />
))}
</Ul>
</Details>
)
}
export default DesignOptions

View file

@ -0,0 +1,32 @@
import { Chevron } from 'shared/components/navigation/primary.js'
import Option from './option'
import { Li, Ul, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const OptionGroup = props => {
const { t } = useTranslation(['optiongroups'])
const config = props.config || props.design.config.optionGroups[props.group]
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
<span className="font-bold">
{ t(props.group) }
</span>
</SumDiv>
<Chevron />
</Summary>
<Ul>
{config.map(option =>
typeof option === 'string' ? <Option {...props} option={option} key={option} />
: Object.keys(option).map((sub) => <OptionGroup {...props} config={option[sub]} group={sub} key={sub}/>)
)}
</Ul>
</Details>
</Li>
)
}
export default OptionGroup

View file

@ -0,0 +1,14 @@
import PctDegOption from 'shared/components/workbench/inputs/design-option-pct-deg'
import CountOption from 'shared/components/workbench/inputs/design-option-count'
import ListOption from 'shared/components/workbench/inputs/design-option-list'
export const Tmp = props => <p>not yet</p>
export const inputs = {
pct: PctDegOption,
count: CountOption,
deg: props => (<PctDegOption {...props} type='deg' />),
list: ListOption,
mm: (<p>Mm options are not supported. Please report this.</p>),
constant: Tmp,
}

View file

@ -0,0 +1,69 @@
import { useTranslation } from 'next-i18next'
import { formatMm, formatPercentage} from 'shared/utils'
export const values = {
pct: props => {
const val = (typeof props.gist?.options?.[props.option] === 'undefined')
? props.design.config.options[props.option].pct/100
: props.gist.options[props.option]
return (
<span className={
val=== props.design.config.options[props.option].pct/100
? 'text-secondary-focus'
: 'text-accent'
}>
{formatPercentage(val)}
{props.design.config.options[props.option]?.toAbs
? ' | ' +formatMm(props.design.config.options[props.option]?.toAbs(val, props.gist))
: null
}
</span>
)
},
bool: props => {
const { t } = useTranslation(['app'])
const dflt = props.design.config.options[props.option].bool
let current = props.gist?.options?.[props.option]
current = current === undefined ? dflt : current;
return (
<span className={
(dflt==current || typeof current === 'undefined')
? 'text-secondary-focus'
: 'text-accent'
}>
{current
? t('yes')
: t('no')
}
</span>
)
},
count: props => {
const dflt = props.design.config.options[props.option].count
const current = props.gist?.options?.[props.option]
return (dflt==current || typeof current === 'undefined')
? (<span className="text-secondary-focus">{dflt}</span>)
: (<span className="text-accent">{current}</span>)
},
list: props => {
const dflt = props.design.config.options[props.option].dflt
const current = props.gist?.options?.[props.option]
const prefix = `${props.option}.o.`
return (dflt==current || typeof current === 'undefined')
? (<span className="text-secondary-focus">{props.t(prefix+dflt)}</span>)
: (<span className="text-accent">{props.t(prefix+current)}</span>)
},
deg: props => {
const dflt = props.design.config.options[props.option].deg
const current = props.gist?.options?.[props.option]
return (dflt==current || typeof current === 'undefined')
? (<span className="text-secondary-focus">{dflt}&deg;</span>)
: (<span className="text-accent">{current}&deg;</span>)
},
mm: props => {
return (<p>No mm val yet</p>)
},
constant: props => {
return (<p>No constant val yet</p>)
}
}

View file

@ -0,0 +1,59 @@
import { Chevron } from 'shared/components/navigation/primary'
import { optionType } from 'shared/utils'
import { Li, Ul, Details, Summary, SumButton, SumDiv, Deg } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
import {values} from 'shared/components/workbench/menu/design-options/option-value'
import {inputs} from 'shared/components/workbench/menu/design-options/option-input'
const Option = props => {
const { t } = useTranslation([`o_${props.design.config.name}`])
const opt = props.design.config.options[props.option];
const type = optionType(opt)
const Input = inputs[type]
const Value = values[type]
const hide = opt.hide && opt.hide(props.draft.settings.options);
if (hide) {
return <Li></Li>
}
if (type === 'bool') {
const toggleBoolean = () => {
const dflt = opt.bool
const current = props.gist?.options?.[props.option]
if (typeof current === 'undefined')
props.updateGist(['options', props.option], !dflt)
else props.unsetGist(['options', props.option])
}
return (
<Li>
<SumButton onClick={toggleBoolean}>
<SumDiv>
<Deg />
<span>{t(`${props.option}.t`) }</span>
</SumDiv>
<Value type={type} {...props} t={t} />
</SumButton>
</Li>
)
}
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
<span>{t(`${props.option}.t`)}</span>
</SumDiv>
<Value type={type} {...props} t={t} />
<Chevron w={6} m={3}/>
</Summary>
<Input {...props} ot={t} />
</Details>
</Li>
)
}
export default Option

View file

@ -0,0 +1,101 @@
import { linkClasses, Chevron } from 'shared/components/navigation/primary.js'
import ViewMenu from './view.js'
import DesignOptions from './design-options'
import CoreSettings from './core-settings'
import Xray from './xray'
import TestDesignOptions from './test-design-options'
export const Ul = props => <ul className="pl-5 list-inside">{props.children}</ul>
export const Li = props => (
<li className="flex flex-row hover:border-r-2 hover:border-r-secondary">
{props.children}
</li>
)
export const Details = props => (
<details className="grow" open={props.open || false}>
{props.children}
</details>
)
export const Deg = props => <span className="text-3xl inline-block p-0 leading-3 px-2 translate-y-3">&deg;</span>
export const NoSumDiv = props => (
<div className={`
grow px-2 ml-2 border-l-2
${linkClasses}
hover:cursor-resize
hover:border-secondary
sm:hover:border-secondary-focus
text-base-content sm:text-base-content
`}>{props.children}</div>
)
export const SumDiv = props => (
<div className={`
grow pl-2 border-l-2
${linkClasses}
hover:cursor-resize
hover:border-secondary
sm:hover:border-secondary-focus
text-base-content sm:text-base-content
`}>{props.children}</div>
)
export const Summary = props => (
<summary className={`
flex flex-row
px-2
text-base-content
sm:text-base-content
hover:cursor-row-resize
items-center
`}>{props.children}</summary>
)
export const TopSummary = props => (
<summary className={`
flex flex-row gap-4 text-lg
hover:cursor-row-resize
p-2
text-base-content
sm:text-base-content
items-center
`}>
<span className="text-secondary-focus mr-4">{props.icon || null}</span>
{props.children}
</summary>
)
export const SumButton = props => (
<button className={`
flex flex-row
px-2
w-full justify-between
text-left
text-base-content
sm:text-base-content
hover:cursor-pointer
items-center
mr-4
`} onClick={props.onClick}>{props.children}</button>
)
export const TopSumTitle = props => (
<span className={`grow ${linkClasses} hover:cursor-resize font-bold uppercase`}>
{props.children}
</span>
)
export const SecText = props => props.raw
? <span className="text-secondary-focus" dangerouslySetInnerHTML={{__html: props.raw}} />
: <span className="text-secondary-focus">{props.children}</span>
const WorkbenchMenu = props => {
return (
<nav className="grow mb-12">
<ViewMenu {...props} />
{props.gist?._state?.view === 'draft' && (
<>
<DesignOptions {...props} />
<CoreSettings {...props} />
{props.gist.renderer === 'react' && <Xray {...props} />}
</>
)}
{props.gist?._state?.view === 'test' && <TestDesignOptions {...props} />}
</nav>
)
}
export default WorkbenchMenu

View file

@ -0,0 +1,25 @@
import OptionsIcon from 'shared/components/icons/options.js'
import { Chevron } from 'shared/components/navigation/primary.js'
import OptionGroup from './option-group'
import { Ul, Details, TopSummary, TopSumTitle } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const DesignOptions = props => {
const { t } = useTranslation(['app'])
return (
<Details open>
<TopSummary icon={<OptionsIcon />}>
<TopSumTitle>{t('app:designOptions')}</TopSumTitle>
<Chevron />
</TopSummary>
<Ul className="pl-5 list-inside">
{Object.keys(props.design.config.optionGroups).map(group => (
<OptionGroup {...props} group={group} key={group} />
))}
</Ul>
</Details>
)
}
export default DesignOptions

View file

@ -0,0 +1,33 @@
import { Chevron } from 'shared/components/navigation/primary.js'
import Option from './option'
import OptionSubGroup from './option-sub-group'
import { Li, Ul, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const OptionGroup = props => {
const { t } = useTranslation(['optiongroups'])
const config = props.config || props.design.config.optionGroups[props.group]
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
<span className="font-bold">
{ t(props.group) }
</span>
</SumDiv>
<Chevron />
</Summary>
<Ul>
{config.map(option => typeof option === 'string'
? <Option {...props} option={option} key={option} />
: <OptionSubGroup {...props} sub={option} config={config} />
)}
</Ul>
</Details>
</Li>
)
}
export default OptionGroup

View file

@ -0,0 +1,29 @@
import { Chevron } from 'shared/components/navigation/primary.js'
import Option from './option'
import { Li, Ul, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const OptionSubGroup = props => {
const { t } = useTranslation(['optiongroups'])
return Object.keys(props.sub).map(name => (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
<span className="font-bold">{ t(name) }</span>
</SumDiv>
<Chevron />
</Summary>
<Ul>
{props.sub[name].map(option => typeof option === 'string'
? <Option {...props} option={option} key={option} />
: <OptionSubGroup {...props} sub={option} config={config} />
)}
</Ul>
</Details>
</Li>
))
}
export default OptionSubGroup

View file

@ -0,0 +1,67 @@
import { linkClasses } from 'shared/components/navigation/primary.js'
import { Li } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const SumButton = props => (
<button className={`
flex flex-row
px-2
w-full justify-between
text-left
text-base-content
sm:text-base-content
hover:cursor-pointer
items-center
mr-4
`} onClick={props.onClick}>{props.children}</button>
)
const SumDiv = (props) => (
<div className={`
grow pl-2 border-l-2
${linkClasses}
hover:cursor-resize
hover:border-secondary
sm:hover:border-secondary-focus
text-base-content sm:text-base-content
${props.active && 'border-secondary-focus'}
`}>{props.children}</div>
)
const Option = props => {
const { t } = useTranslation([`o_${props.design.config.name}`, 'workbench'])
const active = (
props.gist.sample?.type === 'option' &&
props.gist.sample?.option === props.option
)
return (
<Li>
<SumButton onClick={() => props.updateGist(
['sample'],
{
type: 'option',
option: props.option
},
true // Close navigation on mobile
)}>
<SumDiv active={active}>
<span className={`
text-3xl inline-block p-0 leading-3 px-2
${active
? 'text-secondary sm:text-secondary-focus translate-y-1 font-bold'
: 'translate-y-3'
}`}
>
{active ? <span>&bull;</span> : <span>&deg;</span>}
</span>
<span className={active ? 'text-secondary font-bold' : ''}>
{t(`o_${props.design.config.name}:${props.option}.t`)}
</span>
</SumDiv>
</SumButton>
</Li>
)
}
export default Option

View file

@ -0,0 +1,120 @@
import MenuIcon from 'shared/components/icons/menu.js'
import { linkClasses, Chevron } from 'shared/components/navigation/primary.js'
import { useTranslation } from 'next-i18next'
import defaultSettings from '../default-settings'
const View = props => {
const { t } = useTranslation(['app'])
const entries = [
{
name: 'measurements',
title: t('measurements'),
onClick: () => props.updateGist(['_state', 'view'], 'measurements', true)
},
{
name: 'draft',
title: t('draftDesign', { design: props.design.config.name }),
onClick: () => props.updateGist(['_state', 'view'], 'draft', true)
},
{
name: 'test',
title: t('testDesign', { design: props.design.config.name }),
onClick: () => props.updateGist(['_state', 'view'], 'test', true)
},
{
name: 'printingLayout',
title: t('layoutThing', { thing: props.design.config.name })
+ ': ' + t('forPrinting'),
onClick: () => props.updateGist(['_state', 'view'], 'printingLayout', true)
},
{
name: 'cuttingLayout',
title: t('layoutThing', { thing: props.design.config.name })
+ ': ' + t('forCutting'),
onClick: () => props.updateGist(['_state', 'view'], 'cuttingLayout', true)
},
{
name: 'export',
title: t('exportThing', { thing: props.design.config.name }),
onClick: () => props.updateGist(['_state', 'view'], 'export', true)
},
{
name: 'events',
title: t('events'),
onClick: () => props.updateGist(['_state', 'view'], 'events', true)
},
{
name: 'yaml',
title: t('YAML'),
onClick: () => props.updateGist(['_state', 'view'], 'yaml', true)
},
{
name: 'json',
title: t('JSON'),
onClick: () => props.updateGist(['_state', 'view'], 'json', true)
},
{
name: 'edit',
title: t('editThing', { thing: 'YAML' }),
onClick: () => props.updateGist(['_state', 'view'], 'edit', true)
},
{
name: 'clear',
title: t('clearThing', { thing: 'YAML' }),
onClick: () => props.setGist(defaultSettings)
},
]
return (
<details className='py-1' open>
<summary className={`
flex flex-row uppercase gap-4 font-bold text-lg
hover:cursor-row-resize
p-2
text-base-content
sm:text-base-content
items-center
`}>
<span className="text-secondary-focus mr-4"><MenuIcon /></span>
<span className={`grow ${linkClasses} hover:cursor-resize`}>
{t('view')}
</span>
<Chevron />
</summary>
<ul className="pl-5 list-inside">
{entries.map(entry => (
<li key={entry.title} className="flex flex-row">
<button title={entry.title} className={`
grow pl-2 border-l-2
${linkClasses}
hover:cursor-pointer
hover:border-secondary
sm:hover:border-secondary-focus
text-left
capitalize
${entry.name === props.gist?._state?.view
? 'text-secondary border-secondary sm:text-secondary-focus sm:border-secondary-focus'
: 'text-base-content sm:text-base-content'
}
`} onClick={entry.onClick}>
<span className={`
text-3xl mr-2 inline-block p-0 leading-3
${entry.name === props.gist?._state?.view
? 'text-secondary sm:text-secondary-focus translate-y-1 font-bold'
: 'translate-y-3'
}
`}>
{entry.name === props.gist?._state?.view ? <>&bull;</> : <>&deg;</>}
</span>
<span className={entry.name === props.gist?._state?.view ? 'font-bold' : ''}>
{ entry.title }
</span>
</button>
</li>
))}
</ul>
</details>
)
}
export default View

View file

@ -0,0 +1,51 @@
import { Chevron } from 'shared/components/navigation/primary'
import { Ul, Li, Details, Summary, SumDiv, NoSumDiv, Deg } from 'shared/components/workbench/menu'
import { round } from 'shared/utils'
const XrayAttributes = ({ attr=false, t }) => {
if (!attr || !attr.list || Object.keys(attr.list).length < 1) return null
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
Attributes
</SumDiv>
<Chevron />
</Summary>
<Ul>
{Object.keys(attr.list).map(at => (
<Li key={at}>
<Details>
<Summary>
<SumDiv>
<Deg />
{at}
</SumDiv>
<Chevron />
</Summary>
<Ul>
{attr.list[at].map(val => (
<Li key={val}>
<NoSumDiv>
<Deg />
<span>{val === true
? t('app.yes')
: val
}</span>
</NoSumDiv>
</Li>
))}
</Ul>
</Details>
</Li>
))}
</Ul>
</Details>
</Li>
)
}
export default XrayAttributes

View file

@ -0,0 +1,21 @@
import { Li, SumButton, SumDiv, Deg } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const DisableXray = props => {
const { t } = useTranslation(['cfp', 'settings'])
return (
<Li>
<SumButton onClick={() => props.updateGist(['_state', 'xray', 'enabled'], false)}>
<SumDiv>
<Deg />
<span>
{t('cfp:thingIsEnabled', { thing: t('settings:xray.t') })}
</span>
</SumDiv>
</SumButton>
</Li>
)
}
export default DisableXray

View file

@ -0,0 +1,54 @@
import XrayIcon from 'shared/components/icons/xray.js'
import { linkClasses, Chevron } from 'shared/components/navigation/primary.js'
import Log from './log.js'
import Reset from './reset.js'
import Disable from './disable.js'
import List from './list.js'
import { Ul, Details, TopSummary } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const Xray = props => {
const { t } = useTranslation(['app', 'settings'])
return (
<Details open>
<TopSummary icon={<XrayIcon />}>
{props.gist?._state?.xray?.enabled
? (
<>
<span className={`grow ${linkClasses} hover:cursor-resize font-bold uppercase`}>
{t('settings:xray.t')}
</span>
<Chevron />
</>
) : (
<>
<button
className={`grow ${linkClasses} hover:cursor-resize uppercase font-bold text-left`}
onClick={() => props.updateGist(['_state', 'xray', 'enabled'], true)}
>
{t('settings:xray.t')}
</button>
<span className="text-normal text-secondary">
{t('cfp:thingIsDisabled', { thing: t('settings:xray.t') })}
</span>
</>
)
}
</TopSummary>
{props.gist?._state?.xray?.enabled && (
<Ul>
<Disable {...props} />
<Log {...props} />
<Reset {...props} />
{
props.gist?._state?.xray?.parts &&
Object.keys(props.gist._state.xray.parts).map(partName => <List {...props} partName={partName} />)
}
</Ul>
)}
</Details>
)
}
export default Xray

View file

@ -0,0 +1,138 @@
import { Chevron } from 'shared/components/navigation/primary.js'
import ClearIcon from 'shared/components/icons/clear.js'
import FilterIcon from 'shared/components/icons/filter.js'
import SearchIcon from 'shared/components/icons/search.js'
import { Ul, Li, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu'
import Path from './path.js'
import Point from './point.js'
import { useTranslation } from 'next-i18next'
const types = {
paths: Path,
points: Point
}
const XrayList = props => {
const { t } = useTranslation(['app', 'parts'])
const title = t(`parts:${props.partName}`) + ` (${props.partName})`
const part = props.gist._state.xray.parts[props.partName]
// Is this the only part on display?
const only = (
props.gist.only &&
props.gist.only.length === 1 &&
props.gist.only[0] === props.partName
)
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
<span>{title}</span>
<span className="ml-2 opacity-60">[{props.partName}]</span>
</SumDiv>
<button
className={`px-3 hover:text-secondary-focus ${only ? 'text-accent' : 'text-secondary'}`}
title={t('filter')}
onClick={only
? () => props.unsetGist(['only'])
: () => props.updateGist(['only'], [props.partName])
}
>
<FilterIcon />
</button>
<button
className="text-accent px-3 hover:text-secondary-focus"
onClick={() => props.unsetGist(['_state', 'xray', 'parts', props.partName])}
>
<ClearIcon />
</button>
<Chevron w={6} m={3}/>
</Summary>
{Object.keys(types).map(type => part[type] && (
<Ul>
<Li>
<Details>
<Summary>
<SumDiv>
<span className="capitalize">{type}</span>
</SumDiv>
<button
className="text-accent px-3 hover:text-secondary-focus"
onClick={() => props.unsetGist(['_state', 'xray', 'parts', props.partName, type])}
>
<ClearIcon />
</button>
<Chevron />
</Summary>
<Ul>
{Object.keys(part[type])
.map(id => (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
<span>{id}</span>
</SumDiv>
<button
className={`px-3 hover:text-secondary-focus"
${props.gist._state?.xray?.reveal?.[props.partName]?.[type]?.[id]
? 'text-accent'
: 'text-secondary'
}`}
onClick={props.gist._state?.xray?.reveal?.[props.partName]?.[type]?.[id]
? () => props.unsetGist(
['_state', 'xray', 'reveal', props.partName, type, id]
)
: () => props.updateGist(
['_state', 'xray', 'reveal', props.partName, type, id],
id
)
}
>
<SearchIcon />
</button>
<button
className="text-accent px-3 hover:text-secondary-focus"
onClick={() => {
props.unsetGist(['_state', 'xray', 'parts', props.partName, type, id])
props.unsetGist(['_state', 'xray', 'reveal', props.partName, type, id])
}}
>
<ClearIcon />
</button>
<Chevron />
</Summary>
{type === 'paths' && <Path
pathName={id}
partName={props.partName}
draft={props.draft}
t={t}
units={props.gist.units}
/>}
{type === 'points' && <Point
pointName={id}
partName={props.partName}
draft={props.draft}
t={t}
/>}
</Details>
</Li>
))
}
</Ul>
</Details>
</Li>
</Ul>
))}
</Details>
</Li>
)
}
export default XrayList

View file

@ -0,0 +1,33 @@
import { Chevron } from 'shared/components/navigation/primary.js'
import { Ul, Li, Details, Summary, SumButton, SumDiv, Deg } from 'shared/components/workbench/menu'
const ConsoleLog = props => (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
<span>console.log()</span>
</SumDiv>
<Chevron />
</Summary>
<Ul>
{['config', 'gist', 'draft'].map(it => (
<Li key={it}>
<SumButton onClick={() => console.log(it === 'config'
? props.design.config
: props[it]
)}>
<SumDiv>
<Deg />
<span>{it}</span>
</SumDiv>
</SumButton>
</Li>
))}
</Ul>
</Details>
</Li>
)
export default ConsoleLog

View file

@ -0,0 +1,76 @@
import { Chevron } from 'shared/components/navigation/primary'
import { Ul, Li, Details, Summary, SumDiv, NoSumDiv, Deg } from 'shared/components/workbench/menu'
import { round } from 'shared/utils'
import Point from './point'
const MoveLine = ({ op }) => <Point point={op.to} />
const Curve = ({ op }) => ['cp1', 'cp2', 'to'].map(pnt => (
<Li key={pnt}>
<Details>
<Summary>
<SumDiv>
<Deg />
<span className="font-bold">{pnt}</span>
</SumDiv>
<Chevron />
</Summary>
<Point point={op[pnt]} />
</Details>
</Li>
))
const Close = () => (
<p>Close</p>
)
const XrayPathOp = ({ op }) => (
<Li>
{op.type === 'close'
? (
<NoSumDiv>
<Deg />
<span className="font-bold">{op.type}</span>
</NoSumDiv>
) : (
<Details>
<Summary>
<SumDiv>
<Deg />
<span className="font-bold">{op.type}</span>
<span>To</span>
</SumDiv>
<Chevron />
</Summary>
<Ul>
{op.type === 'curve'
? <Curve op={op} />
: <MoveLine op={op} />
}
</Ul>
</Details>
)
}
</Li>
)
const XrayPathOps = ({ ops=false }) => {
if (!ops || ops.length < 1) return null
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
<span className="font-bold">path.ops</span>
</SumDiv>
<Chevron />
</Summary>
<Ul>
{ops.map(op => <XrayPathOp op={op} />)}
</Ul>
</Details>
</Li>
)
}
export default XrayPathOps

View file

@ -0,0 +1,34 @@
import { Ul, Li, Details, Summary, NoSumDiv, Deg } from 'shared/components/workbench/menu'
import { formatMm } from 'shared/utils'
import Attributes from './attributes'
import Ops from './path-ops'
const XrayPath = ({ pathName, partName, draft, t, units }) => {
const path = draft?.parts?.[partName]?.paths?.[pathName]
if (!path) return null
return (
<Ul>
<Attributes attr={path.attributes} />
<Li>
<NoSumDiv>
<Deg />
<span className="font-bold mr-2">path.render =</span>
<span>{JSON.stringify(path.render)}</span>
</NoSumDiv>
</Li>
<Li>
<NoSumDiv>
<Deg />
<span className="font-bold mr-2">path.length() =</span>
<span dangerouslySetInnerHTML={{
__html: formatMm(path.length(), units)
}} />
</NoSumDiv>
</Li>
<Ops ops={path.ops} />
</Ul>
)
}
export default XrayPath

View file

@ -0,0 +1,25 @@
import { Ul, Li, Details, Summary, NoSumDiv, Deg } from 'shared/components/workbench/menu'
import { round } from 'shared/utils'
import Attributes from './attributes'
const XrayPoint = ({ pointName, partName, draft, t }) => {
const point = draft?.parts?.[partName]?.points?.[pointName]
return point
? (
<Ul>
{['x', 'y'].map(coord => (
<Li key={coord}>
<NoSumDiv>
<Deg />
<span className="font-bold mr-2">{coord} =</span>
<span>{round(point[coord])}</span>
</NoSumDiv>
</Li>
))}
<Attributes attr={point.attributes} t={t} />
</Ul>
) : null
}
export default XrayPoint

View file

@ -0,0 +1,19 @@
import { Li, SumButton, SumDiv, Deg } from 'shared/components/workbench/menu'
import { useTranslation } from 'next-i18next'
const ResetXray = props => {
const { t } = useTranslation(['app'])
return (
<Li>
<SumButton onClick={() => props.updateGist(['_state', 'xray'], { enabled: true })}>
<SumDiv>
<Deg />
<span>{ t(`reset`) }</span>
</SumDiv>
</SumButton>
</Li>
)
}
export default ResetXray

View file

@ -0,0 +1,28 @@
import yaml from 'js-yaml'
import axios from 'axios'
const preload = {
github: async (id, pattern) => {
let result
try {
result = await axios.get(`https://api.github.com/gists/${id}`)
}
catch (err) {
console.log(err)
return [false, 'An unexpected error occured']
}
if (result.data.files['pattern.yaml'].content) {
let g = yaml.load(result.data.files['pattern.yaml'].content)
if (g.design !== pattern.config.name) return [
false, `You tried loading a configuration for ${g.design} into a ${pattern.config.name} development environment`
]
return g
}
else return [false, 'This gist does not seem to be a valid pattern configuration']
}
}
export default preload

View file

@ -0,0 +1,36 @@
import { SizeMe } from 'react-sizeme'
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"
import svgattrPlugin from '@freesewing/plugin-svgattr'
import { useTranslation } from 'next-i18next'
const LabSample = ({ gist, draft }) => {
const { t } = useTranslation(['workbench'])
let svg
let title = ''
if (gist.sample) {
try {
draft.use(svgattrPlugin, {
class: 'freesewing pattern max-h-screen'
})
draft.sample()
svg = draft.render()
}
catch(err) {
console.log(err)
}
if (gist.sample.type === 'option') {
title = t('testThing', {
thing: ' : ' + t('option') + ' : ' + gist.sample.option
})
}
}
return (
<>
<h2>{title}</h2>
<div className="freesewing pattern" dangerouslySetInnerHTML={{__html: svg}} />
</>
)
}
export default LabSample

View file

@ -0,0 +1,9 @@
import Yaml from 'shared/components/yaml.js'
const GistAsYaml = props => (
<div className="max-w-screen-xl m-auto">
<Yaml json={JSON.stringify(props.gist, null, 2)} />
</div>
)
export default GistAsYaml