diff --git a/designs/aaron/build.mjs b/designs/aaron/build.mjs index 9d1da1994da..370f8062e19 100644 --- a/designs/aaron/build.mjs +++ b/designs/aaron/build.mjs @@ -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', diff --git a/designs/aaron/config/index.js b/designs/aaron/config/index.js index 023dfd869b9..5538cba1ead 100644 --- a/designs/aaron/config/index.js +++ b/designs/aaron/config/index.js @@ -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 }, - }, -} diff --git a/designs/aaron/config/options.js b/designs/aaron/config/options.js new file mode 100644 index 00000000000..89009658c61 --- /dev/null +++ b/designs/aaron/config/options.js @@ -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 } diff --git a/designs/aaron/src/back.js b/designs/aaron/src/back.js index e487aff46b9..1837205fa2d 100644 --- a/designs/aaron/src/back.js +++ b/designs/aaron/src/back.js @@ -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 } diff --git a/designs/aaron/src/front.js b/designs/aaron/src/front.js index a23c06238c0..d4ad7b678f1 100644 --- a/designs/aaron/src/front.js +++ b/designs/aaron/src/front.js @@ -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 + } } diff --git a/designs/aaron/src/index.js b/designs/aaron/src/index.js index ed1023c4cde..856f6454e01 100644 --- a/designs/aaron/src/index.js +++ b/designs/aaron/src/index.js @@ -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 diff --git a/designs/brian/config/index.js b/designs/brian/config/index.js index 9da7320b456..f71b8902fb1 100644 --- a/designs/brian/config/index.js +++ b/designs/brian/config/index.js @@ -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'] + diff --git a/designs/brian/config/options.js b/designs/brian/config/options.js new file mode 100644 index 00000000000..88cb0ffe1db --- /dev/null +++ b/designs/brian/config/options.js @@ -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 } + + diff --git a/designs/brian/src/back.js b/designs/brian/src/back.js index a640aca822e..ce869337ab3 100644 --- a/designs/brian/src/back.js +++ b/designs/brian/src/back.js @@ -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 + } } diff --git a/designs/brian/src/base.js b/designs/brian/src/base.js index 69286767c23..3e3eb627834 100644 --- a/designs/brian/src/base.js +++ b/designs/brian/src/base.js @@ -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 } diff --git a/designs/brian/src/front.js b/designs/brian/src/front.js index 1d6a47681a2..e3697bd5c82 100644 --- a/designs/brian/src/front.js +++ b/designs/brian/src/front.js @@ -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 + } } diff --git a/designs/brian/src/index.js b/designs/brian/src/index.js index b2168915d57..b3517cf719e 100644 --- a/designs/brian/src/index.js +++ b/designs/brian/src/index.js @@ -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 diff --git a/designs/brian/src/sleeve.js b/designs/brian/src/sleeve.js index 6166647f801..84236dfae39 100644 --- a/designs/brian/src/sleeve.js +++ b/designs/brian/src/sleeve.js @@ -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 } diff --git a/designs/brian/src/sleevecap.js b/designs/brian/src/sleevecap.js index 8fbe87bf3b6..95a5d5e6ff2 100644 --- a/designs/brian/src/sleevecap.js +++ b/designs/brian/src/sleevecap.js @@ -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 + } } diff --git a/packages/core/src/design.js b/packages/core/src/design.js index a48ade57f79..2e1504cd072 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -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 +} + diff --git a/packages/core/src/part.js b/packages/core/src/part.js index 39773658a68..463bb510f21 100644 --- a/packages/core/src/part.js +++ b/packages/core/src/part.js @@ -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 } diff --git a/packages/core/src/pattern.js b/packages/core/src/pattern.js index 49bf5a3d25a..aa5d8827592 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -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 } diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index d263835ef8e..6b3687d4969 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -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 +} + diff --git a/packages/core/tests/design.test.mjs b/packages/core/tests/design.test.mjs index e54761acee4..b497d48c88e 100644 --- a/packages/core/tests/design.test.mjs +++ b/packages/core/tests/design.test.mjs @@ -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.') +}) + + diff --git a/packages/core/tests/path.test.mjs b/packages/core/tests/path.test.mjs index 3cbd2cbefcb..cb43bc103b1 100644 --- a/packages/core/tests/path.test.mjs +++ b/packages/core/tests/path.test.mjs @@ -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')") diff --git a/packages/core/tests/pattern.test.mjs b/packages/core/tests/pattern.test.mjs index 0655fc77b2d..54e243fa4cd 100644 --- a/packages/core/tests/pattern.test.mjs +++ b/packages/core/tests/pattern.test.mjs @@ -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 +}) +