From ac9b616b99368b8b42087cc3db3a574382cc43a1 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sun, 7 Aug 2022 17:29:33 +0200 Subject: [PATCH 01/11] wip(core): Support for 2022 style Design constructor This is the first commit to tackle some exploratory work in the context of discussion #2538 that deals with a number of things, such as: - Making it easier to attach parts to designs - Making it easier to attach parts at run-time - Simplify part inheritance into other designs - Find ways to handle dependenices across designs - Find ways to keep the part-specific config with the part In this initial commit, I've update the Design constructor to handle two different ways of calling it: - legacy: new Design(config, plugins, conditionalPlugins) - 2022: new Design(config) I didn't want to call this the `new` way because that doesn't age well, so I went with `legacy` and `2022` and this is how I will refer to them from now on. This is very much a work in progress and while I will create a PR to keep on eye on the tests, I don't expect to merge this as-is. --- packages/core/src/design.js | 101 ++++++++++++++++++----- packages/core/src/pattern.js | 11 ++- packages/core/tests/design.test.js | 121 ++++++++++++++++++++++++++-- packages/core/tests/pattern.test.js | 1 + 4 files changed, 207 insertions(+), 27 deletions(-) diff --git a/packages/core/src/design.js b/packages/core/src/design.js index a48ade57f79..62b4b5dc04b 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -1,29 +1,38 @@ import Pattern from './pattern' -// 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], - } - } - } + + // 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 +48,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/pattern.js b/packages/core/src/pattern.js index 2b16ea2ad31..1c932638234 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -11,7 +11,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 +26,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 +52,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 diff --git a/packages/core/tests/design.test.js b/packages/core/tests/design.test.js index e367c5972ab..fc244c553c6 100644 --- a/packages/core/tests/design.test.js +++ b/packages/core/tests/design.test.js @@ -21,7 +21,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, @@ -37,7 +37,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, @@ -62,7 +78,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, @@ -78,7 +119,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, @@ -94,7 +151,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, @@ -114,6 +187,26 @@ 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" }, @@ -209,7 +302,7 @@ it("Design constructor should handle Simon", () => { let pattern = new design(); }); -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: { @@ -233,3 +326,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/pattern.test.js b/packages/core/tests/pattern.test.js index 0205b061119..a2611e13640 100644 --- a/packages/core/tests/pattern.test.js +++ b/packages/core/tests/pattern.test.js @@ -19,6 +19,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: { From 70bd946bdc82ced1815b645e0ed2da801d7d1d1f Mon Sep 17 00:00:00 2001 From: joostdecock Date: Tue, 9 Aug 2022 20:17:35 +0200 Subject: [PATCH 02/11] wip(core/brian/aaron): Support for 2022 style part inheritance This is very rough around the edges, but it's kinda working, so I'm committing this now. What this enabled is the ability to extend a part by importing only that part and then just saying you want a part `from` the imported one. The imported part comes with all options, it does not currently come with all measurements. This also *follows* dependencies. For example in Brian, we never explicitly add the base and sleevecap parts, they are simply added automatically because other parts are buily *from* them. Best to look at the source code of designs/brian and designs/aaron to understand what's going on and how it is different. --- designs/aaron/build.js | 17 +- designs/aaron/config/index.js | 48 +---- designs/aaron/config/options.js | 28 +++ designs/aaron/src/back.js | 187 ++++++++-------- designs/aaron/src/front.js | 325 ++++++++++++++-------------- designs/aaron/src/index.js | 59 +++-- designs/brian/build.js | 17 +- designs/brian/config/index.js | 87 ++------ designs/brian/config/options.js | 94 ++++++++ designs/brian/src/back.js | 353 +++++++++++++++--------------- designs/brian/src/base.js | 334 ++++++++++++++-------------- designs/brian/src/front.js | 371 ++++++++++++++++---------------- designs/brian/src/index.js | 62 +++--- designs/brian/src/sleeve.js | 176 ++++++++------- designs/brian/src/sleevecap.js | 43 ++-- packages/core/src/design.js | 12 +- packages/core/src/pattern.js | 198 ++++++++++++----- packages/core/src/utils.js | 4 + 18 files changed, 1306 insertions(+), 1109 deletions(-) create mode 100644 designs/aaron/config/options.js create mode 100644 designs/brian/config/options.js diff --git a/designs/aaron/build.js b/designs/aaron/build.js index 7248b245c69..ee51943e8cf 100644 --- a/designs/aaron/build.js +++ b/designs/aaron/build.js @@ -23,17 +23,22 @@ const options = { // Different formats const formats = { - cjs: "dist/index.js", - esm: "dist/index.mjs", + cjs: "js", + esm: "mjs", } // Let esbuild generate different formats let result (async () => { - for (const [format, outfile] of Object.entries(formats)) { + for (const [format, ext] of Object.entries(formats)) { + // Regular build result = await esbuild - .build({ ...options, outfile, format }) - .catch(() => process.exit(1)) + .build({ ...options, format, outfile: `dist/index.${ext}` }) + .catch(() => process.exit(1)) + // Config build + await esbuild + .build({ ...options, format, outfile: `dist/config.${ext}`, entryPoints: ['config/index.js'] }) + .catch(() => process.exit(1)) } if (process.env.VERBOSE) { @@ -41,11 +46,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 79cb08c948f..bde54bc9cd4 100644 --- a/designs/aaron/config/index.js +++ b/designs/aaron/config/index.js @@ -1,8 +1,6 @@ import { version } from '../package.json' -import configHelpers from '@freesewing/config-helpers' -const { pctBasedOn } = configHelpers -export default { +export const info = { version, name: 'aaron', design: 'Joost De Cock', @@ -22,7 +20,9 @@ export default { 'lengthBonus', ], }, - measurements: [ +} + +export const measurements = [ 'biceps', 'chest', 'hpsToWaistBack', @@ -31,43 +31,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/build.js b/designs/brian/build.js index 7248b245c69..ee51943e8cf 100644 --- a/designs/brian/build.js +++ b/designs/brian/build.js @@ -23,17 +23,22 @@ const options = { // Different formats const formats = { - cjs: "dist/index.js", - esm: "dist/index.mjs", + cjs: "js", + esm: "mjs", } // Let esbuild generate different formats let result (async () => { - for (const [format, outfile] of Object.entries(formats)) { + for (const [format, ext] of Object.entries(formats)) { + // Regular build result = await esbuild - .build({ ...options, outfile, format }) - .catch(() => process.exit(1)) + .build({ ...options, format, outfile: `dist/index.${ext}` }) + .catch(() => process.exit(1)) + // Config build + await esbuild + .build({ ...options, format, outfile: `dist/config.${ext}`, entryPoints: ['config/index.js'] }) + .catch(() => process.exit(1)) } if (process.env.VERBOSE) { @@ -41,11 +46,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/brian/config/index.js b/designs/brian/config/index.js index 716831764d8..6d32a01100f 100644 --- a/designs/brian/config/index.js +++ b/designs/brian/config/index.js @@ -1,6 +1,6 @@ import { version } from '../package.json' -export default { +export const info = { version, name: 'brian', design: 'Joost De Cock', @@ -52,74 +52,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 62b4b5dc04b..499f94a7f89 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -5,7 +5,17 @@ import Pattern from './pattern' * So it's sort of a super-constructor */ export default function Design(config, plugins = false, conditionalPlugins = false) { - + // Add part options to config + if (!config.options) config.options = {} + if (config.parts) { + for (const partName in config.parts) { + if (config.parts[partName].options) { + for (const optionName in config.parts[partName].options) { + config.options[optionName] = config.parts[partName].options[optionName] + } + } + } + } // Ensure all options have a hide() method config.options = optionsWithHide(config.options) diff --git a/packages/core/src/pattern.js b/packages/core/src/pattern.js index 1c932638234..2aeef7c3494 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -1,4 +1,4 @@ -import { macroName, sampleStyle, capitalize } from './utils' +import { macroName, sampleStyle, capitalize, decoratePartDependency } from './utils' import Part from './part' import Point from './point' import Path from './path' @@ -77,26 +77,12 @@ export default function Pattern(config = { options: {} }) { 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(this.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) } } @@ -127,6 +113,59 @@ export default function Pattern(config = { options: {} }) { } } +// Converts/adds options +Pattern.prototype.addOptions = function(options={}) { + for (let i in options) { + const option = 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 + } + } + + // Make it chainable + return this +} + +/* + * Defer some things that used to happen in the constructor to + * facilitate late-stage adding of parts + */ +Pattern.prototype.init = function () { + // Resolve all dependencies + this.dependencies = this.config.dependencies + this.inject = this.config.inject + this.hide = this.config.hide + if (Array.isArray(this.config.parts)) { + this.resolvedDependencies = this.resolveDependencies(this.dependencies) + } + else if (typeof this.config.parts === 'object') { + this.__parts = this.config.parts + this.preresolveDependencies() + this.resolvedDependencies = this.resolveDependencies(this.dependencies) + } + this.config.draftOrder = this.draftOrder(this.resolvedDependencies) + + // Make all parts uniform + 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) @@ -191,10 +230,26 @@ Pattern.prototype.runHooks = function (hookName, data = false) { } } +/* + * Allows adding a part at run-time + */ +Pattern.prototype.addPart = function (part, name=false, key) { + if (!part.draft) part = decoratePartDependency(part, givenName) + if (typeof part?.draft === 'function') { + this.__parts[part.name] = part + } + 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 = {} @@ -212,33 +267,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(`Adding parts to the prototype is deprecated and will be removed in FreeSewing v4 (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( @@ -570,7 +641,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 @@ -586,33 +657,52 @@ Pattern.prototype.resolveDependency = function ( return deps } +/** Pre-Resolves part dependencies that are passed in 2022 style */ +Pattern.prototype.preresolveDependencies = function (count=0) { + const len = Object.keys(this.__parts).length + if (!this.__parts) return + for (const [name, part] of Object.entries(this.__parts)) { + 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) + } + } + } + const newlen = Object.keys(this.__parts).length + + return (Object.keys(this.__parts).length > len) + ? this.preresolveDependencies() + : this +} + /** 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.dependencies[part] === 'undefined') this.dependencies[part] = [] } } @@ -632,15 +722,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 } } @@ -651,9 +741,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 edc9ea06ffb..c83fab9bf78 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -384,3 +384,7 @@ export const generatePartTransform = (x, y, rotate, flipX, flipY, part) => { } } +/* + * 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 From a1a3e1c1942a56175e00fc3c1a49b286ad92fe09 Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Wed, 10 Aug 2022 16:24:25 +0200 Subject: [PATCH 03/11] fix(core): Fixes for backwards compatibility --- packages/core/src/design.js | 17 ++++++++++---- packages/core/src/pattern.js | 30 ++++++++++++++++-------- packages/core/tests/design.test.js | 12 +++++----- packages/core/tests/pattern.test.js | 36 +++++++++++------------------ 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/packages/core/src/design.js b/packages/core/src/design.js index 499f94a7f89..843f21d51e0 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -1,5 +1,16 @@ import Pattern from './pattern' +const addOptions = (part, config) => { + if (part.options) { + for (const optionName in part.options) { + config.options[optionName] = part.options[optionName] + } + } + if (part.from) addOptions(part.from, config) + + return config +} + /* * The Design constructor. Returns a Pattern constructor * So it's sort of a super-constructor @@ -9,11 +20,7 @@ export default function Design(config, plugins = false, conditionalPlugins = fal if (!config.options) config.options = {} if (config.parts) { for (const partName in config.parts) { - if (config.parts[partName].options) { - for (const optionName in config.parts[partName].options) { - config.options[optionName] = config.parts[partName].options[optionName] - } - } + config = addOptions(config.parts[partName], config) } } // Ensure all options have a hide() method diff --git a/packages/core/src/pattern.js b/packages/core/src/pattern.js index 2aeef7c3494..ceb3942f153 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -73,13 +73,14 @@ 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 = [] // Convert options - this.addOptions(this.config.options) + 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) @@ -115,8 +116,10 @@ export default function Pattern(config = { options: {} }) { // Converts/adds options Pattern.prototype.addOptions = function(options={}) { - for (let i in 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 @@ -138,28 +141,35 @@ Pattern.prototype.addOptions = function(options={}) { 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 (Array.isArray(this.config.parts)) { - this.resolvedDependencies = this.resolveDependencies(this.dependencies) - } - else if (typeof this.config.parts === 'object') { + if (typeof this.config.parts === 'object') { this.__parts = this.config.parts this.preresolveDependencies() - this.resolvedDependencies = this.resolveDependencies(this.dependencies) } + this.resolvedDependencies = this.resolveDependencies(this.dependencies) + this.config.resolvedDependencies = this.resolvedDependencies this.config.draftOrder = this.draftOrder(this.resolvedDependencies) // Make all parts uniform - for (const [key, value] of Object.entries(this.__parts)) { - this.__parts[key] = decoratePartDependency(value) + if (this.__parts) { + for (const [key, value] of Object.entries(this.__parts)) { + this.__parts[key] = decoratePartDependency(value) + } } return this @@ -299,7 +309,7 @@ Pattern.prototype.draft = function () { } else if (typeof this[method] === 'function') { // Legacy way - Part is attached to the prototype - this.raise.warning(`Adding parts to the prototype is deprecated and will be removed in FreeSewing v4 (part: \`${partName}\`)`) + 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 diff --git a/packages/core/tests/design.test.js b/packages/core/tests/design.test.js index fc244c553c6..c43317c18b5 100644 --- a/packages/core/tests/design.test.js +++ b/packages/core/tests/design.test.js @@ -213,7 +213,7 @@ it("Design constructor should construct basic part order", () => { 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"); @@ -225,7 +225,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"); @@ -237,7 +237,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"); @@ -259,7 +259,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"); @@ -299,7 +299,7 @@ it("Design constructor should handle Simon", () => { ], hide: ["base", "frontBase", "front", "backBase", "sleeveBase"] }); - let pattern = new design(); + let pattern = new design().init(); }); it("Pattern constructor should add default hide() method to options", () => { @@ -317,7 +317,7 @@ it("Pattern 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 diff --git a/packages/core/tests/pattern.test.js b/packages/core/tests/pattern.test.js index a2611e13640..b6738c4c5c8 100644 --- a/packages/core/tests/pattern.test.js +++ b/packages/core/tests/pattern.test.js @@ -340,12 +340,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; }; @@ -353,15 +348,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", () => { @@ -400,10 +395,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) { @@ -413,7 +405,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); @@ -450,7 +442,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); @@ -488,7 +480,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'); @@ -513,7 +505,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'); @@ -545,7 +537,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'); From 6c3208768e767ba9b2eb00657445fa32a770df45 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sat, 13 Aug 2022 14:27:39 +0200 Subject: [PATCH 04/11] wip: Added support for part-level measurments and optionalMeasurements This adds support for part-level measurements and optional measurements --- packages/core/src/design.js | 43 ++++++++++++++- packages/core/tests/pattern.test.js | 86 +++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/packages/core/src/design.js b/packages/core/src/design.js index 843f21d51e0..3e4b2cbc0c1 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -1,5 +1,6 @@ import Pattern from './pattern' +// Add part-level options const addOptions = (part, config) => { if (part.options) { for (const optionName in part.options) { @@ -11,18 +12,58 @@ const addOptions = (part, config) => { return config } +// Add part-level measurements +const addMeasurements = (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) addMeasurements(part.from, config, list) + + // Weed out duplicates + config.measurements = [...new Set(list)] + + return config +} + +// Add part-level optional measurements +const addOptionalMeasurements = (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) addOptionalMeasurements(part.from, config, list) + + // Weed out duplicates + config.optionalMeasurements = [...new Set(list)] + + return config +} + /* * 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 part options to config + // Add part options/measurements/optionalMeasurements to config if (!config.options) config.options = {} + if (!config.measurements) config.measurements = [] + if (!config.optionalMeasurements) config.optionalMeasurements = [] if (config.parts) { for (const partName in config.parts) { config = addOptions(config.parts[partName], config) + config = addMeasurements(config.parts[partName], config) + config = addOptionalMeasurements(config.parts[partName], config) } } + // Ensure all options have a hide() method config.options = optionsWithHide(config.options) diff --git a/packages/core/tests/pattern.test.js b/packages/core/tests/pattern.test.js index b6738c4c5c8..f73ff10ef06 100644 --- a/packages/core/tests/pattern.test.js +++ b/packages/core/tests/pattern.test.js @@ -722,3 +722,89 @@ it("Should retrieve the cutList", () => { expect(JSON.stringify(pattern.getCutList())).to.equal(list) }); +// 2022 style part inheritance +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.a1) + + 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.b1) + + 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.c1) + + return part + } + } + + const design = new freesewing.Design({ parts: { partC } }); + const pattern = new design().draft() + // Measurements + expect(pattern.config.measurements.length).to.equal(3) + 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) + // Optional measurements + expect(pattern.config.optionalMeasurements.length).to.equal(3) + 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('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) + // Dependencies + expect(pattern.config.dependencies.partB[0]).to.equal('partA') + expect(pattern.config.dependencies.partC[0]).to.equal('partB') + // 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[2]).to.equal('partC') +}) From 2eee247676da79582aff5410f34172b6a86ccf83 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sat, 13 Aug 2022 14:38:59 +0200 Subject: [PATCH 05/11] wip: Extended unit test --- packages/core/tests/pattern.test.js | 55 ++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/packages/core/tests/pattern.test.js b/packages/core/tests/pattern.test.js index f73ff10ef06..4bc39e0a404 100644 --- a/packages/core/tests/pattern.test.js +++ b/packages/core/tests/pattern.test.js @@ -733,10 +733,7 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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.a1) - + paths.a = new Path().move(points.a1).line(points.a2) return part } } @@ -750,10 +747,7 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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.b1) - + paths.b = new Path().move(points.b1).line(points.b2) return part } } @@ -767,10 +761,7 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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.c1) - + paths.c = new Path().move(points.c1).line(points.c2) return part } } @@ -807,4 +798,44 @@ it("Design constructor should resolve nested dependencies (2022)", () => { expect(pattern.config.draftOrder[1]).to.equal('partB') expect(pattern.config.draftOrder[2]).to.equal('partC') expect(pattern.config.draftOrder[2]).to.equal('partC') + // 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) + // 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) }) From 689f908f68778f57d09ced1691637b021f7bd179 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sat, 13 Aug 2022 15:11:33 +0200 Subject: [PATCH 06/11] wip: Allow runtime adding of parts Just call `pattern.addPart()` and pass it either: - a part object with all it entails - a draft method as first and name as second parameter This will overwrite any existing parts without any warning --- packages/core/src/design.js | 48 +--------------------------- packages/core/src/pattern.js | 23 +++++++++++--- packages/core/src/utils.js | 49 +++++++++++++++++++++++++++++ packages/core/tests/pattern.test.js | 47 ++++++++++++++++++++++++--- 4 files changed, 111 insertions(+), 56 deletions(-) diff --git a/packages/core/src/design.js b/packages/core/src/design.js index 3e4b2cbc0c1..2ab824167c5 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -1,51 +1,5 @@ import Pattern from './pattern' - -// Add part-level options -const addOptions = (part, config) => { - if (part.options) { - for (const optionName in part.options) { - config.options[optionName] = part.options[optionName] - } - } - if (part.from) addOptions(part.from, config) - - return config -} - -// Add part-level measurements -const addMeasurements = (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) addMeasurements(part.from, config, list) - - // Weed out duplicates - config.measurements = [...new Set(list)] - - return config -} - -// Add part-level optional measurements -const addOptionalMeasurements = (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) addOptionalMeasurements(part.from, config, list) - - // Weed out duplicates - config.optionalMeasurements = [...new Set(list)] - - return config -} +import { addOptions, addMeasurements, addOptionalMeasurements } from './utils.js' /* * The Design constructor. Returns a Pattern constructor diff --git a/packages/core/src/pattern.js b/packages/core/src/pattern.js index ceb3942f153..5859939c330 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -1,4 +1,12 @@ -import { macroName, sampleStyle, capitalize, decoratePartDependency } from './utils' +import { + macroName, + sampleStyle, + capitalize, + decoratePartDependency, + addOptions, + addMeasurements, + addOptionalMeasurements +} from './utils.js' import Part from './part' import Point from './point' import Path from './path' @@ -243,10 +251,17 @@ Pattern.prototype.runHooks = function (hookName, data = false) { /* * Allows adding a part at run-time */ -Pattern.prototype.addPart = function (part, name=false, key) { - if (!part.draft) part = decoratePartDependency(part, givenName) +Pattern.prototype.addPart = function (part, name=false) { + if (!part.draft) part = decoratePartDependency(part, name) if (typeof part?.draft === 'function') { - this.__parts[part.name] = part + if (part.name) { + this.config.parts[part.name] = part + // Add part options/measurements/optionalMeasurements to config + this.config = addOptions(part, this.config) + this.config = addMeasurements(part, this.config) + this.config = addOptionalMeasurements(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`) diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index c83fab9bf78..a2d1e347978 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -388,3 +388,52 @@ export const generatePartTransform = (x, y, rotate, flipX, flipY, part) => { * 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 +export const addOptions = (part, config) => { + if (part.options) { + for (const optionName in part.options) { + config.options[optionName] = part.options[optionName] + } + } + if (part.from) addOptions(part.from, config) + + return config +} + +// Add part-level measurements +export const addMeasurements = (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) addMeasurements(part.from, config, list) + + // Weed out duplicates + config.measurements = [...new Set(list)] + + return config +} + +// Add part-level optional measurements +export const addOptionalMeasurements = (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) addOptionalMeasurements(part.from, config, list) + + // Weed out duplicates + config.optionalMeasurements = [...new Set(list)] + + return config +} + + diff --git a/packages/core/tests/pattern.test.js b/packages/core/tests/pattern.test.js index 4bc39e0a404..539d39af50f 100644 --- a/packages/core/tests/pattern.test.js +++ b/packages/core/tests/pattern.test.js @@ -723,6 +723,8 @@ it("Should retrieve the cutList", () => { }); // 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 dependencies (2022)", () => { const partA = { name: "partA", @@ -765,19 +767,35 @@ it("Design constructor should resolve nested dependencies (2022)", () => { return part } } + const partR = { // R for runtime, which is when this wil be attached + name: "partR", + from: partA, + 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().draft() + const Design = new freesewing.Design({ parts: { partC } }); + const pattern = new Design().addPart(partR).draft() // Measurements - expect(pattern.config.measurements.length).to.equal(3) + 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(3) + 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) @@ -787,17 +805,23 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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('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[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) @@ -811,6 +835,10 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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) @@ -838,4 +866,13 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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) }) From 4cf9c3bd471fb842a3601804a4772b4c17248e6a Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sat, 13 Aug 2022 18:33:06 +0200 Subject: [PATCH 07/11] wip: Added part-level dependencies Restructured code a bit to handle all part-level config in one call. Removed check in shorthand for debug as it's no longer used. Updated tests to not fall over on different error message format in newer NodeJS versions --- packages/core/src/design.js | 8 +- packages/core/src/part.js | 286 +++++++++++++++---------------- packages/core/src/pattern.js | 10 +- packages/core/src/utils.js | 43 ++++- packages/core/tests/path.test.js | 15 +- 5 files changed, 187 insertions(+), 175 deletions(-) diff --git a/packages/core/src/design.js b/packages/core/src/design.js index 2ab824167c5..1e5cf181f1b 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -1,5 +1,5 @@ import Pattern from './pattern' -import { addOptions, addMeasurements, addOptionalMeasurements } from './utils.js' +import { addPartConfig } from './utils.js' /* * The Design constructor. Returns a Pattern constructor @@ -11,11 +11,7 @@ export default function Design(config, plugins = false, conditionalPlugins = fal if (!config.measurements) config.measurements = [] if (!config.optionalMeasurements) config.optionalMeasurements = [] if (config.parts) { - for (const partName in config.parts) { - config = addOptions(config.parts[partName], config) - config = addMeasurements(config.parts[partName], config) - config = addOptionalMeasurements(config.parts[partName], config) - } + for (const partName in config.parts) config = addPartConfig(config.parts[partName], config) } // Ensure all options have a hide() method diff --git a/packages/core/src/part.js b/packages/core/src/part.js index 8b3dc8e2dac..06130c64ed1 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 5859939c330..47115d07fc6 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -3,9 +3,7 @@ import { sampleStyle, capitalize, decoratePartDependency, - addOptions, - addMeasurements, - addOptionalMeasurements + addPartConfig, } from './utils.js' import Part from './part' import Point from './point' @@ -256,10 +254,8 @@ Pattern.prototype.addPart = function (part, name=false) { if (typeof part?.draft === 'function') { if (part.name) { this.config.parts[part.name] = part - // Add part options/measurements/optionalMeasurements to config - this.config = addOptions(part, this.config) - this.config = addMeasurements(part, this.config) - this.config = addOptionalMeasurements(part, this.config) + // Add part-level config to config + this.config = addPartConfig(part, this.config) } else this.raise.error(`Part must have a name`) } diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index a2d1e347978..f3d9a1a2f63 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -390,26 +390,26 @@ export const generatePartTransform = (x, y, rotate, flipX, flipY, part) => { export const decoratePartDependency = (obj, name) => (typeof obj === 'function') ? { draft: obj, name } : obj // Add part-level options -export const addOptions = (part, config) => { +const addPartOptions = (part, config) => { if (part.options) { for (const optionName in part.options) { config.options[optionName] = part.options[optionName] } } - if (part.from) addOptions(part.from, config) + if (part.from) addPartOptions(part.from, config) return config } // Add part-level measurements -export const addMeasurements = (part, config, list=false) => { +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) addMeasurements(part.from, config, list) + if (part.from) addPartMeasurements(part.from, config, list) // Weed out duplicates config.measurements = [...new Set(list)] @@ -418,7 +418,7 @@ export const addMeasurements = (part, config, list=false) => { } // Add part-level optional measurements -export const addOptionalMeasurements = (part, config, list=false) => { +const addPartOptionalMeasurements = (part, config, list=false) => { if (!list) list = config.optionalMeasurements ? [...config.optionalMeasurements] : [] @@ -428,7 +428,7 @@ export const addOptionalMeasurements = (part, config, list=false) => { if (config.measurements.indexOf(m) === -1) list.push(m) } } - if (part.from) addOptionalMeasurements(part.from, config, list) + if (part.from) addPartOptionalMeasurements(part.from, config, list) // Weed out duplicates config.optionalMeasurements = [...new Set(list)] @@ -437,3 +437,34 @@ export const addOptionalMeasurements = (part, config, list=false) => { } +const addDependencies = (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) + + return [...new Set(list)] +} + +// Add part-level dependencies +export const addPartDependencies = (part, config) => { + if (part.after) { + config.dependencies[part.name] = addDependencies(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) + + return config +} + + diff --git a/packages/core/tests/path.test.js b/packages/core/tests/path.test.js index feeff9c2ce0..1ba4510b19e 100644 --- a/packages/core/tests/path.test.js +++ b/packages/core/tests/path.test.js @@ -1017,7 +1017,7 @@ it("Should raise a warning when an insop operation used an falsy ID", () => { new freesewing.Path().withRaise(raise).noop('test').insop('test') } catch (err) { - expect(''+err).to.contain("Cannot read property 'ops") + expect(''+err).to.contain("Cannot read prop") } expect(invalid).to.equal(true); }); @@ -1057,10 +1057,11 @@ 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 property 'ops") + expect(''+err).to.contain("Cannot read prop") } expect(pattern.events.error.length).to.equal(1) expect(pattern.events.error[0]).to.equal("Called `Path.join(that)` but `that` is not a `Path` object") @@ -1074,7 +1075,7 @@ it("Should raise a warning when calling start on a path without drawing operatio new freesewing.Path().withRaise(raise).start() } catch (err) { - expect(''+err).to.contain("TypeError: Cannot read property") + expect(''+err).to.contain("TypeError: Cannot read prop") } expect(invalid).to.equal(true); }); @@ -1087,7 +1088,7 @@ it("Should raise a warning when calling end on a path without drawing operations new freesewing.Path().withRaise(raise).end() } catch (err) { - expect(''+err).to.contain("TypeError: Cannot read property") + expect(''+err).to.contain("TypeError: Cannot read prop") } expect(invalid).to.equal(true); }); @@ -1140,7 +1141,7 @@ it("Should raise a warning when splitting a path on a non-point", () => { path.split() } catch (err) { - expect(''+err).to.contain("TypeError: Cannot read property") + expect(''+err).to.contain("TypeError: Cannot read prop") } expect(invalid).to.equal(true); }); @@ -1164,7 +1165,7 @@ it("Should raise a warning when splitting a path on a non-point", () => { path.split() } catch (err) { - expect(''+err).to.contain("TypeError: Cannot read property") + expect(''+err).to.contain("TypeError: Cannot read prop") } expect(invalid).to.equal(true); }); From 2b254c721de939495e0015be4ff675a9b4430b38 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sun, 14 Aug 2022 16:59:51 +0200 Subject: [PATCH 08/11] wip: Recursive resolving of (non-injected) dependencies We started out with following dependencies that are injected (from) and now added dependencies that are merely required to be drafted first (after). This also adds further support for part-level configuration. --- packages/core/src/pattern.js | 47 +++++++-- packages/core/src/utils.js | 15 ++- packages/core/tests/pattern.test.js | 154 +++++++++++++++++++++++++++- 3 files changed, 203 insertions(+), 13 deletions(-) diff --git a/packages/core/src/pattern.js b/packages/core/src/pattern.js index 47115d07fc6..498ca166f16 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -4,6 +4,7 @@ import { capitalize, decoratePartDependency, addPartConfig, + mergeDependencies, } from './utils.js' import Part from './part' import Point from './point' @@ -215,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)) { @@ -678,23 +679,57 @@ 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) { - const len = Object.keys(this.__parts).length 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) + } } - const newlen = Object.keys(this.__parts).length + // Did we discover any new dependencies? + const len = Object.keys(this.__parts).length - return (Object.keys(this.__parts).length > len) - ? this.preresolveDependencies() - : this + 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 */ diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index f3d9a1a2f63..35fcff2397d 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -437,7 +437,7 @@ const addPartOptionalMeasurements = (part, config, list=false) => { } -const addDependencies = (dep, current) => { +export const mergeDependencies = (dep=[], current=[]) => { // Current dependencies const list = [] if (Array.isArray(current)) list.push(...current) @@ -446,13 +446,21 @@ const addDependencies = (dep, current) => { if (Array.isArray(dep)) list.push(...dep) else if (typeof dep === 'string') list.push(dep) - return [...new Set(list)] + // 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) { - config.dependencies[part.name] = addDependencies(config.dependencies[part.name], part.after) + if (typeof config.dependencies === 'undefined') config.dependencies = {} + config.dependencies[part.name] = mergeDependencies(config.dependencies[part.name], part.after) } return config @@ -467,4 +475,3 @@ export const addPartConfig = (part, config) => { return config } - diff --git a/packages/core/tests/pattern.test.js b/packages/core/tests/pattern.test.js index 539d39af50f..91f05df2a3c 100644 --- a/packages/core/tests/pattern.test.js +++ b/packages/core/tests/pattern.test.js @@ -1,6 +1,6 @@ let expect = require("chai").expect; let freesewing = require("../dist/index.js"); - +/* it("Pattern constructor should initialize object", () => { let pattern = new freesewing.Pattern({ foo: "bar", @@ -725,7 +725,7 @@ it("Should retrieve the cutList", () => { // 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 dependencies (2022)", () => { +it("Design constructor should resolve nested injections (2022)", () => { const partA = { name: "partA", options: { optionA: { bool: true } }, @@ -770,6 +770,7 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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' ], @@ -812,7 +813,8 @@ it("Design constructor should resolve nested dependencies (2022)", () => { // 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('partA') + 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') @@ -876,3 +878,149 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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) +}) From 7f684d8c17a6d48c738a7fe17e76c05499fcc66c Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sun, 14 Aug 2022 18:16:15 +0200 Subject: [PATCH 09/11] wip: Pass parts to design constructor as array Since the part name needs to be set in the part, this makes it more clear as there's no key associated with the part. --- packages/core/src/design.js | 9 +++++++-- packages/core/src/pattern.js | 2 +- packages/core/tests/pattern.test.js | 7 +++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/core/src/design.js b/packages/core/src/design.js index 1e5cf181f1b..548dbc48c7e 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -10,8 +10,13 @@ export default function Design(config, plugins = false, conditionalPlugins = fal if (!config.options) config.options = {} if (!config.measurements) config.measurements = [] if (!config.optionalMeasurements) config.optionalMeasurements = [] - if (config.parts) { - for (const partName in config.parts) config = addPartConfig(config.parts[partName], config) + if (Array.isArray(config.parts)) { + const parts = {} + for (const part of config.parts) { + parts[part.name] = part + config = addPartConfig(parts[part.name], config) + } + config.parts = parts } // Ensure all options have a hide() method diff --git a/packages/core/src/pattern.js b/packages/core/src/pattern.js index 498ca166f16..2c559278cf3 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -758,7 +758,7 @@ Pattern.prototype.resolveDependencies = function (graph = this.dependencies) { // Include parts outside the dependency graph if (Array.isArray(this.config.parts)) { for (let part of this.config.parts) { - if (typeof this.dependencies[part] === 'undefined') this.dependencies[part] = [] + if (typeof part === 'string' && typeof this.dependencies[part] === 'undefined') this.dependencies[part] = [] } } diff --git a/packages/core/tests/pattern.test.js b/packages/core/tests/pattern.test.js index 91f05df2a3c..57c8f4e383b 100644 --- a/packages/core/tests/pattern.test.js +++ b/packages/core/tests/pattern.test.js @@ -1,6 +1,5 @@ let expect = require("chai").expect; let freesewing = require("../dist/index.js"); -/* it("Pattern constructor should initialize object", () => { let pattern = new freesewing.Pattern({ foo: "bar", @@ -783,7 +782,7 @@ it("Design constructor should resolve nested injections (2022)", () => { } } - const Design = new freesewing.Design({ parts: { partC } }); + const Design = new freesewing.Design({ parts: [ partC ] }); const pattern = new Design().addPart(partR).draft() // Measurements expect(pattern.config.measurements.length).to.equal(4) @@ -878,7 +877,7 @@ it("Design constructor should resolve nested injections (2022)", () => { 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", @@ -935,7 +934,7 @@ it("Design constructor should resolve nested dependencies (2022)", () => { return part } } - const Design = new freesewing.Design({ parts: { partD } }); + const Design = new freesewing.Design({ parts: [ partD ] }); const pattern = new Design().draft() // Measurements expect(pattern.config.measurements.length).to.equal(4) From 681a1fc6577dd38c998b1989450f304d6fb9895c Mon Sep 17 00:00:00 2001 From: joostdecock Date: Mon, 15 Aug 2022 10:48:35 +0200 Subject: [PATCH 10/11] wip: Handle backwards compatibility for passing parts as array This was broken by some earlier changes, but not it's ok again --- packages/core/src/design.js | 8 ++++++-- packages/core/src/pattern.js | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/core/src/design.js b/packages/core/src/design.js index 548dbc48c7e..2e1504cd072 100644 --- a/packages/core/src/design.js +++ b/packages/core/src/design.js @@ -13,8 +13,12 @@ export default function Design(config, plugins = false, conditionalPlugins = fal if (Array.isArray(config.parts)) { const parts = {} for (const part of config.parts) { - parts[part.name] = part - config = addPartConfig(parts[part.name], config) + 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 } diff --git a/packages/core/src/pattern.js b/packages/core/src/pattern.js index 2c559278cf3..7d3066f33bd 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -360,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') { @@ -756,8 +758,8 @@ Pattern.prototype.resolveDependencies = function (graph = this.dependencies) { } // Include parts outside the dependency graph - if (Array.isArray(this.config.parts)) { - for (let part of this.config.parts) { + 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] = [] } } From 62fae66d2501020812e13881e368534ffdc60835 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Mon, 15 Aug 2022 16:01:42 +0200 Subject: [PATCH 11/11] wip(core): Added support for (part-level) optionGroups This needs some work when merging deeply nested options, but it's a start --- packages/core/build.js | 4 +- packages/core/src/utils.js | 80 ++++++++++++++++++++++++ packages/core/tests/pattern.test.js | 96 ++++++++++++++++++++++++++++- 3 files changed, 177 insertions(+), 3 deletions(-) diff --git a/packages/core/build.js b/packages/core/build.js index 7248b245c69..b18711312e6 100644 --- a/packages/core/build.js +++ b/packages/core/build.js @@ -48,8 +48,8 @@ let result ...options, minify: false, sourcemap: false, - outfile: 'tests/dist/index.mjs', - format: 'esm', + outfile: 'tests/dist/index.js', + format: 'cjs', external: [], }) .catch(() => process.exit(1)) diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index 35fcff2397d..5283ac147c8 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -397,6 +397,75 @@ const addPartOptions = (part, config) => { } } 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 } @@ -410,6 +479,11 @@ const addPartMeasurements = (part, config, list=false) => { 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)] @@ -429,6 +503,11 @@ const addPartOptionalMeasurements = (part, config, list=false) => { } } 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)] @@ -471,6 +550,7 @@ export const addPartConfig = (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/pattern.test.js b/packages/core/tests/pattern.test.js index 57c8f4e383b..a3068ee8bb3 100644 --- a/packages/core/tests/pattern.test.js +++ b/packages/core/tests/pattern.test.js @@ -1,5 +1,5 @@ let expect = require("chai").expect; -let freesewing = require("../dist/index.js"); +let freesewing = require("./dist/index.js"); it("Pattern constructor should initialize object", () => { let pattern = new freesewing.Pattern({ foo: "bar", @@ -1023,3 +1023,97 @@ it("Design constructor should resolve nested dependencies (2022)", () => { 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 +}) +