1
0
Fork 0

move all pattern config resolution to separate class

This commit is contained in:
Enoch Riese 2023-02-21 22:33:57 +02:00
parent 49ae4ec61a
commit 5945483f86
5 changed files with 534 additions and 499 deletions

View file

@ -11,9 +11,9 @@ import { Store } from './store.mjs'
import { Hooks } from './hooks.mjs'
import { version } from '../data.mjs'
import { __loadPatternDefaults } from './config.mjs'
import { PatternConfig, getPluginName } from './patternConfig.mjs'
import cloneDeep from 'lodash.clonedeep'
const DISTANCE_DEBUG = false
//////////////////////////////////////////////
// CONSTRUCTOR //
//////////////////////////////////////////////
@ -25,7 +25,7 @@ const DISTANCE_DEBUG = false
* @param {object} config - The Design config
* @return {object} this - The Pattern instance
*/
export function Pattern(designConfig) {
export function Pattern(designConfig = {}) {
// Non-enumerable properties
__addNonEnumProp(this, 'plugins', {})
__addNonEnumProp(this, 'width', 0)
@ -39,19 +39,9 @@ export function Pattern(designConfig) {
__addNonEnumProp(this, 'Attributes', Attributes)
__addNonEnumProp(this, 'macros', {})
__addNonEnumProp(this, '__initialized', false)
__addNonEnumProp(this, '__designParts', {})
__addNonEnumProp(this, '__inject', {})
__addNonEnumProp(this, '__dependencies', {})
__addNonEnumProp(this, '__resolvedDependencies', {})
__addNonEnumProp(this, '__resolvedParts', [])
__addNonEnumProp(this, 'config.parts', {})
__addNonEnumProp(this, 'config.resolvedDependencies', {})
__addNonEnumProp(this, '__storeMethods', new Set())
__addNonEnumProp(this, '__mutated', {
optionDistance: {},
partDistance: {},
partHide: {},
partHideAll: {},
})
__addNonEnumProp(this, '__draftOrder', [])
__addNonEnumProp(this, '__hide', {})
// Enumerable properties
@ -59,6 +49,7 @@ export function Pattern(designConfig) {
this.config = {} // Will hold the resolved pattern after calling __init()
this.store = new Store() // Pattern-wide store
this.setStores = [] // Per-set stores
__addNonEnumProp(this, '__configResolver', new PatternConfig(this)) // handles config resolution during __init() as well as runtime part adding
return this
}
@ -74,16 +65,11 @@ export function Pattern(designConfig) {
* @return {object} this - The Pattern instance
*/
Pattern.prototype.addPart = function (part, resolveImmediately = false) {
if (typeof part?.draft === 'function') {
if (part.name) {
if (this.__configResolver.validatePart(part) && this.designConfig.parts.indexOf(part) === -1) {
this.designConfig.parts.push(part)
if (resolveImmediately) {
this.store.log.debug(`Perfoming runtime resolution of new part ${part.name}`)
this.__resolvePart([part])
} else this.__initialized = false
} else this.store.log.error(`Part must have a name`)
} else this.store.log.error(`Part must have a draft() method`)
if (resolveImmediately) this.__configResolver.addPart(part)
else this.__initialized = false
}
return this
}
@ -132,15 +118,15 @@ Pattern.prototype.createPartForSet = function (partName, set = 0) {
this.parts[set][partName] = this.__createPartWithContext(partName, set)
// Handle inject/inheritance
if (typeof this.__inject[partName] === 'string') {
if (typeof this.config.inject[partName] === 'string') {
this.setStores[set].log.debug(
`Creating part \`${partName}\` from part \`${this.__inject[partName]}\``
`Creating part \`${partName}\` from part \`${this.config.inject[partName]}\``
)
try {
this.parts[set][partName].__inject(this.parts[set][this.__inject[partName]])
this.parts[set][partName].__inject(this.parts[set][this.config.inject[partName]])
} catch (err) {
this.setStores[set].log.error([
`Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``,
`Could not inject part \`${this.config.inject[partName]}\` into part \`${partName}\``,
err,
])
}
@ -161,11 +147,11 @@ Pattern.prototype.createPartForSet = function (partName, set = 0) {
}
Pattern.prototype.draftPartForSet = function (partName, set) {
if (typeof this.__designParts?.[partName]?.draft === 'function') {
if (typeof this.config.parts?.[partName]?.draft === 'function') {
this.activePart = partName
try {
this.__runHooks('prePartDraft')
const result = this.__designParts[partName].draft(this.parts[set][partName].shorthand())
const result = this.config.parts[partName].draft(this.parts[set][partName].shorthand())
this.__runHooks('postPartDraft')
if (typeof result === 'undefined') {
this.setStores[set].log.error(
@ -361,209 +347,6 @@ Pattern.prototype.use = function (plugin, data) {
// PRIVATE METHODS //
//////////////////////////////////////////////
/**
* Adds a part as a simple dependency
*
* @private
* @param {string} name - The name of the dependency
* @param {object} part - The part configuration
* @param {object} dep - The dependency configuration
* @return {object} this - The Pattern instance
*/
Pattern.prototype.__addDependency = function (dependencyList, part, dep) {
this[dependencyList][part.name] = this[dependencyList][part.name] || []
if (dependencyList == '__resolvedDependencies' && DISTANCE_DEBUG)
this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`)
if (this[dependencyList][part.name].indexOf(dep.name) === -1)
this[dependencyList][part.name].push(dep.name)
}
/**
* Resolves/Adds a part's design configuration to the pattern config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @param {onject} config - The global config
* @param {Store} store - The store, used for logging
* @return {object} config - The mutated global config
*/
Pattern.prototype.__addPartConfig = function (part) {
if (this.__resolvedParts.includes(part.name)) return this
// Add parts, using set to keep them unique in the array
this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)]
return this.__addPartOptions(part)
.__addPartMeasurements(part)
.__addPartOptionalMeasurements(part)
.__addPartPlugins(part)
}
/**
* Resolves/Adds a part's configured measurements to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @param {array} list - The list of resolved measurements
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartMeasurements = function (part, list = false) {
if (!this.config.measurements) this.config.measurements = []
if (!list) list = this.config.measurements
if (part.measurements) {
for (const m of part.measurements) {
if (list.indexOf(m) === -1) {
list.push(m)
this.store.log.debug(`🟠 __${m}__ measurement is required in \`${part.name}\``)
}
}
}
// Weed out duplicates
this.config.measurements = [...new Set(list)]
return this
}
/**
* Resolves/Adds a part's configured optional measurements to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @param {array} list - The list of resolved optional measurements
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) {
if (!this.config.optionalMeasurements) this.config.optionalMeasurements = []
if (!list) list = this.config.optionalMeasurements
if (part.optionalMeasurements) {
for (const m of part.optionalMeasurements) {
// Don't add it's a required measurement for another part
if (this.config.measurements.indexOf(m) === -1) {
if (list.indexOf(m) === -1) {
list.push(m)
this.store.log.debug(`🟡 __${m}__ measurement is optional in \`${part.name}\``)
}
}
}
}
// Weed out duplicates
if (list.length > 0) this.config.optionalMeasurements = [...new Set(list)]
return this
}
/**
* Resolves/Adds a part's configured options to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartOptions = function (part) {
if (!this.config.options) this.config.options = {}
if (part.options) {
const partDistance = this.__mutated.partDistance?.[part.name] || 0
for (const optionName in part.options) {
const optionDistance = this.__mutated.optionDistance[optionName]
if (!optionDistance) {
this.__mutated.optionDistance[optionName] = partDistance
// Keep design parts immutable in the pattern or risk subtle bugs
this.config.options[optionName] = Object.freeze(part.options[optionName])
this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``)
} else {
if (DISTANCE_DEBUG)
this.store.log.debug(
`optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__`
)
if (optionDistance > partDistance) {
this.config.options[optionName] = part.options[optionName]
this.__mutated.optionDistance[optionName] = partDistance
this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
}
}
}
}
return this
}
function getPluginName(plugin) {
if (Array.isArray(plugin)) {
if (plugin[0].name) return plugin[0].name
if (plugin[0].plugin.name) return plugin[0].plugin.name
} else {
if (plugin.name) return plugin.name
if (plugin.plugin?.name) return plugin.plugin.name
}
return false
}
/**
* Resolves/Adds a part's configured plugins to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartPlugins = function (part) {
if (!part.plugins) return this
if (!this.config.plugins) this.config.plugins = {}
const plugins = { ...this.config.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.`)
}
}
}
this.config.plugins = { ...plugins }
return this
}
/**
* Creates a store for a set (of settings)
*
@ -620,7 +403,7 @@ Pattern.prototype.__createPartWithContext = function (name, set) {
const part = new Part()
part.name = name
part.set = set
part.stack = this.__designParts[name]?.stack || name
part.stack = this.config.parts[name]?.stack || name
part.context = {
parts: this.parts[set],
config: this.config,
@ -700,14 +483,11 @@ Pattern.prototype.__init = function () {
* This methods does that, and resolves the design config + user settings
*/
this.__resolveParts() // Resolves parts
.__resolveDependencies() // Resolves dependencies
.__resolveDraftOrder() // Resolves draft order
.__resolveConfig() // Gets the config from the resolver
.__loadPlugins() // Loads plugins
.__loadConfigData() // Makes config data available in store
.__filterOptionalMeasurements() // Removes required m's from optional list
.__loadOptionDefaults() // Merges default options with user provided ones
this.store.log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`)
this.store.log.info(`Pattern initialized. Draft order is: ${this.config.draftOrder.join(', ')}`)
this.__runHooks('postInit')
this.__initialized = true
@ -726,10 +506,10 @@ Pattern.prototype.__isPartHidden = function (partName) {
if (Array.isArray(this.settings[this.activeSet || 0].only)) {
if (this.settings[this.activeSet || 0].only.includes(partName)) return false
}
if (this.__designParts?.[partName]?.hide) return true
if (this.__designParts?.[partName]?.hideAll) return true
if (this.__mutated.partHide?.[partName]) return true
if (this.__mutated.partHideAll?.[partName]) return true
if (this.config.parts?.[partName]?.hide) return true
if (this.config.parts?.[partName]?.hideAll) return true
if (this.config.partHide?.[partName]) return true
if (this.config.partHideAll?.[partName]) return true
if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true
return false
@ -822,40 +602,6 @@ Pattern.prototype.__loadConfigData = function () {
return this
}
/**
* Merges defaults for options with user-provided options
*
* @private
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__loadOptionDefaults = function () {
if (!this.config.options) this.config.options = {}
if (Object.keys(this.config.options).length < 1) return this
for (const i in this.settings) {
for (const [name, option] of Object.entries(this.config.options)) {
// Don't overwrite user-provided settings.options
if (typeof this.settings[i].options[name] === 'undefined') {
if (typeof option === 'object') {
if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100
else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm
else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg
else if (typeof option.count !== 'undefined')
this.settings[i].options[name] = option.count
else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool
else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt
else {
let err = 'Unknown option type: ' + JSON.stringify(option)
this.store.log.error(err)
throw new Error(err)
}
} else this.settings[i].options[name] = option
}
}
}
return this
}
/**
* Loads a plugin
*
@ -1049,8 +795,8 @@ Pattern.prototype.__needs = function (partName, set = 0) {
// Walk the only parts, checking each one for a match in its dependencies
for (const part of only) {
if (part === partName) return true
if (this.__resolvedDependencies[part]) {
for (const dependency of this.__resolvedDependencies[part]) {
if (this.config.resolvedDependencies[part]) {
for (const dependency of this.config.resolvedDependencies[part]) {
if (dependency === partName) return true
}
}
@ -1183,117 +929,11 @@ Pattern.prototype.__pack = function () {
return this
}
/**
* Resolves the draft order based on the configuation
*
* @private
* @param {object} graph - The object of resolved dependencies, used to call itself recursively
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDependencies) {
const sorted = Object.keys(this.__designParts).sort(
(p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1]
)
this.__draftOrder = sorted
this.config.draftOrder = sorted
Pattern.prototype.__resolveConfig = function () {
this.config = this.__configResolver.asConfig()
return this
}
Pattern.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) {
const part = depChain[0]
this.__designParts[dependency.name] = Object.freeze(dependency)
this.__addDependency('__dependencies', part, dependency)
depChain.forEach((c) => this.__addDependency('__resolvedDependencies', c, dependency))
switch (depType) {
case 'from':
this.__setFromHide(part, part.name, dependency.name)
this.__inject[part.name] = dependency.name
break
case 'after':
this.__setAfterHide(part, part.name, dependency.name)
}
}
Pattern.prototype.__resolveMutatedPartDistance = function (partName) {
const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1
let didChange = false
if (!this.__dependencies[partName]) return false
this.__dependencies[partName].forEach((dependency) => {
if (
typeof this.__mutated.partDistance[dependency] === 'undefined' ||
this.__mutated.partDistance[dependency] < proposed_dependent_part_distance
) {
didChange = true
this.__mutated.partDistance[dependency] = proposed_dependent_part_distance
this.__resolveMutatedPartDistance(dependency)
}
if (DISTANCE_DEBUG)
this.store.log.debug(
`"${depType}:" partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__`
)
})
return didChange
}
const depTypes = ['from', 'after']
Pattern.prototype.__resolvePartDependencies = function (depChain, distance) {
// Resolve part Dependencies. first from then after
const part = depChain[0]
this.__resolvedDependencies[part.name] = this.__resolvedDependencies[part.name] || []
depTypes.forEach((d) => {
if (part[d]) {
if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`)
const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]]
depsOfType.forEach((dot) => {
let count = Object.keys(this.__designParts).length
// if any changes resulted from resolving this part mutation
this.__resolvePartDependencyChain(depChain, dot, d)
// if a new part was added, resolve the part
const newCount = Object.keys(this.__designParts).length
if (count < newCount) {
this.__resolvePart([dot, ...depChain], distance)
count = newCount
}
})
}
})
this.__resolveMutatedPartDistance(part.name)
}
Pattern.prototype.__resolvePart = function (depChain, distance = 0) {
const part = depChain[0]
if (distance === 0) {
this.__designParts[part.name] = Object.freeze(part)
}
distance++
if (typeof this.__mutated.partDistance[part.name] === 'undefined') {
this.__mutated.partDistance[part.name] = distance
if (DISTANCE_DEBUG)
this.store.log.debug(
`Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__`
)
}
// Hide when hideAll is set
if (part.hideAll) {
this.__mutated.partHide[part.name] = true
}
this.__resolvePartDependencies(depChain, distance)
// add the part's config
this.__addPartConfig(part)
}
/**
* Resolves parts and their dependencies
*
@ -1302,35 +942,15 @@ Pattern.prototype.__resolvePart = function (depChain, distance = 0) {
* @param {int} distance - Keeps track of how far the dependency is from the pattern
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
for (const part of this.designConfig.parts) {
this.__resolvePart([part], distance)
}
Pattern.prototype.__resolveParts = function () {
this.designConfig.parts.forEach((p) => this.__configResolver.addPart(p))
// Print final part distances.
for (const part of this.designConfig.parts) {
let qualifier = DISTANCE_DEBUG ? 'final' : ''
this.store.log.debug(
`⚪️ \`${part.name}\` ${qualifier} options priority is __${
this.__mutated.partDistance[part.name]
}__`
)
}
this.__configResolver.logPartDistances()
return this
}
/**
* Resolves parts depdendencies into a flat array
*
* @private
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveDependencies = function () {
this.config.resolvedDependencies = this.__resolvedDependencies
return this
}
/**
* Runs subscriptions to a given lifecycle hook
*
@ -1364,49 +984,6 @@ Pattern.prototype.__setBase = function () {
}
}
/**
* Sets visibility of a dependency based on its config
*
* @private
* @param {Part} part - The part of which this is a dependency
* @param {string} name - The name of the part
* @param {string} depName - The name of the dependency
* @param {int} set - The index of the set in the list of settings
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__setFromHide = function (part, name, depName) {
if (
part.hideDependencies ||
part.hideAll ||
this.__mutated.partHide[name] ||
this.__mutated.partHideAll[name]
) {
this.__mutated.partHide[depName] = true
this.__mutated.partHideAll[depName] = true
}
return this
}
/**
* Sets visibility of an 'after' dependency based on its config
*
* @private
* @param {Part} part - The part of which this is a dependency
* @param {string} name - The name of the part
* @param {string} depName - The name of the dependency
* @param {int} set - The index of the set in the list of settings
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__setAfterHide = function (part, name, depName) {
if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) {
this.__mutated.partHide[depName] = true
this.__mutated.partHideAll[depName] = true
}
return this
}
/**
* Returns the absolute value of a snapped percentage option
*
@ -1493,22 +1070,3 @@ Pattern.prototype.__wants = function (partName, set = 0) {
return true
}
//////////////////////////////////////////////
// STATIC PRIVATE FUNCTIONS //
//////////////////////////////////////////////
/**
* Merges dependencies into a flat list
*
* @private
* @param {array} dep - New dependencies
* @param {array} current - Current dependencies
* @return {array} deps - Merged dependencies
*/
function mergeDependencies(dep = [], current = []) {
// Current dependencies
const list = [].concat(current, dep)
return [...new Set(list)]
}

View file

@ -0,0 +1,419 @@
import { __addNonEnumProp } from './utils.mjs'
export function getPluginName(plugin) {
if (Array.isArray(plugin)) {
if (plugin[0].name) return plugin[0].name
if (plugin[0].plugin.name) return plugin[0].plugin.name
} else {
if (plugin.name) return plugin.name
if (plugin.plugin?.name) return plugin.plugin.name
}
return false
}
export function PatternConfig(pattern) {
this.pattern = pattern
this.store = pattern.store
__addNonEnumProp(this, 'plugins', { ...(pattern.designConfig.plugins || {}) })
__addNonEnumProp(this, 'options', { ...(pattern.designConfig.options || {}) })
__addNonEnumProp(this, 'measurements', [...(pattern.designConfig.measurements || [])])
__addNonEnumProp(this, 'optionalMeasurements', [
...(pattern.designConfig.optionalMeasurements || []),
])
__addNonEnumProp(this, 'inject', {})
__addNonEnumProp(this, 'directDependencies', {})
__addNonEnumProp(this, 'resolvedDependencies', {})
__addNonEnumProp(this, 'parts', {})
__addNonEnumProp(this, '__resolvedParts', {})
__addNonEnumProp(this, '__mutated', {
optionDistance: {},
partDistance: {},
partHide: {},
partHideAll: {},
})
}
const DISTANCE_DEBUG = false
PatternConfig.prototype.validatePart = 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
}
PatternConfig.prototype.addPart = function (part) {
if (this.validatePart(part)) this.__resolvePart([part])
return this
}
PatternConfig.prototype.logPartDistances = function () {
for (const partName in this.parts) {
let qualifier = DISTANCE_DEBUG ? 'final' : ''
this.store.log.debug(
`⚪️ \`${partName}\` ${qualifier} options priority is __${this.__mutated.partDistance[partName]}__`
)
}
}
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.__resolveDraftOrder(),
partHide: this.__mutated.partHide,
partHideAll: this.__mutated.partHideAll,
}
}
PatternConfig.prototype.__resolvePart = function (depChain, distance = 0) {
const part = depChain[0]
if (distance === 0) {
this.parts[part.name] = Object.freeze(part)
}
distance++
if (typeof this.__mutated.partDistance[part.name] === 'undefined') {
this.__mutated.partDistance[part.name] = distance
if (DISTANCE_DEBUG)
this.store.log.debug(
`Base partDistance for \`${part.name}\` is __${this.__mutated.partDistance[part.name]}__`
)
}
// Hide when hideAll is set
if (part.hideAll) {
this.__mutated.partHide[part.name] = true
}
this.__resolvePartDependencies(depChain, distance)
// add the part's config
this.__addPartConfig(part)
}
/**
* Resolves/Adds a part's design configuration to the pattern config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @param {onject} config - The global config
* @param {Store} store - The store, used for logging
* @return {object} config - The mutated global config
*/
PatternConfig.prototype.__addPartConfig = function (part) {
if (this.__resolvedParts[part.name]) return this
// Add parts, using set to keep them unique in the array
// this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)]
return this.__addPartOptions(part)
.__addPartMeasurements(part, true)
.__addPartMeasurements(part, false)
.__addPartPlugins(part)
}
/**
* Resolves/Adds a part's configured options to the global config
*
* @private
* @param {Part} part - The part of which to resolve the config
* @return {Pattern} this - The Pattern instance
*/
PatternConfig.prototype.__addPartOptions = function (part) {
if (!part.options) return this
const partDistance = this.__mutated.partDistance?.[part.name] || 0
for (const optionName in part.options) {
const option = part.options[optionName]
const optionDistance = this.__mutated.optionDistance[optionName]
if (optionDistance && DISTANCE_DEBUG)
this.store.log.debug(
`optionDistance for __${optionName}__ is __${optionDistance}__ and partDistance for \`${part.name}\` is __${partDistance}__`
)
if (!optionDistance || optionDistance > partDistance) {
this.__mutated.optionDistance[optionName] = partDistance
// Keep design parts immutable in the pattern or risk subtle bugs
this.options[optionName] = Object.freeze(option)
this.store.log.debug(
optionDistance
? `🟣 __${optionName}__ option overwritten by \`${part.name}\``
: `🔵 __${optionName}__ option loaded from part \`${part.name}\``
)
this.__loadOptionDefault(optionName, option)
}
}
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 {array} list - The list of resolved measurements
* @return {Pattern} this - The Pattern instance
*/
PatternConfig.prototype.__addPartMeasurements = function (part, optional = false) {
const listType = optional ? 'optionalMeasurements' : 'measurements'
if (part[listType]) {
part[listType].forEach((m) => {
const isInReqList = this.measurements.indexOf(m) !== -1
const optInd = this.optionalMeasurements.indexOf(m)
const isInOptList = optInd !== -1
if (isInReqList) return
if (optional && !isInOptList) this.optionalMeasurements.push(m)
if (!optional) {
this.measurements.push(m)
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 {Pattern} this - The Pattern 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
}
PatternConfig.prototype.__loadOptionDefault = function (optionName, option) {
this.pattern.settings.forEach((set) => {
if (typeof set.options[optionName] !== 'undefined') return
if (typeof option === 'object') {
if (typeof option.pct !== 'undefined') set.options[optionName] = option.pct / 100
else if (typeof option.mm !== 'undefined') set.options[optionName] = option.mm
else if (typeof option.deg !== 'undefined') set.options[optionName] = option.deg
else if (typeof option.count !== 'undefined') set.options[optionName] = option.count
else if (typeof option.bool !== 'undefined') set.options[optionName] = option.bool
else if (typeof option.dflt !== 'undefined') set.options[optionName] = option.dflt
else {
let err = 'Unknown option type: ' + JSON.stringify(option)
this.store.log.error(err)
throw new Error(err)
}
} else set.options[optionName] = option
})
}
PatternConfig.prototype.__resolvePartDependencyChain = function (depChain, dependency, depType) {
const part = depChain[0]
this.parts[dependency.name] = Object.freeze(dependency)
this.__addDependency('directDependencies', part, dependency)
depChain.forEach((c) => this.__addDependency('resolvedDependencies', c, dependency))
switch (depType) {
case 'from':
this.__setFromHide(part, part.name, dependency.name)
this.inject[part.name] = dependency.name
break
case 'after':
this.__setAfterHide(part, part.name, dependency.name)
}
}
PatternConfig.prototype.__resolveMutatedPartDistance = function (partName) {
const proposed_dependent_part_distance = this.__mutated.partDistance[partName] + 1
let didChange = false
if (!this.directDependencies[partName]) return false
this.directDependencies[partName].forEach((dependency) => {
if (
typeof this.__mutated.partDistance[dependency] === 'undefined' ||
this.__mutated.partDistance[dependency] < proposed_dependent_part_distance
) {
didChange = true
this.__mutated.partDistance[dependency] = proposed_dependent_part_distance
this.__resolveMutatedPartDistance(dependency)
}
if (DISTANCE_DEBUG)
this.store.log.debug(
`partDistance for \`${dependency}\` is __${this.__mutated.partDistance[dependency]}__`
)
})
return didChange
}
const depTypes = ['from', 'after']
PatternConfig.prototype.__resolvePartDependencies = function (depChain, distance) {
// Resolve part Dependencies. first from then after
const part = depChain[0]
this.resolvedDependencies[part.name] = this.resolvedDependencies[part.name] || []
depTypes.forEach((d) => {
if (part[d]) {
if (DISTANCE_DEBUG) this.store.log.debug(`Processing \`${part.name}\` "${d}:"`)
const depsOfType = Array.isArray(part[d]) ? part[d] : [part[d]]
depsOfType.forEach((dot) => {
let count = Object.keys(this.parts).length
// if any changes resulted from resolving this part mutation
this.__resolvePartDependencyChain(depChain, dot, d)
// if a new part was added, resolve the part
const newCount = Object.keys(this.parts).length
if (count < newCount) {
this.__resolvePart([dot, ...depChain], distance)
count = newCount
}
})
}
})
this.__resolveMutatedPartDistance(part.name)
}
/**
* Adds a part as a simple dependency
*
* @private
* @param {string} name - The name of the dependency
* @param {object} part - The part configuration
* @param {object} dep - The dependency configuration
* @return {object} this - The Pattern instance
*/
PatternConfig.prototype.__addDependency = function (dependencyList, part, dep) {
this[dependencyList][part.name] = this[dependencyList][part.name] || []
if (dependencyList == 'resolvedDependencies' && DISTANCE_DEBUG)
this.store.log.debug(`add ${dep.name} to ${part.name} dependencyResolution`)
if (this[dependencyList][part.name].indexOf(dep.name) === -1)
this[dependencyList][part.name].push(dep.name)
}
/**
* Resolves the draft order based on the configuation
*
* @private
* @param {object} graph - The object of resolved dependencies, used to call itself recursively
* @return {Pattern} this - The Pattern instance
*/
PatternConfig.prototype.__resolveDraftOrder = function () {
this.__draftOrder = Object.keys(this.parts).sort(
(p1, p2) => this.__mutated.partDistance[p2] - this.__mutated.partDistance[p1]
)
return this.__draftOrder
}
/**
* Sets visibility of a dependency based on its config
*
* @private
* @param {Part} part - The part of which this is a dependency
* @param {string} name - The name of the part
* @param {string} depName - The name of the dependency
* @param {int} set - The index of the set in the list of settings
* @return {Pattern} this - The Pattern instance
*/
PatternConfig.prototype.__setFromHide = function (part, name, depName) {
if (
part.hideDependencies ||
part.hideAll ||
this.__mutated.partHide[name] ||
this.__mutated.partHideAll[name]
) {
this.__mutated.partHide[depName] = true
this.__mutated.partHideAll[depName] = true
}
return this
}
/**
* Sets visibility of an 'after' dependency based on its config
*
* @private
* @param {Part} part - The part of which this is a dependency
* @param {string} name - The name of the part
* @param {string} depName - The name of the dependency
* @param {int} set - The index of the set in the list of settings
* @return {Pattern} this - The Pattern instance
*/
PatternConfig.prototype.__setAfterHide = function (part, name, depName) {
if (this.__mutated.partHide[name] || this.__mutated.partHideAll[name]) {
this.__mutated.partHide[depName] = true
this.__mutated.partHideAll[depName] = true
}
return this
}

View file

@ -32,12 +32,12 @@ describe('Pattern', () => {
expect(typeof pattern.Snippet).to.equal('function')
expect(typeof pattern.Attributes).to.equal('function')
expect(typeof pattern.macros).to.equal('object')
expect(typeof pattern.__designParts).to.equal('object')
expect(typeof pattern.__inject).to.equal('object')
expect(typeof pattern.__dependencies).to.equal('object')
expect(typeof pattern.__resolvedDependencies).to.equal('object')
expect(typeof pattern.__hide).to.equal('object')
expect(Array.isArray(pattern.__draftOrder)).to.equal(true)
// expect(typeof pattern.__designParts).to.equal('object')
// expect(typeof pattern.config.inject).to.equal('object')
// expect(typeof pattern.config.directDependencies).to.equal('object')
// expect(typeof pattern.__resolvedDependencies).to.equal('object')
// expect(typeof pattern.__hide).to.equal('object')
// expect(Array.isArray(pattern.__draftOrder)).to.equal(true)
expect(pattern.width).to.equal(0)
expect(pattern.height).to.equal(0)
expect(pattern.is).to.equal('')
@ -145,7 +145,7 @@ describe('Pattern', () => {
})
it('Pattern.__init() should resolve parts', () => {
expect(pattern.designConfig.parts.length).to.equal(3)
expect(Object.keys(pattern.config.parts)).to.have.lengthOf(3)
})
it('Pattern.__init() should resolve plugins', () => {
@ -324,14 +324,14 @@ describe('Pattern', () => {
expect(pattern.config.options.optionR.list[1]).to.equal('green')
expect(pattern.config.options.optionR.list[2]).to.equal('blue')
// Dependencies
expect(pattern.__dependencies.partB).to.include('partA')
expect(pattern.__dependencies.partC).to.include('partB')
expect(pattern.__dependencies.partR).to.include('partC')
expect(pattern.__dependencies.partR).to.include('partA')
expect(pattern.config.directDependencies.partB).to.include('partA')
expect(pattern.config.directDependencies.partC).to.include('partB')
expect(pattern.config.directDependencies.partR).to.include('partC')
expect(pattern.config.directDependencies.partR).to.include('partA')
// Inject
expect(pattern.__inject.partB).to.equal('partA')
expect(pattern.__inject.partC).to.equal('partB')
expect(pattern.__inject.partR).to.equal('partA')
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')
@ -472,12 +472,12 @@ describe('Pattern', () => {
expect(pattern.config.options.optionD.list[1]).to.equal('green')
expect(pattern.config.options.optionD.list[2]).to.equal('blue')
// Dependencies
expect(pattern.__dependencies.partB[0]).to.equal('partA')
expect(pattern.__dependencies.partC[0]).to.equal('partB')
expect(pattern.__dependencies.partD[0]).to.equal('partC')
expect(pattern.config.directDependencies.partB[0]).to.equal('partA')
expect(pattern.config.directDependencies.partC[0]).to.equal('partB')
expect(pattern.config.directDependencies.partD[0]).to.equal('partC')
// Inject
expect(pattern.__inject.partB).to.equal('partA')
expect(pattern.__inject.partC).to.equal('partB')
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')

View file

@ -12,31 +12,31 @@ describe('Pattern', () => {
const part2 = {
name: 'test2',
from: part1,
after: part1,
draft: ({ part }) => part,
}
const part3 = {
name: 'test3',
after: part2,
from: part2,
draft: ({ part }) => part,
}
describe('with resolveImmediately: true', () => {
it('Should add the part to the internal part object', () => {
it('Should add the part to parts object', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
pattern.addPart(part2, true)
expect(pattern.__designParts.test2).to.equal(part2)
expect(pattern.config.parts.test2).to.equal(part2)
})
it('Should resolve injected dependencies for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
pattern.addPart(part2, true)
expect(pattern.__inject.test2).to.equal('test')
pattern.addPart(part3, true)
expect(pattern.config.inject.test3).to.equal('test2')
})
it('Should resolve all dependencies for the new part', () => {
@ -45,7 +45,7 @@ describe('Pattern', () => {
pattern.__init()
pattern.addPart(part3, true)
expect(pattern.config.resolvedDependencies.test3).to.have.members(['test', 'test2'])
expect(pattern.__designParts.test2).to.equal(part2)
expect(pattern.config.parts.test2).to.equal(part2)
})
it('Should add a the measurements for the new part', () => {
@ -79,7 +79,7 @@ describe('Pattern', () => {
expect(pattern.config.plugins.testPlugin).to.equal(plugin)
})
it('Should add the options for the new part', () => {
it('Should resolve the options for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
@ -96,6 +96,61 @@ describe('Pattern', () => {
pattern.addPart(part2, true)
expect(pattern.config.options.opt1).to.equal(opt1)
})
it('Should resolve the dependency options for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
const opt1 = { pct: 10, min: 0, max: 50 }
const part2 = {
name: 'test2',
options: {
opt1,
},
draft: ({ part }) => part,
}
const part3 = {
name: 'test3',
from: part2,
draft: ({ part }) => part,
}
pattern.addPart(part3, true)
expect(pattern.config.options.opt1).to.equal(opt1)
})
it('Should resolve the overwritten options for the new part', () => {
const design = new Design({ parts: [part1] })
const pattern = new design()
pattern.__init()
const opt1 = { pct: 10, min: 0, max: 50 }
const part2 = {
name: 'test2',
options: {
opt1: { pct: 15, min: 10, max: 55 },
},
draft: ({ part }) => part,
}
const part3 = {
name: 'test3',
from: part2,
options: {
opt1,
},
draft: ({ part }) => part,
}
pattern.addPart(part3, true)
expect(pattern.config.options.opt1).to.equal(opt1)
})
})
describe('with resolveImmediately: false', () => {
it('does not create duplications in the configuration')
})
})
})

View file

@ -42,6 +42,7 @@ describe('Snapped options', () => {
snap: [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144],
},
},
draft: ({ part }) => part,
}
const design = new Design({ parts: [part] })
const patternA = new design({ options: { test: 0.13 }, measurements }).draft()
@ -67,6 +68,7 @@ describe('Snapped options', () => {
},
},
},
draft: ({ part }) => part,
}
const design = new Design({ parts: [part] })
const patternA = new design({ options: { test: 0.13 }, measurements, units: 'metric' }).draft()
@ -94,6 +96,7 @@ describe('Snapped options', () => {
},
},
},
draft: ({ part }) => part,
}
const design = new Design({ parts: [part] })
const patternA = new design({