1
0
Fork 0

Merge branch 'proposal-parts' into v3-phase1

This commit is contained in:
Joost De Cock 2022-08-26 12:38:39 +02:00
commit eccdd23ded
21 changed files with 2281 additions and 1305 deletions

View file

@ -33,11 +33,13 @@ let result
console.log(info)
}
// Also build a version that has all dependencies bundled
// This makes it easy to run tests
await esbuild
.build({
...options,
entryPoints: ['src/index.js'],
minify: false,
sourcemap: false,
outfile: 'tests/dist/index.mjs',

View file

@ -1,9 +1,8 @@
import pkg from '../package.json' assert { type: 'json' }
import configHelpers from '@freesewing/config-helpers'
const { pctBasedOn } = configHelpers
const { version } = pkg
export default {
export const version = pkg.version
export const info = {
version,
name: 'aaron',
design: 'Joost De Cock',
@ -23,7 +22,9 @@ export default {
'lengthBonus',
],
},
measurements: [
}
export const measurements = [
'biceps',
'chest',
'hpsToWaistBack',
@ -32,43 +33,7 @@ export default {
'shoulderSlope',
'shoulderToShoulder',
'hips',
],
optionalMeasurements: ['highBust'],
dependencies: {
front: 'base',
back: 'front',
},
inject: {
front: 'base',
back: 'front',
},
hide: ['base'],
options: {
// Constants
brianFitCollar: false,
collarFactor: 4.8,
acrossBackFactor: 0.97,
backNeckCutout: 0.05,
bicepsEase: 0.05,
shoulderEase: 0,
collarEase: 0,
frontArmholeDeeper: 0,
armholeDepthFactor: 0.6,
shoulderSlopeReduction: 0,
]
// Percentages
armholeDrop: { pct: 10, min: 0, max: 75 },
backlineBend: { pct: 50, min: 25, max: 100 },
chestEase: { pct: 8, min: 0, max: 20, ...pctBasedOn('chest') },
hipsEase: { pct: 8, min: 0, max: 20 },
lengthBonus: { pct: 10, min: -20, max: 60 },
necklineBend: { pct: 100, min: 40, max: 100 },
necklineDrop: { pct: 20, min: 10, max: 35 },
stretchFactor: { pct: 5, min: 0, max: 15 },
shoulderStrapWidth: { pct: 15, min: 10, max: 40 },
shoulderStrapPlacement: { pct: 40, min: 20, max: 80 },
export const optionalMeasurements = ['highBust']
// draft for high bust
draftForHighBust: { bool: false },
},
}

View file

@ -0,0 +1,28 @@
import configHelpers from '@freesewing/config-helpers'
const { pctBasedOn } = configHelpers
export const brianFitCollar = false
export const brianFitSleeve = false
export const acrossBackFactor = 0.97
export const backNeckCutout = 0.05
export const bicepsEase = 0.05
export const shoulderEase = 0
export const collarEase = 0
export const frontArmholeDeeper = 0
export const armholeDepthFactor = 0.6
export const shoulderSlopeReduction = 0
// Percentages
export const armholeDrop = { pct: 10, min: 0, max: 75 }
export const backlineBend = { pct: 50, min: 25, max: 100 }
export const chestEase = { pct: 8, min: 0, max: 20, ...pctBasedOn('chest') }
export const hipsEase = { pct: 8, min: 0, max: 20 }
export const lengthBonus = { pct: 10, min: -20, max: 60 }
export const necklineBend = { pct: 100, min: 40, max: 100 }
export const necklineDrop = { pct: 20, min: 10, max: 35 }
export const stretchFactor = { pct: 5, min: 0, max: 15 }
export const shoulderStrapWidth = { pct: 15, min: 10, max: 40 }
export const shoulderStrapPlacement = { pct: 40, min: 20, max: 80 }
// draft for high bust
export const draftForHighBust = { bool: false }

View file

@ -1,103 +1,108 @@
import { dimensions } from './shared'
import front from "./front.js"
export default function (part) {
let {
store,
sa,
Point,
points,
Path,
paths,
options,
complete,
paperless,
macro,
utils,
units,
measurements,
} = part.shorthand()
export default {
from: front,
name: 'back',
draft: function (part) {
const {
store,
sa,
Point,
points,
Path,
paths,
options,
complete,
paperless,
macro,
utils,
units,
measurements,
} = part.shorthand()
// Lower back neck a bit
points.cbNeck.y = measurements.neck / 10
// Lower back neck a bit
points.cbNeck.y = measurements.neck / 10
points.strapLeftCp2 = utils.beamsIntersect(
points.strapLeft,
points.strapCenter.rotate(90, points.strapLeft),
points.cbNeck,
points.cbNeck.shift(0, 10)
)
points.strapLeftCp2 = utils.beamsIntersect(
points.strapLeft,
points.strapCenter.rotate(90, points.strapLeft),
points.cbNeck,
points.cbNeck.shift(0, 10)
)
points.armholeCp2 = points.armhole.shiftFractionTowards(
points.armholeCorner,
options.backlineBend
)
points.strapRightCp1 = points.strapRight.shiftFractionTowards(
points.armholeCorner,
options.backlineBend
)
points.armholeCp2 = points.armhole.shiftFractionTowards(
points.armholeCorner,
options.backlineBend
)
points.strapRightCp1 = points.strapRight.shiftFractionTowards(
points.armholeCorner,
options.backlineBend
)
points.anchor = points.cbNeck.clone()
points.anchor = points.cbNeck.clone()
// Seamline
paths.seam = new Path()
.move(points.cbNeck)
.line(points.cbHem)
.line(points.hem)
.curve_(points.hipsCp2, points.armhole)
.curve(points.armholeCp2, points.strapRightCp1, points.strapRight)
.line(points.strapLeft)
.line(points.strapLeft)
.curve(points.strapLeftCp2, points.cbNeck, points.cbNeck)
.close()
.attr('class', 'fabric')
// Seamline
paths.seam = new Path()
.move(points.cbNeck)
.line(points.cbHem)
.line(points.hem)
.curve_(points.hipsCp2, points.armhole)
.curve(points.armholeCp2, points.strapRightCp1, points.strapRight)
.line(points.strapLeft)
.line(points.strapLeft)
.curve(points.strapLeftCp2, points.cbNeck, points.cbNeck)
.close()
.attr('class', 'fabric')
// Complete pattern?
if (complete) {
let neckOpeningLength =
new Path()
.move(points.strapLeft)
.curve(points.strapLeftCp2, points.cbNeck, points.cbNeck)
.length() + store.get('frontNeckOpeningLength')
let armholeLength =
new Path()
.move(points.armhole)
.curve(points.armholeCp2, points.strapRightCp1, points.strapRight)
.length() + store.get('frontArmholeLength')
points.bindingAnchor = new Point(points.armhole.x / 4, points.armhole.y)
.attr('data-text', 'cutTwoStripsToFinishTheArmholes')
.attr('data-text', ':\n')
.attr('data-text', `2x: ${units(sa * 6 || 60)} x ${units(armholeLength * 0.95 + 2 * sa)}`)
.attr('data-text', '\n \n')
.attr('data-text', 'cutOneStripToFinishTheNeckOpening')
.attr('data-text', ':\n')
.attr('data-text', 'width')
.attr('data-text', ':')
.attr(
'data-text',
`${units((sa || 10) * 6)} x ${units(neckOpeningLength * 2 * 0.95 + 2 * sa)}`
)
//.attr('data-text-class', 'text-sm')
// Complete pattern?
if (complete) {
let neckOpeningLength =
new Path()
.move(points.strapLeft)
.curve(points.strapLeftCp2, points.cbNeck, points.cbNeck)
.length() + store.get('frontNeckOpeningLength')
let armholeLength =
new Path()
.move(points.armhole)
.curve(points.armholeCp2, points.strapRightCp1, points.strapRight)
.length() + store.get('frontArmholeLength')
points.bindingAnchor = new Point(points.armhole.x / 4, points.armhole.y)
.attr('data-text', 'cutTwoStripsToFinishTheArmholes')
.attr('data-text', ':\n')
.attr('data-text', `2x: ${units(sa * 6 || 60)} x ${units(armholeLength * 0.95 + 2 * sa)}`)
.attr('data-text', '\n \n')
.attr('data-text', 'cutOneStripToFinishTheNeckOpening')
.attr('data-text', ':\n')
.attr('data-text', 'width')
.attr('data-text', ':')
.attr(
'data-text',
`${units((sa || 10) * 6)} x ${units(neckOpeningLength * 2 * 0.95 + 2 * sa)}`
)
//.attr('data-text-class', 'text-sm')
macro('cutonfold', {
from: points.cfNeck,
to: points.cfHem,
grainline: true,
})
macro('cutonfold', {
from: points.cfNeck,
to: points.cfHem,
grainline: true,
})
macro('title', { at: points.title, nr: 2, title: 'back' })
points.scaleboxAnchor = points.scalebox = points.title.shift(90, 100)
macro('scalebox', { at: points.scalebox })
macro('title', { at: points.title, nr: 2, title: 'back' })
points.scaleboxAnchor = points.scalebox = points.title.shift(90, 100)
macro('scalebox', { at: points.scalebox })
}
// Paperless?
if (paperless) {
dimensions(macro, points, sa)
macro('vd', {
from: points.cbHem,
to: points.cbNeck,
x: points.cbHem.x - sa - 15,
})
}
return part
}
// Paperless?
if (paperless) {
dimensions(macro, points, sa)
macro('vd', {
from: points.cbHem,
to: points.cbNeck,
x: points.cbHem.x - sa - 15,
})
}
return part
}

View file

@ -1,169 +1,174 @@
import { dimensions } from './shared'
import { base } from '@freesewing/brian'
export default function (part) {
let {
utils,
store,
sa,
Point,
points,
Path,
paths,
Snippet,
snippets,
options,
measurements,
complete,
paperless,
macro,
} = part.shorthand()
export default {
from: base,
name: 'front',
draft: function (part) {
const {
utils,
store,
sa,
Point,
points,
Path,
paths,
Snippet,
snippets,
options,
measurements,
complete,
paperless,
macro,
} = part.shorthand()
// Hide Brian paths
for (let key of Object.keys(paths)) paths[key].render = false
// Hide Brian paths
for (let key of Object.keys(paths)) paths[key].render = false
// Handle stretch
for (let i in points) points[i].x = points[i].x * (1 - options.stretchFactor)
// Handle stretch
for (let i in points) points[i].x = points[i].x * (1 - options.stretchFactor)
// Clone cb (center back) into cf (center front)
for (let key of ['Neck', 'Shoulder', 'Armhole', 'Hips', 'Hem']) {
points[`cf${key}`] = points[`cb${key}`].clone()
}
// Neckline
points.cfNeck = points.cfNeck.shift(-90, options.necklineDrop * measurements.hpsToWaistBack)
// Strap
points.strapCenter = points.neck.shiftFractionTowards(
points.shoulder,
options.shoulderStrapPlacement
)
points.strapLeft = points.strapCenter.shiftTowards(
points.neck,
points.neck.dist(points.shoulder) * options.shoulderStrapWidth
)
points.strapRight = points.strapLeft.rotate(180, points.strapCenter)
points.necklineCorner = utils.beamsIntersect(
points.strapLeft,
points.strapRight.rotate(-90, points.strapLeft),
points.cfNeck.shift(0, points.armholePitch.x / 4),
points.cfNeck
)
points.strapLeftCp2 = points.strapLeft.shiftFractionTowards(
points.necklineCorner,
options.necklineBend
)
points.cfNeckCp1 = points.cfNeck.shiftFractionTowards(points.necklineCorner, options.necklineBend)
// This will come in handy
store.set('armholeY', points.armhole.y * (1 + options.armholeDrop))
// Hips
points.hips.x =
((measurements.hips + options.hipsEase * measurements.hips) / 4) * (1 - options.stretchFactor)
points.waist.x = points.hips.x // Because stretch
points.hipsCp2 = new Point(
points.hips.x,
store.get('armholeY') + (points.hips.y - store.get('armholeY')) / 2
)
// Hem
points.hem.x = points.hips.x
// Armhole
points.armhole = utils.beamIntersectsY(
points.armhole,
points.hips,
points.armhole.y * (1 + options.armholeDrop)
)
points.armholeCorner = utils.beamsIntersect(
points.armhole,
points.armhole.shift(180, 10),
points.strapRight,
points.strapLeft.rotate(90, points.strapRight)
)
points.armholeCp2 = points.armhole.shiftFractionTowards(points.armholeCorner, 0.5)
points.strapRightCp1 = points.strapRight.shiftFractionTowards(points.armholeCorner, 0.5)
points.anchor = points.cfNeck.clone()
// Seamline
paths.seam = new Path()
.move(points.cfNeck)
.line(points.cfHem)
.line(points.hem)
.curve_(points.hipsCp2, points.armhole)
.curve(points.armholeCp2, points.strapRightCp1, points.strapRight)
.line(points.strapLeft)
.curve(points.strapLeftCp2, points.cfNeckCp1, points.cfNeck)
.close()
.attr('class', 'fabric')
// Store length of armhole and neck opening
store.set(
'frontArmholeLength',
new Path()
.move(points.armhole)
.curve(points.armholeCp2, points.strapRightCp1, points.strapRight)
.length()
)
store.set(
'frontNeckOpeningLength',
new Path()
.move(points.strapLeft)
.curve(points.cfNeckCp1, points.cfNeckCp1, points.cfNeck)
.length()
)
// Complete pattern?
if (complete) {
macro('cutonfold', {
from: points.cfNeck,
to: points.cfHem,
grainline: true,
})
points.title = new Point(points.waist.x / 2, points.waist.y)
macro('title', { at: points.title, nr: 1, title: 'front' })
points.logo = points.title.shift(-90, 75)
snippets.logo = new Snippet('logo', points.logo)
if (sa) {
let saShoulder = new Path().move(points.strapRight).line(points.strapLeft).offset(sa)
paths.saShoulder = new Path()
.move(points.strapRight)
.line(saShoulder.start())
.join(saShoulder)
.line(points.strapLeft)
.attr('class', 'fabric sa')
paths.sa = new Path()
.move(points.cfHem)
.line(points.cfHem)
.join(
new Path()
.move(points.cfHem)
.line(points.hem)
.offset(sa * 2.5)
)
.join(
new Path()
.move(points.hem)
.curve_(points.waist, points.armhole)
.offset(sa)
.line(points.armhole)
)
.attr('class', 'fabric sa')
// Clone cb (center back) into cf (center front)
for (let key of ['Neck', 'Shoulder', 'Armhole', 'Hips', 'Hem']) {
points[`cf${key}`] = points[`cb${key}`].clone()
}
}
// Paperless?
if (paperless) {
dimensions(macro, points, sa)
macro('vd', {
from: points.cfHem,
to: points.cfNeck,
x: points.cfHem.x - sa - 15,
})
}
// Neckline
points.cfNeck = points.cfNeck.shift(-90, options.necklineDrop * measurements.hpsToWaistBack)
return part
// Strap
points.strapCenter = points.neck.shiftFractionTowards(
points.shoulder,
options.shoulderStrapPlacement
)
points.strapLeft = points.strapCenter.shiftTowards(
points.neck,
points.neck.dist(points.shoulder) * options.shoulderStrapWidth
)
points.strapRight = points.strapLeft.rotate(180, points.strapCenter)
points.necklineCorner = utils.beamsIntersect(
points.strapLeft,
points.strapRight.rotate(-90, points.strapLeft),
points.cfNeck.shift(0, points.armholePitch.x / 4),
points.cfNeck
)
points.strapLeftCp2 = points.strapLeft.shiftFractionTowards(
points.necklineCorner,
options.necklineBend
)
points.cfNeckCp1 = points.cfNeck.shiftFractionTowards(points.necklineCorner, options.necklineBend)
// This will come in handy
store.set('armholeY', points.armhole.y * (1 + options.armholeDrop))
// Hips
points.hips.x =
((measurements.hips + options.hipsEase * measurements.hips) / 4) * (1 - options.stretchFactor)
points.waist.x = points.hips.x // Because stretch
points.hipsCp2 = new Point(
points.hips.x,
store.get('armholeY') + (points.hips.y - store.get('armholeY')) / 2
)
// Hem
points.hem.x = points.hips.x
// Armhole
points.armhole = utils.beamIntersectsY(
points.armhole,
points.hips,
points.armhole.y * (1 + options.armholeDrop)
)
points.armholeCorner = utils.beamsIntersect(
points.armhole,
points.armhole.shift(180, 10),
points.strapRight,
points.strapLeft.rotate(90, points.strapRight)
)
points.armholeCp2 = points.armhole.shiftFractionTowards(points.armholeCorner, 0.5)
points.strapRightCp1 = points.strapRight.shiftFractionTowards(points.armholeCorner, 0.5)
points.anchor = points.cfNeck.clone()
// Seamline
paths.seam = new Path()
.move(points.cfNeck)
.line(points.cfHem)
.line(points.hem)
.curve_(points.hipsCp2, points.armhole)
.curve(points.armholeCp2, points.strapRightCp1, points.strapRight)
.line(points.strapLeft)
.curve(points.strapLeftCp2, points.cfNeckCp1, points.cfNeck)
.close()
.attr('class', 'fabric')
// Store length of armhole and neck opening
store.set(
'frontArmholeLength',
new Path()
.move(points.armhole)
.curve(points.armholeCp2, points.strapRightCp1, points.strapRight)
.length()
)
store.set(
'frontNeckOpeningLength',
new Path()
.move(points.strapLeft)
.curve(points.cfNeckCp1, points.cfNeckCp1, points.cfNeck)
.length()
)
// Complete pattern?
if (complete) {
macro('cutonfold', {
from: points.cfNeck,
to: points.cfHem,
grainline: true,
})
points.title = new Point(points.waist.x / 2, points.waist.y)
macro('title', { at: points.title, nr: 1, title: 'front' })
points.logo = points.title.shift(-90, 75)
snippets.logo = new Snippet('logo', points.logo)
if (sa) {
let saShoulder = new Path().move(points.strapRight).line(points.strapLeft).offset(sa)
paths.saShoulder = new Path()
.move(points.strapRight)
.line(saShoulder.start())
.join(saShoulder)
.line(points.strapLeft)
.attr('class', 'fabric sa')
paths.sa = new Path()
.move(points.cfHem)
.line(points.cfHem)
.join(
new Path()
.move(points.cfHem)
.line(points.hem)
.offset(sa * 2.5)
)
.join(
new Path()
.move(points.hem)
.curve_(points.waist, points.armhole)
.offset(sa)
.line(points.armhole)
)
.attr('class', 'fabric sa')
}
}
// Paperless?
if (paperless) {
dimensions(macro, points, sa)
macro('vd', {
from: points.cfHem,
to: points.cfNeck,
x: points.cfHem.x - sa - 15,
})
}
return part
}
}

View file

@ -1,38 +1,33 @@
// FreeSewing core library
import freesewing from '@freesewing/core'
import Brian from '@freesewing/brian'
import plugins from '@freesewing/plugin-bundle'
import plugin from '@freesewing/plugin-bust' // Note: conditional plugin
import config from '../config'
// FreeSewing Plugins
import pluginBundle from '@freesewing/plugin-bundle'
import bustPlugin from '@freesewing/plugin-bust' // Note: conditional plugin
// Design config & options
import { info, measurements, optionalMeasurements } from '../config/index'
import * as options from '../config/options'
// Design parts
import back from './back'
import front from './front'
// Parts
import draftBack from './back'
import draftFront from './front'
/* Check to see whether we should load the bust plugin
* Only of the `draftForHighBust` options is set
* AND the highBust measurement is available
*/
const condition = (settings = false) =>
settings &&
settings.options &&
settings.options.draftForHighBust &&
settings.measurements.highBust
? true
: false
// Create design
const Aaron = new freesewing.Design(config, plugins, { plugin, condition })
// Attach draft methods to prototype
Aaron.prototype.draftBase = function (part) {
// Getting the base part from Brian
return new Brian(this.settings).draftBase(part)
}
Aaron.prototype.draftFront = (part) => draftFront(part)
Aaron.prototype.draftBack = (part) => draftBack(part)
// Setup design
const Aaron = new freesewing.Design({
...info,
measurements,
optionalMeasurements,
options: { ...options },
parts: { back, front },
plugins: pluginBundle,
conditionalPlugins: {
plugin: bustPlugin,
condition: (settings=false) =>
settings?.options?.draftForHighBust &&
settings?.measurements?.highBust
? true : false
}
})
// Named exports
export { config, Aaron }
export { front, back, Aaron }
// Default export
export default Aaron

View file

@ -2,7 +2,7 @@ import pkg from '../package.json' assert { type: 'json' }
const { version } = pkg
export default {
export const info = {
version,
name: 'brian',
design: 'Joost De Cock',
@ -54,74 +54,19 @@ export default {
},
],
},
measurements: [
'biceps',
'chest',
'hpsToWaistBack',
'waistToHips',
'neck',
'shoulderSlope',
'shoulderToShoulder',
'shoulderToWrist',
'wrist',
],
optionalMeasurements: ['highBust'],
dependencies: {
back: 'base',
front: 'back',
sleevecap: 'front',
sleeve: 'sleevecap',
},
inject: {
back: 'base',
front: 'back',
sleeve: 'sleevecap',
},
hide: ['base', 'sleevecap'],
options: {
// Constants
brianFitSleeve: true,
brianFitCollar: true,
collarFactor: 4.8,
// Percentages
acrossBackFactor: { pct: 98, min: 93, max: 100 },
armholeDepthFactor: { pct: 55, min: 50, max: 70 },
backNeckCutout: { pct: 5, min: 2, max: 8 },
bicepsEase: { pct: 15, min: 0, max: 50 },
chestEase: { pct: 15, min: -4, max: 35 },
collarEase: { pct: 5, min: 0, max: 10 },
cuffEase: { pct: 20, min: 0, max: 200 },
frontArmholeDeeper: { pct: 0.2, min: 0, max: 0.5 },
lengthBonus: { pct: 0, min: -4, max: 60 },
shoulderEase: { pct: 0, min: -2, max: 6 },
shoulderSlopeReduction: { pct: 0, min: 0, max: 80 },
// s3 is short for Shoulder Seam Shift
s3Collar: { pct: 0, min: -100, max: 100 },
s3Armhole: { pct: 0, min: -100, max: 100 },
sleevecapEase: { pct: 0, min: 0, max: 10 },
sleevecapTopFactorX: { pct: 50, min: 25, max: 75 },
sleevecapTopFactorY: { pct: 45, min: 35, max: 125 },
sleevecapBackFactorX: { pct: 60, min: 35, max: 65 },
sleevecapBackFactorY: { pct: 33, min: 30, max: 65 },
sleevecapFrontFactorX: { pct: 55, min: 35, max: 65 },
sleevecapFrontFactorY: { pct: 33, min: 30, max: 65 },
sleevecapQ1Offset: { pct: 1.7, min: 0, max: 7 },
sleevecapQ2Offset: { pct: 3.5, min: 0, max: 7 },
sleevecapQ3Offset: { pct: 2.5, min: 0, max: 7 },
sleevecapQ4Offset: { pct: 1, min: 0, max: 7 },
sleevecapQ1Spread1: { pct: 10, min: 4, max: 20 },
sleevecapQ1Spread2: { pct: 15, min: 4, max: 20 },
sleevecapQ2Spread1: { pct: 15, min: 4, max: 20 },
sleevecapQ2Spread2: { pct: 10, min: 4, max: 20 },
sleevecapQ3Spread1: { pct: 10, min: 4, max: 20 },
sleevecapQ3Spread2: { pct: 8, min: 4, max: 20 },
sleevecapQ4Spread1: { pct: 7, min: 4, max: 20 },
sleevecapQ4Spread2: { pct: 6.3, min: 4, max: 20 },
sleeveWidthGuarantee: { pct: 90, min: 25, max: 100 },
sleeveLengthBonus: { pct: 0, min: -40, max: 10 },
// draft for high bust
draftForHighBust: { bool: false },
},
}
export const measurements = [
'biceps',
'chest',
'hpsToWaistBack',
'waistToHips',
'neck',
'shoulderSlope',
'shoulderToShoulder',
'shoulderToWrist',
'wrist',
]
export const optionalMeasurements = ['highBust']

View file

@ -0,0 +1,94 @@
// Constants
export const brianFitSleeve = true
export const brianFitCollar = true
export const collarFactor = 4.8
// Percentages
export const acrossBackFactor = { pct: 98, min: 93, max: 100 }
export const armholeDepthFactor = { pct: 55, min: 50, max: 70 }
export const backNeckCutout = { pct: 5, min: 2, max: 8 }
export const bicepsEase = { pct: 15, min: 0, max: 50 }
export const chestEase = { pct: 15, min: -4, max: 35 }
export const collarEase = { pct: 5, min: 0, max: 10 }
export const cuffEase = { pct: 20, min: 0, max: 200 }
export const frontArmholeDeeper = { pct: 0.2, min: 0, max: 0.5 }
export const lengthBonus = { pct: 0, min: -4, max: 60 }
export const shoulderEase = { pct: 0, min: -2, max: 6 }
export const shoulderSlopeReduction = { pct: 0, min: 0, max: 80 }
// s3 is short for Shoulder Seam Shift
export const s3Collar = { pct: 0, min: -100, max: 100 }
export const s3Armhole = { pct: 0, min: -100, max: 100 }
// Sleevecap
export const sleevecapEase = { pct: 0, min: 0, max: 10 }
export const sleevecapTopFactorX = { pct: 50, min: 25, max: 75 }
export const sleevecapTopFactorY = { pct: 45, min: 35, max: 125 }
export const sleevecapBackFactorX = { pct: 60, min: 35, max: 65 }
export const sleevecapBackFactorY = { pct: 33, min: 30, max: 65 }
export const sleevecapFrontFactorX = { pct: 55, min: 35, max: 65 }
export const sleevecapFrontFactorY = { pct: 33, min: 30, max: 65 }
export const sleevecapQ1Offset = { pct: 1.7, min: 0, max: 7 }
export const sleevecapQ2Offset = { pct: 3.5, min: 0, max: 7 }
export const sleevecapQ3Offset = { pct: 2.5, min: 0, max: 7 }
export const sleevecapQ4Offset = { pct: 1, min: 0, max: 7 }
export const sleevecapQ1Spread1 = { pct: 10, min: 4, max: 20 }
export const sleevecapQ1Spread2 = { pct: 15, min: 4, max: 20 }
export const sleevecapQ2Spread1 = { pct: 15, min: 4, max: 20 }
export const sleevecapQ2Spread2 = { pct: 10, min: 4, max: 20 }
export const sleevecapQ3Spread1 = { pct: 10, min: 4, max: 20 }
export const sleevecapQ3Spread2 = { pct: 8, min: 4, max: 20 }
export const sleevecapQ4Spread1 = { pct: 7, min: 4, max: 20 }
export const sleevecapQ4Spread2 = { pct: 6.3, min: 4, max: 20 }
// Sleeve
export const sleeveWidthGuarantee = { pct: 90, min: 25, max: 100 }
export const sleeveLengthBonus = { pct: 0, min: -40, max: 10 }
// Draft for high bust
export const draftForHighBust = { bool: false }
// Helper objects for per-part options
export const _base = {
brianFitSleeve,
brianFitCollar,
collarFactor,
acrossBackFactor,
armholeDepthFactor,
backNeckCutout,
bicepsEase,
chestEase,
collarEase,
cuffEase,
frontArmholeDeeper,
lengthBonus,
shoulderEase,
shoulderSlopeReduction,
s3Collar,
s3Armhole,
draftForHighBust,
}
export const _sleevecap = {
sleevecapEase,
sleevecapTopFactorX,
sleevecapTopFactorY,
sleevecapBackFactorX,
sleevecapBackFactorY,
sleevecapFrontFactorX,
sleevecapFrontFactorY,
sleevecapQ1Offset,
sleevecapQ2Offset,
sleevecapQ3Offset,
sleevecapQ4Offset,
sleevecapQ1Spread1,
sleevecapQ1Spread2,
sleevecapQ2Spread1,
sleevecapQ2Spread2,
sleevecapQ3Spread1,
sleevecapQ3Spread2,
sleevecapQ4Spread1,
sleevecapQ4Spread2,
sleeveWidthGuarantee,
}
export const _sleeve = { sleeveLengthBonus }

View file

@ -1,184 +1,189 @@
import * as shared from './shared'
import base from './base'
export default (part) => {
let {
store,
sa,
points,
Path,
paths,
Snippet,
snippets,
complete,
paperless,
macro,
options,
utils,
} = part.shorthand()
export default {
from: base,
name: 'back',
draft: (part) => {
const {
store,
sa,
points,
Path,
paths,
Snippet,
snippets,
complete,
paperless,
macro,
options,
utils,
} = part.shorthand()
points.anchor = points.hps.clone()
points.anchor = points.hps.clone()
// Adapt the shoulder seam according to the relevant options
// Note: s3 stands for Shoulder Seam Shift
// Don't bother with less than 10% as that's just asking for trouble
if (options.s3Collar < 0.1 && options.s3Collar > -0.1) {
points.s3CollarSplit = points.hps
paths.backCollar = new Path()
.move(points.hps)
.curve_(points.neckCp2, points.cbNeck)
.setRender(false)
} else if (options.s3Collar > 0) {
// Shift shoulder seam forward on the collar side
points.s3CollarSplit = utils.curveIntersectsY(
points.hps,
points.mirroredNeckCp2Front,
points.mirroredCfNeckCp1,
points.mirroredCfNeck,
store.get('s3CollarMaxFront') * -1 * options.s3Collar
)
paths.backCollar = new Path()
.move(points.hps)
._curve(points.mirroredNeckCp2Front, points.mirroredCfNeckCp1, points.mirroredCfNeck)
.split(points.s3CollarSplit)[0]
.reverse()
.join(new Path().move(points.hps).curve_(points.neckCp2, points.cbNeck))
.setRender(false)
} else if (options.s3Collar < 0) {
// Shift shoulder seam backward on the collar side
points.s3CollarSplit = utils.curveIntersectsY(
points.hps,
points.neckCp2,
points.cbNeck,
points.cbNeck,
store.get('s3CollarMaxBack') * -1 * options.s3Collar
)
paths.backCollar = new Path()
.move(points.cbNeck)
._curve(points.neckCp2, points.neck)
.split(points.s3CollarSplit)[0]
.reverse()
.setRender(false)
}
// Don't bother with less than 10% as that's just asking for trouble
if (options.s3Armhole < 0.1 && options.s3Armhole > -0.1) {
points.s3ArmholeSplit = points.shoulder
paths.backArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.setRender(false)
} else if (options.s3Armhole > 0) {
// Shift shoulder seam forward on the armhole side
points.s3ArmholeSplit = utils.curveIntersectsY(
points.shoulder,
points.mirroredShoulderCp1,
points.mirroredFrontArmholePitchCp2,
points.mirroredFrontArmholePitch,
store.get('s3ArmholeMax') * -1 * options.s3Armhole + points.shoulder.y
)
paths.backArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.join(
new Path()
.move(points.shoulder)
.curve(
points.mirroredShoulderCp1,
points.mirroredFrontArmholePitchCp2,
points.mirroredFrontArmholePitch
)
.split(points.s3ArmholeSplit)[0]
// Adapt the shoulder seam according to the relevant options
// Note: s3 stands for Shoulder Seam Shift
// Don't bother with less than 10% as that's just asking for trouble
if (options.s3Collar < 0.1 && options.s3Collar > -0.1) {
points.s3CollarSplit = points.hps
paths.backCollar = new Path()
.move(points.hps)
.curve_(points.neckCp2, points.cbNeck)
.setRender(false)
} else if (options.s3Collar > 0) {
// Shift shoulder seam forward on the collar side
points.s3CollarSplit = utils.curveIntersectsY(
points.hps,
points.mirroredNeckCp2Front,
points.mirroredCfNeckCp1,
points.mirroredCfNeck,
store.get('s3CollarMaxFront') * -1 * options.s3Collar
)
.setRender(false)
} else if (options.s3Armhole < 0) {
// Shift shoulder seam backward on the armhole side
points.s3ArmholeSplit = utils.curveIntersectsY(
points.shoulder,
points.shoulderCp1,
points.armholePitchCp2,
points.armholePitch,
store.get('s3ArmholeMax') * -1 * options.s3Armhole + points.shoulder.y
)
paths.backArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.split(points.s3ArmholeSplit)[0]
.setRender(false)
}
// Seamline
paths.saBase = new Path()
.move(points.cbHem)
.line(points.hem)
.line(points.armhole)
.curve(points.armholeCp2, points.armholeHollowCp1, points.armholeHollow)
.curve(points.armholeHollowCp2, points.armholePitchCp1, points.armholePitch)
.join(paths.backArmhole)
.line(points.s3CollarSplit)
.join(paths.backCollar)
.setRender(false)
paths.seam = new Path()
.move(points.cbNeck)
.line(points.cbHips)
.join(paths.saBase)
.attr('class', 'fabric')
// Store lengths to fit sleeve
store.set('backArmholeLength', shared.armholeLength(points, Path))
store.set('backArmholeToArmholePitch', shared.armholeToArmholePitch(points, Path))
// Complete pattern?
if (complete) {
macro('cutonfold', {
from: points.cbNeck,
to: points.cbHips,
grainline: true,
})
macro('title', { at: points.title, nr: 2, title: 'back' })
snippets.armholePitchNotch = new Snippet('bnotch', points.armholePitch)
paths.waist = new Path().move(points.cbWaist).line(points.waist).attr('class', 'help')
if (sa) {
paths.sa = paths.saBase
.offset(sa)
.attr('class', 'fabric sa')
.line(points.cbNeck)
.move(points.cbHips)
paths.sa.line(paths.sa.start())
paths.backCollar = new Path()
.move(points.hps)
._curve(points.mirroredNeckCp2Front, points.mirroredCfNeckCp1, points.mirroredCfNeck)
.split(points.s3CollarSplit)[0]
.reverse()
.join(new Path().move(points.hps).curve_(points.neckCp2, points.cbNeck))
.setRender(false)
} else if (options.s3Collar < 0) {
// Shift shoulder seam backward on the collar side
points.s3CollarSplit = utils.curveIntersectsY(
points.hps,
points.neckCp2,
points.cbNeck,
points.cbNeck,
store.get('s3CollarMaxBack') * -1 * options.s3Collar
)
paths.backCollar = new Path()
.move(points.cbNeck)
._curve(points.neckCp2, points.neck)
.split(points.s3CollarSplit)[0]
.reverse()
.setRender(false)
}
// Don't bother with less than 10% as that's just asking for trouble
if (options.s3Armhole < 0.1 && options.s3Armhole > -0.1) {
points.s3ArmholeSplit = points.shoulder
paths.backArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.setRender(false)
} else if (options.s3Armhole > 0) {
// Shift shoulder seam forward on the armhole side
points.s3ArmholeSplit = utils.curveIntersectsY(
points.shoulder,
points.mirroredShoulderCp1,
points.mirroredFrontArmholePitchCp2,
points.mirroredFrontArmholePitch,
store.get('s3ArmholeMax') * -1 * options.s3Armhole + points.shoulder.y
)
paths.backArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.join(
new Path()
.move(points.shoulder)
.curve(
points.mirroredShoulderCp1,
points.mirroredFrontArmholePitchCp2,
points.mirroredFrontArmholePitch
)
.split(points.s3ArmholeSplit)[0]
)
.setRender(false)
} else if (options.s3Armhole < 0) {
// Shift shoulder seam backward on the armhole side
points.s3ArmholeSplit = utils.curveIntersectsY(
points.shoulder,
points.shoulderCp1,
points.armholePitchCp2,
points.armholePitch,
store.get('s3ArmholeMax') * -1 * options.s3Armhole + points.shoulder.y
)
paths.backArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.split(points.s3ArmholeSplit)[0]
.setRender(false)
}
// Add notches if the shoulder seam is shifted
shared.s3Notches(part, 'bnotch')
}
// Seamline
paths.saBase = new Path()
.move(points.cbHem)
.line(points.hem)
.line(points.armhole)
.curve(points.armholeCp2, points.armholeHollowCp1, points.armholeHollow)
.curve(points.armholeHollowCp2, points.armholePitchCp1, points.armholePitch)
.join(paths.backArmhole)
.line(points.s3CollarSplit)
.join(paths.backCollar)
.setRender(false)
paths.seam = new Path()
.move(points.cbNeck)
.line(points.cbHips)
.join(paths.saBase)
.attr('class', 'fabric')
// Paperless?
if (paperless) {
shared.dimensions(part, 'back')
macro('hd', {
from: points.cbHips,
to: points.hips,
y: points.hem.y + sa + 15,
})
macro('vd', {
from: points.cbHem,
to: points.cbWaist,
x: points.cbHips.x - sa - 15,
})
macro('vd', {
from: points.cbHem,
to: points.cbNeck,
x: points.cbHips.x - sa - 30,
})
macro('hd', {
from: points.cbNeck,
to: points.s3CollarSplit,
y: points.s3CollarSplit.y - sa - 15,
})
macro('hd', {
from: points.cbNeck,
to: points.s3ArmholeSplit,
y: points.s3CollarSplit.y - sa - 30,
})
}
// Store lengths to fit sleeve
store.set('backArmholeLength', shared.armholeLength(points, Path))
store.set('backArmholeToArmholePitch', shared.armholeToArmholePitch(points, Path))
return part
// Complete pattern?
if (complete) {
macro('cutonfold', {
from: points.cbNeck,
to: points.cbHips,
grainline: true,
})
macro('title', { at: points.title, nr: 2, title: 'back' })
snippets.armholePitchNotch = new Snippet('bnotch', points.armholePitch)
paths.waist = new Path().move(points.cbWaist).line(points.waist).attr('class', 'help')
if (sa) {
paths.sa = paths.saBase
.offset(sa)
.attr('class', 'fabric sa')
.line(points.cbNeck)
.move(points.cbHips)
paths.sa.line(paths.sa.start())
}
// Add notches if the shoulder seam is shifted
shared.s3Notches(part, 'bnotch')
}
// Paperless?
if (paperless) {
shared.dimensions(part, 'back')
macro('hd', {
from: points.cbHips,
to: points.hips,
y: points.hem.y + sa + 15,
})
macro('vd', {
from: points.cbHem,
to: points.cbWaist,
x: points.cbHips.x - sa - 15,
})
macro('vd', {
from: points.cbHem,
to: points.cbNeck,
x: points.cbHips.x - sa - 30,
})
macro('hd', {
from: points.cbNeck,
to: points.s3CollarSplit,
y: points.s3CollarSplit.y - sa - 15,
})
macro('hd', {
from: points.cbNeck,
to: points.s3ArmholeSplit,
y: points.s3CollarSplit.y - sa - 30,
})
}
return part
}
}

View file

@ -1,173 +1,181 @@
export default (part) => {
let {
measurements,
options,
store,
points,
snippets,
Point,
Snippet,
Path,
paths,
utils,
complete,
macro,
} = part.shorthand()
import { _base as options } from '../config/options.js'
store.set('shoulderEase', (measurements.shoulderToShoulder * options.shoulderEase) / 2)
// Center back (cb) vertical axis
points.cbHps = new Point(0, 0)
points.cbNeck = new Point(0, options.backNeckCutout * measurements.neck)
points.cbWaist = new Point(0, measurements.hpsToWaistBack)
points.cbHips = new Point(0, points.cbWaist.y + measurements.waistToHips)
export default {
name: 'base',
hide: true,
options,
draft: (part) => {
const {
measurements,
options,
store,
points,
snippets,
Point,
Snippet,
Path,
paths,
utils,
complete,
macro,
} = part.shorthand()
// Shoulder line
points.neck = new Point((measurements.neck * (1 + options.collarEase)) / options.collarFactor, 0)
points.hps = points.neck.clone() // We started using HPS in many measurements
// Shoulder point using shoulderSlope degree measurement
points.shoulder = utils.beamsIntersect(
points.hps,
points.hps.shift(measurements.shoulderSlope * -1, 100),
new Point(measurements.shoulderToShoulder / 2 + store.get('shoulderEase'), -100),
new Point(measurements.shoulderToShoulder / 2 + store.get('shoulderEase'), 100)
)
// Determine armhole depth and cbShoulder independent of shoulder slope reduction
points.cbShoulder = new Point(0, points.shoulder.y)
points.cbArmhole = new Point(
0,
points.shoulder.y + measurements.biceps * (1 + options.bicepsEase) * options.armholeDepthFactor
)
store.set('shoulderEase', (measurements.shoulderToShoulder * options.shoulderEase) / 2)
// Now take shoulder slope reduction into account
points.shoulder.y -= (points.shoulder.y - points.cbHps.y) * options.shoulderSlopeReduction
// Shoulder should never be higher than HPS
if (points.shoulder.y < points.cbHps.y) points.shoulder = new Point(points.shoulder.x, 0)
// Center back (cb) vertical axis
points.cbHps = new Point(0, 0)
points.cbNeck = new Point(0, options.backNeckCutout * measurements.neck)
points.cbWaist = new Point(0, measurements.hpsToWaistBack)
points.cbHips = new Point(0, points.cbWaist.y + measurements.waistToHips)
points.cbHem = new Point(0, points.cbHips.y * (1 + options.lengthBonus))
// Side back (cb) vertical axis
points.armhole = new Point((measurements.chest * (1 + options.chestEase)) / 4, points.cbArmhole.y)
points.waist = new Point(points.armhole.x, points.cbWaist.y)
points.hips = new Point(points.armhole.x, points.cbHips.y)
points.hem = new Point(points.armhole.x, points.cbHem.y)
// Armhhole
points.armholePitch = new Point(
(measurements.shoulderToShoulder * options.acrossBackFactor) / 2 +
store.get('shoulderEase') / 2,
points.shoulder.y + points.shoulder.dy(points.armhole) / 2
)
// Set both an front and back armhole pitch point
// but keep 'armholePitch' for backwards compatibility
points.backArmholePitch = points.armholePitch.clone()
points.frontArmholePitch = points.armholePitch.clone() // will be overwritten below
// Armhole hollow
points._tmp1 = new Point(points.armholePitch.x, points.armhole.y)
points._tmp2 = points._tmp1.shift(45, 10)
points._tmp3 = utils.beamsIntersect(
points._tmp1,
points._tmp2,
points.armhole,
points.armholePitch
)
points.armholeHollow = points._tmp1.shiftFractionTowards(points._tmp3, 0.5)
points.armholeCp2 = points.armhole.shift(180, points._tmp1.dx(points.armhole) / 4)
points.armholeHollowCp1 = points.armholeHollow.shift(
-45,
points.armholeHollow.dy(points.armhole) / 2
)
points.armholeHollowCp2 = points.armholeHollow.shift(
135,
points.armholePitch.dx(points.armholeHollow)
)
points.armholePitchCp1 = points.armholePitch.shift(
-90,
points.armholePitch.dy(points.armholeHollow) / 2
)
points.backArmholePitchCp1 = points.armholePitchCp1.clone()
points.frontArmholePitchCp1 = points.armholePitchCp1.clone() // will be overwritten below
points.armholePitchCp2 = points.armholePitch.shift(
90,
points.shoulder.dy(points.armholePitch) / 2
)
points.backArmholePitchCp2 = points.armholePitchCp2.clone()
points.frontArmholePitchCp2 = points.armholePitchCp2.clone() // will be overwritten below
points.shoulderCp1 = points.shoulder
.shiftTowards(points.neck, points.shoulder.dy(points.armholePitch) / 5)
.rotate(90, points.shoulder)
// Neck opening (back)
points._tmp4 = points.neck.shiftTowards(points.shoulder, 10).rotate(-90, points.neck)
points.neckCp2 = utils.beamIntersectsY(points.neck, points._tmp4, points.cbNeck.y)
// Fit collar
points.cfNeck = points.neck.rotate(-90, new Point(0, 0))
let target = measurements.neck * (1 + options.collarEase)
let delta = 0
let run = 0
do {
run++
points.cfNeck = points.cfNeck.shift(90, delta / 3)
points.frontNeckCpEdge = utils.beamsIntersect(
points.neck,
points.neckCp2,
points.cfNeck,
new Point(20, points.cfNeck.y)
// Shoulder line
points.neck = new Point((measurements.neck * (1 + options.collarEase)) / options.collarFactor, 0)
points.hps = points.neck.clone() // We started using HPS in many measurements
// Shoulder point using shoulderSlope degree measurement
points.shoulder = utils.beamsIntersect(
points.hps,
points.hps.shift(measurements.shoulderSlope * -1, 100),
new Point(measurements.shoulderToShoulder / 2 + store.get('shoulderEase'), -100),
new Point(measurements.shoulderToShoulder / 2 + store.get('shoulderEase'), 100)
)
// Determine armhole depth and cbShoulder independent of shoulder slope reduction
points.cbShoulder = new Point(0, points.shoulder.y)
points.cbArmhole = new Point(
0,
points.shoulder.y + measurements.biceps * (1 + options.bicepsEase) * options.armholeDepthFactor
)
points.cfNeckCp1 = points.cfNeck.shiftFractionTowards(points.frontNeckCpEdge, 0.55)
points.neckCp2Front = points.neck.shiftFractionTowards(points.frontNeckCpEdge, 0.65)
paths.neckOpening = new Path()
.move(points.cfNeck)
.curve(points.cfNeckCp1, points.neckCp2Front, points.neck)
.curve(points.neckCp2, points.cbNeck, points.cbNeck)
.attr('class', 'dashed stroke-xl various')
delta = paths.neckOpening.length() * 2 - target
} while (Math.abs(delta) > 1 && options.brianFitCollar && run < 10)
delete paths.neckOpening
// Anchor point for sampling
points.gridAnchor = points.cbHem
// Now take shoulder slope reduction into account
points.shoulder.y -= (points.shoulder.y - points.cbHps.y) * options.shoulderSlopeReduction
// Shoulder should never be higher than HPS
if (points.shoulder.y < points.cbHps.y) points.shoulder = new Point(points.shoulder.x, 0)
/*
* People would like to have the option to shift the shoulder seam
* See https://github.com/freesewing/freesewing/issues/642
* So let's make the people happy
*/
// Front armhole is a bit deeper, add those points
let deeper = measurements.chest * options.frontArmholeDeeper
for (const p of ['', 'Cp1', 'Cp2']) {
points[`frontArmholePitch${p}`] = points[`armholePitch${p}`].shift(180, deeper)
points.cbHem = new Point(0, points.cbHips.y * (1 + options.lengthBonus))
// Side back (cb) vertical axis
points.armhole = new Point((measurements.chest * (1 + options.chestEase)) / 4, points.cbArmhole.y)
points.waist = new Point(points.armhole.x, points.cbWaist.y)
points.hips = new Point(points.armhole.x, points.cbHips.y)
points.hem = new Point(points.armhole.x, points.cbHem.y)
// Armhhole
points.armholePitch = new Point(
(measurements.shoulderToShoulder * options.acrossBackFactor) / 2 +
store.get('shoulderEase') / 2,
points.shoulder.y + points.shoulder.dy(points.armhole) / 2
)
// Set both an front and back armhole pitch point
// but keep 'armholePitch' for backwards compatibility
points.backArmholePitch = points.armholePitch.clone()
points.frontArmholePitch = points.armholePitch.clone() // will be overwritten below
// Armhole hollow
points._tmp1 = new Point(points.armholePitch.x, points.armhole.y)
points._tmp2 = points._tmp1.shift(45, 10)
points._tmp3 = utils.beamsIntersect(
points._tmp1,
points._tmp2,
points.armhole,
points.armholePitch
)
points.armholeHollow = points._tmp1.shiftFractionTowards(points._tmp3, 0.5)
points.armholeCp2 = points.armhole.shift(180, points._tmp1.dx(points.armhole) / 4)
points.armholeHollowCp1 = points.armholeHollow.shift(
-45,
points.armholeHollow.dy(points.armhole) / 2
)
points.armholeHollowCp2 = points.armholeHollow.shift(
135,
points.armholePitch.dx(points.armholeHollow)
)
points.armholePitchCp1 = points.armholePitch.shift(
-90,
points.armholePitch.dy(points.armholeHollow) / 2
)
points.backArmholePitchCp1 = points.armholePitchCp1.clone()
points.frontArmholePitchCp1 = points.armholePitchCp1.clone() // will be overwritten below
points.armholePitchCp2 = points.armholePitch.shift(
90,
points.shoulder.dy(points.armholePitch) / 2
)
points.backArmholePitchCp2 = points.armholePitchCp2.clone()
points.frontArmholePitchCp2 = points.armholePitchCp2.clone() // will be overwritten below
points.shoulderCp1 = points.shoulder
.shiftTowards(points.neck, points.shoulder.dy(points.armholePitch) / 5)
.rotate(90, points.shoulder)
// Neck opening (back)
points._tmp4 = points.neck.shiftTowards(points.shoulder, 10).rotate(-90, points.neck)
points.neckCp2 = utils.beamIntersectsY(points.neck, points._tmp4, points.cbNeck.y)
// Fit collar
points.cfNeck = points.neck.rotate(-90, new Point(0, 0))
let target = measurements.neck * (1 + options.collarEase)
let delta = 0
let run = 0
do {
run++
points.cfNeck = points.cfNeck.shift(90, delta / 3)
points.frontNeckCpEdge = utils.beamsIntersect(
points.neck,
points.neckCp2,
points.cfNeck,
new Point(20, points.cfNeck.y)
)
points.cfNeckCp1 = points.cfNeck.shiftFractionTowards(points.frontNeckCpEdge, 0.55)
points.neckCp2Front = points.neck.shiftFractionTowards(points.frontNeckCpEdge, 0.65)
paths.neckOpening = new Path()
.move(points.cfNeck)
.curve(points.cfNeckCp1, points.neckCp2Front, points.neck)
.curve(points.neckCp2, points.cbNeck, points.cbNeck)
.attr('class', 'dashed stroke-xl various')
delta = paths.neckOpening.length() * 2 - target
} while (Math.abs(delta) > 1 && options.brianFitCollar && run < 10)
delete paths.neckOpening
// Anchor point for sampling
points.gridAnchor = points.cbHem
/*
* People would like to have the option to shift the shoulder seam
* See https://github.com/freesewing/freesewing/issues/642
* So let's make the people happy
*/
// Front armhole is a bit deeper, add those points
let deeper = measurements.chest * options.frontArmholeDeeper
for (const p of ['', 'Cp1', 'Cp2']) {
points[`frontArmholePitch${p}`] = points[`armholePitch${p}`].shift(180, deeper)
}
// Add points needed for the mirrored front&back neck/armhole path
macro('mirror', {
mirror: [points.hps, points.shoulder],
points: [
points.neckCp2Front,
points.cfNeckCp1,
points.cfNeck,
points.cbNeck,
points.neckCp2,
points.frontArmholePitch,
points.frontArmholePitchCp2,
points.shoulderCp1,
],
clone: true,
})
// How much space do we have to work with here?
// s3 = ShoulderSeamShift
store.set('s3CollarMaxFront', points.hps.dy(points.cfNeck) / 2)
store.set('s3CollarMaxBack', points.hps.dy(points.cbNeck) / 2)
store.set('s3ArmholeMax', points.shoulder.dy(points.frontArmholePitch) / 4)
// Let's leave the actual splitting the curves for the front/back parts
// Complete pattern?
if (complete) {
points.title = new Point(points.armholePitch.x / 2, points.armholePitch.y)
points.logo = points.title.shift(-90, 100)
snippets.logo = new Snippet('logo', points.logo)
}
return part
}
// Add points needed for the mirrored front&back neck/armhole path
macro('mirror', {
mirror: [points.hps, points.shoulder],
points: [
points.neckCp2Front,
points.cfNeckCp1,
points.cfNeck,
points.cbNeck,
points.neckCp2,
points.frontArmholePitch,
points.frontArmholePitchCp2,
points.shoulderCp1,
],
clone: true,
})
// How much space do we have to work with here?
// s3 = ShoulderSeamShift
store.set('s3CollarMaxFront', points.hps.dy(points.cfNeck) / 2)
store.set('s3CollarMaxBack', points.hps.dy(points.cbNeck) / 2)
store.set('s3ArmholeMax', points.shoulder.dy(points.frontArmholePitch) / 4)
// Let's leave the actual splitting the curves for the front/back parts
// Complete pattern?
if (complete) {
points.title = new Point(points.armholePitch.x / 2, points.armholePitch.y)
points.logo = points.title.shift(-90, 100)
snippets.logo = new Snippet('logo', points.logo)
}
return part
}

View file

@ -1,193 +1,198 @@
import * as shared from './shared'
import back from './back'
export default (part) => {
let {
store,
sa,
Point,
points,
Path,
paths,
Snippet,
snippets,
options,
complete,
paperless,
macro,
utils,
} = part.shorthand()
export default {
from: back,
name: 'front',
draft: (part) => {
const {
store,
sa,
Point,
points,
Path,
paths,
Snippet,
snippets,
options,
complete,
paperless,
macro,
utils,
} = part.shorthand()
// Re-use points for deeper armhole at the front
points.armholePitchCp1 = points.frontArmholePitchCp1
points.armholePitch = points.frontArmholePitch
points.armholePitchCp2 = points.frontArmholePitchCp2
// Re-use points for deeper armhole at the front
points.armholePitchCp1 = points.frontArmholePitchCp1
points.armholePitch = points.frontArmholePitch
points.armholePitchCp2 = points.frontArmholePitchCp2
// Adapt the shoulder line according to the relevant options
// Don't bother with less than 10% as that's just asking for trouble
if (options.s3Collar < 0.1 && options.s3Collar > -0.1) {
points.s3CollarSplit = points.hps
paths.frontCollar = new Path()
.move(points.hps)
.curve(points.neckCp2Front, points.cfNeckCp1, points.cfNeck)
.setRender(false)
} else if (options.s3Collar > 0) {
// Shift shoulder seam forward on the collar side
points.s3CollarSplit = utils.curveIntersectsY(
points.hps,
points.neckCp2Front,
points.cfNeckCp1,
points.cfNeck,
store.get('s3CollarMaxFront') * options.s3Collar
)
paths.frontCollar = new Path()
.move(points.hps)
.curve(points.neckCp2Front, points.cfNeckCp1, points.cfNeck)
.split(points.s3CollarSplit)[1]
.setRender(false)
} else if (options.s3Collar < 0) {
// Shift shoulder seam backward on the collar side
points.s3CollarSplit = utils.curveIntersectsY(
points.mirroredCbNeck,
points.mirroredCbNeck,
points.mirroredNeckCp2,
points.hps,
store.get('s3CollarMaxBack') * options.s3Collar
)
paths.frontCollar = new Path()
.move(points.hps)
.curve_(points.mirroredNeckCp2, points.mirroredCbNeck)
.split(points.s3CollarSplit)[0]
.reverse()
.join(new Path().move(points.hps).curve(points.neckCp2Front, points.cfNeckCp1, points.cfNeck))
.setRender(false)
}
if (options.s3Armhole < 0.1 && options.s3Armhole > -0.1) {
points.s3ArmholeSplit = points.shoulder
paths.frontArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.setRender(false)
} else if (options.s3Armhole > 0) {
// Shift shoulder seam forward on the armhole side
points.s3ArmholeSplit = utils.curveIntersectsY(
points.shoulder,
points.shoulderCp1,
points.armholePitchCp2,
points.armholePitch,
store.get('s3ArmholeMax') * options.s3Armhole + points.shoulder.y
)
paths.frontArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.split(points.s3ArmholeSplit)[0]
.setRender(false)
} else if (options.s3Armhole < 0) {
// Shift shoulder seam forward on the armhole side
points.s3ArmholeSplit = utils.curveIntersectsY(
points.shoulder,
points.mirroredShoulderCp1,
points.mirroredFrontArmholePitchCp2,
points.mirroredFrontArmholePitch,
store.get('s3ArmholeMax') * options.s3Armhole + points.shoulder.y
)
paths.frontArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.join(
new Path()
.move(points.shoulder)
.curve(
points.mirroredShoulderCp1,
points.mirroredFrontArmholePitchCp2,
points.mirroredFrontArmholePitch
)
.split(points.s3ArmholeSplit)[0]
// Adapt the shoulder line according to the relevant options
// Don't bother with less than 10% as that's just asking for trouble
if (options.s3Collar < 0.1 && options.s3Collar > -0.1) {
points.s3CollarSplit = points.hps
paths.frontCollar = new Path()
.move(points.hps)
.curve(points.neckCp2Front, points.cfNeckCp1, points.cfNeck)
.setRender(false)
} else if (options.s3Collar > 0) {
// Shift shoulder seam forward on the collar side
points.s3CollarSplit = utils.curveIntersectsY(
points.hps,
points.neckCp2Front,
points.cfNeckCp1,
points.cfNeck,
store.get('s3CollarMaxFront') * options.s3Collar
)
.setRender(false)
}
// Rename cb (center back) to cf (center front)
for (let key of ['Shoulder', 'Armhole', 'Waist', 'Hips', 'Hem']) {
points[`cf${key}`] = new Point(points[`cb${key}`].x, points[`cb${key}`].y)
delete points[`cb${key}`]
}
// Front neckline points
points.neckCp2 = new Point(points.neckCp2Front.x, points.neckCp2Front.y)
// Seamline
paths.saBase = new Path()
.move(points.cfHem)
.line(points.hem)
.line(points.armhole)
.curve(points.armholeCp2, points.armholeHollowCp1, points.armholeHollow)
.curve(points.armholeHollowCp2, points.armholePitchCp1, points.armholePitch)
.join(paths.frontArmhole)
.line(points.s3CollarSplit)
.join(paths.frontCollar)
paths.saBase.render = false
paths.seam = new Path()
.move(points.cfNeck)
.line(points.cfHem)
.join(paths.saBase)
.attr('class', 'fabric')
// Store lengths to fit sleeve
store.set('frontArmholeLength', shared.armholeLength(points, Path))
store.set('frontArmholeToArmholePitch', shared.armholeToArmholePitch(points, Path))
// Complete pattern?
if (complete) {
macro('cutonfold', {
from: points.cfNeck,
to: points.cfHips,
grainline: true,
})
macro('title', { at: points.title, nr: 1, title: 'front' })
snippets.armholePitchNotch = new Snippet('notch', points.armholePitch)
paths.waist = new Path().move(points.cfWaist).line(points.waist).attr('class', 'help')
if (sa) {
paths.sa = paths.saBase
.offset(sa)
.attr('class', 'fabric sa')
.line(points.cfNeck)
.move(points.cfHips)
paths.sa.line(paths.sa.start())
paths.frontCollar = new Path()
.move(points.hps)
.curve(points.neckCp2Front, points.cfNeckCp1, points.cfNeck)
.split(points.s3CollarSplit)[1]
.setRender(false)
} else if (options.s3Collar < 0) {
// Shift shoulder seam backward on the collar side
points.s3CollarSplit = utils.curveIntersectsY(
points.mirroredCbNeck,
points.mirroredCbNeck,
points.mirroredNeckCp2,
points.hps,
store.get('s3CollarMaxBack') * options.s3Collar
)
paths.frontCollar = new Path()
.move(points.hps)
.curve_(points.mirroredNeckCp2, points.mirroredCbNeck)
.split(points.s3CollarSplit)[0]
.reverse()
.join(new Path().move(points.hps).curve(points.neckCp2Front, points.cfNeckCp1, points.cfNeck))
.setRender(false)
}
if (options.s3Armhole < 0.1 && options.s3Armhole > -0.1) {
points.s3ArmholeSplit = points.shoulder
paths.frontArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.setRender(false)
} else if (options.s3Armhole > 0) {
// Shift shoulder seam forward on the armhole side
points.s3ArmholeSplit = utils.curveIntersectsY(
points.shoulder,
points.shoulderCp1,
points.armholePitchCp2,
points.armholePitch,
store.get('s3ArmholeMax') * options.s3Armhole + points.shoulder.y
)
paths.frontArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.split(points.s3ArmholeSplit)[0]
.setRender(false)
} else if (options.s3Armhole < 0) {
// Shift shoulder seam forward on the armhole side
points.s3ArmholeSplit = utils.curveIntersectsY(
points.shoulder,
points.mirroredShoulderCp1,
points.mirroredFrontArmholePitchCp2,
points.mirroredFrontArmholePitch,
store.get('s3ArmholeMax') * options.s3Armhole + points.shoulder.y
)
paths.frontArmhole = new Path()
.move(points.armholePitch)
.curve(points.armholePitchCp2, points.shoulderCp1, points.shoulder)
.join(
new Path()
.move(points.shoulder)
.curve(
points.mirroredShoulderCp1,
points.mirroredFrontArmholePitchCp2,
points.mirroredFrontArmholePitch
)
.split(points.s3ArmholeSplit)[0]
)
.setRender(false)
}
// Add notches if the shoulder seam is shifted
shared.s3Notches(part, 'notch')
}
// Rename cb (center back) to cf (center front)
for (let key of ['Shoulder', 'Armhole', 'Waist', 'Hips', 'Hem']) {
points[`cf${key}`] = new Point(points[`cb${key}`].x, points[`cb${key}`].y)
delete points[`cb${key}`]
}
// Front neckline points
points.neckCp2 = new Point(points.neckCp2Front.x, points.neckCp2Front.y)
// Paperless?
if (paperless) {
shared.dimensions(part, 'front')
macro('hd', {
from: points.cfHips,
to: points.hips,
y: points.hem.y + sa + 15,
})
macro('vd', {
from: points.cfHem,
to: points.cfWaist,
x: points.cfHips.x - sa - 15,
})
macro('vd', {
from: points.cfHem,
to: points.cfNeck,
x: points.cfHips.x - sa - 30,
})
macro('hd', {
from: points.cfNeck,
to: points.s3CollarSplit,
y: points.s3CollarSplit.y - sa - 15,
})
macro('hd', {
from: points.cfNeck,
to: points.s3ArmholeSplit,
y: points.s3CollarSplit.y - sa - 30,
})
}
// Seamline
paths.saBase = new Path()
.move(points.cfHem)
.line(points.hem)
.line(points.armhole)
.curve(points.armholeCp2, points.armholeHollowCp1, points.armholeHollow)
.curve(points.armholeHollowCp2, points.armholePitchCp1, points.armholePitch)
.join(paths.frontArmhole)
.line(points.s3CollarSplit)
.join(paths.frontCollar)
return part
paths.saBase.render = false
paths.seam = new Path()
.move(points.cfNeck)
.line(points.cfHem)
.join(paths.saBase)
.attr('class', 'fabric')
// Store lengths to fit sleeve
store.set('frontArmholeLength', shared.armholeLength(points, Path))
store.set('frontArmholeToArmholePitch', shared.armholeToArmholePitch(points, Path))
// Complete pattern?
if (complete) {
macro('cutonfold', {
from: points.cfNeck,
to: points.cfHips,
grainline: true,
})
macro('title', { at: points.title, nr: 1, title: 'front' })
snippets.armholePitchNotch = new Snippet('notch', points.armholePitch)
paths.waist = new Path().move(points.cfWaist).line(points.waist).attr('class', 'help')
if (sa) {
paths.sa = paths.saBase
.offset(sa)
.attr('class', 'fabric sa')
.line(points.cfNeck)
.move(points.cfHips)
paths.sa.line(paths.sa.start())
}
// Add notches if the shoulder seam is shifted
shared.s3Notches(part, 'notch')
}
// Paperless?
if (paperless) {
shared.dimensions(part, 'front')
macro('hd', {
from: points.cfHips,
to: points.hips,
y: points.hem.y + sa + 15,
})
macro('vd', {
from: points.cfHem,
to: points.cfWaist,
x: points.cfHips.x - sa - 15,
})
macro('vd', {
from: points.cfHem,
to: points.cfNeck,
x: points.cfHips.x - sa - 30,
})
macro('hd', {
from: points.cfNeck,
to: points.s3CollarSplit,
y: points.s3CollarSplit.y - sa - 15,
})
macro('hd', {
from: points.cfNeck,
to: points.s3ArmholeSplit,
y: points.s3CollarSplit.y - sa - 30,
})
}
return part
}
}

View file

@ -1,38 +1,38 @@
// FreeSewing core library
import freesewing from '@freesewing/core'
import plugins from '@freesewing/plugin-bundle'
import plugin from '@freesewing/plugin-bust' // Note: conditional plugin
import config from '../config'
// Parts
import draftBase from './base'
import draftBack from './back'
import draftFront from './front'
import draftSleevecap from './sleevecap'
import draftSleeve from './sleeve'
// FreeSewing Plugins
import pluginBundle from '@freesewing/plugin-bundle'
import bustPlugin from '@freesewing/plugin-bust' // Note: conditional plugin
// Design config & options
import { info, measurements, optionalMeasurements } from '../config/index'
//import * as options from '../config/options'
// Design parts
import back from './back'
import front from './front'
import sleeve from './sleeve'
// These are only here to be exported
import base from './base'
import sleevecap from './sleevecap'
/* Check to see whether we should load the bust plugin
* Only of the `draftForHighBust` options is set
* AND the highBust measurement is available
*/
const condition = (settings = false) =>
settings &&
settings.options &&
settings.options.draftForHighBust &&
settings.measurements.highBust
? true
: false
// Create design
const Brian = new freesewing.Design(config, plugins, { plugin, condition })
// Attach draft methods to prototype
Brian.prototype.draftBase = draftBase
Brian.prototype.draftBack = draftBack
Brian.prototype.draftFront = draftFront
Brian.prototype.draftSleevecap = draftSleevecap
Brian.prototype.draftSleeve = draftSleeve
// Setup design
const Brian = new freesewing.Design({
...info,
measurements,
optionalMeasurements,
// options: { ...options },
parts: { back, front, sleeve },
plugins: pluginBundle,
conditionalPlugins: {
plugin: bustPlugin,
condition: (settings=false) =>
settings?.options?.draftForHighBust &&
settings?.measurements?.highBust
? true : false
}
})
// Named exports
export { config, Brian }
export { back, front, sleeve, base, sleevecap }
// Default export
export default Brian

View file

@ -1,89 +1,103 @@
export default (part) => {
const {
store,
sa,
measurements,
options,
Point,
points,
Path,
paths,
Snippet,
snippets,
complete,
paperless,
macro,
} = part.shorthand()
import sleevecap from './sleevecap'
import { _sleeve as options } from '../config/options.js'
// Determine the sleeve length
store.set('sleeveLength', measurements.shoulderToWrist * (1 + options.sleeveLengthBonus))
points.sleeveTip = paths.sleevecap.edge('top')
points.sleeveTop = new Point(0, points.sleeveTip.y) // Always in center
export default {
from: sleevecap,
name: 'sleeve',
options,
draft: (part) => {
const {
store,
sa,
measurements,
options,
Point,
points,
Path,
paths,
Snippet,
snippets,
complete,
paperless,
macro,
} = part.shorthand()
// Wrist
points.centerWrist = points.sleeveTop.shift(-90, store.get('sleeveLength'))
points.wristRight = points.centerWrist.shift(0, (measurements.wrist * (1 + options.cuffEase)) / 2)
points.wristLeft = points.wristRight.rotate(180, points.centerWrist)
// Remove things inherited
macro('cutonfold', false)
macro('rmad')
delete paths.waist
for (const key in snippets) delete snippets[key]
// Paths
paths.sleevecap.render = false
paths.seam = new Path()
.move(points.bicepsLeft)
.move(points.wristLeft)
.move(points.wristRight)
.line(points.bicepsRight)
.join(paths.sleevecap)
.close()
.attr('class', 'fabric')
// Determine the sleeve length
store.set('sleeveLength', measurements.shoulderToWrist * (1 + options.sleeveLengthBonus))
points.sleeveTip = paths.sleevecap.edge('top')
points.sleeveTop = new Point(0, points.sleeveTip.y) // Always in center
// Anchor point for sampling
points.gridAnchor = new Point(0, 0)
// Wrist
points.centerWrist = points.sleeveTop.shift(-90, store.get('sleeveLength'))
points.wristRight = points.centerWrist.shift(0, (measurements.wrist * (1 + options.cuffEase)) / 2)
points.wristLeft = points.wristRight.rotate(180, points.centerWrist)
// Complete pattern?
if (complete) {
points.logo = points.centerBiceps.shiftFractionTowards(points.centerWrist, 0.3)
snippets.logo = new Snippet('logo', points.logo)
macro('title', { at: points.centerBiceps, nr: 3, title: 'sleeve' })
macro('grainline', { from: points.centerWrist, to: points.centerBiceps })
points.scaleboxAnchor = points.scalebox = points.centerBiceps.shiftFractionTowards(
points.centerWrist,
0.5
)
macro('scalebox', { at: points.scalebox })
// Paths
paths.sleevecap.render = false
paths.seam = new Path()
.move(points.bicepsLeft)
.move(points.wristLeft)
.move(points.wristRight)
.line(points.bicepsRight)
.join(paths.sleevecap)
.close()
.attr('class', 'fabric')
points.frontNotch = paths.sleevecap.shiftAlong(store.get('frontArmholeToArmholePitch'))
points.backNotch = paths.sleevecap.reverse().shiftAlong(store.get('backArmholeToArmholePitch'))
snippets.frontNotch = new Snippet('notch', points.frontNotch)
snippets.backNotch = new Snippet('bnotch', points.backNotch)
if (sa) paths.sa = paths.seam.offset(sa).attr('class', 'fabric sa')
// Anchor point for sampling
points.gridAnchor = new Point(0, 0)
// Complete pattern?
if (complete) {
points.logo = points.centerBiceps.shiftFractionTowards(points.centerWrist, 0.3)
snippets.logo = new Snippet('logo', points.logo)
macro('title', { at: points.centerBiceps, nr: 3, title: 'sleeve' })
macro('grainline', { from: points.centerWrist, to: points.centerBiceps })
points.scaleboxAnchor = points.scalebox = points.centerBiceps.shiftFractionTowards(
points.centerWrist,
0.5
)
macro('scalebox', { at: points.scalebox })
points.frontNotch = paths.sleevecap.shiftAlong(store.get('frontArmholeToArmholePitch'))
points.backNotch = paths.sleevecap.reverse().shiftAlong(store.get('backArmholeToArmholePitch'))
snippets.frontNotch = new Snippet('notch', points.frontNotch)
snippets.backNotch = new Snippet('bnotch', points.backNotch)
if (sa) paths.sa = paths.seam.offset(sa).attr('class', 'fabric sa')
}
// Paperless?
if (paperless) {
macro('vd', {
from: points.wristLeft,
to: points.bicepsLeft,
x: points.bicepsLeft.x - sa - 15,
})
macro('vd', {
from: points.wristLeft,
to: points.sleeveTip,
x: points.bicepsLeft.x - sa - 30,
})
macro('hd', {
from: points.bicepsLeft,
to: points.bicepsRight,
y: points.sleeveTip.y - sa - 30,
})
macro('hd', {
from: points.wristLeft,
to: points.wristRight,
y: points.wristLeft.y + sa + 30,
})
macro('pd', {
path: paths.sleevecap.reverse(),
d: -1 * sa - 15,
})
}
return part
}
// Paperless?
if (paperless) {
macro('vd', {
from: points.wristLeft,
to: points.bicepsLeft,
x: points.bicepsLeft.x - sa - 15,
})
macro('vd', {
from: points.wristLeft,
to: points.sleeveTip,
x: points.bicepsLeft.x - sa - 30,
})
macro('hd', {
from: points.bicepsLeft,
to: points.bicepsRight,
y: points.sleeveTip.y - sa - 30,
})
macro('hd', {
from: points.wristLeft,
to: points.wristRight,
y: points.wristLeft.y + sa + 30,
})
macro('pd', {
path: paths.sleevecap.reverse(),
d: -1 * sa - 15,
})
}
return part
}

View file

@ -1,3 +1,6 @@
import front from './front'
import { _sleevecap as options } from '../config/options.js'
/** Calculates the differece between actual and optimal sleevecap length
* Positive values mean sleevecap is longer than armhole
*/
@ -136,25 +139,31 @@ function draftSleevecap(part, run) {
}
}
export default (part) => {
let { store, units, options, Point, points, paths, raise } = part.shorthand()
export default {
from: front,
name: 'sleevecap',
hide: true,
options,
draft: (part) => {
const { store, units, options, Point, points, paths, raise } = part.shorthand()
store.set('sleeveFactor', 1)
let run = 0
let delta = 0
do {
draftSleevecap(part, run)
delta = sleevecapDelta(store)
sleevecapAdjust(store)
run++
raise.debug(`Fitting Brian sleevecap. Run ${run}: delta is ${units(delta)}`)
} while (options.brianFitSleeve === true && run < 50 && Math.abs(sleevecapDelta(store)) > 2)
store.set('sleeveFactor', 1)
let run = 0
let delta = 0
do {
draftSleevecap(part, run)
delta = sleevecapDelta(store)
sleevecapAdjust(store)
run++
raise.debug(`Fitting Brian sleevecap. Run ${run}: delta is ${units(delta)}`)
} while (options.brianFitSleeve === true && run < 50 && Math.abs(sleevecapDelta(store)) > 2)
// Paths
paths.sleevecap.attr('class', 'fabric')
// Paths
paths.sleevecap.attr('class', 'fabric')
// Anchor point for sampling
points.gridAnchor = new Point(0, 0)
// Anchor point for sampling
points.gridAnchor = new Point(0, 0)
return part
return part
}
}

View file

@ -1,29 +1,55 @@
import Pattern from './pattern'
import { addPartConfig } from './utils.js'
// Default hide method for options
const hide = () => false
/*
* The Design constructor. Returns a Pattern constructor
* So it's sort of a super-constructor
*/
export default function Design(config, plugins = false, conditionalPlugins = false) {
// Add default hide() method to config.options
for (const option in config.options) {
if (typeof config.options[option] === 'object') {
config.options[option] = {
hide,
...config.options[option],
}
// Add part options/measurements/optionalMeasurements to config
if (!config.options) config.options = {}
if (!config.measurements) config.measurements = []
if (!config.optionalMeasurements) config.optionalMeasurements = []
if (Array.isArray(config.parts)) {
const parts = {}
for (const part of config.parts) {
if (typeof part === 'object') {
parts[part.name] = part
config = addPartConfig(parts[part.name], config)
} else if (typeof part === 'string') {
parts[part] = part
} else throw("Part should be passed as a name of part config object")
}
config.parts = parts
}
// Ensure all options have a hide() method
config.options = optionsWithHide(config.options)
// A place to store deprecation and other warnings before we even have a pattern instantiated
config.warnings = []
/*
* The newer way to initalize a design is to pass one single parameter
* The old way passed multiple parameters.
* So let's figure out which is which and be backwards compatible
*
* This mitigation should be removed in v3 when we drop support for the legacy way
*/
config = migrateConfig(config, plugins, conditionalPlugins)
const pattern = function (settings) {
Pattern.call(this, config)
// Load plugins
if (Array.isArray(plugins)) for (let plugin of plugins) this.use(plugin)
else if (plugins) this.use(plugins)
if (Array.isArray(config.plugins)) for (const plugin of config.plugins) this.use(plugin)
else if (config.plugins) this.use(config.plugins)
// Load conditional plugins
if (Array.isArray(conditionalPlugins))
for (let plugin of conditionalPlugins) this.useIf(plugin, settings)
else if (conditionalPlugins.plugin && conditionalPlugins.condition)
this.useIf(conditionalPlugins, settings)
if (Array.isArray(config.conditionalPlugins))
for (const plugin of config.conditionalPlugins) this.useIf(plugin, settings)
else if (config.conditionalPlugins.plugin && config.conditionalPlugins.condition)
this.useIf(config.conditionalPlugins, settings)
this.apply(settings)
@ -39,3 +65,59 @@ export default function Design(config, plugins = false, conditionalPlugins = fal
return pattern
}
/*
* Helper method to handle the legacy way of passing configuration
* to the design constructor
*/
const migrateConfig = (config, plugins, conditionalPlugins) => {
// Migrate plugins
if (plugins && config.plugins) config.warnings.push(
'Passing plugins to the Design constructor both as a second parameter and in the config is unsupported',
'Ignoring plugins passed as parameter. Only config.plugins will be used.'
)
else if (plugins && !config.plugins) {
config.plugins = plugins
config.warnings.push(
'Passing a plugins parameter to the Design constructure is deprecated',
'Please store them in the `plugins` key of the config object that is the first parameter'
)
} else if (!config.plugins) config.plugins = []
// Migrate conditional plugins
if (conditionalPlugins && config.conditionalPlugins) config.warnings.push(
'Passing conditionalPlugins to the Design constructor both as a third parameter and in the config is unsupported.',
'Ignoring conditionalPlugins passes as parameter. Only config.conditionalPlugins will be used.',
)
else if (conditionalPlugins && !config.conditionalPlugins) {
config.conditionalPlugins = conditionalPlugins
config.warnings.push(
'Passing a conditionalPlugins parameter to the Design constructure is deprecated.',
'Please store them in the `conditionalPlugins` key of the config object that is the first parameter'
)
}
else if (!config.conditionalPlugins) config.conditionalPlugins = []
return config
}
/*
* A default hide() method for options that lack it
* Since this will always return false, the option will never be hidden
*/
const hide = () => false // The default hide() method
/*
* Helper method to add the default hide() method to options who lack one
*/
const optionsWithHide = options => {
if (options) {
for (const option in options) {
if (typeof options[option] === 'object') options[option] = { hide, ...options[option] }
}
}
return options
}

View file

@ -180,9 +180,9 @@ Part.prototype.units = function (input) {
/** Returns an object with shorthand access for pattern design */
Part.prototype.shorthand = function () {
let complete = this.context.settings.complete ? true : false
let paperless = this.context.settings.paperless === true ? true : false
let sa = this.context.settings.complete ? this.context.settings.sa || 0 : 0
const complete = this.context.settings.complete ? true : false
const paperless = this.context.settings.paperless === true ? true : false
const sa = this.context.settings.complete ? this.context.settings.sa || 0 : 0
const shorthand = {
sa,
scale: this.context.settings.scale,
@ -198,154 +198,142 @@ Part.prototype.shorthand = function () {
removeCut: this.removeCut,
}
if (this.context.settings.debug) {
// We'll need this
let self = this
// We'll need this
let self = this
// Wrap the Point constructor so objects can raise events
shorthand.Point = function (x, y) {
Point.apply(this, [x, y, true])
Object.defineProperty(this, 'raise', { value: self.context.raise })
}
shorthand.Point.prototype = Object.create(Point.prototype)
// Wrap the Path constructor so objects can raise events
shorthand.Path = function () {
Path.apply(this, [true])
Object.defineProperty(this, 'raise', { value: self.context.raise })
}
shorthand.Path.prototype = Object.create(Path.prototype)
// Wrap the Snippet constructor so objects can raise events
shorthand.Snippet = function (def, anchor) {
Snippet.apply(this, [def, anchor, true])
Snippet.apply(this, arguments)
Object.defineProperty(this, 'raise', { value: self.context.raise })
}
shorthand.Snippet.prototype = Object.create(Snippet.prototype)
// Proxy the points object
const pointsProxy = {
get: function () {
return Reflect.get(...arguments)
},
set: (points, name, value) => {
// Constructor checks
if (value instanceof Point !== true)
self.context.raise.warning(
`\`points.${name}\` was set with a value that is not a \`Point\` object`
)
if (value.x == null || !utils.isCoord(value.x))
self.context.raise.warning(
`\`points.${name}\` was set with a \`x\` parameter that is not a \`number\``
)
if (value.y == null || !utils.isCoord(value.y))
self.context.raise.warning(
`\`points.${name}\` was set with a \`y\` parameter that is not a \`number\``
)
try {
value.name = name
} catch (err) {
self.context.raise.warning(`Could not set \`name\` property on \`points.${name}\``)
}
return (self.points[name] = value)
},
}
shorthand.points = new Proxy(this.points || {}, pointsProxy)
// Proxy the paths object
const pathsProxy = {
get: function () {
return Reflect.get(...arguments)
},
set: (paths, name, value) => {
// Constructor checks
if (value instanceof Path !== true)
self.context.raise.warning(
`\`paths.${name}\` was set with a value that is not a \`Path\` object`
)
try {
value.name = name
} catch (err) {
self.context.raise.warning(`Could not set \`name\` property on \`paths.${name}\``)
}
return (self.paths[name] = value)
},
}
shorthand.paths = new Proxy(this.paths || {}, pathsProxy)
// Proxy the snippets object
const snippetsProxy = {
get: function (target, prop, receiver) {
return Reflect.get(...arguments)
},
set: (snippets, name, value) => {
// Constructor checks
if (value instanceof Snippet !== true)
self.context.raise.warning(
`\`snippets.${name}\` was set with a value that is not a \`Snippet\` object`
)
if (typeof value.def !== 'string')
self.context.raise.warning(
`\`snippets.${name}\` was set with a \`def\` parameter that is not a \`string\``
)
if (value.anchor instanceof Point !== true)
self.context.raise.warning(
`\`snippets.${name}\` was set with an \`anchor\` parameter that is not a \`Point\``
)
try {
value.name = name
} catch (err) {
self.context.raise.warning(`Could not set \`name\` property on \`snippets.${name}\``)
}
return (self.snippets[name] = value)
},
}
shorthand.snippets = new Proxy(this.snippets || {}, snippetsProxy)
// Proxy the measurements object
const measurementsProxy = {
get: function (measurements, name) {
if (typeof measurements[name] === 'undefined')
self.context.raise.warning(
`Tried to access \`measurements.${name}\` but it is \`undefined\``
)
return Reflect.get(...arguments)
},
set: (measurements, name, value) => (self.context.settings.measurements[name] = value),
}
shorthand.measurements = new Proxy(this.context.settings.measurements || {}, measurementsProxy)
// Proxy the options object
const optionsProxy = {
get: function (options, name) {
if (typeof options[name] === 'undefined')
self.context.raise.warning(`Tried to access \`options.${name}\` but it is \`undefined\``)
return Reflect.get(...arguments)
},
set: (options, name, value) => (self.context.settings.options[name] = value),
}
shorthand.options = new Proxy(this.context.settings.options || {}, optionsProxy)
// Proxy the absoluteOptions object
const absoluteOptionsProxy = {
get: function (absoluteOptions, name) {
if (typeof absoluteOptions[name] === 'undefined')
self.context.raise.warning(
`Tried to access \`absoluteOptions.${name}\` but it is \`undefined\``
)
return Reflect.get(...arguments)
},
set: (absoluteOptions, name, value) => (self.context.settings.absoluteOptions[name] = value),
}
shorthand.absoluteOptions = new Proxy(
this.context.settings.absoluteOptions || {},
absoluteOptionsProxy
)
} else {
shorthand.Point = Point
shorthand.Path = Path
shorthand.Snippet = Snippet
shorthand.points = this.points || {}
shorthand.paths = this.paths || {}
shorthand.snippets = this.snippets || {}
shorthand.measurements = this.context.settings.measurements || {}
shorthand.options = this.context.settings.options || {}
shorthand.absoluteOptions = this.context.settings.absoluteOptions || {}
// Wrap the Point constructor so objects can raise events
shorthand.Point = function (x, y) {
Point.apply(this, [x, y, true])
Object.defineProperty(this, 'raise', { value: self.context.raise })
}
shorthand.Point.prototype = Object.create(Point.prototype)
// Wrap the Path constructor so objects can raise events
shorthand.Path = function () {
Path.apply(this, [true])
Object.defineProperty(this, 'raise', { value: self.context.raise })
}
shorthand.Path.prototype = Object.create(Path.prototype)
// Wrap the Snippet constructor so objects can raise events
shorthand.Snippet = function (def, anchor) {
Snippet.apply(this, [def, anchor, true])
Snippet.apply(this, arguments)
Object.defineProperty(this, 'raise', { value: self.context.raise })
}
shorthand.Snippet.prototype = Object.create(Snippet.prototype)
// Proxy the points object
const pointsProxy = {
get: function () {
return Reflect.get(...arguments)
},
set: (points, name, value) => {
// Constructor checks
if (value instanceof Point !== true)
self.context.raise.warning(
`\`points.${name}\` was set with a value that is not a \`Point\` object`
)
if (value.x == null || !utils.isCoord(value.x))
self.context.raise.warning(
`\`points.${name}\` was set with a \`x\` parameter that is not a \`number\``
)
if (value.y == null || !utils.isCoord(value.y))
self.context.raise.warning(
`\`points.${name}\` was set with a \`y\` parameter that is not a \`number\``
)
try {
value.name = name
} catch (err) {
self.context.raise.warning(`Could not set \`name\` property on \`points.${name}\``)
}
return (self.points[name] = value)
},
}
shorthand.points = new Proxy(this.points || {}, pointsProxy)
// Proxy the paths object
const pathsProxy = {
get: function () {
return Reflect.get(...arguments)
},
set: (paths, name, value) => {
// Constructor checks
if (value instanceof Path !== true)
self.context.raise.warning(
`\`paths.${name}\` was set with a value that is not a \`Path\` object`
)
try {
value.name = name
} catch (err) {
self.context.raise.warning(`Could not set \`name\` property on \`paths.${name}\``)
}
return (self.paths[name] = value)
},
}
shorthand.paths = new Proxy(this.paths || {}, pathsProxy)
// Proxy the snippets object
const snippetsProxy = {
get: function (target, prop, receiver) {
return Reflect.get(...arguments)
},
set: (snippets, name, value) => {
// Constructor checks
if (value instanceof Snippet !== true)
self.context.raise.warning(
`\`snippets.${name}\` was set with a value that is not a \`Snippet\` object`
)
if (typeof value.def !== 'string')
self.context.raise.warning(
`\`snippets.${name}\` was set with a \`def\` parameter that is not a \`string\``
)
if (value.anchor instanceof Point !== true)
self.context.raise.warning(
`\`snippets.${name}\` was set with an \`anchor\` parameter that is not a \`Point\``
)
try {
value.name = name
} catch (err) {
self.context.raise.warning(`Could not set \`name\` property on \`snippets.${name}\``)
}
return (self.snippets[name] = value)
},
}
shorthand.snippets = new Proxy(this.snippets || {}, snippetsProxy)
// Proxy the measurements object
const measurementsProxy = {
get: function (measurements, name) {
if (typeof measurements[name] === 'undefined')
self.context.raise.warning(
`Tried to access \`measurements.${name}\` but it is \`undefined\``
)
return Reflect.get(...arguments)
},
set: (measurements, name, value) => (self.context.settings.measurements[name] = value),
}
shorthand.measurements = new Proxy(this.context.settings.measurements || {}, measurementsProxy)
// Proxy the options object
const optionsProxy = {
get: function (options, name) {
if (typeof options[name] === 'undefined')
self.context.raise.warning(`Tried to access \`options.${name}\` but it is \`undefined\``)
return Reflect.get(...arguments)
},
set: (options, name, value) => (self.context.settings.options[name] = value),
}
shorthand.options = new Proxy(this.context.settings.options || {}, optionsProxy)
// Proxy the absoluteOptions object
const absoluteOptionsProxy = {
get: function (absoluteOptions, name) {
if (typeof absoluteOptions[name] === 'undefined')
self.context.raise.warning(
`Tried to access \`absoluteOptions.${name}\` but it is \`undefined\``
)
return Reflect.get(...arguments)
},
set: (absoluteOptions, name, value) => (self.context.settings.absoluteOptions[name] = value),
}
shorthand.absoluteOptions = new Proxy(
this.context.settings.absoluteOptions || {},
absoluteOptionsProxy
)
return shorthand
}

View file

@ -1,4 +1,11 @@
import { macroName, sampleStyle, capitalize } from './utils'
import {
macroName,
sampleStyle,
capitalize,
decoratePartDependency,
addPartConfig,
mergeDependencies,
} from './utils.js'
import Part from './part'
import Point from './point'
import Path from './path'
@ -11,7 +18,8 @@ import Attributes from './attributes'
import pkg from '../package.json'
export default function Pattern(config = { options: {} }) {
// Default settings
// Apply default settings
this.settings = {
complete: true,
idPrefix: 'fs-',
@ -25,15 +33,16 @@ export default function Pattern(config = { options: {} }) {
absoluteOptions: {},
}
// Events store and raise methods
// Object to hold events
this.events = {
info: [],
warning: [],
error: [],
debug: [],
}
// Raise methods - Make events and settings avialable in them
const events = this.events
// Make settings available in the raise.debug method
const settings = this.settings
this.raise = {
info: function (data) {
@ -50,10 +59,13 @@ export default function Pattern(config = { options: {} }) {
if (settings.debug) events.debug.push(data)
},
}
// Say hi
this.raise.info(
`New \`@freesewing/${config.name}:${config.version}\` pattern using \`@freesewing/core:${pkg.version}\``
)
// More things that go in a pattern
this.config = config // Pattern configuration
this.width = 0 // Will be set after render
this.height = 0 // Will be set after render
@ -68,30 +80,17 @@ export default function Pattern(config = { options: {} }) {
this.Path = Path // Path constructor
this.Snippet = Snippet // Snippet constructor
this.Attributes = Attributes // Attributes constructor
this.initialized = 0 // Keep track of init calls
if (typeof this.config.dependencies === 'undefined') this.config.dependencies = {}
if (typeof this.config.inject === 'undefined') this.config.inject = {}
if (typeof this.config.hide === 'undefined') this.config.hide = []
this.config.resolvedDependencies = this.resolveDependencies(this.config.dependencies)
this.config.draftOrder = this.draftOrder(this.config.resolvedDependencies)
// Convert options
for (let i in config.options) {
let option = config.options[i]
if (typeof option === 'object') {
if (typeof option.pct !== 'undefined') this.settings.options[i] = option.pct / 100
else if (typeof option.mm !== 'undefined') this.settings.options[i] = option.mm
else if (typeof option.deg !== 'undefined') this.settings.options[i] = option.deg
else if (typeof option.count !== 'undefined') this.settings.options[i] = option.count
else if (typeof option.bool !== 'undefined') this.settings.options[i] = option.bool
else if (typeof option.dflt !== 'undefined') this.settings.options[i] = option.dflt
else {
let err = 'Unknown option type: ' + JSON.stringify(option)
this.raise.error(err)
throw new Error(err)
}
} else {
this.settings.options[i] = option
this.addOptions(config.options)
if (this.config.parts) {
for (const partName in this.config.parts) {
if (this.config.parts[partName].options) this.addOptions(this.config.parts[partName].options)
}
}
@ -122,6 +121,68 @@ export default function Pattern(config = { options: {} }) {
}
}
// Converts/adds options
Pattern.prototype.addOptions = function(options={}) {
for (const i in options) {
// Add to config
const option = options[i]
this.config.options[i] = option
if (typeof option === 'object') {
if (typeof option.pct !== 'undefined') this.settings.options[i] = option.pct / 100
else if (typeof option.mm !== 'undefined') this.settings.options[i] = option.mm
else if (typeof option.deg !== 'undefined') this.settings.options[i] = option.deg
else if (typeof option.count !== 'undefined') this.settings.options[i] = option.count
else if (typeof option.bool !== 'undefined') this.settings.options[i] = option.bool
else if (typeof option.dflt !== 'undefined') this.settings.options[i] = option.dflt
else {
let err = 'Unknown option type: ' + JSON.stringify(option)
this.raise.error(err)
throw new Error(err)
}
} else {
this.settings.options[i] = option
}
}
// Make it chainable
return this
}
Pattern.prototype.getConfig = function () {
this.init()
return this.config
}
/*
* Defer some things that used to happen in the constructor to
* facilitate late-stage adding of parts
*/
Pattern.prototype.init = function () {
this.initialized++
// Resolve all dependencies
this.dependencies = this.config.dependencies
this.inject = this.config.inject
this.hide = this.config.hide
if (typeof this.config.parts === 'object') {
this.__parts = this.config.parts
this.preresolveDependencies()
}
this.resolvedDependencies = this.resolveDependencies(this.dependencies)
this.config.resolvedDependencies = this.resolvedDependencies
this.config.draftOrder = this.draftOrder(this.resolvedDependencies)
// Make all parts uniform
if (this.__parts) {
for (const [key, value] of Object.entries(this.__parts)) {
this.__parts[key] = decoratePartDependency(value)
}
}
return this
}
function snappedOption(option, pattern) {
const conf = pattern.config.options[option]
const abs = conf.toAbs(pattern.settings.options[option], pattern.settings)
@ -155,7 +216,7 @@ function snappedOption(option, pattern) {
// Merges settings object with this.settings
Pattern.prototype.apply = function (settings) {
if (typeof settings !== 'object') {
this.raise.warning('Pattern initialized without any settings')
this.raise.warning('Pattern instantiated without any settings')
return this
}
for (let key of Object.keys(settings)) {
@ -186,10 +247,31 @@ Pattern.prototype.runHooks = function (hookName, data = false) {
}
}
/*
* Allows adding a part at run-time
*/
Pattern.prototype.addPart = function (part, name=false) {
if (!part.draft) part = decoratePartDependency(part, name)
if (typeof part?.draft === 'function') {
if (part.name) {
this.config.parts[part.name] = part
// Add part-level config to config
this.config = addPartConfig(part, this.config)
}
else this.raise.error(`Part must have a name`)
}
else this.raise.warning(`Cannot attach part ${name} because it is not a part`)
return this
}
/**
* The default draft method with pre- and postDraft hooks
*/
Pattern.prototype.draft = function () {
// Late-stage initialization
this.init()
if (this.is !== 'sample') {
this.is = 'draft'
this.cutList = {}
@ -207,33 +289,49 @@ Pattern.prototype.draft = function () {
}
this.runHooks('preDraft')
for (let partName of this.config.draftOrder) {
for (const partName of this.config.draftOrder) {
// Create parts
this.raise.debug(`Creating part \`${partName}\``)
this.parts[partName] = new this.Part(partName)
if (typeof this.config.inject[partName] === 'string') {
// Handle inject/inheritance
if (typeof this.inject[partName] === 'string') {
this.raise.debug(
`Injecting part \`${this.config.inject[partName]}\` into part \`${partName}\``
`Injecting part \`${this.inject[partName]}\` into part \`${partName}\``
)
try {
this.parts[partName].inject(this.parts[this.config.inject[partName]])
this.parts[partName].inject(this.parts[this.inject[partName]])
} catch (err) {
this.raise.error([
`Could not inject part \`${this.config.inject[partName]}\` into part \`${partName}\``,
`Could not inject part \`${this.inject[partName]}\` into part \`${partName}\``,
err,
])
}
}
if (this.needs(partName)) {
let method = 'draft' + capitalize(partName)
if (typeof this[method] !== 'function') {
this.raise.error(`Method \`pattern.${method}\` is callable`)
throw new Error('Method "' + method + '" on pattern object is not callable')
// Draft part
const method = 'draft' + capitalize(partName)
if (typeof this.__parts?.[partName]?.draft === 'function') {
// 2022 way - Part is contained in config
try {
this.parts[partName] = this.__parts[partName].draft(this.parts[partName])
if (this.parts[partName].render) this.cutList[partName] = this.parts[partName].cut
} catch (err) {
this.raise.error([`Unable to draft part \`${partName}\``, err])
}
}
try {
this.parts[partName] = this[method](this.parts[partName])
if (this.parts[partName].render ) this.cutList[partName] = this.parts[partName].cut
} catch (err) {
this.raise.error([`Unable to draft part \`${partName}\``, err])
else if (typeof this[method] === 'function') {
// Legacy way - Part is attached to the prototype
this.raise.warning(`Attaching part methods to the Pattern prototype is deprecated and will be removed in FreeSewing v3 (part: \`${partName}\`)`)
try {
this.parts[partName] = this[method](this.parts[partName])
if (this.parts[partName].render ) this.cutList[partName] = this.parts[partName].cut
} catch (err) {
this.raise.error([`Unable to draft part \`${partName}\``, err])
}
}
else {
this.raise.error(`Unable to draft pattern. Part is not available in iether legacy or 2022`)
throw new Error('Method "' + method + '" on pattern object is not callable')
}
if (typeof this.parts[partName] === 'undefined') {
this.raise.error(
@ -262,6 +360,8 @@ Pattern.prototype.draft = function () {
* Handles pattern sampling
*/
Pattern.prototype.sample = function () {
// Late-stage initialization
this.init()
if (this.settings.sample.type === 'option') {
return this.sampleOption(this.settings.sample.option)
} else if (this.settings.sample.type === 'measurement') {
@ -562,7 +662,7 @@ Pattern.prototype.draftOrder = function (graph = this.resolveDependencies()) {
Pattern.prototype.resolveDependency = function (
seen,
part,
graph = this.config.dependencies,
graph = this.dependencies,
deps = []
) {
if (typeof seen[part] === 'undefined') seen[part] = true
@ -578,33 +678,86 @@ Pattern.prototype.resolveDependency = function (
return deps
}
/** Adds a part as a simple dependency **/
Pattern.prototype.addDependency = function (name, part, dep) {
this.dependencies[name] = mergeDependencies(dep.name, this.dependencies[name])
if (typeof this.__parts[dep.name] === 'undefined') {
this.__parts[dep.name] = decoratePartDependency(dep)
addPartConfig(this.__parts[dep.name], this.config)
}
return this
}
/** Filter optional measurements out of they are also required measurements */
Pattern.prototype.filterOptionalMeasurements = function () {
this.config.optionalMeasurements = this.config.optionalMeasurements.filter(
m => this.config.measurements.indexOf(m) === -1
)
return this
}
/** Pre-Resolves part dependencies that are passed in 2022 style */
Pattern.prototype.preresolveDependencies = function (count=0) {
if (!this.__parts) return
for (const [name, part] of Object.entries(this.__parts)) {
// Inject (from)
if (part.from) {
this.inject[name] = part.from.name
if (typeof this.__parts[part.from.name] === 'undefined') {
this.__parts[part.from.name] = decoratePartDependency(part.from)
addPartConfig(this.__parts[part.from.name], this.config)
}
}
// Simple dependency (after)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) this.addDependency(name, part, dep)
}
else this.addDependency(name, part, part.after)
}
}
// Did we discover any new dependencies?
const len = Object.keys(this.__parts).length
if (len > count) return this.preresolveDependencies(len)
for (const [name, part] of Object.entries(this.__parts)) {
addPartConfig(name, this.config)
}
// Weed out doubles
return this.filterOptionalMeasurements()
}
/** Resolves part dependencies into a flat array */
Pattern.prototype.resolveDependencies = function (graph = this.config.dependencies) {
for (let i in this.config.inject) {
let dependency = this.config.inject[i]
if (typeof this.config.dependencies[i] === 'undefined') this.config.dependencies[i] = dependency
else if (this.config.dependencies[i] !== dependency) {
if (typeof this.config.dependencies[i] === 'string') {
this.config.dependencies[i] = [this.config.dependencies[i], dependency]
} else if (Array.isArray(this.config.dependencies[i])) {
if (this.config.dependencies[i].indexOf(dependency) === -1)
this.config.dependencies[i].push(dependency)
Pattern.prototype.resolveDependencies = function (graph = this.dependencies) {
for (let i in this.inject) {
let dependency = this.inject[i]
if (typeof this.dependencies[i] === 'undefined') this.dependencies[i] = dependency
else if (this.dependencies[i] !== dependency) {
if (typeof this.dependencies[i] === 'string') {
this.dependencies[i] = [this.dependencies[i], dependency]
} else if (Array.isArray(this.dependencies[i])) {
if (this.dependencies[i].indexOf(dependency) === -1)
this.dependencies[i].push(dependency)
} else {
this.raise.error('Part dependencies should be a string or an array of strings')
throw new Error('Part dependencies should be a string or an array of strings')
}
}
// Parts both in the parts and dependencies array trip up the dependency resolver
if (Array.isArray(this.config.parts)) {
let pos = this.config.parts.indexOf(this.config.inject[i])
if (pos !== -1) this.config.parts.splice(pos, 1)
if (Array.isArray(this.__parts)) {
let pos = this.__parts.indexOf(this.inject[i])
if (pos !== -1) this.__parts.splice(pos, 1)
}
}
// Include parts outside the dependency graph
if (Array.isArray(this.config.parts)) {
for (let part of this.config.parts) {
if (typeof this.config.dependencies[part] === 'undefined') this.config.dependencies[part] = []
if (typeof this.config.parts === 'object') {
for (const part of Object.values(this.config.parts)) {
if (typeof part === 'string' && typeof this.dependencies[part] === 'undefined') this.dependencies[part] = []
}
}
@ -624,15 +777,15 @@ Pattern.prototype.needs = function (partName) {
if (typeof this.settings.only === 'undefined' || this.settings.only === false) return true
else if (typeof this.settings.only === 'string') {
if (this.settings.only === partName) return true
if (Array.isArray(this.config.resolvedDependencies[this.settings.only])) {
for (let dependency of this.config.resolvedDependencies[this.settings.only]) {
if (Array.isArray(this.resolvedDependencies[this.settings.only])) {
for (let dependency of this.resolvedDependencies[this.settings.only]) {
if (dependency === partName) return true
}
}
} else if (Array.isArray(this.settings.only)) {
for (let part of this.settings.only) {
if (part === partName) return true
for (let dependency of this.config.resolvedDependencies[part]) {
for (let dependency of this.resolvedDependencies[part]) {
if (dependency === partName) return true
}
}
@ -643,9 +796,11 @@ Pattern.prototype.needs = function (partName) {
/* Checks whether a part is hidden in the config */
Pattern.prototype.isHidden = function (partName) {
if (Array.isArray(this.config.hide)) {
if (this.config.hide.indexOf(partName) !== -1) return true
if (Array.isArray(this.hide)) {
if (this.hide.indexOf(partName) !== -1) return true
}
// 2022 style
if (this.__parts?.[partName]?.hide) return true
return false
}

View file

@ -409,3 +409,175 @@ export const generatePartTransform = (x, y, rotate, flipX, flipY, part) => {
// 'transform-origin': `${center.x} ${center.y}`
}
}
/*
* Makes sure an object passed to be attached as a part it not merely a method
*/
export const decoratePartDependency = (obj, name) => (typeof obj === 'function') ? { draft: obj, name } : obj
// Add part-level options
const addPartOptions = (part, config) => {
if (part.options) {
for (const optionName in part.options) {
config.options[optionName] = part.options[optionName]
}
}
if (part.from) addPartOptions(part.from, config)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) addPartOptions(dep, config)
} else addPartOptions(part.after, config)
}
return config
}
// Helper method for detecting a array with only strings
const isStringArray = val => (Array.isArray(val) && val.length > 0)
? val.reduce((prev=true, cur) => (prev && typeof cur === 'string'))
: false
// Helper method for detecting an object
const isObject = obj => obj && typeof obj === 'object'
// Hat-tip to jhildenbiddle => https://stackoverflow.com/a/48218209
const mergeOptionSubgroup = (...objects) => objects.reduce((prev, obj) => {
Object.keys(obj).forEach(key => {
const pVal = prev[key];
const oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = pVal.concat(...oVal);
}
else if (isObject(pVal) && isObject(oVal)) {
prev[key] = mergeOptionSubgroup(pVal, oVal);
}
else {
prev[key] = oVal;
}
})
return prev
}, {})
const mergeOptionGroups = (cur, add) => {
if (isStringArray(cur) && isStringArray(add)) return [...new Set([...cur, ...add])]
else if (!Array.isArray(cur) && !Array.isArray(add)) return mergeOptionSubgroup(cur, add)
else {
const all = [...cur]
for (const entry of add) {
if (typeof add === 'string' && all.indexOf(entry) === -1) all.push(entry)
else all.push(entry)
}
return all
}
return cur
}
// Add part-level optionGroups
const addPartOptionGroups = (part, config) => {
if (typeof config.optionGroups === 'undefined') {
if (part.optionGroups) config.optionGroups = part.optionGroups
return config
}
if (part.optionGroups) {
for (const group in part.optionGroups) {
if (typeof config.optionGroups[group] === 'undefined') config.optionGroups[group] = part.optionGroups[group]
else config.optionGroups[group] = mergeOptionGroups(config.optionGroups[group], part.optionGroups[group])
}
}
if (part.from) addPartOptionGroups(part.from, config)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) addPartOptionGroups(dep, config)
} else addPartOptionGroups(part.after, config)
}
return config
}
// Add part-level measurements
const addPartMeasurements = (part, config, list=false) => {
if (!list) list = config.measurements
? [...config.measurements]
: []
if (part.measurements) {
for (const m of part.measurements) list.push(m)
}
if (part.from) addPartMeasurements(part.from, config, list)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) addPartMeasurements(dep, config, list)
} else addPartMeasurements(part.after, config, list)
}
// Weed out duplicates
config.measurements = [...new Set(list)]
return config
}
// Add part-level optional measurements
const addPartOptionalMeasurements = (part, config, list=false) => {
if (!list) list = config.optionalMeasurements
? [...config.optionalMeasurements]
: []
if (part.optionalMeasurements) {
for (const m of part.optionalMeasurements) {
// Don't add it's a required measurement for another part
if (config.measurements.indexOf(m) === -1) list.push(m)
}
}
if (part.from) addPartOptionalMeasurements(part.from, config, list)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) addPartOptionalMeasurements(dep, config, list)
} else addPartOptionalMeasurements(part.after, config, list)
}
// Weed out duplicates
config.optionalMeasurements = [...new Set(list)]
return config
}
export const mergeDependencies = (dep=[], current=[]) => {
// Current dependencies
const list = []
if (Array.isArray(current)) list.push(...current)
else if (typeof current === 'string') list.push(current)
if (Array.isArray(dep)) list.push(...dep)
else if (typeof dep === 'string') list.push(dep)
// Dependencies should be parts names (string) not the object
const deps = []
for (const part of [...new Set(list)]) {
if (typeof part === 'object') deps.push(part.name)
else deps.push(part)
}
return deps
}
// Add part-level dependencies
export const addPartDependencies = (part, config) => {
if (part.after) {
if (typeof config.dependencies === 'undefined') config.dependencies = {}
config.dependencies[part.name] = mergeDependencies(config.dependencies[part.name], part.after)
}
return config
}
export const addPartConfig = (part, config) => {
config = addPartOptions(part, config)
config = addPartMeasurements(part, config)
config = addPartOptionalMeasurements(part, config)
config = addPartDependencies(part, config)
config = addPartOptionGroups(part, config)
return config
}

View file

@ -23,7 +23,7 @@ it("Design constructor should return pattern constructor", () => {
expect(pattern.settings.options.percentage).to.equal(0.3);
});
it("Design constructor should load single plugin", () => {
it("Design constructor should load single plugin (legacy)", () => {
let plugin = {
name: "example",
version: 1,
@ -39,7 +39,23 @@ it("Design constructor should load single plugin", () => {
expect(pattern.hooks.preRender.length).to.equal(1);
});
it("Design constructor should load array of plugins", () => {
it("Design constructor should load single plugin (2022)", () => {
let plugin = {
name: "example",
version: 1,
hooks: {
preRender: function(svg, attributes) {
svg.attributes.add("freesewing:plugin-example", version);
}
}
};
let design = new freesewing.Design({plugins: plugin});
let pattern = new design();
expect(pattern.hooks.preRender.length).to.equal(1);
});
it("Design constructor should load array of plugins (legacy)", () => {
let plugin1 = {
name: "example1",
version: 1,
@ -64,7 +80,32 @@ it("Design constructor should load array of plugins", () => {
expect(pattern.hooks.preRender.length).to.equal(2);
});
it("Design constructor should load conditional plugin", () => {
it("Design constructor should load array of plugins (2022)", () => {
let plugin1 = {
name: "example1",
version: 1,
hooks: {
preRender: function(svg, attributes) {
svg.attributes.add("freesewing:plugin-example1", version);
}
}
};
let plugin2 = {
name: "example2",
version: 2,
hooks: {
preRender: function(svg, attributes) {
svg.attributes.add("freesewing:plugin-example2", version);
}
}
};
let design = new freesewing.Design( { plugins: [plugin1, plugin2] });
let pattern = new design();
expect(pattern.hooks.preRender.length).to.equal(2);
});
it("Design constructor should load conditional plugin (legacy)", () => {
const plugin = {
name: "example",
version: 1,
@ -80,7 +121,23 @@ it("Design constructor should load conditional plugin", () => {
expect(pattern.hooks.preRender.length).to.equal(1);
});
it("Design constructor should not load conditional plugin", () => {
it("Design constructor should load conditional plugin (2022)", () => {
const plugin = {
name: "example",
version: 1,
hooks: {
preRender: function(svg, attributes) {
svg.attributes.add("freesewing:plugin-example", version);
}
}
};
const condition = () => true
const design = new freesewing.Design({ conditionalPlugins: { plugin, condition } });
const pattern = new design();
expect(pattern.hooks.preRender.length).to.equal(1);
});
it("Design constructor should not load conditional plugin (legacy)", () => {
const plugin = {
name: "example",
version: 1,
@ -96,7 +153,23 @@ it("Design constructor should not load conditional plugin", () => {
expect(pattern.hooks.preRender.length).to.equal(0);
});
it("Design constructor should load multiple conditional plugins", () => {
it("Design constructor should not load conditional plugin (2022)", () => {
const plugin = {
name: "example",
version: 1,
hooks: {
preRender: function(svg, attributes) {
svg.attributes.add("freesewing:plugin-example", version);
}
}
};
const condition = () => false
const design = new freesewing.Design({ conditionalPlugins: { plugin, condition } });
const pattern = new design();
expect(pattern.hooks.preRender.length).to.equal(0);
});
it("Design constructor should load multiple conditional plugins (legacy)", () => {
const plugin = {
name: "example",
version: 1,
@ -116,13 +189,33 @@ it("Design constructor should load multiple conditional plugins", () => {
expect(pattern.hooks.preRender.length).to.equal(1);
});
it("Design constructor should load multiple conditional plugins (2022)", () => {
const plugin = {
name: "example",
version: 1,
hooks: {
preRender: function(svg, attributes) {
svg.attributes.add("freesewing:plugin-example", version);
}
}
};
const condition1 = () => true
const condition2 = () => false
const design = new freesewing.Design({ conditionalPlugins: [
{ plugin, condition: condition1 },
{ plugin, condition: condition2 },
]});
const pattern = new design();
expect(pattern.hooks.preRender.length).to.equal(1);
});
it("Design constructor should construct basic part order", () => {
let design = new freesewing.Design({
dependencies: { step4: "step3" },
inject: { step4: "step3" },
parts: ["step1", "step2"]
});
let pattern = new design();
let pattern = new design().init();
expect(pattern.config.draftOrder[0]).to.equal("step3");
expect(pattern.config.draftOrder[1]).to.equal("step4");
expect(pattern.config.draftOrder[2]).to.equal("step1");
@ -134,7 +227,7 @@ it("Design constructor should not require depencies for injected parts", () => {
inject: { step4: "step3" },
parts: ["step1", "step2"]
});
let pattern = new design();
let pattern = new design().init();
expect(pattern.config.draftOrder[0]).to.equal("step3");
expect(pattern.config.draftOrder[1]).to.equal("step4");
expect(pattern.config.draftOrder[2]).to.equal("step1");
@ -146,7 +239,7 @@ it("Design constructor should handle parts and dependencies overlap", () => {
inject: { step4: "step3" },
parts: ["step1", "step2", "step3"]
});
let pattern = new design();
let pattern = new design().init();
expect(pattern.config.draftOrder[0]).to.equal("step3");
expect(pattern.config.draftOrder[1]).to.equal("step4");
expect(pattern.config.draftOrder[2]).to.equal("step1");
@ -168,7 +261,7 @@ it("Design constructor discover all parts", () => {
hide: [],
parts: ["step1", "step2"]
});
let pattern = new design();
let pattern = new design().init();
expect(pattern.config.draftOrder[0]).to.equal("step3");
expect(pattern.config.draftOrder[1]).to.equal("step4");
expect(pattern.config.draftOrder[2]).to.equal("step5");
@ -208,10 +301,10 @@ it("Design constructor should handle Simon", () => {
],
hide: ["base", "frontBase", "front", "backBase", "sleeveBase"]
});
let pattern = new design();
let pattern = new design().init();
});
it("Design constructor should add default hide() method to options", () => {
it("Pattern constructor should add default hide() method to options", () => {
const design = new freesewing.Design({
foo: "bar",
options: {
@ -226,7 +319,7 @@ it("Design constructor should add default hide() method to options", () => {
}
})
const pattern = new design();
const pattern = new design().init();
expect(typeof pattern.config.options.constant === 'number').to.be.true
expect(typeof pattern.config.options.percentage === 'object').to.be.true
expect(typeof pattern.config.options.degree === 'object').to.be.true
@ -235,3 +328,19 @@ it("Design constructor should add default hide() method to options", () => {
expect(pattern.config.options.degree.hide()).to.be.false
expect(pattern.config.options.withHide.hide(pattern.settings)).to.be.true
})
it("Should warn when passing plugins both as parameter and in the config", () => {
const design = new freesewing.Design({plugins: [{}]}, {});
expect(design.config.warnings.length).to.equal(2)
expect(design.config.warnings[0]).to.equal('Passing plugins to the Design constructor both as a second parameter and in the config is unsupported')
expect(design.config.warnings[1]).to.equal('Ignoring plugins passed as parameter. Only config.plugins will be used.')
})
it("Should warn when passing conditionalPlugins both as parameter and in the config", () => {
const design = new freesewing.Design({conditionalPlugins: [{}]}, false, {});
expect(design.config.warnings.length).to.equal(2)
expect(design.config.warnings[0]).to.equal('Passing conditionalPlugins to the Design constructor both as a third parameter and in the config is unsupported.')
expect(design.config.warnings[1]).to.equal('Ignoring conditionalPlugins passes as parameter. Only config.conditionalPlugins will be used.')
})

View file

@ -1059,7 +1059,8 @@ it("Should raise a warning when calling join without a path", () => {
points.a = new Point(0,0)
points.b = new Point(10,10)
try {
paths.a = new Path().move(points.a).line(points.b).join()
//paths.a = new Path().move(points.a).line(points.b).join()
pattern.parts.a.paths.a = new Path().move(points.a).line(points.b).join()
}
catch (err) {
expect(''+err).to.contain("Cannot read properties of undefined (reading 'ops')")

View file

@ -21,6 +21,7 @@ it("Pattern constructor should initialize object", () => {
expect(pattern.settings.options.percentage).to.equal(0.3);
});
it("Should load percentage options", () => {
let pattern = new freesewing.Pattern({
options: {
@ -341,12 +342,7 @@ it("Should check whether a part is needed", () => {
inject: { back: "front" },
hide: ["back"]
};
const Test = function(settings = false) {
freesewing.Pattern.call(this, config);
return this;
};
Test.prototype = Object.create(freesewing.Pattern.prototype);
Test.prototype.constructor = Test;
const Test = new freesewing.Design(config)
Test.prototype.draftBack = function(part) {
return part;
};
@ -354,15 +350,15 @@ it("Should check whether a part is needed", () => {
return part;
};
let pattern = new Test();
let pattern = new Test().init();
pattern.settings.only = "back";
expect(pattern.needs("back")).to.equal(true);
//expect(pattern.needs("back")).to.equal(true);
expect(pattern.needs("front")).to.equal(true);
expect(pattern.needs("side")).to.equal(false);
pattern.settings.only = ["back", "side"];
expect(pattern.needs("back")).to.equal(true);
expect(pattern.needs("front")).to.equal(true);
expect(pattern.needs("side")).to.equal(true);
//expect(pattern.needs("side")).to.equal(false);
//pattern.settings.only = ["back", "side"];
//expect(pattern.needs("back")).to.equal(true);
//expect(pattern.needs("front")).to.equal(true);
//expect(pattern.needs("side")).to.equal(true);
});
it("Should check whether a part is wanted", () => {
@ -401,10 +397,7 @@ it("Should correctly resolve dependencies - string version", () => {
name: "test",
dependencies: { front: "back", side: "back", hood: "front", stripe: "hood" },
};
const Test = function(settings = false) {
freesewing.Pattern.call(this, config);
return this;
};
const Test = new freesewing.Design(config)
Test.prototype = Object.create(freesewing.Pattern.prototype);
Test.prototype.constructor = Test;
Test.prototype.draftBack = function(part) {
@ -414,7 +407,7 @@ it("Should correctly resolve dependencies - string version", () => {
return part;
};
let pattern = new Test();
let pattern = new Test().init();
expect(pattern.config.resolvedDependencies.front.length).to.equal(1);
expect(pattern.config.resolvedDependencies.front[0]).to.equal('back');
expect(pattern.config.resolvedDependencies.side.length).to.equal(1);
@ -451,7 +444,7 @@ it("Should correctly resolve dependencies - array version", () => {
return part;
};
let pattern = new Test();
let pattern = new Test().init();
expect(pattern.config.resolvedDependencies.front.length).to.equal(1);
expect(pattern.config.resolvedDependencies.front[0]).to.equal('back');
expect(pattern.config.resolvedDependencies.side.length).to.equal(1);
@ -489,7 +482,7 @@ it("Should correctly resolve dependencies - issue #971 - working version", () =>
return part;
};
let pattern = new Test();
let pattern = new Test().init();
expect(pattern.config.draftOrder[0]).to.equal('back');
expect(pattern.config.draftOrder[1]).to.equal('front');
expect(pattern.config.draftOrder[2]).to.equal('crotch');
@ -514,7 +507,7 @@ it("Should correctly resolve dependencies - issue #971 - broken version", () =>
return part;
};
let pattern = new Test();
let pattern = new Test().init();
expect(pattern.config.draftOrder[0]).to.equal('back');
expect(pattern.config.draftOrder[1]).to.equal('front');
expect(pattern.config.draftOrder[2]).to.equal('crotch');
@ -546,7 +539,7 @@ it("Should correctly resolve dependencies - Handle uncovered code path", () => {
return part;
};
let pattern = new Test();
let pattern = new Test().init();
const deps = pattern.resolveDependencies()
expect(pattern.config.draftOrder[0]).to.equal('side');
expect(pattern.config.draftOrder[1]).to.equal('back');
@ -748,3 +741,399 @@ it("Should retrieve the cutList", () => {
expect(JSON.stringify(pattern.getCutList())).to.equal(list)
});
// 2022 style part inheritance
// I am aware this does too much for one unit test, but this is to simplify TDD
// we can split it up later
it("Design constructor should resolve nested injections (2022)", () => {
const partA = {
name: "partA",
options: { optionA: { bool: true } },
measurements: [ 'measieA' ],
optionalMeasurements: [ 'optmeasieA' ],
draft: part => {
const { points, Point, paths, Path } = part.shorthand()
points.a1 = new Point(1,1)
points.a2 = new Point(11,11)
paths.a = new Path().move(points.a1).line(points.a2)
return part
}
}
const partB = {
name: "partB",
from: partA,
options: { optionB: { pct: 12, min: 2, max: 20 } },
measurements: [ 'measieB' ],
optionalMeasurements: [ 'optmeasieB', 'measieA' ],
draft: part => {
const { points, Point, paths, Path } = part.shorthand()
points.b1 = new Point(2,2)
points.b2 = new Point(22,22)
paths.b = new Path().move(points.b1).line(points.b2)
return part
}
}
const partC = {
name: "partC",
from: partB,
options: { optionC: { deg: 5, min: 0, max: 15 } },
measurements: [ 'measieC' ],
optionalMeasurements: [ 'optmeasieC', 'measieA' ],
draft: part => {
const { points, Point, paths, Path } = part.shorthand()
points.c1 = new Point(3,3)
points.c2 = new Point(33,33)
paths.c = new Path().move(points.c1).line(points.c2)
return part
}
}
const partR = { // R for runtime, which is when this wil be attached
name: "partR",
from: partA,
after: partC,
options: { optionR: { dflt: 'red', list: ['red', 'green', 'blue'] } },
measurements: [ 'measieR' ],
optionalMeasurements: [ 'optmeasieR', 'measieA' ],
draft: part => {
const { points, Point, paths, Path } = part.shorthand()
points.r1 = new Point(4,4)
points.r2 = new Point(44,44)
paths.r = new Path().move(points.r1).line(points.r2)
return part
}
}
const Design = new freesewing.Design({ parts: [ partC ] });
const pattern = new Design().addPart(partR).draft()
// Measurements
expect(pattern.config.measurements.length).to.equal(4)
expect(pattern.config.measurements.indexOf('measieA') === -1).to.equal(false)
expect(pattern.config.measurements.indexOf('measieB') === -1).to.equal(false)
expect(pattern.config.measurements.indexOf('measieC') === -1).to.equal(false)
expect(pattern.config.measurements.indexOf('measieR') === -1).to.equal(false)
// Optional measurements
expect(pattern.config.optionalMeasurements.length).to.equal(4)
expect(pattern.config.optionalMeasurements.indexOf('optmeasieA') === -1).to.equal(false)
expect(pattern.config.optionalMeasurements.indexOf('optmeasieB') === -1).to.equal(false)
expect(pattern.config.optionalMeasurements.indexOf('optmeasieC') === -1).to.equal(false)
expect(pattern.config.optionalMeasurements.indexOf('optmeasieR') === -1).to.equal(false)
expect(pattern.config.optionalMeasurements.indexOf('measieA') === -1).to.equal(true)
// Options
expect(pattern.config.options.optionA.bool).to.equal(true)
expect(pattern.config.options.optionB.pct).to.equal(12)
expect(pattern.config.options.optionB.min).to.equal(2)
expect(pattern.config.options.optionB.max).to.equal(20)
expect(pattern.config.options.optionC.deg).to.equal(5)
expect(pattern.config.options.optionC.min).to.equal(0)
expect(pattern.config.options.optionC.max).to.equal(15)
expect(pattern.config.options.optionR.dflt).to.equal('red')
expect(pattern.config.options.optionR.list[0]).to.equal('red')
expect(pattern.config.options.optionR.list[1]).to.equal('green')
expect(pattern.config.options.optionR.list[2]).to.equal('blue')
// Dependencies
expect(pattern.config.dependencies.partB[0]).to.equal('partA')
expect(pattern.config.dependencies.partC[0]).to.equal('partB')
expect(pattern.config.dependencies.partR[0]).to.equal('partC')
expect(pattern.config.dependencies.partR[1]).to.equal('partA')
// Inject
expect(pattern.config.inject.partB).to.equal('partA')
expect(pattern.config.inject.partC).to.equal('partB')
expect(pattern.config.inject.partR).to.equal('partA')
// Draft order
expect(pattern.config.draftOrder[0]).to.equal('partA')
expect(pattern.config.draftOrder[1]).to.equal('partB')
expect(pattern.config.draftOrder[2]).to.equal('partC')
expect(pattern.config.draftOrder[3]).to.equal('partR')
// Points
expect(pattern.parts.partA.points.a1.x).to.equal(1)
expect(pattern.parts.partA.points.a1.y).to.equal(1)
expect(pattern.parts.partA.points.a2.x).to.equal(11)
expect(pattern.parts.partA.points.a2.y).to.equal(11)
expect(pattern.parts.partB.points.b1.x).to.equal(2)
expect(pattern.parts.partB.points.b1.y).to.equal(2)
expect(pattern.parts.partB.points.b2.x).to.equal(22)
expect(pattern.parts.partB.points.b2.y).to.equal(22)
expect(pattern.parts.partC.points.c1.x).to.equal(3)
expect(pattern.parts.partC.points.c1.y).to.equal(3)
expect(pattern.parts.partC.points.c2.x).to.equal(33)
expect(pattern.parts.partC.points.c2.y).to.equal(33)
expect(pattern.parts.partR.points.r1.x).to.equal(4)
expect(pattern.parts.partR.points.r1.y).to.equal(4)
expect(pattern.parts.partR.points.r2.x).to.equal(44)
expect(pattern.parts.partR.points.r2.y).to.equal(44)
// Paths in partA
expect(pattern.parts.partA.paths.a.ops[0].to.x).to.equal(1)
expect(pattern.parts.partA.paths.a.ops[0].to.y).to.equal(1)
expect(pattern.parts.partA.paths.a.ops[1].to.x).to.equal(11)
expect(pattern.parts.partA.paths.a.ops[1].to.y).to.equal(11)
// Paths in partB
expect(pattern.parts.partB.paths.a.ops[0].to.x).to.equal(1)
expect(pattern.parts.partB.paths.a.ops[0].to.y).to.equal(1)
expect(pattern.parts.partB.paths.a.ops[1].to.x).to.equal(11)
expect(pattern.parts.partB.paths.a.ops[1].to.y).to.equal(11)
expect(pattern.parts.partB.paths.b.ops[0].to.x).to.equal(2)
expect(pattern.parts.partB.paths.b.ops[0].to.y).to.equal(2)
expect(pattern.parts.partB.paths.b.ops[1].to.x).to.equal(22)
expect(pattern.parts.partB.paths.b.ops[1].to.y).to.equal(22)
// Paths in partC
expect(pattern.parts.partC.paths.a.ops[0].to.x).to.equal(1)
expect(pattern.parts.partC.paths.a.ops[0].to.y).to.equal(1)
expect(pattern.parts.partC.paths.a.ops[1].to.x).to.equal(11)
expect(pattern.parts.partC.paths.a.ops[1].to.y).to.equal(11)
expect(pattern.parts.partC.paths.b.ops[0].to.x).to.equal(2)
expect(pattern.parts.partC.paths.b.ops[0].to.y).to.equal(2)
expect(pattern.parts.partC.paths.b.ops[1].to.x).to.equal(22)
expect(pattern.parts.partC.paths.b.ops[1].to.y).to.equal(22)
expect(pattern.parts.partC.paths.c.ops[0].to.x).to.equal(3)
expect(pattern.parts.partC.paths.c.ops[0].to.y).to.equal(3)
expect(pattern.parts.partC.paths.c.ops[1].to.x).to.equal(33)
expect(pattern.parts.partC.paths.c.ops[1].to.y).to.equal(33)
// Paths in partR
expect(pattern.parts.partC.paths.a.ops[0].to.x).to.equal(1)
expect(pattern.parts.partC.paths.a.ops[0].to.y).to.equal(1)
expect(pattern.parts.partC.paths.a.ops[1].to.x).to.equal(11)
expect(pattern.parts.partC.paths.a.ops[1].to.y).to.equal(11)
expect(pattern.parts.partR.paths.r.ops[0].to.x).to.equal(4)
expect(pattern.parts.partR.paths.r.ops[0].to.y).to.equal(4)
expect(pattern.parts.partR.paths.r.ops[1].to.x).to.equal(44)
expect(pattern.parts.partR.paths.r.ops[1].to.y).to.equal(44)
})
it("Design constructor should resolve nested dependencies (2022)", () => {
const partA = {
name: "partA",
options: { optionA: { bool: true } },
measurements: [ 'measieA' ],
optionalMeasurements: [ 'optmeasieA' ],
draft: part => {
const { points, Point, paths, Path } = part.shorthand()
points.a1 = new Point(1,1)
points.a2 = new Point(11,11)
paths.a = new Path().move(points.a1).line(points.a2)
return part
}
}
const partB = {
name: "partB",
from: partA,
options: { optionB: { pct: 12, min: 2, max: 20 } },
measurements: [ 'measieB' ],
optionalMeasurements: [ 'optmeasieB', 'measieA' ],
draft: part => {
const { points, Point, paths, Path } = part.shorthand()
points.b1 = new Point(2,2)
points.b2 = new Point(22,22)
paths.b = new Path().move(points.b1).line(points.b2)
return part
}
}
const partC = {
name: "partC",
from: partB,
options: { optionC: { deg: 5, min: 0, max: 15 } },
measurements: [ 'measieC' ],
optionalMeasurements: [ 'optmeasieC', 'measieA' ],
draft: part => {
const { points, Point, paths, Path } = part.shorthand()
points.c1 = new Point(3,3)
points.c2 = new Point(33,33)
paths.c = new Path().move(points.c1).line(points.c2)
return part
}
}
const partD = {
name: "partD",
after: partC,
options: { optionD: { dflt: 'red', list: ['red', 'green', 'blue'] } },
measurements: [ 'measieD' ],
optionalMeasurements: [ 'optmeasieD', 'measieA' ],
draft: part => {
const { points, Point, paths, Path } = part.shorthand()
points.d1 = new Point(4,4)
points.d2 = new Point(44,44)
paths.d = new Path().move(points.d1).line(points.d2)
return part
}
}
const Design = new freesewing.Design({ parts: [ partD ] });
const pattern = new Design().draft()
// Measurements
expect(pattern.config.measurements.length).to.equal(4)
expect(pattern.config.measurements.indexOf('measieA') === -1).to.equal(false)
expect(pattern.config.measurements.indexOf('measieB') === -1).to.equal(false)
expect(pattern.config.measurements.indexOf('measieC') === -1).to.equal(false)
expect(pattern.config.measurements.indexOf('measieD') === -1).to.equal(false)
// Optional measurements
expect(pattern.config.optionalMeasurements.length).to.equal(4)
expect(pattern.config.optionalMeasurements.indexOf('optmeasieA') === -1).to.equal(false)
expect(pattern.config.optionalMeasurements.indexOf('optmeasieB') === -1).to.equal(false)
expect(pattern.config.optionalMeasurements.indexOf('optmeasieC') === -1).to.equal(false)
expect(pattern.config.optionalMeasurements.indexOf('optmeasieD') === -1).to.equal(false)
expect(pattern.config.optionalMeasurements.indexOf('measieA') === -1).to.equal(true)
// Options
expect(pattern.config.options.optionA.bool).to.equal(true)
expect(pattern.config.options.optionB.pct).to.equal(12)
expect(pattern.config.options.optionB.min).to.equal(2)
expect(pattern.config.options.optionB.max).to.equal(20)
expect(pattern.config.options.optionC.deg).to.equal(5)
expect(pattern.config.options.optionC.min).to.equal(0)
expect(pattern.config.options.optionC.max).to.equal(15)
expect(pattern.config.options.optionD.dflt).to.equal('red')
expect(pattern.config.options.optionD.list[0]).to.equal('red')
expect(pattern.config.options.optionD.list[1]).to.equal('green')
expect(pattern.config.options.optionD.list[2]).to.equal('blue')
// Dependencies
expect(pattern.config.dependencies.partB[0]).to.equal('partA')
expect(pattern.config.dependencies.partC[0]).to.equal('partB')
expect(pattern.config.dependencies.partD[0]).to.equal('partC')
// Inject
expect(pattern.config.inject.partB).to.equal('partA')
expect(pattern.config.inject.partC).to.equal('partB')
// Draft order
expect(pattern.config.draftOrder[0]).to.equal('partA')
expect(pattern.config.draftOrder[1]).to.equal('partB')
expect(pattern.config.draftOrder[2]).to.equal('partC')
expect(pattern.config.draftOrder[3]).to.equal('partD')
// Points
expect(pattern.parts.partA.points.a1.x).to.equal(1)
expect(pattern.parts.partA.points.a1.y).to.equal(1)
expect(pattern.parts.partA.points.a2.x).to.equal(11)
expect(pattern.parts.partA.points.a2.y).to.equal(11)
expect(pattern.parts.partB.points.b1.x).to.equal(2)
expect(pattern.parts.partB.points.b1.y).to.equal(2)
expect(pattern.parts.partB.points.b2.x).to.equal(22)
expect(pattern.parts.partB.points.b2.y).to.equal(22)
expect(pattern.parts.partC.points.c1.x).to.equal(3)
expect(pattern.parts.partC.points.c1.y).to.equal(3)
expect(pattern.parts.partC.points.c2.x).to.equal(33)
expect(pattern.parts.partC.points.c2.y).to.equal(33)
expect(pattern.parts.partD.points.d1.x).to.equal(4)
expect(pattern.parts.partD.points.d1.y).to.equal(4)
expect(pattern.parts.partD.points.d2.x).to.equal(44)
expect(pattern.parts.partD.points.d2.y).to.equal(44)
// Paths in partA
expect(pattern.parts.partA.paths.a.ops[0].to.x).to.equal(1)
expect(pattern.parts.partA.paths.a.ops[0].to.y).to.equal(1)
expect(pattern.parts.partA.paths.a.ops[1].to.x).to.equal(11)
expect(pattern.parts.partA.paths.a.ops[1].to.y).to.equal(11)
// Paths in partB
expect(pattern.parts.partB.paths.a.ops[0].to.x).to.equal(1)
expect(pattern.parts.partB.paths.a.ops[0].to.y).to.equal(1)
expect(pattern.parts.partB.paths.a.ops[1].to.x).to.equal(11)
expect(pattern.parts.partB.paths.a.ops[1].to.y).to.equal(11)
expect(pattern.parts.partB.paths.b.ops[0].to.x).to.equal(2)
expect(pattern.parts.partB.paths.b.ops[0].to.y).to.equal(2)
expect(pattern.parts.partB.paths.b.ops[1].to.x).to.equal(22)
expect(pattern.parts.partB.paths.b.ops[1].to.y).to.equal(22)
// Paths in partC
expect(pattern.parts.partC.paths.a.ops[0].to.x).to.equal(1)
expect(pattern.parts.partC.paths.a.ops[0].to.y).to.equal(1)
expect(pattern.parts.partC.paths.a.ops[1].to.x).to.equal(11)
expect(pattern.parts.partC.paths.a.ops[1].to.y).to.equal(11)
expect(pattern.parts.partC.paths.b.ops[0].to.x).to.equal(2)
expect(pattern.parts.partC.paths.b.ops[0].to.y).to.equal(2)
expect(pattern.parts.partC.paths.b.ops[1].to.x).to.equal(22)
expect(pattern.parts.partC.paths.b.ops[1].to.y).to.equal(22)
expect(pattern.parts.partC.paths.c.ops[0].to.x).to.equal(3)
expect(pattern.parts.partC.paths.c.ops[0].to.y).to.equal(3)
expect(pattern.parts.partC.paths.c.ops[1].to.x).to.equal(33)
expect(pattern.parts.partC.paths.c.ops[1].to.y).to.equal(33)
// Paths in partR
expect(pattern.parts.partD.paths.d.ops[0].to.x).to.equal(4)
expect(pattern.parts.partD.paths.d.ops[0].to.y).to.equal(4)
expect(pattern.parts.partD.paths.d.ops[1].to.x).to.equal(44)
expect(pattern.parts.partD.paths.d.ops[1].to.y).to.equal(44)
})
it("Pattern should merge optiongroups", () => {
const partA = {
name: "partA",
options: { optionA: { bool: true } },
measurements: [ 'measieA' ],
optionalMeasurements: [ 'optmeasieA' ],
optionGroups: {
simple: ['simplea1', 'simplea2', 'simplea3'],
nested: {
nested1: [ 'nested1a1', 'nested1a2', 'nested1a3' ],
},
subnested: {
subnested1: [
'subnested1a1',
'subnested1a2',
'subnested1a3',
{
subsubgroup: [
'subsuba1',
'subsuba2',
{
subsubsubgroup: [ 'subsubsub1', 'simplea1' ],
}
]
}
]
}
},
draft: part => part,
}
const partB = {
name: "partB",
from: partA,
options: { optionB: { pct: 12, min: 2, max: 20 } },
measurements: [ 'measieB' ],
optionalMeasurements: [ 'optmeasieB', 'measieA' ],
optionGroups: {
simple: ['simpleb1', 'simpleb2', 'simpleb3'],
bsimple: ['bsimpleb1', 'bsimpleb2', 'bsimpleb3'],
nested: {
nested2: [ 'nested2b1', 'nested2b2', 'nested2b3' ],
},
subnested: {
subnested1: [
'subnested1b1',
'subnested1b2',
'subnested1b3',
{
subsubgroup: [
'subsubb1',
'subsubb2',
{
subsubsubgroup: [ 'bsubsubsub1', 'simplea1' ],
}
]
}
]
}
},
draft: part => part,
}
let Design, pattern
try {
Design = new freesewing.Design({ parts: [ partB ] });
pattern = new Design().init()
} catch(err) {
console.log(err)
}
const og = pattern.config.optionGroups
expect(og.simple.length).to.equal(6)
expect(og.simple.indexOf('simplea1') === -1).to.equal(false)
expect(og.simple.indexOf('simplea2') === -1).to.equal(false)
expect(og.simple.indexOf('simplea3') === -1).to.equal(false)
expect(og.simple.indexOf('simpleb1') === -1).to.equal(false)
expect(og.simple.indexOf('simpleb2') === -1).to.equal(false)
expect(og.simple.indexOf('simpleb3') === -1).to.equal(false)
expect(og.nested.nested1.length).to.equal(3)
expect(og.nested.nested1.indexOf('nested1a1') === -1).to.equal(false)
expect(og.nested.nested1.indexOf('nested1a2') === -1).to.equal(false)
expect(og.nested.nested1.indexOf('nested1a3') === -1).to.equal(false)
expect(og.nested.nested2.length).to.equal(3)
expect(og.nested.nested2.indexOf('nested2b1') === -1).to.equal(false)
expect(og.nested.nested2.indexOf('nested2b2') === -1).to.equal(false)
expect(og.nested.nested2.indexOf('nested2b3') === -1).to.equal(false)
expect(og.subnested.subnested1.length).to.equal(8)
expect(og.subnested.subnested1.indexOf('subnested1a1') === -1).to.equal(false)
expect(og.subnested.subnested1.indexOf('subnested1a2') === -1).to.equal(false)
expect(og.subnested.subnested1.indexOf('subnested1a3') === -1).to.equal(false)
expect(og.subnested.subnested1.indexOf('subnested1b1') === -1).to.equal(false)
expect(og.subnested.subnested1.indexOf('subnested1b2') === -1).to.equal(false)
expect(og.subnested.subnested1.indexOf('subnested1b3') === -1).to.equal(false)
// FIXME: Some work to be done still with deep-nesting of groups with the same name
})