1
0
Fork 0

feat(core): Added support for multiple sets of settings

This changes they was settings (what the user provides) are handled.
Before this, settings were passed as an object and that was it.
Now, settings are treated as an array of settings objects and this adds
full support for managing multiple sets of settings in a single pattern
instance.

This is the mechanism that's used for FreeSewing's sampling which used
to be a rather hackish implementation, but now merely sets up the
relevant list of settings, and then calls `pattern.draft()` as usual.

Things to be mindful of is that parts, the store and settings themselves
are tied to each set of settings. So where they used to be an object,
they are now an array of object.
This commit is contained in:
Joost De Cock 2022-09-17 10:24:13 +02:00
parent 11a2a1dd1c
commit 0b18d81e14
7 changed files with 397 additions and 252 deletions

View file

@ -2,6 +2,7 @@ export const loadDesignDefaults = () => ({
measurements: [], measurements: [],
optionalMeasurements: [], optionalMeasurements: [],
options: {}, options: {},
optionDistance: {},
parts: [], parts: [],
data: {}, data: {},
plugins: [], plugins: [],
@ -10,6 +11,7 @@ export const loadDesignDefaults = () => ({
export const loadPatternDefaults = () => ({ export const loadPatternDefaults = () => ({
complete: true, complete: true,
idPrefix: 'fs-', idPrefix: 'fs-',
stackPrefix: '',
locale: 'en', locale: 'en',
units: 'metric', units: 'metric',
margin: 2, margin: 2,

View file

@ -10,12 +10,12 @@ export function Design(config) {
config = { ...loadDesignDefaults(), ...config } config = { ...loadDesignDefaults(), ...config }
// Create the pattern constructor // Create the pattern constructor
const pattern = function (settings) { const pattern = function (...sets) {
// Pass the design config // Pass the design config
Pattern.call(this, config) Pattern.call(this, config)
// Pass the pattern settings // Pass the pattern settings
return this.__applySettings(settings) return this.__applySettings(sets)
} }
// Set up inheritance // Set up inheritance

View file

@ -1,6 +1,7 @@
import { Attributes } from './attributes.mjs' import { Attributes } from './attributes.mjs'
import { Design } from './design.mjs' import { Design } from './design.mjs'
import { Pattern } from './pattern.mjs' import { Pattern } from './pattern.mjs'
import { Part } from './part.mjs'
import { Point } from './point.mjs' import { Point } from './point.mjs'
import { Path } from './path.mjs' import { Path } from './path.mjs'
import { Snippet } from './snippet.mjs' import { Snippet } from './snippet.mjs'
@ -44,6 +45,7 @@ export {
Pattern, Pattern,
Point, Point,
Path, Path,
Part,
Snippet, Snippet,
Store, Store,
Bezier, Bezier,

View file

@ -336,21 +336,21 @@ Part.prototype.shorthand = function () {
return shorthand return shorthand
} }
Part.prototype.isEmpty = function () { //Part.prototype.isEmpty = function () {
if (Object.keys(this.snippets).length > 0) return false // if (Object.keys(this.snippets).length > 0) return false
//
if (Object.keys(this.paths).length > 0) { // if (Object.keys(this.paths).length > 0) {
for (const p in this.paths) { // for (const p in this.paths) {
if (this.paths[p].render && this.paths[p].length()) return false // if (this.paths[p].render && this.paths[p].length()) return false
} // }
} // }
//
for (const p in this.points) { // for (const p in this.points) {
if (this.points[p].attributes.get('data-text')) return false // if (this.points[p].attributes.get('data-text')) return false
if (this.points[p].attributes.get('data-circle')) return false // if (this.points[p].attributes.get('data-circle')) return false
} // }
//
return true // return true
} //}
export default Part export default Part

View file

@ -21,6 +21,7 @@ import { loadPatternDefaults } from './config.mjs'
export function Pattern(config) { export function Pattern(config) {
// Non-enumerable properties // Non-enumerable properties
addNonEnumProp(this, 'plugins', {}) addNonEnumProp(this, 'plugins', {})
addNonEnumProp(this, 'parts', [{}])
addNonEnumProp(this, 'width', 0) addNonEnumProp(this, 'width', 0)
addNonEnumProp(this, 'height', 0) addNonEnumProp(this, 'height', 0)
addNonEnumProp(this, 'autoLayout', { stacks: {} }) addNonEnumProp(this, 'autoLayout', { stacks: {} })
@ -40,9 +41,8 @@ export function Pattern(config) {
// Enumerable properties // Enumerable properties
this.config = config // Design config this.config = config // Design config
this.parts = {} // Drafted parts container
this.stacks = {} // Drafted stacks container this.stacks = {} // Drafted stacks container
this.store = new Store() // Store for sharing data across parts this.stores = [new Store()]
return this return this
} }
@ -65,36 +65,42 @@ Pattern.prototype.init = function () {
.__loadOptionDefaults() // Merges default options with user provided ones .__loadOptionDefaults() // Merges default options with user provided ones
// Say hello // Say hello
this.store.log.info( this.stores[0].log.info(
`New \`${this.store.get('data.name', 'No Name')}:` + `New \`${this.stores[0].get('data.name', 'No Name')}:` +
`${this.store.get( `${this.stores[0].get(
'data.version', 'data.version',
'No version' 'No version'
)}\` pattern using \`@freesewing/core:${version}\`` )}\` pattern using \`@freesewing/core:${version}\``
) )
this.store.log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`) this.stores[0].log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`)
return this return this
} }
Pattern.prototype.__loadConfigData = function () { Pattern.prototype.__loadConfigData = function () {
if (this.config.data) this.store.set('data', this.config.data) if (this.config.data) {
for (const i in this.settings) this.stores[i].set('data', this.config.data)
}
return this return this
} }
Pattern.prototype.__createPartWithContext = function (name) { Pattern.prototype.__createPartWithContext = function (name, set) {
// Context object to add to Part closure // Context object to add to Part closure
const part = new Part() const part = new Part()
part.name = name part.name = name
part.set = set
part.stack = this.__parts[name]?.stack || name part.stack = this.__parts[name]?.stack || name
part.context = { part.context = {
parts: this.parts, parts: this.parts[set],
config: this.config, config: this.config,
settings: this.settings, settings: this.settings[set],
store: this.store, store: this.stores[set],
macros: this.macros, macros: this.macros,
} }
if (this.settings[set]?.partClasses) {
part.attr('class', this.settings[set].partClasses)
}
for (const macro in this.macros) { for (const macro in this.macros) {
part[macroName(macro)] = this.macros[macro] part[macroName(macro)] = this.macros[macro]
@ -110,7 +116,7 @@ Pattern.prototype.__createStackWithContext = function (name) {
stack.context = { stack.context = {
config: this.config, config: this.config,
settings: this.settings, settings: this.settings,
store: this.store, stores: this.stores,
} }
return stack return stack
@ -119,22 +125,25 @@ Pattern.prototype.__createStackWithContext = function (name) {
// Merges default for options with user-provided options // Merges default for options with user-provided options
Pattern.prototype.__loadOptionDefaults = function () { Pattern.prototype.__loadOptionDefaults = function () {
if (Object.keys(this.config.options).length < 1) return this if (Object.keys(this.config.options).length < 1) return this
for (const [name, option] of Object.entries(this.config.options)) { for (const i in this.settings) {
// Don't overwrite user-provided settings.options for (const [name, option] of Object.entries(this.config.options)) {
if (typeof this.settings.options[name] === 'undefined') { // Don't overwrite user-provided settings.options
if (typeof option === 'object') { if (typeof this.settings[i].options[name] === 'undefined') {
if (typeof option.pct !== 'undefined') this.settings.options[name] = option.pct / 100 if (typeof option === 'object') {
else if (typeof option.mm !== 'undefined') this.settings.options[name] = option.mm if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100
else if (typeof option.deg !== 'undefined') this.settings.options[name] = option.deg else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm
else if (typeof option.count !== 'undefined') this.settings.options[name] = option.count else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg
else if (typeof option.bool !== 'undefined') this.settings.options[name] = option.bool else if (typeof option.count !== 'undefined')
else if (typeof option.dflt !== 'undefined') this.settings.options[name] = option.dflt this.settings[i].options[name] = option.count
else { else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool
let err = 'Unknown option type: ' + JSON.stringify(option) else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt
this.store.log.error(err) else {
throw new Error(err) let err = 'Unknown option type: ' + JSON.stringify(option)
} this.stores[i].log.error(err)
} else this.settings.options[name] = option throw new Error(err)
}
} else this.settings[i].options[name] = option
}
} }
} }
@ -153,12 +162,12 @@ Pattern.prototype.getPartList = function () {
return Object.keys(this.config.parts) return Object.keys(this.config.parts)
} }
function snappedOption(option, pattern) { Pattern.prototype.__snappedPercentageOption = function (optionName, set) {
const conf = pattern.config.options[option] const conf = this.config.options[optionName]
const abs = conf.toAbs(pattern.settings.options[option], pattern.settings) const abs = conf.toAbs(this.settings[set].options[optionName], this.settings[set])
// Handle units-specific config // Handle units-specific config
if (!Array.isArray(conf.snap) && conf.snap.metric && conf.snap.imperial) if (!Array.isArray(conf.snap) && conf.snap.metric && conf.snap.imperial)
conf.snap = conf.snap[pattern.settings.units] conf.snap = conf.snap[this.settings[set].units]
// Simple steps // Simple steps
if (typeof conf.snap === 'number') return Math.ceil(abs / conf.snap) * conf.snap if (typeof conf.snap === 'number') return Math.ceil(abs / conf.snap) * conf.snap
// List of snaps // List of snaps
@ -187,7 +196,7 @@ Pattern.prototype.runHooks = function (hookName, data = false) {
if (data === false) data = this if (data === false) data = this
let hooks = this.hooks[hookName] let hooks = this.hooks[hookName]
if (hooks.length > 0) { if (hooks.length > 0) {
this.store.log.debug(`Running \`${hookName}\` hooks`) this.stores[0].log.debug(`Running \`${hookName}\` hooks`)
for (let hook of hooks) { for (let hook of hooks) {
hook.method(data, hook.data) hook.method(data, hook.data)
} }
@ -202,9 +211,30 @@ Pattern.prototype.addPart = function (part) {
if (part.name) { if (part.name) {
this.config.parts[part.name] = part this.config.parts[part.name] = part
// Add part-level config to config // Add part-level config to config
this.config = addPartConfig(part, this.config, this.store) this.config = addPartConfig(part, this.config, this.stores[0])
} else this.store.log.error(`Part must have a name`) } else this.stores[0].log.error(`Part must have a name`)
} else this.store.log.error(`Part must have a draft() method`) } else this.stores[0].log.error(`Part must have a draft() method`)
return this
}
Pattern.prototype.__loadAbsoluteOptionsSet = function (set) {
for (const optionName in this.settings[set].options) {
const option = this.config.options[optionName]
if (
typeof option !== 'undefined' &&
typeof option.snap !== 'undefined' &&
option.toAbs instanceof Function
) {
this.settings[set].absoluteOptions[optionName] = this.__snappedPercentageOption(
optionName,
set
)
this.stores[set].log.debug(
`🧲 Snapped __${optionName}__ to \`${this.settings[set].absoluteOptions[optionName]}\` for set __${set}__`
)
}
}
return this return this
} }
@ -216,66 +246,68 @@ Pattern.prototype.draft = function () {
// Late-stage initialization // Late-stage initialization
this.init() this.init()
if (this.is !== 'sample') { // Iterate over the provided sets of settings (typically just one)
this.is = 'draft' for (const set in this.settings) {
this.store.log.debug(`Drafting pattern`) // Set store
} this.stores[set].log.debug(`📐 Drafting pattern (set ${set})`)
// Handle snap for pct options
for (const i in this.settings.options) {
if (
typeof this.config.options[i] !== 'undefined' &&
typeof this.config.options[i].snap !== 'undefined' &&
this.config.options[i].toAbs instanceof Function
) {
this.settings.absoluteOptions[i] = snappedOption(i, this)
}
}
this.runHooks('preDraft') // Create parts container
for (const partName of this.config.draftOrder) { this.parts[set] = {}
// Create parts
this.store.log.debug(`Creating part \`${partName}\``) // Handle snap for pct options
this.parts[partName] = this.__createPartWithContext(partName) this.__loadAbsoluteOptionsSet(set)
// Handle inject/inheritance
if (typeof this.__inject[partName] === 'string') { this.runHooks('preDraft')
this.store.log.debug(`Creating part \`${partName}\` from part \`${this.__inject[partName]}\``) for (const partName of this.config.draftOrder) {
try { // Create parts
this.parts[partName].inject(this.parts[this.__inject[partName]]) this.stores[set].log.debug(`📦 Creating part \`${partName}\` (set ${set})`)
} catch (err) { this.parts[set][partName] = this.__createPartWithContext(partName, set)
this.store.log.error([ // Handle inject/inheritance
`Could not inject part \`${this.inject[partName]}\` into part \`${partName}\``, if (typeof this.__inject[partName] === 'string') {
err, this.stores[set].log.debug(
]) `Creating part \`${partName}\` from part \`${this.__inject[partName]}\``
} )
}
if (this.needs(partName)) {
// Draft part
if (typeof this.__parts?.[partName]?.draft === 'function') {
try { try {
const result = this.__parts[partName].draft(this.parts[partName].shorthand()) this.parts[set][partName].inject(this.parts[set][this.__inject[partName]])
if (typeof result === 'undefined') {
this.store.log.error(
`Result of drafting part ${partName} was undefined. Did you forget to return the part?`
)
} else this.parts[partName] = result
} catch (err) { } catch (err) {
this.store.log.error([`Unable to draft part \`${partName}\``, err]) this.stores[set].log.error([
`Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``,
err,
])
} }
} else this.store.log.error(`Unable to draft pattern. Part.draft() is not callable`)
try {
this.parts[partName].render =
this.parts[partName].render === false ? false : this.wants(partName)
} catch (err) {
this.store.log.error([`Unable to set \`render\` property on part \`${partName}\``, err])
} }
} else { if (this.needs(partName, set)) {
this.store.log.debug( // Draft part
`Part \`${partName}\` is not needed. Skipping draft and setting render to \`false\`` if (typeof this.__parts?.[partName]?.draft === 'function') {
) try {
this.parts[partName].render = false const result = this.__parts[partName].draft(this.parts[set][partName].shorthand())
if (typeof result === 'undefined') {
this.stores[set].log.error(
`Result of drafting part ${partName} was undefined. Did you forget to return the part?`
)
} else this.parts[set][partName] = result
} catch (err) {
this.stores[set].log.error([`Unable to draft part \`${partName}\` (set ${set})`, err])
}
} else this.stores[set].log.error(`Unable to draft pattern. Part.draft() is not callable`)
try {
this.parts[set][partName].render =
this.parts[set][partName].render === false ? false : this.wants(partName, set)
} catch (err) {
this.stores[set].log.error([
`Unable to set \`render\` property on part \`${partName}\``,
err,
])
}
} else {
this.stores[set].log.debug(
`Part \`${partName}\` is not needed. Skipping draft and setting render to \`false\``
)
this.parts[set][partName].render = false
}
} }
this.runHooks('postDraft')
} }
this.runHooks('postDraft')
return this return this
} }
@ -284,28 +316,26 @@ Pattern.prototype.draft = function () {
* Handles pattern sampling * Handles pattern sampling
*/ */
Pattern.prototype.sample = function () { Pattern.prototype.sample = function () {
// Late-stage initialization if (this.settings[0].sample.type === 'option') {
this.init() return this.sampleOption(this.settings[0].sample.option)
if (this.settings.sample.type === 'option') { } else if (this.settings[0].sample.type === 'measurement') {
return this.sampleOption(this.settings.sample.option) return this.sampleMeasurement(this.settings[0].sample.measurement)
} else if (this.settings.sample.type === 'measurement') {
return this.sampleMeasurement(this.settings.sample.measurement)
} else if (this.settings.sample.type === 'models') { } else if (this.settings.sample.type === 'models') {
return this.sampleModels(this.settings.sample.models, this.settings.sample.focus || false) return this.sampleModels(this.settings[0].sample.models, this.settings[0].sample.focus || false)
} }
} }
Pattern.prototype.sampleParts = function () { //Pattern.prototype.sampleParts = function () {
let parts = {} // let parts = {}
this.settings.complete = false // this.settings.complete = false
this.settings.paperless = false // this.settings.paperless = false
this.draft() // this.draft()
for (let i in this.parts) { // for (let i in this.parts) {
parts[i] = new this.Part() // parts[i] = new this.Part()
parts[i].render = this.parts[i].render // parts[i].render = this.parts[i].render
} // }
return parts // return parts
} //}
Pattern.prototype.sampleRun = function (parts, anchors, run, runs, extraClass = false) { Pattern.prototype.sampleRun = function (parts, anchors, run, runs, extraClass = false) {
this.draft() this.draft()
@ -345,109 +375,175 @@ Pattern.prototype.sampleRun = function (parts, anchors, run, runs, extraClass =
} }
} }
/** Pattern.prototype.__setBase = function () {
* Handles option sampling return {
*/ ...this.settings[0],
Pattern.prototype.sampleOption = function (optionName) { measurements: { ...(this.settings[0].measurements || {}) },
this.is = 'sample' options: { ...(this.settings[0].options || {}) },
this.store.log.debug(`Sampling option \`${optionName}\``)
this.runHooks('preSample')
let step, val
let factor = 1
let anchors = {}
let parts = this.sampleParts()
let option = this.config.options[optionName]
if (typeof option.list === 'object') {
return this.sampleListOption(optionName)
} }
}
Pattern.prototype.__listOptionSets = function (optionName) {
let option = this.config.options[optionName]
const base = this.__setBase()
const sets = []
let run = 1
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++
}
return sets
}
Pattern.prototype.__optionSets = function (optionName) {
let option = this.config.options[optionName]
if (typeof option.list === 'object') return this.__listOptionSets(optionName)
const sets = []
let factor = 1
let step, val
if (typeof option.min === 'undefined' || typeof option.max === 'undefined') { if (typeof option.min === 'undefined' || typeof option.max === 'undefined') {
let min = option * 0.9 const min = option * 0.9
let max = option * 1.1 const max = option * 1.1
option = { min, max } option = { min, max }
} }
if (typeof option.pct !== 'undefined') factor = 100 if (typeof option.pct !== 'undefined') factor = 100
val = option.min / factor val = option.min / factor
step = (option.max / factor - val) / 9 step = (option.max / factor - val) / 9
const base = this.__setBase()
for (let run = 1; run < 11; run++) { for (let run = 1; run < 11; run++) {
this.settings.options[optionName] = val const settings = {
this.sampleRun(parts, anchors, run, 10) ...base,
options: {
...base.options,
},
idPrefix: `sample-${run}`,
partClasses: `sample-${run}`,
}
settings.options[optionName] = val
sets.push(settings)
val += step val += step
} }
this.parts = parts
this.runHooks('postSample')
return this return sets
} }
Pattern.prototype.sampleListOption = function (optionName) { /**
let parts = this.sampleParts() * Handles option sampling
let option = this.config.options[optionName] */
let anchors = {} Pattern.prototype.sampleOption = function (optionName) {
let run = 1 this.stores[0].log.debug(`Sampling option \`${optionName}\``)
let runs = option.list.length this.runHooks('preSample')
for (let val of option.list) { this.__applySettings(this.__optionSets(optionName))
this.settings.options[optionName] = val this.init()
this.sampleRun(parts, anchors, run, runs) this.runHooks('postSample')
run++
}
this.parts = parts
return this return this.draft()
}
//Pattern.prototype.sampleListOption = function (optionName) {
// let parts = this.sampleParts()
// let option = this.config.options[optionName]
// let anchors = {}
// let run = 1
// let runs = option.list.length
// for (let val of option.list) {
// this.settings.options[optionName] = val
// this.sampleRun(parts, anchors, run, runs)
// run++
// }
// this.parts = parts
//
// return this
//}
Pattern.prototype.__measurementSets = function (measurementName) {
let val = this.settings[0].measurements[measurementName]
if (val === undefined)
this.stores.log.error(
`Cannot sample measurement \`${measurementName}\` because it's \`undefined\``
)
let step = val / 50
val = val * 0.9
const sets = []
const base = this.__setBase()
for (let run = 1; run < 11; run++) {
const settings = {
...base,
measurements: {
...base.measurements,
},
idPrefix: `sample-${run}`,
partClasses: `sample-${run}`,
}
settings.measurements[measurementName] = val
sets.push(settings)
val += step
}
return sets
} }
/** /**
* Handles measurement sampling * Handles measurement sampling
*/ */
Pattern.prototype.sampleMeasurement = function (measurementName) { Pattern.prototype.sampleMeasurement = function (measurementName) {
this.is = 'sample'
this.store.log.debug(`Sampling measurement \`${measurementName}\``) this.store.log.debug(`Sampling measurement \`${measurementName}\``)
this.runHooks('preSample') this.runHooks('preSample')
let anchors = {} this.__applySettings(this.__measurementSets(measurementName))
let parts = this.sampleParts() this.init()
let val = this.settings.measurements[measurementName]
if (val === undefined)
this.store.log.error(
`Cannot sample measurement \`${measurementName}\` because it's \`undefined\``
)
let step = val / 50
val = val * 0.9
for (let run = 1; run < 11; run++) {
this.settings.measurements[measurementName] = val
this.sampleRun(parts, anchors, run, 10)
val += step
}
this.parts = parts
this.runHooks('postSample') this.runHooks('postSample')
return this return this.draft()
}
Pattern.prototype.__modelSets = function (models, focus = false) {
const sets = []
const base = this.__setBase()
let run = 1
// If there's a focus, do it first so it's at the bottom of the SVG
if (focus) {
sets.push({
...base,
measurements: models[focus],
idPrefix: `sample-${run}`,
partClasses: `sample-${run} sample-focus`,
})
run++
delete models[focus]
}
for (const measurements of Object.values(models)) {
sets.push({
...base,
measurements,
idPrefix: `sample-${run}`,
partClasses: `sample-${run}`,
})
}
return sets
} }
/** /**
* Handles models sampling * Handles models sampling
*/ */
Pattern.prototype.sampleModels = function (models, focus = false) { Pattern.prototype.sampleModels = function (models, focus = false) {
this.is = 'sample' this.store.log.debug(`Sampling models \`${Object.keys(models).join(', ')}\``)
this.store.log.debug(`Sampling models`)
this.runHooks('preSample') this.runHooks('preSample')
let anchors = {} this.__applySettings(this.__modelSets(models, focus))
let parts = this.sampleParts() this.init()
// If there's a focus, do it first so it's at the bottom of the SVG
if (focus) {
this.settings.measurements = models[focus]
this.sampleRun(parts, anchors, -1, -1, 'sample-focus')
delete models[focus]
}
let run = -1
let runs = Object.keys(models).length
for (let l in models) {
run++
this.settings.measurements = models[l]
this.sampleRun(parts, anchors, run, runs)
}
this.parts = parts
this.runHooks('postSample') this.runHooks('postSample')
return this return this.draft()
} }
Pattern.prototype.render = function () { Pattern.prototype.render = function () {
@ -478,7 +574,7 @@ Pattern.prototype.__loadPlugin = function (plugin, data) {
if (plugin.hooks) this.__loadPluginHooks(plugin, data) if (plugin.hooks) this.__loadPluginHooks(plugin, data)
if (plugin.macros) this.__loadPluginMacros(plugin) if (plugin.macros) this.__loadPluginMacros(plugin)
if (plugin.store) this.__loadPluginStoreMethods(plugin) if (plugin.store) this.__loadPluginStoreMethods(plugin)
this.store.log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``) this.stores[0].log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``)
return this return this
} }
@ -486,7 +582,7 @@ Pattern.prototype.__loadPlugin = function (plugin, data) {
Pattern.prototype.use = function (plugin, data) { Pattern.prototype.use = function (plugin, data) {
if (this.plugins?.[plugin.name]?.condition && !plugin.condition) { if (this.plugins?.[plugin.name]?.condition && !plugin.condition) {
// Plugin was first loaded conditionally, and is now loaded explicitly // Plugin was first loaded conditionally, and is now loaded explicitly
this.store.log.info( this.stores[0].log.info(
`Plugin \`${plugin.plugin.name} was loaded conditionally earlier, but is now loaded explicitly.` `Plugin \`${plugin.plugin.name} was loaded conditionally earlier, but is now loaded explicitly.`
) )
return this.__loadPlugin(plugin, data) return this.__loadPlugin(plugin, data)
@ -497,7 +593,7 @@ Pattern.prototype.use = function (plugin, data) {
? this.__useIf(plugin, data) // Conditional plugin ? this.__useIf(plugin, data) // Conditional plugin
: this.__loadPlugin(plugin, data) // Regular plugin : this.__loadPlugin(plugin, data) // Regular plugin
this.store.log.info( this.stores[0].log.info(
`Plugin \`${ `Plugin \`${
plugin.plugin ? plugin.plugin.name : plugin.name plugin.plugin ? plugin.plugin.name : plugin.name
}\` was requested, but it's already loaded. Skipping.` }\` was requested, but it's already loaded. Skipping.`
@ -506,14 +602,18 @@ Pattern.prototype.use = function (plugin, data) {
return this return this
} }
Pattern.prototype.__useIf = function (plugin, settings) { Pattern.prototype.__useIf = function (plugin) {
if (plugin.condition(settings)) { let load = 0
this.store.log.info( for (const set of this.settings) {
if (plugin.condition(set)) load++
}
if (load > 0) {
this.stores[0].log.info(
`Condition met: Loaded plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` `Condition met: Loaded plugin \`${plugin.plugin.name}:${plugin.plugin.version}\``
) )
this.__loadPlugin(plugin.plugin, plugin.data) this.__loadPlugin(plugin.plugin, plugin.data)
} else { } else {
this.store.log.info( this.stores[0].log.info(
`Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\`` `Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\``
) )
} }
@ -542,8 +642,9 @@ Pattern.prototype.__loadPluginMacros = function (plugin) {
} }
Pattern.prototype.__loadPluginStoreMethods = function (plugin) { Pattern.prototype.__loadPluginStoreMethods = function (plugin) {
if (Array.isArray(plugin.store)) this.store = this.store.extend(...plugin.store) if (Array.isArray(plugin.store)) {
else this.store.log.warning(`Plugin store methods should be an Array`) for (const store of this.stores) store.extend(...plugin.store)
} else this.stores[0].log.warning(`Plugin store methods should be an Array`)
} }
Pattern.prototype.macro = function (key, method) { Pattern.prototype.macro = function (key, method) {
@ -552,18 +653,23 @@ Pattern.prototype.macro = function (key, method) {
/** Packs stacks in a 2D space and sets pattern size */ /** Packs stacks in a 2D space and sets pattern size */
Pattern.prototype.pack = function () { Pattern.prototype.pack = function () {
if (this.store.logs.error.length > 0) { for (const set in this.settings) {
this.store.log.warning(`One or more errors occured. Not packing pattern parts`) if (this.stores[set].logs.error.length > 0) {
return this this.stores[set].log.warning(`One or more errors occured. Not packing pattern parts`)
return this
}
} }
// First, create all stacks // First, create all stacks
this.stacks = {} this.stacks = {}
for (const [name, part] of Object.entries(this.parts)) { for (const set in this.settings) {
const stackName = for (const [name, part] of Object.entries(this.parts[set])) {
typeof part.stack === 'function' ? part.stack(this.settings, name) : part.stack const stackName =
if (typeof this.stacks[stackName] === 'undefined') this.settings[set].stackPrefix +
this.stacks[stackName] = this.__createStackWithContext(stackName) (typeof part.stack === 'function' ? part.stack(this.settings, name) : part.stack)
this.stacks[stackName].addPart(part) if (typeof this.stacks[stackName] === 'undefined')
this.stacks[stackName] = this.__createStackWithContext(stackName, set)
this.stacks[stackName].addPart(part)
}
} }
let bins = [] let bins = []
@ -572,7 +678,7 @@ Pattern.prototype.pack = function () {
stack.attributes.remove('transform') stack.attributes.remove('transform')
if (!this.isStackHidden(key)) { if (!this.isStackHidden(key)) {
stack.home() stack.home()
if (this.settings.layout === true) if (this.settings[0].layout === true)
bins.push({ id: key, width: stack.width, height: stack.height }) bins.push({ id: key, width: stack.width, height: stack.height })
else { else {
if (this.width < stack.width) this.width = stack.width if (this.width < stack.width) this.width = stack.width
@ -580,7 +686,7 @@ Pattern.prototype.pack = function () {
} }
} }
} }
if (this.settings.layout === true) { if (this.settings[0].layout === true) {
let size = pack(bins, { inPlace: true }) let size = pack(bins, { inPlace: true })
for (let bin of bins) { for (let bin of bins) {
this.autoLayout.stacks[bin.id] = { move: {} } this.autoLayout.stacks[bin.id] = { move: {} }
@ -595,10 +701,10 @@ Pattern.prototype.pack = function () {
} }
this.width = size.width this.width = size.width
this.height = size.height this.height = size.height
} else if (typeof this.settings.layout === 'object') { } else if (typeof this.settings[0].layout === 'object') {
this.width = this.settings.layout.width this.width = this.settings[0].layout.width
this.height = this.settings.layout.height this.height = this.settings[0].layout.height
for (let stackId of Object.keys(this.settings.layout.stacks)) { for (let stackId of Object.keys(this.settings[0].layout.stacks)) {
// Some parts are added by late-stage plugins // Some parts are added by late-stage plugins
if (this.stacks[stackId]) { if (this.stacks[stackId]) {
let transforms = this.settings.layout.stacks[stackId] let transforms = this.settings.layout.stacks[stackId]
@ -657,7 +763,7 @@ Pattern.prototype.resolveDependency = function (seen, part, graph = this.depende
Pattern.prototype.__addDependency = function (name, part, dep) { Pattern.prototype.__addDependency = function (name, part, dep) {
this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name]) this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name])
if (typeof this.__parts[dep.name] === 'undefined') { if (typeof this.__parts[dep.name] === 'undefined') {
this.config = addPartConfig(this.__parts[dep.name], this.config, this.store) this.config = addPartConfig(this.__parts[dep.name], this.config, this.stores[0])
} }
return this return this
@ -673,12 +779,17 @@ Pattern.prototype.__filterOptionalMeasurements = function () {
} }
/** Pre-Resolves part dependencies that are passed in 2022 style */ /** Pre-Resolves part dependencies that are passed in 2022 style */
Pattern.prototype.__resolveParts = function (count = 0) { Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
if (count === 0) { if (count === 0) {
for (const part of this.config.parts) { for (const part of this.config.parts) {
part.distance = distance
this.__parts[part.name] = part this.__parts[part.name] = part
} }
} }
distance++
for (const part of this.config.parts) {
if (typeof part.distance === 'undefined') part.distance = distance
}
for (const [name, part] of Object.entries(this.__parts)) { for (const [name, part] of Object.entries(this.__parts)) {
// Hide when hideAll is set // Hide when hideAll is set
if (part.hideAll) part.hide = true if (part.hideAll) part.hide = true
@ -687,6 +798,7 @@ Pattern.prototype.__resolveParts = function (count = 0) {
if (part.hideDependencies || part.hideAll) { if (part.hideDependencies || part.hideAll) {
part.from.hide = true part.from.hide = true
part.from.hideAll = true part.from.hideAll = true
part.from.distance = distance
} }
this.__parts[part.from.name] = part.from this.__parts[part.from.name] = part.from
this.__inject[name] = part.from.name this.__inject[name] = part.from.name
@ -695,11 +807,13 @@ Pattern.prototype.__resolveParts = function (count = 0) {
if (part.after) { if (part.after) {
if (Array.isArray(part.after)) { if (Array.isArray(part.after)) {
for (const dep of part.after) { for (const dep of part.after) {
dep.distance = distance
this.__parts[dep.name] = dep this.__parts[dep.name] = dep
this.__addDependency(name, part, dep) this.__addDependency(name, part, dep)
} }
} else { } else {
if (part.hideDependencies) part.after.hide = true if (part.hideDependencies) part.after.hide = true
part.after.distance = distance
this.__parts[part.after.name] = part.after this.__parts[part.after.name] = part.after
this.__addDependency(name, part, part.after) this.__addDependency(name, part, part.after)
} }
@ -708,10 +822,10 @@ Pattern.prototype.__resolveParts = function (count = 0) {
// Did we discover any new dependencies? // Did we discover any new dependencies?
const len = Object.keys(this.__parts).length const len = Object.keys(this.__parts).length
// If so, resolve recursively // If so, resolve recursively
if (len > count) return this.__resolveParts(len) if (len > count) return this.__resolveParts(len, distance)
for (const part of Object.values(this.__parts)) { for (const part of Object.values(this.__parts)) {
this.config = addPartConfig(part, this.config, this.store) this.config = addPartConfig(part, this.config, this.stores[0])
} }
return this return this
@ -730,7 +844,7 @@ Pattern.prototype.__resolveDependencies = function (graph = false) {
if (this.__dependencies[i].indexOf(dependency) === -1) if (this.__dependencies[i].indexOf(dependency) === -1)
this.__dependencies[i].push(dependency) this.__dependencies[i].push(dependency)
} else { } else {
this.store.log.error('Part dependencies should be a string or an array of strings') this.stores[0].log.error('Part dependencies should be a string or an array of strings')
throw new Error('Part dependencies should be a string or an array of strings') throw new Error('Part dependencies should be a string or an array of strings')
} }
} }
@ -751,17 +865,20 @@ Pattern.prototype.__resolveDependencies = function (graph = false) {
* This depends on the 'only' setting and the * This depends on the 'only' setting and the
* configured dependencies. * configured dependencies.
*/ */
Pattern.prototype.needs = function (partName) { Pattern.prototype.needs = function (partName, set = 0) {
// If only is unset, all parts are needed // If only is unset, all parts are needed
if ( if (
typeof this.settings.only === 'undefined' || typeof this.settings[set].only === 'undefined' ||
this.settings.only === false || this.settings[set].only === false ||
(Array.isArray(this.settings.only) && this.settings.only.length === 0) (Array.isArray(this.settings[set].only) && this.settings[set].only.length === 0)
) )
return true return true
// Make only to always be an array // Make only to always be an array
const only = typeof this.settings.only === 'string' ? [this.settings.only] : this.settings.only const only =
typeof this.settings[set].only === 'string'
? [this.settings[set].only]
: this.settings[set].only
// Walk the only parts, checking each one for a match in its dependencies // Walk the only parts, checking each one for a match in its dependencies
for (const part of only) { for (const part of only) {
@ -779,12 +896,12 @@ Pattern.prototype.needs = function (partName) {
/** Determines whether a part is wanted by the user /** Determines whether a part is wanted by the user
* This depends on the 'only' setting * This depends on the 'only' setting
*/ */
Pattern.prototype.wants = function (partName) { Pattern.prototype.wants = function (partName, set = 0) {
// Hidden parts are not wanted // Hidden parts are not wanted
if (this.isHidden(partName)) return false if (this.isHidden(partName)) return false
else if (typeof this.settings.only === 'string') return this.settings.only === partName else if (typeof this.settings[set].only === 'string') return this.settings[set].only === partName
else if (Array.isArray(this.settings.only)) { else if (Array.isArray(this.settings[set].only)) {
for (const part of this.settings.only) { for (const part of this.settings[set].only) {
if (part === partName) return true if (part === partName) return true
} }
return false return false
@ -838,12 +955,12 @@ Pattern.prototype.getRenderProps = function () {
props.height = this.height props.height = this.height
props.autoLayout = this.autoLayout props.autoLayout = this.autoLayout
props.settings = this.settings props.settings = this.settings
props.logs = { props.logs = this.stores.map((store) => ({
debug: this.store.logs.debug, debug: store.logs.debug,
info: this.store.logs.info, info: store.logs.info,
error: this.store.logs.error, error: store.logs.error,
warning: this.store.logs.warning, warning: store.logs.warning,
} }))
props.parts = {} props.parts = {}
for (let p in this.parts) { for (let p in this.parts) {
if (this.parts[p].render) { if (this.parts[p].render) {
@ -856,6 +973,7 @@ Pattern.prototype.getRenderProps = function () {
width: this.parts[p].width, width: this.parts[p].width,
bottomRight: this.parts[p].bottomRight, bottomRight: this.parts[p].bottomRight,
topLeft: this.parts[p].topLeft, topLeft: this.parts[p].topLeft,
store: this.stores[this.parts[p].set],
} }
} }
} }
@ -870,8 +988,14 @@ Pattern.prototype.getRenderProps = function () {
} }
// Merges settings object with default settings // Merges settings object with default settings
Pattern.prototype.__applySettings = function (settings) { Pattern.prototype.__applySettings = function (sets) {
this.settings = { ...loadPatternDefaults(), ...settings } 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 = []
for (const set in sets) {
this.settings.push({ ...loadPatternDefaults(), ...sets[set] })
if (set > 0) this.stores.push(new Store())
}
return this return this
} }

View file

@ -76,8 +76,11 @@ Stack.prototype.home = function () {
if (this.bottomRight.y === -Infinity) this.bottomRight.y = 0 if (this.bottomRight.y === -Infinity) this.bottomRight.y = 0
// Add margin // Add margin
let margin = this.context.settings.margin let margin = 0
if (this.context.settings.paperless && margin < 10) margin = 10 for (const set in this.context.settings) {
if (this.context.settings[set].margin > margin) margin = this.context.settings[set].margin
if (this.context.settings[set].paperless && margin < 10) margin = 10
}
this.topLeft.x -= margin this.topLeft.x -= margin
this.topLeft.y -= margin this.topLeft.y -= margin
this.bottomRight.x += margin this.bottomRight.x += margin
@ -90,7 +93,9 @@ Stack.prototype.home = function () {
this.height = this.bottomRight.y - this.topLeft.y this.height = this.bottomRight.y - this.topLeft.y
// Add transform // Add transform
this.anchor = this.getAnchor() //this.anchor = this.getAnchor()
// FIXME: Can we be certain this is always (0,0) /
this.anchor = new Point(0, 0)
if (this.topLeft.x === this.anchor.x && this.topLeft.y === this.anchor.y) return this if (this.topLeft.x === this.anchor.x && this.topLeft.y === this.anchor.y) return this
else { else {

View file

@ -15,7 +15,7 @@ export function isCoord(value) {
/** Returns internal hook name for a macro */ /** Returns internal hook name for a macro */
export function macroName(name) { export function macroName(name) {
return `_macro_${name}` return `__macro_${name}`
} }
/** Find intersection of two (endless) lines */ /** Find intersection of two (endless) lines */
@ -441,8 +441,14 @@ export function addNonEnumProp(obj, name, value) {
const addPartOptions = (part, config, store) => { const addPartOptions = (part, config, store) => {
if (part.options) { if (part.options) {
for (const optionName in part.options) { for (const optionName in part.options) {
store.log.debug(`Config resolver: Option __${optionName}__ in ${part.name}`) if (!config.optionDistance[optionName]) {
config.options[optionName] = part.options[optionName] config.optionDistance[optionName] = part.distance
config.options[optionName] = part.options[optionName]
store.log.debug(`🔵 __${optionName}__ option loaded from \`${part.name}\``)
} else if (config.optionDistance[optionName] > part.distance) {
config.options[optionName] = part.options[optionName]
store.log.debug(`🟣 __${optionName}__ option overwritten by \`${part.name}\``)
}
} }
} }
if (part.from) addPartOptions(part.from, config, store) if (part.from) addPartOptions(part.from, config, store)
@ -460,8 +466,10 @@ const addPartMeasurements = (part, config, store, list = false) => {
if (!list) list = config.measurements ? [...config.measurements] : [] if (!list) list = config.measurements ? [...config.measurements] : []
if (part.measurements) { if (part.measurements) {
for (const m of part.measurements) { for (const m of part.measurements) {
list.push(m) if (list.indexOf(m) === -1) {
store.log.debug(`Config resolver: Measurement __${m}__ is required in ${part.name}`) list.push(m)
store.log.debug(`🟠 __${m}__ measurement is required in \`${part.name}\``)
}
} }
} }
if (part.from) addPartMeasurements(part.from, config, store, list) if (part.from) addPartMeasurements(part.from, config, store, list)
@ -484,8 +492,10 @@ const addPartOptionalMeasurements = (part, config, store, list = false) => {
for (const m of part.optionalMeasurements) { for (const m of part.optionalMeasurements) {
// Don't add it's a required measurement for another part // Don't add it's a required measurement for another part
if (config.measurements.indexOf(m) === -1) { if (config.measurements.indexOf(m) === -1) {
store.log.debug(`Config resolver: Measurement __${m}__ is optional in ${part.name}`) if (list.indexOf(m) === -1) {
list.push(m) list.push(m)
store.log.debug(`🟡 __${m}__ measurement is optional in \`${part.name}\``)
}
} }
} }
} }
@ -514,7 +524,7 @@ export const addPartPlugins = (part, config, store) => {
const pluginObj = { ...plugin[0], data: plugin[1] } const pluginObj = { ...plugin[0], data: plugin[1] }
plugin = pluginObj plugin = pluginObj
} }
store.log.debug(`Config resolver: Plugin __${plugin.name}__ in ${part.name}`) store.log.debug(`🔌 __${plugin.name}__ plugin in \`${part.name}\``)
// Do not overwrite an existing plugin with a conditional plugin unless it is also conditional // Do not overwrite an existing plugin with a conditional plugin unless it is also conditional
if (plugin.plugin && plugin.condition) { if (plugin.plugin && plugin.condition) {
if (plugins[plugin.plugin.name]?.condition) { if (plugins[plugin.plugin.name]?.condition) {
@ -538,7 +548,9 @@ export const addPartPlugins = (part, config, store) => {
} }
export const addPartConfig = (part, config, store) => { export const addPartConfig = (part, config, store) => {
if (part.resolved) return config
// Add parts, using set to keep them unique in the array // Add parts, using set to keep them unique in the array
part.resolved = true
config.parts = [...new Set(config.parts).add(part)] config.parts = [...new Set(config.parts).add(part)]
config = addPartOptions(part, config, store) config = addPartOptions(part, config, store)
config = addPartMeasurements(part, config, store) config = addPartMeasurements(part, config, store)