diff --git a/packages/i18n/src/locales/en/plugin/plugins/cutlist.yaml b/packages/i18n/src/locales/en/plugin/plugins/cutlist.yaml index b35b0c444fe..ca55d8e5b74 100644 --- a/packages/i18n/src/locales/en/plugin/plugins/cutlist.yaml +++ b/packages/i18n/src/locales/en/plugin/plugins/cutlist.yaml @@ -1,6 +1,8 @@ canvas: Canvas cut: Cut +cuttingLayout: Suggested Cutting Layout fabric: Main Fabric +fabricSize: "{length} of {width} wide material" heavyCanvas: Heavy Canvas interfacing: Interfacing lining: Lining diff --git a/plugins/plugin-cutlist/src/index.mjs b/plugins/plugin-cutlist/src/index.mjs index 7400a3862cd..ea146f52f97 100644 --- a/plugins/plugin-cutlist/src/index.mjs +++ b/plugins/plugin-cutlist/src/index.mjs @@ -8,6 +8,7 @@ export const plugin = { ['cutlist.removeCut', removeCut], ['cutlist.setGrain', setGrain], ['cutlist.setCutOnFold', setCutOnFold], + ['cutlist.getCutFabrics', getCutFabrics], ], } @@ -79,3 +80,20 @@ function setCutOnFold(store, p1, p2) { return store } + +function getCutFabrics(store, settings) { + const cutlist = store.get('cutlist') + const list = settings.only ? [].concat(settings.only) : Object.keys(cutlist) + + const fabrics = [] + list.forEach((partName) => { + if (!cutlist[partName].materials) { + return + } + for (var m in cutlist[partName].materials) { + if (!fabrics.includes(m)) fabrics.push(m) + } + }) + + return fabrics +} diff --git a/sites/shared/components/workbench/exporting/export-handler.mjs b/sites/shared/components/workbench/exporting/export-handler.mjs index 1e82c9aa462..3abc04af4e7 100644 --- a/sites/shared/components/workbench/exporting/export-handler.mjs +++ b/sites/shared/components/workbench/exporting/export-handler.mjs @@ -2,8 +2,13 @@ import Worker from 'web-worker' import fileSaver from 'file-saver' import { themePlugin } from '@freesewing/plugin-theme' import { pluginI18n } from '@freesewing/plugin-i18n' -import { pagesPlugin } from '../layout/plugin-layout-part.mjs' -import { capitalize } from 'shared/utils.mjs' +import { pagesPlugin, fabricPlugin } from '../layout/plugin-layout-part.mjs' +import { pluginCutlist } from '@freesewing/plugin-cutlist' +import { cutLayoutPlugin } from '../layout/cut/plugin-cut-layout.mjs' +import { fabricSettingsOrDefault } from '../layout/cut/index.mjs' +import { useFabricLength } from '../layout/cut/settings.mjs' +import { capitalize, formatFraction128, formatMm } from 'shared/utils.mjs' +import get from 'lodash.get' export const exportTypes = { exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'], @@ -16,8 +21,49 @@ export const defaultPdfSettings = { orientation: 'portrait', margin: 10, coverPage: true, + cutlist: true, } +const themedPattern = (design, gist, overwrite, format, t) => { + const pattern = new design({ ...gist, ...overwrite }) + + // add the theme and translation to the pattern + pattern.use(themePlugin, { stripped: format !== 'svg', skipGrid: ['pages'] }) + pattern.use(pluginI18n, { t }) + pattern.use(pluginCutlist) + + return pattern +} +const generateCutLayouts = (pattern, design, gist, format, t) => { + const fabrics = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics( + pattern.settings[0] + ) || ['fabric'] + if (!fabrics.length) return + + const isImperial = gist.units === 'imperial' + const cutLayouts = {} + fabrics.forEach((f) => { + const fabricSettings = fabricSettingsOrDefault(gist, f) + const fabricLayout = get(gist, ['layouts', 'cuttingLayout', f], true) + const fabricPattern = themedPattern(design, gist, { layout: fabricLayout }, format, t) + .use(cutLayoutPlugin(f, fabricSettings.grainDirection)) + .use(fabricPlugin({ ...fabricSettings, printStyle: true, setPatternSize: 'width' })) + + fabricPattern.draft() + const svg = fabricPattern.render() + cutLayouts[f] = { + svg, + title: t('plugin:' + f), + dimensions: t('plugin:fabricSize', { + width: formatMm(fabricSettings.sheetWidth, gist.units, 'notags'), + length: useFabricLength(isImperial, fabricPattern.height, 'notags'), + interpolation: { escapeValue: false }, + }), + } + }) + + return cutLayouts +} /** * Handle exporting the draft or gist * format: format to export to @@ -72,11 +118,7 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE gist.embed = false // make a pattern instance for export rendering const layout = gist.layouts?.printingLayout || gist.layout || true - let pattern = new design({ ...gist, layout }) - - // add the theme and translation to the pattern - pattern.use(themePlugin, { stripped: format !== 'svg', skipGrid: ['pages'] }) - pattern.use(pluginI18n, { t }) + let pattern = themedPattern(design, gist, { layout }, format, t) // a specified size should override the gist one if (format !== 'pdf') { @@ -100,6 +142,7 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE design: capitalize(pattern.designConfig.data.name.replace('@freesewing/', '')), tagline: t('common:sloganCome') + '. ' + t('common:sloganStay'), url: window.location.href, + cuttingLayout: t('plugin:cuttingLayout'), } } @@ -110,6 +153,11 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE // add the svg and pages data to the worker args 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) + } + // post a message to the worker with all needed data worker.postMessage(workerArgs) } catch (err) { diff --git a/sites/shared/components/workbench/exporting/pdf-maker.mjs b/sites/shared/components/workbench/exporting/pdf-maker.mjs index 36dd06b3b91..17c26866dc7 100644 --- a/sites/shared/components/workbench/exporting/pdf-maker.mjs +++ b/sites/shared/components/workbench/exporting/pdf-maker.mjs @@ -13,7 +13,7 @@ const logoSvg = ` * The svg uses mm internally, so when we do spatial reasoning inside the svg, we need to know values in mm * */ const mmToPoints = 2.834645669291339 - +const lineStart = 50 /** * Freesewing's first explicit class? * handles pdf exporting @@ -47,12 +47,14 @@ export class PdfMaker { svgHeight pageCount = 0 + lineLevel = 50 - constructor({ svg, settings, pages, strings }) { + constructor({ svg, settings, pages, strings, cutLayouts }) { this.settings = settings this.pagesWithContent = pages.withContent this.svg = svg this.strings = strings + this.cutLayouts = cutLayouts this.initPdf() @@ -88,6 +90,7 @@ export class PdfMaker { /** make the pdf */ async makePdf() { await this.generateCoverPage() + await this.generateCutLayoutPages() await this.generatePages() } @@ -116,15 +119,19 @@ export class PdfMaker { return } - const headerLevel = await this.generateCoverPageTitle() + this.nextPage() + await this.generateCoverPageTitle() + await this.generateSvgPage(this.svg) + } + async generateSvgPage(svg) { //abitrary margin for visual space let coverMargin = 85 - let coverHeight = this.pdf.page.height - coverMargin * 2 - headerLevel + let coverHeight = this.pdf.page.height - coverMargin * 2 - this.lineLevel let coverWidth = this.pdf.page.width - coverMargin * 2 // add the entire pdf to the page, so that it fills the available space as best it can - await SVGtoPDF(this.pdf, this.svg, coverMargin, headerLevel + coverMargin, { + await SVGtoPDF(this.pdf, svg, coverMargin, this.lineLevel + coverMargin, { width: coverWidth, height: coverHeight, assumePt: false, @@ -133,40 +140,50 @@ export class PdfMaker { }) this.pageCount++ } - async generateCoverPageTitle() { - let lineLevel = 50 - let lineStart = 50 - - this.pdf.fontSize(28) - this.pdf.text('FreeSewing', lineStart, lineLevel) - lineLevel += 28 - - this.pdf.fontSize(12) - this.pdf.text(this.strings.tagline, lineStart, lineLevel) - lineLevel += 12 + 20 - - this.pdf.fontSize(48) - this.pdf.text(this.strings.design, lineStart, lineLevel) - lineLevel += 48 + this.addText('FreeSewing', 28) + .addText(this.strings.tagline, 12, 20) + .addText(this.strings.design, 48, -8) await SVGtoPDF(this.pdf, logoSvg, this.pdf.page.width - lineStart - 100, lineStart, { width: 100, - height: lineLevel - lineStart - 8, + height: this.lineLevel - lineStart, preserveAspectRatio: 'xMaxYMin meet', }) this.pdf.lineWidth(1) this.pdf - .moveTo(lineStart, lineLevel - 8) - .lineTo(this.pdf.page.width - lineStart, lineLevel - 8) + .moveTo(lineStart, this.lineLevel) + .lineTo(this.pdf.page.width - lineStart, this.lineLevel) .stroke() + this.lineLevel += 8 this.pdf.fillColor('#888888') - this.pdf.fontSize(10) - this.pdf.text(this.strings.url, lineStart, lineLevel) + this.addText(this.strings.url, 10) + } - return lineLevel + async generateCutLayoutTitle(fabricTitle, fabricDimensions) { + this.addText(this.strings.cuttingLayout, 12, 2).addText(fabricTitle, 28) + + this.pdf.lineWidth(1) + this.pdf + .moveTo(lineStart, this.lineLevel) + .lineTo(this.pdf.page.width - lineStart, this.lineLevel) + .stroke() + + this.lineLevel += 5 + this.addText(fabricDimensions, 16) + } + + async generateCutLayoutPages() { + if (!this.settings.cutlist || !this.cutLayouts) return + + for (const fabric in this.cutLayouts) { + this.nextPage() + const { title, dimensions, svg } = this.cutLayouts[fabric] + await this.generateCutLayoutTitle(title, dimensions) + await this.generateSvgPage(svg) + } } /** generate the pages of the pdf */ @@ -190,11 +207,7 @@ export class PdfMaker { let x = -w * this.pageWidth + startMargin let y = -h * this.pageHeight + startMargin - // if there was no cover page, the first page already exists - if (this.pageCount > 0) { - // otherwise make a new page - this.pdf.addPage() - } + this.nextPage() // add the pdf to the page, offset by the page distances await SVGtoPDF(this.pdf, this.svg, x, y, options) @@ -202,4 +215,21 @@ export class PdfMaker { } } } + + nextPage() { + // if no pages have been made, we can use the current + this.lineLevel = lineStart + if (this.pageCount === 0) return + + // otherwise make a new page + this.pdf.addPage() + } + + addText(text, fontSize, marginBottom = 0) { + this.pdf.fontSize(fontSize) + this.pdf.text(text, 50, this.lineLevel) + + this.lineLevel += fontSize + marginBottom + return this + } } diff --git a/sites/shared/components/workbench/layout/cut/index.mjs b/sites/shared/components/workbench/layout/cut/index.mjs index fb94955e436..484f535ca24 100644 --- a/sites/shared/components/workbench/layout/cut/index.mjs +++ b/sites/shared/components/workbench/layout/cut/index.mjs @@ -4,22 +4,25 @@ import { Draft } from '../draft/index.mjs' import { fabricPlugin } from '../plugin-layout-part.mjs' import { cutLayoutPlugin } from './plugin-cut-layout.mjs' import { pluginCutlist } from '@freesewing/plugin-cutlist' -import { pluginFlip } from '@freesewing/plugin-flip' import { measurementAsMm } from 'shared/utils.mjs' import { useEffect } from 'react' import get from 'lodash.get' -const activeFabricPath = ['_state', 'layout', 'forCutting', 'activeFabric'] -const useFabricSettings = (gist) => { +export const fabricSettingsOrDefault = (gist, fabric) => { const isImperial = gist.units === 'imperial' const sheetHeight = measurementAsMm(isImperial ? 36 : 100, gist.units) - const activeFabric = get(gist, activeFabricPath) || 'fabric' - const gistSettings = get(gist, ['_state', 'layout', 'forCutting', 'fabric', activeFabric]) + 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, sheetWidth, grainDirection, sheetHeight } + 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) => { @@ -42,7 +45,6 @@ const useFabricDraft = (gist, design, fabricSettings) => { draft.use(cutLayoutPlugin(fabricSettings.activeFabric, fabricSettings.grainDirection)) // also, pluginCutlist and pluginFlip are needed draft.use(pluginCutlist) - draft.use(pluginFlip) // draft the pattern draft.draft() @@ -55,17 +57,7 @@ const useFabricDraft = (gist, design, fabricSettings) => { } const useFabricList = (draft) => { - const cutList = draft.setStores[0].get('cutlist') - const fabricList = ['fabric'] - for (const partName in cutList) { - if (draft.settings[0].only && !draft.settings[0].only.includes(partName)) continue - - for (const matName in cutList[partName].materials) { - if (!fabricList.includes(matName)) fabricList.push(matName) - } - } - - return fabricList + return draft.setStores[0].cutlist.getCutFabrics(draft.settings[0]) } const bgProps = { fill: 'none' } diff --git a/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs b/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs index 522da549345..7539b97a6d7 100644 --- a/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs +++ b/sites/shared/components/workbench/layout/cut/plugin-cut-layout.mjs @@ -1,8 +1,11 @@ import { addToOnly } from '../plugin-layout-part.mjs' +import { pluginFlip } from '@freesewing/plugin-flip' +import { pluginMirror } from '@freesewing/plugin-mirror' const prefix = 'mirroredOnFold' // types of path operations const opTypes = ['to', 'cp1', 'cp2'] +const avoidRegx = new RegExp(`^(cutonfold|grainline|__scalebox|__miniscale|${prefix})`) /** * The plugin to handle all business related to mirroring, rotating, and duplicating parts for the cutting layout @@ -90,6 +93,8 @@ export const cutLayoutPlugin = function (material, grainAngle) { }, }, macros: { + ...pluginFlip.macros, + ...pluginMirror.macros, // handle mirroring on the fold and rotating to sit along the grain or bias handleFoldAndGrain: ({ partCutlist, grainSpec, ignoreOnFold, bias }, { points, macro }) => { // if the part has cutonfold instructions @@ -114,8 +119,7 @@ export const cutLayoutPlugin = function (material, grainAngle) { const mirrorPaths = [] for (const p in paths) { // skip ones that are hidden - if (!paths[p].hidden && !p.match(/^(cutonfold|grainline|__scalebox|__miniscale)/)) - mirrorPaths.push(paths[p]) + if (!paths[p].hidden && !p.match(avoidRegx)) mirrorPaths.push(paths[p]) } // store all the points to mirror diff --git a/sites/shared/components/workbench/layout/cut/settings.mjs b/sites/shared/components/workbench/layout/cut/settings.mjs index af5475100d1..f0920a458aa 100644 --- a/sites/shared/components/workbench/layout/cut/settings.mjs +++ b/sites/shared/components/workbench/layout/cut/settings.mjs @@ -78,7 +78,7 @@ export const GrainDirectionPicker = ({ grainDirection, activeFabric, updateGist ) } -const useFabricLength = (isImperial, height) => { +export const useFabricLength = (isImperial, height) => { // regular conversion from mm to inches or cm const unit = isImperial ? 25.4 : 10 // conversion from inches or cm to yards or meters diff --git a/sites/shared/components/workbench/layout/plugin-layout-part.mjs b/sites/shared/components/workbench/layout/plugin-layout-part.mjs index 385368b001e..e9c6e472301 100644 --- a/sites/shared/components/workbench/layout/plugin-layout-part.mjs +++ b/sites/shared/components/workbench/layout/plugin-layout-part.mjs @@ -179,11 +179,11 @@ const basePlugin = ({ pattern.draftPartForSet(partName, pattern.activeSet) // if the pattern size is supposed to be re-set to the full width and height of all pages, do that - if (setPatternSize) { - const generatedPageData = pattern.setStores[pattern.activeSet].get('pages') - pattern.width = sheetWidth * generatedPageData.cols - pattern.height = sheetHeight * generatedPageData.rows - } + const generatedPageData = pattern.setStores[pattern.activeSet].get(partName) + if (setPatternSize === true || setPatternSize === 'width') + pattern.width = Math.max(pattern.width, sheetWidth * generatedPageData.cols) + if (setPatternSize === true || setPatternSize === 'height') + pattern.height = Math.max(pattern.height, sheetHeight * generatedPageData.rows) removeFromOnly(pattern, partName) }, diff --git a/sites/shared/components/workbench/layout/print/settings.mjs b/sites/shared/components/workbench/layout/print/settings.mjs index ca8374234b5..beb60d37097 100644 --- a/sites/shared/components/workbench/layout/print/settings.mjs +++ b/sites/shared/components/workbench/layout/print/settings.mjs @@ -24,6 +24,12 @@ export const PrintLayoutSettings = (props) => { ) } + const setCutlist = () => { + props.updateGist( + ['_state', 'layout', 'forPrinting', 'page', 'cutlist'], + !props.layoutSettings.cutlist + ) + } return (
{
-
-
- +
+
+
-