2023-03-12 11:12:11 -05:00
|
|
|
import { addToOnly } from '../plugin-layout-part.mjs'
|
2023-03-15 12:48:46 -05:00
|
|
|
import { pluginFlip } from '@freesewing/plugin-flip'
|
|
|
|
import { pluginMirror } from '@freesewing/plugin-mirror'
|
2023-02-19 16:24:46 +02:00
|
|
|
const prefix = 'mirroredOnFold'
|
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// types of path operations
|
2023-03-18 20:01:00 -05:00
|
|
|
const opTypes = ['to', 'from', 'cp1', 'cp2']
|
2023-03-15 12:48:46 -05:00
|
|
|
const avoidRegx = new RegExp(`^(cutonfold|grainline|__scalebox|__miniscale|${prefix})`)
|
2023-03-09 17:45:10 -06:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The plugin to handle all business related to mirroring, rotating, and duplicating parts for the cutting layout
|
|
|
|
* @param {string} material the material to generate a cutting layout for
|
|
|
|
* @param {number} grainAngle the angle of the material's grain
|
|
|
|
* @return {Object} the plugin
|
|
|
|
*/
|
2023-03-01 12:34:18 -06:00
|
|
|
export const cutLayoutPlugin = function (material, grainAngle) {
|
2023-02-27 17:47:34 -06:00
|
|
|
return {
|
|
|
|
hooks: {
|
2023-03-09 17:45:10 -06:00
|
|
|
// after each part
|
2023-02-27 17:47:34 -06:00
|
|
|
postPartDraft: (pattern) => {
|
2023-03-12 11:12:11 -05:00
|
|
|
// 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)
|
|
|
|
)
|
2023-03-01 12:34:18 -06:00
|
|
|
return
|
2023-02-19 16:24:46 +02:00
|
|
|
|
2023-03-12 11:12:11 -05:00
|
|
|
// get the part that's just been drafted
|
|
|
|
const part = pattern.parts[pattern.activeSet][pattern.activePart]
|
2023-03-09 17:45:10 -06:00
|
|
|
// get this part's cutlist configuration
|
2023-03-02 09:36:09 -06:00
|
|
|
let partCutlist = pattern.setStores[pattern.activeSet].get(['cutlist', pattern.activePart])
|
2023-03-09 17:45:10 -06:00
|
|
|
// if there isn't one, we're done here
|
2023-03-09 11:20:17 -06:00
|
|
|
if (!partCutlist) return
|
2023-02-23 23:30:38 +02:00
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// if the cutlist has materials but this isn't one of them
|
|
|
|
// or it has no materials but this isn't the main fabric
|
2023-03-09 11:20:17 -06:00
|
|
|
if (partCutlist.materials ? !partCutlist.materials[material] : material !== 'fabric') {
|
2023-03-09 17:45:10 -06:00
|
|
|
// hide the part because it shouldn't be shown on this fabric
|
2023-03-01 12:34:18 -06:00
|
|
|
part.hide()
|
2023-02-27 17:47:34 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-18 20:01:00 -05:00
|
|
|
// get the cutlist configuration for this material, or default to one
|
|
|
|
const matCutConfig =
|
|
|
|
partCutlist.materials?.[material] || (material === 'fabric' ? [{ cut: 1 }] : [])
|
|
|
|
|
|
|
|
// get the config of the active part to be inherited by all duplicates
|
|
|
|
const activePartConfig = pattern.config.parts[pattern.activePart]
|
|
|
|
|
|
|
|
// hide the active part so that all others can inherit from it and be manipulated separately
|
|
|
|
part.hide()
|
|
|
|
|
|
|
|
// 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
|
2023-04-01 19:16:32 -05:00
|
|
|
const grainSpec = partCutlist.grain ? partCutlist.grain + (bias ? 45 : 0) : undefined
|
2023-03-18 20:01:00 -05:00
|
|
|
|
|
|
|
// for each piece that should be cut
|
|
|
|
for (let c = 0; c < 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,
|
2023-04-01 19:16:32 -05:00
|
|
|
draft: ({ part, macro, store, points }) => {
|
|
|
|
// the amount to rotate is the difference between this part's grain angle (as drafted) and the fabric's grain angle
|
|
|
|
let toRotate = grainSpec === undefined ? 0 : grainAngle - grainSpec
|
|
|
|
// don't over rotate
|
|
|
|
toRotate = toRotate % 180
|
|
|
|
if (toRotate < 0) toRotate += 180
|
|
|
|
|
2023-03-18 20:01:00 -05:00
|
|
|
// handle fold and grain for these cutting instructions
|
2023-04-01 19:16:32 -05:00
|
|
|
const titleSo = store.get(['title', part.name])
|
|
|
|
|
|
|
|
// if they shouldn't be identical, flip every other piece
|
|
|
|
if (!identical && c % 2 === 1) macro('flip')
|
|
|
|
|
2023-03-18 20:01:00 -05:00
|
|
|
macro('handleFoldAndGrain', {
|
|
|
|
partCutlist,
|
2023-04-01 19:16:32 -05:00
|
|
|
toRotate,
|
2023-03-18 20:01:00 -05:00
|
|
|
ignoreOnFold,
|
2023-04-01 19:16:32 -05:00
|
|
|
grainSpec,
|
2023-03-18 20:01:00 -05:00
|
|
|
bias,
|
|
|
|
})
|
|
|
|
|
2023-04-01 19:16:32 -05:00
|
|
|
if (titleSo) {
|
|
|
|
const newTitleSo = {
|
|
|
|
...titleSo,
|
|
|
|
at: points[`_${titleSo.prefix || ''}_titleNr`],
|
|
|
|
append: false,
|
|
|
|
}
|
|
|
|
if (titleSo.rotation !== undefined)
|
|
|
|
newTitleSo.rotation = titleSo.rotation - toRotate
|
|
|
|
macro('title', newTitleSo)
|
|
|
|
}
|
2023-03-18 20:01:00 -05:00
|
|
|
return part
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// add it to the only list if there is one
|
|
|
|
addToOnly(pattern, dupPartName)
|
|
|
|
}
|
|
|
|
})
|
2023-02-27 17:47:34 -06:00
|
|
|
},
|
2023-02-19 16:24:46 +02:00
|
|
|
},
|
2023-02-27 17:47:34 -06:00
|
|
|
macros: {
|
2023-03-15 12:48:46 -05:00
|
|
|
...pluginFlip.macros,
|
|
|
|
...pluginMirror.macros,
|
2023-03-09 17:45:10 -06:00
|
|
|
// handle mirroring on the fold and rotating to sit along the grain or bias
|
2023-04-01 19:16:32 -05:00
|
|
|
handleFoldAndGrain: (
|
|
|
|
{ partCutlist, toRotate, grainSpec, ignoreOnFold, bias },
|
|
|
|
{ points, macro }
|
|
|
|
) => {
|
2023-03-18 20:01:00 -05:00
|
|
|
// if there's a grain angle, rotate the part to be along it
|
2023-03-09 17:45:10 -06:00
|
|
|
// 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 we are meant to ignore those instructions, but there's a grainline
|
|
|
|
else if (grainSpec !== undefined) {
|
|
|
|
// replace the cutonfold with a grainline
|
|
|
|
macro('grainline', { from: points.cutonfoldVia1, to: points.cutonfoldVia2 })
|
|
|
|
macro('cutonfold', false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-01 19:16:32 -05:00
|
|
|
macro('rotateToGrain', { toRotate, bias })
|
2023-03-09 17:45:10 -06:00
|
|
|
},
|
|
|
|
// mirror the part across the line indicated by cutonfold
|
2023-02-27 17:47:34 -06:00
|
|
|
mirrorOnFold: ({ fold }, { paths, snippets, utils, macro, points }) => {
|
2023-03-09 17:45:10 -06:00
|
|
|
// get all the paths to mirror
|
2023-02-27 17:47:34 -06:00
|
|
|
const mirrorPaths = []
|
|
|
|
for (const p in paths) {
|
2023-03-09 17:45:10 -06:00
|
|
|
// skip ones that are hidden
|
2023-03-15 12:48:46 -05:00
|
|
|
if (!paths[p].hidden && !p.match(avoidRegx)) mirrorPaths.push(paths[p])
|
2023-02-27 17:47:34 -06:00
|
|
|
}
|
2023-02-19 16:24:46 +02:00
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// store all the points to mirror
|
2023-02-27 17:47:34 -06:00
|
|
|
const mirrorPoints = []
|
2023-03-09 17:45:10 -06:00
|
|
|
// store snippets by type so we can re-sprinkle later
|
2023-02-27 17:47:34 -06:00
|
|
|
const snippetsByType = {}
|
2023-03-09 17:45:10 -06:00
|
|
|
// for each snippet
|
2023-02-27 17:47:34 -06:00
|
|
|
for (var s in snippets) {
|
|
|
|
const snip = snippets[s]
|
2023-03-09 17:45:10 -06:00
|
|
|
// don't mirror these ones
|
2023-02-27 17:47:34 -06:00
|
|
|
if (['logo'].indexOf(snip.def) > -1) continue
|
2023-02-19 16:24:46 +02:00
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// get or make an array for this type of snippet
|
2023-02-27 17:47:34 -06:00
|
|
|
snippetsByType[snip.def] = snippetsByType[snip.def] || []
|
2023-02-19 16:24:46 +02:00
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// put the anchor on the list to mirror
|
2023-02-27 17:47:34 -06:00
|
|
|
mirrorPoints.push(snip.anchor)
|
2023-03-09 17:45:10 -06:00
|
|
|
|
|
|
|
// then we have to find the name of that point so we can apply the snippet to its mirror
|
2023-02-27 17:47:34 -06:00
|
|
|
for (var pName in points) {
|
|
|
|
if (points[pName] === snip.anchor) {
|
2023-03-09 17:45:10 -06:00
|
|
|
// add the name-to-be of the mirrored anchor to the list for resprinkling
|
2023-02-27 17:47:34 -06:00
|
|
|
snippetsByType[snip.def].push(prefix + utils.capitalize(pName))
|
|
|
|
break
|
|
|
|
}
|
2023-02-19 16:24:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// mirror
|
2023-02-27 17:47:34 -06:00
|
|
|
let unnamed = 0
|
|
|
|
macro('mirror', {
|
2023-03-09 17:45:10 -06:00
|
|
|
paths: mirrorPaths,
|
2023-02-27 17:47:34 -06:00
|
|
|
points: mirrorPoints,
|
|
|
|
mirror: fold,
|
|
|
|
prefix,
|
2023-03-08 17:06:46 -06:00
|
|
|
nameFormat: () => {
|
2023-02-27 17:47:34 -06:00
|
|
|
unnamed++
|
|
|
|
return `${prefix}_${unnamed}`
|
|
|
|
},
|
2023-02-19 16:24:46 +02:00
|
|
|
})
|
2023-02-27 17:47:34 -06:00
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// sprinkle the snippets
|
2023-02-27 17:47:34 -06:00
|
|
|
for (var def in snippetsByType) {
|
|
|
|
macro('sprinkle', {
|
|
|
|
snippet: def,
|
|
|
|
on: snippetsByType[def],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
2023-03-09 17:45:10 -06:00
|
|
|
/**
|
|
|
|
* rotate the part so that it is oriented properly with regard to the fabric grain
|
|
|
|
* 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
|
|
|
|
*/
|
2023-04-01 19:16:32 -05:00
|
|
|
rotateToGrain: (
|
|
|
|
{ toRotate, bias },
|
|
|
|
{ paths, snippets, Point, points, part, store, macro }
|
|
|
|
) => {
|
2023-03-09 17:45:10 -06:00
|
|
|
// if there's no difference, don't rotate
|
2023-03-02 09:36:09 -06:00
|
|
|
if (toRotate === 0) return
|
2023-03-09 17:45:10 -06:00
|
|
|
// we'll pivot rotations along the grainline to point, with a fallback
|
|
|
|
const pivot = points.grainlineTo || new Point(0, 0)
|
2023-03-01 12:34:18 -06:00
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// go through all the paths
|
2023-03-01 12:34:18 -06:00
|
|
|
for (const pathName in paths) {
|
|
|
|
const path = paths[pathName]
|
2023-03-09 17:45:10 -06:00
|
|
|
// don't rotate hidden paths
|
2023-03-01 12:34:18 -06:00
|
|
|
if (paths[pathName].hidden) continue
|
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// 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
|
2023-03-01 12:34:18 -06:00
|
|
|
path.ops.forEach((op) => {
|
|
|
|
opTypes.forEach((t) => {
|
2023-03-09 17:45:10 -06:00
|
|
|
if (op[t]) op[t] = op[t].rotate(thisRotation, pivot)
|
2023-03-01 12:34:18 -06:00
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-03-09 17:45:10 -06:00
|
|
|
// replace all snippet anchors with ones that have been rotated
|
2023-03-01 12:34:18 -06:00
|
|
|
for (const snippetName in snippets) {
|
|
|
|
snippets[snippetName].anchor = snippets[snippetName].anchor.rotate(toRotate, pivot)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const pointName in points) {
|
|
|
|
const point = points[pointName]
|
2023-04-01 19:16:32 -05:00
|
|
|
const pointAttrs = point.attributes.clone()
|
|
|
|
|
|
|
|
points[pointName] = point.rotate(toRotate, pivot)
|
2023-03-09 17:45:10 -06:00
|
|
|
|
2023-04-01 19:16:32 -05:00
|
|
|
points[pointName].attributes = pointAttrs
|
|
|
|
let textTransform = pointAttrs.get('data-text-transform')
|
|
|
|
const textTransformRotation = textTransform ? textTransform.match(/rotate\(\s*\d+/) : null
|
2023-03-09 17:45:10 -06:00
|
|
|
|
2023-04-01 19:16:32 -05:00
|
|
|
let textRotationAmt = -toRotate
|
|
|
|
if (textTransformRotation) {
|
|
|
|
const textRotation = textTransformRotation[0]
|
|
|
|
textRotationAmt = parseFloat(textRotation.replace(/rotate\(\s*/, ''))
|
2023-03-01 12:34:18 -06:00
|
|
|
}
|
2023-04-01 19:16:32 -05:00
|
|
|
|
|
|
|
let newTransform = `rotate(${textRotationAmt} ${points[pointName].x} ${points[pointName].y})`
|
|
|
|
|
|
|
|
if (textTransform)
|
|
|
|
newTransform = textTransform.replace(/rotate\((\s*\d+\.*\d*,*){3}\)/, newTransform)
|
|
|
|
|
|
|
|
points[pointName].attr('data-text-transform', newTransform, true)
|
2023-03-01 12:34:18 -06:00
|
|
|
}
|
|
|
|
},
|
2023-02-19 16:24:46 +02:00
|
|
|
},
|
2023-02-27 17:47:34 -06:00
|
|
|
}
|
2023-02-19 16:24:46 +02:00
|
|
|
}
|