1
0
Fork 0
freesewing/packages/core/src/pattern.mjs
Joost De Cock 0b18d81e14 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.
2022-09-17 10:24:13 +02:00

1001 lines
30 KiB
JavaScript

import { Attributes } from './attributes.mjs'
import pack from 'bin-pack'
import {
addNonEnumProp,
macroName,
sampleStyle,
addPartConfig,
mergeDependencies,
} from './utils.mjs'
import { Part } from './part.mjs'
import { Stack } from './stack.mjs'
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'
import { version } from '../data.mjs'
import { loadPatternDefaults } from './config.mjs'
export function Pattern(config) {
// Non-enumerable properties
addNonEnumProp(this, 'plugins', {})
addNonEnumProp(this, 'parts', [{}])
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', {})
addNonEnumProp(this, '__parts', {})
addNonEnumProp(this, '__inject', {})
addNonEnumProp(this, '__dependencies', {})
addNonEnumProp(this, '__resolvedDependencies', {})
addNonEnumProp(this, '__draftOrder', [])
addNonEnumProp(this, '__hide', {})
// Enumerable properties
this.config = config // Design config
this.stacks = {} // Drafted stacks container
this.stores = [new Store()]
return this
}
/*
* 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
* Defer some things that used to happen in the constructor to
* and in doing so creating a pattern we can draft
*/
Pattern.prototype.init = function () {
// Resolve configuration
this.__resolveParts() // Resolves parts
.__resolveDependencies() // Resolves dependencies
.__resolveDraftOrder() // Resolves draft order
.__loadPlugins() // Loads plugins
.__filterOptionalMeasurements() // Removes required m's from optional list
.__loadConfigData() // Makes config data available in store
.__loadOptionDefaults() // Merges default options with user provided ones
// Say hello
this.stores[0].log.info(
`New \`${this.stores[0].get('data.name', 'No Name')}:` +
`${this.stores[0].get(
'data.version',
'No version'
)}\` pattern using \`@freesewing/core:${version}\``
)
this.stores[0].log.info(`Pattern initialized. Draft order is: ${this.__draftOrder.join(', ')}`)
return this
}
Pattern.prototype.__loadConfigData = function () {
if (this.config.data) {
for (const i in this.settings) this.stores[i].set('data', this.config.data)
}
return this
}
Pattern.prototype.__createPartWithContext = function (name, set) {
// Context object to add to Part closure
const part = new Part()
part.name = name
part.set = set
part.stack = this.__parts[name]?.stack || name
part.context = {
parts: this.parts[set],
config: this.config,
settings: this.settings[set],
store: this.stores[set],
macros: this.macros,
}
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
}
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,
stores: this.stores,
}
return stack
}
// Merges default for options with user-provided options
Pattern.prototype.__loadOptionDefaults = function () {
if (Object.keys(this.config.options).length < 1) return this
for (const i in this.settings) {
for (const [name, option] of Object.entries(this.config.options)) {
// Don't overwrite user-provided settings.options
if (typeof this.settings[i].options[name] === 'undefined') {
if (typeof option === 'object') {
if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100
else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm
else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg
else if (typeof option.count !== 'undefined')
this.settings[i].options[name] = option.count
else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool
else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt
else {
let err = 'Unknown option type: ' + JSON.stringify(option)
this.stores[i].log.error(err)
throw new Error(err)
}
} else this.settings[i].options[name] = option
}
}
}
return this
}
/* Utility method to get the (initialized) config */
Pattern.prototype.getConfig = function () {
this.init()
return this.config
}
/* Utility method to get the (initialized) part list */
Pattern.prototype.getPartList = function () {
this.init()
return Object.keys(this.config.parts)
}
Pattern.prototype.__snappedPercentageOption = function (optionName, set) {
const conf = this.config.options[optionName]
const abs = conf.toAbs(this.settings[set].options[optionName], this.settings[set])
// Handle units-specific config
if (!Array.isArray(conf.snap) && conf.snap.metric && conf.snap.imperial)
conf.snap = conf.snap[this.settings[set].units]
// Simple steps
if (typeof conf.snap === 'number') return Math.ceil(abs / conf.snap) * conf.snap
// List of snaps
if (Array.isArray(conf.snap) && conf.snap.length > 1) {
for (const snap of conf.snap
.sort((a, b) => a - b)
.map((snap, i) => {
const margin =
i < conf.snap.length - 1
? (conf.snap[Number(i) + 1] - snap) / 2 // Look forward
: (snap - conf.snap[i - 1]) / 2 // Final snap, look backward
return {
min: snap - margin,
max: snap + Number(margin),
snap,
}
}))
if (abs <= snap.max && abs >= snap.min) return snap.snap
}
return abs
}
Pattern.prototype.runHooks = function (hookName, data = false) {
if (data === false) data = this
let hooks = this.hooks[hookName]
if (hooks.length > 0) {
this.stores[0].log.debug(`Running \`${hookName}\` hooks`)
for (let hook of hooks) {
hook.method(data, hook.data)
}
}
}
/*
* Allows adding a part at run-time
*/
Pattern.prototype.addPart = function (part) {
if (typeof part?.draft === 'function') {
if (part.name) {
this.config.parts[part.name] = part
// Add part-level config to config
this.config = addPartConfig(part, this.config, this.stores[0])
} else this.stores[0].log.error(`Part must have a name`)
} 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
}
/**
* The default draft method with pre- and postDraft hooks
*/
Pattern.prototype.draft = function () {
// Late-stage initialization
this.init()
// Iterate over the provided sets of settings (typically just one)
for (const set in this.settings) {
// Set store
this.stores[set].log.debug(`📐 Drafting pattern (set ${set})`)
// Create parts container
this.parts[set] = {}
// Handle snap for pct options
this.__loadAbsoluteOptionsSet(set)
this.runHooks('preDraft')
for (const partName of this.config.draftOrder) {
// Create parts
this.stores[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.stores[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.stores[set].log.error([
`Could not inject part \`${this.__inject[partName]}\` into part \`${partName}\``,
err,
])
}
}
if (this.needs(partName, set)) {
// Draft part
if (typeof this.__parts?.[partName]?.draft === 'function') {
try {
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')
}
return this
}
/**
* Handles pattern sampling
*/
Pattern.prototype.sample = function () {
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)
} else if (this.settings.sample.type === 'models') {
return this.sampleModels(this.settings[0].sample.models, this.settings[0].sample.focus || false)
}
}
//Pattern.prototype.sampleParts = function () {
// let parts = {}
// this.settings.complete = false
// this.settings.paperless = false
// this.draft()
// for (let i in this.parts) {
// parts[i] = new this.Part()
// parts[i].render = this.parts[i].render
// }
// return parts
//}
Pattern.prototype.sampleRun = function (parts, anchors, run, runs, extraClass = false) {
this.draft()
for (let i in this.parts) {
let dx = 0
let dy = 0
if (this.parts[i].points.anchor) {
if (typeof anchors[i] === 'undefined') anchors[i] = this.parts[i].points.anchor
else {
if (!anchors[i].sitsOn(this.parts[i].points.anchor)) {
dx = this.parts[i].points.anchor.dx(anchors[i])
dy = this.parts[i].points.anchor.dy(anchors[i])
}
}
}
for (let j in this.parts[i].paths) {
parts[i].paths[j + '_' + run] = this.parts[i].paths[j]
.clone()
.attr(
'style',
extraClass === 'sample-focus'
? this.settings.sample
? this.settings.sample.focusStyle || sampleStyle(run, runs)
: sampleStyle(run, runs)
: sampleStyle(
run,
runs,
this.settings.sample ? this.settings.sample.styles || false : false
)
)
.attr('data-sample-run', run)
.attr('data-sample-runs', runs)
if (this.parts[i].points.anchor)
parts[i].paths[j + '_' + run] = parts[i].paths[j + '_' + run].translate(dx, dy)
if (extraClass !== false) parts[i].paths[j + '_' + run].attributes.add('class', extraClass)
}
}
}
Pattern.prototype.__setBase = function () {
return {
...this.settings[0],
measurements: { ...(this.settings[0].measurements || {}) },
options: { ...(this.settings[0].options || {}) },
}
}
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') {
const min = option * 0.9
const max = option * 1.1
option = { min, max }
}
if (typeof option.pct !== 'undefined') factor = 100
val = option.min / factor
step = (option.max / factor - val) / 9
const base = this.__setBase()
for (let run = 1; run < 11; run++) {
const settings = {
...base,
options: {
...base.options,
},
idPrefix: `sample-${run}`,
partClasses: `sample-${run}`,
}
settings.options[optionName] = val
sets.push(settings)
val += step
}
return sets
}
/**
* Handles option sampling
*/
Pattern.prototype.sampleOption = function (optionName) {
this.stores[0].log.debug(`Sampling option \`${optionName}\``)
this.runHooks('preSample')
this.__applySettings(this.__optionSets(optionName))
this.init()
this.runHooks('postSample')
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
*/
Pattern.prototype.sampleMeasurement = function (measurementName) {
this.store.log.debug(`Sampling measurement \`${measurementName}\``)
this.runHooks('preSample')
this.__applySettings(this.__measurementSets(measurementName))
this.init()
this.runHooks('postSample')
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
*/
Pattern.prototype.sampleModels = function (models, focus = false) {
this.store.log.debug(`Sampling models \`${Object.keys(models).join(', ')}\``)
this.runHooks('preSample')
this.__applySettings(this.__modelSets(models, focus))
this.init()
this.runHooks('postSample')
return this.draft()
}
Pattern.prototype.render = function () {
this.svg = new Svg(this)
this.svg.hooks = this.hooks
return this.pack().svg.render(this)
}
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
}
Pattern.prototype.__loadPlugins = function () {
for (const plugin of this.config.plugins) this.use(plugin, plugin.data)
return this
}
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)
this.stores[0].log.info(`Loaded plugin \`${plugin.name}:${plugin.version}\``)
return this
}
Pattern.prototype.use = function (plugin, data) {
if (this.plugins?.[plugin.name]?.condition && !plugin.condition) {
// Plugin was first loaded conditionally, and is now loaded explicitly
this.stores[0].log.info(
`Plugin \`${plugin.plugin.name} was loaded conditionally earlier, but is now loaded explicitly.`
)
return this.__loadPlugin(plugin, data)
}
// New plugin
else if (!this.plugins?.[plugin.name])
return plugin.plugin && plugin.condition
? this.__useIf(plugin, data) // Conditional plugin
: this.__loadPlugin(plugin, data) // Regular plugin
this.stores[0].log.info(
`Plugin \`${
plugin.plugin ? plugin.plugin.name : plugin.name
}\` was requested, but it's already loaded. Skipping.`
)
return this
}
Pattern.prototype.__useIf = function (plugin) {
let load = 0
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}\``
)
this.__loadPlugin(plugin.plugin, plugin.data)
} else {
this.stores[0].log.info(
`Condition not met: Skipped loading plugin \`${plugin.plugin.name}:${plugin.plugin.version}\``
)
}
return this
}
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)
}
}
}
}
Pattern.prototype.__loadPluginMacros = function (plugin) {
for (let macro in plugin.macros) {
if (typeof plugin.macros[macro] === 'function') {
this.macro(macro, plugin.macros[macro])
}
}
}
Pattern.prototype.__loadPluginStoreMethods = function (plugin) {
if (Array.isArray(plugin.store)) {
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) {
this.macros[key] = method
}
/** Packs stacks in a 2D space and sets pattern size */
Pattern.prototype.pack = function () {
for (const set in this.settings) {
if (this.stores[set].logs.error.length > 0) {
this.stores[set].log.warning(`One or more errors occured. Not packing pattern parts`)
return this
}
}
// First, create all stacks
this.stacks = {}
for (const set in this.settings) {
for (const [name, part] of Object.entries(this.parts[set])) {
const stackName =
this.settings[set].stackPrefix +
(typeof part.stack === 'function' ? part.stack(this.settings, name) : part.stack)
if (typeof this.stacks[stackName] === 'undefined')
this.stacks[stackName] = this.__createStackWithContext(stackName, set)
this.stacks[stackName].addPart(part)
}
}
let bins = []
for (const [key, stack] of Object.entries(this.stacks)) {
// Avoid multiple render calls to cause addition of transforms
stack.attributes.remove('transform')
if (!this.isStackHidden(key)) {
stack.home()
if (this.settings[0].layout === true)
bins.push({ id: key, width: stack.width, height: stack.height })
else {
if (this.width < stack.width) this.width = stack.width
if (this.height < stack.height) this.height = stack.height
}
}
}
if (this.settings[0].layout === true) {
let size = pack(bins, { inPlace: true })
for (let bin of bins) {
this.autoLayout.stacks[bin.id] = { move: {} }
let stack = this.stacks[bin.id]
if (bin.x !== 0 || bin.y !== 0) {
stack.attr('transform', `translate(${bin.x}, ${bin.y})`)
}
this.autoLayout.stacks[bin.id].move = {
x: bin.x + stack.layout.move.x,
y: bin.y + stack.layout.move.y,
}
}
this.width = size.width
this.height = size.height
} 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)) {
// Some parts are added by late-stage plugins
if (this.stacks[stackId]) {
let transforms = this.settings.layout.stacks[stackId]
this.stacks[stackId].generateTransform(transforms)
}
}
}
return this
}
/** Determines the order to draft parts in, based on dependencies */
Pattern.prototype.__resolveDraftOrder = function (graph = this.__resolvedDependencies) {
let sorted = []
let visited = {}
Object.keys(graph).forEach(function visit(name, ancestors) {
if (!Array.isArray(ancestors)) ancestors = []
ancestors.push(name)
visited[name] = true
if (typeof graph[name] !== 'undefined') {
graph[name].forEach(function (dep) {
if (visited[dep]) return
visit(dep, ancestors.slice(0))
})
}
if (sorted.indexOf(name) < 0) sorted.push(name)
})
// Don't forget about parts without dependencies
for (const part in this.__parts) {
if (sorted.indexOf(part) === -1) sorted.push(part)
}
this.__draftOrder = sorted
this.config.draftOrder = sorted
return this
}
/** Recursively solves part dependencies for a part */
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
}
/** Adds a part as a simple dependency **/
Pattern.prototype.__addDependency = function (name, part, dep) {
this.__dependencies[name] = mergeDependencies(dep.name, this.__dependencies[name])
if (typeof this.__parts[dep.name] === 'undefined') {
this.config = addPartConfig(this.__parts[dep.name], this.config, this.stores[0])
}
return this
}
/** Filter optional measurements out if they are also required measurements */
Pattern.prototype.__filterOptionalMeasurements = function () {
this.config.optionalMeasurements = this.config.optionalMeasurements.filter(
(m) => this.config.measurements.indexOf(m) === -1
)
return this
}
/** Pre-Resolves part dependencies that are passed in 2022 style */
Pattern.prototype.__resolveParts = function (count = 0, distance = 0) {
if (count === 0) {
for (const part of this.config.parts) {
part.distance = distance
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)) {
// Hide when hideAll is set
if (part.hideAll) part.hide = true
// Inject (from)
if (part.from) {
if (part.hideDependencies || part.hideAll) {
part.from.hide = true
part.from.hideAll = true
part.from.distance = distance
}
this.__parts[part.from.name] = part.from
this.__inject[name] = part.from.name
}
// Simple dependency (after)
if (part.after) {
if (Array.isArray(part.after)) {
for (const dep of part.after) {
dep.distance = distance
this.__parts[dep.name] = dep
this.__addDependency(name, part, dep)
}
} else {
if (part.hideDependencies) part.after.hide = true
part.after.distance = distance
this.__parts[part.after.name] = part.after
this.__addDependency(name, part, part.after)
}
}
}
// Did we discover any new dependencies?
const len = Object.keys(this.__parts).length
// If so, resolve recursively
if (len > count) return this.__resolveParts(len, distance)
for (const part of Object.values(this.__parts)) {
this.config = addPartConfig(part, this.config, this.stores[0])
}
return this
}
/** Resolves part dependencies into a flat array */
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)
} else {
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')
}
}
}
let resolved = {}
let seen = {}
for (let part in graph) resolved[part] = this.resolveDependency(seen, part, graph)
for (let part in seen) if (typeof resolved[part] === 'undefined') resolved[part] = []
this.__resolvedDependencies = resolved
this.config.resolvedDependencies = resolved
return this
}
/** Determines whether a part is needed
* This depends on the 'only' setting and the
* configured dependencies.
*/
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)
)
return true
// Make only to always be an array
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
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
}
}
}
return false
}
/** Determines whether a part is wanted by the user
* This depends on the 'only' setting
*/
Pattern.prototype.wants = function (partName, set = 0) {
// Hidden parts are not wanted
if (this.isHidden(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
}
return false
}
return true
}
/* Checks whether a part is hidden in the config */
Pattern.prototype.isHidden = function (partName) {
if (Array.isArray(this.settings.only)) {
if (this.settings.only.includes(partName)) return false
}
if (this.__parts?.[partName]?.hide) return true
if (this.__parts?.[partName]?.hideAll) return true
return false
}
/* Checks whether (all parts in) a stack is hidden in the config */
Pattern.prototype.isStackHidden = function (stackName) {
if (!this.stacks[stackName]) return true
const parts = this.stacks[stackName].getPartNames()
if (Array.isArray(this.settings.only)) {
for (const partName of parts) {
if (this.settings.only.includes(partName)) return false
}
}
for (const partName of parts) {
if (this.__parts?.[partName]?.hide) return true
if (this.__parts?.[partName]?.hideAll) return true
}
return false
}
/** Returns props required to render this pattern through
* an external renderer (eg. a React component)
*/
Pattern.prototype.getRenderProps = function () {
// Run pre-render hook
let svg = new Svg(this)
svg.hooks = this.hooks
svg.runHooks('preRender')
this.pack()
// Run post-layout hook
this.runHooks('postLayout')
let props = { svg }
props.width = this.width
props.height = this.height
props.autoLayout = this.autoLayout
props.settings = this.settings
props.logs = this.stores.map((store) => ({
debug: store.logs.debug,
info: store.logs.info,
error: store.logs.error,
warning: store.logs.warning,
}))
props.parts = {}
for (let p in this.parts) {
if (this.parts[p].render) {
props.parts[p] = {
paths: this.parts[p].paths,
points: this.parts[p].points,
snippets: this.parts[p].snippets,
attributes: this.parts[p].attributes,
height: this.parts[p].height,
width: this.parts[p].width,
bottomRight: this.parts[p].bottomRight,
topLeft: this.parts[p].topLeft,
store: this.stores[this.parts[p].set],
}
}
}
props.stacks = {}
for (let s in this.stacks) {
if (!this.isStackHidden(s)) {
props.stacks[s] = this.stacks[s]
}
}
return props
}
// Merges settings object with default settings
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 = []
for (const set in sets) {
this.settings.push({ ...loadPatternDefaults(), ...sets[set] })
if (set > 0) this.stores.push(new Store())
}
return this
}