feat(core): Snapping of percentage options
This commit implements the snapping of percentage options as outlined in this proposal: https://github.com/freesewing/freesewing/discussions/1331 Please refer to the link above for all details
This commit is contained in:
parent
0da866d923
commit
025cf9b88e
7 changed files with 161 additions and 4 deletions
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}\``)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
79
packages/core/tests/snap-proposal.js
Normal file
79
packages/core/tests/snap-proposal.js
Normal file
|
@ -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)
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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'))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue