Merge pull request #3919 from freesewing/develop
Get up to date with develop
This commit is contained in:
commit
e9ab680c53
394 changed files with 9683 additions and 6225 deletions
|
@ -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 { 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 { capitalize, formatMm } from 'shared/utils.mjs'
|
||||
import get from 'lodash.get'
|
||||
|
||||
export const exportTypes = {
|
||||
exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'],
|
||||
|
@ -16,8 +21,76 @@ export const defaultPdfSettings = {
|
|||
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 {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 })
|
||||
|
||||
// add the theme and translation to the pattern
|
||||
pattern.use(themePlugin, { stripped: format !== 'svg', skipGrid: ['pages'] })
|
||||
pattern.use(pluginI18n, { t })
|
||||
pattern.use(pluginAnnotations)
|
||||
|
||||
return pattern
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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
|
||||
*/
|
||||
const generateCutLayouts = (pattern, design, gist, format, t) => {
|
||||
// get the fabrics from the already drafted base pattern
|
||||
const fabrics = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics(
|
||||
pattern.settings[0]
|
||||
) || ['fabric']
|
||||
if (!fabrics.length) return
|
||||
|
||||
const isImperial = gist.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)
|
||||
|
||||
// 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' }))
|
||||
|
||||
// draft and render
|
||||
fabricPattern.draft()
|
||||
const svg = fabricPattern.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'),
|
||||
interpolation: { escapeValue: false },
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
return cutLayouts
|
||||
}
|
||||
/**
|
||||
* Handle exporting the draft or gist
|
||||
* format: format to export to
|
||||
|
@ -72,11 +145,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 +169,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 +180,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
|
||||
|
@ -29,6 +29,8 @@ export class PdfMaker {
|
|||
buffers
|
||||
/** translated strings to add to the cover page */
|
||||
strings
|
||||
/** cutting layout svgs and strings */
|
||||
cutLayouts
|
||||
|
||||
/** the usable width (excluding margin) of the pdf page, in points */
|
||||
pageWidth
|
||||
|
@ -47,12 +49,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 +92,7 @@ export class PdfMaker {
|
|||
/** make the pdf */
|
||||
async makePdf() {
|
||||
await this.generateCoverPage()
|
||||
await this.generateCutLayoutPages()
|
||||
await this.generatePages()
|
||||
}
|
||||
|
||||
|
@ -116,57 +121,78 @@ export class PdfMaker {
|
|||
return
|
||||
}
|
||||
|
||||
const headerLevel = await this.generateCoverPageTitle()
|
||||
this.nextPage()
|
||||
await this.generateCoverPageTitle()
|
||||
await this.generateSvgPage(this.svg)
|
||||
}
|
||||
|
||||
/** generate a page that has an svg centered in it below any text */
|
||||
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,
|
||||
// use aspect ratio to center it
|
||||
preserveAspectRatio: 'xMidYMid meet',
|
||||
})
|
||||
|
||||
// increment page count
|
||||
this.pageCount++
|
||||
}
|
||||
|
||||
/** generate the title for the cover page */
|
||||
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
|
||||
/** generate the title for a cutting layout page */
|
||||
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)
|
||||
}
|
||||
|
||||
/** generate all cutting layout pages */
|
||||
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 +216,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 +224,30 @@ export class PdfMaker {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset to a clean page */
|
||||
nextPage() {
|
||||
// set the line level back to the top
|
||||
this.lineLevel = lineStart
|
||||
|
||||
// if no pages have been made, we can use the current
|
||||
if (this.pageCount === 0) return
|
||||
|
||||
// otherwise make a new page
|
||||
this.pdf.addPage()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Text to the page at the current line level
|
||||
* @param {String} text the text to add
|
||||
* @param {Number} fontSize the size for the text
|
||||
* @param {Number} marginBottom additional margin to add below the text
|
||||
*/
|
||||
addText(text, fontSize, marginBottom = 0) {
|
||||
this.pdf.fontSize(fontSize)
|
||||
this.pdf.text(text, 50, this.lineLevel)
|
||||
|
||||
this.lineLevel += fontSize + marginBottom
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,23 +3,26 @@ 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 { pluginCutlist } from '@freesewing/plugin-cutlist'
|
||||
import { pluginFlip } from '@freesewing/plugin-flip'
|
||||
import { pluginAnnotations } from '@freesewing/plugin-annotations'
|
||||
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) => {
|
||||
|
@ -40,9 +43,8 @@ const useFabricDraft = (gist, design, fabricSettings) => {
|
|||
draft.use(fabricPlugin(layoutSettings))
|
||||
// add the cutLayout plugin
|
||||
draft.use(cutLayoutPlugin(fabricSettings.activeFabric, fabricSettings.grainDirection))
|
||||
// also, pluginCutlist and pluginFlip are needed
|
||||
draft.use(pluginCutlist)
|
||||
draft.use(pluginFlip)
|
||||
// also, pluginAnnotations and pluginFlip are needed
|
||||
draft.use(pluginAnnotations)
|
||||
|
||||
// draft the pattern
|
||||
draft.draft()
|
||||
|
@ -55,15 +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) {
|
||||
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,7 +1,10 @@
|
|||
import { addToOnly } from '../plugin-layout-part.mjs'
|
||||
import { pluginMirror } from '@freesewing/plugin-mirror'
|
||||
const prefix = 'mirroredOnFold'
|
||||
|
||||
// types of path operations
|
||||
const opTypes = ['to', 'cp1', 'cp2']
|
||||
const opTypes = ['to', 'from', '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
|
||||
|
@ -14,12 +17,16 @@ export const cutLayoutPlugin = function (material, grainAngle) {
|
|||
hooks: {
|
||||
// after each part
|
||||
postPartDraft: (pattern) => {
|
||||
// get the part that's just been drafted
|
||||
const part = pattern.parts[pattern.activeSet][pattern.activePart]
|
||||
// if it's a duplicated cut part, the fabric part, or it's hidden, leave it alone
|
||||
if (pattern.activePart.startsWith('cut.') || pattern.activePart === 'fabric' || part.hidden)
|
||||
// if it's a duplicated cut part, the fabric part, or it's not wanted by the pattern
|
||||
if (
|
||||
pattern.activePart.startsWith('cut.') ||
|
||||
pattern.activePart === 'fabric' ||
|
||||
!pattern.__wants(pattern.activePart)
|
||||
)
|
||||
return
|
||||
|
||||
// get the part that's just been drafted
|
||||
const part = pattern.parts[pattern.activeSet][pattern.activePart]
|
||||
// get this part's cutlist configuration
|
||||
let partCutlist = pattern.setStores[pattern.activeSet].get(['cutlist', pattern.activePart])
|
||||
// if there isn't one, we're done here
|
||||
|
@ -33,61 +40,70 @@ export const cutLayoutPlugin = function (material, grainAngle) {
|
|||
return
|
||||
}
|
||||
|
||||
// get the cutlist configuration for this material
|
||||
const matCutConfig = partCutlist.materials?.[material]
|
||||
// if there's specific instructions for this material
|
||||
if (matCutConfig) {
|
||||
// get the config of the active part to be inherited by all duplicates
|
||||
const activePartConfig = pattern.config.parts[pattern.activePart]
|
||||
// get the cutlist configuration for this material, or default to one
|
||||
const matCutConfig =
|
||||
partCutlist.materials?.[material] || (material === 'fabric' ? [{ cut: 1 }] : [])
|
||||
|
||||
// hide the active part so that all others can inherit from it and be manipulated separately
|
||||
part.hide()
|
||||
// get the config of the active part to be inherited by all duplicates
|
||||
const activePartConfig = pattern.config.parts[pattern.activePart]
|
||||
|
||||
// for each set of cutting instructions for this material
|
||||
matCutConfig.forEach(({ cut, identical, bias, ignoreOnFold }, i) => {
|
||||
// get the grain angle for the part for this set of instructions
|
||||
const cGrain = partCutlist.grain ? partCutlist.grain + (bias ? 45 : 0) : undefined
|
||||
// hide the active part so that all others can inherit from it and be manipulated separately
|
||||
part.hide()
|
||||
|
||||
// for each piece that should be cut
|
||||
for (let c = 0; c < cut; c++) {
|
||||
const dupPartName = `cut.${pattern.activePart}.${material}_${c + i + 1}`
|
||||
// for each set of cutting instructions for this material
|
||||
matCutConfig.forEach((instruction, i) => {
|
||||
// for each piece that should be cut
|
||||
for (let c = 0; c < instruction.cut; c++) {
|
||||
const dupPartName = `cut.${pattern.activePart}.${material}_${c + i + 1}`
|
||||
|
||||
// make a new part that will follow these cutting instructions
|
||||
pattern.addPart({
|
||||
name: dupPartName,
|
||||
from: activePartConfig,
|
||||
draft: ({ part, macro }) => {
|
||||
// handle fold and grain for these cutting instructions
|
||||
macro('handleFoldAndGrain', {
|
||||
partCutlist,
|
||||
grainSpec: cGrain,
|
||||
ignoreOnFold,
|
||||
bias,
|
||||
})
|
||||
// make a new part that will follow these cutting instructions
|
||||
pattern.addPart({
|
||||
name: dupPartName,
|
||||
from: activePartConfig,
|
||||
draft: ({ part, macro, utils }) => {
|
||||
part.attributes.remove('transform')
|
||||
|
||||
// if they shouldn't be identical, flip every other piece
|
||||
if (!identical && c % 2 === 1) macro('flip')
|
||||
// if they shouldn't be identical, flip every other piece
|
||||
if (!instruction.identical && c % 2 === 1) {
|
||||
part.attributes.add(
|
||||
'transform',
|
||||
grainAngle === 90 ? 'scale(-1, 1)' : 'scale(1, -1)'
|
||||
)
|
||||
}
|
||||
|
||||
return part
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// if there wasn't a specific configuration, still make sure to handle fold and grain
|
||||
else {
|
||||
const { macro } = part.shorthand()
|
||||
macro('handleFoldAndGrain', { partCutlist, grainSpec: partCutlist.grain })
|
||||
}
|
||||
macro('handleFoldAndGrain', {
|
||||
partCutlist,
|
||||
instruction,
|
||||
})
|
||||
|
||||
// combine the transforms
|
||||
const combinedTransform = utils.combineTransforms(
|
||||
part.attributes.getAsArray('transform')
|
||||
)
|
||||
part.attributes.set('transform', combinedTransform)
|
||||
|
||||
return part
|
||||
},
|
||||
})
|
||||
|
||||
// add it to the only list if there is one
|
||||
addToOnly(pattern, dupPartName)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
macros: {
|
||||
...pluginMirror.macros,
|
||||
// handle mirroring on the fold and rotating to sit along the grain or bias
|
||||
handleFoldAndGrain: ({ partCutlist, grainSpec, ignoreOnFold, bias }, { points, macro }) => {
|
||||
handleFoldAndGrain: ({ partCutlist, instruction }, { points, macro }) => {
|
||||
// get the grain angle for the part for this set of instructions
|
||||
const grainSpec = partCutlist.grain
|
||||
? partCutlist.grain + (instruction.bias ? 45 : 0)
|
||||
: undefined
|
||||
// if the part has cutonfold instructions
|
||||
if (partCutlist.cutOnFold) {
|
||||
// if we're not meant to igore those instructions, mirror on the fold
|
||||
if (!ignoreOnFold) macro('mirrorOnFold', { fold: partCutlist.cutOnFold })
|
||||
if (!instruction.ignoreOnFold) macro('mirrorOnFold', { fold: partCutlist.cutOnFold })
|
||||
// if we are meant to ignore those instructions, but there's a grainline
|
||||
else if (grainSpec !== undefined) {
|
||||
// replace the cutonfold with a grainline
|
||||
|
@ -97,17 +113,15 @@ export const cutLayoutPlugin = function (material, grainAngle) {
|
|||
}
|
||||
|
||||
// if there's a grain angle, rotate the part to be along it
|
||||
if (grainSpec !== undefined)
|
||||
macro('rotateToGrain', { grainAngle, bias, partGrain: grainSpec })
|
||||
macro('rotateToGrain', { bias: instruction.bias, grainSpec })
|
||||
},
|
||||
// mirror the part across the line indicated by cutonfold
|
||||
mirrorOnFold: ({ fold }, { paths, snippets, utils, macro, points }) => {
|
||||
mirrorOnFold: ({ fold }, { paths, snippets, macro, points, utils, Point }) => {
|
||||
// get all the paths to mirror
|
||||
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(p)
|
||||
}
|
||||
|
||||
// store all the points to mirror
|
||||
|
@ -115,6 +129,7 @@ export const cutLayoutPlugin = function (material, grainAngle) {
|
|||
// store snippets by type so we can re-sprinkle later
|
||||
const snippetsByType = {}
|
||||
// for each snippet
|
||||
let anchorNames = 0
|
||||
for (var s in snippets) {
|
||||
const snip = snippets[s]
|
||||
// don't mirror these ones
|
||||
|
@ -124,29 +139,18 @@ export const cutLayoutPlugin = function (material, grainAngle) {
|
|||
snippetsByType[snip.def] = snippetsByType[snip.def] || []
|
||||
|
||||
// put the anchor on the list to mirror
|
||||
mirrorPoints.push(snip.anchor)
|
||||
|
||||
// then we have to find the name of that point so we can apply the snippet to its mirror
|
||||
for (var pName in points) {
|
||||
if (points[pName] === snip.anchor) {
|
||||
// add the name-to-be of the mirrored anchor to the list for resprinkling
|
||||
snippetsByType[snip.def].push(prefix + utils.capitalize(pName))
|
||||
break
|
||||
}
|
||||
}
|
||||
const anchorName = `snippetAnchors_${anchorNames++}`
|
||||
points[anchorName] = new Point(snip.anchor.x, snip.anchor.y)
|
||||
mirrorPoints.push(anchorName)
|
||||
snippetsByType[snip.def].push(`${prefix}${utils.capitalize(anchorName)}`)
|
||||
}
|
||||
|
||||
// mirror
|
||||
let unnamed = 0
|
||||
macro('mirror', {
|
||||
paths: mirrorPaths,
|
||||
points: mirrorPoints,
|
||||
mirror: fold,
|
||||
prefix,
|
||||
nameFormat: () => {
|
||||
unnamed++
|
||||
return `${prefix}_${unnamed}`
|
||||
},
|
||||
})
|
||||
|
||||
// sprinkle the snippets
|
||||
|
@ -162,62 +166,25 @@ export const cutLayoutPlugin = function (material, grainAngle) {
|
|||
* if the part should be on the bias, this rotates the part to lie on the bias
|
||||
* while keeping the grainline annotation along the grain
|
||||
*/
|
||||
rotateToGrain: ({ partGrain, grainAngle, bias }, { paths, snippets, Point, points }) => {
|
||||
// if this part doesn't have a grain recorded, bail
|
||||
if (partGrain === undefined) return
|
||||
|
||||
rotateToGrain: ({ bias, grainSpec }, { part, paths, points, Point }) => {
|
||||
// the amount to rotate is the difference between this part's grain angle (as drafted) and the fabric's grain angle
|
||||
let toRotate = grainAngle - partGrain
|
||||
let toRotate = grainSpec === undefined ? 0 : grainAngle + grainSpec
|
||||
// don't over rotate
|
||||
if (toRotate >= 180) toRotate -= 180
|
||||
else if (toRotate <= -180) toRotate += 180
|
||||
toRotate = toRotate % 180
|
||||
if (toRotate < 0) toRotate += 180
|
||||
// if there's no difference, don't rotate
|
||||
if (toRotate === 0) return
|
||||
|
||||
// we'll pivot rotations along the grainline to point, with a fallback
|
||||
const pivot = points.grainlineTo || new Point(0, 0)
|
||||
|
||||
// go through all the paths
|
||||
for (const pathName in paths) {
|
||||
const path = paths[pathName]
|
||||
// don't rotate hidden paths
|
||||
if (paths[pathName].hidden) continue
|
||||
|
||||
// we want the grainline indicator to always go in the fabric grain direction
|
||||
// so if this part is on the bias and this path is the grainline indicator
|
||||
// we'll rotate it 45 degrees less than necessary
|
||||
let thisRotation = toRotate
|
||||
if (pathName === 'grainline' && bias) thisRotation -= 45
|
||||
|
||||
// replace all the points in all the ops of this path with ones that have been rotated
|
||||
path.ops.forEach((op) => {
|
||||
if (paths.grainline && bias) {
|
||||
const pivot = points.grainlineFrom || new Point(0, 0)
|
||||
paths.grainline.ops.forEach((op) => {
|
||||
opTypes.forEach((t) => {
|
||||
if (op[t]) op[t] = op[t].rotate(thisRotation, pivot)
|
||||
if (op[t]) op[t] = op[t].rotate(45, pivot)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// replace all snippet anchors with ones that have been rotated
|
||||
for (const snippetName in snippets) {
|
||||
snippets[snippetName].anchor = snippets[snippetName].anchor.rotate(toRotate, pivot)
|
||||
}
|
||||
|
||||
// go through all the points
|
||||
for (const pointName in points) {
|
||||
const point = points[pointName]
|
||||
const pointAttrs = point.attributes
|
||||
// if it has attributes, we want to rotate it
|
||||
if (Object.keys(pointAttrs.list).length) {
|
||||
points[pointName] = point.rotate(toRotate, pivot)
|
||||
|
||||
// title points need to be re-rotated around the top title point to avoid text collisions
|
||||
if (pointName.match(/_(title|exportDate)(?!Nr)/))
|
||||
points[pointName] = points[pointName].rotate(-toRotate, points.__titleNr)
|
||||
|
||||
// put the attributes back onto the new point
|
||||
points[pointName].attributes = pointAttrs.clone()
|
||||
}
|
||||
}
|
||||
part.attributes.add('transform', `rotate(${toRotate})`)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ const FabricSizer = ({ gist, updateGist, activeFabric, sheetWidth }) => {
|
|||
<input
|
||||
key="input-fabricWidth"
|
||||
type="text"
|
||||
className="input input-bordered grow text-base-content border-r-0"
|
||||
className="input input-bordered grow text-base-content border-r-0 w-20"
|
||||
value={val}
|
||||
onChange={update}
|
||||
/>
|
||||
|
@ -78,7 +78,7 @@ export const GrainDirectionPicker = ({ grainDirection, activeFabric, updateGist
|
|||
)
|
||||
}
|
||||
|
||||
const useFabricLength = (isImperial, height) => {
|
||||
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
|
||||
|
@ -91,7 +91,7 @@ const useFabricLength = (isImperial, height) => {
|
|||
// 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, 'none') : round(roundCount, 1)
|
||||
const count = isImperial ? formatFraction128(roundCount, format) : round(roundCount, 1)
|
||||
|
||||
return `${count}${isImperial ? 'yds' : 'm'}`
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ export const CutLayoutSettings = ({
|
|||
<SheetIcon className="h-6 w-6 mr-2 inline align-middle" />
|
||||
<span className="text-xl font-bold align-middle">{fabricLength}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex">
|
||||
<ShowButtonsToggle
|
||||
gist={gist}
|
||||
updateGist={updateGist}
|
||||
|
@ -128,7 +128,7 @@ export const CutLayoutSettings = ({
|
|||
<button
|
||||
key="reset"
|
||||
onClick={() => unsetGist(['layouts', 'cuttingLayout', activeFabric])}
|
||||
className="btn btn-primary btn-outline"
|
||||
className="btn btn-primary btn-outline ml-4"
|
||||
>
|
||||
<ClearIcon className="h-6 w-6 mr-2" />
|
||||
{t('reset')}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { pluginAnnotations } from '@freesewing/plugin-annotations'
|
||||
|
||||
const name = 'Pages Plugin'
|
||||
const version = '1.0.0'
|
||||
export const sizes = {
|
||||
|
@ -94,7 +96,7 @@ const doScanForBlanks = (stacks, layout, x, y, w, h) => {
|
|||
return hasContent
|
||||
}
|
||||
|
||||
function addToOnly(pattern, partName) {
|
||||
export function addToOnly(pattern, partName) {
|
||||
const only = pattern.settings[0].only
|
||||
if (only && !only.includes(partName)) {
|
||||
pattern.settings[0].only = [].concat(only, partName)
|
||||
|
@ -179,11 +181,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)
|
||||
},
|
||||
|
@ -195,6 +197,7 @@ const basePlugin = ({
|
|||
},
|
||||
},
|
||||
macros: {
|
||||
banner: pluginAnnotations.macros.banner,
|
||||
/** draft the pages */
|
||||
addPages: function (so, shorthand) {
|
||||
const [h, w] = so.size
|
||||
|
@ -229,7 +232,7 @@ const basePlugin = ({
|
|||
.attr('data-circle-id', `${pageName}-circle`)
|
||||
points[`${pageName}-text`] = new Point(x + w / 2, y + h / 2)
|
||||
.setText(
|
||||
`${indexStr(col + 1)}${row + 1}`,
|
||||
`${responsiveColumns ? indexStr(col + 1) : ''}${row + 1}`,
|
||||
'text-4xl center baseline-center bold muted fill-fabric'
|
||||
)
|
||||
.attr('data-text-id', `${pageName}-text`)
|
||||
|
@ -242,6 +245,22 @@ const basePlugin = ({
|
|||
.line(points[`${pageName}-tr`])
|
||||
.close()
|
||||
|
||||
// add an edge warning if it can't expand horizontally
|
||||
if (!responsiveColumns) {
|
||||
paths[pageName + '_edge'] = new Path()
|
||||
.move(points[`${pageName}-tr`])
|
||||
.line(points[`${pageName}-br`])
|
||||
// .move(points[`${pageName}-br`].translate(20, 0))
|
||||
.addClass('help contrast stroke-xl')
|
||||
|
||||
shorthand.macro('banner', {
|
||||
path: paths[pageName + '_edge'],
|
||||
text: 'plugin:edgeOf' + shorthand.utils.capitalize(partName),
|
||||
className: 'text-xl center',
|
||||
spaces: 20,
|
||||
})
|
||||
}
|
||||
|
||||
if (col === cols - 1 && row === rows - 1) {
|
||||
const br = points[`${pageName}-br`]
|
||||
part.width = br.x
|
||||
|
@ -254,7 +273,7 @@ const basePlugin = ({
|
|||
.attr('class', 'fill-fabric')
|
||||
.attr(
|
||||
'style',
|
||||
`stroke-opacity: 0; fill-opacity: ${(col + row) % 2 === 0 ? 0.03 : 0.09};`
|
||||
`stroke-opacity: 0; fill-opacity: ${(col + row) % 2 === 0 ? 0.03 : 0.15};`
|
||||
)
|
||||
} else {
|
||||
paths[pageName].attr('class', 'interfacing stroke-xs')
|
||||
|
|
|
@ -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