2022-08-28 02:14:39 +02:00
|
|
|
import { Attributes } from './attributes.mjs'
|
2022-08-26 18:51:02 +02:00
|
|
|
import pack from 'bin-pack'
|
2022-09-24 12:44:41 +02:00
|
|
|
import { __addNonEnumProp, __macroName } from './utils.mjs'
|
2022-08-28 02:14:39 +02:00
|
|
|
import { Part } from './part.mjs'
|
2022-09-12 20:10:22 +02:00
|
|
|
import { Stack } from './stack.mjs'
|
2022-08-28 02:14:39 +02:00
|
|
|
import { Point } from './point.mjs'
|
|
|
|
import { Path } from './path.mjs'
|
|
|
|
import { Snippet } from './snippet.mjs'
|
|
|
|
import { Svg } from './svg.mjs'
|
|
|
|
import { Store } from './store.mjs'
|
|
|
|
import { Hooks } from './hooks.mjs'
|
2022-08-31 20:21:27 +02:00
|
|
|
import { version } from '../data.mjs'
|
2022-09-18 15:11:10 +02:00
|
|
|
import { __loadPatternDefaults } from './config.mjs'
|
2022-11-14 14:01:50 -06:00
|
|
|
import cloneDeep from 'lodash.clonedeep'
|
2018-07-23 11:12:06 +00:00
|
|
|
|
2023-02-20 06:08:07 +02:00
|
|
|
const DISTANCE_DEBUG = true
|
2022-11-11 19:18:02 -08:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
|
|
|
// CONSTRUCTOR //
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor for a Pattern
|
|
|
|
*
|
|
|
|
* @constructor
|
|
|
|
* @param {object} config - The Design config
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
2022-09-24 12:44:41 +02:00
|
|
|
export function Pattern(designConfig) {
|
2022-09-09 20:20:38 +02:00
|
|
|
// Non-enumerable properties
|
2022-09-18 15:11:10 +02:00
|
|
|
__addNonEnumProp(this, 'plugins', {})
|
|
|
|
__addNonEnumProp(this, 'width', 0)
|
|
|
|
__addNonEnumProp(this, 'height', 0)
|
|
|
|
__addNonEnumProp(this, 'autoLayout', { stacks: {} })
|
|
|
|
__addNonEnumProp(this, 'is', '')
|
|
|
|
__addNonEnumProp(this, 'hooks', new Hooks())
|
|
|
|
__addNonEnumProp(this, 'Point', Point)
|
|
|
|
__addNonEnumProp(this, 'Path', Path)
|
|
|
|
__addNonEnumProp(this, 'Snippet', Snippet)
|
|
|
|
__addNonEnumProp(this, 'Attributes', Attributes)
|
|
|
|
__addNonEnumProp(this, 'macros', {})
|
2022-09-25 11:17:36 +02:00
|
|
|
__addNonEnumProp(this, '__initialized', false)
|
2022-09-24 18:37:49 +02:00
|
|
|
__addNonEnumProp(this, '__designParts', {})
|
2022-09-18 15:11:10 +02:00
|
|
|
__addNonEnumProp(this, '__inject', {})
|
|
|
|
__addNonEnumProp(this, '__dependencies', {})
|
|
|
|
__addNonEnumProp(this, '__resolvedDependencies', {})
|
2022-09-24 18:37:49 +02:00
|
|
|
__addNonEnumProp(this, '__resolvedParts', [])
|
|
|
|
__addNonEnumProp(this, '__storeMethods', new Set())
|
|
|
|
__addNonEnumProp(this, '__mutated', {
|
|
|
|
optionDistance: {},
|
|
|
|
partDistance: {},
|
|
|
|
partHide: {},
|
|
|
|
partHideAll: {},
|
|
|
|
})
|
2022-09-18 15:11:10 +02:00
|
|
|
__addNonEnumProp(this, '__draftOrder', [])
|
|
|
|
__addNonEnumProp(this, '__hide', {})
|
2022-09-09 20:20:38 +02:00
|
|
|
|
|
|
|
// Enumerable properties
|
2022-11-14 14:01:50 -06:00
|
|
|
this.designConfig = cloneDeep(designConfig) // The design configuration (unresolved)
|
2022-09-24 12:44:41 +02:00
|
|
|
this.config = {} // Will hold the resolved pattern after calling __init()
|
|
|
|
this.store = new Store() // Pattern-wide store
|
|
|
|
this.setStores = [] // Per-set stores
|
2022-08-28 02:14:39 +02:00
|
|
|
|
2022-09-09 20:20:38 +02:00
|
|
|
return this
|
|
|
|
}
|
2022-08-28 02:14:39 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
|
|
|
// PUBLIC METHODS //
|
|
|
|
//////////////////////////////////////////////
|
2018-09-30 16:25:03 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* FIXME: Allows adding parts to the config at runtime
|
|
|
|
*
|
|
|
|
* @param {object} part - The part to add
|
|
|
|
* @return {object} this - The Pattern instance
|
2022-08-09 20:17:35 +02:00
|
|
|
*/
|
2023-02-20 06:08:07 +02:00
|
|
|
Pattern.prototype.addPart = function (part, runtime = false) {
|
2022-08-09 20:17:35 +02:00
|
|
|
if (typeof part?.draft === 'function') {
|
2022-09-25 11:17:36 +02:00
|
|
|
if (part.name) {
|
|
|
|
this.designConfig.parts.push(part)
|
2023-02-20 06:08:07 +02:00
|
|
|
if (runtime) {
|
|
|
|
} else this.__initialized = false
|
2022-09-25 11:17:36 +02:00
|
|
|
} else this.store.log.error(`Part must have a name`)
|
2022-09-24 12:44:41 +02:00
|
|
|
} else this.store.log.error(`Part must have a draft() method`)
|
2022-09-17 10:24:13 +02:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2018-07-23 20:14:32 +02:00
|
|
|
/**
|
2022-09-18 15:11:10 +02:00
|
|
|
* Drafts this pattern, aka the raison d'etre of FreeSewing
|
|
|
|
*
|
|
|
|
* @return {object} this - The Pattern instance
|
2018-07-23 20:14:32 +02:00
|
|
|
*/
|
2020-04-18 11:36:02 +02:00
|
|
|
Pattern.prototype.draft = function () {
|
2022-09-20 15:24:10 +02:00
|
|
|
this.__init()
|
2022-09-18 22:48:55 +02:00
|
|
|
this.__runHooks('preDraft')
|
2022-09-24 18:37:49 +02:00
|
|
|
// Keep container for drafted parts fresh
|
|
|
|
this.parts = []
|
2022-08-09 20:17:35 +02:00
|
|
|
|
2022-09-17 10:24:13 +02:00
|
|
|
// Iterate over the provided sets of settings (typically just one)
|
|
|
|
for (const set in this.settings) {
|
2022-09-18 22:48:55 +02:00
|
|
|
this.activeSet = set
|
2022-09-24 18:37:49 +02:00
|
|
|
this.setStores[set] = this.__createSetStore()
|
2022-09-24 12:44:41 +02:00
|
|
|
this.setStores[set].log.debug(`Initialized store for set ${set}`)
|
2022-09-18 22:48:55 +02:00
|
|
|
this.__runHooks('preSetDraft')
|
2022-09-24 12:44:41 +02:00
|
|
|
this.setStores[set].log.debug(`📐 Drafting pattern for set ${set}`)
|
2022-09-17 10:24:13 +02:00
|
|
|
|
|
|
|
// Create parts container
|
|
|
|
this.parts[set] = {}
|
|
|
|
|
|
|
|
// Handle snap for pct options
|
|
|
|
this.__loadAbsoluteOptionsSet(set)
|
|
|
|
|
|
|
|
for (const partName of this.config.draftOrder) {
|
2022-11-14 14:01:50 -06:00
|
|
|
this.createPartForSet(partName, set)
|
2018-12-16 18:06:01 +01:00
|
|
|
}
|
2022-09-18 22:48:55 +02:00
|
|
|
this.__runHooks('postSetDraft')
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
2022-09-18 22:48:55 +02:00
|
|
|
this.__runHooks('postDraft')
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-11-14 14:01:50 -06:00
|
|
|
Pattern.prototype.createPartForSet = function (partName, set = 0) {
|
2022-12-08 19:31:57 -06:00
|
|
|
// gotta protect against attacks
|
2022-12-08 20:06:22 -06:00
|
|
|
if (set === '__proto__') {
|
2022-12-08 19:31:57 -06:00
|
|
|
throw new Error('malicious attempt at altering Object.prototype. Stopping action')
|
|
|
|
}
|
2022-11-14 14:01:50 -06:00
|
|
|
// 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(
|
|
|
|
`Creating part \`${partName}\` from part \`${this.__inject[partName]}\``
|
|
|
|
)
|
|
|
|
try {
|
|
|
|
this.parts[set][partName].__inject(this.parts[set][this.__inject[partName]])
|
|
|
|
} catch (err) {
|
|
|
|
this.setStores[set].log.error([
|
|
|
|
`Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``,
|
|
|
|
err,
|
|
|
|
])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this.__needs(partName, set)) {
|
|
|
|
// Draft part
|
|
|
|
const result = this.draftPartForSet(partName, set)
|
2022-11-14 16:33:57 -06:00
|
|
|
if (typeof result !== 'undefined') this.parts[set][partName] = result
|
2022-11-14 14:01:50 -06:00
|
|
|
// 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.__designParts?.[partName]?.draft === 'function') {
|
|
|
|
this.activePart = partName
|
|
|
|
try {
|
|
|
|
this.__runHooks('prePartDraft')
|
|
|
|
const result = this.__designParts[partName].draft(this.parts[set][partName].shorthand())
|
|
|
|
this.__runHooks('postPartDraft')
|
2022-11-14 16:33:57 -06:00
|
|
|
if (typeof result === 'undefined') {
|
|
|
|
this.setStores[set].log.error(
|
|
|
|
`Result of drafting part ${partName} was undefined. Did you forget to return the part?`
|
|
|
|
)
|
|
|
|
}
|
2022-11-14 14:01:50 -06:00
|
|
|
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`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-09-18 17:01:19 +02:00
|
|
|
/**
|
|
|
|
* Return the initialized configuration
|
|
|
|
*
|
|
|
|
* @return {object} config - The initialized config
|
|
|
|
*/
|
|
|
|
Pattern.prototype.getConfig = function () {
|
2022-09-20 15:24:10 +02:00
|
|
|
return this.__init().config
|
2022-09-18 17:01:19 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/** Returns props required to render this pattern through
|
|
|
|
* an external renderer (eg. a React component)
|
|
|
|
*
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.getRenderProps = function () {
|
2022-09-28 18:07:32 +02:00
|
|
|
this.store.log.info('Gathering render props')
|
2022-09-18 15:11:10 +02:00
|
|
|
// Run pre-render hook
|
|
|
|
let svg = new Svg(this)
|
|
|
|
svg.hooks = this.hooks
|
|
|
|
|
|
|
|
this.__pack()
|
2022-11-14 14:01:50 -06:00
|
|
|
svg.__runHooks('preRender')
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
let props = { svg }
|
|
|
|
props.width = this.width
|
|
|
|
props.height = this.height
|
|
|
|
props.autoLayout = this.autoLayout
|
|
|
|
props.settings = this.settings
|
2022-09-24 18:37:49 +02:00
|
|
|
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],
|
|
|
|
}
|
2022-09-28 18:07:32 +02:00
|
|
|
} else if (this.setStores[set?.set]) {
|
|
|
|
this.setStores[set.set].log.info(
|
|
|
|
`Part${p} is hidden in set ${set.set}. Not adding to render props`
|
|
|
|
)
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
}
|
2022-09-24 18:37:49 +02:00
|
|
|
props.parts.push(setParts)
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
props.stacks = {}
|
|
|
|
for (let s in this.stacks) {
|
|
|
|
if (!this.__isStackHidden(s)) {
|
2022-09-24 18:37:49 +02:00
|
|
|
props.stacks[s] = this.stacks[s].asProps()
|
2022-09-28 18:07:32 +02:00
|
|
|
} else this.store.log.info(`Stack ${s} is hidden. Skipping in render props.`)
|
|
|
|
}
|
|
|
|
props.logs = {
|
|
|
|
pattern: this.store.logs,
|
|
|
|
sets: this.setStores.map((store) => ({
|
|
|
|
debug: store.logs.debug,
|
|
|
|
info: store.logs.info,
|
|
|
|
error: store.logs.error,
|
|
|
|
warning: store.logs.warning,
|
|
|
|
})),
|
2018-12-16 18:06:01 +01:00
|
|
|
}
|
2018-09-30 16:25:03 +02:00
|
|
|
|
2022-12-13 08:39:16 -06:00
|
|
|
svg.__runHooks('postRender')
|
2022-09-18 15:11:10 +02:00
|
|
|
return props
|
|
|
|
}
|
|
|
|
|
2018-08-09 15:10:15 +02:00
|
|
|
/**
|
|
|
|
* Handles pattern sampling
|
2022-09-18 15:11:10 +02:00
|
|
|
*
|
|
|
|
* @return {object} this - The Pattern instance
|
2018-08-09 15:10:15 +02:00
|
|
|
*/
|
2020-04-18 11:36:02 +02:00
|
|
|
Pattern.prototype.sample = function () {
|
2022-09-20 15:24:10 +02:00
|
|
|
this.__init()
|
2022-09-17 10:24:13 +02:00
|
|
|
if (this.settings[0].sample.type === 'option') {
|
|
|
|
return this.sampleOption(this.settings[0].sample.option)
|
|
|
|
} else if (this.settings[0].sample.type === 'measurement') {
|
|
|
|
return this.sampleMeasurement(this.settings[0].sample.measurement)
|
2022-09-19 23:35:52 +02:00
|
|
|
} else if (this.settings[0].sample.type === 'models') {
|
2022-09-17 10:24:13 +02:00
|
|
|
return this.sampleModels(this.settings[0].sample.models, this.settings[0].sample.focus || false)
|
2018-08-09 15:10:15 +02:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-09 15:10:15 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Handles measurement sampling
|
|
|
|
*
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.sampleMeasurement = function (measurementName) {
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.debug(`Sampling measurement \`${measurementName}\``)
|
2022-09-18 15:11:10 +02:00
|
|
|
this.__runHooks('preSample')
|
|
|
|
this.__applySettings(this.__measurementSets(measurementName))
|
2022-09-20 15:24:10 +02:00
|
|
|
this.__init()
|
2022-09-18 15:11:10 +02:00
|
|
|
this.__runHooks('postSample')
|
2018-09-07 16:09:14 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return this.draft()
|
2022-09-17 10:24:13 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Handles models sampling
|
|
|
|
*
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.sampleModels = function (models, focus = false) {
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.debug(`Sampling models \`${Object.keys(models).join(', ')}\``)
|
2022-09-18 15:11:10 +02:00
|
|
|
this.__runHooks('preSample')
|
|
|
|
this.__applySettings(this.__modelSets(models, focus))
|
2022-09-20 15:24:10 +02:00
|
|
|
this.__init()
|
2022-09-18 15:11:10 +02:00
|
|
|
this.__runHooks('postSample')
|
|
|
|
|
|
|
|
return this.draft()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles option sampling
|
|
|
|
*
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.sampleOption = function (optionName) {
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.debug(`Sampling option \`${optionName}\``)
|
2022-09-18 15:11:10 +02:00
|
|
|
this.__runHooks('preSample')
|
|
|
|
this.__applySettings(this.__optionSets(optionName))
|
2022-09-20 15:24:10 +02:00
|
|
|
this.__init()
|
2022-09-18 15:11:10 +02:00
|
|
|
this.__runHooks('postSample')
|
|
|
|
|
|
|
|
return this.draft()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a lifecycle hook method to the pattern
|
|
|
|
*
|
|
|
|
* @param {string} hook - Name of the lifecycle hook
|
|
|
|
* @param {function} method - The method to run
|
|
|
|
* @param {object} data - Any data to pass to the hook method
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.on = function (hook, method, data) {
|
|
|
|
for (const added of this.hooks[hook]) {
|
|
|
|
// Don't add it twice
|
|
|
|
if (added.method === method) return this
|
|
|
|
}
|
|
|
|
this.hooks[hook].push({ method, data })
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders the pattern to SVG
|
|
|
|
*
|
|
|
|
* @return {string} svg - The rendered SVG
|
|
|
|
*/
|
|
|
|
Pattern.prototype.render = function () {
|
|
|
|
this.svg = new Svg(this)
|
|
|
|
this.svg.hooks = this.hooks
|
|
|
|
|
2022-09-19 18:04:47 +02:00
|
|
|
return this.__pack().svg.render()
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads a plugin
|
|
|
|
*
|
|
|
|
* @param {object} plugin - The plugin to load
|
|
|
|
* @param {object} data - Any data to pass to the plugin
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.use = function (plugin, data) {
|
2022-09-24 18:37:49 +02:00
|
|
|
const name = getPluginName(plugin)
|
2022-09-25 09:18:41 +02:00
|
|
|
if (!this.plugins?.[name])
|
|
|
|
return plugin.plugin && plugin.condition
|
2022-09-18 15:11:10 +02:00
|
|
|
? this.__useIf(plugin, data) // Conditional plugin
|
|
|
|
: this.__loadPlugin(plugin, data) // Regular plugin
|
|
|
|
|
2022-09-25 09:18:41 +02:00
|
|
|
this.store.log.info(`Plugin \`${name}\` was requested, but it's already loaded. Skipping.`)
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
// 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 (name, part, dep) {
|
|
|
|
this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name])
|
2023-02-20 06:08:07 +02:00
|
|
|
// #FIXME What's supposed to happen here?
|
2022-09-24 18:37:49 +02:00
|
|
|
if (typeof this.__designParts[dep.name] === 'undefined') {
|
|
|
|
this.config = this.__addPartConfig(this.__designParts[dep.name])
|
2022-09-24 12:44:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2022-09-24 18:37:49 +02:00
|
|
|
if (this.__resolvedParts.includes(part.name)) return this
|
|
|
|
|
2022-09-24 12:44:41 +02:00
|
|
|
// 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
|
|
|
|
*/
|
2022-09-25 09:18:41 +02:00
|
|
|
Pattern.prototype.__addPartMeasurements = function (part, list = false) {
|
2022-09-24 12:44:41 +02:00
|
|
|
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
|
|
|
|
*/
|
2022-09-25 09:18:41 +02:00
|
|
|
Pattern.prototype.__addPartOptionalMeasurements = function (part, list = false) {
|
2022-09-24 12:44:41 +02:00
|
|
|
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}\``)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
2022-09-24 12:44:41 +02:00
|
|
|
|
|
|
|
// 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) {
|
2023-02-20 06:08:07 +02:00
|
|
|
const partDistance = this.__mutated.partDistance?.[part.name] || 0
|
2022-09-24 12:44:41 +02:00
|
|
|
for (const optionName in part.options) {
|
2023-02-20 06:08:07 +02:00
|
|
|
const optionDistance = this.__mutated.optionDistance[optionName]
|
|
|
|
if (!optionDistance) {
|
|
|
|
this.__mutated.optionDistance[optionName] = partDistance
|
2022-09-24 18:37:49 +02:00
|
|
|
// Keep design parts immutable in the pattern or risk subtle bugs
|
|
|
|
this.config.options[optionName] = Object.freeze(part.options[optionName])
|
2022-09-28 19:22:22 +02:00
|
|
|
this.store.log.debug(`🔵 __${optionName}__ option loaded from part \`${part.name}\``)
|
2022-11-11 19:18:02 -08:00
|
|
|
} else {
|
|
|
|
if (DISTANCE_DEBUG)
|
|
|
|
this.store.log.debug(
|
2023-02-20 06:08:07 +02:00
|
|
|
`optionDistance for ${optionName} is ${optionDistance} and partDistance for ${part.name} is ${partDistance}`
|
2022-11-11 19:18:02 -08:00
|
|
|
)
|
2023-02-20 06:08:07 +02:00
|
|
|
if (optionDistance > partDistance) {
|
2022-11-11 19:18:02 -08:00
|
|
|
this.config.options[optionName] = part.options[optionName]
|
2023-02-20 06:08:07 +02:00
|
|
|
this.__mutated.optionDistance[optionName] = partDistance
|
2022-11-11 19:18:02 -08:00
|
|
|
this.store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
|
|
|
|
}
|
2022-09-24 12:44:41 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-24 18:37:49 +02:00
|
|
|
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
|
2022-09-25 09:18:41 +02:00
|
|
|
} else {
|
2022-09-24 18:37:49 +02:00
|
|
|
if (plugin.name) return plugin.name
|
2022-10-23 19:49:41 +02:00
|
|
|
if (plugin.plugin?.name) return plugin.plugin.name
|
2022-09-24 18:37:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-09-24 12:44:41 +02:00
|
|
|
/**
|
|
|
|
* 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) {
|
2022-09-28 16:47:45 +02:00
|
|
|
if (!part.plugins) return this
|
2022-09-24 18:37:49 +02:00
|
|
|
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]
|
2022-09-28 16:47:45 +02:00
|
|
|
// Go through list of part plugins
|
2022-09-24 18:37:49 +02:00
|
|
|
for (let plugin of partPlugins) {
|
|
|
|
const name = getPluginName(plugin)
|
2022-09-28 16:47:45 +02:00
|
|
|
this.store.log.debug(
|
|
|
|
plugin.plugin
|
|
|
|
? `🔌 Resolved __${name}__ conditional plugin in \`${part.name}\``
|
|
|
|
: `🔌 Resolved __${name}__ plugin in \`${part.name}\``
|
|
|
|
)
|
2022-09-24 12:44:41 +02:00
|
|
|
// Handle [plugin, data] scenario
|
|
|
|
if (Array.isArray(plugin)) {
|
|
|
|
const pluginObj = { ...plugin[0], data: plugin[1] }
|
|
|
|
plugin = pluginObj
|
|
|
|
}
|
2022-09-28 16:47:45 +02:00
|
|
|
if (!plugins[name]) {
|
|
|
|
// New plugin, so we load it
|
2022-09-24 18:37:49 +02:00
|
|
|
plugins[name] = plugin
|
2022-09-28 16:47:45 +02:00
|
|
|
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.`
|
|
|
|
)
|
|
|
|
}
|
2022-09-24 12:44:41 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-25 09:18:41 +02:00
|
|
|
this.config.plugins = { ...plugins }
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-24 18:37:49 +02:00
|
|
|
/**
|
|
|
|
* 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)
|
2022-09-25 09:18:41 +02:00
|
|
|
store.extend([...this.__storeMethods])
|
2022-09-24 18:37:49 +02:00
|
|
|
|
|
|
|
return store
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Merges (sets of) settings with the default settings
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Array} sets - An array of settings objects
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__applySettings = function (sets) {
|
|
|
|
if (!Array.isArray(sets)) throw 'Sets should be an array of settings objects'
|
|
|
|
if (sets.length === 0) sets.push({}) // Required to load default settings
|
|
|
|
this.settings = []
|
2022-09-28 22:30:42 +02:00
|
|
|
for (const i in sets) {
|
2022-09-28 22:51:55 +02:00
|
|
|
// Don't mutate the input itself
|
2022-09-28 22:30:42 +02:00
|
|
|
const set = { ...sets[i] }
|
|
|
|
if (!set.options) set.options = {}
|
|
|
|
if (!set.measurements) set.measurements = {}
|
|
|
|
this.settings.push({
|
|
|
|
...__loadPatternDefaults(),
|
|
|
|
...set,
|
|
|
|
// Force creation of a new objects
|
|
|
|
// so we don't reference the original
|
|
|
|
options: { ...set.options },
|
|
|
|
measurements: { ...set.measurements },
|
|
|
|
})
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2022-09-24 18:37:49 +02:00
|
|
|
part.stack = this.__designParts[name]?.stack || name
|
2022-09-18 15:11:10 +02:00
|
|
|
part.context = {
|
|
|
|
parts: this.parts[set],
|
|
|
|
config: this.config,
|
|
|
|
settings: this.settings[set],
|
2022-09-24 12:44:41 +02:00
|
|
|
store: this.setStores[set],
|
2022-09-18 15:11:10 +02:00
|
|
|
macros: this.macros,
|
|
|
|
}
|
2022-09-24 18:37:49 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
if (this.settings[set]?.partClasses) {
|
|
|
|
part.attr('class', this.settings[set].partClasses)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const macro in this.macros) {
|
|
|
|
part[__macroName(macro)] = this.macros[macro]
|
|
|
|
}
|
|
|
|
|
|
|
|
return part
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Instantiates a new Stack instance and populates it with the pattern context
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} name - The name of the stack
|
|
|
|
* @return {Stack} stack - The instantiated Stack
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__createStackWithContext = function (name) {
|
|
|
|
// Context object to add to Stack closure
|
|
|
|
const stack = new Stack()
|
|
|
|
stack.name = name
|
|
|
|
stack.context = {
|
|
|
|
config: this.config,
|
|
|
|
settings: this.settings,
|
2022-09-24 12:44:41 +02:00
|
|
|
setStores: this.setStores,
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return stack
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Filter optional measurements out id they are also required measurments
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__filterOptionalMeasurements = function () {
|
2022-09-24 12:44:41 +02:00
|
|
|
if (!this.config.optionalMeasurements) {
|
|
|
|
this.config.optionalMeasurements = []
|
|
|
|
return this
|
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
this.config.optionalMeasurements = this.config.optionalMeasurements.filter(
|
|
|
|
(m) => this.config.measurements.indexOf(m) === -1
|
|
|
|
)
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-20 15:24:10 +02:00
|
|
|
/**
|
|
|
|
* Initializes the pattern coniguration and settings
|
|
|
|
*
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__init = function () {
|
2022-09-25 11:17:36 +02:00
|
|
|
if (this.__initialized) return this
|
|
|
|
|
2022-09-20 15:24:10 +02:00
|
|
|
this.__runHooks('preInit')
|
2022-09-25 11:17:36 +02:00
|
|
|
// Say hello
|
|
|
|
this.store.log.info(
|
2022-09-26 09:52:09 +02:00
|
|
|
`New \`${this.designConfig?.data?.name || 'No Name'}:` +
|
|
|
|
`${this.designConfig?.data?.version || 'No version'}\` ` +
|
|
|
|
`pattern using \`@freesewing/core:${version}\``
|
2022-09-25 11:17:36 +02:00
|
|
|
)
|
|
|
|
|
2022-09-20 15:24:10 +02:00
|
|
|
/*
|
|
|
|
* We allow late-stage updating of the design config (adding parts for example)
|
|
|
|
* so we need to do the things we used to do in the contructor at a later stage.
|
|
|
|
* This methods does that, and resolves the design config + user settings
|
|
|
|
*/
|
|
|
|
this.__resolveParts() // Resolves parts
|
|
|
|
.__resolveDependencies() // Resolves dependencies
|
|
|
|
.__resolveDraftOrder() // Resolves draft order
|
|
|
|
.__loadPlugins() // Loads plugins
|
|
|
|
.__loadConfigData() // Makes config data available in store
|
2022-09-24 18:37:49 +02:00
|
|
|
.__filterOptionalMeasurements() // Removes required m's from optional list
|
2022-09-20 15:24:10 +02:00
|
|
|
.__loadOptionDefaults() // Merges default options with user provided ones
|
|
|
|
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`)
|
2022-09-20 15:24:10 +02:00
|
|
|
this.__runHooks('postInit')
|
|
|
|
|
2022-09-25 11:17:36 +02:00
|
|
|
this.__initialized = true
|
|
|
|
|
2022-09-20 15:24:10 +02:00
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Checks whether a part is hidden in the config
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} partName - Name of the part to check
|
|
|
|
* @return {bool} hidden - true if the part is hidden, or false if not
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__isPartHidden = function (partName) {
|
2022-09-19 23:35:52 +02:00
|
|
|
if (Array.isArray(this.settings[this.activeSet || 0].only)) {
|
|
|
|
if (this.settings[this.activeSet || 0].only.includes(partName)) return false
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
2022-09-24 18:37:49 +02:00
|
|
|
if (this.__designParts?.[partName]?.hide) return true
|
|
|
|
if (this.__designParts?.[partName]?.hideAll) return true
|
2022-09-25 10:47:09 +02:00
|
|
|
if (this.__mutated.partHide?.[partName]) return true
|
|
|
|
if (this.__mutated.partHideAll?.[partName]) return true
|
2022-09-28 18:07:32 +02:00
|
|
|
if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether a stack is hidden in the config
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} stackName - Name of the stack to check
|
|
|
|
* @return {bool} hidden - true if the part is hidden, or false if not
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__isStackHidden = function (stackName) {
|
|
|
|
if (!this.stacks[stackName]) return true
|
|
|
|
const parts = this.stacks[stackName].getPartNames()
|
2022-09-19 23:35:52 +02:00
|
|
|
if (Array.isArray(this.settings[this.activeStack || 0].only)) {
|
2022-09-18 15:11:10 +02:00
|
|
|
for (const partName of parts) {
|
2022-09-19 23:35:52 +02:00
|
|
|
if (this.settings[this.activeStack || 0].only.includes(partName)) return false
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
2022-09-21 17:41:12 +02:00
|
|
|
return true
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
for (const partName of parts) {
|
2022-09-24 18:37:49 +02:00
|
|
|
if (this.__designParts?.[partName]?.hide) return true
|
|
|
|
if (this.__designParts?.[partName]?.hideAll) return true
|
2022-09-25 10:47:09 +02:00
|
|
|
if (this.__mutated.partHide?.[partName]) return true
|
|
|
|
if (this.__mutated.partHideAll?.[partName]) return true
|
2022-09-28 18:07:32 +02:00
|
|
|
if (this.parts?.[this.activeSet]?.[partName]?.hidden) return true
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-12-08 20:45:17 -08:00
|
|
|
* Generates an array of settings.options objects for sampling a list or boolean option
|
2022-09-18 15:11:10 +02:00
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} optionName - Name of the option to sample
|
|
|
|
* @return {Array} sets - The list of settings objects
|
|
|
|
*/
|
2022-12-08 20:45:17 -08:00
|
|
|
Pattern.prototype.__listBoolOptionSets = function (optionName) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let option = this.config.options[optionName]
|
2022-09-17 10:24:13 +02:00
|
|
|
const base = this.__setBase()
|
|
|
|
const sets = []
|
|
|
|
let run = 1
|
2022-12-08 20:45:17 -08:00
|
|
|
if (typeof option.bool !== 'undefined') option = { list: [false, true] }
|
2022-09-17 10:24:13 +02:00
|
|
|
for (const choice of option.list) {
|
|
|
|
const settings = {
|
|
|
|
...base,
|
|
|
|
options: {
|
|
|
|
...base.options,
|
|
|
|
},
|
|
|
|
idPrefix: `sample-${run}`,
|
|
|
|
partClasses: `sample-${run}`,
|
|
|
|
}
|
|
|
|
settings.options[optionName] = choice
|
|
|
|
sets.push(settings)
|
|
|
|
run++
|
2018-09-11 16:22:08 +02:00
|
|
|
}
|
2022-09-17 10:24:13 +02:00
|
|
|
|
|
|
|
return sets
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
)
|
2022-09-24 12:44:41 +02:00
|
|
|
this.setStores[set].log.debug(
|
2022-09-18 15:11:10 +02:00
|
|
|
`🧲 Snapped __${optionName}__ to \`${this.settings[set].absoluteOptions[optionName]}\` for set __${set}__`
|
|
|
|
)
|
|
|
|
}
|
2018-08-12 18:50:48 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads data from the design config into the store
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__loadConfigData = function () {
|
2022-09-24 18:37:49 +02:00
|
|
|
if (this.designConfig.data) this.store.set('data', this.designConfig.data)
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Merges defaults for options with user-provided options
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__loadOptionDefaults = function () {
|
2022-09-24 12:44:41 +02:00
|
|
|
if (!this.config.options) this.config.options = {}
|
2022-09-18 15:11:10 +02:00
|
|
|
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)
|
2022-09-25 09:18:41 +02:00
|
|
|
this.store.log.error(err)
|
2022-09-18 15:11:10 +02:00
|
|
|
throw new Error(err)
|
|
|
|
}
|
|
|
|
} else this.settings[i].options[name] = option
|
|
|
|
}
|
2022-09-17 10:24:13 +02:00
|
|
|
}
|
2018-08-09 16:45:46 +02:00
|
|
|
}
|
2018-09-11 16:22:08 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return this
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-09-11 16:22:08 +02:00
|
|
|
|
2018-08-09 16:45:46 +02:00
|
|
|
/**
|
2022-09-18 15:11:10 +02:00
|
|
|
* Loads a plugin
|
|
|
|
*
|
|
|
|
* @private
|
2022-09-28 19:22:22 +02:00
|
|
|
* @param {object} plugin - The plugin object, or an object with `plugin` and `condition` keys
|
2022-09-18 15:11:10 +02:00
|
|
|
* @param {object} data - Any plugin data to load
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
2018-08-09 16:45:46 +02:00
|
|
|
*/
|
2022-09-18 15:11:10 +02:00
|
|
|
Pattern.prototype.__loadPlugin = function (plugin, data) {
|
|
|
|
this.plugins[plugin.name] = plugin
|
|
|
|
if (plugin.hooks) this.__loadPluginHooks(plugin, data)
|
|
|
|
if (plugin.macros) this.__loadPluginMacros(plugin)
|
|
|
|
if (plugin.store) this.__loadPluginStoreMethods(plugin)
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``)
|
2022-09-17 10:24:13 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return this
|
2022-09-17 10:24:13 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Loads a plugin's hooks
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {object} plugin - The plugin object
|
|
|
|
* @param {object} data - Any plugin data to load
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__loadPluginHooks = function (plugin, data) {
|
|
|
|
for (let hook of Object.keys(this.hooks)) {
|
|
|
|
if (typeof plugin.hooks[hook] === 'function') {
|
|
|
|
this.on(hook, plugin.hooks[hook], data)
|
|
|
|
} else if (Array.isArray(plugin.hooks[hook])) {
|
|
|
|
for (let method of plugin.hooks[hook]) {
|
|
|
|
this.on(hook, method, data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
2022-09-17 10:24:13 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Loads a plugin's macros
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {object} plugin - The plugin object
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__loadPluginMacros = function (plugin) {
|
|
|
|
for (let macro in plugin.macros) {
|
|
|
|
if (typeof plugin.macros[macro] === 'function') {
|
|
|
|
this.__macro(macro, plugin.macros[macro])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads the plugins that are part of the config
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__loadPlugins = function () {
|
2022-09-24 12:44:41 +02:00
|
|
|
if (!this.config.plugins) return this
|
2022-09-25 09:18:41 +02:00
|
|
|
for (const plugin in this.config.plugins)
|
|
|
|
this.use(this.config.plugins[plugin], this.config.plugins[plugin]?.data)
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads a plugin's store methods
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {object} plugin - The plugin object
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__loadPluginStoreMethods = function (plugin) {
|
|
|
|
if (Array.isArray(plugin.store)) {
|
2022-09-24 18:37:49 +02:00
|
|
|
for (const method of plugin.store) this.__storeMethods.add(method)
|
2022-09-25 09:18:41 +02:00
|
|
|
} else this.store.log.warning(`Plugin store methods should be an Array`)
|
2022-09-24 18:37:49 +02:00
|
|
|
|
|
|
|
return this
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets a method for a macro
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} macro - Name of the macro to run
|
|
|
|
* @param {function} method - The macro method
|
|
|
|
* @return {object} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__macro = function (key, method) {
|
|
|
|
this.macros[key] = method
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates an array of settings objects for sampling a measurement
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} measurementName - The name of the measurement to sample
|
|
|
|
* @return {Array} sets - The list of settings objects
|
|
|
|
*/
|
2022-09-17 10:24:13 +02:00
|
|
|
Pattern.prototype.__measurementSets = function (measurementName) {
|
|
|
|
let val = this.settings[0].measurements[measurementName]
|
2018-12-29 12:56:09 +01:00
|
|
|
if (val === undefined)
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.error(
|
2022-09-10 15:04:57 +02:00
|
|
|
`Cannot sample measurement \`${measurementName}\` because it's \`undefined\``
|
|
|
|
)
|
2019-08-03 15:03:33 +02:00
|
|
|
let step = val / 50
|
|
|
|
val = val * 0.9
|
2022-09-17 10:24:13 +02:00
|
|
|
const sets = []
|
|
|
|
const base = this.__setBase()
|
2018-09-22 10:41:51 +02:00
|
|
|
for (let run = 1; run < 11; run++) {
|
2022-09-17 10:24:13 +02:00
|
|
|
const settings = {
|
|
|
|
...base,
|
|
|
|
measurements: {
|
|
|
|
...base.measurements,
|
|
|
|
},
|
|
|
|
idPrefix: `sample-${run}`,
|
|
|
|
partClasses: `sample-${run}`,
|
|
|
|
}
|
|
|
|
settings.measurements[measurementName] = val
|
|
|
|
sets.push(settings)
|
2019-08-03 15:03:33 +02:00
|
|
|
val += step
|
2018-08-10 14:25:26 +02:00
|
|
|
}
|
|
|
|
|
2022-09-17 10:24:13 +02:00
|
|
|
return sets
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-10 14:25:26 +02:00
|
|
|
|
|
|
|
/**
|
2022-09-18 15:11:10 +02:00
|
|
|
* Generates an array of settings objects for sampling a list of models
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {object} models - The models to sample
|
|
|
|
* @param {string} focus - The ID of the model that should be highlighted
|
|
|
|
* @return {Array} sets - The list of settings objects
|
2018-08-10 14:25:26 +02:00
|
|
|
*/
|
2022-09-17 10:24:13 +02:00
|
|
|
Pattern.prototype.__modelSets = function (models, focus = false) {
|
|
|
|
const sets = []
|
|
|
|
const base = this.__setBase()
|
|
|
|
let run = 1
|
2020-07-11 15:15:02 +02:00
|
|
|
// If there's a focus, do it first so it's at the bottom of the SVG
|
|
|
|
if (focus) {
|
2022-09-17 10:24:13 +02:00
|
|
|
sets.push({
|
|
|
|
...base,
|
|
|
|
measurements: models[focus],
|
|
|
|
idPrefix: `sample-${run}`,
|
|
|
|
partClasses: `sample-${run} sample-focus`,
|
|
|
|
})
|
|
|
|
run++
|
2020-07-11 15:15:02 +02:00
|
|
|
delete models[focus]
|
|
|
|
}
|
2022-09-17 10:24:13 +02:00
|
|
|
for (const measurements of Object.values(models)) {
|
|
|
|
sets.push({
|
|
|
|
...base,
|
|
|
|
measurements,
|
|
|
|
idPrefix: `sample-${run}`,
|
|
|
|
partClasses: `sample-${run}`,
|
|
|
|
})
|
2018-08-09 15:10:15 +02:00
|
|
|
}
|
2022-09-17 10:24:13 +02:00
|
|
|
|
|
|
|
return sets
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-09-18 15:11:10 +02:00
|
|
|
* Determines whether a part is needed, depending on the 'only' setting and the configured dependencies
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} partName - Name of the part
|
|
|
|
* @param {int} set - The index of the set of settings
|
|
|
|
* @return {bool} needs - true if the part is needed, or false if not
|
2022-09-17 10:24:13 +02:00
|
|
|
*/
|
2022-09-18 15:11:10 +02:00
|
|
|
Pattern.prototype.__needs = function (partName, set = 0) {
|
|
|
|
// If only is unset, all parts are needed
|
|
|
|
if (
|
|
|
|
typeof this.settings[set].only === 'undefined' ||
|
|
|
|
this.settings[set].only === false ||
|
|
|
|
(Array.isArray(this.settings[set].only) && this.settings[set].only.length === 0)
|
2022-09-04 18:22:02 +02:00
|
|
|
)
|
2022-09-18 15:11:10 +02:00
|
|
|
return true
|
2018-07-23 11:12:06 +00:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
// Make only to always be an array
|
|
|
|
const only =
|
|
|
|
typeof this.settings[set].only === 'string'
|
|
|
|
? [this.settings[set].only]
|
|
|
|
: this.settings[set].only
|
2020-09-12 19:01:12 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
// 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 (dependency === partName) return true
|
2018-08-11 14:03:06 +02:00
|
|
|
}
|
2018-07-23 11:12:06 +00:00
|
|
|
}
|
2018-07-23 20:14:32 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return false
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-07-23 11:12:06 +00:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Generates an array of settings objects for sampling an option
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} optionName - The name of the option to sample
|
|
|
|
* @return {Array} sets - The list of settings objects
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__optionSets = function (optionName) {
|
|
|
|
const sets = []
|
2022-12-08 20:45:17 -08:00
|
|
|
if (!(optionName in this.config.options)) return sets
|
|
|
|
let option = this.config.options[optionName]
|
|
|
|
if (typeof option.list === 'object' || typeof option.bool !== 'undefined')
|
|
|
|
return this.__listBoolOptionSets(optionName)
|
2022-09-18 15:11:10 +02:00
|
|
|
let factor = 1
|
|
|
|
let step, val
|
2022-12-08 20:45:17 -08:00
|
|
|
let numberRuns = 10
|
|
|
|
let stepFactor = numberRuns - 1
|
2022-09-18 15:11:10 +02:00
|
|
|
if (typeof option.min === 'undefined' || typeof option.max === 'undefined') {
|
|
|
|
const min = option * 0.9
|
|
|
|
const max = option * 1.1
|
|
|
|
option = { min, max }
|
|
|
|
}
|
|
|
|
if (typeof option.pct !== 'undefined') factor = 100
|
|
|
|
val = option.min / factor
|
2022-12-08 20:45:17 -08:00
|
|
|
if (typeof option.count !== 'undefined' || typeof option.mm !== 'undefined') {
|
|
|
|
const numberOfCounts = option.max - option.min + 1
|
|
|
|
if (numberOfCounts < 10) {
|
|
|
|
numberRuns = numberOfCounts
|
|
|
|
stepFactor = Math.max(numberRuns - 1, 1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
step = (option.max / factor - val) / stepFactor
|
2022-09-18 15:11:10 +02:00
|
|
|
const base = this.__setBase()
|
2022-12-08 20:45:17 -08:00
|
|
|
for (let run = 1; run <= numberRuns; run++) {
|
2022-09-18 15:11:10 +02:00
|
|
|
const settings = {
|
|
|
|
...base,
|
|
|
|
options: {
|
|
|
|
...base.options,
|
|
|
|
},
|
|
|
|
idPrefix: `sample-${run}`,
|
|
|
|
partClasses: `sample-${run}`,
|
2018-07-23 11:12:06 +00:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
settings.options[optionName] = val
|
|
|
|
sets.push(settings)
|
|
|
|
val += step
|
2022-12-08 20:45:17 -08:00
|
|
|
if (typeof option.count !== 'undefined' || typeof option.mm !== 'undefined')
|
|
|
|
val = Math.round(val)
|
2018-07-23 20:14:32 +02:00
|
|
|
}
|
2022-09-07 16:16:33 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return sets
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-08-01 18:18:29 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Packs stacks in a 2D space and sets pattern size
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__pack = function () {
|
2022-11-14 14:01:50 -06:00
|
|
|
this.__runHooks('preLayout')
|
2022-09-17 10:24:13 +02:00
|
|
|
for (const set in this.settings) {
|
2022-09-24 12:44:41 +02:00
|
|
|
if (this.setStores[set].logs.error.length > 0) {
|
|
|
|
this.setStores[set].log.warning(`One or more errors occured. Not packing pattern parts`)
|
2022-09-17 10:24:13 +02:00
|
|
|
return this
|
|
|
|
}
|
2020-07-19 13:01:01 +02:00
|
|
|
}
|
2022-09-12 20:10:22 +02:00
|
|
|
// First, create all stacks
|
|
|
|
this.stacks = {}
|
2022-09-17 10:24:13 +02:00
|
|
|
for (const set in this.settings) {
|
|
|
|
for (const [name, part] of Object.entries(this.parts[set])) {
|
|
|
|
const stackName =
|
|
|
|
this.settings[set].stackPrefix +
|
2023-01-07 15:14:32 -06:00
|
|
|
(typeof part.stack === 'function' ? part.stack(this.settings[set], name) : part.stack)
|
2022-09-17 10:24:13 +02:00
|
|
|
if (typeof this.stacks[stackName] === 'undefined')
|
|
|
|
this.stacks[stackName] = this.__createStackWithContext(stackName, set)
|
|
|
|
this.stacks[stackName].addPart(part)
|
|
|
|
}
|
2022-09-12 20:10:22 +02:00
|
|
|
}
|
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
let bins = []
|
2022-09-12 20:10:22 +02:00
|
|
|
for (const [key, stack] of Object.entries(this.stacks)) {
|
|
|
|
// Avoid multiple render calls to cause addition of transforms
|
|
|
|
stack.attributes.remove('transform')
|
2022-09-18 15:11:10 +02:00
|
|
|
if (!this.__isStackHidden(key)) {
|
2022-09-12 20:10:22 +02:00
|
|
|
stack.home()
|
2022-09-17 10:24:13 +02:00
|
|
|
if (this.settings[0].layout === true)
|
2022-09-13 17:56:01 +02:00
|
|
|
bins.push({ id: key, width: stack.width, height: stack.height })
|
2019-01-13 15:00:01 +01:00
|
|
|
else {
|
2022-09-12 20:10:22 +02:00
|
|
|
if (this.width < stack.width) this.width = stack.width
|
|
|
|
if (this.height < stack.height) this.height = stack.height
|
2019-01-13 15:00:01 +01:00
|
|
|
}
|
2018-08-01 18:18:29 +02:00
|
|
|
}
|
|
|
|
}
|
2022-09-17 10:24:13 +02:00
|
|
|
if (this.settings[0].layout === true) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let size = pack(bins, { inPlace: true })
|
2019-01-13 15:00:01 +01:00
|
|
|
for (let bin of bins) {
|
2022-09-12 20:10:22 +02:00
|
|
|
this.autoLayout.stacks[bin.id] = { move: {} }
|
|
|
|
let stack = this.stacks[bin.id]
|
2022-02-25 08:29:28 +01:00
|
|
|
if (bin.x !== 0 || bin.y !== 0) {
|
2022-09-12 20:10:22 +02:00
|
|
|
stack.attr('transform', `translate(${bin.x}, ${bin.y})`)
|
2022-08-17 00:34:25 -05:00
|
|
|
}
|
2022-09-12 20:10:22 +02:00
|
|
|
this.autoLayout.stacks[bin.id].move = {
|
|
|
|
x: bin.x + stack.layout.move.x,
|
|
|
|
y: bin.y + stack.layout.move.y,
|
2022-08-17 00:34:25 -05:00
|
|
|
}
|
2019-01-13 15:00:01 +01:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
this.width = size.width
|
|
|
|
this.height = size.height
|
2022-09-17 10:24:13 +02:00
|
|
|
} else if (typeof this.settings[0].layout === 'object') {
|
|
|
|
this.width = this.settings[0].layout.width
|
|
|
|
this.height = this.settings[0].layout.height
|
|
|
|
for (let stackId of Object.keys(this.settings[0].layout.stacks)) {
|
2022-03-06 18:55:13 +01:00
|
|
|
// Some parts are added by late-stage plugins
|
2022-09-12 20:10:22 +02:00
|
|
|
if (this.stacks[stackId]) {
|
2022-09-19 23:35:52 +02:00
|
|
|
let transforms = this.settings[this.activeStack || 0].layout.stacks[stackId]
|
2022-09-12 20:10:22 +02:00
|
|
|
this.stacks[stackId].generateTransform(transforms)
|
2019-01-15 19:23:36 +01:00
|
|
|
}
|
2019-01-15 13:56:31 +01:00
|
|
|
}
|
2018-08-01 18:18:29 +02:00
|
|
|
}
|
|
|
|
|
2022-11-14 14:01:50 -06:00
|
|
|
this.__runHooks('postLayout')
|
2019-08-03 15:03:33 +02:00
|
|
|
return this
|
|
|
|
}
|
2018-08-15 18:54:47 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Recursively solves part dependencies for a part
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {object} seen - Object to keep track of seen dependencies
|
|
|
|
* @param {string} part - Name of the part
|
|
|
|
* @param {object} graph - Dependency graph, used to call itself recursively
|
|
|
|
* @param {array} deps - List of dependencies
|
|
|
|
* @return {Array} deps - The list of dependencies
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__resolveDependency = function (
|
|
|
|
seen,
|
|
|
|
part,
|
|
|
|
graph = this.dependencies,
|
|
|
|
deps = []
|
|
|
|
) {
|
|
|
|
if (typeof seen[part] === 'undefined') seen[part] = true
|
|
|
|
if (typeof graph[part] === 'string') graph[part] = [graph[part]]
|
|
|
|
if (Array.isArray(graph[part])) {
|
|
|
|
if (graph[part].length === 0) return []
|
|
|
|
else {
|
|
|
|
if (deps.indexOf(graph[part]) === -1) deps.push(...graph[part])
|
|
|
|
for (let apart of graph[part]) deps.concat(this.__resolveDependency(seen, apart, graph, deps))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return deps
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2022-09-09 20:20:38 +02:00
|
|
|
Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDependencies) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let sorted = []
|
|
|
|
let visited = {}
|
2018-12-16 18:06:01 +01:00
|
|
|
Object.keys(graph).forEach(function visit(name, ancestors) {
|
2019-08-03 15:03:33 +02:00
|
|
|
if (!Array.isArray(ancestors)) ancestors = []
|
|
|
|
ancestors.push(name)
|
|
|
|
visited[name] = true
|
|
|
|
if (typeof graph[name] !== 'undefined') {
|
2020-04-18 11:36:02 +02:00
|
|
|
graph[name].forEach(function (dep) {
|
2019-08-03 15:03:33 +02:00
|
|
|
if (visited[dep]) return
|
|
|
|
visit(dep, ancestors.slice(0))
|
|
|
|
})
|
2018-12-16 18:06:01 +01:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
if (sorted.indexOf(name) < 0) sorted.push(name)
|
|
|
|
})
|
2018-12-16 18:06:01 +01:00
|
|
|
|
2022-09-09 20:20:38 +02:00
|
|
|
// Don't forget about parts without dependencies
|
2022-09-24 18:37:49 +02:00
|
|
|
for (const part in this.__designParts) {
|
2022-09-09 20:20:38 +02:00
|
|
|
if (sorted.indexOf(part) === -1) sorted.push(part)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.__draftOrder = sorted
|
|
|
|
this.config.draftOrder = sorted
|
|
|
|
|
|
|
|
return this
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-12-16 18:06:01 +01:00
|
|
|
|
2023-02-20 06:08:07 +02:00
|
|
|
Pattern.prototype.__resolvePartMutation = function (part, dependency, depType) {
|
|
|
|
const current_part_distance = this.__mutated.partDistance[part.name]
|
|
|
|
const proposed_dependent_part_distance = current_part_distance + 1
|
|
|
|
|
|
|
|
this.__designParts[dependency.name] = Object.freeze(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)
|
|
|
|
this.__addDependency(part.name, part, dependency)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
typeof this.__mutated.partDistance[dependency.name] === 'undefined' ||
|
|
|
|
this.__mutated.partDistance[dependency.name] < proposed_dependent_part_distance
|
|
|
|
) {
|
|
|
|
this.__mutated.partDistance[dependency.name] = proposed_dependent_part_distance
|
|
|
|
if (DISTANCE_DEBUG)
|
|
|
|
this.store.log.debug(
|
|
|
|
`"${depType}:" partDistance for ${dependency.name} is ${
|
|
|
|
this.__mutated.partDistance[dependency.name]
|
|
|
|
}`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Pattern.prototype.__resolvePart = function (part, distance = 0) {
|
|
|
|
if (distance === 0) {
|
|
|
|
this.__designParts[part.name] = Object.freeze(part)
|
|
|
|
}
|
|
|
|
let count = Object.keys(this.__designParts).length
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resolve part mutations. first from then after
|
|
|
|
;['from', 'after'].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) => {
|
|
|
|
this.__resolvePartMutation(part, dot, d)
|
|
|
|
const newCount = Object.keys(this.__designParts).length
|
|
|
|
if (count < newCount) {
|
|
|
|
this.__resolvePart(dot, distance)
|
|
|
|
count = newCount
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// add the part's config
|
|
|
|
this.__addPartConfig(part)
|
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Resolves parts and their dependencies
|
|
|
|
*
|
|
|
|
* @private
|
2022-09-24 12:44:41 +02:00
|
|
|
* @param {int} count - The count is used to call itself recursively
|
2022-09-18 15:11:10 +02:00
|
|
|
* @param {int} distance - Keeps track of how far the dependency is from the pattern
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
2022-09-17 10:24:13 +02:00
|
|
|
Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
|
2022-09-24 12:44:41 +02:00
|
|
|
for (const part of this.designConfig.parts) {
|
2023-02-20 06:08:07 +02:00
|
|
|
this.__resolvePart(part, distance)
|
2022-11-11 19:18:02 -08:00
|
|
|
}
|
2023-02-20 06:08:07 +02:00
|
|
|
|
2022-11-11 19:18:02 -08:00
|
|
|
// Print final part distances.
|
|
|
|
for (const part of this.designConfig.parts) {
|
2023-02-20 06:08:07 +02:00
|
|
|
let qualifier = DISTANCE_DEBUG ? 'final' : ''
|
2022-11-11 19:18:02 -08:00
|
|
|
this.store.log.debug(
|
2023-02-20 06:08:07 +02:00
|
|
|
`⚪️ ${part.name} ${qualifier} options priority is __${
|
|
|
|
this.__mutated.partDistance[part.name]
|
|
|
|
}__`
|
2022-11-11 19:18:02 -08:00
|
|
|
)
|
|
|
|
}
|
2022-08-14 16:59:51 +02:00
|
|
|
|
2023-02-20 06:08:07 +02:00
|
|
|
// for (const part of Object.values(this.__designParts)) this.__addPartConfig(part)
|
2022-08-09 20:17:35 +02:00
|
|
|
|
2022-09-09 20:20:38 +02:00
|
|
|
return this
|
2022-08-09 20:17:35 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Resolves parts depdendencies into a flat array
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {object} graph - The graph is used to call itsels recursively
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
2022-09-09 20:20:38 +02:00
|
|
|
Pattern.prototype.__resolveDependencies = function (graph = false) {
|
|
|
|
if (!graph) graph = this.__dependencies
|
|
|
|
for (const i in this.__inject) {
|
|
|
|
const dependency = this.__inject[i]
|
|
|
|
if (typeof this.__dependencies[i] === 'undefined') this.__dependencies[i] = dependency
|
|
|
|
else if (this.__dependencies[i] !== dependency) {
|
|
|
|
if (typeof this.__dependencies[i] === 'string') {
|
|
|
|
this.__dependencies[i] = [this.__dependencies[i], dependency]
|
|
|
|
} else if (Array.isArray(this.__dependencies[i])) {
|
|
|
|
if (this.__dependencies[i].indexOf(dependency) === -1)
|
|
|
|
this.__dependencies[i].push(dependency)
|
2020-07-19 13:01:01 +02:00
|
|
|
} else {
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.error('Part dependencies should be a string or an array of strings')
|
2020-07-19 13:01:01 +02:00
|
|
|
throw new Error('Part dependencies should be a string or an array of strings')
|
|
|
|
}
|
2019-05-31 19:18:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
let resolved = {}
|
|
|
|
let seen = {}
|
2022-09-18 15:11:10 +02:00
|
|
|
for (let part in graph) resolved[part] = this.__resolveDependency(seen, part, graph)
|
2019-08-03 15:03:33 +02:00
|
|
|
for (let part in seen) if (typeof resolved[part] === 'undefined') resolved[part] = []
|
2018-12-16 18:06:01 +01:00
|
|
|
|
2022-09-09 20:20:38 +02:00
|
|
|
this.__resolvedDependencies = resolved
|
|
|
|
this.config.resolvedDependencies = resolved
|
|
|
|
|
|
|
|
return this
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-12-16 18:06:01 +01:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Runs subscriptions to a given lifecycle hook
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} hookName - Name of the lifecycle hook
|
|
|
|
* @param {obhect} data - Any data to pass to the hook method
|
|
|
|
* @return {Pattern} this - The Pattern instance
|
2018-08-15 18:54:47 +02:00
|
|
|
*/
|
2022-09-18 15:11:10 +02:00
|
|
|
Pattern.prototype.__runHooks = function (hookName, data = false) {
|
|
|
|
if (data === false) data = this
|
|
|
|
let hooks = this.hooks[hookName]
|
|
|
|
if (hooks.length > 0) {
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.debug(`Running \`${hookName}\` hooks`)
|
2022-09-18 15:11:10 +02:00
|
|
|
for (let hook of hooks) {
|
|
|
|
hook.method(data, hook.data)
|
2018-08-15 18:54:47 +02:00
|
|
|
}
|
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2018-12-16 18:06:01 +01:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Returns the base/defaults to generate a set of settings
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @return {object} settings - The settings object
|
2018-12-16 18:06:01 +01:00
|
|
|
*/
|
2022-09-18 15:11:10 +02:00
|
|
|
Pattern.prototype.__setBase = function () {
|
|
|
|
return {
|
|
|
|
...this.settings[0],
|
|
|
|
measurements: { ...(this.settings[0].measurements || {}) },
|
|
|
|
options: { ...(this.settings[0].options || {}) },
|
2018-12-16 18:06:01 +01:00
|
|
|
}
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2019-05-05 17:06:22 +02:00
|
|
|
|
2022-09-29 01:01:32 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* 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])
|
2022-09-24 18:37:49 +02:00
|
|
|
// 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]
|
2022-09-18 15:11:10 +02:00
|
|
|
// Simple steps
|
2022-09-24 18:37:49 +02:00
|
|
|
if (typeof snapConf === 'number') return Math.ceil(abs / snapConf) * snapConf
|
2022-09-18 15:11:10 +02:00
|
|
|
// List of snaps
|
2022-09-24 18:37:49 +02:00
|
|
|
if (Array.isArray(snapConf) && snapConf.length > 1) {
|
|
|
|
for (const snap of snapConf
|
2022-09-18 15:11:10 +02:00
|
|
|
.sort((a, b) => a - b)
|
|
|
|
.map((snap, i) => {
|
|
|
|
const margin =
|
2022-09-24 18:37:49 +02:00
|
|
|
i < snapConf.length - 1
|
|
|
|
? (snapConf[Number(i) + 1] - snap) / 2 // Look forward
|
|
|
|
: (snap - snapConf[i - 1]) / 2 // Final snap, look backward
|
2022-09-18 15:11:10 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
min: snap - margin,
|
|
|
|
max: snap + Number(margin),
|
|
|
|
snap,
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
if (abs <= snap.max && abs >= snap.min) return snap.snap
|
2022-09-10 15:04:57 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return abs
|
2022-07-31 14:32:24 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Loads a conditional plugin
|
|
|
|
*
|
|
|
|
* @private
|
2022-09-28 19:22:22 +02:00
|
|
|
* @param {object} plugin - An object with `plugin` and `condition` keys
|
2022-09-18 15:11:10 +02:00
|
|
|
* @return {Pattern} this - The Pattern instance
|
|
|
|
*/
|
|
|
|
Pattern.prototype.__useIf = function (plugin) {
|
|
|
|
let load = 0
|
|
|
|
for (const set of this.settings) {
|
|
|
|
if (plugin.condition(set)) load++
|
2022-09-12 20:10:22 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
if (load > 0) {
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.info(
|
2022-09-18 15:11:10 +02:00
|
|
|
`Condition met: Loaded plugin \`${plugin.plugin.name}:${plugin.plugin.version}\``
|
|
|
|
)
|
|
|
|
this.__loadPlugin(plugin.plugin, plugin.data)
|
|
|
|
} else {
|
2022-09-24 12:44:41 +02:00
|
|
|
this.store.log.info(
|
2022-09-18 15:11:10 +02:00
|
|
|
`Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\``
|
|
|
|
)
|
2022-09-12 20:10:22 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return this
|
2022-09-12 20:10:22 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Determines whether a part is wanted, depending on the 'only' setting and the configured dependencies
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} partName - Name of the part
|
|
|
|
* @param {int} set - The index of the set of settings
|
|
|
|
* @return {bool} wants - true if the part is wanted, or false if not
|
2019-05-05 17:06:22 +02:00
|
|
|
*/
|
2022-09-18 15:11:10 +02:00
|
|
|
Pattern.prototype.__wants = function (partName, set = 0) {
|
|
|
|
// Hidden parts are not wanted
|
|
|
|
if (this.__isPartHidden(partName)) return false
|
|
|
|
else if (typeof this.settings[set].only === 'string') return this.settings[set].only === partName
|
|
|
|
else if (Array.isArray(this.settings[set].only)) {
|
|
|
|
for (const part of this.settings[set].only) {
|
|
|
|
if (part === partName) return true
|
2022-09-13 17:56:01 +02:00
|
|
|
}
|
2022-09-18 15:11:10 +02:00
|
|
|
return false
|
2022-09-13 17:56:01 +02:00
|
|
|
}
|
2019-05-05 17:06:22 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return true
|
2019-08-03 15:03:33 +02:00
|
|
|
}
|
2022-09-09 20:20:38 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
2022-09-24 12:44:41 +02:00
|
|
|
// STATIC PRIVATE FUNCTIONS //
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
2022-09-24 12:44:41 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
2022-09-24 12:44:41 +02:00
|
|
|
* Merges dependencies into a flat list
|
2022-09-18 15:11:10 +02:00
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {array} dep - New dependencies
|
|
|
|
* @param {array} current - Current dependencies
|
|
|
|
* @return {array} deps - Merged dependencies
|
|
|
|
*/
|
|
|
|
function mergeDependencies(dep = [], current = []) {
|
|
|
|
// Current dependencies
|
|
|
|
const list = []
|
|
|
|
if (Array.isArray(current)) list.push(...current)
|
|
|
|
else if (typeof current === 'string') list.push(current)
|
|
|
|
|
|
|
|
if (Array.isArray(dep)) list.push(...dep)
|
|
|
|
else if (typeof dep === 'string') list.push(dep)
|
|
|
|
|
|
|
|
// Dependencies should be parts names (string) not the object
|
|
|
|
const deps = []
|
|
|
|
for (const part of [...new Set(list)]) {
|
|
|
|
if (typeof part === 'object') deps.push(part.name)
|
|
|
|
else deps.push(part)
|
2022-09-17 10:24:13 +02:00
|
|
|
}
|
2022-09-09 20:20:38 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return deps
|
2022-09-09 20:20:38 +02:00
|
|
|
}
|