diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs index 9ff25284860..4a37e775321 100644 --- a/packages/core/src/pattern/index.mjs +++ b/packages/core/src/pattern/index.mjs @@ -1,5 +1,4 @@ import { Attributes } from '../attributes.mjs' -import pack from 'bin-pack-with-constraints' import { __addNonEnumProp, __macroName } from '../utils.mjs' import { Part } from '../part.mjs' import { Stack } from '../stack.mjs' @@ -12,7 +11,7 @@ import { Hooks } from '../hooks.mjs' import { version } from '../../data.mjs' import { __loadPatternDefaults } from '../config.mjs' import { PatternConfig } from './pattern-config.mjs' -import { PatternDraftQueue } from './pattern-draft-queue.mjs' +import { PatternDrafter } from './pattern-drafter.mjs' import { PatternSampler } from './pattern-sampler.mjs' import { PatternPlugins, getPluginName } from './pattern-plugins.mjs' import { PatternRenderer } from './pattern-renderer.mjs' @@ -46,7 +45,6 @@ export function Pattern(designConfig = {}) { __addNonEnumProp(this, 'Path', Path) __addNonEnumProp(this, 'Snippet', Snippet) __addNonEnumProp(this, 'Attributes', Attributes) - __addNonEnumProp(this, 'macros', {}) __addNonEnumProp(this, '__initialized', false) __addNonEnumProp(this, 'config.parts', {}) __addNonEnumProp(this, 'config.resolvedDependencies', {}) @@ -92,98 +90,14 @@ Pattern.prototype.addPart = function (part, resolveImmediately = true) { */ Pattern.prototype.draft = function () { this.__init() - this.draftQueue = new PatternDraftQueue(this) - this.__runHooks('preDraft') - // Keep container for drafted parts fresh - this.parts = [] - - // Iterate over the provided sets of settings (typically just one) - for (const set in this.settings) { - this.activeSet = set - this.setStores[set] = this.__createSetStore() - this.setStores[set].log.debug(`Initialized store for set ${set}`) - this.__runHooks('preSetDraft') - this.setStores[set].log.debug(`📐 Drafting pattern for set ${set}`) - - // Create parts container - this.parts[set] = {} - - // Handle snap for pct options - this.__loadAbsoluteOptionsSet(set) - - this.draftQueue.start() - while (this.draftQueue.hasNext()) { - this.createPartForSet(this.draftQueue.next(), set) - } - this.__runHooks('postSetDraft') - } - this.__runHooks('postDraft') + new PatternDrafter(this).draft() return this } -Pattern.prototype.createPartForSet = function (partName, set = 0) { - // gotta protect against attacks - if (set === '__proto__') { - throw new Error('malicious attempt at altering Object.prototype. Stopping action') - } - // Create parts - this.setStores[set].log.debug(`📦 Creating part \`${partName}\` (set ${set})`) - this.parts[set][partName] = this.__createPartWithContext(partName, set) - - // Handle inject/inheritance - if (typeof this.config.inject[partName] === 'string') { - this.setStores[set].log.debug( - `Creating part \`${partName}\` from part \`${this.config.inject[partName]}\`` - ) - try { - this.parts[set][partName].__inject(this.parts[set][this.config.inject[partName]]) - } catch (err) { - this.setStores[set].log.error([ - `Could not inject part \`${this.config.inject[partName]}\` into part \`${partName}\``, - err, - ]) - } - } - if (this.__needs(partName, set)) { - // Draft part - const result = this.draftPartForSet(partName, set) - if (typeof result !== 'undefined') this.parts[set][partName] = result - // FIXME: THis won't work not that this is immutable - // But is it still needed? - // this.parts[set][partName].hidden === true ? true : !this.__wants(partName, set) - } else { - this.setStores[set].log.debug( - `Part \`${partName}\` is not needed. Skipping draft and setting hidden to \`true\`` - ) - this.parts[set][partName].hidden = true - } -} - Pattern.prototype.draftPartForSet = function (partName, set) { - if (typeof this.config.parts?.[partName]?.draft === 'function') { - this.activePart = partName - this.setStores[set].set('activePart', partName) - try { - this.__runHooks('prePartDraft') - const result = this.config.parts[partName].draft(this.parts[set][partName].shorthand()) - if (!this.__wants(partName, set)) { - result.hide() - } - this.__runHooks('postPartDraft') - if (typeof result === 'undefined') { - this.setStores[set].log.error( - `Result of drafting part ${partName} was undefined. Did you forget to return the part?` - ) - } - return result - } catch (err) { - this.setStores[set].log.error([`Unable to draft part \`${partName}\` (set ${set})`, err]) - } - } else - this.setStores[set].log.error( - `Unable to draft pattern part __${partName}__. Part.draft() is not callable` - ) + this.__init() + return new PatternDrafter(this).draftPartForSet(partName, set) } /** @@ -323,39 +237,6 @@ Pattern.prototype.__applySettings = function (sets) { return this } -/** - * Instantiates a new Part instance and populates it with the pattern context - * - * @private - * @param {string} name - The name of the part - * @param {int} set - The index of the settings set in the list of sets - * @return {Part} part - The instantiated Part - */ -Pattern.prototype.__createPartWithContext = function (name, set) { - // Context object to add to Part closure - const part = new Part() - part.name = name - part.set = set - part.stack = this.config.parts[name]?.stack || name - part.context = { - parts: this.parts[set], - config: this.config, - settings: this.settings[set], - store: this.setStores[set], - macros: this.plugins.macros, - } - - if (this.settings[set]?.partClasses) { - part.attr('class', this.settings[set].partClasses) - } - - for (const macro in this.plugins.macros) { - part[__macroName(macro)] = this.plugins.macros[macro] - } - - return part -} - /** * Initializes the pattern coniguration and settings * @@ -426,34 +307,6 @@ Pattern.prototype.__isStackHidden = function (stackName) { return true } -/** - * Generates an array of settings.absoluteOptions objects for sampling a list option - * - * @private - * @param {string} optionName - Name of the option to sample - * @return {Array} sets - The list of settings objects - */ -Pattern.prototype.__loadAbsoluteOptionsSet = function (set) { - for (const optionName in this.settings[set].options) { - const option = this.config.options[optionName] - if ( - typeof option !== 'undefined' && - typeof option.snap !== 'undefined' && - option.toAbs instanceof Function - ) { - this.settings[set].absoluteOptions[optionName] = this.__snappedPercentageOption( - optionName, - set - ) - this.setStores[set].log.debug( - `🧲 Snapped __${optionName}__ to \`${this.settings[set].absoluteOptions[optionName]}\` for set __${set}__` - ) - } - } - - return this -} - /** * Loads data from the design config into the store * @@ -576,45 +429,6 @@ Pattern.prototype.__runHooks = function (hookName, data = false) { } } -/** - * Returns the absolute value of a snapped percentage option - * - * @private - * @param {string} optionName - The name of the option - * @param {int} set - The index of the set in the list of settings - * @return {float} abs - The absolute value of the snapped option - */ -Pattern.prototype.__snappedPercentageOption = function (optionName, set) { - const conf = this.config.options[optionName] - const abs = conf.toAbs(this.settings[set].options[optionName], this.settings[set]) - // Handle units-specific config - Side-step immutability for the snap conf - let snapConf = conf.snap - if (!Array.isArray(snapConf) && snapConf.metric && snapConf.imperial) - snapConf = snapConf[this.settings[set].units] - // Simple steps - if (typeof snapConf === 'number') return Math.ceil(abs / snapConf) * snapConf - // List of snaps - if (Array.isArray(snapConf) && snapConf.length > 1) { - for (const snap of snapConf - .sort((a, b) => a - b) - .map((snap, i) => { - const margin = - i < snapConf.length - 1 - ? (snapConf[Number(i) + 1] - snap) / 2 // Look forward - : (snap - snapConf[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 - } - - return abs -} - /** * Determines whether a part is wanted, depending on the 'only' setting and the configured dependencies * diff --git a/packages/core/src/pattern/pattern-draft-handler.mjs b/packages/core/src/pattern/pattern-draft-handler.mjs deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/core/src/pattern/pattern-drafter.mjs b/packages/core/src/pattern/pattern-drafter.mjs new file mode 100644 index 00000000000..4e9c6d8379d --- /dev/null +++ b/packages/core/src/pattern/pattern-drafter.mjs @@ -0,0 +1,227 @@ +import { PatternDraftQueue } from './pattern-draft-queue.mjs' +import { Part } from '../part.mjs' +import { __macroName } from '../utils.mjs' + +export function PatternDrafter(pattern) { + this.pattern = pattern +} + +Object.defineProperty(PatternDrafter.prototype, 'activeSet', { + get: function () { + return this.pattern.activeSet + }, + set: function (newVal) { + this.pattern.activeSet = newVal + }, +}) + +Object.defineProperty(PatternDrafter.prototype, 'activePart', { + get: function () { + return this.pattern.activePart + }, + set: function (newVal) { + this.pattern.activePart = newVal + this.activeStore.set('activePart', newVal) + }, +}) + +/** + * Drafts this pattern, aka the raison d'etre of FreeSewing + * + * @return {object} this - The Pattern instance + */ +PatternDrafter.prototype.draft = function () { + this.pattern.draftQueue = new PatternDraftQueue(this.pattern) + this.pattern.__runHooks('preDraft') + // Keep container for drafted parts fresh + this.pattern.parts = [] + + // Iterate over the provided sets of settings (typically just one) + for (const set in this.pattern.settings) { + this.pattern.setStores[set] = this.pattern.__createSetStore() + this.__useSet(set) + + this.activeStore.log.debug(`Initialized store for set ${set}`) + this.pattern.__runHooks('preSetDraft') + this.activeStore.log.debug(`📐 Drafting pattern for set ${set}`) + + // Create parts container + this.pattern.parts[set] = {} + + // Handle snap for pct options + this.__loadAbsoluteOptionsSet(set) + + this.pattern.draftQueue.start() + while (this.pattern.draftQueue.hasNext()) { + const partName = this.pattern.draftQueue.next() + if (this.pattern.__needs(partName, set)) { + this.draftPartForSet(partName, set) + } else { + this.activeStore.log.debug(`Part \`${partName}\` is not needed. Skipping part`) + } + } + this.pattern.__runHooks('postSetDraft') + } + this.pattern.__runHooks('postDraft') +} + +PatternDrafter.prototype.draftPartForSet = function (partName, set) { + this.__useSet(set) + this.__createPartForSet(partName, set) + + const configPart = this.pattern.config.parts?.[partName] + if (typeof configPart?.draft !== 'function') { + this.activeStore.log.error( + `Unable to draft pattern part __${partName}__. Part.draft() is not callable` + ) + return + } + + this.activePart = partName + try { + this.pattern.__runHooks('prePartDraft') + const result = configPart.draft(this.pattern.parts[set][partName].shorthand()) + + if (typeof result === 'undefined') { + this.activeStore.log.error( + `Result of drafting part ${partName} was undefined. Did you forget to return the part?` + ) + } else { + if (!this.pattern.__wants(partName, set)) result.hide() + this.pattern.__runHooks('postPartDraft') + this.pattern.parts[set][partName] = result + } + return result + } catch (err) { + this.activeStore.log.error([`Unable to draft part \`${partName}\` (set ${set})`, err]) + } +} + +PatternDrafter.prototype.__createPartForSet = function (partName, set = 0) { + // gotta protect against attacks + if (set === '__proto__') { + throw new Error('malicious attempt at altering Object.prototype. Stopping action') + } + // Create parts + this.activeStore.log.debug(`📦 Creating part \`${partName}\` (set ${set})`) + this.pattern.parts[set][partName] = this.__createPartWithContext(partName, set) + + // Handle inject/inheritance + const parent = this.pattern.config.inject[partName] + if (typeof parent === 'string') { + this.activeStore.log.debug(`Creating part \`${partName}\` from part \`${parent}\``) + try { + this.pattern.parts[set][partName].__inject(this.pattern.parts[set][parent]) + } catch (err) { + this.activeStore.log.error([ + `Could not inject part \`${parent}\` into part \`${partName}\``, + err, + ]) + } + } +} + +PatternDrafter.prototype.__useSet = function (set = 0) { + this.activeSet = set + this.activeSettings = this.pattern.settings[set] + this.activeStore = this.pattern.setStores[set] +} +/** + * Generates an array of settings.absoluteOptions objects for sampling a list option + * + * @private + * @param {string} optionName - Name of the option to sample + * @return {Array} sets - The list of settings objects + */ +PatternDrafter.prototype.__loadAbsoluteOptionsSet = function (set) { + for (const optionName in this.pattern.settings[set].options) { + const option = this.pattern.config.options[optionName] + if ( + typeof option !== 'undefined' && + typeof option.snap !== 'undefined' && + option.toAbs instanceof Function + ) { + this.pattern.settings[set].absoluteOptions[optionName] = this.__snappedPercentageOption( + optionName, + set + ) + this.pattern.setStores[set].log.debug( + `🧲 Snapped __${optionName}__ to \`${this.pattern.settings[set].absoluteOptions[optionName]}\` for set __${set}__` + ) + } + } + + return this +} + +/** + * Returns the absolute value of a snapped percentage option + * + * @private + * @param {string} optionName - The name of the option + * @param {int} set - The index of the set in the list of settings + * @return {float} abs - The absolute value of the snapped option + */ +PatternDrafter.prototype.__snappedPercentageOption = function (optionName, set) { + const conf = this.pattern.config.options[optionName] + const abs = conf.toAbs(this.pattern.settings[set].options[optionName], this.pattern.settings[set]) + // Handle units-specific config - Side-step immutability for the snap conf + let snapConf = conf.snap + if (!Array.isArray(snapConf) && snapConf.metric && snapConf.imperial) + snapConf = snapConf[this.pattern.settings[set].units] + // Simple steps + if (typeof snapConf === 'number') return Math.ceil(abs / snapConf) * snapConf + // List of snaps + if (Array.isArray(snapConf) && snapConf.length > 1) { + for (const snap of snapConf + .sort((a, b) => a - b) + .map((snap, i) => { + const margin = + i < snapConf.length - 1 + ? (snapConf[Number(i) + 1] - snap) / 2 // Look forward + : (snap - snapConf[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 + } + + return abs +} + +/** + * Instantiates a new Part instance and populates it with the pattern context + * + * @private + * @param {string} name - The name of the part + * @param {int} set - The index of the settings set in the list of sets + * @return {Part} part - The instantiated Part + */ +PatternDrafter.prototype.__createPartWithContext = function (name, set) { + // Context object to add to Part closure + const part = new Part() + part.name = name + part.set = set + part.stack = this.pattern.config.parts[name]?.stack || name + part.context = { + parts: this.pattern.parts[set], + config: this.pattern.config, + settings: this.pattern.settings[set], + store: this.pattern.setStores[set], + macros: this.pattern.plugins.macros, + } + + if (this.pattern.settings[set]?.partClasses) { + part.attr('class', this.pattern.settings[set].partClasses) + } + + for (const macro in this.pattern.plugins.macros) { + part[__macroName(macro)] = this.pattern.plugins.macros[macro] + } + + return part +} diff --git a/packages/core/tests/pattern-draft.test.mjs b/packages/core/tests/pattern-draft.test.mjs index fae87165224..bceaa10221f 100644 --- a/packages/core/tests/pattern-draft.test.mjs +++ b/packages/core/tests/pattern-draft.test.mjs @@ -33,18 +33,6 @@ describe('Pattern', () => { expect(count).to.equal(2) }) }) - describe('Pattern.createPartForSet()', () => { - it('Should not allow malicious assignment to Object.prototype', () => { - const objProto = Object.prototype - const Pattern = new Design() - const pattern = new Pattern() - - expect(() => pattern.createPartForSet('part', '__proto__')).to.throw( - 'malicious attempt at altering Object.prototype. Stopping action' - ) - expect(objProto).to.equal(Object.prototype) - }) - }) it('Should check whether a part is needed', () => { const partA = { name: 'test.partA', diff --git a/packages/core/tests/pattern-other.test.mjs b/packages/core/tests/pattern-other.test.mjs index c15c7ce07fa..7b7a51dde9c 100644 --- a/packages/core/tests/pattern-other.test.mjs +++ b/packages/core/tests/pattern-other.test.mjs @@ -61,9 +61,7 @@ describe('Pattern', () => { const design = new Design({ parts: [test, you] }) const pattern = new design({ only: ['you'] }) pattern.draft() - expect(pattern.setStores[0].logs.debug).to.include( - 'Part `test` is not needed. Skipping draft and setting hidden to `true`' - ) + expect(pattern.setStores[0].logs.debug).to.include('Part `test` is not needed. Skipping part') }) it('Should return the initialized config', () => {