1
0
Fork 0

add cutting layouts to pdf export

This commit is contained in:
Enoch Riese 2023-03-15 12:48:46 -05:00
parent 47953e8e27
commit 05c0a9dbbf
9 changed files with 183 additions and 74 deletions

View file

@ -1,6 +1,8 @@
canvas: Canvas canvas: Canvas
cut: Cut cut: Cut
cuttingLayout: Suggested Cutting Layout
fabric: Main Fabric fabric: Main Fabric
fabricSize: "{length} of {width} wide material"
heavyCanvas: Heavy Canvas heavyCanvas: Heavy Canvas
interfacing: Interfacing interfacing: Interfacing
lining: Lining lining: Lining

View file

@ -8,6 +8,7 @@ export const plugin = {
['cutlist.removeCut', removeCut], ['cutlist.removeCut', removeCut],
['cutlist.setGrain', setGrain], ['cutlist.setGrain', setGrain],
['cutlist.setCutOnFold', setCutOnFold], ['cutlist.setCutOnFold', setCutOnFold],
['cutlist.getCutFabrics', getCutFabrics],
], ],
} }
@ -79,3 +80,20 @@ function setCutOnFold(store, p1, p2) {
return store 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
}

View file

@ -2,8 +2,13 @@ import Worker from 'web-worker'
import fileSaver from 'file-saver' import fileSaver from 'file-saver'
import { themePlugin } from '@freesewing/plugin-theme' import { themePlugin } from '@freesewing/plugin-theme'
import { pluginI18n } from '@freesewing/plugin-i18n' import { pluginI18n } from '@freesewing/plugin-i18n'
import { pagesPlugin } from '../layout/plugin-layout-part.mjs' import { pagesPlugin, fabricPlugin } from '../layout/plugin-layout-part.mjs'
import { capitalize } from 'shared/utils.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 = { export const exportTypes = {
exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'], exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'],
@ -16,8 +21,49 @@ export const defaultPdfSettings = {
orientation: 'portrait', orientation: 'portrait',
margin: 10, margin: 10,
coverPage: true, 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 * Handle exporting the draft or gist
* format: format to export to * format: format to export to
@ -72,11 +118,7 @@ export const handleExport = async (format, gist, design, t, app, onComplete, onE
gist.embed = false gist.embed = false
// make a pattern instance for export rendering // make a pattern instance for export rendering
const layout = gist.layouts?.printingLayout || gist.layout || true const layout = gist.layouts?.printingLayout || gist.layout || true
let pattern = new design({ ...gist, layout }) let pattern = themedPattern(design, gist, { layout }, format, t)
// add the theme and translation to the pattern
pattern.use(themePlugin, { stripped: format !== 'svg', skipGrid: ['pages'] })
pattern.use(pluginI18n, { t })
// a specified size should override the gist one // a specified size should override the gist one
if (format !== 'pdf') { 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/', '')), design: capitalize(pattern.designConfig.data.name.replace('@freesewing/', '')),
tagline: t('common:sloganCome') + '. ' + t('common:sloganStay'), tagline: t('common:sloganCome') + '. ' + t('common:sloganStay'),
url: window.location.href, 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 // add the svg and pages data to the worker args
workerArgs.pages = pattern.setStores[pattern.activeSet].get('pages') 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 // post a message to the worker with all needed data
worker.postMessage(workerArgs) worker.postMessage(workerArgs)
} catch (err) { } catch (err) {

View file

@ -13,7 +13,7 @@ const logoSvg = `<svg viewBox="0 0 25 25">
* The svg uses mm internally, so when we do spatial reasoning inside the svg, we need to know values in mm * 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 mmToPoints = 2.834645669291339
const lineStart = 50
/** /**
* Freesewing's first explicit class? * Freesewing's first explicit class?
* handles pdf exporting * handles pdf exporting
@ -47,12 +47,14 @@ export class PdfMaker {
svgHeight svgHeight
pageCount = 0 pageCount = 0
lineLevel = 50
constructor({ svg, settings, pages, strings }) { constructor({ svg, settings, pages, strings, cutLayouts }) {
this.settings = settings this.settings = settings
this.pagesWithContent = pages.withContent this.pagesWithContent = pages.withContent
this.svg = svg this.svg = svg
this.strings = strings this.strings = strings
this.cutLayouts = cutLayouts
this.initPdf() this.initPdf()
@ -88,6 +90,7 @@ export class PdfMaker {
/** make the pdf */ /** make the pdf */
async makePdf() { async makePdf() {
await this.generateCoverPage() await this.generateCoverPage()
await this.generateCutLayoutPages()
await this.generatePages() await this.generatePages()
} }
@ -116,15 +119,19 @@ export class PdfMaker {
return return
} }
const headerLevel = await this.generateCoverPageTitle() this.nextPage()
await this.generateCoverPageTitle()
await this.generateSvgPage(this.svg)
}
async generateSvgPage(svg) {
//abitrary margin for visual space //abitrary margin for visual space
let coverMargin = 85 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 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 // 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, width: coverWidth,
height: coverHeight, height: coverHeight,
assumePt: false, assumePt: false,
@ -133,40 +140,50 @@ export class PdfMaker {
}) })
this.pageCount++ this.pageCount++
} }
async generateCoverPageTitle() { async generateCoverPageTitle() {
let lineLevel = 50 this.addText('FreeSewing', 28)
let lineStart = 50 .addText(this.strings.tagline, 12, 20)
.addText(this.strings.design, 48, -8)
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
await SVGtoPDF(this.pdf, logoSvg, this.pdf.page.width - lineStart - 100, lineStart, { await SVGtoPDF(this.pdf, logoSvg, this.pdf.page.width - lineStart - 100, lineStart, {
width: 100, width: 100,
height: lineLevel - lineStart - 8, height: this.lineLevel - lineStart,
preserveAspectRatio: 'xMaxYMin meet', preserveAspectRatio: 'xMaxYMin meet',
}) })
this.pdf.lineWidth(1) this.pdf.lineWidth(1)
this.pdf this.pdf
.moveTo(lineStart, lineLevel - 8) .moveTo(lineStart, this.lineLevel)
.lineTo(this.pdf.page.width - lineStart, lineLevel - 8) .lineTo(this.pdf.page.width - lineStart, this.lineLevel)
.stroke() .stroke()
this.lineLevel += 8
this.pdf.fillColor('#888888') this.pdf.fillColor('#888888')
this.pdf.fontSize(10) this.addText(this.strings.url, 10)
this.pdf.text(this.strings.url, lineStart, lineLevel) }
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 */ /** generate the pages of the pdf */
@ -190,11 +207,7 @@ export class PdfMaker {
let x = -w * this.pageWidth + startMargin let x = -w * this.pageWidth + startMargin
let y = -h * this.pageHeight + startMargin let y = -h * this.pageHeight + startMargin
// if there was no cover page, the first page already exists this.nextPage()
if (this.pageCount > 0) {
// otherwise make a new page
this.pdf.addPage()
}
// add the pdf to the page, offset by the page distances // add the pdf to the page, offset by the page distances
await SVGtoPDF(this.pdf, this.svg, x, y, options) 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
}
} }

View file

@ -4,22 +4,25 @@ import { Draft } from '../draft/index.mjs'
import { fabricPlugin } from '../plugin-layout-part.mjs' import { fabricPlugin } from '../plugin-layout-part.mjs'
import { cutLayoutPlugin } from './plugin-cut-layout.mjs' import { cutLayoutPlugin } from './plugin-cut-layout.mjs'
import { pluginCutlist } from '@freesewing/plugin-cutlist' import { pluginCutlist } from '@freesewing/plugin-cutlist'
import { pluginFlip } from '@freesewing/plugin-flip'
import { measurementAsMm } from 'shared/utils.mjs' import { measurementAsMm } from 'shared/utils.mjs'
import { useEffect } from 'react' import { useEffect } from 'react'
import get from 'lodash.get' import get from 'lodash.get'
const activeFabricPath = ['_state', 'layout', 'forCutting', 'activeFabric'] export const fabricSettingsOrDefault = (gist, fabric) => {
const useFabricSettings = (gist) => {
const isImperial = gist.units === 'imperial' const isImperial = gist.units === 'imperial'
const sheetHeight = measurementAsMm(isImperial ? 36 : 100, gist.units) const sheetHeight = measurementAsMm(isImperial ? 36 : 100, gist.units)
const activeFabric = get(gist, activeFabricPath) || 'fabric' const gistSettings = get(gist, ['_state', 'layout', 'forCutting', 'fabric', fabric])
const gistSettings = get(gist, ['_state', 'layout', 'forCutting', 'fabric', activeFabric])
const sheetWidth = gistSettings?.sheetWidth || measurementAsMm(isImperial ? 54 : 120, gist.units) const sheetWidth = gistSettings?.sheetWidth || measurementAsMm(isImperial ? 54 : 120, gist.units)
const grainDirection = const grainDirection =
gistSettings?.grainDirection === undefined ? 90 : gistSettings.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) => { const useFabricDraft = (gist, design, fabricSettings) => {
@ -42,7 +45,6 @@ const useFabricDraft = (gist, design, fabricSettings) => {
draft.use(cutLayoutPlugin(fabricSettings.activeFabric, fabricSettings.grainDirection)) draft.use(cutLayoutPlugin(fabricSettings.activeFabric, fabricSettings.grainDirection))
// also, pluginCutlist and pluginFlip are needed // also, pluginCutlist and pluginFlip are needed
draft.use(pluginCutlist) draft.use(pluginCutlist)
draft.use(pluginFlip)
// draft the pattern // draft the pattern
draft.draft() draft.draft()
@ -55,17 +57,7 @@ const useFabricDraft = (gist, design, fabricSettings) => {
} }
const useFabricList = (draft) => { const useFabricList = (draft) => {
const cutList = draft.setStores[0].get('cutlist') return draft.setStores[0].cutlist.getCutFabrics(draft.settings[0])
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
} }
const bgProps = { fill: 'none' } const bgProps = { fill: 'none' }

View file

@ -1,8 +1,11 @@
import { addToOnly } from '../plugin-layout-part.mjs' import { addToOnly } from '../plugin-layout-part.mjs'
import { pluginFlip } from '@freesewing/plugin-flip'
import { pluginMirror } from '@freesewing/plugin-mirror'
const prefix = 'mirroredOnFold' const prefix = 'mirroredOnFold'
// types of path operations // types of path operations
const opTypes = ['to', 'cp1', 'cp2'] 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 * 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: { macros: {
...pluginFlip.macros,
...pluginMirror.macros,
// handle mirroring on the fold and rotating to sit along the grain or bias // handle mirroring on the fold and rotating to sit along the grain or bias
handleFoldAndGrain: ({ partCutlist, grainSpec, ignoreOnFold, bias }, { points, macro }) => { handleFoldAndGrain: ({ partCutlist, grainSpec, ignoreOnFold, bias }, { points, macro }) => {
// if the part has cutonfold instructions // if the part has cutonfold instructions
@ -114,8 +119,7 @@ export const cutLayoutPlugin = function (material, grainAngle) {
const mirrorPaths = [] const mirrorPaths = []
for (const p in paths) { for (const p in paths) {
// skip ones that are hidden // skip ones that are hidden
if (!paths[p].hidden && !p.match(/^(cutonfold|grainline|__scalebox|__miniscale)/)) if (!paths[p].hidden && !p.match(avoidRegx)) mirrorPaths.push(paths[p])
mirrorPaths.push(paths[p])
} }
// store all the points to mirror // store all the points to mirror

View file

@ -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 // regular conversion from mm to inches or cm
const unit = isImperial ? 25.4 : 10 const unit = isImperial ? 25.4 : 10
// conversion from inches or cm to yards or meters // conversion from inches or cm to yards or meters

View file

@ -179,11 +179,11 @@ const basePlugin = ({
pattern.draftPartForSet(partName, pattern.activeSet) 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 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(partName)
const generatedPageData = pattern.setStores[pattern.activeSet].get('pages') if (setPatternSize === true || setPatternSize === 'width')
pattern.width = sheetWidth * generatedPageData.cols pattern.width = Math.max(pattern.width, sheetWidth * generatedPageData.cols)
pattern.height = sheetHeight * generatedPageData.rows if (setPatternSize === true || setPatternSize === 'height')
} pattern.height = Math.max(pattern.height, sheetHeight * generatedPageData.rows)
removeFromOnly(pattern, partName) removeFromOnly(pattern, partName)
}, },

View file

@ -24,6 +24,12 @@ export const PrintLayoutSettings = (props) => {
) )
} }
const setCutlist = () => {
props.updateGist(
['_state', 'layout', 'forPrinting', 'page', 'cutlist'],
!props.layoutSettings.cutlist
)
}
return ( return (
<div> <div>
<div <div
@ -33,13 +39,6 @@ export const PrintLayoutSettings = (props) => {
<div className="flex gap-4"> <div className="flex gap-4">
<PageSizePicker {...props} /> <PageSizePicker {...props} />
<PageOrientationPicker {...props} /> <PageOrientationPicker {...props} />
</div>
<div className="flex gap-4">
<ShowButtonsToggle
gist={props.gist}
updateGist={props.updateGist}
layoutSetType="forPrinting"
></ShowButtonsToggle>
<button <button
key="export" key="export"
onClick={props.exportIt} onClick={props.exportIt}
@ -48,8 +47,15 @@ export const PrintLayoutSettings = (props) => {
aria-disabled={count === 0} aria-disabled={count === 0}
> >
<ExportIcon className="h-6 w-6 mr-2" /> <ExportIcon className="h-6 w-6 mr-2" />
{t('export')} {`${t('export')} PDF`}
</button> </button>
</div>
<div className="flex gap-4">
<ShowButtonsToggle
gist={props.gist}
updateGist={props.updateGist}
layoutSetType="forPrinting"
></ShowButtonsToggle>
<button <button
key="reset" key="reset"
onClick={() => props.unsetGist(['layouts', 'printingLayout'])} onClick={() => props.unsetGist(['layouts', 'printingLayout'])}
@ -62,8 +68,8 @@ export const PrintLayoutSettings = (props) => {
</div> </div>
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<div className="flex flex-row"> <div className="flex flex-row">
<label htmlFor="pageMargin" className="label mr-6"> <label htmlFor="pageMargin" className="label">
<span className="mr-2">{t('pageMargin')}</span> <span className="">{t('pageMargin')}</span>
<input <input
type="range" type="range"
max={50} max={50}
@ -95,6 +101,15 @@ export const PrintLayoutSettings = (props) => {
onChange={setCoverPage} onChange={setCoverPage}
/> />
</label> </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>
<div className="flex flex-row font-bold items-center px-0 text-xl"> <div className="flex flex-row font-bold items-center px-0 text-xl">
<PrintIcon /> <PrintIcon />