diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 9d3a23c0598..ef4c7b0da1a 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -16,4 +16,11 @@ export default { utils, patterns: {}, plugins: {}, + /** + * This pctBasedOn() method makes it easy to add the optional + * toAbs() and fromAbs() methods to percentage options config + * + * It was backported from the v3 roadmap into v2. + **/ + pctBasedOn: utils.pctBasedOn, } diff --git a/packages/core/src/part.js b/packages/core/src/part.js index 77ad8f82267..027cfaa4a48 100644 --- a/packages/core/src/part.js +++ b/packages/core/src/part.js @@ -314,6 +314,16 @@ Part.prototype.shorthand = function () { 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 || {}, optionsProxy) } else { shorthand.Point = Point shorthand.Path = Path @@ -323,6 +333,7 @@ Part.prototype.shorthand = function () { shorthand.snippets = this.snippets || {} shorthand.measurements = this.context.settings.measurements || {} shorthand.options = this.context.settings.options || {} + shorthand.absoluteOptions = this.context.settings.absoluteOptions || {} } return shorthand diff --git a/packages/core/src/pattern.js b/packages/core/src/pattern.js index 531a2e60001..a287833b947 100644 --- a/packages/core/src/pattern.js +++ b/packages/core/src/pattern.js @@ -61,6 +61,7 @@ export default function Pattern(config = { options: {} }) { layout: true, debug: true, options: {}, + absoluteOptions: {}, } if (typeof this.config.dependencies === 'undefined') this.config.dependencies = {} @@ -116,6 +117,37 @@ export default function Pattern(config = { options: {} }) { } } +function snappedOption(option, pattern) { + const conf = pattern.config.options[option] + const abs = conf.toAbs(pattern.settings.options[option], pattern.settings) + // Handle units-specific config + if ( + !Array.isArray(conf.snap) && + conf.snap.metric && + conf.snap.imperial + ) conf.snap = conf.snap[pattern.settings.units] + // Simple steps + if (typeof conf.snap === 'number') return Math.ceil(abs / conf.snap) * conf.snap + // List of snaps + if (Array.isArray(conf.snap) && conf.snap.length > 1) { + for (const snap of conf.snap.sort((a, b) => a - b).map((snap, i) => { + const margin = (i < (conf.snap.length - 1)) + ? (conf.snap[Number(i) + 1] - snap) / 2 // Look forward + : (snap - conf.snap[i - 1]) / 2 // Final snap, look backward + + return { + min: snap - margin, + max: snap + Number(margin), + snap, + } + })) if (abs < snap.max && abs >= snap.min) return snap.snap + } + // If we end up here, the snap config is wrong + pattern.raise.warning(`Invalid snap config for option ${option}`) + + return abs +} + // Merges settings object with this.settings Pattern.prototype.apply = function (settings) { if (typeof settings !== 'object') { @@ -158,6 +190,18 @@ Pattern.prototype.draft = function () { this.is = 'draft' if (this.debug) this.raise.debug(`Drafting pattern`) } + // Handle snap for pct options + for (let i in this.settings.options) { + if ( + typeof this.config.options[i] !== 'undefined' && + typeof this.config.options[i].snap !== 'undefined' && + this.config.options[i].toAbs instanceof Function + ) { + let abs = this.config.options[i].toAbs(this.settings.options[i], this.settings) + this.settings.absoluteOptions[i] = snappedOption(i, this) + } + } + this.runHooks('preDraft') for (let partName of this.config.draftOrder) { if (this.debug) this.raise.debug(`Creating part \`${partName}\``) diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index 514b3e5efc0..b729a3696da 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -352,3 +352,11 @@ export function rad2deg(radians) { // Export bezier-js so plugins can use it export { Bezier } + +export function pctBasedOn(measurement) { + return { + toAbs: (val, { measurements }) => measurements[measurement] * val, + fromAbs: (val, { measurements }) => Math.round( ( 10 * val) / measurements[measurement]) / 10 + } +} + diff --git a/packages/core/tests/snap-proposal.js b/packages/core/tests/snap-proposal.js new file mode 100644 index 00000000000..b86fa69fb52 --- /dev/null +++ b/packages/core/tests/snap-proposal.js @@ -0,0 +1,79 @@ +const expect = require("chai").expect; +const freesewing = require("./dist"); + +const measurements = { head: 400 } +const toAbs = (val, { measurements }) => measurements.head * val + +it("Should snap a percentage options to equal steps", () => { + const design = new freesewing.Design({ + options: { + test: { pct: 30, min: 0, max: 100, snap: 12, toAbs } + } + }) + const patternA = new design({ options: { test: 0.13 }, measurements }) + const patternB = new design({ options: { test: 0.27 }, measurements }) + expect(patternA.settings.absoluteOptions.test).to.equal(60) + expect(patternB.settings.absoluteOptions.test).to.equal(108) +}); + +it("Should snap a percentage options to the Fibonacci sequence", () => { + const design = new freesewing.Design({ + options: { + test: { + pct: 30, min: 0, max: 100, toAbs, + snap: [ 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 ], + } + } + }) + const patternA = new design({ options: { test: 0.13 }, measurements }) + const patternB = new design({ options: { test: 0.27 }, measurements }) + const patternC = new design({ options: { test: 0.97 }, measurements }) + expect(patternA.settings.absoluteOptions.test).to.equal(55) + expect(patternB.settings.absoluteOptions.test).to.equal(89) + expect(patternC.settings.absoluteOptions.test).to.equal(388) +}); + +it("Should snap a percentage options to imperial snaps", () => { + const design = new freesewing.Design({ + options: { + test: { + pct: 30, min: 0, max: 100, toAbs, + snap: { + metric: [ 25, 50, 75, 100 ], + imperial: [ 25.4, 50.8, 76.2, 101.6 ], + } + } + } + }) + const patternA = new design({ options: { test: 0.13 }, measurements, units:'metric' }) + const patternB = new design({ options: { test: 0.27 }, measurements, units:'metric' }) + const patternC = new design({ options: { test: 0.97 }, measurements, units:'metric' }) + const patternD = new design({ options: { test: 0.01 }, measurements, units:'metric' }) + expect(patternA.settings.absoluteOptions.test).to.equal(50) + expect(patternB.settings.absoluteOptions.test).to.equal(100) + expect(patternC.settings.absoluteOptions.test).to.equal(388) + expect(patternD.settings.absoluteOptions.test).to.equal(4) +}); + +it("Should snap a percentage options to metrics snaps", () => { + const design = new freesewing.Design({ + options: { + test: { + pct: 30, min: 0, max: 100, toAbs, + snap: { + metric: [ 25, 50, 75, 100 ], + imperial: [ 25.4, 50.8, 76.2, 101.6 ], + } + } + } + }) + const patternA = new design({ options: { test: 0.13 }, measurements, units:'imperial' }) + const patternB = new design({ options: { test: 0.27 }, measurements, units:'imperial' }) + const patternC = new design({ options: { test: 0.97 }, measurements, units:'imperial' }) + const patternD = new design({ options: { test: 0.01 }, measurements, units:'imperial' }) + expect(patternA.settings.absoluteOptions.test).to.equal(50.8) + expect(patternB.settings.absoluteOptions.test).to.equal(101.6) + expect(patternC.settings.absoluteOptions.test).to.equal(388) + expect(patternD.settings.absoluteOptions.test).to.equal(4) +}); + diff --git a/packages/paco/config/index.js b/packages/paco/config/index.js index 73f5aeb27c6..8818cc01111 100644 --- a/packages/paco/config/index.js +++ b/packages/paco/config/index.js @@ -1,4 +1,5 @@ import { version } from '../package.json' +import { pctBasedOn } from '@freesewing/core' export default { name: 'paco', @@ -95,8 +96,15 @@ export default { elasticatedHem: { bool: true }, // Elastic - waistbandWidth: { pct: 3, min: 1, max: 6 }, - ankleElastic: { pct: 5, min: 1, max: 13 }, + waistbandWidth: { pct: 3, min: 1, max: 6, snap: 5 }, + ankleElastic: { + pct: 5, min: 1, max: 13, + snap: { + metric: [ 5, 10, 12, 20, 25, 30, 40, 50, 80 ], + imperial: [ 6.35, 9.525, 12.7, 15.24, 19.05, 25.4, 30.48, 50.8, 76.2], + }, + ...pctBasedOn('waistToFloor') + }, heelEase: { pct: 5, min: 0, max: 50 }, // Pockets diff --git a/packages/paco/src/back.js b/packages/paco/src/back.js index 00dcf6fea9f..ca082f546d5 100644 --- a/packages/paco/src/back.js +++ b/packages/paco/src/back.js @@ -42,7 +42,7 @@ export default function (part) { } // Shorthand call - let { store, sa, points, Path, paths, options, measurements, complete, paperless, macro } = + let { store, sa, points, Path, paths, options, measurements, complete, paperless, macro, absoluteOptions } = part.shorthand() // Adapt bottom leg width based on heel & heel ease @@ -55,7 +55,7 @@ export default function (part) { points.kneeInCp1 = points.kneeIn // Shorter leg if we have an elasticated hem - store.set('ankleElastic', measurements.waistToFloor * options.ankleElastic) + store.set('ankleElastic', absoluteOptions.ankleElastic) if (options.elasticatedHem) { for (const p of ['floor', 'floorIn', 'floorOut']) points[p] = points[p].shift(90, store.get('ankleElastic'))