Merge branch 'develop' into eriese-imperial
This commit is contained in:
commit
7476d45f54
575 changed files with 2464 additions and 1196 deletions
16
sites/shared/components/workbench/default-settings.js
Normal file
16
sites/shared/components/workbench/default-settings.js
Normal 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
|
10
sites/shared/components/workbench/draft/circle/index.js
Normal file
10
sites/shared/components/workbench/draft/circle/index.js
Normal 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
|
69
sites/shared/components/workbench/draft/defs/index.js
Normal file
69
sites/shared/components/workbench/draft/defs/index.js
Normal 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
|
69
sites/shared/components/workbench/draft/error.js
Normal file
69
sites/shared/components/workbench/draft/error.js
Normal 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
|
||||
|
40
sites/shared/components/workbench/draft/index.js
Normal file
40
sites/shared/components/workbench/draft/index.js
Normal 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
|
114
sites/shared/components/workbench/draft/part/index.js
Normal file
114
sites/shared/components/workbench/draft/part/index.js
Normal 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
|
41
sites/shared/components/workbench/draft/path/index.js
Normal file
41
sites/shared/components/workbench/draft/path/index.js
Normal 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
|
157
sites/shared/components/workbench/draft/point/index.js
Normal file
157
sites/shared/components/workbench/draft/point/index.js
Normal 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
|
27
sites/shared/components/workbench/draft/snippet/index.js
Normal file
27
sites/shared/components/workbench/draft/snippet/index.js
Normal 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
|
59
sites/shared/components/workbench/draft/svg-wrapper.js
Normal file
59
sites/shared/components/workbench/draft/svg-wrapper.js
Normal 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
|
||||
|
35
sites/shared/components/workbench/draft/svg/index.js
Normal file
35
sites/shared/components/workbench/draft/svg/index.js
Normal 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
|
|
@ -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(/"/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
|
41
sites/shared/components/workbench/draft/text/index.js
Normal file
41
sites/shared/components/workbench/draft/text/index.js
Normal 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(/"/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(/"/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
|
36
sites/shared/components/workbench/draft/utils.js
Normal file
36
sites/shared/components/workbench/draft/utils.js
Normal 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
|
||||
}
|
64
sites/shared/components/workbench/events.js
Normal file
64
sites/shared/components/workbench/events.js
Normal 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
|
116
sites/shared/components/workbench/export.js
Normal file
116
sites/shared/components/workbench/export.js
Normal 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
|
115
sites/shared/components/workbench/inputs/design-option-count.js
Normal file
115
sites/shared/components/workbench/inputs/design-option-count.js
Normal 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
|
|
@ -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
|
||||
`}>
|
||||
<>°</>
|
||||
</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
|
|
@ -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
|
122
sites/shared/components/workbench/inputs/measurement.js
Normal file
122
sites/shared/components/workbench/inputs/measurement.js
Normal 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
|
||||
|
9
sites/shared/components/workbench/json.js
Normal file
9
sites/shared/components/workbench/json.js
Normal 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
|
21
sites/shared/components/workbench/layout/cut/index.js
Normal file
21
sites/shared/components/workbench/layout/cut/index.js
Normal 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
|
10
sites/shared/components/workbench/layout/cut/settings.js
Normal file
10
sites/shared/components/workbench/layout/cut/settings.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
const CutLayoutSettings = props => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Fixme: Cut layout settings here</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CutLayoutSettings
|
415
sites/shared/components/workbench/layout/draft.js
Normal file
415
sites/shared/components/workbench/layout/draft.js
Normal 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
|
||||
|
54
sites/shared/components/workbench/layout/print/index.js
Normal file
54
sites/shared/components/workbench/layout/print/index.js
Normal 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
|
|
@ -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
|
|
@ -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
|
77
sites/shared/components/workbench/layout/print/plugin.js
Normal file
77
sites/shared/components/workbench/layout/print/plugin.js
Normal 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
|
28
sites/shared/components/workbench/layout/print/settings.js
Normal file
28
sites/shared/components/workbench/layout/print/settings.js
Normal 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
|
118
sites/shared/components/workbench/measurements/index.js
Normal file
118
sites/shared/components/workbench/measurements/index.js
Normal 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')}
|
||||
{ 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
|
||||
|
51
sites/shared/components/workbench/measurements/non-human.js
Normal file
51
sites/shared/components/workbench/measurements/non-human.js
Normal 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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
`}>
|
||||
<>°</>
|
||||
</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
|
||||
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
118
sites/shared/components/workbench/menu/core-settings/setting.js
Normal file
118
sites/shared/components/workbench/menu/core-settings/setting.js
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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}°</span>)
|
||||
: (<span className="text-accent">{current}°</span>)
|
||||
},
|
||||
mm: props => {
|
||||
return (<p>No mm val yet</p>)
|
||||
},
|
||||
constant: props => {
|
||||
return (<p>No constant val yet</p>)
|
||||
}
|
||||
}
|
|
@ -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
|
101
sites/shared/components/workbench/menu/index.js
Normal file
101
sites/shared/components/workbench/menu/index.js
Normal 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">°</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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>•</span> : <span>°</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
|
120
sites/shared/components/workbench/menu/view.js
Normal file
120
sites/shared/components/workbench/menu/view.js
Normal 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 ? <>•</> : <>°</>}
|
||||
</span>
|
||||
<span className={entry.name === props.gist?._state?.view ? 'font-bold' : ''}>
|
||||
{ entry.title }
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
export default View
|
51
sites/shared/components/workbench/menu/xray/attributes.js
Normal file
51
sites/shared/components/workbench/menu/xray/attributes.js
Normal 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
|
21
sites/shared/components/workbench/menu/xray/disable.js
Normal file
21
sites/shared/components/workbench/menu/xray/disable.js
Normal 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
|
54
sites/shared/components/workbench/menu/xray/index.js
Normal file
54
sites/shared/components/workbench/menu/xray/index.js
Normal 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
|
138
sites/shared/components/workbench/menu/xray/list.js
Normal file
138
sites/shared/components/workbench/menu/xray/list.js
Normal 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
|
33
sites/shared/components/workbench/menu/xray/log.js
Normal file
33
sites/shared/components/workbench/menu/xray/log.js
Normal 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
|
76
sites/shared/components/workbench/menu/xray/path-ops.js
Normal file
76
sites/shared/components/workbench/menu/xray/path-ops.js
Normal 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
|
34
sites/shared/components/workbench/menu/xray/path.js
Normal file
34
sites/shared/components/workbench/menu/xray/path.js
Normal 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
|
25
sites/shared/components/workbench/menu/xray/point.js
Normal file
25
sites/shared/components/workbench/menu/xray/point.js
Normal 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
|
19
sites/shared/components/workbench/menu/xray/reset.js
Normal file
19
sites/shared/components/workbench/menu/xray/reset.js
Normal 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
|
28
sites/shared/components/workbench/preload.js
Normal file
28
sites/shared/components/workbench/preload.js
Normal 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
|
||||
|
36
sites/shared/components/workbench/sample.js
Normal file
36
sites/shared/components/workbench/sample.js
Normal 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
|
9
sites/shared/components/workbench/yaml.js
Normal file
9
sites/shared/components/workbench/yaml.js
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue