diff --git a/sites/lab/package.json b/sites/lab/package.json index 690e5156b5f..39bc676b16d 100644 --- a/sites/lab/package.json +++ b/sites/lab/package.json @@ -55,5 +55,8 @@ }, "browserslist": [ "last 2 versions" - ] + ], + "engines": { + "node": "14.x" + } } diff --git a/sites/lab/page-templates/workbench.js b/sites/lab/page-templates/workbench.js index cce0452eac7..8ee968dc216 100644 --- a/sites/lab/page-templates/workbench.js +++ b/sites/lab/page-templates/workbench.js @@ -1,6 +1,6 @@ import Page from 'site/components/wrappers/page.js' import useApp from 'site/hooks/useApp.js' -import WorkbenchWrapper from 'shared/components/wrappers/workbench.js' +import WorkbenchWrapper from 'shared/components/wrappers/workbench' import { useRouter } from 'next/router' import Layout from 'site/components/layouts/lab' diff --git a/sites/shared/components/workbench/exporting/export_worker.js b/sites/shared/components/workbench/exporting/export_worker.js new file mode 100644 index 00000000000..05c3b6f0fb0 --- /dev/null +++ b/sites/shared/components/workbench/exporting/export_worker.js @@ -0,0 +1,56 @@ +import yaml from 'js-yaml' +import axios from 'axios' +import PdfExporter from './pdfExporter' + +addEventListener('message', async(e) => { + const {format, gist, svg} = e.data + // handle the data exports + + if (format === 'json') return exportJson(gist) + if (format === 'yaml') return exportYaml(gist) + if (format === 'github gist') return exportGithubGist(gist) + + if (format === 'svg') return exportSvg(gist, svg) + + new PdfExporter(e.data).export(postSuccess) +}) + +const postSuccess = (blob) => { + postMessage({success: true, blob}) + close() +} + +const exportJson = gist => { + const blob = new Blob([JSON.stringify(gist, null, 2)], { + type: 'application/json;charset=utf-8' + }) + postSuccess(blob) +} + +const exportYaml = gist => { + const blob = new Blob([yaml.dump(gist)], { + type: 'application/x-yaml;charset=utf-8' + }) + postSuccess(blob) +} + +const exportSvg = (gist, svg) => { + const blob = new Blob([svg], { + type: 'image/svg+xml;charset=utf-8' + }) + postSuccess(blob) +} + +const exportGithubGist = (data) => { + axios.post('https://backend.freesewing.org/github/gist', { + design: data.design, + data: yaml.dump(data) + }) + .then(res => { + postMessage({ + success: true, + link: 'https://gist.github.com/' + res.data.id + }) + }) + .catch(err => console.log(err)) +} diff --git a/sites/shared/components/workbench/exporting/index.js b/sites/shared/components/workbench/exporting/index.js index 0be78905b4a..b43e5dc8216 100644 --- a/sites/shared/components/workbench/exporting/index.js +++ b/sites/shared/components/workbench/exporting/index.js @@ -1,13 +1,11 @@ import { useState } from 'react' import { useTranslation } from 'next-i18next' -import fileSaver from 'file-saver' -import yaml from 'js-yaml' -import axios from 'axios' import Popout from 'shared/components/popout' import WebLink from 'shared/components/web-link' +import Worker from 'web-worker'; +import fileSaver from 'file-saver' import theme from '@freesewing/plugin-theme' import {pagesPlugin} from '../layout/print/plugin' -import PdfExporter from './pdfExporter' export const exports = { exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'], @@ -22,91 +20,73 @@ export const defaultPdfSettings = { coverPage: true } -export const handleExport = (format, gist, design, t, app, setLink, setFormat) => { +export const handleExport = async(format, gist, design, t, app, onComplete) => { + app.startLoading(); - // handle state setting if there's supposed to be any - setLink && setLink(false) - setFormat && setFormat(format) + const worker = new Worker(new URL('./export_worker.js', import.meta.url), {type: module}); - // handle the data exports - if (exports.exportAsData.indexOf(format) !== -1) { - if (format === 'json') exportJson(gist) - else if (format === 'yaml') exportYaml(gist) - else if (format === 'github gist') exportGithubGist(gist, app, setLink) - - return - } - - gist.embed=false - let svg = '' - - // 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(theme, {stripped: format !== 'svg'}) - pattern.use({ - hooks: { - insertText: (locale, text, {t}) => t(text) + worker.addEventListener('message', e => { + if (e.data.blob) { + const fileType = exports.exportForPrinting.indexOf(format) === -1 ? format : 'pdf' + fileSaver.saveAs(e.data.blob, `freesewing-${gist.design || 'gist'}.${fileType}`) } - },{t}) + app.stopLoading() + onComplete && onComplete(e) + }) + let svg = '' // pdf settings const settings = { ...defaultPdfSettings, ...(gist._state.layout?.forPrinting?.page || {}) } + const workerArgs = {format, gist, settings} - // a specified size should override the gist one - if (format !== 'pdf') { - settings.size = format - } + if (exports.exportAsData.indexOf(format) === -1) { + // gist.embed=false + // make a pattern instance for export rendering + const layout = gist.layouts?.printingLayout || gist.layout || true + let pattern = new design({...gist, layout}) - try { - // add pages to pdf exports - if (format !== 'svg') { - pattern.use(pagesPlugin(settings, true)) + + // add the theme and translation to the pattern + pattern.use(theme, {stripped: format !== 'svg'}) + pattern.use({ + hooks: { + insertText: (locale, text, {t}) => t(text) + } + },{t}) + + // a specified size should override the gist one + if (format !== 'pdf') { + settings.size = format + } + + try { + // add pages to pdf exports + if (format !== 'svg') { + pattern.use(pagesPlugin({ + ...settings, + printStyle: true, + renderBlanks: false, + setPatternSize: true + })) + } + + pattern.draft(); + svg = pattern.render() + workerArgs.svg = svg + if (pattern.parts.pages) { + workerArgs.pages = pattern.parts.pages.pages + } + } catch(err) { + console.log(err) + app.stopLoading(); } - pattern.draft(); - svg = pattern.render() - } catch(err) { - console.log(err) } - if (format === 'svg') return exportSvg(gist, svg) - - return new PdfExporter(gist.design, pattern, svg, settings).export(); -} - -const exportJson = gist => { - const blob = new Blob([JSON.stringify(gist, null, 2)], { - type: 'application/json;charset=utf-8' - }) - fileSaver.saveAs(blob, `freesewing-${gist.design || 'gist'}.json`) -} -const exportYaml = gist => { - const blob = new Blob([yaml.dump(gist)], { - type: 'application/x-yaml;charset=utf-8' - }) - fileSaver.saveAs(blob, `freesewing-${gist.design || 'gist'}.yaml`) -} -const exportSvg = (gist, svg) => { - const blob = new Blob([svg], { - type: 'image/svg+xml;charset=utf-8' - }) - fileSaver.saveAs(blob, `freesewing-${gist.design || 'pattern'}.svg`) -} -const exportGithubGist = (data, app, setLink) => { - app.setLoading(true) - axios.post('https://backend.freesewing.org/github/gist', { - design: data.design, - data: yaml.dump(data) - }) - .then(res => setLink('https://gist.github.com/' + res.data.id)) - .catch(err => console.log(err)) - .finally(() => app.stopLoading()) + worker.postMessage(workerArgs) } const ExportDraft = ({ gist, design, app }) => { @@ -115,6 +95,13 @@ const ExportDraft = ({ gist, design, app }) => { const [format, setFormat] = useState(false) const { t } = useTranslation(['app']) + const doExport = (format) => { + setLink(false) + setFormat(format) + handleExport(format, gist, design, t, app, (e) => { + if (e.data.link) {setLink(e.data.link)} + }) + } return (
@@ -135,7 +122,7 @@ const ExportDraft = ({ gist, design, app }) => { {exports[type].map(format => ( diff --git a/sites/shared/components/workbench/exporting/pdfExporter.js b/sites/shared/components/workbench/exporting/pdfExporter.js index 51fa7f49662..9a607a3978a 100644 --- a/sites/shared/components/workbench/exporting/pdfExporter.js +++ b/sites/shared/components/workbench/exporting/pdfExporter.js @@ -13,7 +13,8 @@ const pxToPoints = (72/96); // multiply a mm value by this to get a pixel value const mmToPx = 3.77953 // multiply a mm value by this to get a points value -const mmToPoints = mmToPx * pxToPoints +// const mmToPoints = mmToPx * pxToPoints +const mmToPoints = 2.834645669291339 /** * Freesewing's first explicit class? @@ -30,6 +31,7 @@ export default class Exporter { pattern /** the pdfKit instance that is writing the document */ pdf + buffers /** the usable width (excluding margin) of the pdf page, in points */ pageWidth @@ -51,36 +53,40 @@ export default class Exporter { pagesWithContent = {} - constructor(designName, pattern, svg, settings) { - // default just in case - this.designName = designName || 'freesewing' + constructor({svg, settings, pages}) { this.settings = settings - this.pattern = pattern - + this.pagesWithContent = pages.withContent; + this.svg = svg this.createPdf() this.margin = this.settings.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 // this is in points because it comes from pdfKit - this.pageWidth = this.pdf.page.width - this.margin// this is in points because it comes from pdfKit + 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 + // console.log(pages.width * pages.cols, pages.height * pages.rows, this.pageWidth, this.pageHeight) + // console.log(svg) // we pass the svg as a string, so we need to make it a DOM element so we can manipulate it - const divElem = document.createElement('div'); - divElem.innerHTML = svg; - this.svg = divElem.firstElementChild; + // const divElem = document.createElement('div'); + // divElem.innerHTML = svg; + // this.svg = divElem.firstElementChild; // get the pages data - const pages = this.pattern.parts.pages.pages this.columns = pages.cols this.rows = pages.rows // then set the svg's width and height in points to include all pages in full (the original svg will have been set to show only as much page as is occupied) - this.svgWidth = this.columns * this.pageWidth - this.svgHeight = this.rows * this.pageHeight - this.svg.setAttribute('height', this.svgWidth + 'pt') - this.svg.setAttribute('width', this.svgHeight + 'pt') + this.svgWidth = this.columns * pages.width * mmToPoints + this.svgHeight = this.rows * pages.height * mmToPoints + + console.log(this.columns, this.rows, pages.width, pages.height, this.svgWidth, this.svgHeight) + console.log(svg) + // this.svg.replace(/width="\d+mm"/, `width="${this.svgWidth}pt"`) + // this.svg.replace(/height="\d+mm"/, `height="${this.svgHeight}pt"`) + // this.svg.setAttribute('height', this.svgWidth + 'pt') + // this.svg.setAttribute('width', this.svgHeight + 'pt') // set the viewbox to include all pages in full as well, this time in mm - this.svg.setAttribute('viewBox', `0 0 ${this.columns * this.pageWidthInMm} ${this.rows * this.pageHeightInMm}`) + // this.svg.setAttribute('viewBox', `0 0 ${this.columns * this.pageWidthInMm} ${this.rows * this.pageHeightInMm}`) } /** pdf page usable (excluding margin) width, in mm */ @@ -99,18 +105,9 @@ export default class Exporter { // PdfKit wants to flush the buffer on each new page. // We don't want to try to save the document until it's complete, so we have to manage the buffers ourselves - const buffers = []; + this.buffers = []; // add new data to our buffer storage - this.pdf.on('data', buffers.push.bind(buffers)); - // when the end event fires, then we save the whole thing - this.pdf.on('end', () => { - // convert buffers to a blob - const blob = new Blob(buffers, { - type: 'application/pdf' - }) - // save the blob - fileSaver.saveAs(blob, `freesewing-${this.designName}.pdf`) - }); + this.pdf.on('data', this.buffers.push.bind(this.buffers)); } /** @@ -197,8 +194,16 @@ export default class Exporter { } /** export to pdf */ - async export() { - this.scanPages() + async export(onComplete) { + // when the end event fires, then we save the whole thing + this.pdf.on('end', () => { + // convert buffers to a blob + const blob = new Blob(this.buffers, { + type: 'application/pdf' + }) + onComplete(blob) + }); + await this.generateCoverPage() await this.generatePages(); this.save() @@ -218,7 +223,7 @@ export default class Exporter { let coverWidth = this.pageWidth - 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.outerHTML, coverMargin, coverMargin, { + await SVGtoPDF(this.pdf, this.svg, coverMargin, coverMargin, { width: coverWidth, height: coverHeight, assumePt: true, @@ -234,11 +239,12 @@ export default class Exporter { assumePt: true, width: this.svgWidth, height: this.svgHeight, - preserveAspectRatio: 'xMinYMin slice' + preserveAspectRatio: 'xMinYMin slice', + useCSS: true } // everything is offset by half a margin so that it's centered on the page - const startMargin = 0.5 * this.margin + const startMargin = this.margin for (var h = 0; h < this.rows; h++) { for (var w = 0; w < this.columns; w++) { // skip empty pages @@ -255,7 +261,7 @@ export default class Exporter { } // add the pdf to the page, offset by the page distances - await SVGtoPDF(this.pdf, this.svg.outerHTML, x, y, options) + await SVGtoPDF(this.pdf, this.svg, x, y, options) } } } diff --git a/sites/shared/components/workbench/layout/draft/index.js b/sites/shared/components/workbench/layout/draft/index.js index 455f043e835..288a1798ed2 100644 --- a/sites/shared/components/workbench/layout/draft/index.js +++ b/sites/shared/components/workbench/layout/draft/index.js @@ -7,6 +7,8 @@ import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch" const Draft = props => { const { draft, patternProps, gist, updateGist, app, bgProps={}, fitLayoutPart = false, layoutType="printingLayout"} = 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 let layout = draft.settings.layout === true ? { ...patternProps.autoLayout, @@ -14,9 +16,7 @@ const Draft = props => { height: patternProps.height } : {...draft.settings.layout} - const svgRef = useRef(null); - if (!patternProps) return null // Helper method to update part layout and re-calculate width * height const updateLayout = (name, config, history=true) => { diff --git a/sites/shared/components/workbench/layout/print/index.js b/sites/shared/components/workbench/layout/print/index.js index 848681f58c9..d3687ca16fe 100644 --- a/sites/shared/components/workbench/layout/print/index.js +++ b/sites/shared/components/workbench/layout/print/index.js @@ -35,7 +35,7 @@ const PrintLayout = props => { const bgProps = { fill: "url(#page)" } const exportIt = () => { - handleExport('pdf', props.gist, props.design, t) + handleExport('pdf', props.gist, props.design, t, props.app) } return ( diff --git a/sites/shared/components/workbench/layout/print/plugin.js b/sites/shared/components/workbench/layout/print/plugin.js index b30259631d1..10e99e122a7 100644 --- a/sites/shared/components/workbench/layout/print/plugin.js +++ b/sites/shared/components/workbench/layout/print/plugin.js @@ -16,15 +16,55 @@ const indexLetter = (i) => String.fromCharCode('A'.charCodeAt(0) + i - 1) /** * A plugin to add printer pages * */ -export const pagesPlugin = ({size='a4', orientation='portrait', margin=10}, printStyle = false /** should the pages be rendered for printing or for screen viewing? */ ) => { +export const pagesPlugin = ({ + size='a4', + orientation='portrait', + margin=10, + printStyle = false, /** should the pages be rendered for printing or for screen viewing? */ + setPatternSize = true +}) => { const ls = orientation === 'landscape' - let sheetHeight = sizes[size][ls ? 1 : 0] - let sheetWidth = sizes[size][ls ? 0 : 1] - sheetWidth -= margin - sheetHeight -= margin - return basePlugin({sheetWidth, sheetHeight, orientation, printStyle}) + let sheetHeight = sizes[size][ls ? 0 : 1] + let sheetWidth = sizes[size][ls ? 1 : 0] + sheetWidth -= margin * 2 + sheetHeight -= margin * 2 + return basePlugin({sheetWidth, sheetHeight, orientation, printStyle, setPatternSize}) } +const doScanForBlanks = (parts, layout, x, y, w, h) => { + let hasContent = false + for (var p in parts) { + let part = parts[p] + // skip the pages part and any that aren't rendered + if (part === this || part.render === false || part.isEmpty()) continue + + // get the position of the part + let partLayout = layout.parts[p] + let partMinX = (partLayout.tl?.x || (partLayout.move.x + part.topLeft.x)) + let partMinY = (partLayout.tl?.y || (partLayout.move.y + part.topLeft.y)) + let partMaxX = (partLayout.br?.x || (partMinX + part.width)) + let partMaxY = (partLayout.br?.y || (partMinY + part.height)) + + // check if the part overlaps the page extents + if ( + // if the left of the part is further left than the right end of the page + partMinX < x + w && + // and the top of the part is above the bottom of the page + partMinY < y + h && + // and the right of the part is further right than the left of the page + partMaxX > x && + // and the bottom of the part is below the top to the page + partMaxY > y + ) { + // the part has content inside the page + hasContent = true; + // so we stop looking + break; + } + } + + return hasContent +} /** * The base plugin for adding a layout helper part like pages or fabric * sheetWidth: the width of the helper part @@ -39,7 +79,10 @@ const basePlugin = ({ boundary=false, partName="pages", responsiveColumns=true, - printStyle=false + printStyle=false, + scanForBlanks=true, + renderBlanks=true, + setPatternSize=false }) => ({ name, version, @@ -62,9 +105,13 @@ const basePlugin = ({ responsiveColumns && (width += pattern.settings.layout.topLeft.x) } - macro('addPages', { size: [sheetWidth, sheetHeight], height, width }) + macro('addPages', { size: [sheetHeight,sheetWidth, ], height, width }) if (boundary) pattern.parts[partName].boundary(); + if (setPatternSize) { + pattern.width = sheetWidth * pattern.parts[partName].pages.cols + pattern.height = sheetHeight * pattern.parts[partName].pages.rows + } } }, macros: { @@ -74,13 +121,22 @@ const basePlugin = ({ const cols = Math.ceil(so.width / w) const rows = Math.ceil(so.height / h) const { points, Point, paths, Path, macro } = this.shorthand() - let x = 0 - let y = 0 let count = 0 + let withContent = {} + // get the layout from the pattern + const layout = typeof this.context.settings.layout === 'object' ? this.context.settings.layout : this.context.autoLayout; for (let row=0;row { { const pageTemplate = design => `${header} import design from 'designs/${design}/src/index.js' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -import PageTemplate from 'site/page-templates/workbench.js' +import PageTemplate from 'site/page-templates/workbench' const Page = (props) => export default Page diff --git a/yarn.lock b/yarn.lock index 7126618a43a..33c3a0a0969 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24378,6 +24378,11 @@ web-streams-polyfill@^3.0.3: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== +web-worker@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da" + integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"