add cutting layouts to pdf export
This commit is contained in:
parent
47953e8e27
commit
05c0a9dbbf
9 changed files with 183 additions and 74 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
* */
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -24,6 +24,12 @@ export const PrintLayoutSettings = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
const setCutlist = () => {
|
||||
props.updateGist(
|
||||
['_state', 'layout', 'forPrinting', 'page', 'cutlist'],
|
||||
!props.layoutSettings.cutlist
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
|
@ -33,13 +39,6 @@ export const PrintLayoutSettings = (props) => {
|
|||
<div className="flex gap-4">
|
||||
<PageSizePicker {...props} />
|
||||
<PageOrientationPicker {...props} />
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<ShowButtonsToggle
|
||||
gist={props.gist}
|
||||
updateGist={props.updateGist}
|
||||
layoutSetType="forPrinting"
|
||||
></ShowButtonsToggle>
|
||||
<button
|
||||
key="export"
|
||||
onClick={props.exportIt}
|
||||
|
@ -48,8 +47,15 @@ export const PrintLayoutSettings = (props) => {
|
|||
aria-disabled={count === 0}
|
||||
>
|
||||
<ExportIcon className="h-6 w-6 mr-2" />
|
||||
{t('export')}
|
||||
{`${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'])}
|
||||
|
@ -62,8 +68,8 @@ export const PrintLayoutSettings = (props) => {
|
|||
</div>
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row">
|
||||
<label htmlFor="pageMargin" className="label mr-6">
|
||||
<span className="mr-2">{t('pageMargin')}</span>
|
||||
<label htmlFor="pageMargin" className="label">
|
||||
<span className="">{t('pageMargin')}</span>
|
||||
<input
|
||||
type="range"
|
||||
max={50}
|
||||
|
@ -95,6 +101,15 @@ export const PrintLayoutSettings = (props) => {
|
|||
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 />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue