1
0
Fork 0

Merge pull request #4227 from eriese/eriese-movable

WIP Feature (Shared) Port Print and Cut Layout to v3 ui
This commit is contained in:
Joost De Cock 2023-06-06 20:52:18 +02:00 committed by GitHub
commit 29d904cd7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1343 additions and 1160 deletions

View file

@ -1,16 +0,0 @@
canvas: Canvas
cut: Cut
cuttingLayout: Suggested Cutting Layout
fabric: Main Fabric
fabricSize: "{length} of {width} wide material"
heavyCanvas: Heavy Canvas
interfacing: Interfacing
lining: Lining
lmhCanvas: Light to Medium Hair Canvas
mirrored: mirrored
onFoldLower: on the fold
onFoldAndBias: folded on the bias
onBias: on the bias
plastic: Plastic
ribbing: Ribbing
edgeOfFabric: Edge of Fabric

View file

@ -1,3 +1,7 @@
import React from 'react'
import React, { forwardRef } from 'react'
export const Group = (props) => <g {...props}>{props.children}</g>
export const Group = forwardRef((props, ref) => (
<g {...props} ref={ref}>
{props.children}
</g>
))

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, { forwardRef } from 'react'
// Components that can be swizzled
import { Svg as DefaultSvg } from './svg.mjs'
import { Defs as DefaultDefs } from './defs.mjs'
@ -10,6 +10,7 @@ import { Snippet as DefaultSnippet } from './snippet.mjs'
import { Path as DefaultPath } from './path.mjs'
import { Grid as DefaultGrid } from './grid.mjs'
import { Text as DefaultText, TextOnPath as DefaultTextOnPath } from './text.mjs'
import { Circle as DefaultCircle } from './circle.mjs'
/*
* Allow people to swizzle these components
@ -26,55 +27,60 @@ const defaultComponents = {
Grid: DefaultGrid,
Text: DefaultText,
TextOnPath: DefaultTextOnPath,
Circle: DefaultCircle,
}
export const Pattern = ({
renderProps = false,
t = (string) => string,
components = {},
children = false,
className = 'freesewing pattern',
ref = false,
}) => {
if (!renderProps) return null
export const Pattern = forwardRef(
(
{
renderProps = false,
t = (string) => string,
components = {},
children = false,
className = 'freesewing pattern',
},
ref
) => {
if (!renderProps) return null
// Merge default and swizzled components
components = {
...defaultComponents,
...components,
// Merge default and swizzled components
components = {
...defaultComponents,
...components,
}
const { Svg, Defs, Stack, Group } = components
const optionalProps = {}
if (className) optionalProps.className = className
return (
<Svg
viewBox={`0 0 ${renderProps.width} ${renderProps.height}`}
embed={renderProps.settings.embed}
{...renderProps}
{...optionalProps}
ref={ref}
>
<Defs {...renderProps} />
<style>{`:root { --pattern-scale: ${renderProps.settings.scale || 1}} ${
renderProps.svg.style
}`}</style>
<Group>
{children
? children
: Object.keys(renderProps.stacks).map((stackName) => (
<Stack
key={stackName}
stackName={stackName}
stack={renderProps.stacks[stackName]}
settings={renderProps.settings}
components={components}
t={t}
/>
))}
</Group>
</Svg>
)
}
const { Svg, Defs, Stack, Group } = components
const optionalProps = {}
if (ref) optionalProps.ref = ref
if (className) optionalProps.className = className
return (
<Svg
viewBox={`0 0 ${renderProps.width} ${renderProps.height}`}
embed={renderProps.settings.embed}
{...renderProps}
{...optionalProps}
>
<Defs {...renderProps} />
<style>{`:root { --pattern-scale: ${renderProps.settings.scale || 1}} ${
renderProps.svg.style
}`}</style>
<Group>
{children
? children
: Object.keys(renderProps.stacks).map((stackName) => (
<Stack
key={stackName}
stackName={stackName}
stack={renderProps.stacks[stackName]}
settings={renderProps.settings}
components={components}
t={t}
/>
))}
</Group>
</Svg>
)
}
)

View file

@ -13,7 +13,7 @@ export const PartInner = forwardRef(
path={part.paths[pathName]}
topLeft={part.topLeft}
bottomRight={part.bottomRight}
units={settings.units}
units={settings[0].units}
{...{ stackName, partName, pathName, part, settings, components, t }}
/>
))}

View file

@ -2,39 +2,36 @@ import Worker from 'web-worker'
import fileSaver from 'file-saver'
import { themePlugin } from '@freesewing/plugin-theme'
import { pluginI18n } from '@freesewing/plugin-i18n'
import { pagesPlugin, fabricPlugin } from '../layout/plugin-layout-part.mjs'
import { pagesPlugin, materialPlugin } from 'shared/plugins/plugin-layout-part.mjs'
import { pluginAnnotations } from '@freesewing/plugin-annotations'
import { cutLayoutPlugin } from '../layout/cut/plugin-cut-layout.mjs'
import { fabricSettingsOrDefault } from '../layout/cut/index.mjs'
import { useFabricLength } from '../layout/cut/settings.mjs'
import { cutLayoutPlugin } from 'shared/plugins/plugin-cut-layout.mjs'
import { materialSettingsOrDefault } from 'shared/components/workbench/views/cut/hooks.mjs'
import { useMaterialLength } from 'shared/components/workbench/views/cut/hooks.mjs'
import { capitalize, formatMm } from 'shared/utils.mjs'
import {
defaultPrintSettings,
printSettingsPath,
} from 'shared/components/workbench/views/print/config.mjs'
import get from 'lodash.get'
export const ns = ['cut', 'plugin', 'common']
export const exportTypes = {
exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'],
exportForEditing: ['svg', 'pdf'],
exportAsData: ['json', 'yaml', 'github gist'],
}
export const defaultPdfSettings = {
size: 'a4',
orientation: 'portrait',
margin: 10,
coverPage: true,
cutlist: true,
}
/**
* Instantiate a pattern that uses plugins theme, i18n, and cutlist
* @param {Design} design the design to construct the pattern from
* @param {Object} gist the gist
* @param {Object} overwrite settings to overwrite gist settings with
* @param {Design} Design the design to construct the pattern from
* @param {Object} settings the settings
* @param {Object} overwrite settings to overwrite settings settings with
* @param {string} format the export format this pattern will be prepared for
* @param {function} t the i18n function
* @return {Pattern} a pattern
*/
const themedPattern = (design, gist, overwrite, format, t) => {
const pattern = new design({ ...gist, ...overwrite })
const themedPattern = (Design, settings, overwrite, format, t) => {
const pattern = new Design({ ...settings, ...overwrite })
// add the theme and translation to the pattern
pattern.use(themePlugin, { stripped: format !== 'svg', skipGrid: ['pages'] })
@ -48,42 +45,42 @@ const themedPattern = (design, gist, overwrite, format, t) => {
* Generate svgs of all cutting layouts for the pattern
* @param {Pattern} pattern the pattern to generate cutting layouts for
* @param {Design} design the design constructor for the pattern
* @param {Object} gist the gist
* @param {Object} settings the settings
* @param {string} format the export format this pattern will be prepared for
* @param {function} t the i18n function
* @return {Object} a dictionary of svgs and related translation strings, keyed by fabric
* @return {Object} a dictionary of svgs and related translation strings, keyed by material
*/
const generateCutLayouts = (pattern, design, gist, format, t) => {
// get the fabrics from the already drafted base pattern
const fabrics = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics(
const generateCutLayouts = (pattern, Design, settings, format, t, ui) => {
// get the materials from the already drafted base pattern
const materials = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics(
pattern.settings[0]
) || ['fabric']
if (!fabrics.length) return
if (!materials.length) return
const isImperial = gist.units === 'imperial'
const isImperial = settings.units === 'imperial'
const cutLayouts = {}
// each fabric
fabrics.forEach((f) => {
// get the settings and layout for that fabric
const fabricSettings = fabricSettingsOrDefault(gist, f)
const fabricLayout = get(gist, ['layouts', 'cuttingLayout', f], true)
// each material
materials.forEach((f) => {
// get the settings and layout for that material
const materialSettings = materialSettingsOrDefault(settings.units, ui, f)
const materialLayout = get(ui, ['layouts', 'cut', f], true)
// make a new pattern
const fabricPattern = themedPattern(design, gist, { layout: fabricLayout }, format, t)
// add cut layout plugin and fabric plugin
.use(cutLayoutPlugin(f, fabricSettings.grainDirection))
.use(fabricPlugin({ ...fabricSettings, printStyle: true, setPatternSize: 'width' }))
const materialPattern = themedPattern(Design, settings, { layout: materialLayout }, format, t)
// add cut layout plugin and material plugin
.use(cutLayoutPlugin(f, materialSettings.grainDirection))
.use(materialPlugin({ ...materialSettings, printStyle: true, setPatternSize: 'width' }))
// draft and render
fabricPattern.draft()
const svg = fabricPattern.render()
materialPattern.draft()
const svg = materialPattern.render()
// include translations
cutLayouts[f] = {
svg,
title: t('plugin:' + f),
dimensions: t('plugin:fabricSize', {
width: formatMm(fabricSettings.sheetWidth, gist.units, 'notags'),
length: useFabricLength(isImperial, fabricPattern.height, 'notags'),
title: t('cut:' + f),
dimensions: t('cut:materialSize', {
width: formatMm(materialSettings.sheetWidth, settings.units, 'notags'),
length: useMaterialLength(isImperial, materialPattern.height, 'notags'),
interpolation: { escapeValue: false },
}),
}
@ -92,18 +89,28 @@ const generateCutLayouts = (pattern, design, gist, format, t) => {
return cutLayouts
}
/**
* Handle exporting the draft or gist
* Handle exporting the draft or settings
* format: format to export to
* gist: the gist
* design: the pattern constructor for the design to be exported
* settings: the settings
* Design: the pattern constructor for the design to be exported
* t: a translation function to attach to the draft
* app: an app instance
* onComplete: business to perform after a successful export
* onError: business to perform on error
* */
export const handleExport = async (format, gist, design, t, app, onComplete, onError) => {
export const handleExport = async ({
format,
settings,
Design,
design,
t,
startLoading,
stopLoading,
onComplete,
onError,
ui,
}) => {
// start the loading indicator
app.startLoading()
if (typeof startLoading === 'function') startLoading()
// get a worker going
const worker = new Worker(new URL('./export-worker.js', import.meta.url), { type: 'module' })
@ -115,7 +122,7 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
// save it out
if (e.data.blob) {
const fileType = exportTypes.exportForPrinting.indexOf(format) === -1 ? format : 'pdf'
fileSaver.saveAs(e.data.blob, `freesewing-${gist.design || 'gist'}.${fileType}`)
fileSaver.saveAs(e.data.blob, `freesewing-${design || 'pattern'}.${fileType}`)
}
// do additional business
onComplete && onComplete(e)
@ -127,29 +134,29 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
}
// stop the loader
app.stopLoading()
if (typeof stopLoading === 'function') stopLoading()
})
// pdf settings
const settings = {
...defaultPdfSettings,
...(gist._state.layout?.forPrinting?.page || {}),
const pageSettings = {
...defaultPrintSettings(settings.units),
...get(ui, printSettingsPath, {}),
}
// arguments to pass to the worker
const workerArgs = { format, gist, settings }
const workerArgs = { format, settings, pageSettings }
// data passed to the worker must be JSON serializable, so we can't pass functions or prototypes
// that means if it's not a data export there's more work to do before we can hand off to the worker
if (exportTypes.exportAsData.indexOf(format) === -1) {
gist.embed = false
settings.embed = false
// make a pattern instance for export rendering
const layout = gist.layouts?.printingLayout || gist.layout || true
let pattern = themedPattern(design, gist, { layout }, format, t)
const layout = settings.layout || ui.layouts?.print || true
let pattern = themedPattern(Design, settings, { layout }, format, t)
// a specified size should override the gist one
// a specified size should override the settings one
if (format !== 'pdf') {
settings.size = format
pageSettings.size = format
}
try {
@ -157,7 +164,7 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
if (format !== 'svg') {
pattern.use(
pagesPlugin({
...settings,
...pageSettings,
printStyle: true,
renderBlanks: false,
setPatternSize: true,
@ -166,10 +173,10 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
// add the strings that are used on the cover page
workerArgs.strings = {
design: capitalize(pattern.designConfig.data.name.replace('@freesewing/', '')),
design: capitalize(design),
tagline: t('common:sloganCome') + '. ' + t('common:sloganStay'),
url: window.location.href,
cuttingLayout: t('plugin:cuttingLayout'),
cuttingLayout: t('cut:cuttingLayout'),
}
}
@ -181,15 +188,15 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
workerArgs.pages = pattern.setStores[pattern.activeSet].get('pages')
// add cutting layouts if requested
if (format !== 'svg' && settings.cutlist) {
workerArgs.cutLayouts = generateCutLayouts(pattern, design, gist, format, t)
if (format !== 'svg' && pageSettings.cutlist) {
workerArgs.cutLayouts = generateCutLayouts(pattern, Design, settings, format, t, ui)
}
// post a message to the worker with all needed data
worker.postMessage(workerArgs)
} catch (err) {
console.log(err)
app.stopLoading()
if (typeof stopLoading === 'function') stopLoading()
onError && onError(err)
}
}

View file

@ -7,16 +7,21 @@ import { PdfMaker } from './pdf-maker'
/** when the worker receives data from the page, do the appropriate export */
addEventListener('message', async (e) => {
const { format, gist, svg } = e.data
const { format, settings, svg } = e.data
// handle export by type
try {
if (format === 'json') return exportJson(gist)
if (format === 'yaml') return exportYaml(gist)
if (format === 'github gist') return exportGithubGist(gist)
if (format === 'svg') return exportSvg(svg)
await exportPdf(e.data)
switch (format) {
case 'json':
return exportJson(settings)
case 'yaml':
return exportYaml(settings)
case 'github gist':
return exportGithubGist(settings)
case 'svg':
return exportSvg(svg)
default:
return await exportPdf(e.data)
}
} catch (e) {
postMessage({ success: false, error: e })
close()
@ -37,9 +42,9 @@ const exportBlob = (blobContent, type) => {
postSuccess(blob)
}
const exportJson = (gist) => exportBlob(JSON.stringify(gist, null, 2), 'application/json')
const exportJson = (settings) => exportBlob(JSON.stringify(settings, null, 2), 'application/json')
const exportYaml = (gist) => exportBlob(yaml.dump(gist), 'application/x-yaml')
const exportYaml = (settings) => exportBlob(yaml.dump(settings), 'application/x-yaml')
const exportSvg = (svg) => exportBlob(svg, 'image/svg+xml')

View file

@ -14,23 +14,23 @@ export const ExportDraft = ({ gist, design, app }) => {
setLink(false)
setError(false)
setFormat(format)
handleExport(
format,
gist,
design,
t,
app,
(e) => {
if (e.data.link) {
setLink(e.data.link)
}
},
(e) => {
if (e.data?.error) {
setError(true)
}
}
)
// handleExport(
// format,
// gist,
// design,
// t,
// app,
// (e) => {
// if (e.data.link) {
// setLink(e.data.link)
// }
// },
// (e) => {
// if (e.data?.error) {
// setError(true)
// }
// }
// )
}
return (

View file

@ -22,7 +22,7 @@ export class PdfMaker {
/** the svg as text to embed in the pdf */
svg
/** the document configuration */
settings
pageSettings
/** the pdfKit instance that is writing the document */
pdf
/** the export buffer to hold pdfKit output */
@ -51,8 +51,8 @@ export class PdfMaker {
pageCount = 0
lineLevel = 50
constructor({ svg, settings, pages, strings, cutLayouts }) {
this.settings = settings
constructor({ svg, pageSettings, pages, strings, cutLayouts }) {
this.pageSettings = pageSettings
this.pagesWithContent = pages.withContent
this.svg = svg
this.strings = strings
@ -60,7 +60,7 @@ export class PdfMaker {
this.initPdf()
this.margin = this.settings.margin * mmToPoints // margin is in mm because it comes from us, so we convert it to points
this.margin = this.pageSettings.margin * mmToPoints // margin is in mm because it comes from us, so we convert it to points
this.pageHeight = this.pdf.page.height - this.margin * 2 // this is in points because it comes from pdfKit
this.pageWidth = this.pdf.page.width - this.margin * 2 // this is in points because it comes from pdfKit
@ -77,8 +77,8 @@ export class PdfMaker {
initPdf() {
// instantiate with the correct size and orientation
this.pdf = new PDFDocument({
size: this.settings.size.toUpperCase(),
layout: this.settings.orientation,
size: this.pageSettings.size.toUpperCase(),
layout: this.pageSettings.orientation,
})
// PdfKit wants to flush the buffer on each new page.
@ -117,7 +117,7 @@ export class PdfMaker {
/** generate the cover page for the pdf */
async generateCoverPage() {
// don't make one if it's not requested
if (!this.settings.coverPage) {
if (!this.pageSettings.coverPage) {
return
}
@ -170,8 +170,8 @@ export class PdfMaker {
}
/** generate the title for a cutting layout page */
async generateCutLayoutTitle(fabricTitle, fabricDimensions) {
this.addText(this.strings.cuttingLayout, 12, 2).addText(fabricTitle, 28)
async generateCutLayoutTitle(materialTitle, materialDimensions) {
this.addText(this.strings.cuttingLayout, 12, 2).addText(materialTitle, 28)
this.pdf.lineWidth(1)
this.pdf
@ -180,16 +180,16 @@ export class PdfMaker {
.stroke()
this.lineLevel += 5
this.addText(fabricDimensions, 16)
this.addText(materialDimensions, 16)
}
/** generate all cutting layout pages */
async generateCutLayoutPages() {
if (!this.settings.cutlist || !this.cutLayouts) return
if (!this.pageSettings.cutlist || !this.cutLayouts) return
for (const fabric in this.cutLayouts) {
for (const material in this.cutLayouts) {
this.nextPage()
const { title, dimensions, svg } = this.cutLayouts[fabric]
const { title, dimensions, svg } = this.cutLayouts[material]
await this.generateCutLayoutTitle(title, dimensions)
await this.generateSvgPage(svg)
}

View file

@ -14,16 +14,24 @@ import { ModalSpinner } from 'shared/components/modal/spinner.mjs'
// Views
import { DraftView, ns as draftNs } from 'shared/components/workbench/views/draft/index.mjs'
import { SaveView, ns as saveNs } from 'shared/components/workbench/views/save/index.mjs'
import { PrintView, ns as printNs } from 'shared/components/workbench/views/print/index.mjs'
import { CutView, ns as cutNs } from 'shared/components/workbench/views/cut/index.mjs'
export const ns = ['account', 'workbench', ...draftNs, ...saveNs]
export const ns = ['account', 'workbench', ...draftNs, ...saveNs, ...printNs, ...cutNs]
const defaultUi = {
renderer: 'react',
}
const views = {
draft: DraftView,
print: PrintView,
cut: CutView,
}
const draftViews = ['draft', 'test']
export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from, set }) => {
export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) => {
// Hooks
const { t, i18n } = useTranslation(ns)
const { language } = i18n
@ -69,37 +77,44 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from, set
ui,
language,
DynamicDocs,
Design,
}
let viewContent = null
// Draft view
if (view === 'draft') {
// Generate the pattern here so we can pass it down to both the view and the options menu
const pattern =
settings.measurements && draftViews.includes(view) ? new Design(settings) : false
switch (view) {
// Save view
case 'save':
viewContent = <SaveView {...viewProps} from={from} />
break
default: {
const layout = ui.layouts?.[view] || settings.layout || true
// Generate the pattern here so we can pass it down to both the view and the options menu
const pattern = settings.measurements !== undefined && new Design({ layout, ...settings })
// Return early if the pattern is not initialized yet
if (typeof pattern.getConfig !== 'function') return null
// Return early if the pattern is not initialized yet
if (typeof pattern.getConfig !== 'function') return null
const patternConfig = pattern.getConfig()
if (ui.renderer === 'svg') {
// Add theme to svg renderer
pattern.use(pluginI18n, { t })
pattern.use(pluginTheme, { skipGrid: ['pages'] })
const patternConfig = pattern.getConfig()
if (ui.renderer === 'svg') {
// Add theme to svg renderer
pattern.use(pluginI18n, { t })
pattern.use(pluginTheme, { skipGrid: ['pages'] })
}
if (draftViews.includes(view)) {
// Draft the pattern or die trying
try {
pattern.draft()
} catch (error) {
console.log(error)
setError(<ErrorView>{JSON.stringify(error)}</ErrorView>)
}
}
const View = views[view]
viewContent = <View {...{ ...viewProps, pattern, patternConfig }} />
}
// Draft the pattern or die trying
try {
pattern.draft()
} catch (error) {
console.log(error)
setError(<ErrorView>{JSON.stringify(error)}</ErrorView>)
}
viewContent = <DraftView {...viewProps} {...{ pattern, patternConfig }} />
}
// Save view
else if (view === 'save') viewContent = <SaveView {...viewProps} from={from} />
return (
<>
<WorkbenchHeader {...{ view, setView, update }} />

View file

@ -1,126 +0,0 @@
import { useTranslation } from 'next-i18next'
import { CutLayoutSettings } from './settings.mjs'
import { Draft } from '../draft/index.mjs'
import { fabricPlugin } from '../plugin-layout-part.mjs'
import { cutLayoutPlugin } from './plugin-cut-layout.mjs'
import { pluginAnnotations } from '@freesewing/plugin-annotations'
import { measurementAsMm } from 'shared/utils.mjs'
import { useEffect } from 'react'
import get from 'lodash.get'
export const fabricSettingsOrDefault = (gist, fabric) => {
const isImperial = gist.units === 'imperial'
const sheetHeight = measurementAsMm(isImperial ? 36 : 100, gist.units)
const gistSettings = get(gist, ['_state', 'layout', 'forCutting', 'fabric', fabric])
const sheetWidth = gistSettings?.sheetWidth || measurementAsMm(isImperial ? 54 : 120, gist.units)
const grainDirection =
gistSettings?.grainDirection === undefined ? 90 : gistSettings.grainDirection
return { activeFabric: fabric, sheetWidth, grainDirection, sheetHeight }
}
const activeFabricPath = ['_state', 'layout', 'forCutting', 'activeFabric']
const useFabricSettings = (gist) => {
const activeFabric = get(gist, activeFabricPath) || 'fabric'
return fabricSettingsOrDefault(gist, activeFabric)
}
const useFabricDraft = (gist, design, fabricSettings) => {
// get the appropriate layout for the view
const layout =
get(gist, ['layouts', gist._state.view, fabricSettings.activeFabric]) || gist.layout || true
// hand it separately to the design
const draft = new design({ ...gist, layout })
const layoutSettings = {
sheetWidth: fabricSettings.sheetWidth,
sheetHeight: fabricSettings.sheetHeight,
}
let patternProps
try {
// add the fabric plugin to the draft
draft.use(fabricPlugin(layoutSettings))
// add the cutLayout plugin
draft.use(cutLayoutPlugin(fabricSettings.activeFabric, fabricSettings.grainDirection))
// also, pluginAnnotations and pluginFlip are needed
draft.use(pluginAnnotations)
// draft the pattern
draft.draft()
patternProps = draft.getRenderProps()
} catch (err) {
console.log(err, gist)
}
return { draft, patternProps }
}
const useFabricList = (draft) => {
return draft.setStores[0].cutlist.getCutFabrics(draft.settings[0])
}
const bgProps = { fill: 'none' }
export const CutLayout = (props) => {
const { t } = useTranslation(['workbench', 'plugin'])
const { gist, design, updateGist } = props
// disable xray
useEffect(() => {
if (gist?._state?.xray?.enabled) updateGist(['_state', 'xray', 'enabled'], false)
})
const fabricSettings = useFabricSettings(gist)
const { draft, patternProps } = useFabricDraft(gist, design, fabricSettings)
const fabricList = useFabricList(draft)
const setCutFabric = (newFabric) => {
updateGist(activeFabricPath, newFabric)
}
let name = design.designConfig.data.name
name = name.replace('@freesewing/', '')
const settingsProps = {
gist,
updateGist,
patternProps,
unsetGist: props.unsetGist,
...fabricSettings,
}
return patternProps ? (
<div>
<h2 className="capitalize">{t('layoutThing', { thing: name }) + ': ' + t('forCutting')}</h2>
<CutLayoutSettings {...settingsProps} />
<div className="my-4">
{fabricList.length > 1 ? (
<div className="tabs">
{fabricList.map((title) => (
<button
key={title}
className={`text-xl font-bold capitalize tab tab-bordered grow ${
fabricSettings.activeFabric === title ? 'tab-active' : ''
}`}
onClick={() => setCutFabric(title)}
>
{t('plugin:' + title)}
</button>
))}
</div>
) : null}
<Draft
draft={draft}
gist={gist}
updateGist={updateGist}
patternProps={patternProps}
bgProps={bgProps}
gistReady={props.gistReady}
layoutPart="fabric"
layoutType={['cuttingLayout', fabricSettings.activeFabric]}
layoutSetType="forCutting"
/>
</div>
</div>
) : null
}

View file

@ -1,139 +0,0 @@
import { ClearIcon, IconWrapper } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
import { formatFraction128, measurementAsMm, round, formatMm } from 'shared/utils.mjs'
import { ShowButtonsToggle } from '../draft/buttons.mjs'
const SheetIcon = (props) => (
<IconWrapper {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z"
/>
</IconWrapper>
)
const GrainIcon = (props) => (
<IconWrapper {...props}>
<g>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 17l-5-5 5-5" />
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12l8 0" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 7l5 5-5 5" />
</g>
</IconWrapper>
)
const FabricSizer = ({ gist, updateGist, activeFabric, sheetWidth }) => {
const { t } = useTranslation(['workbench'])
let val = formatMm(sheetWidth, gist.units, 'none')
// onChange
const update = (evt) => {
evt.stopPropagation()
let evtVal = evt.target.value
// set Val immediately so that the input reflects it
val = evtVal
let useVal = measurementAsMm(evtVal, gist.units)
// only set to the gist if it's valid
if (!isNaN(useVal)) {
updateGist(['_state', 'layout', 'forCutting', 'fabric', activeFabric, 'sheetWidth'], useVal)
}
}
return (
<label className="input-group">
<span className="label-text font-bold">{`${t(activeFabric)} ${t('width')}`}</span>
<input
key="input-fabricWidth"
type="text"
className="input input-bordered grow text-base-content border-r-0 w-20"
value={val}
onChange={update}
/>
<span className="bg-transparent border input-bordered">
{gist.units == 'metric' ? 'cm' : 'in'}
</span>
</label>
)
}
export const GrainDirectionPicker = ({ grainDirection, activeFabric, updateGist }) => {
const { t } = useTranslation(['workbench'])
return (
<button
className={`
btn btn-primary flex flex-row gap-2 items-center
hover:text-primary-content ml-4
`}
onClick={() =>
updateGist(
['_state', 'layout', 'forCutting', 'fabric', activeFabric, 'grainDirection'],
grainDirection === 0 ? 90 : 0
)
}
>
<GrainIcon className={`h-6 w-6 mr-2 ${grainDirection === 0 ? '' : 'rotate-90'}`} />
<span>{t(`grainDirection`)}</span>
</button>
)
}
export const useFabricLength = (isImperial, height, format = 'none') => {
// regular conversion from mm to inches or cm
const unit = isImperial ? 25.4 : 10
// conversion from inches or cm to yards or meters
const fabricUnit = isImperial ? 36 : 100
// for fabric, these divisions are granular enough
const rounder = isImperial ? 16 : 10
// we convert the used fabric height to the right units so we can round it
const inFabricUnits = height / (fabricUnit * unit)
// we multiply it by the rounder, round it up, then divide by the rounder again to get the rounded amount
const roundCount = Math.ceil(rounder * inFabricUnits) / rounder
// format as a fraction for imperial, a decimal for metric
const count = isImperial ? formatFraction128(roundCount, format) : round(roundCount, 1)
return `${count}${isImperial ? 'yds' : 'm'}`
}
export const CutLayoutSettings = ({
gist,
patternProps,
unsetGist,
updateGist,
activeFabric,
sheetWidth,
grainDirection,
}) => {
const { t } = useTranslation(['workbench'])
const fabricLength = useFabricLength(gist.units === 'imperial', patternProps.height)
return (
<div className="flex flex-row justify-between mb-2 items-center">
<div className="flex">
<FabricSizer {...{ gist, updateGist, activeFabric, sheetWidth }} />
<GrainDirectionPicker {...{ updateGist, activeFabric, grainDirection }} />
</div>
<div>
<SheetIcon className="h-6 w-6 mr-2 inline align-middle" />
<span className="text-xl font-bold align-middle">{fabricLength}</span>
</div>
<div className="flex">
<ShowButtonsToggle
gist={gist}
updateGist={updateGist}
layoutSetType="forCutting"
></ShowButtonsToggle>
<button
key="reset"
onClick={() => unsetGist(['layouts', 'cuttingLayout', activeFabric])}
className="btn btn-primary btn-outline ml-4"
>
<ClearIcon className="h-6 w-6 mr-2" />
{t('reset')}
</button>
</div>
</div>
)
}

View file

@ -1,116 +0,0 @@
import { useRef } from 'react'
import { Stack } from './stack.mjs'
import { SvgWrapper } from '../../pattern/svg.mjs'
import { PartInner } from '../../pattern/part.mjs'
import get from 'lodash.get'
export const Draft = (props) => {
const {
patternProps,
gist,
updateGist,
app,
bgProps = {},
fitLayoutPart = false,
layoutType = 'printingLayout',
layoutSetType = 'forPrinting',
} = props
const svgRef = useRef(null)
if (!patternProps) return null
// keep a fresh copy of the layout because we might manipulate it without saving to the gist
const layoutPath = ['layouts'].concat(layoutType)
let layout = get(patternProps.settings[0], layoutPath) || {
...patternProps.autoLayout,
width: patternProps.width,
height: patternProps.height,
}
// Helper method to update part layout and re-calculate width * height
const updateLayout = (name, config, history = true) => {
// Start creating new layout
const newLayout = { ...layout }
newLayout.stacks[name] = config
// Pattern topLeft and bottomRight
let topLeft = { x: 0, y: 0 }
let bottomRight = { x: 0, y: 0 }
for (const pname in patternProps.stacks) {
if (pname == props.layoutPart && !fitLayoutPart) continue
let partLayout = newLayout.stacks[pname]
// Pages part does not have its topLeft and bottomRight set by core since it's added post-draft
if (partLayout?.tl) {
// set the pattern extremes
topLeft.x = Math.min(topLeft.x, partLayout.tl.x)
topLeft.y = Math.min(topLeft.y, partLayout.tl.y)
bottomRight.x = Math.max(bottomRight.x, partLayout.br.x)
bottomRight.y = Math.max(bottomRight.y, partLayout.br.y)
}
}
newLayout.width = bottomRight.x - topLeft.x
newLayout.height = bottomRight.y - topLeft.y
newLayout.bottomRight = bottomRight
newLayout.topLeft = topLeft
if (history) {
updateGist(layoutPath, newLayout, history)
} else {
// we don't put it in the gist if it shouldn't contribute to history because we need some of the data calculated here for rendering purposes on the initial layout, but we don't want to actually save a layout until the user manipulates it. This is what allows the layout to respond appropriately to settings changes. Once the user has starting playing with the layout, all bets are off
layout = newLayout
}
}
const viewBox = layout.topLeft
? `${layout.topLeft.x} ${layout.topLeft.y} ${layout.width} ${layout.height}`
: false
// 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 stacks = [
<PartInner
{...{
part: patternProps.parts[0][props.layoutPart],
partName: props.layoutPart,
gist,
skipGrid: true,
}}
key={props.layoutPart}
/>,
]
// then make a stack component for each remaining stack
for (var stackName in patternProps.stacks) {
if (stackName === props.layoutPart) {
continue
}
let stack = patternProps.stacks[stackName]
const stackPart = (
<Stack
{...{
key: stackName,
stackName,
stack,
layout,
app,
gist,
updateLayout,
isLayoutPart: stackName === props.layoutPart,
layoutSetType: layoutSetType,
}}
/>
)
stacks.push(stackPart)
}
return (
<SvgWrapper {...{ patternProps, gist, viewBox }} ref={svgRef}>
<rect x="0" y="0" width={layout.width} height={layout.height} {...bgProps} />
{stacks}
</SvgWrapper>
)
}

View file

@ -1,260 +0,0 @@
/*
* This React component is a long way from perfect, but it's a start for
* handling custom layouts.
*
* There are a few 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 update 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.
*
* We run `getScreenCTM().inverse()` on the svg element to pass to `matrixTransform` on a `DOMPointReadOnly` for dom to svg space conversions.
*
* 3) Part-level transforms
*
* All parts use their center as the transform-origin to simplify transforms, especially flipping and rotating.
*
* 4) Bounding box
*
* We use `getBoundingClientRect` rather than `getBBox` because it provides more data and factors in the transforms.
* We then use our `domToSvg` function to move the points back into the SVG space.
*
*
* Known issues
* - currently none
*
* 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 { useRef, useState, useEffect } from 'react'
import { generateStackTransform, getTransformedBounds } from '@freesewing/core'
import { Part } from '../../draft/part.mjs'
import { getProps, angle } from '../../draft/utils.mjs'
import { drag } from 'd3-drag'
import { select } from 'd3-selection'
import { Buttons } from './buttons.mjs'
import get from 'lodash.get'
export const Stack = (props) => {
const { layout, stack, stackName, gist } = props
const stackLayout = layout.stacks?.[stackName]
const stackExists = typeof stackLayout?.move?.x !== 'undefined'
// Use a ref for direct DOM manipulation
const stackRef = useRef(null)
const centerRef = useRef(null)
const innerRef = useRef(null)
// State variable to switch between moving or rotating the part
const [rotate, setRotate] = useState(false)
// update the layout on mount
useEffect(() => {
// only update if there's a rendered part and it's not the pages or fabric part
if (stackRef.current && !props.isLayoutPart) {
updateLayout(false)
}
}, [stackRef, stackLayout])
// Initialize drag handler
useEffect(() => {
// don't drag the pages
if (props.isLayoutPart || !stackExists) return
handleDrag(select(stackRef.current))
}, [rotate, stackRef, stackLayout])
// // Don't just assume this makes sense
if (!stackExists) return null
// 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 = stackLayout.move.x
let translateY = stackLayout.move.y
let stackRotation = stackLayout.rotate || 0
let rotation = stackRotation
let flipX = !!stackLayout.flipX
let flipY = !!stackLayout.flipY
const center = {
x: stack.topLeft.x + (stack.bottomRight.x - stack.topLeft.x) / 2,
y: stack.topLeft.y + (stack.bottomRight.y - stack.topLeft.y) / 2,
}
/** get the delta rotation from the start of the drag event to now */
const getRotation = (event) =>
angle(center, event.subject) - angle(center, { x: event.x, y: event.y })
const setTransforms = () => {
// get the transform attributes
const transforms = generateStackTransform(translateX, translateY, rotation, flipX, flipY, stack)
const me = select(stackRef.current)
me.attr('transform', transforms.join(' '))
return transforms
}
let didDrag = false
const handleDrag = drag()
// subject allows us to save data from the start of the event to use throughout event handing
.subject(function (event) {
return rotate
? // if we're rotating, the subject is the mouse position
{ x: event.x, y: event.y }
: // if we're moving, the subject is the part's x,y coordinates
{ x: translateX, y: translateY }
})
.on('drag', function (event) {
if (!event.dx && !event.dy) return
if (rotate) {
let newRotation = getRotation(event)
// shift key to snap the rotation
if (event.sourceEvent.shiftKey) {
newRotation = Math.ceil(newRotation / 15) * 15
}
// reverse the rotation direction one time per flip. if we're flipped both directions, rotation will be positive again
if (flipX) newRotation *= -1
if (flipY) newRotation *= -1
rotation = stackRotation + newRotation
} else {
translateX = event.x
translateY = event.y
}
// a drag happened, so we should update the layout when we're done
didDrag = true
setTransforms()
})
.on('end', function () {
// save to gist if anything actually changed
if (didDrag) updateLayout()
didDrag = false
})
/** reset the part's transforms */
const resetPart = () => {
rotation = 0
flipX = 0
flipY = 0
updateLayout()
}
/** toggle between dragging and rotating */
const toggleDragRotate = () => {
// only respond if the part should be able to drag/rotate
if (!stackRef.current || props.isLayoutPart) {
return
}
setRotate(!rotate)
}
/** update the layout either locally or in the gist */
const updateLayout = (history = true) => {
/** don't mess with what we don't lay out */
if (!stackRef.current || props.isLayoutPart) return
// set the transforms on the stack in order to calculate from the latest position
const transforms = setTransforms()
// apply the transforms to the bounding box to get the new extents of the stack
const { tl, br } = getTransformedBounds(stack, transforms)
// update it on the draft component
props.updateLayout(
stackName,
{
move: {
x: translateX,
y: translateY,
},
rotate: rotation % 360,
flipX,
flipY,
tl,
br,
},
history
)
}
/** Method to flip (mirror) the part along the X or Y axis */
const flip = (axis) => {
if (axis === 'x') flipX = !flipX
else flipY = !flipY
updateLayout()
}
/** method to rotate 90 degrees */
const rotate90 = (direction = 1) => {
if (flipX) direction *= -1
if (flipY) direction *= -1
rotation += 90 * direction
updateLayout()
}
// don't render if the part is empty
// if (Object.keys(part.snippets).length === 0 && Object.keys(part.paths).length === 0) return null;
const showButtons = get(gist, ['_state', 'layout', props.layoutSetType, 'showButtons'], true)
return (
<g id={`stack-${stackName}`} {...getProps(stack)} ref={stackRef}>
<g id={`stack-inner-${stackName}`} ref={innerRef}>
{stack.parts.map((part) => (
<Part {...{ part, partName: part.name, gist }} key={part.name}></Part>
))}
</g>
{!props.isLayoutPart && (
<>
<text x={center.x} y={center.y} ref={centerRef} />
<rect
x={stack.topLeft.x}
y={stack.topLeft.y}
width={stack.width}
height={stack.height}
className={`layout-rect ${rotate ? 'rotate' : 'move'}`}
id={`${stackName}-layout-rect`}
onClick={toggleDragRotate}
/>
{showButtons ? (
<Buttons
transform={`translate(${center.x}, ${center.y}) rotate(${-rotation}) scale(${
flipX ? -1 : 1
},${flipY ? -1 : 1})`}
flip={flip}
rotate={rotate}
setRotate={setRotate}
resetPart={resetPart}
rotate90={rotate90}
partName={stackName}
/>
) : null}
</>
)}
</g>
)
}

View file

@ -1,78 +0,0 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import { PrintLayoutSettings } from './settings.mjs'
import { Draft } from '../draft/index.mjs'
import { pagesPlugin } from '../plugin-layout-part.mjs'
import {
handleExport,
defaultPdfSettings,
} from 'shared/components/workbench/exporting/export-handler.mjs'
import { Popout } from 'shared/components/popout.mjs'
export const PrintLayout = (props) => {
// disable xray
useEffect(() => {
if (props.gist?._state?.xray?.enabled) props.updateGist(['_state', 'xray', 'enabled'], false)
})
const { t } = useTranslation(['workbench', 'plugin'])
const [error, setError] = useState(false)
const draft = props.draft
// add the pages plugin to the draft
const layoutSettings = {
...defaultPdfSettings,
...props.gist?._state?.layout?.forPrinting?.page,
}
draft.use(pagesPlugin(layoutSettings))
let patternProps
try {
// draft the pattern
draft.draft()
patternProps = draft.getRenderProps()
} catch (err) {
console.log(err, props.gist)
}
const bgProps = { fill: 'none' }
const exportIt = () => {
setError(false)
handleExport(
'pdf',
props.gist,
props.design,
t,
props.app,
() => setError(false),
() => setError(true)
)
}
let name = props.design.designConfig.data.name
name = name.replace('@freesewing/', '')
return (
<div>
<h2 className="capitalize">{t('layoutThing', { thing: name }) + ': ' + t('forPrinting')}</h2>
<div className="m-4">
<PrintLayoutSettings {...{ ...props, exportIt, layoutSettings }} draft={draft} />
{error && (
<Popout warning compact>
<span className="font-bold mr-4 uppercase text-sm">{t('error')}:</span>
{t('somethingWentWrong')}
</Popout>
)}
</div>
<Draft
draft={draft}
gist={props.gist}
updateGist={props.updateGist}
patternProps={patternProps}
bgProps={bgProps}
gistReady={props.gistReady}
layoutPart="pages"
/>
</div>
)
}

View file

@ -1,32 +0,0 @@
import { PageIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
export 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>
)
}

View file

@ -1,58 +0,0 @@
import { PageSizeIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
import { Popout } from 'shared/components/popout.mjs'
const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid']
export 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>
)
}

View file

@ -1,129 +0,0 @@
import { PageSizePicker } from './pagesize-picker.mjs'
import { PageOrientationPicker } from './orientation-picker.mjs'
import { PrintIcon, RightIcon, ClearIcon, ExportIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
import { ShowButtonsToggle } from '../draft/buttons.mjs'
export const PrintLayoutSettings = (props) => {
const { t } = useTranslation(['workbench'])
let pages = props.draft?.setStores[0].get('pages')
if (!pages) return null
const { cols, rows, count } = pages
const setMargin = (evt) => {
props.updateGist(
['_state', 'layout', 'forPrinting', 'page', 'margin'],
parseInt(evt.target.value)
)
}
const setCoverPage = () => {
props.updateGist(
['_state', 'layout', 'forPrinting', 'page', 'coverPage'],
!props.layoutSettings.coverPage
)
}
const setCutlist = () => {
props.updateGist(
['_state', 'layout', 'forPrinting', 'page', 'cutlist'],
!props.layoutSettings.cutlist
)
}
return (
<div>
<div
className="flex flex-row justify-between
mb-2"
>
<div className="flex gap-4">
<PageSizePicker {...props} />
<PageOrientationPicker {...props} />
<button
key="export"
onClick={props.exportIt}
className="btn btn-primary btn-outline"
disabled={count === 0}
aria-disabled={count === 0}
>
<ExportIcon className="h-6 w-6 mr-2" />
{`${t('export')} PDF`}
</button>
</div>
<div className="flex gap-4">
<ShowButtonsToggle
gist={props.gist}
updateGist={props.updateGist}
layoutSetType="forPrinting"
></ShowButtonsToggle>
<button
key="reset"
onClick={() => props.unsetGist(['layouts', 'printingLayout'])}
className="btn btn-primary btn-outline"
>
<ClearIcon className="h-6 w-6 mr-2" />
{t('reset')}
</button>
</div>
</div>
<div className="flex flex-row justify-between">
<div className="flex flex-row">
<label htmlFor="pageMargin" className="label">
<span className="">{t('pageMargin')}</span>
<input
type="range"
max={50}
min={0}
step={1}
onChange={setMargin}
value={props.layoutSettings.margin}
className="range range-sm mx-2"
name="pageMargin"
/>
<div className="text-center">
<span className="text-secondary">{props.layoutSettings.margin}mm</span>
</div>
<button
title={t('reset')}
className="btn btn-ghost btn-xs text-accent mx-2"
disabled={props.layoutSettings.margin == 10}
onClick={() => setMargin({ target: { value: 10 } })}
>
<ClearIcon />
</button>
</label>
<label htmlFor="coverPage" className="label">
<span className="mr-2">{t('coverPage')}</span>
<input
type="checkbox"
className="toggle toggle-primary"
checked={props.layoutSettings.coverPage}
onChange={setCoverPage}
/>
</label>
<label htmlFor="cutlist" className="label">
<span className="mr-2">{t('cutlist')}</span>
<input
type="checkbox"
className="toggle toggle-primary"
checked={props.layoutSettings.cutlist}
onChange={setCutlist}
/>
</label>
</div>
<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>
</div>
)
}

View file

@ -71,7 +71,7 @@ export const loadSettingsConfig = ({
units: {
control: 1, // Show when control > 2
list: ['metric', 'imperial'],
dflt: units,
dflt: 'metric',
choiceTitles: {
metric: 'metric',
imperial: 'imperial',

View file

@ -91,7 +91,7 @@ export const DesignOptions = ({
language,
ns: menuNs,
passProps: { settings },
updateFunc: (name, value) => update.settings(['options', name], value),
updateFunc: (name, value) => update.settings(['options', ...name], value),
}}
/>
)

View file

@ -1,7 +1,6 @@
import { useContext } from 'react'
import { MenuItemGroup } from './menu-item.mjs'
import { useTranslation } from 'next-i18next'
import { HelpIcon } from 'shared/components/icons.mjs'
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
import { ModalContext } from 'shared/context/modal-context.mjs'
@ -51,7 +50,7 @@ export const useDocsLoader = (DynamicDocs, getDocsPath, language) => {
export const WorkbenchMenu = ({
updateFunc,
ns,
Icon,
Icon = () => null,
name,
config,
control,

View file

@ -45,7 +45,7 @@ const useSharedHandlers = ({ dflt, updateFunc, name }) => {
(newCurrent) => {
if (newCurrent === dflt) newCurrent = undefined
updateFunc(name, newCurrent)
updateFunc([name], newCurrent)
},
[dflt, updateFunc, name]
)
@ -85,9 +85,7 @@ export const ListToggle = ({ config, changed, updateFunc, name }) => {
return (
<input
type="checkbox"
className={`toggle toggle-sm ${
checked && !boolConfig.dflt ? 'toggle-accent' : 'toggle-secondary'
}`}
className={`toggle toggle-sm ${changed ? 'toggle-accent' : 'toggle-secondary'}`}
checked={checked}
onChange={doToggle}
/>
@ -276,4 +274,25 @@ export const MmInput = (props) => {
}
/** A placeholder for an input to handle constant values */
export const ConstantInput = () => <p>FIXME: Constant options are not implemented (yet)</p>
export const ConstantInput = ({
type = 'number',
name,
current,
updateFunc,
t,
changed,
config,
}) => (
<>
<p>{t(`${name}.d`)}</p>
<input
type={type}
className={`
input input-bordered w-full text-base-content
input-${changed ? 'secondary' : 'accent'}
`}
value={changed ? current : config.dflt}
onChange={(evt) => updateFunc([name], evt.target.value)}
/>
</>
)

View file

@ -119,7 +119,7 @@ export const MenuItem = ({
className={open ? openButtonClass : 'btn btn-accent'}
onClick={(evt) => {
evt.stopPropagation()
updateFunc(name)
updateFunc([name])
}}
>
<ClearIcon />

View file

@ -58,7 +58,7 @@ export const PanZoomPattern = forwardRef((props, ref) => {
>
<TransformComponent>
<div style={{ width: size.width + 'px' }} className="max-h-screen">
<Pattern {...{ t, ref, components, renderProps }} />
<Pattern {...{ t, components, renderProps }} ref={ref} />
</div>
</TransformComponent>
</TransformWrapper>

View file

@ -0,0 +1,100 @@
import { useRef } from 'react'
import { PanZoomPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
import { MovableStack } from './stack.mjs'
export const MovablePattern = ({
renderProps,
showButtons = true,
update,
fitImmovable = false,
immovable = [],
layoutPath,
}) => {
const svgRef = useRef(null)
if (!renderProps) return null
// keep a fresh copy of the layout because we might manipulate it without saving to the gist
let layout =
renderProps.settings[0].layout === true
? {
...renderProps.autoLayout,
width: renderProps.width,
height: renderProps.height,
}
: renderProps.settings[0].layout
// Helper method to update part layout and re-calculate width * height
const updateLayout = (name, config, history = true) => {
// Start creating new layout
const newLayout = { ...layout }
newLayout.stacks[name] = config
// Pattern topLeft and bottomRight
let topLeft = { x: 0, y: 0 }
let bottomRight = { x: 0, y: 0 }
for (const pname in renderProps.stacks) {
if (immovable.includes(pname) && !fitImmovable) continue
let partLayout = newLayout.stacks[pname]
// Pages part does not have its topLeft and bottomRight set by core since it's added post-draft
if (partLayout?.tl) {
// set the pattern extremes
topLeft.x = Math.min(topLeft.x, partLayout.tl.x)
topLeft.y = Math.min(topLeft.y, partLayout.tl.y)
bottomRight.x = Math.max(bottomRight.x, partLayout.br.x)
bottomRight.y = Math.max(bottomRight.y, partLayout.br.y)
}
}
newLayout.width = bottomRight.x - topLeft.x
newLayout.height = bottomRight.y - topLeft.y
newLayout.bottomRight = bottomRight
newLayout.topLeft = topLeft
if (history) {
update.ui(layoutPath, newLayout)
} else {
// we don't put it in the gist if it shouldn't contribute to history because we need some of the data calculated here for rendering purposes on the initial layout, but we don't want to actually save a layout until the user manipulates it. This is what allows the layout to respond appropriately to settings changes. Once the user has starting playing with the layout, all bets are off
layout = newLayout
}
}
const sortedStacks = {}
Object.keys(renderProps.stacks)
.sort((a, b) => {
const hasA = immovable.includes(a)
const hasB = immovable.includes(b)
if (hasA && !hasB) return -1
if (!hasA && hasB) return 1
return 0
})
.forEach((s) => (sortedStacks[s] = renderProps.stacks[s]))
const sortedRenderProps = { ...renderProps, stacks: sortedStacks }
const Stack = ({ stackName, stack, settings, components, t }) => (
<MovableStack
{...{
stackName,
stack,
components,
t,
movable: !immovable.includes(stackName),
layout: layout.stacks[stackName],
updateLayout,
showButtons,
settings,
}}
/>
)
return (
<PanZoomPattern
{...{
renderProps: sortedRenderProps,
components: { Stack },
}}
ref={svgRef}
/>
)
}

View file

@ -0,0 +1,272 @@
/*
* This React component is a long way from perfect, but it's a start for
* handling custom layouts.
*
* There are a few 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 update 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.
*
* We run `getScreenCTM().inverse()` on the svg element to pass to `matrixTransform` on a `DOMPointReadOnly` for dom to svg space conversions.
*
* 3) Part-level transforms
*
* All parts use their center as the transform-origin to simplify transforms, especially flipping and rotating.
*
* 4) Bounding box
*
* We use `getBoundingClientRect` rather than `getBBox` because it provides more data and factors in the transforms.
* We then use our `domToSvg` function to move the points back into the SVG space.
*
*
* Known issues
* - currently none
*
* 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 { useRef, useState, useEffect, useCallback } from 'react'
import { generateStackTransform, getTransformedBounds } from '@freesewing/core'
import { getProps } from 'pkgs/react-components/src/pattern/utils.mjs'
import { angle } from '../utils.mjs'
import { drag } from 'd3-drag'
import { select } from 'd3-selection'
import { Buttons } from './transform-buttons.mjs'
export const MovableStack = ({
stackName,
stack,
components,
t,
movable = true,
layout,
updateLayout,
showButtons,
settings,
}) => {
const stackExists = !movable || typeof layout?.move?.x !== 'undefined'
// Use a ref for direct DOM manipulation
const stackRef = useRef(null)
const innerRef = useRef(null)
// State variable to switch between moving or rotating the part
const [rotate, setRotate] = useState(false)
// This is kept as state to avoid re-rendering on drag, which would kill performance
// It's a bit of an anti-pattern, but we'll directly manipulate the properties instead of updating the state
// Managing the difference between re-render and direct DOM updates makes this
// whole thing a bit tricky to wrap your head around
const stackRotation = layout?.rotate || 0
const [liveTransforms] = useState({
translateX: layout?.move.x,
translateY: layout?.move.y,
rotation: stackRotation,
flipX: !!layout?.flipX,
flipY: !!layout?.flipY,
})
const center = stack.topLeft && {
x: stack.topLeft.x + (stack.bottomRight.x - stack.topLeft.x) / 2,
y: stack.topLeft.y + (stack.bottomRight.y - stack.topLeft.y) / 2,
}
const setTransforms = useCallback(() => {
// get the transform attributes
const { translateX, translateY, rotation, flipX, flipY } = liveTransforms
const transforms = generateStackTransform(translateX, translateY, rotation, flipX, flipY, stack)
const me = select(stackRef.current)
me.attr('transform', transforms.join(' '))
return transforms
}, [liveTransforms, stackRef, stack])
/** update the layout either locally or in the gist */
const updateStacklayout = useCallback(
(history = true) => {
/** don't mess with what we don't lay out */
if (!stackRef.current || !movable) return
// set the transforms on the stack in order to calculate from the latest position
const transforms = setTransforms()
// apply the transforms to the bounding box to get the new extents of the stack
const { tl, br } = getTransformedBounds(stack, transforms)
// update it on the draft component
updateLayout(
stackName,
{
move: {
x: liveTransforms.translateX,
y: liveTransforms.translateY,
},
rotate: liveTransforms.rotation % 360,
flipX: liveTransforms.flipX,
flipY: liveTransforms.flipY,
tl,
br,
},
history
)
},
[stackRef, setTransforms, updateLayout, liveTransforms, movable, stack, stackName]
)
// update the layout on mount
useEffect(() => {
// only update if there's a rendered part and it's not an imovable part
if (stackRef.current && movable) {
updateStacklayout(false)
}
}, [stackRef, movable, updateStacklayout])
/** reset the part's transforms */
const resetPart = () => {
liveTransforms.rotation = 0
liveTransforms.flipX = 0
liveTransforms.flipY = 0
updateStacklayout()
}
/** toggle between dragging and rotating */
const toggleDragRotate = () => {
// only respond if the part should be able to drag/rotate
if (!stackRef.current || !movable) {
return
}
setRotate(!rotate)
}
/** Method to flip (mirror) the part along the X or Y axis */
const flip = (axis) => {
if (axis === 'x') liveTransforms.flipX = !liveTransforms.flipX
else liveTransforms.flipY = !liveTransforms.flipY
updateStacklayout()
}
/** method to rotate 90 degrees */
const rotate90 = (direction = 1) => {
if (liveTransforms.flipX) direction *= -1
if (liveTransforms.flipY) direction *= -1
liveTransforms.rotation += 90 * direction
updateStacklayout()
}
/** get the delta rotation from the start of the drag event to now */
const getRotation = (event) =>
angle(center, event.subject) - angle(center, { x: event.x, y: event.y })
let didDrag = false
const handleDrag =
movable &&
drag()
// subject allows us to save data from the start of the event to use throughout event handing
.subject(function (event) {
return rotate
? // if we're rotating, the subject is the mouse position
{ x: event.x, y: event.y }
: // if we're moving, the subject is the part's x,y coordinates
{ x: liveTransforms.translateX, y: liveTransforms.translateY }
})
.on('drag', function (event) {
if (!event.dx && !event.dy) return
if (rotate) {
let newRotation = getRotation(event)
// shift key to snap the rotation
if (event.sourceEvent.shiftKey) {
newRotation = Math.ceil(newRotation / 15) * 15
}
// reverse the rotation direction one time per flip. if we're flipped both directions, rotation will be positive again
if (liveTransforms.flipX) newRotation *= -1
if (liveTransforms.flipY) newRotation *= -1
liveTransforms.rotation = stackRotation + newRotation
} else {
liveTransforms.translateX = event.x
liveTransforms.translateY = event.y
}
// a drag happened, so we should update the layout when we're done
didDrag = true
setTransforms()
})
.on('end', function () {
// save to gist if anything actually changed
if (didDrag) updateStacklayout()
didDrag = false
})
// Initialize drag handler
useEffect(() => {
// don't drag the pages
if (!movable || !stackExists) return
handleDrag(select(stackRef.current))
}, [stackRef, movable, stackExists, handleDrag])
// // Don't just assume this makes sense
if (!stackExists) return null
const { Group, Part } = components
return (
<Group id={`stack-${stackName}`} {...getProps(stack)} ref={stackRef}>
<Group id={`stack-inner-${stackName}`} ref={innerRef}>
{[...stack.parts].map((part, key) => (
<Part {...{ components, t, part, stackName, settings }} key={key} />
))}
</Group>
{movable && (
<>
<rect
x={stack.topLeft.x}
y={stack.topLeft.y}
width={stack.width}
height={stack.height}
className={`layout-rect ${rotate ? 'rotate' : 'move'}`}
id={`${stackName}-layout-rect`}
onClick={toggleDragRotate}
/>
{showButtons ? (
<Buttons
transform={`translate(${center.x}, ${
center.y
}) rotate(${-liveTransforms.rotation}) scale(${liveTransforms.flipX ? -1 : 1},${
liveTransforms.flipY ? -1 : 1
})`}
flip={flip}
rotate={rotate}
setRotate={setRotate}
resetPart={resetPart}
rotate90={rotate90}
partName={stackName}
/>
) : null}
</>
)}
</Group>
)
}

View file

@ -1,6 +1,5 @@
import { useTranslation } from 'next-i18next'
import { ClearIcon } from 'shared/components/icons.mjs'
import get from 'lodash.get'
const Triangle = ({ transform = 'translate(0,0)', fill = 'currentColor' }) => (
<path
@ -54,20 +53,19 @@ const Button = ({ onClickCb, transform, Icon, children }) => {
)
}
export const ShowButtonsToggle = ({ gist, layoutSetType, updateGist }) => {
export const ShowButtonsToggle = ({ ui, update }) => {
const { t } = useTranslation('workbench')
const path = ['_state', 'layout', layoutSetType, 'showButtons']
const showButtons = get(gist, path, true)
const setShowButtons = () => updateGist(path, !showButtons)
const hideButtons = (evt) => {
update.ui('hideMovableButtons', !evt.target.checked)
}
return (
<label htmlFor="showButtons" className="label">
<span className="mr-2">{t('showButtons')}</span>
<label className="label cursor-pointer">
<span className="label-text text-lg mr-2">{t('showMovableButtons')}</span>
<input
type="checkbox"
className="toggle toggle-primary"
checked={showButtons}
onChange={setShowButtons}
checked={!ui.hideMovableButtons}
onChange={hideButtons}
/>
</label>
)

View file

@ -0,0 +1,26 @@
canvas: Canvas
cut: Cut
cuttingLayout: Suggested Cutting Layout
fabric: Main Fabric
materialSize: "{length} of {width} wide material"
heavyCanvas: Heavy Canvas
interfacing: Interfacing
lining: Lining
lmhCanvas: Light to Medium Hair Canvas
mirrored: mirrored
onFoldLower: on the fold
onFoldAndBias: folded on the bias
onBias: on the bias
plastic: Plastic
ribbing: Ribbing
edgeOfFabric: Edge of Fabric
sheetWidth.t: Material Width
sheetWidth.d: How wide is the material?
grainDirection.t: Grain Direction
grainDirection.d: What direction does the grain run on the material?
horizontal.t: Horizontal
horizontal.d: Grain runs left to right
vertical.t: Vertical
vertical.d: Grain runs top to bottom
cutSettings: Material Settings
cutSettings.d: These settings allow you to specify properties of the material to aid in making a cutting layout

View file

@ -0,0 +1,80 @@
import { measurementAsMm, formatFraction128 } from 'shared/utils.mjs'
import { materialPlugin } from 'shared/plugins/plugin-layout-part.mjs'
import { cutLayoutPlugin } from 'shared/plugins/plugin-cut-layout.mjs'
import { pluginAnnotations } from '@freesewing/plugin-annotations'
import { round } from 'shared/utils'
import get from 'lodash.get'
export const activeMaterialPath = ['cutting', 'activeMaterial']
export const materialSettingsPath = ['cutting', 'materials']
export const useMaterialLength = (isImperial, height, format = 'none') => {
// regular conversion from mm to inches or cm
const unit = isImperial ? 25.4 : 10
// conversion from inches or cm to yards or meters
const materialUnit = isImperial ? 36 : 100
// for material, these divisions are granular enough
const rounder = isImperial ? 16 : 10
// we convert the used material height to the right units so we can round it
const inMaterialUnits = height / (materialUnit * unit)
// we multiply it by the rounder, round it up, then divide by the rounder again to get the rounded amount
const roundCount = Math.ceil(rounder * inMaterialUnits) / rounder
// format as a fraction for imperial, a decimal for metric
const count = isImperial ? formatFraction128(roundCount, format) : round(roundCount, 1)
return `${count}${isImperial ? 'yds' : 'm'}`
}
export const materialSettingsOrDefault = (units, ui, material) => {
const isImperial = units === 'imperial'
const sheetHeight = measurementAsMm(isImperial ? 36 : 100, units)
const uiSettings = get(ui, [...materialSettingsPath, material], {})
const sheetWidth = uiSettings.sheetWidth || measurementAsMm(isImperial ? 54 : 120, units)
const grainDirection = uiSettings.grainDirection === undefined ? 90 : uiSettings.grainDirection
return { activeMaterial: material, sheetWidth, grainDirection, sheetHeight }
}
export const useMaterialSettings = ({ ui }) => {
const activeMaterial = get(ui, activeMaterialPath, 'fabric')
return {
activeMaterial,
...get(ui, [...materialSettingsPath, activeMaterial]),
}
}
export const useMaterialDraft = ({ settings, ui, Design, materialSettings }) => {
// get the appropriate layout for the view
const layout = get(ui, ['layouts', 'cut', materialSettings.activeMaterial], true)
// hand it separately to the design
const pattern = new Design({ ...settings, layout })
materialSettings = materialSettingsOrDefault(settings.units, ui, materialSettings.activeMaterial)
const layoutSettings = {
sheetWidth: materialSettings.sheetWidth,
sheetHeight: materialSettings.sheetHeight,
}
let renderProps
try {
// add the material plugin to the draft
pattern.use(materialPlugin(layoutSettings))
// add the cutLayout plugin
pattern.use(cutLayoutPlugin(materialSettings.activeMaterial, materialSettings.grainDirection))
// also, pluginAnnotations and pluginFlip are needed
pattern.use(pluginAnnotations)
// draft the pattern
pattern.draft()
renderProps = pattern.getRenderProps()
} catch (err) {
console.log(err)
}
return { pattern, renderProps }
}
export const useMaterialList = (pattern) => {
return pattern.setStores[0].cutlist.getCutFabrics(pattern.settings[0])
}

View file

@ -0,0 +1,121 @@
import { useEffect, useCallback } from 'react'
import { useTranslation } from 'next-i18next'
import { CutMenu, ns as menuNs } from './menu.mjs'
import { MovablePattern } from 'shared/components/workbench/pattern/movable/index.mjs'
import { IconWrapper } from 'shared/components/icons.mjs'
import {
activeMaterialPath,
useMaterialSettings,
useMaterialDraft,
useMaterialList,
useMaterialLength,
} from './hooks'
export const ns = [...menuNs]
const SheetIcon = (props) => (
<IconWrapper {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z"
/>
</IconWrapper>
)
const MaterialCounter = ({ settings, renderProps }) => {
const materialLength = useMaterialLength(settings.units === 'imperial', renderProps.height)
return (
<div className="flex flex-row font-bold items-center text-2xl justify-center">
<SheetIcon className="h-6 w-6 mr-2 inline align-middle" />
<span className="align-middle">{materialLength}</span>
</div>
)
}
export const CutView = ({
design,
patternConfig,
settings,
ui,
update,
language,
account,
DynamicDocs,
Design,
}) => {
const { t } = useTranslation(['workbench', 'plugin'])
const materialSettings = useMaterialSettings({ ui, units: settings.units })
const { pattern, renderProps } = useMaterialDraft({ settings, ui, Design, materialSettings })
const materialList = useMaterialList(pattern)
const setActiveMaterial = useCallback(
(newMaterial) => {
update.ui(activeMaterialPath, newMaterial)
},
[update]
)
useEffect(() => {
if (!materialList.includes(materialSettings.activeMaterial)) setActiveMaterial(materialList[0])
}, [materialSettings, materialList, setActiveMaterial])
return (
<div>
<div className="flex flex-row">
<div className="w-2/3 shrink-0 grow lg:p-4 sticky top-0">
<div className="flex justify-between items-baseline">
<h2 className="capitalize">
{t('layoutThing', { thing: design }) + ' ' + t('forCutting')}
</h2>
<MaterialCounter settings={settings} renderProps={renderProps} />
</div>
<div className="my-4">
{materialList.length > 1 ? (
<div className="tabs">
{materialList.map((title) => (
<button
key={title}
className={`text-xl font-bold capitalize tab tab-bordered grow ${
materialSettings.activeMaterial === title ? 'tab-active' : ''
}`}
onClick={() => setActiveMaterial(title)}
>
{t('plugin:' + title)}
</button>
))}
</div>
) : null}
<MovablePattern
{...{
renderProps,
update,
immovable: ['material'],
layoutPath: ['layouts', 'cut', materialSettings.activeMaterial],
showButtons: !ui.hideMovableButtons,
}}
/>
</div>
</div>
<div className="w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-screen overflow-scroll">
<CutMenu
{...{
design,
pattern,
patternConfig,
settings,
ui,
update,
language,
account,
DynamicDocs,
materialSettings,
}}
/>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,64 @@
import { useTranslation } from 'next-i18next'
import {
DesignOptions,
ns as designMenuNs,
} from 'shared/components/workbench/menus/design-options/index.mjs'
import {
CoreSettings,
ns as coreMenuNs,
} from 'shared/components/workbench/menus/core-settings/index.mjs'
import { CutSettings, ns as cutNs } from './settings.mjs'
import { ShowButtonsToggle } from 'shared/components/workbench/pattern/movable/transform-buttons.mjs'
import { ClearIcon } from 'shared/components/icons.mjs'
export const ns = [...coreMenuNs, ...designMenuNs, ...cutNs, 'workbench']
const CutActions = ({ update, ui, materialSettings }) => {
// get translation for the menu
const { t } = useTranslation(['workbench'])
const resetLayout = () => update.ui(['layouts', 'cut', materialSettings.activeMaterial])
return (
<div className="mt-2 mb-4">
<div className="flex justify-evenly flex-col lg:flex-row">
<ShowButtonsToggle update={update} ui={ui} />
<button className="btn btn-primary btn-outline" onClick={resetLayout}>
<ClearIcon />
<span className="ml-2">{t('reset')}</span>
</button>
</div>
</div>
)
}
export const CutMenu = ({
design,
patternConfig,
settings,
ui,
update,
language,
account,
DynamicDocs,
materialSettings,
}) => {
const control = account.control
const menuProps = {
design,
patternConfig,
settings,
update,
language,
account,
DynamicDocs,
control,
}
return (
<nav className="grow mb-12">
<CutActions update={update} ui={ui} materialSettings={materialSettings} />
<CutSettings {...menuProps} ui={ui} materialSettings={materialSettings} />
<DesignOptions {...menuProps} />
<CoreSettings {...menuProps} />
</nav>
)
}

View file

@ -0,0 +1,70 @@
import { CutIcon } from 'shared/components/icons.mjs'
import { measurementAsMm, measurementAsUnits } from 'shared/utils.mjs'
import { ConstantInput, ListInput } from 'shared/components/workbench/menus/shared/inputs.mjs'
import { MmValue, ListValue } from 'shared/components/workbench/menus/shared/values.mjs'
import { WorkbenchMenu } from 'shared/components/workbench/menus/shared/index.mjs'
import { materialSettingsPath } from './hooks.mjs'
export const ns = ['cut']
const FabricSizeInput = (props) => {
const mmCurrent = measurementAsUnits(props.current, props.units)
const mmUpdateFunc = (path, newVal) =>
props.updateFunc(path, measurementAsMm(newVal, props.units))
return <ConstantInput {...props} current={mmCurrent} updateFunc={mmUpdateFunc} />
}
const loadConfig = (units) => ({
sheetWidth: {
dflt: units === 'imperial' ? 54 : 120,
},
grainDirection: {
list: [0, 90],
dflt: 90,
choiceTitles: {
0: 'horizontal',
90: 'vertical',
},
valueTitles: {
0: 'horizontal',
90: 'vertical',
},
},
})
const inputs = {
sheetWidth: FabricSizeInput,
grainDirection: ListInput,
}
const values = {
sheetWidth: MmValue,
grainDirection: ListValue,
}
export const CutSettings = ({ update, settings, account, materialSettings }) => {
const { activeMaterial } = materialSettings
const config = loadConfig(settings.units)
const passProps = { units: settings.units }
const updateFunc = (path, newVal) =>
update.ui([...materialSettingsPath, activeMaterial, ...path], newVal)
return (
<WorkbenchMenu
{...{
config,
control: account.control,
currentValues: materialSettings,
Icon: CutIcon,
inputs,
values,
name: 'cutSettings',
ns,
passProps,
updateFunc,
}}
/>
)
}

View file

@ -0,0 +1,28 @@
import { useTranslation } from 'next-i18next'
import { ClearIcon, ExportIcon } from 'shared/components/icons.mjs'
import { ShowButtonsToggle } from 'shared/components/workbench/pattern/movable/transform-buttons.mjs'
export const ns = ['workbench', 'print']
export const PrintActions = ({ update, ui, exportIt }) => {
// get translation for the menu
const { t } = useTranslation(ns)
const resetLayout = () => update.ui(['layouts', 'print'])
return (
<div className="mt-2 mb-4">
<div className="flex justify-evenly flex-col lg:flex-row">
<ShowButtonsToggle update={update} ui={ui} />
<button className="btn btn-primary btn-outline" onClick={resetLayout}>
<ClearIcon />
<span className="ml-2">{t('reset')}</span>
</button>
<button className="btn btn-primary" onClick={exportIt}>
<ExportIcon />
<span className="ml-2">{t('export')}</span>
</button>
</div>
</div>
)
}

View file

@ -0,0 +1,62 @@
import { measurementAsMm } from 'shared/utils.mjs'
export const printSettingsPath = ['print', 'pages']
export const defaultPrintSettings = (units, inMm = true) => {
const margin = units === 'imperial' ? 0.5 : 1
return {
size: 'a4',
orientation: 'portrait',
margin: inMm ? measurementAsMm(margin, units) : margin,
coverPage: true,
cutlist: true,
}
}
const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'legal', 'tabloid']
export const loadPrintConfig = (units) => {
const defaults = defaultPrintSettings(units, false)
const config = {
size: {
control: 2,
list: sizes,
dflt: defaults.size,
choiceTitles: {},
valueTitles: {},
},
orientation: {
control: 2,
list: ['portrait', 'landscape'],
choiceTitles: {
portrait: 'portrait',
landscape: 'landscape',
},
valueTitles: {
portrait: 'portrait',
landscape: 'landscape',
},
dflt: defaults.orientation,
},
margin: {
control: 2,
min: units === 'imperial' ? 0.25 : 0.5,
max: 2.5,
step: units === 'imperial' ? 0.125 : 0.1,
dflt: defaults.margin,
},
coverPage: {
control: 3,
dflt: defaults.coverPage,
},
cutlist: {
control: 3,
dflt: defaults.cutlist,
},
}
sizes.forEach((s) => {
config.size.choiceTitles[s] = s
config.size.valueTitles[s] = s
})
return config
}

View file

@ -0,0 +1,126 @@
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { pagesPlugin } from 'shared/plugins/plugin-layout-part.mjs'
import {
handleExport,
ns as exportNs,
} from 'shared/components/workbench/exporting/export-handler.mjs'
import get from 'lodash.get'
import { MovablePattern } from 'shared/components/workbench/pattern/movable/index.mjs'
import { PrintMenu, ns as menuNs } from './menu.mjs'
import { defaultPrintSettings, printSettingsPath } from './config.mjs'
import { PrintIcon, RightIcon } from 'shared/components/icons.mjs'
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
const viewNs = ['print', ...exportNs]
export const ns = [...viewNs, ...menuNs]
const PageCounter = ({ pattern }) => {
const pages = pattern.setStores[0].get('pages', {})
const { cols, rows, count } = pages
return (
<div className="flex flex-row font-bold items-center text-2xl justify-center ">
<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="ml-2">{rows}</span>
</div>
)
}
export const PrintView = ({
design,
pattern,
patternConfig,
settings,
ui,
update,
language,
account,
DynamicDocs,
Design,
}) => {
const { t } = useTranslation(ns)
const loading = useContext(LoadingContext)
const toast = useToast()
const defaultSettings = defaultPrintSettings(settings.units)
// add the pages plugin to the draft
const pageSettings = {
...defaultSettings,
...get(ui, printSettingsPath, {}),
}
pattern.use(pagesPlugin(pageSettings))
let renderProps
try {
// draft the pattern
pattern.draft()
renderProps = pattern.getRenderProps()
} catch (err) {
console.log(err)
}
const exportIt = () => {
handleExport({
format: 'pdf',
settings,
design,
t,
Design,
ui,
startLoading: loading.startLoading,
stopLoading: loading.stopLoading,
onComplete: () => {},
onError: (err) => toast.error(err.message),
})
}
return (
<div>
<div className="flex flex-row">
<div className="w-2/3 shrink-0 grow lg:p-4 sticky top-0">
<div className="flex justify-between items-baseline">
<h2 className="capitalize">
{t('layoutThing', { thing: design }) + ' ' + t('forPrinting')}
</h2>
<PageCounter pattern={pattern} />
</div>
<MovablePattern
{...{
renderProps,
update,
immovable: ['pages'],
layoutPath: ['layouts', 'print'],
showButtons: !ui.hideMovableButtons,
}}
/>
</div>
<div className="w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-screen overflow-scroll">
<PrintMenu
{...{
design,
pattern,
patternConfig,
settings,
ui,
update,
language,
account,
DynamicDocs,
exportIt,
}}
/>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,44 @@
import {
DesignOptions,
ns as designMenuNs,
} from 'shared/components/workbench/menus/design-options/index.mjs'
import {
CoreSettings,
ns as coreMenuNs,
} from 'shared/components/workbench/menus/core-settings/index.mjs'
import { PrintSettings, ns as printMenuNs } from './settings.mjs'
import { PrintActions } from './actions.mjs'
export const ns = [...coreMenuNs, ...designMenuNs, ...printMenuNs]
export const PrintMenu = ({
design,
patternConfig,
settings,
ui,
update,
language,
account,
DynamicDocs,
exportIt,
}) => {
const control = account.control
const menuProps = {
design,
patternConfig,
settings,
update,
language,
account,
DynamicDocs,
control,
}
return (
<nav className="grow mb-12">
<PrintActions {...menuProps} ui={ui} exportIt={exportIt} />
<PrintSettings {...menuProps} ui={ui} />
<DesignOptions {...menuProps} />
<CoreSettings {...menuProps} />
</nav>
)
}

View file

@ -0,0 +1,40 @@
printSettings: Print Settings
printSettings.d: These settings control aspects of the page for PDF export and printing
size.t: Paper Size
size.d: Choose a size of paper
a4.t: A4
a4.d: 210 x 297 mm
a3.t: A3
a3.d: 297 × 420 mm
a2.t: A2
a2.d: 420 × 594 mm
a1.t: A1
a1.d: 594 × 841 mm
a0.t: A0
a0.d: 841 × 1189 mm
letter.t: Letter
letter.d: 8.5 x 11 in
legal.t: Legal
legal.d: 8.5 x 14 in
tabloid.t: Tabloid
tabloid.d: 11 x 17 in
cutlist.t: Include Cutting Layouts
cutlist.d: Should images of the suggested cutting layouts for each material be included in the exported PDF?
cutlistNo.t: Do not include cutting layouts
cutlistNo.d: Exported PDFs will not include any suggested cutting layouts
cutlistYes.t: Include cutting layouts
cutlistYes.d: Exported PDFs will include a page for each material used by the pattern listing how much of the material is needed and suggesting a layout for cutting out the appropriate pieces
coverPage.t: Include Cover Page
coverPage.d: Should the exported PDF include a cover page?
coverPageYes.t: Include a cover page
coverPageYes.d: Add a cover page to the exported PDF including information about the pattern and an image of the entire assembled layout
coverPageNo.t: Do not include a cover page
coverPageNo.d: Exported PDFs will skip the cover page
margin.t: Page Margin
margin.d: How much margin should pages of the PDF have? This is useful for making sure that all content is within the margins of your printer. It also controls how much adjacent pages will overlap.
orientation.t: Page Orientation
orientation.d: Which direction should the pages be rotated?
portrait.t: Portrait
portrait.d: Pages will be longer than they are wide
landscape.t: Landscape
landscape.d: Pages will be wider than they are long

View file

@ -0,0 +1,47 @@
import { PrintIcon } from 'shared/components/icons.mjs'
import { WorkbenchMenu } from 'shared/components/workbench/menus/shared/index.mjs'
import { ListInput, BoolInput, MmInput } from 'shared/components/workbench/menus/shared/inputs.mjs'
import { ListValue, BoolValue, MmValue } from 'shared/components/workbench/menus/shared/values.mjs'
import { loadPrintConfig, printSettingsPath } from './config.mjs'
import get from 'lodash.get'
const inputs = {
size: ListInput,
orientation: ListInput,
margin: MmInput,
coverPage: BoolInput,
cutlist: BoolInput,
}
const values = {
size: ListValue,
orientation: ListValue,
margin: MmValue,
coverPage: BoolValue,
cutlist: BoolValue,
}
export const ns = ['print']
export const PrintSettings = ({ update, settings, ui, account }) => {
const config = loadPrintConfig(settings.units)
const passProps = { units: settings.units }
const updateFunc = (path, newVal) => update.ui([...printSettingsPath, ...path], newVal)
return (
<WorkbenchMenu
{...{
config,
control: account.control,
currentValues: get(ui, printSettingsPath, {}),
Icon: PrintIcon,
inputs,
values,
name: 'printSettings',
ns,
passProps,
updateFunc,
}}
/>
)
}

View file

@ -17,6 +17,7 @@ bottomRight: Bottom Right
attributes: Attributes
showAllParts: Show all pattern parts
showOnlyThisPart: Show only this pattern part
showMovableButtons: Buttons
partInfo: Pattern part info
pathInfo: Path info
part: Pattern part

View file

@ -1,4 +1,4 @@
import { addToOnly } from '../plugin-layout-part.mjs'
import { addToOnly } from 'shared/plugins/plugin-layout-part.mjs'
import { pluginMirror } from '@freesewing/plugin-mirror'
const prefix = 'mirroredOnFold'

View file

@ -9,6 +9,7 @@ export const sizes = {
a1: [594, 841],
a0: [841, 1188],
letter: [215.9, 279.4],
legal: [215.9, 355.6],
tabloid: [279.4, 431.8],
}
@ -52,10 +53,10 @@ export const pagesPlugin = ({ size = 'a4', ...settings }) => {
return basePlugin({ ...settings, sheetWidth, sheetHeight })
}
export const fabricPlugin = (settings) => {
export const materialPlugin = (settings) => {
return basePlugin({
...settings,
partName: 'fabric',
partName: 'material',
responsiveColumns: false,
})
}
@ -206,7 +207,9 @@ const basePlugin = ({
const { points, Point, paths, Path, part, macro, store } = shorthand
let count = 0
let withContent = {}
part.topLeft = so.layout.topLeft || { x: 0, y: 0 }
part.topLeft = so.layout.topLeft
? new Point(so.layout.topLeft.x, so.layout.topLeft.y)
: new Point(0, 0)
// get the layout from the pattern
const { layout } = so
@ -265,7 +268,7 @@ const basePlugin = ({
const br = points[`${pageName}-br`]
part.width = br.x
part.height = br.y
part.bottomRight = { x: br.x, y: br.y }
part.bottomRight = new Point(br.x, br.y)
}
if (!printStyle) {

View file

@ -54,7 +54,7 @@ export const formatFraction128 = (fraction, format = 'html') => {
rest = fraction - inches
}
let fraction128 = Math.round(rest * 128)
if (fraction128 == 0) return formatImperial(negative, 0, false, false, format)
if (fraction128 == 0) return formatImperial(negative, inches, false, false, format)
for (let i = 1; i < 7; i++) {
const numoFactor = Math.pow(2, 7 - i)