1
0
Fork 0

wip(core): Ensure imutable design parts and options

This commit is contained in:
joostdecock 2022-09-24 18:37:49 +02:00
parent 485492e452
commit 12c5c6a3f8
11 changed files with 298 additions and 233 deletions

View file

@ -26,7 +26,6 @@ import { __loadPatternDefaults } from './config.mjs'
export function Pattern(designConfig) {
// Non-enumerable properties
__addNonEnumProp(this, 'plugins', {})
__addNonEnumProp(this, 'parts', [{}])
__addNonEnumProp(this, 'width', 0)
__addNonEnumProp(this, 'height', 0)
__addNonEnumProp(this, 'autoLayout', { stacks: {} })
@ -37,17 +36,24 @@ export function Pattern(designConfig) {
__addNonEnumProp(this, 'Snippet', Snippet)
__addNonEnumProp(this, 'Attributes', Attributes)
__addNonEnumProp(this, 'macros', {})
__addNonEnumProp(this, '__parts', {})
__addNonEnumProp(this, '__designParts', {})
__addNonEnumProp(this, '__inject', {})
__addNonEnumProp(this, '__dependencies', {})
__addNonEnumProp(this, '__resolvedDependencies', {})
__addNonEnumProp(this, '__resolvedParts', [])
__addNonEnumProp(this, '__storeMethods', new Set())
__addNonEnumProp(this, '__mutated', {
optionDistance: {},
partDistance: {},
partHide: {},
partHideAll: {},
})
__addNonEnumProp(this, '__draftOrder', [])
__addNonEnumProp(this, '__hide', {})
// Enumerable properties
this.designConfig = designConfig // The design configuration (unresolved)
this.config = {} // Will hold the resolved pattern after calling __init()
this.stacks = {} // Drafted stacks container
this.store = new Store() // Pattern-wide store
this.setStores = [] // Per-set stores
@ -81,10 +87,13 @@ Pattern.prototype.addPart = function (part) {
Pattern.prototype.draft = function () {
this.__init()
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}`)
@ -99,6 +108,7 @@ Pattern.prototype.draft = function () {
// 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.__inject[partName] === 'string') {
this.setStores[set].log.debug(
@ -115,11 +125,11 @@ Pattern.prototype.draft = function () {
}
if (this.__needs(partName, set)) {
// Draft part
if (typeof this.__parts?.[partName]?.draft === 'function') {
if (typeof this.__designParts?.[partName]?.draft === 'function') {
this.activePart = partName
try {
this.__runHooks('prePartDraft')
const result = this.__parts[partName].draft(this.parts[set][partName].shorthand())
const result = this.__designParts[partName].draft(this.parts[set][partName].shorthand())
this.__runHooks('postPartDraft')
if (typeof result === 'undefined') {
this.setStores[set].log.error(
@ -129,7 +139,7 @@ Pattern.prototype.draft = function () {
} 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.draft() is not callable`)
} else this.setStores[set].log.error(`Unable to draft pattern part __${partName}__. Part.draft() is not callable`)
this.parts[set][partName].hidden =
this.parts[set][partName].hidden === true ? true : !this.__wants(partName, set)
} else {
@ -183,26 +193,23 @@ Pattern.prototype.getRenderProps = function () {
warning: store.logs.warning,
}))
}
props.parts = {}
for (let p in this.parts) {
if (!this.parts[p].hidden) {
props.parts[p] = {
paths: this.parts[p].paths,
points: this.parts[p].points,
snippets: this.parts[p].snippets,
attributes: this.parts[p].attributes,
height: this.parts[p].height,
width: this.parts[p].width,
bottomRight: this.parts[p].bottomRight,
topLeft: this.parts[p].topLeft,
store: this.setStores[this.parts[p].set],
props.parts = []
for (const set of this.parts) {
const setParts = {}
for (let p in set) {
if (!set[p].hidden) {
setParts[p] = {
...set[p].asProps(),
store: this.setStores[set[p].set],
}
}
}
props.parts.push(setParts)
}
props.stacks = {}
for (let s in this.stacks) {
if (!this.__isStackHidden(s)) {
props.stacks[s] = this.stacks[s]
props.stacks[s] = this.stacks[s].asProps()
}
}
@ -308,12 +315,8 @@ Pattern.prototype.render = function () {
* @return {object} this - The Pattern instance
*/
Pattern.prototype.use = function (plugin, data) {
const name = plugin.plugin
? plugin.plugin.name
: plugin.name
console.log('@@@@@@@@@ in use', name, plugin, data)
if (!this.plugins?.[name])
return (plugin.plugin && plugin.condition)
const name = getPluginName(plugin)
if (!this.plugins?.[name]) return (plugin.plugin && plugin.condition)
? this.__useIf(plugin, data) // Conditional plugin
: this.__loadPlugin(plugin, data) // Regular plugin
@ -339,8 +342,8 @@ Pattern.prototype.use = function (plugin, data) {
*/
Pattern.prototype.__addDependency = function (name, part, dep) {
this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name])
if (typeof this.__parts[dep.name] === 'undefined') {
this.config = this.__addPartConfig(this.__parts[dep.name])
if (typeof this.__designParts[dep.name] === 'undefined') {
this.config = this.__addPartConfig(this.__designParts[dep.name])
}
return this
@ -356,9 +359,10 @@ Pattern.prototype.__addDependency = function (name, part, dep) {
* @return {object} config - The mutated global config
*/
Pattern.prototype.__addPartConfig = function (part) {
if (this.__parts[part.name].resolved) return config
if (this.__resolvedParts.includes(part.name)) return this
// Add parts, using set to keep them unique in the array
this.__parts[part.name].resolved = true
this.designConfig.parts = [...new Set(this.designConfig.parts).add(part)]
return this.__addPartOptions(part)
@ -442,15 +446,15 @@ Pattern.prototype.__addPartOptionalMeasurements = function (part, list=false) {
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartOptions = function (part) {
if (!this.config.optionDistance) this.config.optionDistance = {}
if (!this.config.options) this.config.options = {}
if (part.options) {
for (const optionName in part.options) {
if (!this.config.optionDistance[optionName]) {
this.config.optionDistance[optionName] = part.distance
this.config.options[optionName] = part.options[optionName]
if (!this.__mutated.optionDistance[optionName]) {
this.__mutated.optionDistance[optionName] = this.__mutated.partDistance?.[part.name] || 0
// 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.name}\``)
} else if (this.config.optionDistance[optionName] > part.distance) {
} else if (this.__mutated.optionDistance[optionName] > this.__mutated.partDistance[part.name]) {
this.config.options[optionName] = part.options[optionName]
this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
}
@ -466,6 +470,18 @@ Pattern.prototype.__addPartOptions = function (part) {
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
*
@ -474,44 +490,63 @@ Pattern.prototype.__addPartOptions = function (part) {
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__addPartPlugins = function (part) {
if (!this.config.plugins) this.config.plugins = []
const plugins = {}
if (!this.config.plugins) this.config.plugins = {}
const plugins = { ...this.config.plugins }
if (!part.plugins) return this
for (const plugin of part.plugins) plugins[plugin.name] = plugin
if (!Array.isArray(part.plugins)) part.plugins = [part.plugins]
for (let plugin of part.plugins) {
// Side-step immutability of the part object to ensure plugins is an array
let partPlugins = part.plugins
if (!Array.isArray(partPlugins)) partPlugins = [partPlugins]
for (const plugin of partPlugins) plugins[getPluginName(plugin)] = plugin
for (let plugin of partPlugins) {
const name = getPluginName(plugin)
// Handle [plugin, data] scenario
if (Array.isArray(plugin)) {
const pluginObj = { ...plugin[0], data: plugin[1] }
plugin = pluginObj
}
if (plugin.plugin) this.store.log.debug(`🔌 Resolved __${plugin.plugin.name}__ conditional plugin in \`${part.name}\``)
else this.store.log.debug(`🔌 Resolved __${plugin.name}__ plugin in \`${part.name}\``)
if (plugin.plugin) this.store.log.debug(`🔌 Resolved __${name}__ conditional plugin in \`${part.name}\``)
else this.store.log.debug(`🔌 Resolved __${name}__ plugin in \`${part.name}\``)
// Do not overwrite an existing plugin with a conditional plugin unless it is also conditional
if (plugin.plugin && plugin.condition) {
if (!plugins[plugin.plugin.name]) {
plugins[plugin.plugin.name] = plugin
this.store.log.info(`Plugin \`${plugin.plugin.name}\` was conditionally added.`)
if (!plugins[name]) {
plugins[name] = plugin
this.store.log.info(`Plugin \`${name}\` was conditionally added.`)
}
else if (plugins[plugin.plugin.name]?.condition) {
plugins[plugin.plugin.name+'_'] = plugin
this.store.log.info(`Plugin \`${plugin.plugin.name}\` was conditionally added again. Renaming to ${plugin.plugin.name}_.`)
else 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 \`${plugin.plugin.name}\` was requested conditionally, but is already added explicitly. Not loading.`
`Plugin \`${name}\` was requested conditionally, but is already added explicitly. Not loading.`
)
} else {
plugins[plugin.name] = plugin
this.store.log.info(`Plugin \`${plugin.name}\` was added.`)
plugins[name] = plugin
this.store.log.info(`Plugin \`${name}\` was added.`)
}
}
// Weed out doubles
this.config.plugins = [...new Set(Object.values(plugins))]
this.config.plugins = {...plugins }
return this
}
/**
* Creates a store for a set (of settings)
*
* @private
* @return {Store} store - A new store populated with relevant data/methods
*/
Pattern.prototype.__createSetStore = function () {
const store = new Store()
store.set('data', this.store.data)
for (const method of [...this.__storeMethods]) {
store.set(method[0], (...args) => method[1](store, ...args))
}
return store
}
/**
* Merges (sets of) settings with the default settings
*
@ -525,7 +560,6 @@ Pattern.prototype.__applySettings = function (sets) {
this.settings = []
for (const set in sets) {
this.settings.push({ ...__loadPatternDefaults(), ...sets[set] })
this.setStores.push(new Store())
}
return this
@ -544,7 +578,7 @@ Pattern.prototype.__createPartWithContext = function (name, set) {
const part = new Part()
part.name = name
part.set = set
part.stack = this.__parts[name]?.stack || name
part.stack = this.__designParts[name]?.stack || name
part.context = {
parts: this.parts[set],
config: this.config,
@ -552,6 +586,7 @@ Pattern.prototype.__createPartWithContext = function (name, set) {
store: this.setStores[set],
macros: this.macros,
}
if (this.settings[set]?.partClasses) {
part.attr('class', this.settings[set].partClasses)
}
@ -617,8 +652,8 @@ Pattern.prototype.__init = function () {
.__resolveDependencies() // Resolves dependencies
.__resolveDraftOrder() // Resolves draft order
.__loadPlugins() // Loads plugins
.__filterOptionalMeasurements() // Removes required m's from optional list
.__loadConfigData() // Makes config data available in store
.__filterOptionalMeasurements() // Removes required m's from optional list
.__loadOptionDefaults() // Merges default options with user provided ones
// Say hello
@ -646,8 +681,8 @@ 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.__parts?.[partName]?.hide) return true
if (this.__parts?.[partName]?.hideAll) return true
if (this.__designParts?.[partName]?.hide) return true
if (this.__designParts?.[partName]?.hideAll) return true
return false
}
@ -669,8 +704,8 @@ Pattern.prototype.__isStackHidden = function (stackName) {
return true
}
for (const partName of parts) {
if (this.__parts?.[partName]?.hide) return true
if (this.__parts?.[partName]?.hideAll) return true
if (this.__designParts?.[partName]?.hide) return true
if (this.__designParts?.[partName]?.hideAll) return true
}
return false
@ -740,9 +775,7 @@ Pattern.prototype.__loadAbsoluteOptionsSet = function (set) {
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__loadConfigData = function () {
if (this.designConfig.data) {
for (const i in this.settings) this.setStores[i].set('data', this.designConfig.data)
}
if (this.designConfig.data) this.store.set('data', this.designConfig.data)
return this
}
@ -844,7 +877,10 @@ Pattern.prototype.__loadPluginMacros = function (plugin) {
*/
Pattern.prototype.__loadPlugins = function () {
if (!this.config.plugins) return this
for (const plugin of this.config.plugins) this.use(plugin, plugin.data)
for (const plugin in this.config.plugins) this.use(
this.config.plugins[plugin],
this.config.plugins[plugin]?.data,
)
return this
}
@ -858,8 +894,11 @@ Pattern.prototype.__loadPlugins = function () {
*/
Pattern.prototype.__loadPluginStoreMethods = function (plugin) {
if (Array.isArray(plugin.store)) {
for (const store of this.setStores) store.extend(plugin.store)
} else this.store.log.warning(`Plugin store methods should be an Array`)
for (const method of plugin.store) this.__storeMethods.add(method)
}
else this.store.log.warning(`Plugin store methods should be an Array`)
return this
}
/**
@ -1143,7 +1182,7 @@ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDepende
})
// Don't forget about parts without dependencies
for (const part in this.__parts) {
for (const part in this.__designParts) {
if (sorted.indexOf(part) === -1) sorted.push(part)
}
@ -1164,49 +1203,53 @@ Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDepende
Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
if (count === 0) {
for (const part of this.designConfig.parts) {
part.distance = distance
this.__parts[part.name] = part
// Keep design parts immutable in the pattern or risk subtle bugs
this.__designParts[part.name] = Object.freeze(part)
}
}
distance++
for (const part of this.designConfig.parts) {
if (typeof part.distance === 'undefined') part.distance = distance
if (typeof this.__mutated.partDistance[part.name] === 'undefined') this.__mutated.partDistance[part.name] = distance
}
for (const [name, part] of Object.entries(this.__parts)) {
for (const [name, part] of Object.entries(this.__designParts)) {
// Hide when hideAll is set
if (part.hideAll) part.hide = true
// Inject (from)
if (part.from) {
if (part.hideDependencies || part.hideAll) {
part.from.hide = true
part.from.hideAll = true
part.from.distance = distance
// Don't mutate the part, keep this info in the pattern object
this.__mutated.partHide[from.name] = true
this.__mutated.partHideAll[from.name] = true
this.__mutated.partDistance = distance
}
this.__parts[part.from.name] = part.from
this.__designParts[part.from.name] = part.from
this.__inject[name] = part.from.name
}
// Simple dependency (after)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) {
dep.distance = distance
this.__parts[dep.name] = dep
// Don't mutate the part, keep this info in the pattern object
this.__mutated.partDistance[dep.name] = distance
this.__designParts[dep.name] = dep
this.__addDependency(name, part, dep)
}
} else {
if (part.hideDependencies) part.after.hide = true
part.after.distance = distance
this.__parts[part.after.name] = part.after
if (part.hideDependencies) {
this.__mutated.partHide[part.after.name] = true
}
this.__mutated.partDistance[part.after.name] = distance
this.__designParts[part.after.name] = part.after
this.__addDependency(name, part, part.after)
}
}
}
// Did we discover any new dependencies?
const len = Object.keys(this.__parts).length
const len = Object.keys(this.__designParts).length
// If so, resolve recursively
if (len > count) return this.__resolveParts(len, distance)
for (const part of Object.values(this.__parts)) this.__addPartConfig(part)
for (const part of Object.values(this.__designParts)) this.__addPartConfig(part)
return this
}
@ -1291,20 +1334,21 @@ Pattern.prototype.__setBase = function () {
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
if (!Array.isArray(conf.snap) && conf.snap.metric && conf.snap.imperial)
conf.snap = conf.snap[this.settings[set].units]
// 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 conf.snap === 'number') return Math.ceil(abs / conf.snap) * conf.snap
if (typeof snapConf === 'number') return Math.ceil(abs / snapConf) * snapConf
// List of snaps
if (Array.isArray(conf.snap) && conf.snap.length > 1) {
for (const snap of conf.snap
if (Array.isArray(snapConf) && snapConf.length > 1) {
for (const snap of snapConf
.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
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,
@ -1326,7 +1370,6 @@ Pattern.prototype.__snappedPercentageOption = function (optionName, set) {
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__useIf = function (plugin) {
console.log(')_________', plugin)
let load = 0
for (const set of this.settings) {
if (plugin.condition(set)) load++