UNPKG

@freesewing/core

Version:

A library for creating made-to-measure sewing patterns

514 lines (458 loc) 18.5 kB
import { __addNonEnumProp } from '../utils.mjs' import { getPluginName } from './pattern-plugins.mjs' export const hidePresets = { HIDE_ALL: { self: true, from: true, after: true, inherited: true, }, HIDE_TREE: { from: true, inherited: true, }, } ///////////////// // CONSTRUCTOR // ///////////////// /** * A class for handling config resolution for a Pattern * @class * @param {Pattern} pattern the pattern whose config is being handled */ export function PatternConfig(pattern) { /** @type {Store} the pattern's store, for logging */ this.store = pattern.store /** @type {Object} resolved plugins keyed by name */ this.plugins = { ...(pattern.designConfig.plugins || {}) } /** @type {Object} resolved options keyed by name */ this.options = { ...(pattern.designConfig.options || {}) } /** @type {string[]} required measurements */ this.measurements = [...(pattern.designConfig.measurements || [])] /** @type {string[]} optional measurements */ this.optionalMeasurements = [...(pattern.designConfig.optionalMeasurements || [])] /** @type {Object} the names of the parts that will be injected */ this.inject = {} /** @type {Object} arrays of parts that are direct dependencies of the key */ this.directDependencies = {} /** @type {Object} arrays of all dependencies of the key */ this.resolvedDependencies = {} /** @type {Object} parts to include in the pattern */ this.parts = {} /** @type {Object} which parts are hidden */ this.partHide = {} /** @type {String[]} The order in which parts should be drafted */ this.draftOrder = [] /** @type {Object} to track when to overwrite options */ __addNonEnumProp(this, '__mutated', { optionDistance: {}, partDistance: {}, hideDistance: {}, }) /** @type {Object} tracking for dependency hiding */ __addNonEnumProp(this, '__hiding', { from: {}, after: {}, inherited: {}, always: {}, never: {}, }) } //////////////////// // PUBLIC METHODS // //////////////////// /** * Validate that a part meets the requirements to be added to the pattern * @param {Object} part a part configuration * @return {boolean} whether the part is valid */ PatternConfig.prototype.isPartValid = function (part) { if (typeof part?.draft !== 'function') { this.store.log.error(`Part must have a draft() method`) return false } if (!part.name) { this.store.log.error(`Part must have a name`) return false } return true } /** * Chainable method to add a part to the configuration * @param {Object} part */ PatternConfig.prototype.addPart = function (part) { if (this.isPartValid(part)) this.__addPart([part]) return this } /** Log the final report on part inheritance order */ PatternConfig.prototype.logPartDistances = function () { const priorities = {} for (const partName of Object.keys(this.parts)) { const p = this.__mutated.partDistance[partName] if (typeof priorities[p] === 'undefined') priorities[p] = new Set() priorities[p].add(partName) } for (const p of Object.keys(priorities)) this.store.log.debug( `⚪️ Options priority __${p}__ : ` + `${[...priorities[p]].map((p) => '`' + p + '`').join(', ')}` ) } /** * Return a configuration in the structure expected by the pattern * @return {Object} contains parts, plugins, measurements, options, optionalMeasurements, resolvedDependencies, directDependencies, inject, draftOrder, partHide */ PatternConfig.prototype.asConfig = function () { return { parts: this.parts, plugins: this.plugins, measurements: this.measurements, options: this.options, optionalMeasurements: this.optionalMeasurements, resolvedDependencies: this.resolvedDependencies, directDependencies: this.directDependencies, inject: this.inject, draftOrder: this.draftOrder, partHide: this.partHide, } } ///////////////////// // PRIVATE METHODS // ///////////////////// /** * Add a part's configuration * Uses recursion to also add that part's dependencies * @private * @param {Object[]} depChain an array starting with the current part to add and containing its dependents/descendents in order */ PatternConfig.prototype.__addPart = function (depChain) { // the current part is the head of the chain const part = depChain[0] // only process a part that hasn't already been processed if (this.parts[part.name]) return this.parts[part.name] = Object.freeze(part) // if it hasn't been registered with a distance, do that now if (typeof this.__mutated.partDistance[part.name] === 'undefined') { // the longer the chain, the deeper the part is down it this.__mutated.partDistance[part.name] = depChain.length } // Handle various hiding possibilities this.__resolvePartHiding(part) // resolve its dependencies this.__resolvePartDependencies(depChain) // add the part's config this.__addPartConfig(part) // if it's a top level part if (depChain.length === 1) { // its resolved dependency list is a backwards representation of the draft order of its dependencies for (var i = this.resolvedDependencies[part.name].length - 1; i >= 0; i--) { let dep = this.resolvedDependencies[part.name][i] // only add it if it's not already on the list from another part if (this.draftOrder.indexOf(dep) === -1) this.draftOrder.push(dep) } // add it last of all this.draftOrder.push(part.name) } } /** * Resolves/Adds a part's design configuration to the pattern config * * @private * @param {Part} part - The part of which to resolve the config * @return this */ PatternConfig.prototype.__addPartConfig = function (part) { return this.__addPartOptions(part) // add options .__addPartMeasurements(part, false) // add required measurements .__addPartMeasurements(part, true) // add optional measurements .__addPartPlugins(part) // add plugins } /** * Resolves/Adds a part's configured options to the global config * * @private * @param {Part} part - The part of which to resolve the config * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartOptions = function (part) { // skip empty options if (!part.options) return this // get the part's option priority const partDistance = this.__mutated.partDistance?.[part.name] // loop through options for (const optionName in part.options) { const option = part.options[optionName] // get the priority of this option's current registration const optionDistance = this.__mutated.optionDistance[optionName] // if it's never been registered, or it's registered at a further distance if (!optionDistance || optionDistance > partDistance) { // Keep options immutable in the pattern or risk subtle bugs this.options[optionName] = Object.freeze(option) // register the new distance this.__mutated.optionDistance[optionName] = partDistance // debug appropriately this.store.log.debug( optionDistance ? `🟣 __${optionName}__ option overwritten by \`${part.name}\`` : `🔵 __${optionName}__ option loaded from part \`${part.name}\`` ) } } return this } /** * Resolves/Adds a part's configured measurements to the global config * * @private * @param {Part} part - The part of which to resolve the config * @param {boolean} optional - are these measurements optional? * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartMeasurements = function (part, optional = false) { // which list are we drawing from? const listType = optional ? 'optionalMeasurements' : 'measurements' // if the part has measurements of this type, go through them if (part[listType]) { part[listType].forEach((m) => { // we need to know what lists it's already present on const isInReqList = this.measurements.indexOf(m) !== -1 // if it's already registered as required, we're done here if (isInReqList) return // check if it's registered as optional const optInd = this.optionalMeasurements.indexOf(m) const isInOptList = optInd !== -1 // if it is optional and not in the list, push it if (optional && !isInOptList) this.optionalMeasurements.push(m) // if it's not optional if (!optional) { // push it to required list this.measurements.push(m) // make sure it's not also registered as optional if (isInOptList) this.optionalMeasurements.splice(optInd, 1) } this.store.log.debug( `🟠 __${m}__ measurement is ${optional ? 'optional' : 'required'} in \`${part.name}\`` ) }) } return this } /** * Resolves/Adds a part's configured plugins to the global config * * @private * @param {Part} part - The part of which to resolve the config * @return {PatternConfig} this - The PatternConfig instance */ PatternConfig.prototype.__addPartPlugins = function (part) { if (!part.plugins) return this const plugins = this.plugins // Side-step immutability of the part object to ensure plugins is an array let partPlugins = part.plugins if (!Array.isArray(partPlugins)) partPlugins = [partPlugins] // Go through list of part plugins for (let plugin of partPlugins) { const name = getPluginName(plugin) this.store.log.debug( plugin.plugin ? `🔌 Resolved __${name}__ conditional plugin in \`${part.name}\`` : `🔌 Resolved __${name}__ plugin in \`${part.name}\`` ) // Handle [plugin, data] scenario if (Array.isArray(plugin)) { const pluginObj = { ...plugin[0], data: plugin[1] } plugin = pluginObj } if (!plugins[name]) { // New plugin, so we load it plugins[name] = plugin this.store.log.info( plugin.condition ? `New plugin conditionally added: \`${name}\`` : `New plugin added: \`${name}\`` ) } else { // Existing plugin, takes some more work if (plugin.plugin && plugin.condition) { // Multiple instances of the same plugin with different conditions // will all be added, so we need to change the name. if (plugins[name]?.condition) { plugins[name + '_'] = plugin this.store.log.info( `Plugin \`${name}\` was conditionally added again. Renaming to ${name}_.` ) } else this.store.log.info( `Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.` ) } // swap from a conditional if needed else if (plugins[name].condition) { plugins[name] = plugin this.store.log.info(`Plugin \`${name}\` was explicitly added. Changing from conditional.`) } } } return this } // the two types of dependencies const depTypes = ['from', 'after'] // the two lists of special istructions const exceptionTypes = ['never', 'always'] /** * Resolve the hiding configuration of this part * This method does not hide dependencies, * but it does hide or unhide parts listed in `never` and `always` in the config * according to this part's options priority * @param {Part} part the part whose config should be resolved * @private */ PatternConfig.prototype.__resolvePartHiding = function (part) { // get the config let hide = part.hide // if it's a string, get the preset by that name if (typeof hide === 'string') hide = hidePresets[hide] // no config, nothing to do if (!hide) return // get the part's option priority const partDistance = this.__mutated.partDistance?.[part.name] // get the current distances that dictate if this part should never or always be hidden const neverDistance = this.__hiding.never[part.name] || Infinity const alwaysDistance = this.__hiding.always[part.name] || Infinity // if the part is configured to hide, and it takes priority over other instructions, hide it if (hide.self && (neverDistance > partDistance || alwaysDistance <= neverDistance)) this.partHide[part.name] = true // for each exception list, starting with never exceptionTypes.forEach((e, i) => { // if there are instructions for this list if (hide[e]) { // each part in the list hide[e].forEach((p) => { // get the current distance of a call to never or always hide this part const otherDistance = this.__hiding[exceptionTypes[Math.abs(i - 1)]][p] || Infinity // if a current command is less important than this one, if (otherDistance > partDistance) { const thisDistance = this.__hiding[e][p] || Infinity // record the new priority this.__hiding[e][p] = Math.min(thisDistance, partDistance) // hide or show the part this.partHide[p] = i == 1 } }) } }) // add the dependency hiding instructions if they haven't already been set depTypes.concat('inherited').forEach((k) => { if (this.__hiding[k][part.name] === undefined) this.__hiding[k][part.name] = hide[k] }) } /** * Recursively register part dependencies * triggers {@link __addPart} on new parts found during resolution * @param {Object[]} depChain an array starting with the current part to register and containing its dependents/descendents in order * @return {PatternConfig} this * @private */ PatternConfig.prototype.__resolvePartDependencies = function (depChain) { // the current part is the head of the chain const part = depChain[0] // get or make its array of resolved dependencies this.resolvedDependencies[part.name] = this.resolvedDependencies[part.name] || [] // for each dependency type (from, after) depTypes.forEach((d) => { // if the part has dependencies of that type if (part[d]) { // enforce an array const depsOfType = [].concat(part[d]) // loop through backwards so that we're resolving last to first // this order is necessary so that when we add the reversed chain to the draft order // afters are included in the order they were listed in the config, but recursive // resolution order is also correct for (var i = depsOfType.length - 1; i >= 0; i--) { const dot = depsOfType[i] // add it as a direct dependency of the current part this.__addDependency('directDependencies', part.name, dot.name) // add it as a resolved dependency of all parts in the chain depChain.forEach((c) => this.__addDependency('resolvedDependencies', c.name, dot.name)) // handle hiding and injecting this.__handlePartDependencyOfType(part, dot.name, d) // if the dependency isn't registered, register it if (!this.parts[dot.name]) { // add the part's configuration. this will recursively add the part's dependencies to all parts in the chain this.__addPart([dot, ...depChain]) } else { // if it's already registered, recursion won't happen, but we still need to add its resolved dependencies to all parts in the chain this.__resolvePartDependencies([dot, ...depChain]) } } } }) // now that the chain has been registered, recalculate the part distances this.__resolveMutatedPartDistance(part.name) } /** * Adds a part as either a direct or a resolved dependency * @param {string} dependencyList which list to add the part to, 'resolvedDependencies' or 'directDependencies' * @param {string} partName the name of the part to add the dependency to in the list * @param {string} depName the name of the dependency to add to the list * @private */ PatternConfig.prototype.__addDependency = function (dependencyList, partName, depName) { this[dependencyList][partName] = this[dependencyList][partName] || [] // if it's already in the dependency list, take it out because it needs to be put on the end const depIndex = this[dependencyList][partName].indexOf(depName) if (depIndex !== -1) this[dependencyList][partName].splice(depIndex, 1) // put it at the end of the list this[dependencyList][partName].push(depName) } /** * Handle dependency-type specific config business * @param {Object} part the part to add the dependency to * @param {string} depName the name of the dependency to add * @param {string} depType the type of dependency, 'from' or 'after' * @private */ PatternConfig.prototype.__handlePartDependencyOfType = function (part, depName, depType) { // if this dependency should be hidden based on dependency type, and doesn't already have an instruction, hide it if (this.__hiding[depType][part.name] === true && this.partHide[depName] === undefined) { this.partHide[depName] = true } // get the part's inherited hide instructions const hideInherited = this.__hiding.inherited[part.name] // for from dependencies if (depType === 'from') { // inject the dependency into the part this.inject[part.name] = depName // hide after dependencies if inherited dependencies should hide this.__hiding.after[depName] = hideInherited } // for all depependency types, from and inherited are dictated by the dependendent part's policy this.__hiding.from[depName] = hideInherited this.__hiding.inherited[depName] = hideInherited } /** * Resolve part option priority * Recursively bumps priorities down the dependency chain * @param {string} partName the name of the part to resolve * @private */ PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) { // if the part has no dependencies, bail if (!this.directDependencies[partName]) return // propose that each of the part's direct dependencies should be at a distance 1 further than the part's distance let proposedDependencyDistance = this.__mutated.partDistance[partName] + 1 // check each direct dependency this.directDependencies[partName].forEach((dependency) => { // if the dependency doesn't have a distance, or that distance is less than the proposal if ( typeof this.__mutated.partDistance[dependency] === 'undefined' || this.__mutated.partDistance[dependency] < proposedDependencyDistance ) { // set the new distance this.__mutated.partDistance[dependency] = proposedDependencyDistance // bump the dependency's dependencies as well this.__resolveMutatedPartDistance(dependency) } }) }