1
0
Fork 0

feat(components): Added pan and zoom to Workbench. Closes #368

This commit is contained in:
Joost De Cock 2020-04-25 13:29:02 +02:00
parent b53615aea0
commit 054b7565e6
9 changed files with 397 additions and 91 deletions

View file

@ -44,6 +44,7 @@
"@material-ui/core": "^4.0.1",
"@material-ui/icons": "^4.0.1",
"@material-ui/lab": "^v4.0.0-alpha.14",
"react-svg-pan-zoom": "^3.8.0",
"prismjs": "1.16.0",
"file-saver": "^2.0.2"
},

View file

@ -1,17 +1,17 @@
import babel from "rollup-plugin-babel";
import resolve from "rollup-plugin-node-resolve";
import json from "rollup-plugin-json";
import minify from "rollup-plugin-babel-minify";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import { name, version, description, author, license } from "./package.json";
import components from "./src/index.js";
import babel from 'rollup-plugin-babel'
import resolve from 'rollup-plugin-node-resolve'
import json from 'rollup-plugin-json'
import minify from 'rollup-plugin-babel-minify'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import { name, version, description, author, license } from './package.json'
import components from './src/index.js'
const createConfig = (component, module) => {
return {
input: `./src/${component + "/"}index.js`,
input: `./src/${component + '/'}index.js`,
output: {
file: `./${component}/index` + (module ? ".mjs" : ".js"),
format: module ? "es" : "cjs",
file: `./${component}/index` + (module ? '.mjs' : '.js'),
format: module ? 'es' : 'cjs',
sourcemap: true
},
plugins: [
@ -19,8 +19,8 @@ const createConfig = (component, module) => {
resolve({ modulesOnly: true }),
json(),
babel({
exclude: "node_modules/**",
plugins: ["@babel/plugin-proposal-object-rest-spread"]
exclude: 'node_modules/**',
plugins: ['@babel/plugin-proposal-object-rest-spread']
}),
minify({
comments: false,
@ -28,13 +28,16 @@ const createConfig = (component, module) => {
banner: `/**\n * ${name}/${component} | v${version}\n * ${description}\n * (c) ${new Date().getFullYear()} ${author}\n * @license ${license}\n */`
})
]
};
};
}
}
const config = [];
const config = []
// When developing, you can use this to only rebuild the components you're working on
let dev = false
let only = ['Workbench']
for (let component of components) {
config.push(createConfig(component, false));
if (!dev || only.indexOf(component) !== -1) config.push(createConfig(component, false))
// Webpack doesn't handle .mjs very well
//config.push(createConfig(component, true));
}
export default config;
export default config

View file

@ -7,7 +7,7 @@ const Svg = (props) => {
'xmlns:svg': 'http://www.w3.org/2000/svg',
xmlnsXlink: 'http://www.w3.org/1999/xlink',
xmlLang: props.language,
viewBox: `0 0 ${props.width} ${props.height}`,
viewBox: props.viewBox || `0 0 ${props.width} ${props.height}`,
className: props.className,
style: props.style
}
@ -33,7 +33,8 @@ Svg.defaultProps = {
design: false,
language: 'en',
className: 'freesewing draft',
style: {}
style: {},
viewBox: false
}
export default Svg

View file

@ -13,6 +13,8 @@ const Draft = (props) => (
id={props.settings.idPrefix + 'svg'}
design={props.design}
style={props.style}
viewBox={props.viewBox}
className={props.className || 'freesewing draft'}
>
<Defs
units={props.settings.units}

View file

@ -1,16 +1,28 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import Draft from '../../Draft'
import Zoombox from '../Zoombox'
import Design from '../Design'
import DraftConfigurator from '../../DraftConfigurator'
import { FormattedMessage } from 'react-intl'
import Prism from 'prismjs'
import fileSaver from 'file-saver'
import theme from '@freesewing/plugin-theme'
import IconButton from '@material-ui/core/IconButton'
import DesignIcon from '@material-ui/icons/Fingerprint'
import DumpIcon from '@material-ui/icons/LocalSee'
import ClearIcon from '@material-ui/icons/HighlightOff'
import AdvancedIcon from '@material-ui/icons/Policy'
import PaperlessIcon from '@material-ui/icons/Nature'
import CompleteIcon from '@material-ui/icons/Style'
import UnhideIcon from '@material-ui/icons/ChevronLeft'
import HideIcon from '@material-ui/icons/ChevronRight'
const DraftPattern = props => {
const DraftPattern = (props) => {
const [design, setDesign] = useState(true)
const [focus, setFocus] = useState(null)
const [viewBox, setViewBox] = useState(false)
const [hideAside, setHideAside] = useState(false)
const raiseEvent = (type, data) => {
if (type === 'clearFocusAll') {
@ -31,7 +43,7 @@ const DraftPattern = props => {
setFocus(f)
}
const svgToFile = svg => {
const svgToFile = (svg) => {
const blob = new Blob([svg], {
type: 'image/svg+xml;charset=utf-8'
})
@ -54,6 +66,27 @@ const DraftPattern = props => {
const styles = {
paragraph: {
padding: '0 1rem'
},
aside: {
maxWidth: '350px',
background: 'transparent',
border: 0,
fontSize: '90%',
boxShadow: '0 0 1px #cccc',
display: hideAside ? 'none' : 'block'
},
icon: {
margin: '0 0.25rem'
},
unhide: {
position: 'absolute',
top: '76px',
right: 0,
background: props.theme === 'dark' ? '#f8f9fa' : '#212529',
borderTopLeftRadius: '50%',
borderBottomLeftRadius: '50%',
width: '26px',
height: '30px'
}
}
let pattern = new props.Pattern(props.gist.settings)
@ -73,57 +106,116 @@ const DraftPattern = props => {
Prism.languages.javascript,
'javascript'
)
let iconProps = {
size: 'small',
style: styles.icon,
color: 'inherit'
}
const color = (check) => (check ? '#40c057' : '#fa5252')
return (
<div className="fs-sa">
<section>
<h2>
<FormattedMessage id="app.pattern" />
</h2>
<Draft {...patternProps} design={design} focus={focus} raiseEvent={raiseEvent} />
<h2>gist</h2>
<div className="gatsby-highlight">
<pre className="language-json" dangerouslySetInnerHTML={{ __html: gist }} />
</div>
<Draft
{...patternProps}
design={design}
focus={focus}
raiseEvent={raiseEvent}
viewBox={viewBox}
className="freesewing draft shadow"
/>
{hideAside && (
<div style={styles.unhide}>
<IconButton
onClick={() => setHideAside(false)}
title="Show sidebar"
{...iconProps}
style={{ margin: 0 }}
>
<span style={{ color: props.theme === 'dark' ? '#212529' : '#f8f9fa' }}>
<UnhideIcon />
</span>
</IconButton>
</div>
)}
</section>
<aside>
<aside style={styles.aside}>
<div className="sticky">
{design ? (
<React.Fragment>
<p style={styles.paragraph}>
<FormattedMessage id="cfp.designModeIsOn" />
&nbsp;(
<a href="#logo" onClick={() => setDesign(false)}>
<FormattedMessage id="cfp.turnOff" />
</a>
)
{focusCount > 0 ? (
<React.Fragment>
&ensp;(
<a href="#logo" onClick={() => raiseEvent('clearFocusAll', null)}>
<FormattedMessage id="app.reset" />
</a>
)
</React.Fragment>
) : null}
</p>
<Design
focus={focus}
design={design}
raiseEvent={raiseEvent}
parts={patternProps.parts}
/>
</React.Fragment>
) : (
<p style={styles.paragraph}>
<FormattedMessage id="cfp.designModeIsOff" />
&nbsp;(
<a href="#logo" onClick={() => setDesign(true)}>
<FormattedMessage id="cfp.turnOn" />
</a>
)
</p>
<div style={{ padding: '5px' }}>
<Zoombox patternProps={patternProps} setViewBox={setViewBox} />
</div>
<div style={{ margin: '1rem auto 0', textAlign: 'center' }}>
<IconButton
onClick={() => setDesign(!design)}
title="Toggle design mode"
{...iconProps}
>
<span style={{ color: color(design) }}>
<DesignIcon />
</span>
</IconButton>
{design && (
<IconButton
onClick={() => raiseEvent('clearFocusAll', null)}
title="Clear design mode"
{...iconProps}
>
<ClearIcon color="primary" />
</IconButton>
)}
<IconButton
onClick={() => console.log(pattern)}
title="console.log(pattern)"
{...iconProps}
>
<DumpIcon color="primary" />
</IconButton>
|
<IconButton
onClick={() =>
props.updateGist(!props.gist.settings.advanced, 'settings', 'advanced')
}
title="Toggle advanced settings"
{...iconProps}
>
<span style={{ color: color(props.gist.settings.advanced) }}>
<AdvancedIcon />
</span>
</IconButton>
<IconButton
onClick={() =>
props.updateGist(!props.gist.settings.paperless, 'settings', 'paperless')
}
title="Toggle paperless"
{...iconProps}
>
<span style={{ color: color(props.gist.settings.paperless) }}>
<PaperlessIcon />
</span>
</IconButton>
<IconButton
onClick={() =>
props.updateGist(!props.gist.settings.complete, 'settings', 'complete')
}
title="Toggle complete"
{...iconProps}
>
<span style={{ color: color(props.gist.settings.complete) }}>
<CompleteIcon />
</span>
</IconButton>
<IconButton onClick={() => setHideAside(true)} title="Hide sidebar" {...iconProps}>
<HideIcon />
</IconButton>
</div>
{design && (
<Design
focus={focus}
design={design}
raiseEvent={raiseEvent}
parts={patternProps.parts}
/>
)}
<DraftConfigurator
noDocs

View file

@ -0,0 +1,20 @@
import React, { useState } from 'react'
import Prism from 'prismjs'
const PatternJson = (props) => {
let gist = Prism.highlight(
JSON.stringify(props.gist, null, 2),
Prism.languages.javascript,
'javascript'
)
return (
<div style={{ padding: '1rem' }}>
<div className="gatsby-highlight">
<pre className="language-json" dangerouslySetInnerHTML={{ __html: gist }} />
</div>
</div>
)
}
export default PatternJson

View file

@ -1,27 +1,30 @@
import React from "react";
import PropTypes from "prop-types";
import SampleConfigurator from "../../SampleConfigurator";
import svgattrPlugin from "@freesewing/plugin-svgattr";
import { FormattedMessage } from "react-intl";
import React from 'react'
import PropTypes from 'prop-types'
import SampleConfigurator from '../../SampleConfigurator'
import svgattrPlugin from '@freesewing/plugin-svgattr'
import { FormattedMessage } from 'react-intl'
const SamplePattern = props => {
const SamplePattern = (props) => {
let pattern = new props.Pattern(props.gist.settings).use(svgattrPlugin, {
class: "freesewing draft"
});
class: 'freesewing draft'
})
try {
pattern.sample();
pattern.sample()
} catch (err) {
console.log(err);
console.log(err)
}
return (
<div className="fs-sa">
<section>
<h2>
<FormattedMessage id="app.pattern" />
</h2>
<div dangerouslySetInnerHTML={{ __html: pattern.render() }} />
<h2>gist</h2>
<pre>{JSON.stringify(props.gist, null, 2)}</pre>
<div style={{ padding: '1rem' }}>
<div className="gatsby-highlight">
<pre
className="language-json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(props.gist, null, 2) }}
/>
</div>
</div>
</section>
<aside>
@ -37,8 +40,8 @@ const SamplePattern = props => {
</div>
</aside>
</div>
);
};
)
}
SamplePattern.propTypes = {
gist: PropTypes.object.isRequired,
@ -46,12 +49,12 @@ SamplePattern.propTypes = {
config: PropTypes.object.isRequired,
raiseEvent: PropTypes.func.isRequired,
Pattern: PropTypes.func.isRequired,
units: PropTypes.oneOf(["metric", "imperial"])
};
units: PropTypes.oneOf(['metric', 'imperial'])
}
SamplePattern.defaultProps = {
units: "metric",
units: 'metric',
pointInfo: null
};
}
export default SamplePattern;
export default SamplePattern

View file

@ -0,0 +1,158 @@
import React, { useState, useRef, useEffect } from 'react'
import Draft from '../../Draft'
import IconButton from '@material-ui/core/IconButton'
import ZoomIcon from '@material-ui/icons/Cancel'
const Zoombox = (props) => {
const [from, setFrom] = useState(false)
const [to, setTo] = useState(false)
const [dragging, setDragging] = useState(false)
const [factor, setFactor] = useState(1)
const [box, setBox] = useState(false)
const [panning, setPanning] = useState(false)
const [falseAlarm, setFalseAlarm] = useState(false)
const [panFrom, setPanFrom] = useState(false)
const ref = useRef(null)
useEffect(() => {
let box = ref.current.getBoundingClientRect()
setBox(box)
setFactor(props.patternProps.width / box.width)
}, [])
const resetZoom = (evt) => {
evt.stopPropagation()
evt.preventDefault()
setFrom(false)
setTo(false)
setDragging(false)
props.setViewBox(false)
}
const startPan = (evt) => {
if (!dragging && !panning) {
evt.stopPropagation()
evt.preventDefault()
setPanning(true)
setPanFrom([evt.clientX, evt.clientY])
}
}
const endPan = (evt) => {
if (!dragging && panning) {
evt.stopPropagation()
evt.preventDefault()
setPanning(false)
setPanFrom(false)
updateViewBox(evt)
//props.setViewBox(`${from[0] * factor} ${from[1] * factor} ${to[0] * factor} ${to[1] * factor}`)
}
}
const handlePan = (evt) => {
if (!dragging && panning) {
evt.stopPropagation()
evt.preventDefault()
let x, y
if (from[0] + (evt.clientX - panFrom[0]) <= -5) {
// Bump into left
} else if (from[1] + (evt.clientY - panFrom[1]) <= -5) {
// Bump into top
} else if (to[0] + (evt.clientX - panFrom[0]) >= box.width - 11) {
// Bump into right
} else if (to[1] + (evt.clientY - panFrom[1]) >= box.height - 11) {
// Bump into bottom
} else {
setPanFrom([evt.clientX, evt.clientY])
setFrom([from[0] + (evt.clientX - panFrom[0]), from[1] + (evt.clientY - panFrom[1])])
setTo([to[0] + (evt.clientX - panFrom[0]), to[1] + (evt.clientY - panFrom[1])])
}
}
}
const handleMouseDown = (evt) => {
evt.stopPropagation()
evt.preventDefault()
setFrom([evt.clientX - box.x, evt.clientY - box.y])
setTo([evt.clientX - box.x, evt.clientY - box.y])
setDragging(1)
setPanning(false)
}
const handleMouseUp = (evt) => {
if (dragging == 2) {
updateViewBox(evt)
if (falseAlarm) setFalseAlarm(false)
} else setFalseAlarm(true)
setDragging(false)
setPanning(false)
evt.stopPropagation()
evt.preventDefault()
}
const handleMouseMove = (evt) => {
if (dragging) {
evt.stopPropagation()
evt.preventDefault()
if (dragging === 1) setDragging(2)
if (falseAlarm) setFalseAlarm(false)
setTo([evt.clientX - box.x, evt.clientY - box.y])
}
}
const handleMouseOver = (evt) => {
evt.stopPropagation()
evt.preventDefault()
setFactor(props.patternProps.width / box.width)
}
const updateViewBox = (evt) => {
props.setViewBox(
from[0] * factor +
' ' +
from[1] * factor +
' ' +
(evt.clientX - box.x - from[0]) * factor +
' ' +
(evt.clientY - box.y - from[1]) * factor
)
}
return (
<>
<div
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseOver={handleMouseOver}
onMouseMove={handleMouseMove}
className="zoombox"
ref={ref}
>
<Draft {...props.patternProps} />
<div className="mask" />
{box && from && to && dragging !== 1 && !falseAlarm && (
<div
className={'box' + (dragging ? ' active' : ' inactive')}
style={{
// Remove 16px because of the close icon
width: to[0] - from[0] - 16 + 'px',
height: to[1] - from[1] - 16 + 'px',
left: from[0] + 'px',
top: from[1] + 'px'
}}
onMouseDown={startPan}
onMouseUp={endPan}
onMouseMove={handlePan}
>
{!dragging && (
<IconButton
size="small"
color="primary"
className="close"
onMouseDown={resetZoom}
onMouseUp={resetZoom}
>
<ZoomIcon />
</IconButton>
)}
</div>
)}
</div>
<pre>{false && JSON.stringify({ from, to, panFrom }, null, 2)}</pre>
</>
)
}
export default Zoombox

View file

@ -11,12 +11,13 @@ import LanguageIcon from '@material-ui/icons/Translate'
import DarkModeIcon from '@material-ui/icons/Brightness3'
import LanguageChooser from './LanguageChooser'
import DraftPattern from './DraftPattern'
import Json from './Json'
import SamplePattern from './SamplePattern'
import Welcome from './Welcome'
import Footer from '../Footer'
import Measurements from './Measurements'
const Workbench = props => {
const Workbench = (props) => {
const [display, setDisplay] = useState(null)
const [pattern, setPattern] = useState(false)
const [theme, setTheme] = useState('light')
@ -39,12 +40,12 @@ const Workbench = props => {
}, [props.language])
const getDisplay = () => storage.get(props.config.name + '-display')
const saveDisplay = d => {
const saveDisplay = (d) => {
setDisplay(d)
storage.set(props.config.name + '-display', d)
}
const getMeasurements = () => storage.get(props.config.name + '-measurements')
const saveMeasurements = data => {
const saveMeasurements = (data) => {
storage.set(props.config.name + '-measurements', data)
props.updateGist(data, 'settings', 'measurements')
}
@ -54,7 +55,7 @@ const Workbench = props => {
setMeasurements(updatedMeasurements)
saveMeasurements(updatedMeasurements)
}
const preloadMeasurements = model => {
const preloadMeasurements = (model) => {
let updatedMeasurements = {
...measurements,
...model
@ -97,6 +98,12 @@ const Workbench = props => {
onClick: () => saveDisplay('measurements'),
text: 'app.measurements',
active: display === 'measurements' ? true : false
},
json: {
type: 'button',
onClick: () => saveDisplay('json'),
text: 'JSON',
active: display === 'json' ? true : false
}
},
right: {
@ -148,6 +155,7 @@ const Workbench = props => {
units={props.units}
svgExport={svgExport}
setSvgExport={setSvgExport}
theme={theme}
/>
)
break
@ -177,6 +185,24 @@ const Workbench = props => {
/>
)
break
case 'json':
main = <Json gist={props.gist} />
break
case 'inspect':
main = (
<InspectPattern
freesewing={props.freesewing}
Pattern={props.Pattern}
config={props.config}
gist={props.gist}
updateGist={props.updateGist}
raiseEvent={raiseEvent}
units={props.units}
svgExport={svgExport}
setSvgExport={setSvgExport}
/>
)
break
default:
main = <Welcome language={props.language} setDisplay={saveDisplay} />
}