1
0
Fork 0

Merge pull request #3919 from freesewing/develop

Get up to date with develop
This commit is contained in:
Wouter van Wageningen 2023-04-26 23:24:53 -07:00 committed by GitHub
commit e9ab680c53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
394 changed files with 9683 additions and 6225 deletions

View file

@ -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) {

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
* */
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
}
}

View file

@ -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' }

View file

@ -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})`)
},
},
}

View file

@ -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')}

View file

@ -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')

View file

@ -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 />