1
0
Fork 0
freesewing/packages/core/src/pattern/index.mjs

453 lines
14 KiB
JavaScript
Raw Normal View History

import { Attributes } from '../attributes.mjs'
import { __addNonEnumProp, __macroName } 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'
import { PatternConfig } from './pattern-config.mjs'
import { PatternDrafter } from './pattern-drafter.mjs'
import { PatternSampler } from './pattern-sampler.mjs'
import { PatternPlugins, getPluginName } from './pattern-plugins.mjs'
import { PatternRenderer } from './pattern-renderer.mjs'
import cloneDeep from 'lodash.clonedeep'
2018-07-23 11:12:06 +00: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
*/
export function Pattern(designConfig = {}) {
2023-02-22 16:09:43 +02:00
// Enumerable properties
this.designConfig = cloneDeep(designConfig) // The design configuration (unresolved)
this.config = {} // Will hold the resolved pattern after calling __init()
this.store = new Store() // Pattern-wide store
this.setStores = [] // Per-set stores
// Non-enumerable properties
2022-09-18 15:11:10 +02:00
__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, '__initialized', false)
__addNonEnumProp(this, 'config.parts', {})
__addNonEnumProp(this, 'config.resolvedDependencies', {})
__addNonEnumProp(this, 'plugins', new PatternPlugins(this))
__addNonEnumProp(this, '__configResolver', new PatternConfig(this)) // handles config resolution during __init() as well as runtime part adding
return this
}
2022-09-18 15:11:10 +02:00
//////////////////////////////////////////////
// PUBLIC METHODS //
//////////////////////////////////////////////
2022-09-18 15:11:10 +02:00
/**
2023-02-22 16:09:43 +02:00
* Allows adding parts to the config at runtime
2022-09-18 15:11:10 +02:00
*
* @param {object} part - The part to add
2023-02-22 16:09:43 +02:00
* @param {boolean} resolveImmediately - Should the part be resolved now, or wait until the next call to {@link __init()}?
* It is useful to resolve immediately if one part is being added at runtime
* It might be useful to not resolve immediately if a number of parts will be added over multiple calls
2022-09-18 15:11:10 +02:00
* @return {object} this - The Pattern instance
*/
2023-02-23 19:15:20 +02:00
Pattern.prototype.addPart = function (part, resolveImmediately = true) {
if (
this.__configResolver.isPartValid(part) &&
!this.designConfig.parts.find((p) => p.name == part.name)
) {
2023-02-27 18:58:47 -06:00
this.store.log.debug(`Adding Part \`${part.name}\` at runtime`)
this.designConfig.parts.push(part)
2023-02-23 18:22:22 +02:00
if (resolveImmediately) {
if (this.__configResolver.addPart(part) && typeof this.draftQueue !== 'undefined')
this.draftQueue.addPart(part.name)
} else this.__initialized = false
}
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
*/
Pattern.prototype.draft = function () {
this.__init()
new PatternDrafter(this).draft()
2022-09-18 15:11:10 +02:00
return this
}
Pattern.prototype.draftPartForSet = function (partName, set) {
this.__init()
return new PatternDrafter(this).draftPartForSet(partName, set)
}
2022-09-18 17:01:19 +02:00
/**
* Return the initialized configuration
*
* @return {object} config - The initialized config
*/
Pattern.prototype.getConfig = function () {
return this.__init().config
2022-09-18 17:01:19 +02:00
}
/**
* Renders the pattern to SVG
*
* @return {string} svg - The rendered SVG
*/
Pattern.prototype.render = function () {
return new PatternRenderer(this).render()
}
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 () {
return new PatternRenderer(this).getRenderProps()
2022-09-18 15:11:10 +02:00
}
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
*/
Pattern.prototype.sample = function () {
return new PatternSampler(this).sample()
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) {
return new PatternSampler(this).sampleMeasurement(measurementName)
}
2022-09-18 15:11:10 +02:00
/**
* Handles models sampling
*
* @return {object} this - The Pattern instance
*/
Pattern.prototype.sampleModels = function (models, focus = false) {
return new PatternSampler(this).sampleModels(models, focus)
2022-09-18 15:11:10 +02:00
}
/**
* Handles option sampling
*
* @return {object} this - The Pattern instance
*/
Pattern.prototype.sampleOption = function (optionName) {
return new PatternSampler(this).sampleOption(optionName)
2022-09-18 15:11:10 +02:00
}
/**
* 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) {
this.plugins.on(hook, method, data)
2022-09-18 15:11:10 +02:00
return this
}
/**
* 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) {
this.plugins.use(plugin, data, this.settings)
2022-09-18 15:11:10 +02:00
return this
}
//////////////////////////////////////////////
// PRIVATE METHODS //
//////////////////////////////////////////////
/**
* 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)
store.extend([...this.plugins.__storeMethods])
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 = []
2023-04-15 15:31:25 +02:00
for (let i = 0; i < sets.length; i++) {
2022-09-28 22:51:55 +02:00
// Don't mutate the input itself
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
}
/**
* Initializes the pattern coniguration and settings
*
* @return {object} this - The Pattern instance
*/
Pattern.prototype.__init = function () {
if (this.__initialized) return this
this.__runHooks('preInit')
// Say hello
this.store.log.info(
`New \`${this.designConfig?.data?.name || 'No Name'}:` +
`${this.designConfig?.data?.version || 'No version'}\` ` +
`pattern using \`@freesewing/core:${version}\``
)
/*
* 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
.__resolveConfig() // Gets the config from the resolver
.__loadConfigData() // Makes config data available in store
.__loadOptionDefaults() // Merges default options with user provided ones
this.plugins.loadConfigPlugins(this.config, this.settings) // Loads plugins
this.store.log.info(`Pattern initialized. Draft order is: ${this.config.draftOrder.join(', ')}`)
this.__runHooks('postInit')
this.__initialized = true
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) {
const partHidden = this.parts?.[this.activeSet]?.[partName]?.hidden || false
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 partHidden
2022-09-18 15:11:10 +02:00
}
if (this.config.partHide?.[partName]) return true
2022-09-18 15:11:10 +02:00
return partHidden
2022-09-18 15:11:10 +02:00
}
/**
* 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()
for (const partName of parts) {
if (!this.__isPartHidden(partName)) return false
2022-09-18 15:11:10 +02:00
}
return true
2022-09-18 15:11:10 +02:00
}
/**
* Loads data from the design config into the store
*
* @private
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__loadConfigData = function () {
if (this.designConfig.data) this.store.set('data', this.designConfig.data)
2022-09-18 15:11:10 +02:00
return this
}
2023-02-22 16:09:43 +02:00
/**
* Merges defaults for options with user-provided options
*
* @private
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__loadOptionDefaults = function () {
if (!this.config.options) this.config.options = {}
if (Object.keys(this.config.options).length < 1) return this
for (const i in this.settings) {
for (const [name, option] of Object.entries(this.config.options)) {
// Don't overwrite user-provided settings.options
if (typeof this.settings[i].options[name] === 'undefined') {
if (typeof option === 'object') {
if (typeof option.pct !== 'undefined') this.settings[i].options[name] = option.pct / 100
else if (typeof option.mm !== 'undefined') this.settings[i].options[name] = option.mm
else if (typeof option.deg !== 'undefined') this.settings[i].options[name] = option.deg
else if (typeof option.count !== 'undefined')
this.settings[i].options[name] = option.count
else if (typeof option.bool !== 'undefined') this.settings[i].options[name] = option.bool
else if (typeof option.dflt !== 'undefined') this.settings[i].options[name] = option.dflt
else {
let err = 'Unknown option type: ' + JSON.stringify(option)
this.store.log.error(err)
throw new Error(err)
}
} else this.settings[i].options[name] = option
}
}
}
return this
}
/**
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-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
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
2023-02-23 08:13:44 +02:00
if (this.config.resolvedDependencies[part]?.indexOf(partName) !== -1) return true
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
2023-02-28 12:23:15 -06:00
/**
* Gets the configuration for the config resolver and sets it on the pattern
* @private
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveConfig = function () {
this.config = this.__configResolver.asConfig()
return this
2019-08-03 15:03:33 +02:00
}
2022-09-18 15:11:10 +02:00
/**
* Resolves parts and their dependencies
*
* @private
* @return {Pattern} this - The Pattern instance
*/
Pattern.prototype.__resolveParts = function () {
this.designConfig.parts.forEach((p) => this.__configResolver.addPart(p))
// Print final part distances.
this.__configResolver.logPartDistances()
return this
}
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
*/
2022-09-18 15:11:10 +02:00
Pattern.prototype.__runHooks = function (hookName, data = false) {
if (data === false) data = this
let hooks = this.plugins.hooks[hookName]
2022-09-18 15:11:10 +02:00
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)
}
}
2019-08-03 15:03:33 +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-18 15:11:10 +02:00
return false
}
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
}